refactor: activity game messages as structs

Converts ActivityEnter, ActivityExit, ActivityStart, ActivityStop,
ActivityPause, StartActivityTime, RequestActivityEnter,
RequestActivityExit and ShowActivityCountdown to NetGameMsgs in
ActivityMessages.{h,cpp}, switches their callers (racing, shooting
gallery, survival, waves, AG course, NT combat challenge) and deletes
the old GameMessages::Send* / HandleRequestActivityExit functions.
REQUEST_ACTIVITY_EXIT is registered in GameMessageHandler's map.

No wire change. Verified by comparing every struct byte for byte with a
frozen verbatim copy of the old functions (tests/.../Legacy) over a
grid of inputs, both to one client and as a broadcast (a temporary test
proved the copy matched production before it was deleted), plus hand
computed golden bytes, round trips and a deliberate field mutation that
made the tests fail. Layouts confirmed against the 1.10.64 client in
Ghidra. Only difference: a broadcast no longer makes
the extra Send(UNASSIGNED, false) that RakNet already rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 10:32:32 -05:00
parent ac858dd1e2
commit 7fb75f0484
15 changed files with 589 additions and 145 deletions

View File

@@ -0,0 +1,82 @@
#include "ActivityMessages.h"
#include "BitStreamUtils.h"
#include "Entity.h"
#include "EntityManager.h"
#include "Game.h"
namespace GameMessages {
void ActivityStop::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bExit);
bitStream.Write(bUserCancel);
}
bool ActivityStop::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bExit));
VALIDATE_READ(bitStream.Read(bUserCancel));
return true;
}
void ActivityPause::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bPause);
}
bool ActivityPause::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bPause));
return true;
}
void StartActivityTime::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(startTime);
}
bool StartActivityTime::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(startTime));
return true;
}
void RequestActivityEnter::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bStart);
bitStream.Write(userID);
}
bool RequestActivityEnter::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bStart));
VALIDATE_READ(bitStream.Read(userID));
return true;
}
void RequestActivityExit::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bUserCancel);
bitStream.Write(userID);
}
bool RequestActivityExit::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bUserCancel));
VALIDATE_READ(bitStream.Read(userID));
return true;
}
void RequestActivityExit::Handle(Entity& entity, const SystemAddress& sysAddr) {
if (!bUserCancel) return;
auto* player = Game::entityManager->GetEntity(userID);
if (!player) return;
entity.RequestActivityExit(&entity, userID, bUserCancel);
}
void ShowActivityCountdown::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bPlayAdditionalSound);
bitStream.Write(bPlayCountdownSound);
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, sndName);
bitStream.Write(stateToPlaySoundOn);
}
bool ShowActivityCountdown::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bPlayAdditionalSound));
VALIDATE_READ(bitStream.Read(bPlayCountdownSound));
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(bitStream, sndName));
VALIDATE_READ(bitStream.Read(stateToPlaySoundOn));
return true;
}
}

View File

