From 674c7147de2d330ec4b3297419c370fa112d773d Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 18 Feb 2026 01:41:27 +0100 Subject: [PATCH] =?UTF-8?q?feat(client):=20sync=20protocol=20to=20v8=20?= =?UTF-8?q?=E2=80=94=20dialogue=5Fresponse=20+=20Interact=20encoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump PROTOCOL_VERSION from 7 to 8 to match server. Three changes: 1. Add dialogue_response field decoding (DialogueResponseEvent with line_id, text, speaker_entity_id) from server #305/D-028. 2. Fix Interact encoding: server changed PlayerAction::Interact from unit variant to struct variant with {target_entity_id, verb}. Extract _encode_action() helper to handle this consistently. 3. Update all test assertions that checked version=7 or expected Interact as a bare string. Co-Authored-By: Claude Opus 4.6 --- client/scripts/protocol/protocol.gd | 41 ++++++++++++++++--------- client/tests/test_interaction_prompt.gd | 10 ++++-- client/tests/test_local_bridge.gd | 4 ++- client/tests/test_protocol.gd | 8 +++-- client/tests/test_protocol_bridge.gd | 14 ++++----- 5 files changed, 50 insertions(+), 27 deletions(-) diff --git a/client/scripts/protocol/protocol.gd b/client/scripts/protocol/protocol.gd index 28a5e010d..e068f5bc9 100644 --- a/client/scripts/protocol/protocol.gd +++ b/client/scripts/protocol/protocol.gd @@ -11,7 +11,7 @@ class_name Protocol ## Protocol version — must match server PROTOCOL_VERSION in bridge/types.rs. ## Reject snapshots where version != this value. -const PROTOCOL_VERSION: int = 7 +const PROTOCOL_VERSION: int = 8 # -- Decode: bytes from server → GDScript types -------------------------------- @@ -169,6 +169,17 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: "options": dialogue_options, } + # v8: dialogue_response (#305, D-028) — NPC's spoken dialogue line from server + # Separate from current_dialogue (client-side rich dialogue state). + var dialogue_response: Variant = null + var raw_dr: Variant = raw.get("dialogue_response") + if raw_dr is Dictionary and raw_dr.has("text"): + dialogue_response = { + "line_id": str(raw_dr.get("line_id", "")), + "text": str(raw_dr["text"]), + "speaker_entity_id": int(raw_dr.get("speaker_entity_id", -1)), + } + return { "tick": tick, "entities": entities, @@ -182,6 +193,7 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: "nearby_interactions": nearby_interactions, "current_monologue": current_monologue, "current_dialogue": current_dialogue, + "dialogue_response": dialogue_response, "pending_recognitions": pending_recognitions, } @@ -293,13 +305,7 @@ static func _decode_enum_variant(raw) -> Dictionary: ## "Interact", "UsePerceptionMode", "Pause", "Unpause" ## action_data: null for unit variants, String for UsePerceptionMode static func encode_player_input(tick: int, action_name: String, action_data: Variant = null) -> PackedByteArray: - var action_value: Variant - if action_data != null: - # Data variant → single-element map - action_value = { action_name: action_data } - else: - # Unit variant → bare string - action_value = action_name + var action_value: Variant = _encode_action(action_name, action_data) var input := { "tick": tick, @@ -322,14 +328,9 @@ static func encode_player_inputs(inputs: Array) -> PackedByteArray: for input in inputs: var action_name: String = input["action_name"] var action_data: Variant = input.get("action_data") - var action_value: Variant - if action_data != null: - action_value = { action_name: action_data } - else: - action_value = action_name wire_inputs.append({ "tick": input["tick"], - "action": action_value, + "action": _encode_action(action_name, action_data), }) var result = Messagepack.encode(wire_inputs) @@ -340,6 +341,18 @@ static func encode_player_inputs(inputs: Array) -> PackedByteArray: return result.value +## Encode a PlayerAction for the wire. +## Struct variants (Interact) always need their fields even when null. +## Unit variants (MoveNorth, Pause, etc.) encode as bare strings. +static func _encode_action(action_name: String, action_data: Variant) -> Variant: + if action_data != null: + return { action_name: action_data } + # Interact is a struct variant — server expects named fields, not a bare string + if action_name == "Interact": + return { "Interact": { "target_entity_id": null, "verb": null } } + return action_name + + ## Decode a PlayerInput from MessagePack bytes (used in tests / echo scenarios). ## Returns { "tick": int, "action": { "variant": String, "data": Variant } } or null. static func decode_player_input(bytes: PackedByteArray) -> Variant: diff --git a/client/tests/test_interaction_prompt.gd b/client/tests/test_interaction_prompt.gd index c0237c770..722e9c2e6 100644 --- a/client/tests/test_interaction_prompt.gd +++ b/client/tests/test_interaction_prompt.gd @@ -195,15 +195,19 @@ func test_prompt_hidden_initially() -> void: # -- Input encoding: Interact -- -func test_interact_encodes_as_unit_variant() -> void: +func test_interact_encodes_as_struct_variant() -> void: + # Interact is always a struct variant: {"Interact": {"target_entity_id": null, "verb": null}} var inputs: Array = [{"tick": 100, "action_name": "Interact"}] var bytes := Protocol.encode_player_inputs(inputs) var raw = Messagepack.decode(bytes) assert_that(raw.status == null).is_true() - assert_that(raw.value[0]["action"]).is_equal("Interact") + assert_that(raw.value[0]["action"] is Dictionary).is_true() + assert_that(raw.value[0]["action"].has("Interact")).is_true() + var interact_data: Dictionary = raw.value[0]["action"]["Interact"] + assert_that(interact_data.has("target_entity_id")).is_true() + assert_that(interact_data.has("verb")).is_true() func test_interact_with_data_encodes_as_data_variant() -> void: - # Future: once server accepts Interact(InteractData) var inputs: Array = [{ "tick": 100, "action_name": "Interact", diff --git a/client/tests/test_local_bridge.gd b/client/tests/test_local_bridge.gd index 08cb729ab..880e12ab1 100644 --- a/client/tests/test_local_bridge.gd +++ b/client/tests/test_local_bridge.gd @@ -142,7 +142,9 @@ func test_framed_protocol_batch_input_roundtrip() -> void: assert_that(raw.value is Array).is_true() assert_that(raw.value.size()).is_equal(2) assert_that(raw.value[0]["action"]).is_equal("MoveNorth") - assert_that(raw.value[1]["action"]).is_equal("Interact") + # Interact is a struct variant: {"Interact": {"target_entity_id": null, "verb": null}} + assert_that(raw.value[1]["action"] is Dictionary).is_true() + assert_that(raw.value[1]["action"].has("Interact")).is_true() # -- Diagonal movement wire mapping -------------------------------------------- diff --git a/client/tests/test_protocol.gd b/client/tests/test_protocol.gd index ea39cf760..7dd843dd7 100644 --- a/client/tests/test_protocol.gd +++ b/client/tests/test_protocol.gd @@ -244,7 +244,9 @@ func test_encode_player_inputs_multiple() -> void: assert_that(raw.status).is_null() assert_that(raw.value.size()).is_equal(3) assert_that(raw.value[0]["action"]).is_equal("MoveNorth") - assert_that(raw.value[1]["action"]).is_equal("Interact") + # Interact is a struct variant: {"Interact": {"target_entity_id": null, "verb": null}} + assert_that(raw.value[1]["action"] is Dictionary).is_true() + assert_that(raw.value[1]["action"].has("Interact")).is_true() assert_that(raw.value[2]["action"]).is_equal("MoveSouthwest") @@ -358,7 +360,9 @@ func test_decode_batch_input_fixture() -> void: assert_that(raw.value[0]["tick"]).is_equal(0) assert_that(raw.value[0]["action"]).is_equal("MoveNorth") assert_that(raw.value[1]["tick"]).is_equal(0) - assert_that(raw.value[1]["action"]).is_equal("Interact") + # Interact is a struct variant: {"Interact": {"target_entity_id": null, "verb": null}} + assert_that(raw.value[1]["action"] is Dictionary).is_true() + assert_that(raw.value[1]["action"].has("Interact")).is_true() # -- Diagonal movement fixtures (D-030 Layer 1 cross-language) ----------------- diff --git a/client/tests/test_protocol_bridge.gd b/client/tests/test_protocol_bridge.gd index eeb243b11..534a74dd5 100644 --- a/client/tests/test_protocol_bridge.gd +++ b/client/tests/test_protocol_bridge.gd @@ -26,17 +26,17 @@ func _load_fixture(name: String) -> PackedByteArray: # -- Protocol version upgrade ------------------------------------------------- -func test_protocol_version_is_7() -> void: - assert_that(Protocol.PROTOCOL_VERSION).is_equal(7) +func test_protocol_version_is_8() -> void: + assert_that(Protocol.PROTOCOL_VERSION).is_equal(8) -func test_fixtures_at_protocol_version_7() -> void: - # All regenerated fixtures should be at v7 +func test_fixtures_at_protocol_version_8() -> void: + # All regenerated fixtures should be at v8 for fixture_name in ["snapshot_one_npc", "snapshot_empty", "snapshot_player", "snapshot_multi_entity"]: var bytes = _load_fixture(fixture_name) var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() - assert_that(snapshot.version).is_equal(7) + assert_that(snapshot.version).is_equal(8) func test_rejects_version_6() -> void: @@ -282,10 +282,10 @@ func test_sim_bridge_test_snapshot_has_player_inventory() -> void: assert_that(snap.player_inventory is Array).is_true() -func test_sim_bridge_test_snapshot_version_7() -> void: +func test_sim_bridge_test_snapshot_version_8() -> void: SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() - assert_that(snap.version).is_equal(7) + assert_that(snap.version).is_equal(8) # -- Fixture: v6 snapshots include new fields ----------------------------------