Files
DarkflameServer/dGame/dGameMessages/PlayerMessages.h
Aaron Kimbrell 752f079100 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>
2026-09-29 02:42:02 -05:00

165 lines
6.3 KiB
C++

#ifndef PLAYERMESSAGES_H
#define PLAYERMESSAGES_H
#include "GameMessages.h"
#include "eGameMasterLevel.h"
#include "eHelpType.h"
#include "eLootSourceType.h"
#include <string>
// Game messages about a player's own state: GM level and chat mode, score, coins, reputation, statistics, chat
// commands and bug reports.
// Fields are listed in wire order; names follow the client (legouniverse.exe 1.10.64) where known.
namespace GameMessages {
// Server -> client, broadcast.
struct UpdateChatMode : public NetGameMsg {
UpdateChatMode() : NetGameMsg(MessageType::Game::UPDATE_CHAT_MODE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
eGameMasterLevel level{};
};
// Server -> client, broadcast.
struct SetGMLevel : public NetGameMsg {
SetGMLevel() : NetGameMsg(MessageType::Game::SET_GM_LEVEL) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
// DLU always writes this bit set.
bool bOverride{ true };
eGameMasterLevel level{};
};
// Server -> client, to one client.
struct ModifyLEGOScore : public NetGameMsg {
ModifyLEGOScore() : NetGameMsg(MessageType::Game::MODIFY_LEGO_SCORE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
int64_t score{};
eLootSourceType sourceType{ eLootSourceType::NONE }; // optional
};
// Server -> client, to one client.
struct SetCurrency : public NetGameMsg {
SetCurrency() : NetGameMsg(MessageType::Game::SET_CURRENCY) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
int64_t currency{};
int32_t lootType{ LOOTTYPE_NONE }; // optional
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 };
eLootSourceType sourceType{ eLootSourceType::NONE }; // optional
};
// Server -> client, to one client.
struct UpdateReputation : public NetGameMsg {
UpdateReputation() : NetGameMsg(MessageType::Game::UPDATE_REPUTATION) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
int64_t reputation{};
};
// Server -> client, broadcast.
struct ToggleGMInvis : public NetGameMsg {
ToggleGMInvis() : NetGameMsg(MessageType::Game::TOGGLE_GM_INVIS) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
bool bStateOut{ false };
};
using ToggleGMInvisEvent = NetGameMsgEvent<ToggleGMInvis>;
// Client -> server. DLU reads only the amount; lu_packets has a position after it.
struct PickupCurrency : public NetGameMsg {
PickupCurrency() : NetGameMsg(MessageType::Game::PICKUP_CURRENCY) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
uint32_t currency{};
};
// Client -> server.
struct ModifyPlayerZoneStatistic : public NetGameMsg {
ModifyPlayerZoneStatistic() : NetGameMsg(MessageType::Game::MODIFY_PLAYER_ZONE_STATISTIC) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
bool bSet{ false };
std::u16string statName{};
int32_t statValue{ 0 }; // optional
LWOMAPID zoneID{ LWOMAPID_INVALID }; // optional
};
// 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 {
UpdatePlayerStatistic() : NetGameMsg(MessageType::Game::UPDATE_PLAYER_STATISTIC) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
int32_t updateID{};
int64_t updateValue{ 1 }; // optional
};
// Client -> server. Chat text the client parses as a command (starts with '/').
struct ParseChatMessage : public NetGameMsg {
ParseChatMessage() : NetGameMsg(MessageType::Game::PARSE_CHAT_MESSAGE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
int32_t iClientState{};
std::u16string wsString{};
};
// Client -> server. A bug report, or a player report when otherPlayerID holds a player's ID.
struct ReportBug : public NetGameMsg {
ReportBug() : NetGameMsg(MessageType::Game::REPORT_BUG) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
std::u16string body{};
std::string clientVersion{};
std::string otherPlayerID{};
std::string selection{};
};
// Client -> server. Nothing is done with it.
struct VerifyAck : public NetGameMsg {
VerifyAck() : NetGameMsg(MessageType::Game::VERIFY_ACK) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
bool bDifferent{ false };
std::string sBitStream{};
uint32_t uiHandle{ 0 }; // optional
};
// Server -> client, to the player. Shows a one-time tutorial tooltip (LWOCharacterComponent::ShowHelp); the
// client skips it once the player flag with the same ID is set, and sets that flag itself.
// Serialize 0x00dc51e0 / Deserialize 0x00dc5220: a single int32.
struct Help : public NetGameMsg {
Help() : NetGameMsg(MessageType::Game::HELP) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
eHelpType helpId{ eHelpType::NONE };
};
}
#endif // PLAYERMESSAGES_H