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 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 23:34:11 +02:00
co-authored by Claude Opus 4.6
parent e8a053080c
commit 4c1e0e9c85
3 changed files with 15 additions and 4 deletions
+8
View File
@@ -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:
+2 -2
View File
@@ -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:
+5 -2
View File
@@ -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)