chore: foundations for struct based packets and game messages

First step of moving hand written bitstream code to struct based
messages (see docs/PacketArchitecture.md). No wire changes.

- GameMsg is now only a server-internal event (delivered to handlers
  registered with RegisterMsg); NetGameMsg is a wire message with
  Send(sysAddr) (UNASSIGNED broadcasts), SendToClient(sysAddr) (one
  client, never broadcasts), WritePacket, Serialize, Deserialize and
  Handle. Mixing them up is now a compile error. NetGameMsgEvent<T> /
  DeliverLocally carry a wire message to local handlers; loot drops,
  pickup, the object debugger, GM invisibility and model RequestUse
  use them. The GM invisibility message keeps its target, so its bytes
  are unchanged.
- Behaviour change: GameMessageHandler logs and drops messages whose
  Deserialize fails instead of handling them with default fields (the
  4 messages already registered: RequestUse, RequestServerObjectInfo,
  ShootingGalleryFire, PickupItem).
- LUBitStream (kept as on main) gets a virtual destructor (Mail deleted
  derived packets through the base) and WritePacket, also used by
  ChatPackets::SendRoutedMsg with identical bytes.
- BitStreamUtils::WriteOptional/ReadOptional for default-flag fields
  and WriteLengthPrefixed/ReadLengthPrefixed for length prefixed
  strings.
- Every message ID enumerator (MessageType::*, ServiceType, Mail's
  wire enums) is pinned with static_asserts, so renumbering or removing
  one fails to compile.
- Tests: dServerMock copies each sent packet (it kept a pointer to the
  caller's destroyed BitStream); PacketTestUtils.h compares packets bit
  for bit; helper tests use hand computed golden bytes and equality
  with the hand written patterns they replace; compile-time checks
  keep the wire/internal split in place.
- docs/PacketArchitecture.md: survey, target architecture,
  conventions, verification method and migration plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 10:25:27 -05:00
parent 6f3a8d502f
commit 5f60900934
27 changed files with 2796 additions and 81 deletions

View File

@@ -43,7 +43,7 @@
namespace {
using enum MessageType::Game;
using namespace GameMessages;
using MessageCreator = std::function<std::unique_ptr<GameMessages::GameMsg>()>;
using MessageCreator = std::function<std::unique_ptr<GameMessages::NetGameMsg>()>;
std::map<MessageType::Game, MessageCreator> g_MessageHandlers = {
{ REQUEST_USE, []() { return std::make_unique<RequestUse>(); }},
{ REQUEST_SERVER_OBJECT_INFO, []() { return std::make_unique<RequestServerObjectInfo>(); } },
@@ -87,7 +87,10 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System
}
}
msg->Deserialize(inStream);
if (!msg->Deserialize(inStream)) {
LOG("Dropping malformed GM %4i, %s from (%llu) targeting (%llu)", messageID, StringifiedEnum::ToString(messageID).data(), usr->GetLoggedInChar(), objectID);
return;
}
msg->Handle(*entity, sysAddr);
return;
}

View File

