From 4c1e0e9c85b2eef001067477c7342645e4a900d6 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 4 Apr 2026 23:34:11 +0200 Subject: [PATCH] feat(client): wire CharacterVisualDescriptor into startup IPC (#718) encode_startup_message() now accepts optional CharacterVisualDescriptor as third param, serialized via to_dict(). sim_bridge passes the descriptor from GameState on new game start. apply_snapshot() restores descriptor from server snapshot on save/load cycle. Co-Authored-By: Claude Opus 4.6 --- client/scripts/autoloads/game_state.gd | 8 ++++++++ client/scripts/autoloads/sim_bridge.gd | 4 ++-- client/scripts/protocol/protocol.gd | 7 +++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/scripts/autoloads/game_state.gd b/client/scripts/autoloads/game_state.gd index 9180493b5..05cacc630 100644 --- a/client/scripts/autoloads/game_state.gd +++ b/client/scripts/autoloads/game_state.gd @@ -362,6 +362,14 @@ func apply_snapshot(snapshot: Dictionary) -> void: else: settings_response = null + # #718: character_visual_descriptor — restored from server snapshot on save/load. + # Server persists the descriptor and includes it in ObserverSnapshot after load. + # Only update when field is present (null means no change). + if snapshot.has("character_visual_descriptor") and snapshot.character_visual_descriptor is Dictionary: + var restored := CharacterVisualDescriptor.from_dict(snapshot.character_visual_descriptor) + if restored != null: + character_visual_descriptor = restored + # v14: player_knowledge (#264, D-041) — partial KG dump for journal panel. # Only update when field is present (null means no change, server sends when KG changes). if snapshot.has("player_knowledge") and snapshot.player_knowledge is Dictionary: diff --git a/client/scripts/autoloads/sim_bridge.gd b/client/scripts/autoloads/sim_bridge.gd index 5424ac1ea..e09bf99aa 100644 --- a/client/scripts/autoloads/sim_bridge.gd +++ b/client/scripts/autoloads/sim_bridge.gd @@ -231,9 +231,9 @@ func _process(delta: float) -> void: _set_state(ConnectionState.ERROR) return - # Send startup message with world_seed (#175, D-010/D-029). + # Send startup message with world_seed and character appearance (#175, D-010/D-029, #718). # Server blocks waiting for this before entering the tick loop. - var startup_bytes := Protocol.encode_startup_message(GameState.world_seed, GameState.character_archetype) + var startup_bytes := Protocol.encode_startup_message(GameState.world_seed, GameState.character_archetype, GameState.character_visual_descriptor) if startup_bytes.size() > 0: var send_err := _bridge.send_message(startup_bytes) if send_err != OK: diff --git a/client/scripts/protocol/protocol.gd b/client/scripts/protocol/protocol.gd index 995b88c0a..60be0a6ba 100644 --- a/client/scripts/protocol/protocol.gd +++ b/client/scripts/protocol/protocol.gd @@ -494,11 +494,12 @@ static func _decode_enum_variant(raw) -> Dictionary: # -- Encode: GDScript types → bytes to server ---------------------------------- -## Encode a StartupMessage to MessagePack bytes (#175, #588). +## Encode a StartupMessage to MessagePack bytes (#175, #588, #718). ## Sent by the client immediately after handshake validation. ## Server reads this to initialize SimRng (D-010, D-029) and select monologue pool (D-032). ## character_archetype: "detective" → "Detective", "smuggler" → "Smuggler" (server enum variant). -static func encode_startup_message(world_seed: int, character_archetype: String = "detective") -> PackedByteArray: +## character_visual: optional CharacterVisualDescriptor — included as "character_visual_descriptor" dict. +static func encode_startup_message(world_seed: int, character_archetype: String = "detective", character_visual: Variant = null) -> PackedByteArray: # Map client lowercase archetype string to server PascalCase enum variant. # Explicit match prevents unknown strings silently reaching the server as # garbage enum values — fail loudly and fall back to "Detective". @@ -515,6 +516,8 @@ static func encode_startup_message(world_seed: int, character_archetype: String "world_seed": world_seed, "character_archetype": archetype_variant, } + if character_visual != null and character_visual is CharacterVisualDescriptor: + msg["character_visual_descriptor"] = (character_visual as CharacterVisualDescriptor).to_dict() var result = Messagepack.encode(msg) if result.status != null: push_error("Protocol: startup message encode failed: %s" % result.status)