mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 10:33:57 +00:00
* Add stateful multi-part emote system with disassemble/reassemble Introduces a generalized multi-part emote framework where emotes can have two phases. The first trigger plays phase 1 and freezes the character at its last frame; the second trigger plays phase 2 to restore normal state. Movement is blocked for the entire duration of a multi-part emote (from phase 1 start through frozen state to phase 2 completion). The frozen state is synced to all peers via customizeFlags bits in PlayerStateMsg, so new joiners see disassembled players correctly. The emote table is now a 2D array (g_emoteAnims[][2]) where [1] is the phase-2 animation name (nullptr for one-shot emotes). Adding future multi-part emotes only requires a new row in the table. https://claude.ai/code/session_01L5FiuVFUqASR93iJcaXfEi * Fix emote movement blocking and frozen state sync Move movement blocking from CalculateTransform hook (which broke the camera by skipping p_transform output) to ThirdPersonCamera::Tick where it zeroes speed/velocity directly. Remove ShouldBlockMovement and ShouldInvertMovement hooks entirely. Rebuild frozen emote animation cache in InitAnimCaches when the frozen state was set before the ROI was available (state message arrived before world was ready). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Clean up emote branch: remove unused include, extract ClearFrozenState helper - Remove unused multiplayer.h include and using-directive from legopathactor.cpp - Extract ClearFrozenState() to DRY up 4 identical frozen state reset blocks - Clarify bit-encoding comment with mask value and emote ID limit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
231 lines
6.1 KiB
C++
231 lines
6.1 KiB
C++
#pragma once
|
|
|
|
#include <SDL3/SDL_stdinc.h>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <type_traits>
|
|
|
|
class LegoPathActor;
|
|
|
|
namespace Multiplayer
|
|
{
|
|
|
|
// Routing target constants for MessageHeader.target
|
|
const uint32_t TARGET_BROADCAST = 0; // Broadcast to all except sender
|
|
const uint32_t TARGET_HOST = 0xFFFFFFFF; // Send to host only
|
|
const uint32_t TARGET_BROADCAST_ALL = 0xFFFFFFFE; // Broadcast to all including sender
|
|
|
|
enum MessageType : uint8_t {
|
|
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_EMOTE = 9,
|
|
MSG_CUSTOMIZE = 10,
|
|
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,
|
|
ENTITY_SKY = 2,
|
|
ENTITY_LIGHT = 3
|
|
};
|
|
|
|
// 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
|
|
};
|
|
|
|
// 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)
|
|
|
|
struct MessageHeader {
|
|
uint8_t type;
|
|
uint32_t peerId;
|
|
uint32_t sequence;
|
|
uint32_t target;
|
|
};
|
|
|
|
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;
|
|
uint8_t walkAnimId; // Index into walk animation table (0 = default)
|
|
uint8_t idleAnimId; // Index into idle animation table (0 = default)
|
|
char name[8]; // Player display name (7 chars + null terminator)
|
|
uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65)
|
|
uint8_t customizeData[5]; // Packed CustomizeState
|
|
uint8_t customizeFlags; // Bit 0 = allowRemoteCustomize
|
|
};
|
|
|
|
// 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 header.target and routes to that peer only.
|
|
struct WorldSnapshotMsg {
|
|
MessageHeader header;
|
|
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
|
|
};
|
|
|
|
// One-shot emote trigger, broadcast to all peers
|
|
struct EmoteMsg {
|
|
MessageHeader header;
|
|
uint8_t emoteId; // Index into emote table
|
|
};
|
|
|
|
// Immediate customization change, broadcast to all peers
|
|
struct CustomizeMsg {
|
|
MessageHeader header;
|
|
uint32_t targetPeerId; // Who is being customized
|
|
uint8_t changeType; // WorldChangeType (VARIANT/SOUND/MOVE/COLOR/MOOD)
|
|
uint8_t partIndex; // Body part for color changes (0-9), 0xFF otherwise
|
|
};
|
|
|
|
#pragma pack(pop)
|
|
|
|
// Animation and vehicle tables (defined in protocol.cpp)
|
|
extern const char* const g_walkAnimNames[];
|
|
extern const int g_walkAnimCount;
|
|
|
|
extern const char* const g_idleAnimNames[];
|
|
extern const int g_idleAnimCount;
|
|
|
|
// Emote animation table: [emoteId][phase]. Phase 0 = primary, phase 1 = phase-2 (nullptr for one-shot).
|
|
extern const char* const g_emoteAnims[][2];
|
|
extern const int g_emoteAnimCount;
|
|
|
|
// Returns true if the emote is a multi-part stateful emote (has a phase-2 animation).
|
|
inline bool IsMultiPartEmote(uint8_t p_emoteId)
|
|
{
|
|
return p_emoteId < g_emoteAnimCount && g_emoteAnims[p_emoteId][1] != nullptr;
|
|
}
|
|
|
|
extern const char* const g_vehicleROINames[VEHICLE_COUNT];
|
|
extern const char* const g_rideAnimNames[VEHICLE_COUNT];
|
|
extern const char* const g_rideVehicleROINames[VEHICLE_COUNT];
|
|
|
|
// Returns true if the vehicle type has no ride animation (model swap instead)
|
|
bool IsLargeVehicle(int8_t p_vehicleType);
|
|
|
|
// Detect the vehicle type of a given actor, or VEHICLE_NONE if not a vehicle
|
|
int8_t DetectVehicleType(LegoPathActor* p_actor);
|
|
|
|
// Validate actorId is a playable character (1-5, not brickster)
|
|
inline bool IsValidActorId(uint8_t p_actorId)
|
|
{
|
|
return p_actorId >= 1 && p_actorId <= 5;
|
|
}
|
|
|
|
// Convert LegoGameState::Username letter indices (0-25 = A-Z) to ASCII.
|
|
// Writes up to 7 characters + null terminator into p_out (must be at least 8 bytes).
|
|
void EncodeUsername(char p_out[8]);
|
|
|
|
static const uint8_t DISPLAY_ACTOR_NONE = 0xFF;
|
|
|
|
// 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
|