diff --git a/dGame/Character.cpp b/dGame/Character.cpp index d61c6c7ed..1f4a7bbb6 100644 --- a/dGame/Character.cpp +++ b/dGame/Character.cpp @@ -590,7 +590,38 @@ const NiPoint3& Character::GetRespawnPoint(LWOMAPID map) const { return pair->second; } -void Character::SetCoins(int64_t newCoins, eLootSourceType lootSource) { +CoinSource CoinSource::Object(const Entity& source) { + CoinSource coinSource; + coinSource.object = source.GetObjectID(); + coinSource.lot = source.GetLOT(); + return coinSource; +} + +CoinSource CoinSource::Position(const NiPoint3& position) { + CoinSource coinSource; + coinSource.position = position; + return coinSource; +} + +CoinSource CoinSource::Trade(const LWOOBJID tradeID) { + CoinSource coinSource; + coinSource.tradeID = tradeID; + return coinSource; +} + +GameMessages::SetCurrency Character::MakeSetCurrency(const LWOOBJID player, const int64_t coins, const eLootSourceType coinSource, const CoinSource& source) { + GameMessages::SetCurrency setCurrency; + setCurrency.target = player; + setCurrency.currency = coins; + setCurrency.position = source.position; + setCurrency.sourceLOT = source.lot; + setCurrency.sourceID = source.object; + setCurrency.sourceTradeID = source.tradeID; + setCurrency.sourceType = coinSource; + return setCurrency; +} + +void Character::SetCoins(int64_t newCoins, eLootSourceType lootSource, const CoinSource& source) { if (newCoins < 0) { newCoins = 0; } @@ -600,15 +631,7 @@ void Character::SetCoins(int64_t newCoins, eLootSourceType lootSource) { m_Coins = newCoins; auto* entity = Game::entityManager->GetEntity(m_ObjectID); - GameMessages::SetCurrency setCurrency; - setCurrency.target = entity->GetObjectID(); - setCurrency.currency = m_Coins; - setCurrency.lootType = 0; - setCurrency.sourceLOT = 0; - setCurrency.sourceID = 0; - setCurrency.sourceTradeID = 0; - setCurrency.sourceType = lootSource; - setCurrency.SendToClient(entity->GetSystemAddress()); + MakeSetCurrency(entity->GetObjectID(), m_Coins, lootSource, source).SendToClient(entity->GetSystemAddress()); // Live followed every coin gain with CurrencyCollected (the amount gained); losses had none auto* characterComponent = entity->GetComponent(); diff --git a/dGame/Character.h b/dGame/Character.h index b5fd33284..0154c09a6 100644 --- a/dGame/Character.h +++ b/dGame/Character.h @@ -17,6 +17,26 @@ class Entity; enum class ePermissionMap : uint64_t; enum class eGameMasterLevel : uint8_t; enum class eLootSourceType : uint32_t; +namespace GameMessages { + struct SetCurrency; +} + +/** + * Where a coin change came from, as live filled SetCurrency (18,417 live SetCurrency): + * pickups carry the position the client picked the coins up at (the client starts the coin animation there); + * mission, achievement and activity rewards name the player (LOT 1); vendors name the vendor and its LOT; + * trades carry the trade ID. Everything else (death, mail) names nothing. + */ +struct CoinSource { + NiPoint3 position{ NiPoint3Constant::ZERO }; + LWOOBJID object{ LWOOBJID_EMPTY }; + LOT lot{ LOT_NULL }; + LWOOBJID tradeID{ LWOOBJID_EMPTY }; + + static CoinSource Object(const Entity& source); + static CoinSource Position(const NiPoint3& position); + static CoinSource Trade(LWOOBJID tradeID); +}; /** * Meta information about a character, like their name and style @@ -352,7 +372,12 @@ public: * @param newCoins the amount of coins to update by * @param coinSource The source of the loot */ - void SetCoins(int64_t newCoins, eLootSourceType coinSource); + void SetCoins(int64_t newCoins, eLootSourceType coinSource, const CoinSource& source = {}); + + /** + * The SetCurrency live sent for a coin change. + */ + static GameMessages::SetCurrency MakeSetCurrency(LWOOBJID player, int64_t coins, eLootSourceType coinSource, const CoinSource& source); /** * Get the entity this character belongs to diff --git a/dGame/TradingManager.cpp b/dGame/TradingManager.cpp index 37885dbc4..c13edbd84 100644 --- a/dGame/TradingManager.cpp +++ b/dGame/TradingManager.cpp @@ -177,8 +177,8 @@ void Trade::Complete() { // Now actually do the trade. Nothing is created or destroyed, so the economy ledger records transfers instead. EconomyLedger::ScopedItemTransfer transfer; - characterA->SetCoins(characterA->GetCoins() - m_CoinsA + m_CoinsB, eLootSourceType::TRADE); - characterB->SetCoins(characterB->GetCoins() - m_CoinsB + m_CoinsA, eLootSourceType::TRADE); + characterA->SetCoins(characterA->GetCoins() - m_CoinsA + m_CoinsB, eLootSourceType::TRADE, CoinSource::Trade(m_TradeId)); + characterB->SetCoins(characterB->GetCoins() - m_CoinsB + m_CoinsA, eLootSourceType::TRADE, CoinSource::Trade(m_TradeId)); const auto zone = Game::server ? Game::server->GetZoneID() : 0; const auto recordCoins = [zone](const uint64_t coins, const LWOOBJID from, const LWOOBJID to) { diff --git a/dGame/dComponents/VendorComponent.cpp b/dGame/dComponents/VendorComponent.cpp index b998d7030..131594019 100644 --- a/dGame/dComponents/VendorComponent.cpp +++ b/dGame/dComponents/VendorComponent.cpp @@ -207,7 +207,7 @@ void VendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) { inventoryComponent->RemoveItem(itemComp.currencyLOT, altCurrencyCost, eInventoryType::ALL); } - character->SetCoins(character->GetCoins() - (coinCost), eLootSourceType::VENDOR); + character->SetCoins(character->GetCoins() - (coinCost), eLootSourceType::VENDOR, CoinSource::Object(*m_Parent)); inventoryComponent->AddItem(lot, count, eLootSourceType::VENDOR); SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_SUCCESS); } @@ -278,7 +278,7 @@ void VendorComponent::SellToVendor(Entity& player, const SystemAddress& sysAddr, MakeRoomInBuyback(*inv, *item, static_cast(std::max(count, 0))); inv->MoveItemToInventory(item, eInventoryType::VENDOR_BUYBACK, count, true, false, true); - character->SetCoins(std::floor(character->GetCoins() + (static_cast(itemComp.baseValue * sellScalar) * count)), eLootSourceType::VENDOR); + character->SetCoins(std::floor(character->GetCoins() + (static_cast(itemComp.baseValue * sellScalar) * count)), eLootSourceType::VENDOR, CoinSource::Object(*m_Parent)); SendTransactionResult(m_Parent->GetObjectID(), sysAddr, eVendorTransactionResult::SELL_SUCCESS); } @@ -316,7 +316,7 @@ void VendorComponent::BuybackFromVendor(Entity& player, const SystemAddress& sys //inv->RemoveItem(count, -1, iObjID); inv->MoveItemToInventory(item, Inventory::FindInventoryTypeForLot(item->GetLot()), count, true, false); - character->SetCoins(character->GetCoins() - cost, eLootSourceType::VENDOR); + character->SetCoins(character->GetCoins() - cost, eLootSourceType::VENDOR, CoinSource::Object(*m_Parent)); //Game::entityManager->SerializeEntity(player); // so inventory updates SendTransactionResult(m_Parent->GetObjectID(), sysAddr, eVendorTransactionResult::PURCHASE_SUCCESS); } diff --git a/dGame/dGameMessages/PlayerMessages.cpp b/dGame/dGameMessages/PlayerMessages.cpp index 608071ce0..e495a82d0 100644 --- a/dGame/dGameMessages/PlayerMessages.cpp +++ b/dGame/dGameMessages/PlayerMessages.cpp @@ -50,7 +50,7 @@ namespace GameMessages { bitStream.Write(position.z); BitStreamUtils::WriteOptional(bitStream, sourceLOT, LOT_NULL); BitStreamUtils::WriteOptional(bitStream, sourceID, LWOOBJID_EMPTY); - BitStreamUtils::WriteOptional(bitStream, sourceTradeID, 0); + BitStreamUtils::WriteOptional(bitStream, sourceTradeID, LWOOBJID_EMPTY); BitStreamUtils::WriteOptional(bitStream, sourceType, eLootSourceType::NONE); } @@ -62,7 +62,7 @@ namespace GameMessages { VALIDATE_READ(bitStream.Read(position.z)); VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, sourceLOT, LOT_NULL)); VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, sourceID, LWOOBJID_EMPTY)); - VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, sourceTradeID, 0)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, sourceTradeID, LWOOBJID_EMPTY)); VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, sourceType, eLootSourceType::NONE)); return true; } @@ -115,10 +115,17 @@ namespace GameMessages { void PickupCurrency::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(currency); + bitStream.Write(position.x); + bitStream.Write(position.y); + bitStream.Write(position.z); } bool PickupCurrency::Deserialize(RakNet::BitStream& bitStream) { - return bitStream.Read(currency); + VALIDATE_READ(bitStream.Read(currency)); + VALIDATE_READ(bitStream.Read(position.x)); + VALIDATE_READ(bitStream.Read(position.y)); + VALIDATE_READ(bitStream.Read(position.z)); + return true; } void PickupCurrency::Handle(Entity& entity, const SystemAddress& sysAddr) { @@ -126,7 +133,7 @@ namespace GameMessages { auto* ch = entity.GetCharacter(); if (ch && entity.PickupCoins(currency)) { - ch->SetCoins(ch->GetCoins() + currency, eLootSourceType::PICKUP); + ch->SetCoins(ch->GetCoins() + currency, eLootSourceType::PICKUP, CoinSource::Position(position)); } } diff --git a/dGame/dGameMessages/PlayerMessages.h b/dGame/dGameMessages/PlayerMessages.h index b80bb5cb9..e44476d84 100644 --- a/dGame/dGameMessages/PlayerMessages.h +++ b/dGame/dGameMessages/PlayerMessages.h @@ -53,8 +53,7 @@ namespace GameMessages { NiPoint3 position{ NiPoint3Constant::ZERO }; LOT sourceLOT{ LOT_NULL }; // optional LWOOBJID sourceID{ LWOOBJID_EMPTY }; // optional - // optional (default 0). DLU writes it as an int32_t; lu_packets has an object ID (8 bytes). Only ever 0. - int32_t sourceTradeID{ 0 }; + LWOOBJID sourceTradeID{ LWOOBJID_EMPTY }; // optional; the client reads an object ID (8 bytes) eLootSourceType sourceType{ eLootSourceType::NONE }; // optional }; @@ -109,6 +108,7 @@ namespace GameMessages { void Handle(Entity& entity, const SystemAddress& sysAddr) override; uint32_t currency{}; + NiPoint3 position{ NiPoint3Constant::ZERO }; // where the client picked the coins up }; // Client -> server. diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index b0ec505c2..ea853702c 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -528,7 +528,7 @@ void Mission::YieldRewards() { if (info.reward_currency_repeatable > 0 || coinsToSend > 0) { eLootSourceType lootSource = info.isMission ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT; - character->SetCoins(character->GetCoins() + info.reward_currency_repeatable + coinsToSend, lootSource); + character->SetCoins(character->GetCoins() + info.reward_currency_repeatable + coinsToSend, lootSource, CoinSource::Object(*entity)); } return; @@ -556,7 +556,7 @@ void Mission::YieldRewards() { if (info.reward_currency > 0 || coinsToSend > 0) { eLootSourceType lootSource = info.isMission ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT; - character->SetCoins(character->GetCoins() + info.reward_currency + coinsToSend, lootSource); + character->SetCoins(character->GetCoins() + info.reward_currency + coinsToSend, lootSource, CoinSource::Object(*entity)); } if (info.reward_maxinventory > 0) { diff --git a/dGame/dUtilities/Loot.cpp b/dGame/dUtilities/Loot.cpp index b9a421b84..4315b3e72 100644 --- a/dGame/dUtilities/Loot.cpp +++ b/dGame/dUtilities/Loot.cpp @@ -570,7 +570,7 @@ void Loot::GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t acti auto* character = player->GetCharacter(); - character->SetCoins(character->GetCoins() + coins, eLootSourceType::ACTIVITY); + character->SetCoins(character->GetCoins() + coins, eLootSourceType::ACTIVITY, CoinSource::Object(*player)); } void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins) { diff --git a/tests/dGameTests/CMakeLists.txt b/tests/dGameTests/CMakeLists.txt index 5ee37e527..03b30e00f 100644 --- a/tests/dGameTests/CMakeLists.txt +++ b/tests/dGameTests/CMakeLists.txt @@ -18,6 +18,7 @@ set(DGAMETEST_SOURCES "UgcManifestTests.cpp" "SceneGhostingTests.cpp" "HolidayEventsTests.cpp" + "SetCurrencySourceTests.cpp" ) add_subdirectory(dComponentsTests) diff --git a/tests/dGameTests/SetCurrencySourceTests.cpp b/tests/dGameTests/SetCurrencySourceTests.cpp new file mode 100644 index 000000000..4b3a2942b --- /dev/null +++ b/tests/dGameTests/SetCurrencySourceTests.cpp @@ -0,0 +1,55 @@ +#include "GameDependencies.h" +#include "dGameMessagesTests/GameMessageTestUtils.h" + +#include "Character.h" +#include "Entity.h" +#include "PlayerMessages.h" +#include "eLootSourceType.h" + +#include + +using namespace GameMessageTestUtils; + +// SetCurrency source fields per source type, as live sent them. Each expected packet is a live SetCurrency with the +// player ID replaced by 0x1000000000000001 and any other object or trade ID by 0x0102030405060708. +class SetCurrencySourceTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } + + static constexpr LWOOBJID PLAYER = 0x1000000000000001LL; + static constexpr LWOOBJID OTHER = 0x0102030405060708LL; +}; + +TEST_F(SetCurrencySourceTests, PickupCarriesThePickupPosition) { + const auto msg = Character::MakeSetCurrency(PLAYER, 253493, eLootSourceType::PICKUP, CoinSource::Position(NiPoint3(-142.26820373535156f, 69.87815856933594f, 391.2568359375f))); + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 85 00 35 de 03 00 00 00 00 00 54 a2 07 61 cf 60 c5 a1 70 50 61 a1 88 58 00 00 00", 341), StructPacket(msg)); +} + +TEST_F(SetCurrencySourceTests, MissionAndAchievementNameThePlayer) { + auto playerInfo = info; + playerInfo.lot = 1; + Entity player(PLAYER, playerInfo); + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 85 00 02 25 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 40 00 00 20 20 00 00 00 00 00 02 08 10 00 00 00", 437), + StructPacket(Character::MakeSetCurrency(PLAYER, 206082, eLootSourceType::MISSION, CoinSource::Object(player)))); + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 85 00 0a ef 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 40 00 00 20 20 00 00 00 00 00 02 08 28 00 00 00", 437), + StructPacket(Character::MakeSetCurrency(PLAYER, 257802, eLootSourceType::ACHIEVEMENT, CoinSource::Object(player)))); +} + +TEST_F(SetCurrencySourceTests, VendorNamesTheVendor) { + auto vendorInfo = info; + vendorInfo.lot = 8212; + Entity vendor(OTHER, vendorInfo); + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 85 00 18 39 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 45 08 00 00 21 00 e0 c0 a0 80 60 40 28 48 00 00 00", 437), + StructPacket(Character::MakeSetCurrency(PLAYER, 14616, eLootSourceType::VENDOR, CoinSource::Object(vendor)))); +} + +TEST_F(SetCurrencySourceTests, TradeCarriesAnEightByteTradeId) { + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 85 00 34 66 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 10 80 70 60 50 40 30 20 18 30 00 00 00", 405), + StructPacket(Character::MakeSetCurrency(PLAYER, 222772, eLootSourceType::TRADE, CoinSource::Trade(OTHER)))); +} + +TEST_F(SetCurrencySourceTests, DeathNamesNothing) { + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 85 00 61 59 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 40 00 00 00", 341), + StructPacket(Character::MakeSetCurrency(PLAYER, 22881, eLootSourceType::DELETION, {}))); +} diff --git a/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp index 389ddb991..accbdf551 100644 --- a/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp @@ -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(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) {