mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
Sync sky light (#9)
* Sync sky color and light position in multiplayer Add ENTITY_SKY and ENTITY_LIGHT to the WorldEvent system so the host controls sky color (hue/saturation via observatory sun/moon/palette buttons) and light position (globe arrows) with the same host-authoritative pattern used for plants and buildings. Non-host players send requests to the host who applies and broadcasts. Sky/light state is appended to the world snapshot so joining players get the current values. https://claude.ai/code/session_01X2cPVQEo7c92wpWA7QPPMG * Clean up sky/light sync: remove debug logging, DRY apply logic, fix host routing - Extract ApplySkyLightState helper to deduplicate sky/light apply code between RestoreSkyLightState and HandleWorldSnapshot - Remove all SDL_Log debug calls and SDL_log.h includes - Remove dead OnWorldEnabled method from WorldStateSync (replaced by OnHostChanged in OnSaveLoaded) - Fix HandleSkyLightMutation host path: return FALSE to let local switch case proceed, instead of duplicating via ApplyWorldEvent - Simplify isle.cpp HandleControl: split observatory cases into individual switch arms with single early-return multiplayer hook - Add save load hooks to sync world state with multiplayer peers - Fix player count to exclude local player without valid actor - Support broadcast snapshots (targetPeerId=0) in relay server ---------
This commit is contained in:
parent
853e8981fa
commit
eb6d2b8728
@ -23,6 +23,11 @@ class RaceCar;
|
|||||||
class SkateBoard;
|
class SkateBoard;
|
||||||
class TowTrack;
|
class TowTrack;
|
||||||
|
|
||||||
|
namespace Multiplayer
|
||||||
|
{
|
||||||
|
class WorldStateSync;
|
||||||
|
}
|
||||||
|
|
||||||
// VTABLE: LEGO1 0x100d7028
|
// VTABLE: LEGO1 0x100d7028
|
||||||
// VTABLE: BETA10 0x101b9d40
|
// VTABLE: BETA10 0x101b9d40
|
||||||
// SIZE 0x26c
|
// SIZE 0x26c
|
||||||
@ -164,6 +169,7 @@ class Isle : public LegoWorld {
|
|||||||
void SwitchToInfocenter();
|
void SwitchToInfocenter();
|
||||||
|
|
||||||
friend class Act1State;
|
friend class Act1State;
|
||||||
|
friend class Multiplayer::WorldStateSync;
|
||||||
|
|
||||||
// SYNTHETIC: LEGO1 0x10030a30
|
// SYNTHETIC: LEGO1 0x10030a30
|
||||||
// Isle::`scalar deleting destructor'
|
// Isle::`scalar deleting destructor'
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
#include "dunebuggy.h"
|
#include "dunebuggy.h"
|
||||||
#include "dunecar_actions.h"
|
#include "dunecar_actions.h"
|
||||||
#include "elevbott_actions.h"
|
#include "elevbott_actions.h"
|
||||||
|
#include "extensions/multiplayer.h"
|
||||||
#include "garage_actions.h"
|
#include "garage_actions.h"
|
||||||
#include "helicopter.h"
|
#include "helicopter.h"
|
||||||
#include "histbook_actions.h"
|
#include "histbook_actions.h"
|
||||||
@ -64,6 +65,8 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
using namespace Extensions;
|
||||||
|
|
||||||
DECOMP_SIZE_ASSERT(LegoGameState::Username, 0x0e)
|
DECOMP_SIZE_ASSERT(LegoGameState::Username, 0x0e)
|
||||||
DECOMP_SIZE_ASSERT(LegoGameState::ScoreItem, 0x2c)
|
DECOMP_SIZE_ASSERT(LegoGameState::ScoreItem, 0x2c)
|
||||||
DECOMP_SIZE_ASSERT(LegoGameState::History, 0x374)
|
DECOMP_SIZE_ASSERT(LegoGameState::History, 0x374)
|
||||||
@ -364,6 +367,8 @@ MxResult LegoGameState::DeleteState()
|
|||||||
// FUNCTION: BETA10 0x10084329
|
// FUNCTION: BETA10 0x10084329
|
||||||
MxResult LegoGameState::Load(MxULong p_slot)
|
MxResult LegoGameState::Load(MxULong p_slot)
|
||||||
{
|
{
|
||||||
|
Extension<MultiplayerExt>::Call(HandleBeforeSaveLoad);
|
||||||
|
|
||||||
MxResult result = FAILURE;
|
MxResult result = FAILURE;
|
||||||
LegoFile storage;
|
LegoFile storage;
|
||||||
MxVariableTable* variableTable = VariableTable();
|
MxVariableTable* variableTable = VariableTable();
|
||||||
@ -456,6 +461,8 @@ MxResult LegoGameState::Load(MxULong p_slot)
|
|||||||
result = SUCCESS;
|
result = SUCCESS;
|
||||||
m_isDirty = FALSE;
|
m_isDirty = FALSE;
|
||||||
|
|
||||||
|
Extension<MultiplayerExt>::Call(HandleSaveLoaded);
|
||||||
|
|
||||||
done:
|
done:
|
||||||
if (result != SUCCESS) {
|
if (result != SUCCESS) {
|
||||||
OmniError("Game state loading was not successful!", 0);
|
OmniError("Game state loading was not successful!", 0);
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include "bike.h"
|
#include "bike.h"
|
||||||
#include "carrace.h"
|
#include "carrace.h"
|
||||||
#include "dunebuggy.h"
|
#include "dunebuggy.h"
|
||||||
|
#include "extensions/multiplayer.h"
|
||||||
#include "extensions/siloader.h"
|
#include "extensions/siloader.h"
|
||||||
#include "helicopter.h"
|
#include "helicopter.h"
|
||||||
#include "isle_actions.h"
|
#include "isle_actions.h"
|
||||||
@ -296,6 +297,10 @@ void Isle::ReadyWorld()
|
|||||||
MxLong Isle::HandleControl(LegoControlManagerNotificationParam& p_param)
|
MxLong Isle::HandleControl(LegoControlManagerNotificationParam& p_param)
|
||||||
{
|
{
|
||||||
if (p_param.m_enabledChild == 1) {
|
if (p_param.m_enabledChild == 1) {
|
||||||
|
if (Extension<MultiplayerExt>::Call(HandleSkyLightControl, (MxU32) p_param.m_clickedObjectId).value_or(FALSE)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
MxDSAction action;
|
MxDSAction action;
|
||||||
|
|
||||||
switch (p_param.m_clickedObjectId) {
|
switch (p_param.m_clickedObjectId) {
|
||||||
|
|||||||
@ -33,6 +33,10 @@ class MultiplayerExt {
|
|||||||
// Returns TRUE if the click should be suppressed locally (non-host).
|
// Returns TRUE if the click should be suppressed locally (non-host).
|
||||||
static MxBool HandleEntityNotify(LegoEntity* p_entity);
|
static MxBool HandleEntityNotify(LegoEntity* p_entity);
|
||||||
|
|
||||||
|
// Intercepts observatory sky/light controls for multiplayer routing.
|
||||||
|
// Returns TRUE if the local action should be suppressed (non-host).
|
||||||
|
static MxBool HandleSkyLightControl(MxU32 p_controlId);
|
||||||
|
|
||||||
// Handles clicks on entity-less ROIs (remote players, display actor overrides).
|
// Handles clicks on entity-less ROIs (remote players, display actor overrides).
|
||||||
static MxBool HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationParam& p_param);
|
static MxBool HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationParam& p_param);
|
||||||
|
|
||||||
@ -50,6 +54,12 @@ class MultiplayerExt {
|
|||||||
// Returns TRUE if the name belongs to a multiplayer clone (entity-less ROI).
|
// Returns TRUE if the name belongs to a multiplayer clone (entity-less ROI).
|
||||||
static MxBool IsClonedCharacter(const char* p_name);
|
static MxBool IsClonedCharacter(const char* p_name);
|
||||||
|
|
||||||
|
// Called before a save file is loaded. Captures current sky/light state.
|
||||||
|
static void HandleBeforeSaveLoad();
|
||||||
|
|
||||||
|
// Called after a save file is loaded. Re-syncs world state with multiplayer peers.
|
||||||
|
static void HandleSaveLoaded();
|
||||||
|
|
||||||
// Returns true if the multiplayer connection was rejected (e.g. room full).
|
// Returns true if the multiplayer connection was rejected (e.g. room full).
|
||||||
static MxBool CheckRejected();
|
static MxBool CheckRejected();
|
||||||
|
|
||||||
@ -68,23 +78,29 @@ LEGO1_EXPORT bool IsMultiplayerRejected();
|
|||||||
constexpr auto HandleCreate = &MultiplayerExt::HandleCreate;
|
constexpr auto HandleCreate = &MultiplayerExt::HandleCreate;
|
||||||
constexpr auto HandleWorldEnable = &MultiplayerExt::HandleWorldEnable;
|
constexpr auto HandleWorldEnable = &MultiplayerExt::HandleWorldEnable;
|
||||||
constexpr auto HandleEntityNotify = &MultiplayerExt::HandleEntityNotify;
|
constexpr auto HandleEntityNotify = &MultiplayerExt::HandleEntityNotify;
|
||||||
|
constexpr auto HandleSkyLightControl = &MultiplayerExt::HandleSkyLightControl;
|
||||||
constexpr auto HandleROIClick = &MultiplayerExt::HandleROIClick;
|
constexpr auto HandleROIClick = &MultiplayerExt::HandleROIClick;
|
||||||
constexpr auto HandleActorEnter = &MultiplayerExt::HandleActorEnter;
|
constexpr auto HandleActorEnter = &MultiplayerExt::HandleActorEnter;
|
||||||
constexpr auto HandleActorExit = &MultiplayerExt::HandleActorExit;
|
constexpr auto HandleActorExit = &MultiplayerExt::HandleActorExit;
|
||||||
constexpr auto HandleCamAnimEnd = &MultiplayerExt::HandleCamAnimEnd;
|
constexpr auto HandleCamAnimEnd = &MultiplayerExt::HandleCamAnimEnd;
|
||||||
constexpr auto ShouldInvertMovement = &MultiplayerExt::ShouldInvertMovement;
|
constexpr auto ShouldInvertMovement = &MultiplayerExt::ShouldInvertMovement;
|
||||||
constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter;
|
constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter;
|
||||||
|
constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad;
|
||||||
|
constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded;
|
||||||
constexpr auto CheckRejected = &MultiplayerExt::CheckRejected;
|
constexpr auto CheckRejected = &MultiplayerExt::CheckRejected;
|
||||||
#else
|
#else
|
||||||
constexpr decltype(&MultiplayerExt::HandleCreate) HandleCreate = nullptr;
|
constexpr decltype(&MultiplayerExt::HandleCreate) HandleCreate = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullptr;
|
constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::HandleEntityNotify) HandleEntityNotify = nullptr;
|
constexpr decltype(&MultiplayerExt::HandleEntityNotify) HandleEntityNotify = nullptr;
|
||||||
|
constexpr decltype(&MultiplayerExt::HandleSkyLightControl) HandleSkyLightControl = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::HandleROIClick) HandleROIClick = nullptr;
|
constexpr decltype(&MultiplayerExt::HandleROIClick) HandleROIClick = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::HandleActorEnter) HandleActorEnter = nullptr;
|
constexpr decltype(&MultiplayerExt::HandleActorEnter) HandleActorEnter = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::HandleActorExit) HandleActorExit = nullptr;
|
constexpr decltype(&MultiplayerExt::HandleActorExit) HandleActorExit = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::HandleCamAnimEnd) HandleCamAnimEnd = nullptr;
|
constexpr decltype(&MultiplayerExt::HandleCamAnimEnd) HandleCamAnimEnd = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::ShouldInvertMovement) ShouldInvertMovement = nullptr;
|
constexpr decltype(&MultiplayerExt::ShouldInvertMovement) ShouldInvertMovement = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::IsClonedCharacter) IsClonedCharacter = nullptr;
|
constexpr decltype(&MultiplayerExt::IsClonedCharacter) IsClonedCharacter = nullptr;
|
||||||
|
constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr;
|
||||||
|
constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::CheckRejected) CheckRejected = nullptr;
|
constexpr decltype(&MultiplayerExt::CheckRejected) CheckRejected = nullptr;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,8 @@ class NetworkManager : public MxCore {
|
|||||||
|
|
||||||
void OnWorldEnabled(LegoWorld* p_world);
|
void OnWorldEnabled(LegoWorld* p_world);
|
||||||
void OnWorldDisabled(LegoWorld* p_world);
|
void OnWorldDisabled(LegoWorld* p_world);
|
||||||
|
void OnBeforeSaveLoad();
|
||||||
|
void OnSaveLoaded();
|
||||||
|
|
||||||
ThirdPersonCamera& GetThirdPersonCamera() { return m_thirdPersonCamera; }
|
ThirdPersonCamera& GetThirdPersonCamera() { return m_thirdPersonCamera; }
|
||||||
|
|
||||||
@ -74,6 +76,10 @@ class NetworkManager : public MxCore {
|
|||||||
// Returns TRUE if the mutation should be suppressed locally (non-host).
|
// Returns TRUE if the mutation should be suppressed locally (non-host).
|
||||||
MxBool HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType);
|
MxBool HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType);
|
||||||
|
|
||||||
|
// Called from multiplayer extension when a sky/light control is used.
|
||||||
|
// Returns TRUE if the local action should be suppressed (non-host).
|
||||||
|
MxBool HandleSkyLightMutation(uint8_t p_entityType, uint8_t p_changeType);
|
||||||
|
|
||||||
bool IsHost() const { return m_localPeerId != 0 && m_localPeerId == m_hostPeerId; }
|
bool IsHost() const { return m_localPeerId != 0 && m_localPeerId == m_hostPeerId; }
|
||||||
uint32_t GetLocalPeerId() const { return m_localPeerId; }
|
uint32_t GetLocalPeerId() const { return m_localPeerId; }
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,9 @@ enum VehicleType : int8_t {
|
|||||||
// Entity types for world events
|
// Entity types for world events
|
||||||
enum WorldEntityType : uint8_t {
|
enum WorldEntityType : uint8_t {
|
||||||
ENTITY_PLANT = 0,
|
ENTITY_PLANT = 0,
|
||||||
ENTITY_BUILDING = 1
|
ENTITY_BUILDING = 1,
|
||||||
|
ENTITY_SKY = 2,
|
||||||
|
ENTITY_LIGHT = 3
|
||||||
};
|
};
|
||||||
|
|
||||||
// Change types for world events (maps to Switch* methods on LegoEntity)
|
// Change types for world events (maps to Switch* methods on LegoEntity)
|
||||||
@ -53,6 +55,19 @@ enum WorldChangeType : uint8_t {
|
|||||||
CHANGE_DECREMENT = 5
|
CHANGE_DECREMENT = 5
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Change types for ENTITY_SKY
|
||||||
|
enum SkyChangeType : uint8_t {
|
||||||
|
SKY_TOGGLE_COLOR = 0,
|
||||||
|
SKY_DAY = 1,
|
||||||
|
SKY_NIGHT = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change types for ENTITY_LIGHT
|
||||||
|
enum LightChangeType : uint8_t {
|
||||||
|
LIGHT_INCREMENT = 0,
|
||||||
|
LIGHT_DECREMENT = 1
|
||||||
|
};
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
#pragma pack(push, 1)
|
||||||
|
|
||||||
struct MessageHeader {
|
struct MessageHeader {
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include "mxtypes.h"
|
#include "mxtypes.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class LegoEntity;
|
class LegoEntity;
|
||||||
@ -24,6 +25,15 @@ class WorldStateSync {
|
|||||||
// Called when the host peer changes. Requests a snapshot if we're not host.
|
// Called when the host peer changes. Requests a snapshot if we're not host.
|
||||||
void OnHostChanged();
|
void OnHostChanged();
|
||||||
|
|
||||||
|
// Captures current sky/light state before a save load (for non-host restore).
|
||||||
|
void SaveSkyLightState();
|
||||||
|
|
||||||
|
// Restores previously saved sky/light state (non-host only, prevents flicker).
|
||||||
|
void RestoreSkyLightState();
|
||||||
|
|
||||||
|
// Sends a snapshot to a specific peer, or broadcasts to all if p_targetPeerId is 0.
|
||||||
|
void SendWorldSnapshotTo(uint32_t p_targetPeerId);
|
||||||
|
|
||||||
// Incoming message handlers (called from NetworkManager::ProcessIncomingPackets)
|
// Incoming message handlers (called from NetworkManager::ProcessIncomingPackets)
|
||||||
void HandleRequestSnapshot(const RequestSnapshotMsg& p_msg);
|
void HandleRequestSnapshot(const RequestSnapshotMsg& p_msg);
|
||||||
void HandleWorldSnapshot(const uint8_t* p_data, size_t p_length);
|
void HandleWorldSnapshot(const uint8_t* p_data, size_t p_length);
|
||||||
@ -34,7 +44,12 @@ class WorldStateSync {
|
|||||||
// Returns TRUE if the mutation should be suppressed locally (non-host).
|
// Returns TRUE if the mutation should be suppressed locally (non-host).
|
||||||
MxBool HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType);
|
MxBool HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType);
|
||||||
|
|
||||||
|
// Called from multiplayer extension when a sky/light control is used.
|
||||||
|
// Returns TRUE if the local action should be suppressed (non-host).
|
||||||
|
MxBool HandleSkyLightMutation(uint8_t p_entityType, uint8_t p_changeType);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void ApplySkyLightState(const char* p_skyColor, int p_lightPos);
|
||||||
void SendSnapshotRequest();
|
void SendSnapshotRequest();
|
||||||
void SendWorldSnapshot(uint32_t p_targetPeerId);
|
void SendWorldSnapshot(uint32_t p_targetPeerId);
|
||||||
void BroadcastWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex);
|
void BroadcastWorldEvent(uint8_t p_entityType, uint8_t p_changeType, uint8_t p_entityIndex);
|
||||||
@ -51,6 +66,10 @@ class WorldStateSync {
|
|||||||
bool m_inIsleWorld;
|
bool m_inIsleWorld;
|
||||||
bool m_snapshotRequested;
|
bool m_snapshotRequested;
|
||||||
std::vector<WorldEventMsg> m_pendingWorldEvents;
|
std::vector<WorldEventMsg> m_pendingWorldEvents;
|
||||||
|
|
||||||
|
// Saved sky/light state for non-host restore across save loads.
|
||||||
|
std::string m_savedSkyColor;
|
||||||
|
int m_savedLightPos;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Multiplayer
|
} // namespace Multiplayer
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include "extensions/multiplayer/networkmanager.h"
|
#include "extensions/multiplayer/networkmanager.h"
|
||||||
#include "extensions/multiplayer/networktransport.h"
|
#include "extensions/multiplayer/networktransport.h"
|
||||||
#include "extensions/multiplayer/protocol.h"
|
#include "extensions/multiplayer/protocol.h"
|
||||||
|
#include "isle_actions.h"
|
||||||
#include "islepathactor.h"
|
#include "islepathactor.h"
|
||||||
#include "legoactor.h"
|
#include "legoactor.h"
|
||||||
#include "legoactors.h"
|
#include "legoactors.h"
|
||||||
@ -213,6 +214,57 @@ MxBool MultiplayerExt::HandleEntityNotify(LegoEntity* p_entity)
|
|||||||
return s_networkManager->HandleEntityMutation(p_entity, changeType);
|
return s_networkManager->HandleEntityMutation(p_entity, changeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MxBool MultiplayerExt::HandleSkyLightControl(MxU32 p_controlId)
|
||||||
|
{
|
||||||
|
if (!s_networkManager) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t entityType;
|
||||||
|
uint8_t changeType;
|
||||||
|
|
||||||
|
switch (p_controlId) {
|
||||||
|
case IsleScript::c_Observe_SkyColor_Ctl:
|
||||||
|
entityType = Multiplayer::ENTITY_SKY;
|
||||||
|
changeType = Multiplayer::SKY_TOGGLE_COLOR;
|
||||||
|
break;
|
||||||
|
case IsleScript::c_Observe_Sun_Ctl:
|
||||||
|
entityType = Multiplayer::ENTITY_SKY;
|
||||||
|
changeType = Multiplayer::SKY_DAY;
|
||||||
|
break;
|
||||||
|
case IsleScript::c_Observe_Moon_Ctl:
|
||||||
|
entityType = Multiplayer::ENTITY_SKY;
|
||||||
|
changeType = Multiplayer::SKY_NIGHT;
|
||||||
|
break;
|
||||||
|
case IsleScript::c_Observe_GlobeRArrow_Ctl:
|
||||||
|
entityType = Multiplayer::ENTITY_LIGHT;
|
||||||
|
changeType = Multiplayer::LIGHT_INCREMENT;
|
||||||
|
break;
|
||||||
|
case IsleScript::c_Observe_GlobeLArrow_Ctl:
|
||||||
|
entityType = Multiplayer::ENTITY_LIGHT;
|
||||||
|
changeType = Multiplayer::LIGHT_DECREMENT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s_networkManager->HandleSkyLightMutation(entityType, changeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerExt::HandleBeforeSaveLoad()
|
||||||
|
{
|
||||||
|
if (s_networkManager) {
|
||||||
|
s_networkManager->OnBeforeSaveLoad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerExt::HandleSaveLoaded()
|
||||||
|
{
|
||||||
|
if (s_networkManager) {
|
||||||
|
s_networkManager->OnSaveLoaded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MultiplayerExt::HandleActorEnter(IslePathActor* p_actor)
|
void MultiplayerExt::HandleActorEnter(IslePathActor* p_actor)
|
||||||
{
|
{
|
||||||
if (s_networkManager) {
|
if (s_networkManager) {
|
||||||
|
|||||||
@ -192,11 +192,42 @@ void NetworkManager::OnWorldDisabled(LegoWorld* p_world)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkManager::OnBeforeSaveLoad()
|
||||||
|
{
|
||||||
|
if (m_transport && m_transport->IsConnected() && !IsHost()) {
|
||||||
|
m_worldSync.SaveSkyLightState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkManager::OnSaveLoaded()
|
||||||
|
{
|
||||||
|
if (!m_transport || !m_transport->IsConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After a save file load, the local plant/building/sky/light state comes
|
||||||
|
// from the save file and may diverge from the host's state.
|
||||||
|
// Host broadcasts to all peers (targetPeerId=0); non-host restores the
|
||||||
|
// pre-load sky/light to avoid visual flicker, then requests a fresh snapshot.
|
||||||
|
if (IsHost()) {
|
||||||
|
m_worldSync.SendWorldSnapshotTo(0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_worldSync.RestoreSkyLightState();
|
||||||
|
m_worldSync.OnHostChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MxBool NetworkManager::HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType)
|
MxBool NetworkManager::HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType)
|
||||||
{
|
{
|
||||||
return m_worldSync.HandleEntityMutation(p_entity, p_changeType);
|
return m_worldSync.HandleEntityMutation(p_entity, p_changeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MxBool NetworkManager::HandleSkyLightMutation(uint8_t p_entityType, uint8_t p_changeType)
|
||||||
|
{
|
||||||
|
return m_worldSync.HandleSkyLightMutation(p_entityType, p_changeType);
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkManager::ProcessPendingRequests()
|
void NetworkManager::ProcessPendingRequests()
|
||||||
{
|
{
|
||||||
if (m_pendingToggleThirdPerson.exchange(false, std::memory_order_relaxed)) {
|
if (m_pendingToggleThirdPerson.exchange(false, std::memory_order_relaxed)) {
|
||||||
@ -608,7 +639,15 @@ void NetworkManager::NotifyPlayerCountChanged()
|
|||||||
|
|
||||||
int count = -1;
|
int count = -1;
|
||||||
if (m_inIsleWorld) {
|
if (m_inIsleWorld) {
|
||||||
count = 1; // local player
|
count = 0;
|
||||||
|
|
||||||
|
// Only count the local player if they have a valid actor
|
||||||
|
// (players who enter Isle without selecting a save have no actor).
|
||||||
|
LegoPathActor* userActor = UserActor();
|
||||||
|
if (userActor && IsValidActorId(static_cast<LegoActor*>(userActor)->GetActorId())) {
|
||||||
|
count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
for (auto& [peerId, player] : m_remotePlayers) {
|
for (auto& [peerId, player] : m_remotePlayers) {
|
||||||
if (player->GetWorldId() == (int8_t) LegoOmni::e_act1) {
|
if (player->GetWorldId() == (int8_t) LegoOmni::e_act1) {
|
||||||
count++;
|
count++;
|
||||||
|
|||||||
@ -178,7 +178,12 @@ export class GameRoom implements DurableObject {
|
|||||||
msgType === MSG_WORLD_SNAPSHOT &&
|
msgType === MSG_WORLD_SNAPSHOT &&
|
||||||
data.length >= SNAPSHOT_MIN_SIZE
|
data.length >= SNAPSHOT_MIN_SIZE
|
||||||
) {
|
) {
|
||||||
this.sendToTarget(stamped);
|
const targetId = readTargetPeerId(stamped);
|
||||||
|
if (targetId === 0) {
|
||||||
|
this.broadcastExcept(stamped.buffer, peerId);
|
||||||
|
} else {
|
||||||
|
this.sendToTarget(stamped);
|
||||||
|
}
|
||||||
} else if (msgType === MSG_CUSTOMIZE) {
|
} else if (msgType === MSG_CUSTOMIZE) {
|
||||||
// Broadcast to all including sender so the clicker sees effects
|
// Broadcast to all including sender so the clicker sees effects
|
||||||
// on the target's clone on their own screen.
|
// on the target's clone on their own screen.
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
#include "extensions/multiplayer/worldstatesync.h"
|
#include "extensions/multiplayer/worldstatesync.h"
|
||||||
|
|
||||||
|
#include "isle.h"
|
||||||
#include "legobuildingmanager.h"
|
#include "legobuildingmanager.h"
|
||||||
#include "legoentity.h"
|
#include "legoentity.h"
|
||||||
|
#include "legogamestate.h"
|
||||||
#include "legomain.h"
|
#include "legomain.h"
|
||||||
#include "legoplantmanager.h"
|
#include "legoplantmanager.h"
|
||||||
#include "legoplants.h"
|
#include "legoplants.h"
|
||||||
|
#include "legoutils.h"
|
||||||
#include "legoworld.h"
|
#include "legoworld.h"
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
#include "misc/legostorage.h"
|
#include "misc/legostorage.h"
|
||||||
|
#include "mxmisc.h"
|
||||||
|
#include "mxvariable.h"
|
||||||
|
|
||||||
#include <SDL3/SDL_stdinc.h>
|
#include <SDL3/SDL_stdinc.h>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
extern MxU8 g_counters[];
|
extern MxU8 g_counters[];
|
||||||
@ -33,10 +40,33 @@ void WorldStateSync::SendMessage(const T& p_msg)
|
|||||||
|
|
||||||
WorldStateSync::WorldStateSync()
|
WorldStateSync::WorldStateSync()
|
||||||
: m_transport(nullptr), m_localPeerId(0), m_sequence(0), m_isHost(false), m_inIsleWorld(false),
|
: m_transport(nullptr), m_localPeerId(0), m_sequence(0), m_isHost(false), m_inIsleWorld(false),
|
||||||
m_snapshotRequested(false)
|
m_snapshotRequested(false), m_savedLightPos(2)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WorldStateSync::SaveSkyLightState()
|
||||||
|
{
|
||||||
|
const char* bgValue = GameState()->GetBackgroundColor()->GetValue()->GetData();
|
||||||
|
m_savedSkyColor = bgValue ? bgValue : "set 56 54 68";
|
||||||
|
m_savedLightPos = atoi(VariableTable()->GetVariable("lightposition"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorldStateSync::RestoreSkyLightState()
|
||||||
|
{
|
||||||
|
ApplySkyLightState(m_savedSkyColor.c_str(), m_savedLightPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorldStateSync::ApplySkyLightState(const char* p_skyColor, int p_lightPos)
|
||||||
|
{
|
||||||
|
GameState()->GetBackgroundColor()->SetValue(p_skyColor);
|
||||||
|
GameState()->GetBackgroundColor()->SetLightColor();
|
||||||
|
SetLightPosition(p_lightPos);
|
||||||
|
|
||||||
|
char buf[32];
|
||||||
|
sprintf(buf, "%d", p_lightPos);
|
||||||
|
VariableTable()->SetVariable("lightposition", buf);
|
||||||
|
}
|
||||||
|
|
||||||
void WorldStateSync::OnHostChanged()
|
void WorldStateSync::OnHostChanged()
|
||||||
{
|
{
|
||||||
if (!m_isHost) {
|
if (!m_isHost) {
|
||||||
@ -46,6 +76,11 @@ void WorldStateSync::OnHostChanged()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WorldStateSync::SendWorldSnapshotTo(uint32_t p_targetPeerId)
|
||||||
|
{
|
||||||
|
SendWorldSnapshot(p_targetPeerId);
|
||||||
|
}
|
||||||
|
|
||||||
void WorldStateSync::HandleRequestSnapshot(const RequestSnapshotMsg& p_msg)
|
void WorldStateSync::HandleRequestSnapshot(const RequestSnapshotMsg& p_msg)
|
||||||
{
|
{
|
||||||
if (!m_isHost) {
|
if (!m_isHost) {
|
||||||
@ -74,6 +109,18 @@ void WorldStateSync::HandleWorldSnapshot(const uint8_t* p_data, size_t p_length)
|
|||||||
PlantManager()->Read(&memory);
|
PlantManager()->Read(&memory);
|
||||||
BuildingManager()->Read(&memory);
|
BuildingManager()->Read(&memory);
|
||||||
|
|
||||||
|
// Read sky/light state appended after plant + building data.
|
||||||
|
LegoU32 memPos;
|
||||||
|
memory.GetPosition(memPos);
|
||||||
|
const uint8_t* extraData = snapshotData + memPos;
|
||||||
|
size_t remaining = header.dataLength - memPos;
|
||||||
|
|
||||||
|
if (remaining >= 4) {
|
||||||
|
char skyBuffer[32];
|
||||||
|
sprintf(skyBuffer, "set %d %d %d", extraData[0], extraData[1], extraData[2]);
|
||||||
|
ApplySkyLightState(skyBuffer, extraData[3]);
|
||||||
|
}
|
||||||
|
|
||||||
// Read() updates data arrays but not entity positions; reload to refresh.
|
// Read() updates data arrays but not entity positions; reload to refresh.
|
||||||
if (m_inIsleWorld) {
|
if (m_inIsleWorld) {
|
||||||
LegoWorld* world = CurrentWorld();
|
LegoWorld* world = CurrentWorld();
|
||||||
@ -163,6 +210,22 @@ MxBool WorldStateSync::HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MxBool WorldStateSync::HandleSkyLightMutation(uint8_t p_entityType, uint8_t p_changeType)
|
||||||
|
{
|
||||||
|
if (!m_transport || !m_transport->IsConnected()) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_isHost) {
|
||||||
|
BroadcastWorldEvent(p_entityType, p_changeType, 0);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SendWorldEventRequest(p_entityType, p_changeType, 0);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Send helpers ----
|
// ---- Send helpers ----
|
||||||
|
|
||||||
void WorldStateSync::SendSnapshotRequest()
|
void WorldStateSync::SendSnapshotRequest()
|
||||||
@ -181,7 +244,7 @@ void WorldStateSync::SendWorldSnapshot(uint32_t p_targetPeerId)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize plant + building state (~1133 bytes max, use 4096 for safety).
|
// Serialize plant + building + sky/light state (~1149 bytes max, use 4096 for safety).
|
||||||
uint8_t stateBuffer[4096];
|
uint8_t stateBuffer[4096];
|
||||||
LegoMemory memory(stateBuffer, sizeof(stateBuffer));
|
LegoMemory memory(stateBuffer, sizeof(stateBuffer));
|
||||||
|
|
||||||
@ -191,6 +254,20 @@ void WorldStateSync::SendWorldSnapshot(uint32_t p_targetPeerId)
|
|||||||
LegoU32 dataLength;
|
LegoU32 dataLength;
|
||||||
memory.GetPosition(dataLength);
|
memory.GetPosition(dataLength);
|
||||||
|
|
||||||
|
// Append sky color HSV (parse from "set H S V" string) and light position.
|
||||||
|
int skyH = 56, skyS = 54, skyV = 68; // defaults matching "set 56 54 68"
|
||||||
|
const char* bgValue = GameState()->GetBackgroundColor()->GetValue()->GetData();
|
||||||
|
if (bgValue) {
|
||||||
|
sscanf(bgValue, "set %d %d %d", &skyH, &skyS, &skyV);
|
||||||
|
}
|
||||||
|
|
||||||
|
int lightPos = atoi(VariableTable()->GetVariable("lightposition"));
|
||||||
|
|
||||||
|
stateBuffer[dataLength++] = (uint8_t) skyH;
|
||||||
|
stateBuffer[dataLength++] = (uint8_t) skyS;
|
||||||
|
stateBuffer[dataLength++] = (uint8_t) skyV;
|
||||||
|
stateBuffer[dataLength++] = (uint8_t) lightPos;
|
||||||
|
|
||||||
WorldSnapshotMsg msg{};
|
WorldSnapshotMsg msg{};
|
||||||
msg.header = {MSG_WORLD_SNAPSHOT, m_localPeerId, m_sequence++};
|
msg.header = {MSG_WORLD_SNAPSHOT, m_localPeerId, m_sequence++};
|
||||||
msg.targetPeerId = p_targetPeerId;
|
msg.targetPeerId = p_targetPeerId;
|
||||||
@ -385,4 +462,34 @@ void WorldStateSync::ApplyWorldEvent(uint8_t p_entityType, uint8_t p_changeType,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (p_entityType == ENTITY_SKY) {
|
||||||
|
switch (p_changeType) {
|
||||||
|
case SKY_TOGGLE_COLOR:
|
||||||
|
GameState()->GetBackgroundColor()->ToggleSkyColor();
|
||||||
|
break;
|
||||||
|
case SKY_DAY:
|
||||||
|
GameState()->GetBackgroundColor()->ToggleDayNight(TRUE);
|
||||||
|
break;
|
||||||
|
case SKY_NIGHT:
|
||||||
|
GameState()->GetBackgroundColor()->ToggleDayNight(FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (p_entityType == ENTITY_LIGHT) {
|
||||||
|
switch (p_changeType) {
|
||||||
|
case LIGHT_INCREMENT:
|
||||||
|
UpdateLightPosition(1);
|
||||||
|
break;
|
||||||
|
case LIGHT_DECREMENT:
|
||||||
|
UpdateLightPosition(-1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_inIsleWorld) {
|
||||||
|
LegoWorld* world = CurrentWorld();
|
||||||
|
if (world && world->GetWorldId() == LegoOmni::e_act1) {
|
||||||
|
((Isle*) world)->UpdateGlobe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user