isle-portable/extensions/src/multiplayer/customizestate.cpp
foxtacles a9747dec11
Character customization (#8)
* WIP: Add character customization to multiplayer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Refine character customization: fix message buffering, DRY up code, request-based model

- Register NetworkManager with TickleManager via HandleCreate hook in
  LegoOmni::Create(), so packets are processed continuously instead of
  buffering between Connect() and OnWorldEnabled()
- Spawn unspawned remote players in OnWorldEnabled() (created before
  world was ready)
- Switch to request-based customization: HandleROIClick sends
  MSG_CUSTOMIZE to server, server echoes to all peers, HandleCustomize
  applies state and plays effects
- DRY up SwitchVariant to delegate LOD cloning to ApplyHatVariant
- Add ApplyChange helper consolidating the switch-on-changeType pattern
- Fix InitFromActorInfo to derive dependent color parts from independent
  parts (matching Unpack rules)
- Remove dead code: m_hasBeenTicked, ApplyCustomizeChange on
  RemotePlayer, m_localCustomizeState on NetworkManager
- Add null ROI checks in HandleCustomize for unspawned players
- Move MSG_CUSTOMIZE constant to shared protocol.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix

* Fix character customization bugs from code review

- BUG-1: Add bounds check in SwitchColor to prevent OOB access from
  unvalidated network input (p_partIndex could exceed array bounds)
- BUG-2: Enforce allowRemoteCustomize on receiver side in HandleCustomize
  (was only checked on sender side, byppassable by malicious client)
- BUG-3: Document stale-state sound asymmetry for remote targets
- OBS-1: Remove unused bodyVariantIndex from CustomizeState (never
  modified, wasted 3 bits per state message)
- NAME-1: Fix p_ prefix convention on ApplyCustomizeChange parameters
2026-03-07 23:20:55 +01:00

89 lines
3.1 KiB
C++

#include "extensions/multiplayer/customizestate.h"
#include "legoactors.h"
#include "misc.h"
#include <SDL3/SDL_stdinc.h>
using namespace Multiplayer;
void CustomizeState::InitFromActorInfo(uint8_t p_actorInfoIndex)
{
if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) {
return;
}
const LegoActorInfo& info = g_actorInfoInit[p_actorInfoIndex];
// Set the 6 independent colorable parts from actor info
colorIndices[c_infohatPart] = info.m_parts[c_infohatPart].m_nameIndex;
colorIndices[c_infogronPart] = info.m_parts[c_infogronPart].m_nameIndex;
colorIndices[c_armlftPart] = info.m_parts[c_armlftPart].m_nameIndex;
colorIndices[c_armrtPart] = info.m_parts[c_armrtPart].m_nameIndex;
colorIndices[c_leglftPart] = info.m_parts[c_leglftPart].m_nameIndex;
colorIndices[c_legrtPart] = info.m_parts[c_legrtPart].m_nameIndex;
// Derive dependent parts (must match Unpack derivation rules)
colorIndices[c_bodyPart] = colorIndices[c_infogronPart];
colorIndices[c_headPart] = colorIndices[c_infohatPart];
colorIndices[c_clawlftPart] = colorIndices[c_armlftPart];
colorIndices[c_clawrtPart] = colorIndices[c_armrtPart];
hatVariantIndex = info.m_parts[c_infohatPart].m_partNameIndex;
sound = (uint8_t) info.m_sound;
move = (uint8_t) info.m_move;
mood = info.m_mood;
}
void CustomizeState::Pack(uint8_t p_out[5]) const
{
// byte 0: hatVariantIndex(5 bits) | reserved(3 bits)
p_out[0] = (hatVariantIndex & 0x1F);
// byte 1: sound(4 bits) | move(2 bits) | mood(2 bits)
p_out[1] = (sound & 0x0F) | ((move & 0x03) << 4) | ((mood & 0x03) << 6);
// byte 2: infohatColor(4 bits) | infogronColor(4 bits)
p_out[2] = (colorIndices[c_infohatPart] & 0x0F) | ((colorIndices[c_infogronPart] & 0x0F) << 4);
// byte 3: armlftColor(4 bits) | armrtColor(4 bits)
p_out[3] = (colorIndices[c_armlftPart] & 0x0F) | ((colorIndices[c_armrtPart] & 0x0F) << 4);
// byte 4: leglftColor(4 bits) | legrtColor(4 bits)
p_out[4] = (colorIndices[c_leglftPart] & 0x0F) | ((colorIndices[c_legrtPart] & 0x0F) << 4);
}
void CustomizeState::Unpack(const uint8_t p_in[5])
{
// byte 0: hatVariantIndex(5 bits) | reserved(3 bits)
hatVariantIndex = p_in[0] & 0x1F;
// byte 1: sound(4 bits) | move(2 bits) | mood(2 bits)
sound = p_in[1] & 0x0F;
move = (p_in[1] >> 4) & 0x03;
mood = (p_in[1] >> 6) & 0x03;
// byte 2: infohatColor(4 bits) | infogronColor(4 bits)
colorIndices[c_infohatPart] = p_in[2] & 0x0F;
colorIndices[c_infogronPart] = (p_in[2] >> 4) & 0x0F;
// byte 3: armlftColor(4 bits) | armrtColor(4 bits)
colorIndices[c_armlftPart] = p_in[3] & 0x0F;
colorIndices[c_armrtPart] = (p_in[3] >> 4) & 0x0F;
// byte 4: leglftColor(4 bits) | legrtColor(4 bits)
colorIndices[c_leglftPart] = p_in[4] & 0x0F;
colorIndices[c_legrtPart] = (p_in[4] >> 4) & 0x0F;
// Derive non-independent parts
colorIndices[c_bodyPart] = colorIndices[c_infogronPart];
colorIndices[c_headPart] = colorIndices[c_infohatPart];
colorIndices[c_clawlftPart] = colorIndices[c_armlftPart];
colorIndices[c_clawrtPart] = colorIndices[c_armrtPart];
}
bool CustomizeState::operator==(const CustomizeState& p_other) const
{
return SDL_memcmp(this, &p_other, sizeof(CustomizeState)) == 0;
}