isle-portable/extensions/include/extensions/multiplayer/protocol.h
Christian Semmler 12a63c105c
Implement multiplayer world state sync for plants and buildings
Add serialization framework using C++ templates and table-driven lookup
to sync plant and building state between players. Includes world snapshot
routing to requesting peer, relay server Docker support, and fixes for
building color sync, ride vehicle visibility, and ARM compilation.
2026-03-01 09:48:03 -08:00

161 lines
3.5 KiB
C++

#pragma once
#include <SDL3/SDL_stdinc.h>
#include <cstddef>
#include <cstdint>
#include <type_traits>
namespace Multiplayer
{
enum MessageType : uint8_t {
MSG_JOIN = 1,
MSG_LEAVE = 2,
MSG_STATE = 3,
MSG_HOST_ASSIGN = 4,
MSG_REQUEST_SNAPSHOT = 5,
MSG_WORLD_SNAPSHOT = 6,
MSG_WORLD_EVENT = 7,
MSG_WORLD_EVENT_REQUEST = 8,
MSG_ASSIGN_ID = 0xFF
};
enum VehicleType : int8_t {
VEHICLE_NONE = -1,
VEHICLE_HELICOPTER = 0,
VEHICLE_JETSKI = 1,
VEHICLE_DUNEBUGGY = 2,
VEHICLE_BIKE = 3,
VEHICLE_SKATEBOARD = 4,
VEHICLE_MOTOCYCLE = 5,
VEHICLE_TOWTRACK = 6,
VEHICLE_AMBULANCE = 7,
VEHICLE_COUNT = 8
};
// Entity types for world events
enum WorldEntityType : uint8_t {
ENTITY_PLANT = 0,
ENTITY_BUILDING = 1
};
// Change types for world events (maps to Switch* methods on LegoEntity)
enum WorldChangeType : uint8_t {
CHANGE_VARIANT = 0,
CHANGE_SOUND = 1,
CHANGE_MOVE = 2,
CHANGE_COLOR = 3,
CHANGE_MOOD = 4,
CHANGE_DECREMENT = 5
};
#pragma pack(push, 1)
struct MessageHeader {
uint8_t type;
uint32_t peerId;
uint32_t sequence;
};
struct PlayerJoinMsg {
MessageHeader header;
uint8_t actorId;
char name[20];
};
struct PlayerLeaveMsg {
MessageHeader header;
};
struct PlayerStateMsg {
MessageHeader header;
uint8_t actorId;
int8_t worldId;
int8_t vehicleType;
float position[3];
float direction[3];
float up[3];
float speed;
};
// Server -> all: announces which peer is the host
struct HostAssignMsg {
MessageHeader header;
uint32_t hostPeerId;
};
// Client -> host: request full world state snapshot
struct RequestSnapshotMsg {
MessageHeader header;
};
// Host -> specific client: full world state blob (variable length)
// Relay reads targetPeerId at offset 9 and routes to that peer only.
struct WorldSnapshotMsg {
MessageHeader header;
uint32_t targetPeerId;
uint16_t dataLength;
// Followed by dataLength bytes of serialized plant + building state
};
// Host -> all: single world state mutation
struct WorldEventMsg {
MessageHeader header;
uint8_t entityType; // WorldEntityType
uint8_t changeType; // WorldChangeType
uint8_t entityIndex; // Index into g_plantInfo[] or g_buildingInfo[]
uint8_t padding; // Alignment
};
// Non-host -> host: request a mutation (same layout as WorldEventMsg)
struct WorldEventRequestMsg {
MessageHeader header;
uint8_t entityType; // WorldEntityType
uint8_t changeType; // WorldChangeType
uint8_t entityIndex; // Index into g_plantInfo[] or g_buildingInfo[]
uint8_t padding; // Alignment
};
#pragma pack(pop)
// Validate actorId is a playable character (1-5, not brickster)
inline bool IsValidActorId(uint8_t p_actorId)
{
return p_actorId >= 1 && p_actorId <= 5;
}
// Parse the message type from a buffer. Returns MSG type or 0 on error.
inline uint8_t ParseMessageType(const uint8_t* p_data, size_t p_length)
{
if (p_length < 1) {
return 0;
}
return p_data[0];
}
// Generic serialization: copy a packed message struct into a buffer.
template <typename T>
inline size_t SerializeMsg(uint8_t* p_buf, size_t p_bufLen, const T& p_msg)
{
static_assert(std::is_trivially_copyable_v<T>);
if (p_bufLen < sizeof(T)) {
return 0;
}
SDL_memcpy(p_buf, &p_msg, sizeof(T));
return sizeof(T);
}
// Generic deserialization: copy raw bytes into a packed message struct.
template <typename T>
inline bool DeserializeMsg(const uint8_t* p_data, size_t p_length, T& p_out)
{
static_assert(std::is_trivially_copyable_v<T>);
if (p_length < sizeof(T)) {
return false;
}
SDL_memcpy(&p_out, p_data, sizeof(T));
return true;
}
} // namespace Multiplayer