mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
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:
@@ -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<CharacterComponent>();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<uint32_t>(std::max(count, 0)));
|
||||
inv->MoveItemToInventory(item, eInventoryType::VENDOR_BUYBACK, count, true, false, true);
|
||||
character->SetCoins(std::floor(character->GetCoins() + (static_cast<uint32_t>(itemComp.baseValue * sellScalar) * count)), eLootSourceType::VENDOR);
|
||||
character->SetCoins(std::floor(character->GetCoins() + (static_cast<uint32_t>(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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -18,6 +18,7 @@ set(DGAMETEST_SOURCES
|
||||
"UgcManifestTests.cpp"
|
||||
"SceneGhostingTests.cpp"
|
||||
"HolidayEventsTests.cpp"
|
||||
"SetCurrencySourceTests.cpp"
|
||||
)
|
||||
|
||||
add_subdirectory(dComponentsTests)
|
||||
|
||||
55
tests/dGameTests/SetCurrencySourceTests.cpp
Normal file
55
tests/dGameTests/SetCurrencySourceTests.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#include "GameDependencies.h"
|
||||
#include "dGameMessagesTests/GameMessageTestUtils.h"
|
||||
|
||||
#include "Character.h"
|
||||
#include "Entity.h"
|
||||
#include "PlayerMessages.h"
|
||||
#include "eLootSourceType.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
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, {})));
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user