fix(currency): SetCurrency names its source the way live did

Live filled SetCurrency's source fields per source type (16,816 decoded
live SetCurrency): pickups carry the position the client picked the
coins up at (the one in its PickupCurrency) and no source; mission,
achievement and activity rewards name the player (object and LOT 1);
vendor buys, sells and buybacks name the vendor and its LOT; trades
carry the trade ID; death, mail and everything else name nothing. No
live SetCurrency sets loot_type. DLU wrote a present source LOT of 0,
no object and a zero position for all of them.

The position is the one the client uses: for pickups it projects it to
the screen as the start of the coin counter animation
(LWOCharacterComponent::SetCurrency), so it started from the world
origin before. PickupCurrency now reads that position (the client
always sends it). The trade ID is an 8-byte object ID in the client;
DLU declared it 4 bytes, which only never broke because it was never
set.

Check: pick up coins and watch the coin counter animate from where the
coins were; buy and sell at a vendor, finish a mission and trade coins
with another player, and the coin total updates each time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 02:55:26 -05:00
parent f7bb178c2d
commit 81db3a94ac
11 changed files with 148 additions and 28 deletions

View File

@@ -408,9 +408,9 @@ TEST_F(RemainingMessagesTests, PlayerMessagesMatchLegacy) {
currency.lootType = smallValue;
currency.sourceID = smallValue;
currency.sourceLOT = smallValue;
currency.sourceTradeID = smallValue;
currency.sourceType = source;
ExpectSameSends([&] { LegacyGameMessages::SendSetCurrency(&entity, value, smallValue, smallValue, smallValue, smallValue, true, source); }, [&] { currency.SendToClient(entity.GetSystemAddress()); });
// The legacy sender wrote a set trade ID as 4 bytes; the client reads 8 (see SetCurrencyTradeIdIsAnObjectId).
ExpectSameSends([&] { LegacyGameMessages::SendSetCurrency(&entity, value, smallValue, smallValue, smallValue, 0, true, source); }, [&] { currency.SendToClient(entity.GetSystemAddress()); });
RoundTrip(currency);
}
}
@@ -427,7 +427,16 @@ TEST_F(RemainingMessagesTests, InboundPlayerMessagesMatchLegacy) {
for (const uint32_t amount : { 0u, 1u, 0xffffffffu }) {
GameMessages::PickupCurrency pickup;
pickup.currency = amount;
EXPECT_EQ(ReadWithLegacy<unsigned int>(pickup, LegacyGameMessages::ReadPickupCurrency), amount);
pickup.position = NiPoint3(1.5f, -2.0f, 300.25f);
// The old reader stopped after the amount; the client also sends where it picked the coins up.
RakNet::BitStream wire;
pickup.Serialize(wire);
RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false);
EXPECT_EQ(LegacyGameMessages::ReadPickupCurrency(legacyStream), amount);
const auto copy = RoundTrip(pickup);
EXPECT_EQ(copy.currency, amount);
EXPECT_EQ(copy.position, pickup.position);
EXPECT_EQ(wire.GetNumberOfBitsUsed(), 16u * 8u);
ExpectTruncatedFails(pickup);
}
for (const auto& name : g_WStrings) {