Extract WorldStateSync from NetworkManager

Move world state synchronization logic (snapshots, events, entity
mutation routing) into a dedicated WorldStateSync class, reducing
NetworkManager from ~790 to ~420 lines.
This commit is contained in:
Christian Semmler 2026-03-01 10:12:24 -08:00
parent a0629c45a1
commit c760db50a9
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
9 changed files with 478 additions and 412 deletions

View File

@ -536,6 +536,7 @@ if (ISLE_EXTENSIONS)
extensions/src/multiplayer/networkmanager.cpp
extensions/src/multiplayer/remoteplayer.cpp
extensions/src/multiplayer/websockettransport.cpp
extensions/src/multiplayer/worldstatesync.cpp
)
endif()

View File

@ -14,7 +14,7 @@ class LegoCacheSound;
class LegoPathBoundary;
namespace Multiplayer
{
class NetworkManager;
class WorldStateSync;
}
// SIZE 0x2c
@ -102,7 +102,7 @@ class LegoBuildingManager : public MxCore {
// LegoBuildingManager::`scalar deleting destructor'
private:
friend class Multiplayer::NetworkManager;
friend class Multiplayer::WorldStateSync;
static char* g_customizeAnimFile;
static MxS32 g_maxMove[16];

View File

@ -13,7 +13,7 @@ class LegoStorage;
class LegoWorld;
namespace Multiplayer
{
class NetworkManager;
class WorldStateSync;
}
// VTABLE: LEGO1 0x100d6758
@ -71,7 +71,7 @@ class LegoPlantManager : public MxCore {
// LegoPlantManager::`scalar deleting destructor'
private:
friend class Multiplayer::NetworkManager;
friend class Multiplayer::WorldStateSync;
void RemovePlant(MxS32 p_index, LegoOmni::World p_worldId);
void AdjustHeight(MxS32 p_index);

View File

@ -484,12 +484,8 @@ MxLong LegoEntity::Notify(MxParam& p_param)
InvokeAction(m_actionType, MxAtomId(m_siFile, e_lowerCase2), m_targetEntityId, this);
}
else {
// Multiplayer extension intercept: for plants and buildings, route through
// the multiplayer system. Returns TRUE if the click should be suppressed
// locally (non-host sends a request to the host instead of applying directly).
auto intercepted =
Extensions::Extension<Extensions::MultiplayerExt>::Call(Extensions::HandleEntityNotify, this);
if (intercepted.has_value() && intercepted.value()) {
auto handled = Extensions::Extension<Extensions::MultiplayerExt>::Call(Extensions::HandleEntityNotify, this);
if (handled.has_value() && handled.value()) {
return 1;
}

View File

@ -754,6 +754,7 @@ void LegoWorld::Enable(MxBool p_enable)
#ifndef BETA10
SetIsWorldActive(TRUE);
#endif
Extensions::Extension<Extensions::MultiplayerExt>::Call(Extensions::HandleWorldEnable, this, TRUE);
}
else if (!p_enable && m_disabledObjects.size() == 0) {
@ -817,6 +818,7 @@ void LegoWorld::Enable(MxBool p_enable)
}
GetViewManager()->RemoveAll(NULL);
Extensions::Extension<Extensions::MultiplayerExt>::Call(Extensions::HandleWorldEnable, this, FALSE);
}
}

View File

