mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-13 16:23:44 +00:00
feat(stats): send UpdatePlayerStatistic to the player as live did
Live sent UpdatePlayerStatistic (1481) server -> client for every statistic the server counted (59,933 messages in the captures, none client -> server); DLU only counted them, so the passport stayed stale until the next login. CharacterComponent::UpdatePlayerStatistic now tells the player's client what it counts (not what a client reported), with the amount left out when it is 1, byte for byte as the live samples. MetersTraveled: whole meters now count (each position update's distance was cut to a whole number before, so short steps never counted) and go to the client once 25 have gathered (live's values were nearly all 25-33 while moving) and the rest, 0 included, when the player is taken down on leaving the world (live sent one more after TRANSFER_TO_WORLD). DistanceDriven goes out every 10 seconds of racing (inferred: live sent it every few hundred packets, about 1300 units at race speed). Check in game: open the passport's statistics page, walk around, collect a power-up, complete a mission; the numbers go up without relogging. Walk 25+ meters; Meters Traveled matches what the passport shows after relogging. Change worlds and relog: the meters were kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
#include "eReplicaPacketType.h"
|
||||
#include "PlayerManager.h"
|
||||
#include "GhostComponent.h"
|
||||
#include "CharacterComponent.h"
|
||||
#include <ranges>
|
||||
|
||||
// Configure which zones have ghosting disabled, mostly small worlds.
|
||||
@@ -452,6 +453,12 @@ void EntityManager::DestructEntity(Entity* entity, const SystemAddress& sysAddr)
|
||||
|
||||
Game::server->Send(stream, sysAddr, sysAddr == UNASSIGNED_SYSTEM_ADDRESS);
|
||||
|
||||
// A player taken down for everyone is leaving the world: live then sent the meters its client hadn't been told
|
||||
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS && entity->IsPlayer()) {
|
||||
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
||||
if (characterComponent) characterComponent->FlushMovementStatistics();
|
||||
}
|
||||
|
||||
// Players still loading were to get it once loaded: only the ones it was taken down for (all of them, or the one
|
||||
// player; taking it down for one player, e.g. out of their ghosting range, leaves the others' queue alone)
|
||||
for (auto* player : PlayerManager::GetAllPlayers()) {
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
#include "ZoneInstanceManager.h"
|
||||
#include "ClientPackets.h"
|
||||
#include "MessageType/Game.h"
|
||||
#include "PlayerMessages.h"
|
||||
#include <ctime>
|
||||
#include <cmath>
|
||||
#include <ranges>
|
||||
|
||||
CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID, Character* character, const SystemAddress& systemAddress) : Component(parent, componentID) {
|
||||
@@ -619,14 +621,52 @@ void CharacterComponent::TrackRaceCompleted(bool won) {
|
||||
|
||||
void CharacterComponent::TrackPositionUpdate(const NiPoint3& newPosition) {
|
||||
const auto distance = NiPoint3::Distance(newPosition, m_Parent->GetPosition());
|
||||
if (!std::isfinite(distance)) return;
|
||||
|
||||
// Whole meters count at once; the parts of a meter add up
|
||||
auto& remainder = m_IsRacing ? m_DrivenRemainder : m_MetersRemainder;
|
||||
remainder += distance;
|
||||
const auto meters = static_cast<uint64_t>(remainder);
|
||||
remainder -= static_cast<float>(meters);
|
||||
|
||||
if (m_IsRacing) {
|
||||
UpdatePlayerStatistic(DistanceDriven, static_cast<uint64_t>(distance));
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (m_LastDrivenUpdate != std::chrono::steady_clock::time_point{}) m_DrivenSinceSend += std::chrono::duration<float>(now - m_LastDrivenUpdate).count();
|
||||
m_LastDrivenUpdate = now;
|
||||
if (meters > 0) UpdatePlayerStatistic(DistanceDriven, meters);
|
||||
m_UnsentDriven += meters;
|
||||
if (m_DrivenSinceSend >= DISTANCE_DRIVEN_INTERVAL) {
|
||||
SendPlayerStatistic(DistanceDriven, m_UnsentDriven);
|
||||
m_UnsentDriven = 0;
|
||||
m_DrivenSinceSend = 0.0f;
|
||||
}
|
||||
} else {
|
||||
UpdatePlayerStatistic(MetersTraveled, static_cast<uint64_t>(distance));
|
||||
if (meters > 0) UpdatePlayerStatistic(MetersTraveled, meters);
|
||||
m_UnsentMeters += meters;
|
||||
if (m_UnsentMeters >= METERS_TRAVELED_BATCH) {
|
||||
SendPlayerStatistic(MetersTraveled, m_UnsentMeters);
|
||||
m_UnsentMeters = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterComponent::FlushMovementStatistics() {
|
||||
if (m_UnsentDriven > 0) SendPlayerStatistic(DistanceDriven, m_UnsentDriven);
|
||||
SendPlayerStatistic(MetersTraveled, m_UnsentMeters);
|
||||
m_UnsentDriven = 0;
|
||||
m_UnsentMeters = 0;
|
||||
m_DrivenSinceSend = 0.0f;
|
||||
}
|
||||
|
||||
void CharacterComponent::SendPlayerStatistic(StatisticID updateID, uint64_t updateValue) const {
|
||||
if (m_SystemAddress == UNASSIGNED_SYSTEM_ADDRESS) return;
|
||||
GameMessages::UpdatePlayerStatistic statistic;
|
||||
statistic.target = m_Parent->GetObjectID();
|
||||
statistic.updateID = static_cast<int32_t>(updateID);
|
||||
statistic.updateValue = static_cast<int64_t>(updateValue);
|
||||
statistic.SendToClient(m_SystemAddress);
|
||||
}
|
||||
|
||||
void CharacterComponent::HandleZoneStatisticsUpdate(LWOMAPID zoneID, const std::u16string& name, int32_t value) {
|
||||
auto zoneStatistics = &GetZoneStatisticsForMap(zoneID);
|
||||
|
||||
@@ -730,6 +770,9 @@ void CharacterComponent::UpdatePlayerStatistic(StatisticID updateID, uint64_t up
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Live told the client at once; the meters go out in batches (TrackPositionUpdate)
|
||||
if (!fromClient && updateID != MetersTraveled && updateID != DistanceDriven) SendPlayerStatistic(updateID, updateValue);
|
||||
}
|
||||
|
||||
void CharacterComponent::InitializeStatisticsFromString(const std::string& statisticsString) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "Loot.h"
|
||||
#include "StatisticID.h"
|
||||
#include <optional>
|
||||
#include <chrono>
|
||||
|
||||
enum class eGameActivity : uint32_t;
|
||||
|
||||
@@ -228,10 +229,24 @@ public:
|
||||
void TrackRaceCompleted(bool won);
|
||||
|
||||
/**
|
||||
* Tracks an updated position for a player
|
||||
* Tracks an updated position for a player: MetersTraveled (DistanceDriven while racing). The whole meters count at
|
||||
* once; the client is told in batches, as live did (MetersTraveled once METERS_TRAVELED_BATCH have gathered,
|
||||
* DistanceDriven every DISTANCE_DRIVEN_INTERVAL seconds), and the rest when the player leaves the world
|
||||
* (FlushMovementStatistics).
|
||||
*/
|
||||
void TrackPositionUpdate(const NiPoint3& newPosition);
|
||||
|
||||
/**
|
||||
* Tells the client the meters it hasn't been told yet (live sent a last MetersTraveled, 0 included, after the
|
||||
* player's objects were taken down on leaving the world).
|
||||
*/
|
||||
void FlushMovementStatistics();
|
||||
|
||||
// How many unsent MetersTraveled make live send them (live's values: nearly all 25 to 33 while moving)
|
||||
static constexpr uint64_t METERS_TRAVELED_BATCH = 25;
|
||||
// How often DistanceDriven went out while racing (live: every few hundred packets, about 1300 units at race speed; inferred)
|
||||
static constexpr float DISTANCE_DRIVEN_INTERVAL = 10.0f;
|
||||
|
||||
/**
|
||||
* Handles a zone statistic update
|
||||
* @param zoneID the zone that the stat belongs to
|
||||
@@ -248,6 +263,12 @@ public:
|
||||
*/
|
||||
void UpdatePlayerStatistic(StatisticID updateID, uint64_t updateValue = 1, bool fromClient = false);
|
||||
|
||||
/**
|
||||
* Sends UpdatePlayerStatistic to the player: the client adds updateValue to the passport statistic. Live sent it
|
||||
* for every statistic the server counted (docs/CaptureUnknowns.md).
|
||||
*/
|
||||
void SendPlayerStatistic(StatisticID updateID, uint64_t updateValue) const;
|
||||
|
||||
/**
|
||||
* Add a venture vision effect to the player minimap.
|
||||
*/
|
||||
@@ -508,6 +529,14 @@ private:
|
||||
*/
|
||||
uint64_t m_MetersTraveled;
|
||||
|
||||
// Parts of a meter not counted yet, and counted meters the client hasn't been told (TrackPositionUpdate)
|
||||
float m_MetersRemainder = 0.0f;
|
||||
uint64_t m_UnsentMeters = 0;
|
||||
float m_DrivenRemainder = 0.0f;
|
||||
uint64_t m_UnsentDriven = 0;
|
||||
float m_DrivenSinceSend = 0.0f; // seconds
|
||||
std::chrono::steady_clock::time_point m_LastDrivenUpdate{};
|
||||
|
||||
/**
|
||||
* Total amount of times this character was smashed, either by other entities or by going out of bounds
|
||||
*/
|
||||
|
||||
@@ -100,7 +100,8 @@ namespace GameMessages {
|
||||
LWOMAPID zoneID{ LWOMAPID_INVALID }; // optional
|
||||
};
|
||||
|
||||
// Client -> server here; live sent it server -> client (live captures hold no client -> server copy). The client's CharacterComponent adds updateValue
|
||||
// Server -> client (CharacterComponent::SendPlayerStatistic), as live sent it; live captures hold no client -> server
|
||||
// copy, but one is still counted when it comes. The client's CharacterComponent adds updateValue
|
||||
// to the passport statistic and, for every statistic but MetersTraveled, updates the passport UI. updateID is a
|
||||
// StatisticID. See docs/CaptureUnknowns.md.
|
||||
struct UpdatePlayerStatistic : public NetGameMsg {
|
||||
|
||||
@@ -11,6 +11,7 @@ set(DGAMETEST_SOURCES
|
||||
"CollisionFilterTests.cpp"
|
||||
"ItemSetPassiveAbilityTests.cpp"
|
||||
"PowerUpStatisticTests.cpp"
|
||||
"PlayerStatisticTests.cpp"
|
||||
"BrickByBrickTests.cpp"
|
||||
"PropertyBuildersTests.cpp"
|
||||
"ModularBuildTableTests.cpp"
|
||||
|
||||
111
tests/dGameTests/PlayerStatisticTests.cpp
Normal file
111
tests/dGameTests/PlayerStatisticTests.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "GameDependencies.h"
|
||||
#include "Character.h"
|
||||
#include "CharacterComponent.h"
|
||||
#include "Entity.h"
|
||||
#include "PlayerMessages.h"
|
||||
#include "dGameMessagesTests/GameMessageTestUtils.h"
|
||||
|
||||
// Live sent UpdatePlayerStatistic (1481) server -> client for the statistics the server counted (docs/CaptureUnknowns.md).
|
||||
// The packets below are from 2011/2012 live captures of the player 0x100000008eea7d5e.
|
||||
|
||||
using namespace GameMessageTestUtils;
|
||||
|
||||
class PlayerStatisticTest : public GameDependenciesTest {
|
||||
protected:
|
||||
static constexpr LWOOBJID PLAYER = 0x100000008eea7d5eLL;
|
||||
std::unique_ptr<Entity> entity;
|
||||
std::unique_ptr<Character> character;
|
||||
CharacterComponent* characterComponent = nullptr;
|
||||
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
entity = std::make_unique<Entity>(PLAYER, info);
|
||||
character = std::make_unique<Character>(1, nullptr);
|
||||
entity->SetCharacter(character.get());
|
||||
characterComponent = entity->AddComponent<CharacterComponent>(-1, character.get(), ClientAddress());
|
||||
characterComponent->InitializeStatisticsFromString(""); // every statistic 0
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
entity->SetCharacter(nullptr);
|
||||
entity.reset();
|
||||
character.reset();
|
||||
TearDownDependencies();
|
||||
}
|
||||
|
||||
// The UpdatePlayerStatistic packets sent, each checked to go to the player's client only
|
||||
static std::vector<GameMessages::UpdatePlayerStatistic> Statistics(const std::vector<CapturedPacket>& packets) {
|
||||
std::vector<GameMessages::UpdatePlayerStatistic> statistics;
|
||||
for (const auto& packet : packets) {
|
||||
RakNet::BitStream bitStream(const_cast<uint8_t*>(packet.bytes.data()), packet.bytes.size(), false);
|
||||
LWOOBJID target{};
|
||||
MessageType::Game msgId{};
|
||||
if (!GameMessages::NetGameMsg::ReadPacketHeader(bitStream, target, msgId) || msgId != MessageType::Game::UPDATE_PLAYER_STATISTIC) continue;
|
||||
EXPECT_EQ(packet.sysAddr, ClientAddress());
|
||||
EXPECT_FALSE(packet.broadcast);
|
||||
auto& statistic = statistics.emplace_back();
|
||||
EXPECT_TRUE(statistic.Deserialize(bitStream));
|
||||
statistic.target = target;
|
||||
}
|
||||
return statistics;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(PlayerStatisticTest, SendsWhatTheServerCountsAsLiveDid) {
|
||||
// CurrencyCollected 1 (the amount is left out, the client's default) and 500
|
||||
auto packets = Capture([&] { characterComponent->UpdatePlayerStatistic(CurrencyCollected); });
|
||||
ASSERT_EQ(packets.size(), 1);
|
||||
EXPECT_EQ(FromHex("5305000c000000005e7dea8e00000010c9050100000000").bytes, packets[0].bytes); // captures are padded to whole bytes
|
||||
EXPECT_EQ(packets[0].sysAddr, ClientAddress());
|
||||
EXPECT_FALSE(packets[0].broadcast);
|
||||
|
||||
packets = Capture([&] { characterComponent->UpdatePlayerStatistic(CurrencyCollected, 500); });
|
||||
ASSERT_EQ(packets.size(), 1);
|
||||
EXPECT_EQ(FromHex("5305000c000000005e7dea8e00000010c90501000000fa0080000000000000").bytes, packets[0].bytes); // captures are padded to whole bytes
|
||||
EXPECT_EQ(characterComponent->StatisticsToString().substr(0, 4), "501;");
|
||||
}
|
||||
|
||||
TEST_F(PlayerStatisticTest, WhatTheClientReportsIsCountedNotEchoed) {
|
||||
const auto packets = Capture([&] { characterComponent->UpdatePlayerStatistic(PetsTamed, 1, true); });
|
||||
EXPECT_TRUE(packets.empty());
|
||||
EXPECT_EQ(characterComponent->StatisticsToString().substr(0, 16), "0;0;0;0;0;0;0;1;");
|
||||
}
|
||||
|
||||
TEST_F(PlayerStatisticTest, MetersTraveledGoOutInBatches) {
|
||||
// The entity has no physics, so every update is measured from the origin
|
||||
auto packets = Capture([&] {
|
||||
characterComponent->TrackPositionUpdate(NiPoint3(10.0f, 0.0f, 0.0f));
|
||||
characterComponent->TrackPositionUpdate(NiPoint3(0.0f, 0.0f, 10.5f));
|
||||
});
|
||||
EXPECT_TRUE(Statistics(packets).empty()); // 20 counted, not sent yet
|
||||
|
||||
packets = Capture([&] { characterComponent->TrackPositionUpdate(NiPoint3(0.0f, 9.0f, 0.0f)); });
|
||||
auto statistics = Statistics(packets);
|
||||
ASSERT_EQ(statistics.size(), 1);
|
||||
EXPECT_EQ(statistics[0].target, PLAYER);
|
||||
EXPECT_EQ(statistics[0].updateID, static_cast<int32_t>(MetersTraveled));
|
||||
EXPECT_EQ(statistics[0].updateValue, 29); // 10 + 10 + 9, the half meter waits
|
||||
EXPECT_EQ(FromHex("5305000c000000005e7dea8e00000010c9050c0000008e8000000000000000").bytes, packets[0].bytes); // captures are padded to whole bytes
|
||||
|
||||
// The half meters add up; what's left goes out when the player leaves the world
|
||||
packets = Capture([&] {
|
||||
characterComponent->TrackPositionUpdate(NiPoint3(0.5f, 0.0f, 0.0f));
|
||||
characterComponent->TrackPositionUpdate(NiPoint3(3.0f, 0.0f, 0.0f));
|
||||
characterComponent->FlushMovementStatistics();
|
||||
});
|
||||
statistics = Statistics(packets);
|
||||
ASSERT_EQ(statistics.size(), 1);
|
||||
EXPECT_EQ(statistics[0].updateID, static_cast<int32_t>(MetersTraveled));
|
||||
EXPECT_EQ(statistics[0].updateValue, 4);
|
||||
|
||||
// Every meter was counted once
|
||||
const auto split = GeneralUtils::SplitString(characterComponent->StatisticsToString(), ';');
|
||||
EXPECT_EQ(split[11], "33");
|
||||
|
||||
// Live sent the last one even when nothing was left
|
||||
statistics = Statistics(Capture([&] { characterComponent->FlushMovementStatistics(); }));
|
||||
ASSERT_EQ(statistics.size(), 1);
|
||||
EXPECT_EQ(statistics[0].updateValue, 0);
|
||||
}
|
||||
Reference in New Issue
Block a user