mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
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:
parent
a0629c45a1
commit
c760db50a9
@ -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()
|
||||
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
56
extensions/include/extensions/multiplayer/worldstatesync.h
Normal file
56
extensions/include/extensions/multiplayer/worldstatesync.h
Normal 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
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
393
extensions/src/multiplayer/worldstatesync.cpp
Normal file
393
extensions/src/multiplayer/worldstatesync.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user