@ -3,6 +3,7 @@
#include "extensions/multiplayer/networktransport.h"
#include "extensions/multiplayer/protocol.h"
#include "extensions/multiplayer/remoteplayer.h"
#include "extensions/multiplayer/worldstatesync.h"
#include "mxcore.h"
#include "mxtypes.h"
@ -59,31 +60,18 @@ class NetworkManager : public MxCore {
void HandleLeave(const PlayerLeaveMsg& p_msg);
void HandleState(const PlayerStateMsg& p_msg);
void HandleHostAssign(const HostAssignMsg& p_msg);
void HandleRequestSnapshot(const RequestSnapshotMsg& p_msg);
void HandleWorldSnapshot(const uint8_t* p_data, size_t p_length);
void HandleWorldEvent(const WorldEventMsg& p_msg);
void HandleWorldEventRequest(const WorldEventRequestMsg& p_msg);
void RemoveRemotePlayer(uint32_t p_peerId);
void RemoveAllRemotePlayers();
int8_t DetectLocalVehicleType();
bool IsInIsleWorld() const;
// Serialize and send a fixed-size message via the transport
template <typename T>
void SendMessage(const T& p_msg);
// World state sync helpers
void SendSnapshotRequest();
void SendWorldSnapshot(uint32_t p_targetPeerId);
void BroadcastWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex);
void SendWorldEventRequest(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex);
// Apply a world event mutation locally (for both host and receiving peers)
void ApplyWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex);
NetworkTransport* m_transport;
WorldStateSync m_worldSync;
std::map<uint32_t, std::unique_ptr<RemotePlayer>> m_remotePlayers;
uint32_t m_localPeerId;
@ -93,10 +81,6 @@ class NetworkManager : public MxCore {
uint8_t m_lastValidActorId;
bool m_inIsleWorld;
bool m_registered;
bool m_snapshotRequested;
// Queue world events that arrive between snapshot request and response
std::vector<WorldEventMsg> m_pendingWorldEvents;
static const uint32_t BROADCAST_INTERVAL_MS = 66; // ~15Hz
static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout

View File

@ -0,0 +1,56 @@
#pragma once
#include "extensions/multiplayer/networktransport.h"
#include "extensions/multiplayer/protocol.h"
#include "mxtypes.h"
#include <cstdint>
#include <vector>
class LegoEntity;
namespace Multiplayer
{
class WorldStateSync {
public:
WorldStateSync();
void SetTransport(NetworkTransport* p_transport) { m_transport = p_transport; }
void SetLocalPeerId(uint32_t p_peerId) { m_localPeerId = p_peerId; }
void SetHost(bool p_isHost) { m_isHost = p_isHost; }
void SetInIsleWorld(bool p_inIsle) { m_inIsleWorld = p_inIsle; }
// Called when the host peer changes. Requests a snapshot if we're not host.
void OnHostChanged();
// Incoming message handlers (called from NetworkManager::ProcessIncomingPackets)
void HandleRequestSnapshot(const RequestSnapshotMsg& p_msg);
void HandleWorldSnapshot(const uint8_t* p_data, size_t p_length);
void HandleWorldEvent(const WorldEventMsg& p_msg);
void HandleWorldEventRequest(const WorldEventRequestMsg& p_msg);
// Called from multiplayer extension when a plant/building entity is clicked.
// Returns TRUE if the mutation should be suppressed locally (non-host).
MxBool HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType);
private:
void SendSnapshotRequest();
void SendWorldSnapshot(uint32_t p_targetPeerId);
void BroadcastWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex);
void SendWorldEventRequest(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex);
void ApplyWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex);
template <typename T>
void SendMessage(const T& p_msg);
NetworkTransport* m_transport;
uint32_t m_localPeerId;
uint32_t m_sequence;
bool m_isHost;
bool m_inIsleWorld;
bool m_snapshotRequested;
std::vector<WorldEventMsg> m_pendingWorldEvents;
};
} // namespace Multiplayer

View File

@ -1,14 +1,9 @@
#include "extensions/multiplayer/networkmanager.h"
#include "legobuildingmanager.h"
#include "legoentity.h"
#include "legomain.h"
#include "legopathactor.h"
#include "legoplantmanager.h"
#include "legoplants.h"
#include "legoworld.h"
#include "misc.h"
#include "misc/legostorage.h"
#include "mxmisc.h"
#include "mxticklemanager.h"
#include "roi/legoroi.h"
@ -17,9 +12,6 @@
#include <SDL3/SDL_timer.h>
#include <vector>
extern MxU8 g_counters[];
extern MxU8 g_buildingInfoDownshift[];
using namespace Multiplayer;
template <typename T>
@ -38,7 +30,7 @@ void NetworkManager::SendMessage(const T& p_msg)
NetworkManager::NetworkManager()
: m_transport(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0), m_lastBroadcastTime(0),
m_lastValidActorId(0), m_inIsleWorld(false), m_registered(false), m_snapshotRequested(false)
m_lastValidActorId(0), m_inIsleWorld(false), m_registered(false)
{
}
@ -86,6 +78,7 @@ MxResult NetworkManager::Tickle()
void NetworkManager::Initialize(NetworkTransport* p_transport)
{
m_transport = p_transport;
m_worldSync.SetTransport(p_transport);
}
void NetworkManager::Shutdown()
@ -97,6 +90,7 @@ void NetworkManager::Shutdown()
m_registered = false;
}
m_transport = nullptr;
m_worldSync.SetTransport(nullptr);
}
RemoveAllRemotePlayers();
@ -135,6 +129,7 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
if (p_world->GetWorldId() == LegoOmni::e_act1) {
m_inIsleWorld = true;
m_worldSync.SetInIsleWorld(true);
for (auto& [peerId, player] : m_remotePlayers) {
if (player->IsSpawned()) {
@ -156,12 +151,18 @@ void NetworkManager::OnWorldDisabled(LegoWorld* p_world)
if (p_world->GetWorldId() == LegoOmni::e_act1) {
m_inIsleWorld = false;
m_worldSync.SetInIsleWorld(false);
for (auto& [peerId, player] : m_remotePlayers) {
player->SetVisible(false);
}
}
}
MxBool NetworkManager::HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType)
{
return m_worldSync.HandleEntityMutation(p_entity, p_changeType);
}
void NetworkManager::BroadcastLocalState()
{
if (!m_transport) {
@ -225,6 +226,7 @@ void NetworkManager::ProcessIncomingPackets()
uint32_t assignedId;
SDL_memcpy(&assignedId, data + 1, sizeof(uint32_t));
m_localPeerId = assignedId;
m_worldSync.SetLocalPeerId(assignedId);
}
break;
}
@ -262,25 +264,25 @@ void NetworkManager::ProcessIncomingPackets()
case MSG_REQUEST_SNAPSHOT: {
RequestSnapshotMsg msg;
if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_REQUEST_SNAPSHOT) {
HandleRequestSnapshot(msg);
m_worldSync.HandleRequestSnapshot(msg);
}
break;
}
case MSG_WORLD_SNAPSHOT: {
HandleWorldSnapshot(data, length);
m_worldSync.HandleWorldSnapshot(data, length);
break;
}
case MSG_WORLD_EVENT: {
WorldEventMsg msg;
if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_WORLD_EVENT) {
HandleWorldEvent(msg);
m_worldSync.HandleWorldEvent(msg);
}
break;
}
case MSG_WORLD_EVENT_REQUEST: {
WorldEventRequestMsg msg;
if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_WORLD_EVENT_REQUEST) {
HandleWorldEventRequest(msg);
m_worldSync.HandleWorldEventRequest(msg);
}
break;
}
@ -364,92 +366,13 @@ void NetworkManager::HandleHostAssign(const HostAssignMsg& p_msg)
uint32_t oldHost = m_hostPeerId;
m_hostPeerId = p_msg.hostPeerId;
// If the host changed and we're not the new host, request a snapshot.
// Reset any pending snapshot state since the old host may have disconnected
// before responding to our previous request.
m_worldSync.SetHost(IsHost());
if (!IsHost() && oldHost != m_hostPeerId) {
m_snapshotRequested = false;
m_pendingWorldEvents.clear();
SendSnapshotRequest();
m_worldSync.OnHostChanged();
}
}
void NetworkManager::HandleRequestSnapshot(const RequestSnapshotMsg& p_msg)
{
// Only the host should respond to snapshot requests
if (!IsHost()) {
return;
}
SendWorldSnapshot(p_msg.header.peerId);
}
void NetworkManager::HandleWorldSnapshot(const uint8_t* p_data, size_t p_length)
{
WorldSnapshotMsg header;
if (!DeserializeMsg(p_data, p_length, header) || header.header.type != MSG_WORLD_SNAPSHOT) {
return;
}
if (p_length < sizeof(WorldSnapshotMsg) + header.dataLength) {
return;
}
const uint8_t* snapshotData = p_data + sizeof(WorldSnapshotMsg);
// Apply the snapshot using LegoMemory with the existing Read() methods
LegoMemory memory((void*) snapshotData, header.dataLength);
PlantManager()->Read(&memory);
BuildingManager()->Read(&memory);
// If we're in the Isle world, update entity visuals after applying the snapshot.
// Read() calls AdjustHeight() which updates data arrays, but doesn't update
// entity positions. We need to reload world info to refresh visuals.
if (m_inIsleWorld) {
// Reset and reload plant entities with the new data
LegoWorld* world = CurrentWorld();
if (world && world->GetWorldId() == LegoOmni::e_act1) {
PlantManager()->Reset(LegoOmni::e_act1);
PlantManager()->LoadWorldInfo(LegoOmni::e_act1);
BuildingManager()->Reset();
BuildingManager()->LoadWorldInfo();
}
}
// Apply any world events that were queued between snapshot request and response
for (const auto& evt : m_pendingWorldEvents) {
ApplyWorldEvent(evt.entityType, evt.changeType, evt.entityIndex);
}
m_pendingWorldEvents.clear();
m_snapshotRequested = false;
}
void NetworkManager::HandleWorldEvent(const WorldEventMsg& p_msg)
{
// If we're waiting for a snapshot, queue this event for later
if (m_snapshotRequested) {
m_pendingWorldEvents.push_back(p_msg);
return;
}
ApplyWorldEvent(p_msg.entityType, p_msg.changeType, p_msg.entityIndex);
}
void NetworkManager::HandleWorldEventRequest(const WorldEventRequestMsg& p_msg)
{
// Only the host processes event requests
if (!IsHost()) {
return;
}
// Apply locally on the host
ApplyWorldEvent(p_msg.entityType, p_msg.changeType, p_msg.entityIndex);
// Broadcast to all peers as an authoritative world event
BroadcastWorldEvent(p_msg.entityType, p_msg.changeType, p_msg.entityIndex);
}
void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId)
{
auto it = m_remotePlayers.find(p_peerId);
@ -495,292 +418,3 @@ int8_t NetworkManager::DetectLocalVehicleType()
}
return VEHICLE_NONE;
}
bool NetworkManager::IsInIsleWorld() const
{
return m_inIsleWorld;
}
// ---- World state sync ----
template <typename TInfo>
static int FindEntityIndex(TInfo* p_infoArray, MxS32 p_count, LegoEntity* p_entity)
{
for (MxS32 i = 0; i < p_count; i++) {
if (p_infoArray[i].m_entity == p_entity) {
return i;
}
}
return -1;
}
MxBool NetworkManager::HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType)
{
if (!IsConnected()) {
return FALSE;
}
uint8_t entityType;
int idx;
if (p_entity->GetType() == LegoEntity::e_plant) {
entityType = ENTITY_PLANT;
MxS32 count;
idx = FindEntityIndex(PlantManager()->GetInfoArray(count), count, p_entity);
}
else if (p_entity->GetType() == LegoEntity::e_building) {
entityType = ENTITY_BUILDING;
MxS32 count;
idx = FindEntityIndex(BuildingManager()->GetInfoArray(count), count, p_entity);
}
else {
return FALSE;
}
if (idx < 0) {
return FALSE;
}
if (IsHost()) {
// Host: allow local mutation, then broadcast to all peers
BroadcastWorldEvent(entityType, p_changeType, (uint8_t) idx);
return FALSE; // FALSE = allow local mutation to proceed
}
else {
// Non-host: send request to host, block local mutation
SendWorldEventRequest(entityType, p_changeType, (uint8_t) idx);
return TRUE; // TRUE = suppress local mutation
}
}
void NetworkManager::SendSnapshotRequest()
{
RequestSnapshotMsg msg{};
msg.header = {MSG_REQUEST_SNAPSHOT, m_localPeerId, m_sequence++};
SendMessage(msg);
m_snapshotRequested = true;
m_pendingWorldEvents.clear();
}
void NetworkManager::SendWorldSnapshot(uint32_t p_targetPeerId)
{
if (!m_transport || !m_transport->IsConnected()) {
return;
}
// Serialize plant + building state into a buffer using existing Write() methods
// Max sizes: 81 plants * (1+4+4+1+1+1) = 81*12 = 972 bytes
// 16 buildings * (4+4+1+1) = 16*10 = 160 bytes + 1 byte nextVariant
// Total ~1133 bytes. Use 4096 for safety.
uint8_t stateBuffer[4096];
LegoMemory memory(stateBuffer, sizeof(stateBuffer));
PlantManager()->Write(&memory);
BuildingManager()->Write(&memory);
LegoU32 dataLength;
memory.GetPosition(dataLength);
// Build the snapshot header + trailing payload
WorldSnapshotMsg msg{};
msg.header = {MSG_WORLD_SNAPSHOT, m_localPeerId, m_sequence++};
msg.targetPeerId = p_targetPeerId;
msg.dataLength = (uint16_t) dataLength;
std::vector<uint8_t> msgBuf(sizeof(WorldSnapshotMsg) + dataLength);
SDL_memcpy(msgBuf.data(), &msg, sizeof(WorldSnapshotMsg));
SDL_memcpy(msgBuf.data() + sizeof(WorldSnapshotMsg), stateBuffer, dataLength);
m_transport->Send(msgBuf.data(), msgBuf.size());
}
void NetworkManager::BroadcastWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex)
{
WorldEventMsg msg{};
msg.header = {MSG_WORLD_EVENT, m_localPeerId, m_sequence++};
msg.entityType = p_entityType;
msg.changeType = p_changeType;
msg.entityIndex = p_entityIndex;
SendMessage(msg);
}
void NetworkManager::SendWorldEventRequest(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex)
{
WorldEventRequestMsg msg{};
msg.header = {MSG_WORLD_EVENT_REQUEST, m_localPeerId, m_sequence++};
msg.entityType = p_entityType;
msg.changeType = p_changeType;
msg.entityIndex = p_entityIndex;
SendMessage(msg);
}
// Dispatch Switch*() calls shared by all entity types.
// Returns true if the change was handled, false for type-specific changes.
static bool DispatchEntitySwitch(LegoEntity* p_entity, uint8_t p_changeType)
{
switch (p_changeType) {
case CHANGE_VARIANT:
p_entity->SwitchVariant();
return true;
case CHANGE_SOUND:
p_entity->SwitchSound();
return true;
case CHANGE_MOVE:
p_entity->SwitchMove();
return true;
case CHANGE_MOOD:
p_entity->SwitchMood();
return true;
default:
return false;
}
}
void NetworkManager::ApplyWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex)
{
if (p_entityType == ENTITY_PLANT) {
MxS32 numPlants;
LegoPlantInfo* plantInfo = PlantManager()->GetInfoArray(numPlants);
if (p_entityIndex >= numPlants) {
return;
}
LegoPlantInfo* info = &plantInfo[p_entityIndex];
// If entity exists (we're in the Isle world), use LegoEntity::Switch*()
// which handles data mutation + visual update + sound + animation + counter
if (info->m_entity != NULL) {
if (!DispatchEntitySwitch(info->m_entity, p_changeType)) {
if (p_changeType == CHANGE_COLOR) {
info->m_entity->SwitchColor(info->m_entity->GetROI());
}
else if (p_changeType == CHANGE_DECREMENT) {
PlantManager()->DecrementCounter(info->m_entity);
}
}
}
else {
// Entity is NULL (we're outside the Isle world).
// Apply changes directly to the data array.
switch (p_changeType) {
case CHANGE_VARIANT:
if (info->m_counter == -1) {
info->m_variant++;
if (info->m_variant > LegoPlantInfo::e_palm) {
info->m_variant = LegoPlantInfo::e_flower;
}
// Clamp move to the new variant's max (mirrors SwitchVariant)
if (info->m_move != 0 && info->m_move >= (MxU32) LegoPlantManager::g_maxMove[info->m_variant]) {
info->m_move = LegoPlantManager::g_maxMove[info->m_variant] - 1;
}
}
break;
case CHANGE_SOUND:
info->m_sound++;
if (info->m_sound >= LegoPlantManager::g_maxSound) {
info->m_sound = 0;
}
break;
case CHANGE_MOVE:
info->m_move++;
if (info->m_move >= (MxU32) LegoPlantManager::g_maxMove[info->m_variant]) {
info->m_move = 0;
}
break;
case CHANGE_COLOR:
info->m_color++;
if (info->m_color > LegoPlantInfo::e_green) {
info->m_color = LegoPlantInfo::e_white;
}
break;
case CHANGE_MOOD:
info->m_mood++;
if (info->m_mood > 3) {
info->m_mood = 0;
}
break;
case CHANGE_DECREMENT: {
if (info->m_counter < 0) {
info->m_counter = g_counters[info->m_variant];
}
if (info->m_counter > 0) {
info->m_counter--;
if (info->m_counter == 1) {
info->m_counter = 0;
}
}
break;
}
}
}
}
else if (p_entityType == ENTITY_BUILDING) {
MxS32 numBuildings;
LegoBuildingInfo* buildingInfo = BuildingManager()->GetInfoArray(numBuildings);
if (p_entityIndex >= numBuildings) {
return;
}
LegoBuildingInfo* info = &buildingInfo[p_entityIndex];
// If entity exists (we're in the Isle world), use LegoEntity::Switch*()
if (info->m_entity != NULL) {
if (!DispatchEntitySwitch(info->m_entity, p_changeType)) {
if (p_changeType == CHANGE_COLOR) {
info->m_entity->SwitchColor(info->m_entity->GetROI());
}
else if (p_changeType == CHANGE_DECREMENT) {
BuildingManager()->DecrementCounter(info->m_entity);
}
}
}
else {
// Entity is NULL (we're outside the Isle world).
// Apply changes directly to the data array.
switch (p_changeType) {
case CHANGE_SOUND:
if (info->m_flags & LegoBuildingInfo::c_hasSounds) {
info->m_sound++;
if (info->m_sound >= LegoBuildingManager::g_maxSound) {
info->m_sound = 0;
}
}
break;
case CHANGE_MOVE:
if (info->m_flags & LegoBuildingInfo::c_hasMoves) {
info->m_move++;
if (info->m_move >= (MxU32) LegoBuildingManager::g_maxMove[p_entityIndex]) {
info->m_move = 0;
}
}
break;
case CHANGE_MOOD:
if (info->m_flags & LegoBuildingInfo::c_hasMoods) {
info->m_mood++;
if (info->m_mood > 3) {
info->m_mood = 0;
}
}
break;
case CHANGE_DECREMENT: {
if (info->m_counter < 0) {
info->m_counter = g_buildingInfoDownshift[p_entityIndex];
}
if (info->m_counter > 0) {
info->m_counter -= 2;
if (info->m_counter == 1) {
info->m_counter = 0;
}
}
break;
}
case CHANGE_VARIANT:
case CHANGE_COLOR:
// Variant switching is config-dependent, color N/A for buildings
break;
}
}
}
}

View File

@ -0,0 +1,393 @@
#include "extensions/multiplayer/worldstatesync.h"
#include "legobuildingmanager.h"
#include "legoentity.h"
#include "legomain.h"
#include "legoplantmanager.h"
#include "legoplants.h"
#include "legoworld.h"
#include "misc.h"
#include "misc/legostorage.h"
#include <SDL3/SDL_stdinc.h>
#include <vector>
extern MxU8 g_counters[];
extern MxU8 g_buildingInfoDownshift[];
using namespace Multiplayer;
template <typename T>
void WorldStateSync::SendMessage(const T& p_msg)
{
if (!m_transport || !m_transport->IsConnected()) {
return;
}
uint8_t buf[sizeof(T)];
size_t len = SerializeMsg(buf, sizeof(buf), p_msg);
if (len > 0) {
m_transport->Send(buf, len);
}
}
WorldStateSync::WorldStateSync()
: m_transport(nullptr), m_localPeerId(0), m_sequence(0), m_isHost(false), m_inIsleWorld(false),
m_snapshotRequested(false)
{
}
void WorldStateSync::OnHostChanged()
{
if (!m_isHost) {
m_snapshotRequested = false;
m_pendingWorldEvents.clear();
SendSnapshotRequest();
}
}
void WorldStateSync::HandleRequestSnapshot(const RequestSnapshotMsg& p_msg)
{
if (!m_isHost) {
return;
}
SendWorldSnapshot(p_msg.header.peerId);
}
void WorldStateSync::HandleWorldSnapshot(const uint8_t* p_data, size_t p_length)
{
WorldSnapshotMsg header;
if (!DeserializeMsg(p_data, p_length, header) || header.header.type != MSG_WORLD_SNAPSHOT) {
return;
}
if (p_length < sizeof(WorldSnapshotMsg) + header.dataLength) {
return;
}
const uint8_t* snapshotData = p_data + sizeof(WorldSnapshotMsg);
// Apply the snapshot using LegoMemory with the existing Read() methods
LegoMemory memory((void*) snapshotData, header.dataLength);
PlantManager()->Read(&memory);
BuildingManager()->Read(&memory);
// If we're in the Isle world, update entity visuals after applying the snapshot.
// Read() calls AdjustHeight() which updates data arrays, but doesn't update
// entity positions. We need to reload world info to refresh visuals.
if (m_inIsleWorld) {
LegoWorld* world = CurrentWorld();
if (world && world->GetWorldId() == LegoOmni::e_act1) {
PlantManager()->Reset(LegoOmni::e_act1);
PlantManager()->LoadWorldInfo(LegoOmni::e_act1);
BuildingManager()->Reset();
BuildingManager()->LoadWorldInfo();
}
}
// Apply any world events that were queued between snapshot request and response
for (const auto& evt : m_pendingWorldEvents) {
ApplyWorldEvent(evt.entityType, evt.changeType, evt.entityIndex);
}
m_pendingWorldEvents.clear();
m_snapshotRequested = false;
}
void WorldStateSync::HandleWorldEvent(const WorldEventMsg& p_msg)
{
if (m_snapshotRequested) {
m_pendingWorldEvents.push_back(p_msg);
return;
}
ApplyWorldEvent(p_msg.entityType, p_msg.changeType, p_msg.entityIndex);
}
void WorldStateSync::HandleWorldEventRequest(const WorldEventRequestMsg& p_msg)
{
if (!m_isHost) {
return;
}
ApplyWorldEvent(p_msg.entityType, p_msg.changeType, p_msg.entityIndex);
BroadcastWorldEvent(p_msg.entityType, p_msg.changeType, p_msg.entityIndex);
}
// ---- Entity mutation routing ----
template <typename TInfo>
static int FindEntityIndex(TInfo* p_infoArray, MxS32 p_count, LegoEntity* p_entity)
{
for (MxS32 i = 0; i < p_count; i++) {
if (p_infoArray[i].m_entity == p_entity) {
return i;
}
}
return -1;
}
MxBool WorldStateSync::HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType)
{
if (!m_transport || !m_transport->IsConnected()) {
return FALSE;
}
uint8_t entityType;
int idx;
if (p_entity->GetType() == LegoEntity::e_plant) {
entityType = ENTITY_PLANT;
MxS32 count;
idx = FindEntityIndex(PlantManager()->GetInfoArray(count), count, p_entity);
}
else if (p_entity->GetType() == LegoEntity::e_building) {
entityType = ENTITY_BUILDING;
MxS32 count;
idx = FindEntityIndex(BuildingManager()->GetInfoArray(count), count, p_entity);
}
else {
return FALSE;
}
if (idx < 0) {
return FALSE;
}
if (m_isHost) {
BroadcastWorldEvent(entityType, p_changeType, (uint8_t) idx);
return FALSE;
}
else {
SendWorldEventRequest(entityType, p_changeType, (uint8_t) idx);
return TRUE;
}
}
// ---- Send helpers ----
void WorldStateSync::SendSnapshotRequest()
{
RequestSnapshotMsg msg{};
msg.header = {MSG_REQUEST_SNAPSHOT, m_localPeerId, m_sequence++};
SendMessage(msg);
m_snapshotRequested = true;
m_pendingWorldEvents.clear();
}
void WorldStateSync::SendWorldSnapshot(uint32_t p_targetPeerId)
{
if (!m_transport || !m_transport->IsConnected()) {
return;
}
// Serialize plant + building state into a buffer using existing Write() methods
// Max sizes: 81 plants * (1+4+4+1+1+1) = 81*12 = 972 bytes
// 16 buildings * (4+4+1+1) = 16*10 = 160 bytes + 1 byte nextVariant
// Total ~1133 bytes. Use 4096 for safety.
uint8_t stateBuffer[4096];
LegoMemory memory(stateBuffer, sizeof(stateBuffer));
PlantManager()->Write(&memory);
BuildingManager()->Write(&memory);
LegoU32 dataLength;
memory.GetPosition(dataLength);
WorldSnapshotMsg msg{};
msg.header = {MSG_WORLD_SNAPSHOT, m_localPeerId, m_sequence++};
msg.targetPeerId = p_targetPeerId;
msg.dataLength = (uint16_t) dataLength;
std::vector<uint8_t> msgBuf(sizeof(WorldSnapshotMsg) + dataLength);
SDL_memcpy(msgBuf.data(), &msg, sizeof(WorldSnapshotMsg));
SDL_memcpy(msgBuf.data() + sizeof(WorldSnapshotMsg), stateBuffer, dataLength);
m_transport->Send(msgBuf.data(), msgBuf.size());
}
void WorldStateSync::BroadcastWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex)
{
WorldEventMsg msg{};
msg.header = {MSG_WORLD_EVENT, m_localPeerId, m_sequence++};
msg.entityType = p_entityType;
msg.changeType = p_changeType;
msg.entityIndex = p_entityIndex;
SendMessage(msg);
}
void WorldStateSync::SendWorldEventRequest(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex)
{
WorldEventRequestMsg msg{};
msg.header = {MSG_WORLD_EVENT_REQUEST, m_localPeerId, m_sequence++};
msg.entityType = p_entityType;
msg.changeType = p_changeType;
msg.entityIndex = p_entityIndex;
SendMessage(msg);
}
// ---- Apply world events ----
// Dispatch Switch*() calls shared by all entity types.
// Returns true if the change was handled, false for type-specific changes.
static bool DispatchEntitySwitch(LegoEntity* p_entity, uint8_t p_changeType)
{
switch (p_changeType) {
case CHANGE_VARIANT:
p_entity->SwitchVariant();
return true;
case CHANGE_SOUND:
p_entity->SwitchSound();
return true;
case CHANGE_MOVE:
p_entity->SwitchMove();
return true;
case CHANGE_MOOD:
p_entity->SwitchMood();
return true;
default:
return false;
}
}
void WorldStateSync::ApplyWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex)
{
if (p_entityType == ENTITY_PLANT) {
MxS32 numPlants;
LegoPlantInfo* plantInfo = PlantManager()->GetInfoArray(numPlants);
if (p_entityIndex >= numPlants) {
return;
}
LegoPlantInfo* info = &plantInfo[p_entityIndex];
if (info->m_entity != NULL) {
if (!DispatchEntitySwitch(info->m_entity, p_changeType)) {
if (p_changeType == CHANGE_COLOR) {
info->m_entity->SwitchColor(info->m_entity->GetROI());
}
else if (p_changeType == CHANGE_DECREMENT) {
PlantManager()->DecrementCounter(info->m_entity);
}
}
}
else {
switch (p_changeType) {
case CHANGE_VARIANT:
if (info->m_counter == -1) {
info->m_variant++;
if (info->m_variant > LegoPlantInfo::e_palm) {
info->m_variant = LegoPlantInfo::e_flower;
}
if (info->m_move != 0 && info->m_move >= (MxU32) LegoPlantManager::g_maxMove[info->m_variant]) {
info->m_move = LegoPlantManager::g_maxMove[info->m_variant] - 1;
}
}
break;
case CHANGE_SOUND:
info->m_sound++;
if (info->m_sound >= LegoPlantManager::g_maxSound) {
info->m_sound = 0;
}
break;
case CHANGE_MOVE:
info->m_move++;
if (info->m_move >= (MxU32) LegoPlantManager::g_maxMove[info->m_variant]) {
info->m_move = 0;
}
break;
case CHANGE_COLOR:
info->m_color++;
if (info->m_color > LegoPlantInfo::e_green) {
info->m_color = LegoPlantInfo::e_white;
}
break;
case CHANGE_MOOD:
info->m_mood++;
if (info->m_mood > 3) {
info->m_mood = 0;
}
break;
case CHANGE_DECREMENT: {
if (info->m_counter < 0) {
info->m_counter = g_counters[info->m_variant];
}
if (info->m_counter > 0) {
info->m_counter--;
if (info->m_counter == 1) {
info->m_counter = 0;
}
}
break;
}
}
}
}
else if (p_entityType == ENTITY_BUILDING) {
MxS32 numBuildings;
LegoBuildingInfo* buildingInfo = BuildingManager()->GetInfoArray(numBuildings);
if (p_entityIndex >= numBuildings) {
return;
}
LegoBuildingInfo* info = &buildingInfo[p_entityIndex];
if (info->m_entity != NULL) {
if (!DispatchEntitySwitch(info->m_entity, p_changeType)) {
if (p_changeType == CHANGE_COLOR) {
info->m_entity->SwitchColor(info->m_entity->GetROI());
}
else if (p_changeType == CHANGE_DECREMENT) {
BuildingManager()->DecrementCounter(info->m_entity);
}
}
}
else {
switch (p_changeType) {
case CHANGE_SOUND:
if (info->m_flags & LegoBuildingInfo::c_hasSounds) {
info->m_sound++;
if (info->m_sound >= LegoBuildingManager::g_maxSound) {
info->m_sound = 0;
}
}
break;
case CHANGE_MOVE:
if (info->m_flags & LegoBuildingInfo::c_hasMoves) {
info->m_move++;
if (info->m_move >= (MxU32) LegoBuildingManager::g_maxMove[p_entityIndex]) {
info->m_move = 0;
}
}
break;
case CHANGE_MOOD:
if (info->m_flags & LegoBuildingInfo::c_hasMoods) {
info->m_mood++;
if (info->m_mood > 3) {
info->m_mood = 0;
}
}
break;
case CHANGE_DECREMENT: {
if (info->m_counter < 0) {
info->m_counter = g_buildingInfoDownshift[p_entityIndex];
}
if (info->m_counter > 0) {
info->m_counter -= 2;
if (info->m_counter == 1) {
info->m_counter = 0;
}
}
break;
}
case CHANGE_VARIANT:
case CHANGE_COLOR:
// Variant switching is config-dependent, color N/A for buildings
break;
}
}
}
}