mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-01 01:17:13 +00:00
feat(stats): coins, bricks and smashes counted by the server as live did
The passport totals for coins, bricks and enemies came from the client's ModifyPlayerZoneStatistic, which live's client sent for its per-zone counts; live's server counted the totals itself and told the client (UpdatePlayerStatistic). Now: - CurrencyCollected (the amount gained) right after every SetCurrency that raised the coins (pickups, missions, achievements, selling, activities); none for losses. Captures: 15,815 pickups + mission/achievement/vendor/ activity gains, all with the stat; no loss had one. - BricksCollected (the count) after every add to the bricks inventory the client is told about (captures: 999 of 1,010, pickups and relocations). - The kill counts go to the killer right after Die, before the loot: EnemiesSmashed when the DestructibleComponent's isnpc is set, else SmashablesSmashed for a smashable (captures: 2,766 NPC kills and 538 smashables; faction or AI don't decide it, e.g. the Banana Cluster has AI and counted as a smashable). Neither while racing. Before, every kill was a SmashablesSmashed. - ModifyPlayerZoneStatistic from the client only updates the zone counts, so nothing is counted twice. Check in game: pick up coins, sell an item, finish a mission, pick up bricks, smash a stromling and a crate; each passport number goes up by the right amount at once and stays the same after relogging (no double counts). The per-zone statistics still go up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include "Entity.h"
|
#include "Entity.h"
|
||||||
#include "EntityManager.h"
|
#include "EntityManager.h"
|
||||||
|
#include "CharacterComponent.h"
|
||||||
#include "GameMessages.h"
|
#include "GameMessages.h"
|
||||||
#include "EffectsMessages.h"
|
#include "EffectsMessages.h"
|
||||||
#include "MissionMessages.h"
|
#include "MissionMessages.h"
|
||||||
@@ -595,6 +596,7 @@ void Character::SetCoins(int64_t newCoins, eLootSourceType lootSource) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EconomyLedger::RecordCoins(m_ID, newCoins - m_Coins, lootSource);
|
EconomyLedger::RecordCoins(m_ID, newCoins - m_Coins, lootSource);
|
||||||
|
const auto gained = newCoins > m_Coins ? static_cast<uint64_t>(newCoins - m_Coins) : 0;
|
||||||
m_Coins = newCoins;
|
m_Coins = newCoins;
|
||||||
|
|
||||||
auto* entity = Game::entityManager->GetEntity(m_ObjectID);
|
auto* entity = Game::entityManager->GetEntity(m_ObjectID);
|
||||||
@@ -607,6 +609,10 @@ void Character::SetCoins(int64_t newCoins, eLootSourceType lootSource) {
|
|||||||
setCurrency.sourceTradeID = 0;
|
setCurrency.sourceTradeID = 0;
|
||||||
setCurrency.sourceType = lootSource;
|
setCurrency.sourceType = lootSource;
|
||||||
setCurrency.SendToClient(entity->GetSystemAddress());
|
setCurrency.SendToClient(entity->GetSystemAddress());
|
||||||
|
|
||||||
|
// Live followed every coin gain with CurrencyCollected (the amount gained); losses had none
|
||||||
|
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
||||||
|
if (gained > 0 && characterComponent) characterComponent->UpdatePlayerStatistic(CurrencyCollected, gained);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Character::HasBeenToWorld(LWOMAPID mapID) const {
|
bool Character::HasBeenToWorld(LWOMAPID mapID) const {
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ void Entity::Initialize() {
|
|||||||
comp->SetDeathBehavior(destCompData[0].death_behavior);
|
comp->SetDeathBehavior(destCompData[0].death_behavior);
|
||||||
|
|
||||||
comp->SetIsSmashable(comp->GetIsSmashable() || destCompData[0].isSmashable);
|
comp->SetIsSmashable(comp->GetIsSmashable() || destCompData[0].isSmashable);
|
||||||
|
comp->SetIsNPC(destCompData[0].isnpc);
|
||||||
|
|
||||||
comp->SetLootMatrixID(destCompData[0].LootMatrixIndex);
|
comp->SetLootMatrixID(destCompData[0].LootMatrixIndex);
|
||||||
comp->SetCurrencyIndex(destCompData[0].CurrencyIndex);
|
comp->SetCurrencyIndex(destCompData[0].CurrencyIndex);
|
||||||
@@ -1473,13 +1474,6 @@ void Entity::Kill(Entity* murderer, const eKillType killType) {
|
|||||||
characterComponent->UpdatePlayerStatistic(TimesSmashed);
|
characterComponent->UpdatePlayerStatistic(TimesSmashed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track a player smashing something else
|
|
||||||
if (murderer != nullptr) {
|
|
||||||
auto* murdererCharacterComponent = murderer->GetComponent<CharacterComponent>();
|
|
||||||
if (murdererCharacterComponent != nullptr) {
|
|
||||||
murdererCharacterComponent->UpdatePlayerStatistic(SmashablesSmashed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Entity::AddDieCallback(const std::function<void()>& callback) {
|
void Entity::AddDieCallback(const std::function<void()>& callback) {
|
||||||
|
|||||||
@@ -606,6 +606,10 @@ void CharacterComponent::TrackArmorDelta(int32_t armor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CharacterComponent::TrackItemsAdded(eInventoryType inventoryType, uint32_t count) {
|
||||||
|
if (inventoryType == eInventoryType::BRICKS && count > 0) UpdatePlayerStatistic(BricksCollected, count);
|
||||||
|
}
|
||||||
|
|
||||||
void CharacterComponent::TrackQuickBuildComplete() {
|
void CharacterComponent::TrackQuickBuildComplete() {
|
||||||
UpdatePlayerStatistic(QuickBuildsCompleted);
|
UpdatePlayerStatistic(QuickBuildsCompleted);
|
||||||
|
|
||||||
@@ -670,14 +674,13 @@ void CharacterComponent::SendPlayerStatistic(StatisticID updateID, uint64_t upda
|
|||||||
void CharacterComponent::HandleZoneStatisticsUpdate(LWOMAPID zoneID, const std::u16string& name, int32_t value) {
|
void CharacterComponent::HandleZoneStatisticsUpdate(LWOMAPID zoneID, const std::u16string& name, int32_t value) {
|
||||||
auto zoneStatistics = &GetZoneStatisticsForMap(zoneID);
|
auto zoneStatistics = &GetZoneStatisticsForMap(zoneID);
|
||||||
|
|
||||||
|
// The client's per-zone counts; the passport totals are the server's (UpdatePlayerStatistic), so they aren't
|
||||||
|
// counted twice
|
||||||
if (name == u"BricksCollected") {
|
if (name == u"BricksCollected") {
|
||||||
m_BricksCollected += value;
|
|
||||||
zoneStatistics->m_BricksCollected += value;
|
zoneStatistics->m_BricksCollected += value;
|
||||||
} else if (name == u"CoinsCollected") {
|
} else if (name == u"CoinsCollected") {
|
||||||
m_CurrencyCollected += value;
|
|
||||||
zoneStatistics->m_CoinsCollected += value;
|
zoneStatistics->m_CoinsCollected += value;
|
||||||
} else if (name == u"EnemiesSmashed") {
|
} else if (name == u"EnemiesSmashed") {
|
||||||
m_EnemiesSmashed += value;
|
|
||||||
zoneStatistics->m_EnemiesSmashed += value;
|
zoneStatistics->m_EnemiesSmashed += value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
#include "Loot.h"
|
#include "Loot.h"
|
||||||
#include "StatisticID.h"
|
#include "StatisticID.h"
|
||||||
|
#include "eInventoryType.h"
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
@@ -217,6 +218,12 @@ public:
|
|||||||
*/
|
*/
|
||||||
void TrackArmorDelta(int32_t armor);
|
void TrackArmorDelta(int32_t armor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts items added to the inventory the client was told about (AddItemToInventoryClientSync): live followed
|
||||||
|
* every add to the bricks inventory with BricksCollected (the count), whatever the source.
|
||||||
|
*/
|
||||||
|
void TrackItemsAdded(eInventoryType inventoryType, uint32_t count);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles completing a rebuild by updating the statistics
|
* Handles completing a rebuild by updating the statistics
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -743,6 +743,16 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
|
|||||||
die.lootOwnerID = source;
|
die.lootOwnerID = source;
|
||||||
die.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
die.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||||
|
|
||||||
|
// Live told the killer right after Die, before the loot: EnemiesSmashed for an NPC, SmashablesSmashed for another
|
||||||
|
// smashable. Racing counts its own (RacingSmashablesSmashed).
|
||||||
|
if (owner && owner != m_Parent) {
|
||||||
|
auto* killerCharacter = owner->GetComponent<CharacterComponent>();
|
||||||
|
if (killerCharacter && !killerCharacter->GetIsRacing()) {
|
||||||
|
if (m_IsNPC) killerCharacter->UpdatePlayerStatistic(EnemiesSmashed);
|
||||||
|
else if (m_IsSmashable) killerCharacter->UpdatePlayerStatistic(SmashablesSmashed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//NANI?!
|
//NANI?!
|
||||||
if (!isPlayer) {
|
if (!isPlayer) {
|
||||||
if (owner != nullptr) {
|
if (owner != nullptr) {
|
||||||
|
|||||||
@@ -225,6 +225,13 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool GetIsSmashable() const { return m_IsSmashable; }
|
bool GetIsSmashable() const { return m_IsSmashable; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this is an NPC (the DestructibleComponent's isnpc): live counted smashing one as EnemiesSmashed, other
|
||||||
|
* smashables as SmashablesSmashed
|
||||||
|
*/
|
||||||
|
void SetIsNPC(bool value) { m_IsNPC = value; }
|
||||||
|
bool GetIsNPC() const { return m_IsNPC; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current is-dead value, this is mostly unused
|
* Returns the current is-dead value, this is mostly unused
|
||||||
* @return the current is-dead value, this is mostly unused
|
* @return the current is-dead value, this is mostly unused
|
||||||
@@ -574,6 +581,8 @@ private:
|
|||||||
*/
|
*/
|
||||||
bool m_IsSmashable;
|
bool m_IsSmashable;
|
||||||
|
|
||||||
|
bool m_IsNPC = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this entity is dead. Unused, here for serialization
|
* Whether this entity is dead. Unused, here for serialization
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ ReceivedItem InventoryComponent::ReceiveItem(const LWOOBJID id, const LOT lot, c
|
|||||||
addItem.newObjID = id;
|
addItem.newObjID = id;
|
||||||
addItem.showFlyingLoot = options.showFlyingLoot && !options.equip;
|
addItem.showFlyingLoot = options.showFlyingLoot && !options.equip;
|
||||||
addItem.SendToClient(m_Parent->GetSystemAddress());
|
addItem.SendToClient(m_Parent->GetSystemAddress());
|
||||||
|
if (auto* characterComponent = m_Parent->GetComponent<CharacterComponent>()) characterComponent->TrackItemsAdded(inventoryType, count);
|
||||||
EconomyLedger::RecordItems(m_Parent, lot, count, static_cast<uint32_t>(lootSourceType));
|
EconomyLedger::RecordItems(m_Parent, lot, count, static_cast<uint32_t>(lootSourceType));
|
||||||
Contraband::OnItemAdded(m_Parent, lot, count, lootSourceType, options.sourceInventory);
|
Contraband::OnItemAdded(m_Parent, lot, count, lootSourceType, options.sourceInventory);
|
||||||
if (options.equip) {
|
if (options.equip) {
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ Item::Item(
|
|||||||
addItem.newObjID = id;
|
addItem.newObjID = id;
|
||||||
addItem.showFlyingLoot = showFlyingLoot;
|
addItem.showFlyingLoot = showFlyingLoot;
|
||||||
addItem.SendToClient(entity->GetSystemAddress());
|
addItem.SendToClient(entity->GetSystemAddress());
|
||||||
|
if (auto* characterComponent = entity->GetComponent<CharacterComponent>()) characterComponent->TrackItemsAdded(inventory->GetType(), this->count);
|
||||||
|
|
||||||
if (isModMoveAndEquip) {
|
if (isModMoveAndEquip) {
|
||||||
Equip();
|
Equip();
|
||||||
@@ -206,6 +207,7 @@ void Item::SetCount(const uint32_t value, const bool silent, const bool disassem
|
|||||||
addItem.newObjID = id;
|
addItem.newObjID = id;
|
||||||
addItem.showFlyingLoot = showFlyingLoot;
|
addItem.showFlyingLoot = showFlyingLoot;
|
||||||
addItem.SendToClient(entity->GetSystemAddress());
|
addItem.SendToClient(entity->GetSystemAddress());
|
||||||
|
if (auto* characterComponent = entity->GetComponent<CharacterComponent>()) characterComponent->TrackItemsAdded(inventory->GetType(), delta);
|
||||||
} else {
|
} else {
|
||||||
GameMessages::RemoveItemFromInventory removeItem;
|
GameMessages::RemoveItemFromInventory removeItem;
|
||||||
removeItem.target = entity->GetObjectID();
|
removeItem.target = entity->GetObjectID();
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include "CharacterComponent.h"
|
#include "CharacterComponent.h"
|
||||||
#include "Entity.h"
|
#include "Entity.h"
|
||||||
#include "PlayerMessages.h"
|
#include "PlayerMessages.h"
|
||||||
|
#include "DestroyableComponent.h"
|
||||||
|
#include "CDClientDatabase.h"
|
||||||
#include "dGameMessagesTests/GameMessageTestUtils.h"
|
#include "dGameMessagesTests/GameMessageTestUtils.h"
|
||||||
|
|
||||||
// Live sent UpdatePlayerStatistic (1481) server -> client for the statistics the server counted (docs/CaptureUnknowns.md).
|
// Live sent UpdatePlayerStatistic (1481) server -> client for the statistics the server counted (docs/CaptureUnknowns.md).
|
||||||
@@ -109,3 +111,66 @@ TEST_F(PlayerStatisticTest, MetersTraveledGoOutInBatches) {
|
|||||||
ASSERT_EQ(statistics.size(), 1);
|
ASSERT_EQ(statistics.size(), 1);
|
||||||
EXPECT_EQ(statistics[0].updateValue, 0);
|
EXPECT_EQ(statistics[0].updateValue, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(PlayerStatisticTest, BricksAddedAreCollected) {
|
||||||
|
auto statistics = Statistics(Capture([&] { characterComponent->TrackItemsAdded(eInventoryType::BRICKS, 3); }));
|
||||||
|
ASSERT_EQ(statistics.size(), 1);
|
||||||
|
EXPECT_EQ(statistics[0].updateID, static_cast<int32_t>(BricksCollected));
|
||||||
|
EXPECT_EQ(statistics[0].updateValue, 3);
|
||||||
|
EXPECT_TRUE(Statistics(Capture([&] { characterComponent->TrackItemsAdded(eInventoryType::ITEMS, 1); })).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PlayerStatisticTest, ZoneStatisticsFromTheClientLeaveThePassportTotalsAlone) {
|
||||||
|
// The client reports these per zone; the totals come from what the server sent (not counted twice)
|
||||||
|
characterComponent->HandleZoneStatisticsUpdate(1100, u"CoinsCollected", 10);
|
||||||
|
characterComponent->HandleZoneStatisticsUpdate(1100, u"BricksCollected", 2);
|
||||||
|
characterComponent->HandleZoneStatisticsUpdate(1100, u"EnemiesSmashed", 1);
|
||||||
|
EXPECT_EQ(characterComponent->StatisticsToString().substr(0, 10), "0;0;0;0;0;");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live: Die, then (to the killer) EnemiesSmashed for an NPC or SmashablesSmashed for another smashable, then the loot
|
||||||
|
TEST_F(PlayerStatisticTest, SmashingCountsEnemiesAndSmashables) {
|
||||||
|
// Smash finds the killer through the entity manager, which looks the new entity up in the CDClient
|
||||||
|
CDClientDatabase::Connect(":memory:");
|
||||||
|
CDClientDatabase::ExecuteDML("CREATE TABLE ComponentsRegistry (id INTEGER, component_type INTEGER, component_id INTEGER);");
|
||||||
|
ASSERT_NE(Game::entityManager->CreateEntity(info, nullptr, nullptr, true, 0x3FFFFFFFFFFELL), nullptr); // the zone control object loot asks
|
||||||
|
auto* killer = Game::entityManager->CreateEntity(info, nullptr, nullptr, false, PLAYER + 1);
|
||||||
|
ASSERT_NE(killer, nullptr);
|
||||||
|
auto* killerCharacter = killer->AddComponent<CharacterComponent>(-1, character.get(), ClientAddress());
|
||||||
|
killerCharacter->InitializeStatisticsFromString("");
|
||||||
|
|
||||||
|
LWOOBJID victimID = 0x3FFF000000000001LL;
|
||||||
|
const auto smash = [&](bool npc, bool smashable) {
|
||||||
|
auto* victim = Game::entityManager->CreateEntity(info, nullptr, nullptr, false, victimID++);
|
||||||
|
auto* destroyable = victim->AddComponent<DestroyableComponent>(-1);
|
||||||
|
destroyable->SetMaxHealth(10.0f);
|
||||||
|
destroyable->SetHealth(10);
|
||||||
|
destroyable->SetIsNPC(npc);
|
||||||
|
destroyable->SetIsSmashable(smashable);
|
||||||
|
return Capture([&] { destroyable->Smash(killer->GetObjectID()); });
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& [npc, smashable, expected] : { std::tuple{ true, true, EnemiesSmashed }, std::tuple{ false, true, SmashablesSmashed } }) {
|
||||||
|
const auto packets = smash(npc, smashable);
|
||||||
|
size_t die = packets.size(), statistic = packets.size();
|
||||||
|
for (size_t i = 0; i < packets.size(); i++) {
|
||||||
|
RakNet::BitStream bitStream(const_cast<uint8_t*>(packets[i].bytes.data()), packets[i].bytes.size(), false);
|
||||||
|
LWOOBJID target{};
|
||||||
|
MessageType::Game msgId{};
|
||||||
|
if (!GameMessages::NetGameMsg::ReadPacketHeader(bitStream, target, msgId)) continue;
|
||||||
|
if (msgId == MessageType::Game::DIE && die == packets.size()) die = i;
|
||||||
|
if (msgId == MessageType::Game::UPDATE_PLAYER_STATISTIC) statistic = i;
|
||||||
|
}
|
||||||
|
ASSERT_LT(die, packets.size());
|
||||||
|
ASSERT_LT(statistic, packets.size());
|
||||||
|
EXPECT_LT(die, statistic);
|
||||||
|
const auto statistics = Statistics({ packets[statistic] });
|
||||||
|
ASSERT_EQ(statistics.size(), 1);
|
||||||
|
EXPECT_EQ(statistics[0].target, killer->GetObjectID());
|
||||||
|
EXPECT_EQ(statistics[0].updateID, static_cast<int32_t>(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neither an NPC nor smashable (e.g. a collectible spawner): nothing
|
||||||
|
EXPECT_TRUE(Statistics(smash(false, false)).empty());
|
||||||
|
killer->SetCharacter(nullptr);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user