@@ -6285,14 +6285,24 @@ namespace GameMessages {
return Send();
}
void GameMsg::Send(const SystemAddress& sysAddr) const {
CBITSTREAM;
void NetGameMsg::WritePacket(RakNet::BitStream& bitStream) const {
CMSGHEADER;
bitStream.Write(target); // Who this message will be sent to on the (a) client
bitStream.Write(msgId); // the ID of this message
Serialize(bitStream); // write the message data
}
void NetGameMsg::SendToClient(const SystemAddress& sysAddr) const {
CBITSTREAM;
WritePacket(bitStream);
SEND_PACKET;
}
void NetGameMsg::Send(const SystemAddress& sysAddr) const {
CBITSTREAM;
WritePacket(bitStream);
// Send to everyone if someone sent unassigned system address, or to one specific client.
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) {
@@ -6378,8 +6388,10 @@ namespace GameMessages {
void RequestServerObjectInfo::Handle(Entity& entity, const SystemAddress& sysAddr) {
auto* handlingEntity = Game::entityManager->GetEntity(targetForReport);
if (handlingEntity) handlingEntity->HandleMsg(*this);
else LOG("Failed to find target %llu", targetForReport);
if (handlingEntity) {
RequestServerObjectInfoEvent event(*this);
handlingEntity->HandleMsg(event);
} else LOG("Failed to find target %llu", targetForReport);
}
bool RequestUse::Deserialize(RakNet::BitStream& stream) {
@@ -6418,7 +6430,8 @@ namespace GameMessages {
interactedObject->OnUse(&entity);
}
interactedObject->HandleMsg(*this);
RequestUseEvent event(*this);
interactedObject->HandleMsg(event);
//Perform use task if possible:
auto missionComponent = entity.GetComponent<MissionComponent>();
@@ -6480,7 +6493,8 @@ namespace GameMessages {
LOG("Has team %i picking up %llu:%llu", team != nullptr, lootID, lootOwnerID);
if (team) {
for (const auto memberId : team->members) {
this->Send(memberId);
PickupItemEvent event(*this);
event.Send(memberId);
TeamPickupItem teamPickupMsg{};
teamPickupMsg.target = lootID;
teamPickupMsg.lootID = lootID;

View File

@@ -3,6 +3,7 @@
#include "dCommonVars.h"
#include <map>
#include <type_traits>
#include <string>
#include <vector>
#include "eMovementPlatformState.h"
@@ -53,26 +54,78 @@ enum class eCameraTargetCyclingMode : int32_t {
};
namespace GameMessages {
/**
* A server-internal event, delivered only to the handlers entities/components/scripts registered with
* RegisterMsg. It never goes on the wire: it has no Serialize and no way to send it to a client.
*/
struct GameMsg {
GameMsg(MessageType::Game gmId, eGameMasterLevel lvl) : msgId{ gmId }, requiredGmLevel{ lvl } {}
GameMsg(MessageType::Game gmId) : GameMsg(gmId, eGameMasterLevel::CIVILIAN) {}
GameMsg(MessageType::Game gmId) : msgId{ gmId } {}
virtual ~GameMsg() = default;
// Sends a message to the entity manager to route to the target
// Delivers the message to the handlers registered on the target entity.
bool Send();
bool Send(const LWOOBJID _target);
MessageType::Game msgId;
LWOOBJID target{ LWOOBJID_EMPTY };
};
/**
* A game message that goes on the wire, to (Serialize) or from (Deserialize + Handle) a client.
* It is not a GameMsg, so it cannot be delivered to local handlers by mistake. When local handlers need to
* observe a network message, wrap it in a NetGameMsgEvent.
*/
struct NetGameMsg {
NetGameMsg(MessageType::Game gmId, eGameMasterLevel lvl) : msgId{ gmId }, requiredGmLevel{ lvl } {}
NetGameMsg(MessageType::Game gmId) : NetGameMsg(gmId, eGameMasterLevel::CIVILIAN) {}
virtual ~NetGameMsg() = default;
// Sends the message to the specified client or
// all clients if UNASSIGNED_SYSTEM_ADDRESS is specified
void Send(const SystemAddress& sysAddr) const;
// Sends the message to the specified client only and never broadcasts. Given UNASSIGNED_SYSTEM_ADDRESS
// nothing is delivered (RakNet rejects a non-broadcast send without an address).
void SendToClient(const SystemAddress& sysAddr) const;
// Writes the complete client packet (CLIENT/GAME_MSG header, target, msgId, then Serialize()) into bitStream.
// This is exactly what Send(sysAddr) puts on the wire; tests use it to compare bytes without a server.
void WritePacket(RakNet::BitStream& bitStream) const;
virtual void Serialize(RakNet::BitStream& bitStream) const {}
virtual bool Deserialize(RakNet::BitStream& bitStream) { return true; }
// Called for messages received from a client, after a successful Deserialize.
virtual void Handle(Entity& entity, const SystemAddress& sysAddr) {};
MessageType::Game msgId;
LWOOBJID target{ LWOOBJID_EMPTY };
// Minimum GM level the sending client needs for Handle to be called.
eGameMasterLevel requiredGmLevel;
};
/**
* A server-internal event carrying a network message, so entity/component handlers can react to a message
* received from (or about to be sent to) a client. Uses the network message's msgId for local dispatch.
*/
template<typename Msg>
struct NetGameMsgEvent : public GameMsg {
static_assert(std::is_base_of_v<NetGameMsg, Msg>, "NetGameMsgEvent carries a NetGameMsg");
NetGameMsgEvent() : GameMsg(Msg{}.msgId) {}
NetGameMsgEvent(const Msg& message) : GameMsg(message.msgId), msg{ message } { target = message.target; }
Msg msg;
};
// Delivers a copy of a network message to the local handlers registered on msg.target (never to a client).
// Returns whether a handler handled it. Handlers see (and may modify) only the copy.
template<typename Msg>
bool DeliverLocally(const Msg& msg) {
NetGameMsgEvent<Msg> event(msg);
return event.Send();
}
class PropertyDataMessage;
void SendFireEventClientSide(const LWOOBJID& objectID, const SystemAddress& sysAddr, std::u16string args, const LWOOBJID& object, int64_t param1, int param2, const LWOOBJID& sender);
void SendTeleport(const LWOOBJID& objectID, const NiPoint3& pos, const NiQuaternion& rot, const SystemAddress& sysAddr, bool bSetRotation = false);
@@ -705,8 +758,8 @@ namespace GameMessages {
// This is a client gm however its default values are exactly what we need to get around the invisible inventory item issues.
void SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress& sysAddr);
struct DisplayTooltip : public GameMsg {
DisplayTooltip() : GameMsg(MessageType::Game::DISPLAY_TOOLTIP) {}
struct DisplayTooltip : public NetGameMsg {
DisplayTooltip() : NetGameMsg(MessageType::Game::DISPLAY_TOOLTIP) {}
bool doOrDie{};
bool noRepeat{};
bool noRevive{};
@@ -721,8 +774,8 @@ namespace GameMessages {
void Serialize(RakNet::BitStream& bitStream) const override;
};
struct UseItemOnClient : public GameMsg {
UseItemOnClient() : GameMsg(MessageType::Game::USE_ITEM_ON_CLIENT) {}
struct UseItemOnClient : public NetGameMsg {
UseItemOnClient() : NetGameMsg(MessageType::Game::USE_ITEM_ON_CLIENT) {}
LWOOBJID playerId{};
LWOOBJID itemToUse{};
uint32_t itemType{};
@@ -741,14 +794,14 @@ namespace GameMessages {
LwoNameValue racingSettings{};
};
struct SetModelToBuild : public GameMsg {
SetModelToBuild() : GameMsg(MessageType::Game::SET_MODEL_TO_BUILD) {}
struct SetModelToBuild : public NetGameMsg {
SetModelToBuild() : NetGameMsg(MessageType::Game::SET_MODEL_TO_BUILD) {}
void Serialize(RakNet::BitStream& bitStream) const override;
LOT modelLot{ -1 };
};
struct SpawnModelBricks : public GameMsg {
SpawnModelBricks() : GameMsg(MessageType::Game::SPAWN_MODEL_BRICKS) {}
struct SpawnModelBricks : public NetGameMsg {
SpawnModelBricks() : NetGameMsg(MessageType::Game::SPAWN_MODEL_BRICKS) {}
void Serialize(RakNet::BitStream& bitStream) const override;
float amount{ 0.0f };
@@ -761,8 +814,8 @@ namespace GameMessages {
LwoNameValue notification{};
};
struct ShootingGalleryFire : public GameMsg {
ShootingGalleryFire() : GameMsg(MessageType::Game::SHOOTING_GALLERY_FIRE) {}
struct ShootingGalleryFire : public NetGameMsg {
ShootingGalleryFire() : NetGameMsg(MessageType::Game::SHOOTING_GALLERY_FIRE) {}
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
@@ -781,15 +834,16 @@ namespace GameMessages {
PlayerResurrectionFinished() : GameMsg(MessageType::Game::PLAYER_RESURRECTION_FINISHED) {}
};
struct RequestServerObjectInfo : public GameMsg {
struct RequestServerObjectInfo : public NetGameMsg {
bool bVerbose{};
LWOOBJID clientId{};
LWOOBJID targetForReport{};
RequestServerObjectInfo() : GameMsg(MessageType::Game::REQUEST_SERVER_OBJECT_INFO, eGameMasterLevel::DEVELOPER) {}
RequestServerObjectInfo() : NetGameMsg(MessageType::Game::REQUEST_SERVER_OBJECT_INFO, eGameMasterLevel::DEVELOPER) {}
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
};
using RequestServerObjectInfoEvent = NetGameMsgEvent<RequestServerObjectInfo>;
struct GetObjectReportInfo : public GameMsg {
AMFArrayValue* info{};
@@ -797,11 +851,11 @@ namespace GameMessages {
bool bVerbose{};
LWOOBJID clientID{};
GetObjectReportInfo() : GameMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, eGameMasterLevel::DEVELOPER) {}
GetObjectReportInfo() : GameMsg(MessageType::Game::GET_OBJECT_REPORT_INFO) {}
};
struct RequestUse : public GameMsg {
RequestUse() : GameMsg(MessageType::Game::REQUEST_USE) {}
struct RequestUse : public NetGameMsg {
RequestUse() : NetGameMsg(MessageType::Game::REQUEST_USE) {}
bool Deserialize(RakNet::BitStream& stream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
@@ -819,11 +873,12 @@ namespace GameMessages {
// Used only for multi-interaction, is of the enum type InteractionType
int multiInteractType{};
};
using RequestUseEvent = NetGameMsgEvent<RequestUse>;
struct Smash : public GameMsg {
Smash() : GameMsg(MessageType::Game::SMASH) {}
struct Smash : public NetGameMsg {
Smash() : NetGameMsg(MessageType::Game::SMASH) {}
void Serialize(RakNet::BitStream& stream) const;
void Serialize(RakNet::BitStream& stream) const override;
bool bIgnoreObjectVisibility{};
bool force{};
@@ -831,19 +886,19 @@ namespace GameMessages {
LWOOBJID killerID{};
};
struct UnSmash : public GameMsg {
UnSmash() : GameMsg(MessageType::Game::UN_SMASH) {}
struct UnSmash : public NetGameMsg {
UnSmash() : NetGameMsg(MessageType::Game::UN_SMASH) {}
void Serialize(RakNet::BitStream& stream) const;
void Serialize(RakNet::BitStream& stream) const override;
LWOOBJID builderID{ LWOOBJID_EMPTY };
float duration{ 3.0f };
};
struct PlayBehaviorSound : public GameMsg {
PlayBehaviorSound() : GameMsg(MessageType::Game::PLAY_BEHAVIOR_SOUND) {}
struct PlayBehaviorSound : public NetGameMsg {
PlayBehaviorSound() : NetGameMsg(MessageType::Game::PLAY_BEHAVIOR_SOUND) {}
void Serialize(RakNet::BitStream& stream) const;
void Serialize(RakNet::BitStream& stream) const override;
int32_t soundID{ -1 };
};
@@ -857,8 +912,8 @@ namespace GameMessages {
bool bResetBehaviors{ true };
};
struct EmotePlayed : public GameMsg {
EmotePlayed() : GameMsg(MessageType::Game::EMOTE_PLAYED), emoteID(0), targetID(0) {}
struct EmotePlayed : public NetGameMsg {
EmotePlayed() : NetGameMsg(MessageType::Game::EMOTE_PLAYED), emoteID(0), targetID(0) {}
void Serialize(RakNet::BitStream& stream) const override;
@@ -880,8 +935,8 @@ namespace GameMessages {
bool bIgnoreChecks{ false };
};
struct DropClientLoot : public GameMsg {
DropClientLoot() : GameMsg(MessageType::Game::DROP_CLIENT_LOOT) {}
struct DropClientLoot : public NetGameMsg {
DropClientLoot() : NetGameMsg(MessageType::Game::DROP_CLIENT_LOOT) {}
void Serialize(RakNet::BitStream& stream) const override;
LWOOBJID sourceID{ LWOOBJID_EMPTY };
@@ -894,6 +949,7 @@ namespace GameMessages {
LWOOBJID lootID{ LWOOBJID_EMPTY };
LWOOBJID ownerID{ LWOOBJID_EMPTY };
};
using DropClientLootEvent = NetGameMsgEvent<DropClientLoot>;
struct GetMissionState : public GameMsg {
GetMissionState() : GameMsg(MessageType::Game::GET_MISSION_STATE) {}
@@ -923,17 +979,18 @@ namespace GameMessages {
LOT item{};
};
struct PickupItem : public GameMsg {
PickupItem() : GameMsg(MessageType::Game::PICKUP_ITEM) {}
struct PickupItem : public NetGameMsg {
PickupItem() : NetGameMsg(MessageType::Game::PICKUP_ITEM) {}
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
bool Deserialize(RakNet::BitStream& stream) override;
LWOOBJID lootID{};
LWOOBJID lootOwnerID{};
};
using PickupItemEvent = NetGameMsgEvent<PickupItem>;
struct TeamPickupItem : public GameMsg {
TeamPickupItem() : GameMsg(MessageType::Game::TEAM_PICKUP_ITEM) {}
struct TeamPickupItem : public NetGameMsg {
TeamPickupItem() : NetGameMsg(MessageType::Game::TEAM_PICKUP_ITEM) {}
void Serialize(RakNet::BitStream& stream) const override;
LWOOBJID lootID{};
@@ -946,13 +1003,14 @@ namespace GameMessages {
bool bDead{};
};
struct ToggleGMInvis : public GameMsg {
ToggleGMInvis() : GameMsg(MessageType::Game::TOGGLE_GM_INVIS) {}
struct ToggleGMInvis : public NetGameMsg {
ToggleGMInvis() : NetGameMsg(MessageType::Game::TOGGLE_GM_INVIS) {}
void Serialize(RakNet::BitStream& stream) const override;
bool bStateOut{ false };
};
using ToggleGMInvisEvent = NetGameMsgEvent<ToggleGMInvis>;
struct GetGMInvis : public GameMsg {
GetGMInvis() : GameMsg(MessageType::Game::GET_GM_INVIS) {}
@@ -966,8 +1024,8 @@ namespace GameMessages {
LWOOBJID childID{};
};
struct UseSkillSet : public GameMsg {
UseSkillSet() : GameMsg(MessageType::Game::USE_SKILL_SET) {}
struct UseSkillSet : public NetGameMsg {
UseSkillSet() : NetGameMsg(MessageType::Game::USE_SKILL_SET) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool bRemove{};