@@ -0,0 +1,88 @@
#ifndef ACTIVITYMESSAGES_H
#define ACTIVITYMESSAGES_H
#include "GameMessages.h"
#include <string>
// Game messages for the activity lifecycle (minigames, races, shooting galleries, survival...).
// Field names and wire order match the client (legouniverse.exe 1.10.64). Fields are listed in wire order.
namespace GameMessages {
// Server -> client. No payload.
struct ActivityEnter : public NetGameMsg {
ActivityEnter() : NetGameMsg(MessageType::Game::ACTIVITY_ENTER) {}
};
// Server -> client. No payload.
struct ActivityExit : public NetGameMsg {
ActivityExit() : NetGameMsg(MessageType::Game::ACTIVITY_EXIT) {}
};
// Server -> client. No payload.
struct ActivityStart : public NetGameMsg {
ActivityStart() : NetGameMsg(MessageType::Game::ACTIVITY_START) {}
};
// Server -> client.
struct ActivityStop : public NetGameMsg {
ActivityStop() : NetGameMsg(MessageType::Game::ACTIVITY_STOP) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
bool bExit{};
bool bUserCancel{};
};
// Server -> client.
struct ActivityPause : public NetGameMsg {
ActivityPause() : NetGameMsg(MessageType::Game::ACTIVITY_PAUSE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
bool bPause{};
};
// Server -> client.
struct StartActivityTime : public NetGameMsg {
StartActivityTime() : NetGameMsg(MessageType::Game::START_ACTIVITY_TIME) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
float startTime{};
};
// Client -> server in the client, but historically also sent by DLU. Kept for completeness.
struct RequestActivityEnter : public NetGameMsg {
RequestActivityEnter() : NetGameMsg(MessageType::Game::REQUEST_ACTIVITY_ENTER) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
bool bStart{};
LWOOBJID userID{};
};
// Client -> server. Sent when the player leaves an activity (e.g. closes the minigame UI).
struct RequestActivityExit : public NetGameMsg {
RequestActivityExit() : NetGameMsg(MessageType::Game::REQUEST_ACTIVITY_EXIT) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
bool bUserCancel{};
LWOOBJID userID{};
};
// Server -> client.
struct ShowActivityCountdown : public NetGameMsg {
ShowActivityCountdown() : NetGameMsg(MessageType::Game::SHOW_ACTIVITY_COUNTDOWN) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
bool bPlayAdditionalSound{};
bool bPlayCountdownSound{};
std::u16string sndName{};
int32_t stateToPlaySoundOn{};
};
};
#endif // ACTIVITYMESSAGES_H

View File

@@ -1,4 +1,5 @@
set(DGAME_DGAMEMESSAGES_SOURCES
"ActivityMessages.cpp"
"GameMessageHandler.cpp"
"GameMessages.cpp"
"PropertyDataMessage.cpp"

View File

@@ -30,6 +30,7 @@
#include "StartSkill.h"
#include "EchoStartSkill.h"
#include "EchoSyncSkill.h"
#include "ActivityMessages.h"
#include "eMissionTaskType.h"
#include "eReplicaComponentType.h"
#include "ServiceType.h"
@@ -49,6 +50,7 @@ namespace {
{ REQUEST_SERVER_OBJECT_INFO, []() { return std::make_unique<RequestServerObjectInfo>(); } },
{ SHOOTING_GALLERY_FIRE, []() { return std::make_unique<ShootingGalleryFire>(); } },
{ PICKUP_ITEM, []() { return std::make_unique<PickupItem>(); } },
{ REQUEST_ACTIVITY_EXIT, []() { return std::make_unique<RequestActivityExit>(); } },
};
};
@@ -718,9 +720,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System
case MessageType::Game::ZONE_SUMMARY_DISMISSED:
GameMessages::HandleZoneSummaryDismissed(inStream, entity);
break;
case MessageType::Game::REQUEST_ACTIVITY_EXIT:
GameMessages::HandleRequestActivityExit(inStream, entity);
break;
case MessageType::Game::ADD_DONATION_ITEM:
GameMessages::HandleAddDonationItem(inStream, entity, sysAddr);
break;

View File

@@ -1460,43 +1460,6 @@ void GameMessages::SendRequestActivitySummaryLeaderboardData(const LWOOBJID& obj
SEND_PACKET;
}
void GameMessages::SendActivityPause(LWOOBJID objectId, bool pause, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::ACTIVITY_PAUSE);
bitStream.Write(pause);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
void GameMessages::SendStartActivityTime(LWOOBJID objectId, float_t startTime, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::START_ACTIVITY_TIME);
bitStream.Write<float_t>(startTime);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
void GameMessages::SendRequestActivityEnter(LWOOBJID objectId, const SystemAddress& sysAddr, bool bStart, LWOOBJID userID) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::REQUEST_ACTIVITY_ENTER);
bitStream.Write<bool>(bStart);
bitStream.Write<LWOOBJID>(userID);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
void GameMessages::NotifyLevelRewards(LWOOBJID objectID, const SystemAddress& sysAddr, int level, bool sending_rewards) {
CBITSTREAM;
CMSGHEADER;
@@ -4394,55 +4357,8 @@ void GameMessages::SendNotifyRacingClient(LWOOBJID objectId, int32_t eventType,
}
void GameMessages::SendActivityEnter(LWOOBJID objectId, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::ACTIVITY_ENTER);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
void GameMessages::SendActivityStart(LWOOBJID objectId, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::ACTIVITY_START);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
void GameMessages::SendActivityExit(LWOOBJID objectId, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::ACTIVITY_EXIT);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
void GameMessages::SendActivityStop(LWOOBJID objectId, bool bExit, bool bUserCancel, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::ACTIVITY_STOP);
bitStream.Write(bExit);
bitStream.Write(bUserCancel);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
void GameMessages::SendVehicleAddPassiveBoostAction(LWOOBJID objectId, const SystemAddress& sysAddr) {
@@ -4603,28 +4519,6 @@ void GameMessages::SendResponseMoveItemBetweenInventoryTypes(LWOOBJID objectId,
}
void GameMessages::SendShowActivityCountdown(LWOOBJID objectId, bool bPlayAdditionalSound, bool bPlayCountdownSound, std::u16string sndName, int32_t stateToPlaySoundOn, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::SHOW_ACTIVITY_COUNTDOWN);
bitStream.Write(bPlayAdditionalSound);
bitStream.Write(bPlayCountdownSound);
bitStream.Write<uint32_t>(sndName.size());
for (auto character : sndName) {
bitStream.Write(character);
}
bitStream.Write(stateToPlaySoundOn);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
//-----------------------------------------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------- Handlers ------------------------------------------------------------------
@@ -6061,18 +5955,6 @@ void GameMessages::SendShowBillboardInteractIcon(const SystemAddress& sysAddr, L
else SEND_PACKET
}
void GameMessages::HandleRequestActivityExit(RakNet::BitStream& inStream, Entity* entity) {
bool canceled = false;
inStream.Read(canceled);
if (!canceled) return;
LWOOBJID player_id = LWOOBJID_EMPTY;
inStream.Read(player_id);
auto player = Game::entityManager->GetEntity(player_id);
if (!entity || !player) return;
entity->RequestActivityExit(entity, player_id, canceled);
}
void GameMessages::HandleAddDonationItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) {
uint32_t count = 1;
bool hasCount = false;

View File

@@ -618,13 +618,6 @@ namespace GameMessages {
void SendNotifyRacingClient(LWOOBJID objectId, int32_t eventType, int32_t param1, LWOOBJID paramObj, std::u16string paramStr, LWOOBJID singleClient, const SystemAddress& sysAddr);
void SendActivityEnter(LWOOBJID objectId, const SystemAddress& sysAddr);
void SendActivityStart(LWOOBJID objectId, const SystemAddress& sysAddr);
void SendActivityExit(LWOOBJID objectId, const SystemAddress& sysAddr);
void SendActivityStop(LWOOBJID objectId, bool bExit, bool bUserCancel, const SystemAddress& sysAddr);
void SendActivityPause(LWOOBJID objectId, bool pause = false, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS);
void SendStartActivityTime(LWOOBJID objectId, float_t startTime, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS);
void SendRequestActivityEnter(LWOOBJID objectId, const SystemAddress& sysAddr, bool bStart, LWOOBJID userID);
void SendUseItemRequirementsResponse(LWOOBJID objectID, const SystemAddress& sysAddr, eUseItemResponse itemResponse);
// SG:
@@ -675,8 +668,6 @@ namespace GameMessages {
void HandleRequestMoveItemBetweenInventoryTypes(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr);
void SendResponseMoveItemBetweenInventoryTypes(LWOOBJID objectId, const SystemAddress& sysAddr, eInventoryType inventoryTypeDestination, eInventoryType inventoryTypeSource, eReponseMoveItemBetweenInventoryTypeCode response);
void SendShowActivityCountdown(LWOOBJID objectId, bool bPlayAdditionalSound, bool bPlayCountdownSound, std::u16string sndName, int32_t stateToPlaySoundOn, const SystemAddress& sysAddr);
//Handlers:
void HandleToggleGhostReferenceOverride(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr);
@@ -741,7 +732,6 @@ namespace GameMessages {
void SendDeactivateBubbleBuffFromServer(LWOOBJID objectId, const SystemAddress& sysAddr);
void HandleZoneSummaryDismissed(RakNet::BitStream& inStream, Entity* entity);
void HandleRequestActivityExit(RakNet::BitStream& inStream, Entity* entity);
// Donation vendor
void HandleAddDonationItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr);