From c760db50a98df33816dc969b68f45d0f761b24f4 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Sun, 1 Mar 2026 10:12:24 -0800 Subject: [PATCH] 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. --- CMakeLists.txt | 1 + .../legoomni/include/legobuildingmanager.h | 4 +- .../lego/legoomni/include/legoplantmanager.h | 4 +- LEGO1/lego/legoomni/src/entity/legoentity.cpp | 8 +- LEGO1/lego/legoomni/src/entity/legoworld.cpp | 2 + .../extensions/multiplayer/networkmanager.h | 20 +- .../extensions/multiplayer/worldstatesync.h | 56 +++ extensions/src/multiplayer/networkmanager.cpp | 402 +----------------- extensions/src/multiplayer/worldstatesync.cpp | 393 +++++++++++++++++ 9 files changed, 478 insertions(+), 412 deletions(-) create mode 100644 extensions/include/extensions/multiplayer/worldstatesync.h create mode 100644 extensions/src/multiplayer/worldstatesync.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ccfe122..533b9957 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/LEGO1/lego/legoomni/include/legobuildingmanager.h b/LEGO1/lego/legoomni/include/legobuildingmanager.h index 6b65ff9a..6d2d4f86 100644 --- a/LEGO1/lego/legoomni/include/legobuildingmanager.h +++ b/LEGO1/lego/legoomni/include/legobuildingmanager.h @@ -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]; diff --git a/LEGO1/lego/legoomni/include/legoplantmanager.h b/LEGO1/lego/legoomni/include/legoplantmanager.h index 95d616ce..2fa53ccd 100644 --- a/LEGO1/lego/legoomni/include/legoplantmanager.h +++ b/LEGO1/lego/legoomni/include/legoplantmanager.h @@ -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); diff --git a/LEGO1/lego/legoomni/src/entity/legoentity.cpp b/LEGO1/lego/legoomni/src/entity/legoentity.cpp index 63b8b3cf..b49be644 100644 --- a/LEGO1/lego/legoomni/src/entity/legoentity.cpp +++ b/LEGO1/lego/legoomni/src/entity/legoentity.cpp @@ -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::Call(Extensions::HandleEntityNotify, this); - if (intercepted.has_value() && intercepted.value()) { + auto handled = Extensions::Extension::Call(Extensions::HandleEntityNotify, this); + if (handled.has_value() && handled.value()) { return 1; } diff --git a/LEGO1/lego/legoomni/src/entity/legoworld.cpp b/LEGO1/lego/legoomni/src/entity/legoworld.cpp index dc741d08..4c47f694 100644 --- a/LEGO1/lego/legoomni/src/entity/legoworld.cpp +++ b/LEGO1/lego/legoomni/src/entity/legoworld.cpp @@ -754,6 +754,7 @@ void LegoWorld::Enable(MxBool p_enable) #ifndef BETA10 SetIsWorldActive(TRUE); #endif + Extensions::Extension::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::Call(Extensions::HandleWorldEnable, this, FALSE); } } diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index b85635ae..64e7e299 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -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 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> 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 m_pendingWorldEvents; static const uint32_t BROADCAST_INTERVAL_MS = 66; // ~15Hz static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout diff --git a/extensions/include/extensions/multiplayer/worldstatesync.h b/extensions/include/extensions/multiplayer/worldstatesync.h new file mode 100644 index 00000000..f0245f00 --- /dev/null +++ b/extensions/include/extensions/multiplayer/worldstatesync.h @@ -0,0 +1,56 @@ +#pragma once + +#include "extensions/multiplayer/networktransport.h" +#include "extensions/multiplayer/protocol.h" +#include "mxtypes.h" + +#include +#include + +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 + 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 m_pendingWorldEvents; +}; + +} // namespace Multiplayer diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index 0270510a..6feba6c9 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -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 #include -extern MxU8 g_counters[]; -extern MxU8 g_buildingInfoDownshift[]; - using namespace Multiplayer; template @@ -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 -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 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; - } - } - } -} diff --git a/extensions/src/multiplayer/worldstatesync.cpp b/extensions/src/multiplayer/worldstatesync.cpp new file mode 100644 index 00000000..238749f1 --- /dev/null +++ b/extensions/src/multiplayer/worldstatesync.cpp @@ -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 +#include + +extern MxU8 g_counters[]; +extern MxU8 g_buildingInfoDownshift[]; + +using namespace Multiplayer; + +template +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 +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 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; + } + } + } +}