From d72fcc7847c90f11070c4c402e4903df027d5afb Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 21 Apr 2026 17:29:49 +0200 Subject: [PATCH] =?UTF-8?q?fix(protocol):=20drop=20PROTOCOL=5FVERSION=20lo?= =?UTF-8?q?ckstep=20=E2=80=94=20D-192=20(#875)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the version-mismatch guard from Protocol.decode_snapshot() and the PROTOCOL_VERSION constant from the client (server side done in #874). Core changes: - protocol.gd: remove const PROTOCOL_VERSION, remove version mismatch guard, remove "version" from return dict, add gauntlet_mode/room_id decode - sim_bridge.gd: remove handshake version check; relax handshake guard to require only a valid Dictionary (server no longer sends protocol_version); emit handshake_complete(0) for API compat - loading_screen.gd: drop "· protocol N" suffix from version label - test_harness.gd: replace Protocol.PROTOCOL_VERSION with literal 23 Test updates (21 files): replace "version": Protocol.PROTOCOL_VERSION with "version": 23 in all snapshot bytes dicts; remove snapshot.version == N assertions; remove version-rejection tests (test_rejects_version_6, test_decode_snapshot_rejects_missing_version, test_decode_snapshot_rejects_old_version, test_protocol_rejects_version_mismatch, test_sim_bridge_test_snapshot_uses_current_protocol_version). Also includes: #872 bookmark_catalog carry-forward regression test, and #873 merge-path flow tests (test_merge_path_flows_sprint37.gd). Co-Authored-By: Claude Sonnet 4.6 --- client/scripts/autoloads/sim_bridge.gd | 32 +- client/scripts/protocol/protocol.gd | 35 +- client/scripts/protocol/test_harness.gd | 2 +- client/tests/test_anti_tedium.gd | 24 +- client/tests/test_bug_report_ring_buffer.gd | 10 +- client/tests/test_hub_teleport.gd | 4 +- client/tests/test_input_roundtrip.gd | 3 - client/tests/test_interaction_list.gd | 4 +- client/tests/test_interaction_prompt.gd | 29 +- client/tests/test_ipc_fixtures.gd | 7 - client/tests/test_local_bridge.gd | 2 +- .../tests/test_merge_path_flows_sprint37.gd | 317 ++++++++++++++++++ client/tests/test_p0_regressions.gd | 78 ++++- client/tests/test_protocol.gd | 32 +- client/tests/test_protocol_bridge.gd | 52 +-- client/tests/test_protocol_v7.gd | 23 +- client/tests/test_rendering.gd | 2 - client/tests/test_signal_sprint24.gd | 18 +- client/tests/test_sprint2_proof.gd | 1 - client/tests/test_time_display_sprint17.gd | 2 +- .../ui/meta/screens/loading/loading_screen.gd | 6 +- 21 files changed, 490 insertions(+), 193 deletions(-) create mode 100644 client/tests/test_merge_path_flows_sprint37.gd diff --git a/client/scripts/autoloads/sim_bridge.gd b/client/scripts/autoloads/sim_bridge.gd index 1e8758502..0c297e519 100644 --- a/client/scripts/autoloads/sim_bridge.gd +++ b/client/scripts/autoloads/sim_bridge.gd @@ -246,13 +246,10 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns if msg.is_empty(): return # Not ready yet, continue polling - # Decode HandshakeMessage: { "protocol_version": N } + # Decode HandshakeMessage — D-192 (#875): protocol_version field dropped. + # Server sends {} or a minimal dict; only structural validity is required. var decoded: Variant = Messagepack.decode(msg) - if ( - decoded.status != null - or not (decoded.value is Dictionary) - or not decoded.value.has("protocol_version") - ): + if decoded.status != null or not (decoded.value is Dictionary): var reason := "Handshake decode failed: malformed HandshakeMessage" push_error("SimBridge: %s" % reason) handshake_failed.emit(reason) @@ -260,18 +257,6 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns _set_state(ConnectionState.ERROR) return - var server_version: int = decoded.value["protocol_version"] - if server_version != Protocol.PROTOCOL_VERSION: - var reason := ( - "Protocol version mismatch: server=%d, client=%d" - % [server_version, Protocol.PROTOCOL_VERSION] - ) - push_error("SimBridge: %s" % reason) - handshake_failed.emit(reason) - _bridge.disconnect_from_server() - _set_state(ConnectionState.ERROR) - return - # 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( @@ -296,7 +281,7 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns _set_state(ConnectionState.ERROR) return - handshake_complete.emit(server_version) + handshake_complete.emit(0) # D-192: protocol_version field dropped; signal kept for API compat _set_state(ConnectionState.CONNECTED) # #646: Request full settings dump on connect — hydrates GameState.ai_enhanced_dialogue_enabled # from server SQLite so the client reflects the authoritative persisted state (D-138). @@ -464,6 +449,15 @@ func receive_bytes(bytes: PackedByteArray) -> void: and _last_snapshot.get("settings_response") != null ): snapshot["settings_response"] = _last_snapshot["settings_response"] + # #872: Carry forward bookmark_catalog (one-shot, consumed by main_menu._on_snapshot_received_for_catalog). + # Server sends catalog on tick 0 and after RequestBookmarkCatalog. If tick 0 and tick 1 + # arrive in the same TCP batch, the inner receive loop overwrites _last_snapshot and the + # catalog is silently lost — this carry-forward prevents that race. + if ( + snapshot.get("bookmark_catalog") == null + and _last_snapshot.get("bookmark_catalog") != null + ): + snapshot["bookmark_catalog"] = _last_snapshot["bookmark_catalog"] _last_snapshot = snapshot diff --git a/client/scripts/protocol/protocol.gd b/client/scripts/protocol/protocol.gd index f8230bfba..8180bf935 100644 --- a/client/scripts/protocol/protocol.gd +++ b/client/scripts/protocol/protocol.gd @@ -9,13 +9,6 @@ extends Node ## Unit enum variants (no data) → bare strings ("MoveNorth", "Npc") ## Data enum variants → single-element maps ({"UsePerceptionMode": "thermal"}) -## Protocol version — must match server PROTOCOL_VERSION in bridge/types.rs. -## Reject snapshots where version != this value. -## v20: adds settings_response field to ObserverSnapshot (#627, D-138). -## v21: adds economy_snapshot field to ObserverSnapshot (#822, D-181). -## v23: adds bookmark_catalog field to ObserverSnapshot (#614). -const PROTOCOL_VERSION: int = 23 - # -- Decode: bytes from server → GDScript types -------------------------------- @@ -34,17 +27,6 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: push_error("Protocol: snapshot missing required fields") return null - # Version check: reject snapshots from incompatible server - var version: Variant = raw.get("version") - if version != PROTOCOL_VERSION: - push_error( - ( - "Protocol: version mismatch (got %s, expected %s). Server and client are out of sync." - % [version, PROTOCOL_VERSION] - ) - ) - return null - var entities: Array[Dictionary] = [] var raw_entities: Array = raw["entities"] var dropped := 0 @@ -67,7 +49,7 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: # in any realistic scenario (would require ~29 billion years at 10 ticks/game-minute per D-031). var tick: int = raw["tick"] - # version already checked above; game_time for HUD display + # game_time for HUD display var game_time: Variant = raw.get("game_time") # player_facing: FacingDirection is a unit enum → bare string in rmp_serde @@ -223,6 +205,18 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: "speaker_entity_id": int(raw_dr.get("speaker_entity_id", -1)), } + # v8: gauntlet_mode and room_id (#496) — present only in Gauntlet sessions. + # gauntlet_mode is a bool flag; room_id is a String room identifier or absent. + # Snapshot handler (snapshot_handler.gd) reads these via snapshot.has() guards. + var gauntlet_mode: bool = false + var raw_gauntlet: Variant = raw.get("gauntlet_mode") + if raw_gauntlet == true: + gauntlet_mode = true + var room_id: Variant = null + var raw_room_id: Variant = raw.get("room_id") + if raw_room_id is String: + room_id = raw_room_id + # v9: conversation_events (#535, D-078) — overheard NPC-to-NPC dialogue lines. # Each event carries pre-occluded text plus speaker/target attribution. var conversation_events: Array = [] @@ -484,7 +478,6 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: "tick": tick, "entities": entities, "decode_errors": dropped, - "version": version, "game_time": game_time, "player_facing": player_facing, "player_stance": player_stance, @@ -508,6 +501,8 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: "current_ticker": current_ticker, "settings_response": settings_response, "bookmark_catalog": bookmark_catalog, + "gauntlet_mode": gauntlet_mode, + "room_id": room_id, } diff --git a/client/scripts/protocol/test_harness.gd b/client/scripts/protocol/test_harness.gd index cf6cbffdf..b2b7e2e34 100644 --- a/client/scripts/protocol/test_harness.gd +++ b/client/scripts/protocol/test_harness.gd @@ -272,7 +272,7 @@ func snapshot() -> Dictionary: return { "tick": tick, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "game_time": { "day": 0, diff --git a/client/tests/test_anti_tedium.gd b/client/tests/test_anti_tedium.gd index 22d4766e0..b16d5ae48 100644 --- a/client/tests/test_anti_tedium.gd +++ b/client/tests/test_anti_tedium.gd @@ -15,7 +15,7 @@ extends GdUnitTestSuite const MAIN_SCENE = preload("res://scenes/main.tscn") var GauntletHUDScript = load("res://ui/gauntlet_hud.gd") -var BugReportDialogScript = load("res://ui/bug_report_dialog.gd") +const BugReportDialogScene = preload("res://ui/bug_report_dialog.tscn") var _instance: Node = null @@ -47,7 +47,7 @@ func after_test() -> void: func _make_snapshot_bytes(overrides: Dictionary = {}) -> PackedByteArray: var snapshot := { "tick": overrides.get("tick", 1), - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": overrides.get("entities", [{ "entity_id": 1, "x": 10.0, @@ -92,8 +92,10 @@ func _make_gauntlet_hud() -> Control: func _make_bug_report_dialog() -> Control: - var dialog = Control.new() - dialog.set_script(BugReportDialogScript) + # Instantiate via .tscn — preserves the MetaScreen runtime stack. + # (Sprint 36 migrated bug_report_dialog.gd to extends MetaScreen; bare + # Control.new() + set_script() no longer satisfies the base contract.) + var dialog: Control = BugReportDialogScene.instantiate() auto_free(dialog) add_child(dialog) return dialog @@ -203,13 +205,15 @@ func test_snapshot_without_room_id_shows_no_gauntlet_ui() -> void: var snapshot: Variant = SimBridge._last_snapshot assert_that(snapshot).is_not_null() - # Snapshot should NOT contain gauntlet fields - assert_that(snapshot.has("room_id")).override_failure_message( - "Non-gauntlet snapshot must not contain room_id" - ).is_false() - assert_that(snapshot.has("gauntlet_mode")).override_failure_message( - "Non-gauntlet snapshot must not contain gauntlet_mode" + # Non-gauntlet snapshot: gauntlet fields must be present with default values. + # (Protocol.decode_snapshot always decodes gauntlet fields; non-gauntlet + # snapshots produce false/null defaults. Check values, not key presence.) + assert_that(snapshot.get("gauntlet_mode", false)).override_failure_message( + "Non-gauntlet snapshot must decode gauntlet_mode == false" ).is_false() + assert_that(snapshot.get("room_id")).override_failure_message( + "Non-gauntlet snapshot must decode room_id == null" + ).is_null() # Apply to GameState — gauntlet-related state should not exist GameState.apply_snapshot(snapshot) diff --git a/client/tests/test_bug_report_ring_buffer.gd b/client/tests/test_bug_report_ring_buffer.gd index eaacf3a1a..c8fc62f24 100644 --- a/client/tests/test_bug_report_ring_buffer.gd +++ b/client/tests/test_bug_report_ring_buffer.gd @@ -14,7 +14,7 @@ extends GdUnitTestSuite # Expected ring buffer capacity per spec. const EXPECTED_CAPACITY := 60 -var BugReportDialogScript = load("res://ui/bug_report_dialog.gd") +const BugReportDialogScene = preload("res://ui/bug_report_dialog.tscn") func after_each() -> void: @@ -35,8 +35,10 @@ func after_each() -> void: # -- Helpers ------------------------------------------------------------------- func _make_dialog() -> Control: - var dialog = Control.new() - dialog.set_script(BugReportDialogScript) + # Instantiate via .tscn — preserves the MetaScreen runtime stack. + # (Sprint 36 migrated bug_report_dialog.gd to extends MetaScreen; bare + # Control.new() + set_script() no longer satisfies the base contract.) + var dialog: Control = BugReportDialogScene.instantiate() auto_free(dialog) add_child(dialog) return dialog @@ -52,7 +54,7 @@ func _make_input(tick: int, action: String = "MoveNorth") -> Dictionary: func _make_snapshot_json(tick: int) -> String: return JSON.stringify({ "tick": tick, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], }) diff --git a/client/tests/test_hub_teleport.gd b/client/tests/test_hub_teleport.gd index e39366f5b..3d1fedc58 100644 --- a/client/tests/test_hub_teleport.gd +++ b/client/tests/test_hub_teleport.gd @@ -15,7 +15,7 @@ const _THRESHOLD: float = 5.0 # Mirror of main.gd TELEPORT_DISTANCE_THRESHOLD var _gauntlet_snapshot := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "game_time": {"day": 0, "time_of_day": 100, "day_phase": "Morning", "tick_rate": "Full"}, "player_facing": "North", "player_stance": "Walk", @@ -36,7 +36,7 @@ var _gauntlet_snapshot := { var _normal_snapshot := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "game_time": {"day": 0, "time_of_day": 100, "day_phase": "Morning", "tick_rate": "Full"}, "player_facing": "North", "player_stance": "Walk", diff --git a/client/tests/test_input_roundtrip.gd b/client/tests/test_input_roundtrip.gd index 3a99fec44..ac58eeb74 100644 --- a/client/tests/test_input_roundtrip.gd +++ b/client/tests/test_input_roundtrip.gd @@ -160,8 +160,6 @@ func test_interact_roundtrip() -> void: # Server accepts it (currently a no-op) and responds with a valid snapshot. var snapshot: Dictionary = await _send_and_receive("Interact", 0) - # Snapshot should be valid with correct protocol version - assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION) assert_that(snapshot.entities.size()).is_greater(0) # Player should be at start position (Interact doesn't move) @@ -173,7 +171,6 @@ func test_interact_roundtrip() -> void: # Send Interact again at next tick — server should still accept it var snap2: Dictionary = await _send_and_receive("Interact", 1) assert_that(snap2).is_not_null() - assert_that(snap2.version).is_equal(Protocol.PROTOCOL_VERSION) # -- Mixed sequence: movement then interact in one session --------------------- diff --git a/client/tests/test_interaction_list.gd b/client/tests/test_interaction_list.gd index 5f4cd3f3b..76f5c34fa 100644 --- a/client/tests/test_interaction_list.gd +++ b/client/tests/test_interaction_list.gd @@ -305,7 +305,7 @@ func test_contradicted_entity_verbs_decode() -> void: # #422: NearbyInteraction.contradicted=true should be decodeable var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "nearby_interactions": [{ "entity_id": 2, @@ -329,7 +329,7 @@ func test_object_type_verbs_decode() -> void: # #421: ObjectType appears in NearbyInteraction.object_type var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "nearby_interactions": [{ "entity_id": 3, diff --git a/client/tests/test_interaction_prompt.gd b/client/tests/test_interaction_prompt.gd index 7e3557b3d..51cdfdb51 100644 --- a/client/tests/test_interaction_prompt.gd +++ b/client/tests/test_interaction_prompt.gd @@ -11,7 +11,7 @@ extends GdUnitTestSuite func test_protocol_decode_v4_with_nearby_interactions() -> void: var raw := { "tick": 10, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Player", "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, @@ -46,7 +46,7 @@ func test_protocol_decode_v4_with_nearby_interactions() -> void: func test_protocol_decode_v4_no_nearby_interactions() -> void: var raw := { "tick": 5, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], } var encoded = Messagepack.encode(raw) @@ -57,7 +57,7 @@ func test_protocol_decode_v4_no_nearby_interactions() -> void: func test_protocol_decode_empty_nearby_interactions() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "nearby_interactions": [], } @@ -68,7 +68,7 @@ func test_protocol_decode_empty_nearby_interactions() -> void: func test_protocol_decode_interaction_missing_verbs() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "nearby_interactions": [{"entity_id": 2}], } @@ -79,7 +79,7 @@ func test_protocol_decode_interaction_missing_verbs() -> void: func test_protocol_decode_interaction_empty_verbs() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "nearby_interactions": [{"entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": []}], } @@ -90,7 +90,7 @@ func test_protocol_decode_interaction_empty_verbs() -> void: func test_protocol_decode_v4_entity_relationship() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Npc", "visibility": "Forward", "relationship": "Friendly", "observation": "Visible"}, @@ -100,17 +100,6 @@ func test_protocol_decode_v4_entity_relationship() -> void: var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.entities[0].relationship).is_equal("Friendly") -func test_protocol_rejects_version_mismatch() -> void: - var raw := { - "tick": 5, - "version": 2, - "entities": [], - } - var encoded = Messagepack.encode(raw) - var snapshot = Protocol.decode_snapshot(encoded.value) - assert_that(snapshot).is_null() - - # -- GameState: nearby_interactions storage -- func test_game_state_stores_nearby_interactions() -> void: @@ -153,12 +142,6 @@ func test_sim_bridge_test_snapshot_interaction_at_range_2() -> void: var snap = SimBridge._test_snapshot() assert_that(snap.nearby_interactions.size()).is_equal(1) -func test_sim_bridge_test_snapshot_protocol_version() -> void: - SimBridge.reset_test_state() - var snap = SimBridge._test_snapshot() - assert_that(snap.version).is_equal(Protocol.PROTOCOL_VERSION) - - # -- InteractionPrompt UI -- func test_prompt_get_selected_verb_returns_first_kind() -> void: diff --git a/client/tests/test_ipc_fixtures.gd b/client/tests/test_ipc_fixtures.gd index 12594b279..d95085882 100644 --- a/client/tests/test_ipc_fixtures.gd +++ b/client/tests/test_ipc_fixtures.gd @@ -19,13 +19,6 @@ func _load_fixture(name: String) -> PackedByteArray: # -- snapshot_minimal ---------------------------------------------------------- -func test_fixture_snapshot_minimal_version() -> void: - var bytes = _load_fixture("snapshot_minimal") - var snapshot = Protocol.decode_snapshot(bytes) - assert_that(snapshot).is_not_null() - assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION) - - func test_fixture_snapshot_minimal_tick() -> void: var bytes = _load_fixture("snapshot_minimal") var snapshot = Protocol.decode_snapshot(bytes) diff --git a/client/tests/test_local_bridge.gd b/client/tests/test_local_bridge.gd index 9bc822498..afcfb4f14 100644 --- a/client/tests/test_local_bridge.gd +++ b/client/tests/test_local_bridge.gd @@ -95,7 +95,7 @@ func test_frame_encode_large_payload_length() -> void: func test_framed_protocol_snapshot_roundtrip() -> void: # Encode a snapshot with Protocol, frame it, decode the frame, decode the snapshot - var snapshot_data := {"tick": 42, "version": Protocol.PROTOCOL_VERSION, "entities": []} + var snapshot_data := {"tick": 42, "version": 23, "entities": []} var encoded: Variant = Messagepack.encode(snapshot_data) assert_that(encoded.status).is_null() diff --git a/client/tests/test_merge_path_flows_sprint37.gd b/client/tests/test_merge_path_flows_sprint37.gd new file mode 100644 index 000000000..2038e0e0c --- /dev/null +++ b/client/tests/test_merge_path_flows_sprint37.gd @@ -0,0 +1,317 @@ +## Sprint 37 — Scene-level merge-path UI flow tests (#873) +## +## Four flows that cover the critical paths through the pre-game UI. +## These tests are the merge-gate mechanism added in the Sprint 36 retro: +## regressions like #872 (New Game hang) must be caught here, not in post-merge +## smoke tests. +## +## Pattern: load scene → simulate input via button.pressed.emit() or direct +## handler call → assert terminal state. No pixel diffing, no xdotool. +## +## NOTE: Tests that end in a scene transition (change_scene_to_file) assert state +## synchronously before the deferred transition fires. The test scene is +## queue_freed in after_test() regardless. +## +## Reference: test_character_creation_sprint28.gd +## Ticket: #873 | motivating regression: #872 +class_name TestMergePathFlowsSprint37 +extends GdUnitTestSuite + +const MAIN_MENU_SCENE_PATH := "res://scenes/main_menu.tscn" +const CHAR_CREATE_SCENE_PATH := "res://scenes/character_creation.tscn" + +var _scene = null # MainMenu or CharacterCreation — untyped, varies per test + + +func before_test() -> void: + # Reset all shared state that these flows touch + SimBridge.disconnect_from_sim() + SimBridge._last_snapshot = null + SimBridge._outbound_buffer.clear() + GameState.bookmark_catalog = [] + GameState.pending_load_path = "" + # Clear MetaStack from any leftover overlays to prevent push/pop ordering issues + MetaStack._stack.clear() + + +func after_test() -> void: + if is_instance_valid(_scene): + _scene.queue_free() + _scene = null + SimBridge.disconnect_from_sim() + SimBridge._outbound_buffer.clear() + MetaStack._stack.clear() + + +# ----------------------------------------------------------------------------- +# Helpers +# ----------------------------------------------------------------------------- + +func _load_main_menu() -> void: + var packed := load(MAIN_MENU_SCENE_PATH) as PackedScene + if packed == null: + push_warning("TestMergePathFlowsSprint37: main_menu.tscn not found — skipping") + return + _scene = packed.instantiate() + add_child(_scene) + + +func _load_char_create() -> void: + var packed := load(CHAR_CREATE_SCENE_PATH) as PackedScene + if packed == null: + push_warning("TestMergePathFlowsSprint37: character_creation.tscn not found — skipping") + return + _scene = packed.instantiate() + # Seed required state so Start is not disabled (guard added in PR #134 / R2-Hoshe-1). + # Individual tests override these as needed. + _scene._selected_bookmark_id = "test-bookmark" + _scene._selected_location_id = "test-location" + if _scene.has_method("_update_start_btn_state"): + _scene._update_start_btn_state() + add_child(_scene) + + +func _make_catalog_snapshot() -> Dictionary: + ## Minimal valid snapshot with a bookmark_catalog for flow-1 testing. + return { + "tick": 0, + "version": 23, + "entities": [], + "game_time": {"day": 0, "time_of_day": 0, "day_phase": "Morning", "tick_rate": "Full"}, + "player_facing": "North", + "player_stance": "Walk", + "player_inventory": [], + "visible_tiles": [], + "nearby_interactions": [], + "pending_recognitions": [], + "bookmark_catalog": { + "bookmarks": [ + { + "id": "bm_tycoon_arion", + "title": "Arion Freight Broker", + "subtitle": "Start at Arion orbital", + "flavor": "Commodities and logistics.", + "default_location": "arion", + "allowed_locations": ["arion", "arion_low"], + "allowed_locations_cultures": ["arion"], + "career": "tycoon", + "starting_capital_tractus": 50000, + } + ] + }, + } + + +# ============================================================================= +# Flow 1: main menu → new game → loading state → catalog received → resolved +# ============================================================================= +# Regression guard for #872: New Game used to hang on "Connecting to simulation..." +# because bookmark_catalog was overwritten in receive_bytes before poll_snapshot consumed it. +# This test catches that regression by verifying the full state machine: +# pressed → loading visible → catalog signal → loading dismissed. + +func test_new_game_shows_loading_screen() -> void: + ## Pressing New Game must show the loading screen and set _waiting_for_catalog. + _load_main_menu() + if _scene == null: + return + + assert_bool(_scene._waiting_for_catalog).override_failure_message( + "_waiting_for_catalog must be false before New Game is pressed" + ).is_false() + + # Press New Game via the button signal (same as real player input) + _scene._new_game_btn.pressed.emit() + + assert_bool(_scene._waiting_for_catalog).override_failure_message( + "_waiting_for_catalog must be true after New Game pressed" + ).is_true() + assert_that(_scene._loading_screen).override_failure_message( + "Loading screen instance must exist after New Game pressed" + ).is_not_null() + assert_bool(_scene._loading_screen.visible).override_failure_message( + "Loading screen must be visible after New Game pressed" + ).is_true() + + +func test_new_game_catalog_snapshot_resolves_loading_state() -> void: + ## When snapshot_received fires with a bookmark_catalog, the loading state must clear. + ## This is the exact regression introduced in #872 — if the catalog is never delivered, + ## _waiting_for_catalog stays true and the screen hangs forever. + _load_main_menu() + if _scene == null: + return + + # Simulate the New Game press to set up the signal subscription and loading state. + # In test mode, connect_to_sim() immediately fires CONNECTED, which triggers + # _on_sim_state_changed_for_new_game and connects snapshot_received. + _scene._new_game_btn.pressed.emit() + + assert_bool(_scene._waiting_for_catalog).override_failure_message( + "Precondition: _waiting_for_catalog must be true before catalog arrives" + ).is_true() + + # Deliver the catalog via the signal path (same path the server uses in live mode). + # snapshot_received is emitted here directly because in test mode poll_snapshot() + # uses the harness snapshot (no catalog). The carry-forward fix (#872) ensures + # this signal path also works correctly in live mode when ticks batch. + var catalog_snapshot := _make_catalog_snapshot() + SimBridge.snapshot_received.emit(catalog_snapshot) + + # Terminal state: loading resolved + assert_bool(_scene._waiting_for_catalog).override_failure_message( + "_waiting_for_catalog must be false after catalog snapshot delivered — #872 regression" + ).is_false() + assert_bool(GameState.bookmark_catalog.size() > 0).override_failure_message( + "GameState.bookmark_catalog must be populated after catalog snapshot applied" + ).is_true() + assert_str(GameState.bookmark_catalog[0].get("id", "")).override_failure_message( + "Catalog entry must have the expected bookmark id" + ).is_equal("bm_tycoon_arion") + + +# ============================================================================= +# Flow 2: main menu → load game → save picker → save selected +# ============================================================================= + +func test_load_game_save_picker_shows_on_browse() -> void: + ## Calling _on_load_game_browse() must show the save picker panel. + _load_main_menu() + if _scene == null: + return + + assert_bool(_scene._load_panel.visible).override_failure_message( + "Load panel must be hidden before Load Game is pressed" + ).is_false() + + _scene._on_load_game_browse() + + assert_bool(_scene._load_panel.visible).override_failure_message( + "Load panel must be visible after _on_load_game_browse()" + ).is_true() + + +func test_load_game_save_selection_sets_pending_load_path() -> void: + ## Selecting a save entry must set GameState.pending_load_path for main.gd to consume. + _load_main_menu() + if _scene == null: + return + + var mock_save := { + "game_id": "20260421-120000-abc123", + "newest_save": "quicksave.sav", + } + + # Call _on_save_selected directly — mirrors what the generated save-list button does. + _scene._on_save_selected(mock_save) + + assert_str(GameState.pending_load_path).override_failure_message( + "pending_load_path must be set to the selected save's full path" + ).is_equal("user://saves/20260421-120000-abc123/quicksave.sav") + + +# ============================================================================= +# Flow 3: character creation → submit → sim_bridge receives correct payload +# ============================================================================= + +func test_character_creation_submit_sends_confirm_bookmark_action() -> void: + ## _on_start() must queue a ConfirmBookmark action with the selected bookmark + ## and location IDs. This is the payload the server uses to initialize the run. + _load_char_create() + if _scene == null: + return + + # Ensure SimBridge is connected so send_named_action doesn't silently drop the action + SimBridge.connect_to_sim() # test mode: immediately CONNECTED + + _scene._selected_bookmark_id = "bm_tycoon_arion" + _scene._selected_location_id = "arion" + if _scene.has_method("_update_start_btn_state"): + _scene._update_start_btn_state() + + SimBridge._outbound_buffer.clear() + _scene._on_start() + + var found := false + for entry in SimBridge._outbound_buffer: + if entry.get("action_name") == "ConfirmBookmark": + var data: Variant = entry.get("action_data") + if ( + data is Dictionary + and data.get("bookmark_id") == "bm_tycoon_arion" + and data.get("starting_location_id") == "arion" + ): + found = true + break + assert_bool(found).override_failure_message( + "_outbound_buffer must contain ConfirmBookmark{bookmark_id='bm_tycoon_arion', starting_location_id='arion'}" + ).is_true() + + +# ============================================================================= +# Flow 4: bookmark tab → select location → confirm → server gets bookmark action +# ============================================================================= + +func test_bookmark_tab_select_location_then_confirm_queues_action() -> void: + ## Exercises the full selection path: picking a bookmark, picking a location, then + ## confirming. Verifies the correct bookmark_id + starting_location_id reach the server. + ## This is the flow the player actually takes — selection handlers must propagate + ## to the outbound buffer correctly. + + # Populate catalog before scene instantiation so _build_bookmark_cards() sees it + GameState.bookmark_catalog = [ + { + "id": "bm_tycoon_arion", + "title": "Arion Freight Broker", + "subtitle": "Start at Arion orbital", + "flavor": "Commodities and logistics.", + "default_location": "arion", + "allowed_locations": ["arion", "arion_low"], + "allowed_locations_cultures": ["arion"], + "career": "tycoon", + "starting_capital_tractus": 50000, + } + ] + + _load_char_create() + if _scene == null: + return + + SimBridge.connect_to_sim() # test mode: immediately CONNECTED + SimBridge._outbound_buffer.clear() + + # Simulate the player selecting the bookmark card + var bm: Dictionary = GameState.bookmark_catalog[0] + _scene._on_bookmark_selected(bm) + + assert_str(_scene._selected_bookmark_id).override_failure_message( + "_on_bookmark_selected must update _selected_bookmark_id" + ).is_equal("bm_tycoon_arion") + assert_str(_scene._selected_location_id).override_failure_message( + "_on_bookmark_selected must populate _selected_location_id from default_location" + ).is_not_empty() + + # Simulate the player picking a specific allowed location + _scene._on_location_selected("arion_low") + + assert_str(_scene._selected_location_id).override_failure_message( + "_on_location_selected must update _selected_location_id" + ).is_equal("arion_low") + + # Confirm — sends the action to the server + _scene._on_start() + + var found := false + for entry in SimBridge._outbound_buffer: + if entry.get("action_name") == "ConfirmBookmark": + var data: Variant = entry.get("action_data") + if ( + data is Dictionary + and data.get("bookmark_id") == "bm_tycoon_arion" + and data.get("starting_location_id") == "arion_low" + ): + found = true + break + assert_bool(found).override_failure_message( + "_outbound_buffer must contain ConfirmBookmark{bookmark_id='bm_tycoon_arion', starting_location_id='arion_low'}" + ).is_true() diff --git a/client/tests/test_p0_regressions.gd b/client/tests/test_p0_regressions.gd index 845eec824..3b4855da0 100644 --- a/client/tests/test_p0_regressions.gd +++ b/client/tests/test_p0_regressions.gd @@ -1,10 +1,14 @@ -## Client P0 regression tests: guards for Bug #5 (monologue lost) and Bug #2 (camera drift). +## Client P0 regression tests: guards for Bug #5 (monologue lost), Bug #2 (camera drift), +## and Bug #872 (bookmark_catalog lost on batch receive). ## These must pass before any other client testing is meaningful. ## ## Bug #5: Monologue text lost when server sends snapshots faster than client ## consumes them. Fix: carry-forward one-shot events in receive_bytes(). ## Bug #2: Camera doesn't center at startup / drifts during pause. Fix: anchor ## pattern with smoothing disabled until first snapshot applied. +## Bug #872: bookmark_catalog silently dropped when tick 0 (with catalog) and tick 1 +## (without catalog) arrive in the same TCP batch. Fix: carry-forward +## bookmark_catalog in receive_bytes() like save_result/settings_response. ## ## Spec ref: stig-round3.md Section 1 (P0 tests #1, #2). class_name TestP0Regressions @@ -44,7 +48,7 @@ func after_test() -> void: func _make_snapshot_bytes(overrides: Dictionary = {}) -> PackedByteArray: var snapshot := { "tick": overrides.get("tick", 1), - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": overrides.get("entities", [{ "entity_id": 1, "x": 10.0, @@ -71,6 +75,8 @@ func _make_snapshot_bytes(overrides: Dictionary = {}) -> PackedByteArray: snapshot["current_monologue"] = overrides["current_monologue"] if overrides.has("current_dialogue"): snapshot["current_dialogue"] = overrides["current_dialogue"] + if overrides.has("bookmark_catalog"): + snapshot["bookmark_catalog"] = overrides["bookmark_catalog"] var result = Messagepack.encode(snapshot) return result.value @@ -229,3 +235,71 @@ func test_camera_anchored_after_pause_unpause() -> void: # Camera still tracking player (position may have changed due to test_snapshot) var player_pos := GameState.player_position * Constants.TILE_SIZE assert_that(camera.global_position).is_equal(player_pos) + + +# -- Bug #872: bookmark_catalog carry-forward --------------------------------- +# Server sends bookmark_catalog on tick 0. If tick 1 arrives before poll_snapshot() +# is called (same TCP batch), the inner receive loop overwrites _last_snapshot and +# the catalog is silently lost. The carry-forward fix must preserve the catalog. + +func test_bookmark_catalog_not_lost_on_snapshot_overwrite() -> void: + var catalog := { + "bookmarks": [ + { + "id": "bm_tycoon_arion", + "title": "Arion Freight Broker", + "subtitle": "Start at Arion orbital", + "flavor": "Commodities and logistics.", + "default_location": "arion", + "allowed_locations": ["arion"], + "allowed_locations_cultures": ["arion"], + "career": "tycoon", + "starting_capital_tractus": 50000, + } + ] + } + + # Tick 0: server sends catalog automatically after handshake + var bytes_tick0 := _make_snapshot_bytes({ + "tick": 0, + "bookmark_catalog": catalog, + }) + SimBridge.receive_bytes(bytes_tick0) + + # Tick 1: server sends next tick WITHOUT catalog (fast server, same TCP batch) + var bytes_tick1 := _make_snapshot_bytes({"tick": 1}) + SimBridge.receive_bytes(bytes_tick1) + + # Assert: catalog must survive the overwrite — this is the Bug #872 fix. + assert_that(SimBridge._last_snapshot).is_not_null() + var bmc: Variant = SimBridge._last_snapshot.get("bookmark_catalog") + assert_that(bmc).is_not_null() + assert_that(bmc is Dictionary).is_true() + var bookmarks: Variant = bmc.get("bookmarks") + assert_that(bookmarks is Array).is_true() + assert_that((bookmarks as Array).size()).is_equal(1) + assert_that((bookmarks as Array)[0].get("id")).is_equal("bm_tycoon_arion") + + +func test_bookmark_catalog_not_carried_forward_after_consumption() -> void: + # After poll_snapshot() consumes the catalog, the next snapshot without catalog + # must NOT carry it forward (it was already consumed and the scene transitioned). + var catalog := { + "bookmarks": [{"id": "bm_test", "title": "Test", "subtitle": "", "flavor": "", + "default_location": "arion", "allowed_locations": [], "allowed_locations_cultures": [], + "career": "tycoon", "starting_capital_tractus": 0}] + } + var bytes_tick0 := _make_snapshot_bytes({"tick": 0, "bookmark_catalog": catalog}) + SimBridge.receive_bytes(bytes_tick0) + + # Simulate poll_snapshot() consumption — sets _last_snapshot to null + var snapshot = SimBridge._last_snapshot + SimBridge._last_snapshot = null + assert_that(snapshot).is_not_null() + + # Next snapshot arrives without catalog — no carry-forward should happen + var bytes_tick1 := _make_snapshot_bytes({"tick": 1}) + SimBridge.receive_bytes(bytes_tick1) + + var bmc: Variant = SimBridge._last_snapshot.get("bookmark_catalog") + assert_that(bmc).is_null() diff --git a/client/tests/test_protocol.gd b/client/tests/test_protocol.gd index cb53e9d55..be744df71 100644 --- a/client/tests/test_protocol.gd +++ b/client/tests/test_protocol.gd @@ -188,7 +188,7 @@ func test_decode_snapshot_malformed_entities_counted() -> void: # Snapshot with one valid and one malformed entity — decode_errors should count the bad one var raw := { "tick": 7, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [ {"entity_id": 1, "x": 5.0, "y": 10.0, "z": 0, "kind": "Npc"}, {"entity_id": 2, "broken": true}, # Missing required fields @@ -282,7 +282,6 @@ func test_decode_snapshot_v2_full() -> void: assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(500) - assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION) # game_time assert_that(snapshot.game_time).is_not_null() @@ -309,7 +308,6 @@ func test_existing_fixtures_have_v2_fields() -> void: var bytes = _load_fixture(fixture_name) var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() - assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION) assert_that(snapshot.player_facing).is_equal("North") assert_that(snapshot.game_time).is_not_null() @@ -324,30 +322,6 @@ func test_multi_entity_visibility_sectors() -> void: assert_that(snapshot.entities[3].visibility).is_equal("Forward") -# -- Version enforcement (strict PROTOCOL_VERSION check) -------------------- - -func test_decode_snapshot_rejects_missing_version() -> void: - # Snapshot without version field → rejected by strict version check - var v1_raw := {"tick": 10, "entities": [ - {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Player"}, - ]} - var encoded: Variant = Messagepack.encode(v1_raw) - assert_that(encoded.status).is_null() - - var snapshot: Variant = Protocol.decode_snapshot(encoded.value) - assert_that(snapshot).is_null() - - -func test_decode_snapshot_rejects_old_version() -> void: - # Snapshot with version 2 → rejected by strict version check - var old_raw := {"tick": 10, "version": 2, "entities": []} - var encoded: Variant = Messagepack.encode(old_raw) - assert_that(encoded.status).is_null() - - var snapshot: Variant = Protocol.decode_snapshot(encoded.value) - assert_that(snapshot).is_null() - - # -- Batch input fixture (D-030 Layer 1 bidirectional symmetry) ---------------- func test_decode_batch_input_fixture() -> void: @@ -391,7 +365,7 @@ func test_decode_snapshot_with_bookmark_catalog() -> void: # Hand-built dict — fixture generation requires server work, skip round-trip (#614). var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "bookmark_catalog": { "bookmarks": [ @@ -451,7 +425,7 @@ func test_decode_snapshot_no_bookmark_catalog_is_null() -> void: # Snapshot without bookmark_catalog key → field should be null. var raw := { "tick": 2, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], } var encoded: Variant = Messagepack.encode(raw) diff --git a/client/tests/test_protocol_bridge.gd b/client/tests/test_protocol_bridge.gd index 383a355e0..91b9b393d 100644 --- a/client/tests/test_protocol_bridge.gd +++ b/client/tests/test_protocol_bridge.gd @@ -24,33 +24,12 @@ func _load_fixture(name: String) -> PackedByteArray: return file.get_buffer(file.get_length()) -# -- Protocol version upgrade ------------------------------------------------- -# Tautological "PROTOCOL_VERSION == N" assertions deleted: they assert a constant -# equals its own literal, fail mechanically on every protocol bump, and have -# never caught a real bug. Mismatch handling is exercised by test_rejects_version_6 -# below; field-presence is exercised by the per-version decode tests. - -func test_fixtures_at_protocol_version_8() -> void: - # NOTE: These binary fixtures embed version 8 and are rejected by the version - # mismatch guard in decode_snapshot(). This test is pre-existing broken since v9+. - # Fixtures need regeneration via `make fixtures-gauntlet` to match current protocol. - # Skipping rather than deleting to preserve the fixture round-trip pattern. - pass - - -func test_rejects_version_6() -> void: - var raw := {"tick": 1, "version": 6, "entities": []} - var encoded = Messagepack.encode(raw) - var snapshot = Protocol.decode_snapshot(encoded.value) - assert_that(snapshot).is_null() - - # -- player_stance decode (D-053) --------------------------------------------- func test_decode_player_stance_walk() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "player_stance": "Walk", "player_inventory": [], @@ -64,7 +43,7 @@ func test_decode_player_stance_walk() -> void: func test_decode_player_stance_sprint() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "player_stance": "Sprint", "player_inventory": [], @@ -77,7 +56,7 @@ func test_decode_player_stance_sprint() -> void: func test_decode_player_stance_careful() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "player_stance": "Careful", "player_inventory": [], @@ -90,7 +69,7 @@ func test_decode_player_stance_careful() -> void: func test_decode_player_stance_crouch() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "player_stance": "Crouch", "player_inventory": [], @@ -104,7 +83,7 @@ func test_decode_player_stance_missing_defaults_to_walk() -> void: # v6 snapshot without player_stance → should default to "Walk" var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], } var encoded = Messagepack.encode(raw) @@ -118,7 +97,7 @@ func test_decode_player_stance_missing_defaults_to_walk() -> void: func test_decode_empty_inventory() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "player_stance": "Walk", "player_inventory": [], @@ -132,7 +111,7 @@ func test_decode_smuggler_inventory_3_items() -> void: # D-065: smuggler carries 3 specific items var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "player_stance": "Walk", "player_inventory": [ @@ -161,7 +140,7 @@ func test_decode_full_9_slot_inventory() -> void: items.append({"item_id": 100 + i, "name": "Item %d" % i, "slot": i}) var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "player_inventory": items, } @@ -177,7 +156,7 @@ func test_decode_full_9_slot_inventory() -> void: func test_decode_inventory_missing_defaults_to_empty() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], } var encoded = Messagepack.encode(raw) @@ -188,7 +167,7 @@ func test_decode_inventory_missing_defaults_to_empty() -> void: func test_decode_inventory_skips_malformed_items() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "player_inventory": [ {"item_id": 100, "name": "Valid Item", "slot": 0}, @@ -209,7 +188,7 @@ func test_decode_inventory_skips_malformed_items() -> void: func test_decode_inventory_item_slot_defaults_to_zero() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "player_inventory": [ {"item_id": 100, "name": "No Slot"}, @@ -281,12 +260,6 @@ 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_uses_current_protocol_version() -> void: - SimBridge.reset_test_state() - var snap = SimBridge._test_snapshot() - assert_that(snap.version).is_equal(Protocol.PROTOCOL_VERSION) - - # -- Fixture: v6 snapshots include new fields ---------------------------------- func test_fixture_snapshots_have_v6_defaults() -> void: @@ -334,7 +307,7 @@ func test_full_v6_snapshot_decode() -> void: # Simulate a realistic v6 snapshot with all fields populated var raw := { "tick": 100, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "game_time": {"day": 1, "time_of_day": 720, "day_phase": "Evening", "tick_rate": "Full"}, "player_facing": "Southeast", "player_stance": "Careful", @@ -367,7 +340,6 @@ func test_full_v6_snapshot_decode() -> void: assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(100) - assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION) assert_that(snapshot.player_facing).is_equal("Southeast") assert_that(snapshot.player_stance).is_equal("Careful") assert_that(snapshot.player_inventory.size()).is_equal(3) diff --git a/client/tests/test_protocol_v7.gd b/client/tests/test_protocol_v7.gd index 649036aee..16cd0987a 100644 --- a/client/tests/test_protocol_v7.gd +++ b/client/tests/test_protocol_v7.gd @@ -12,7 +12,7 @@ extends GdUnitTestSuite func test_decode_pending_recognitions_basic() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "pending_recognitions": [ {"entity_id": 100, "x": 13.5, "y": 12.5, "z": 0, "remaining_ticks": 4, "total_delay_ticks": 6}, @@ -33,7 +33,7 @@ func test_decode_pending_recognitions_basic() -> void: func test_decode_pending_recognitions_empty() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "pending_recognitions": [], } @@ -45,7 +45,7 @@ func test_decode_pending_recognitions_empty() -> void: func test_decode_pending_recognitions_missing_defaults_empty() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], } var encoded = Messagepack.encode(raw) @@ -56,7 +56,7 @@ func test_decode_pending_recognitions_missing_defaults_empty() -> void: func test_decode_pending_recognitions_skips_malformed() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "pending_recognitions": [ {"entity_id": 100, "x": 13.5, "y": 12.5, "z": 0, "remaining_ticks": 4, "total_delay_ticks": 6}, @@ -76,7 +76,7 @@ func test_decode_pending_recognitions_defaults() -> void: # remaining_ticks and total_delay_ticks default to 0 and 1 var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "pending_recognitions": [ {"entity_id": 100, "x": 5.0, "y": 5.0}, @@ -94,7 +94,7 @@ func test_decode_pending_recognitions_defaults() -> void: func test_decode_current_dialogue() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "current_dialogue": { "npc_name": "Kael", @@ -123,7 +123,7 @@ func test_decode_current_dialogue() -> void: func test_decode_current_dialogue_missing_is_null() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], } var encoded = Messagepack.encode(raw) @@ -136,7 +136,7 @@ func test_decode_current_dialogue_options_default_fields() -> void: # response_id and priority default when absent var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "current_dialogue": { "speech": "Just speech.", @@ -158,7 +158,7 @@ func test_decode_current_dialogue_options_default_fields() -> void: func test_decode_current_dialogue_confrontation_option() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "current_dialogue": { "npc_name": "Sera", @@ -179,7 +179,7 @@ func test_decode_current_dialogue_confrontation_option() -> void: func test_decode_current_dialogue_skips_malformed_options() -> void: var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "current_dialogue": { "npc_name": "Sera", @@ -332,7 +332,7 @@ func test_insert_color_constants_exist() -> void: func test_full_v7_snapshot_decode() -> void: var raw := { "tick": 200, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "game_time": {"day": 2, "time_of_day": 1000, "day_phase": "Evening", "tick_rate": "Full"}, "player_facing": "West", "player_stance": "Careful", @@ -362,7 +362,6 @@ func test_full_v7_snapshot_decode() -> void: assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(200) - assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION) assert_that(snapshot.player_facing).is_equal("West") assert_that(snapshot.player_stance).is_equal("Careful") assert_that(snapshot.player_inventory.size()).is_equal(1) diff --git a/client/tests/test_rendering.gd b/client/tests/test_rendering.gd index 399283250..3a7f85ff3 100644 --- a/client/tests/test_rendering.gd +++ b/client/tests/test_rendering.gd @@ -174,8 +174,6 @@ func test_sim_bridge_test_tiles_contain_all_types() -> void: func test_sim_bridge_test_snapshot_has_v2_fields() -> void: SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() - assert_that(snap.has("version")).is_true() - assert_that(snap.version).is_equal(Protocol.PROTOCOL_VERSION) assert_that(snap.has("game_time")).is_true() assert_that(snap.has("player_facing")).is_true() assert_that(snap.has("visible_tiles")).is_true() diff --git a/client/tests/test_signal_sprint24.gd b/client/tests/test_signal_sprint24.gd index 60f16c0cf..3bdeed7aa 100644 --- a/client/tests/test_signal_sprint24.gd +++ b/client/tests/test_signal_sprint24.gd @@ -80,7 +80,7 @@ func test_protocol_decode_includes_triangle_crisis_events_field() -> void: # decode_snapshot() must return a "triangle_crisis_events" key (#590). var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "triangle_crisis_events": [{"triangle_id": 42}], } @@ -102,7 +102,7 @@ func test_protocol_decode_triangle_crisis_events_empty_array() -> void: # When no events are present, field is present and empty. var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "triangle_crisis_events": [], } @@ -117,7 +117,7 @@ func test_protocol_decode_triangle_crisis_events_absent_returns_empty() -> void: # When server doesn't send field (pre-#589), field defaults to empty array. var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], } var encoded = Messagepack.encode(raw) @@ -167,7 +167,7 @@ func test_protocol_decode_includes_current_ticker_field() -> void: # decode_snapshot() must return a "current_ticker" key (#592). var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "current_ticker": {"id": "ticker_001", "text": "Station systems nominal.", "category": "System"}, } @@ -187,7 +187,7 @@ func test_protocol_decode_current_ticker_null_when_absent() -> void: # When server doesn't send current_ticker (player outside bar zone), field is null. var raw := { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], } var encoded = Messagepack.encode(raw) @@ -210,7 +210,7 @@ func test_news_ticker_hidden_when_snapshot_has_no_ticker() -> void: # Snapshot with no current_ticker (player outside bar zone). GameState.current_snapshot = { "tick": 1, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], } ticker.update_from_state() @@ -229,7 +229,7 @@ func test_news_ticker_visible_when_snapshot_has_ticker() -> void: GameState.current_snapshot = { "tick": 2, - "version": Protocol.PROTOCOL_VERSION, + "version": 23, "entities": [], "current_ticker": {"id": "t1", "text": "Station systems nominal.", "category": "System"}, } @@ -249,7 +249,7 @@ func test_news_ticker_hides_when_ticker_becomes_null() -> void: # Show it first. GameState.current_snapshot = { - "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], + "tick": 1, "version": 23, "entities": [], "current_ticker": {"id": "t1", "text": "Breaking news.", "category": "System"}, } ticker.update_from_state() @@ -257,7 +257,7 @@ func test_news_ticker_hides_when_ticker_becomes_null() -> void: # Null current_ticker — player left the bar zone. GameState.current_snapshot = { - "tick": 2, "version": Protocol.PROTOCOL_VERSION, "entities": [], + "tick": 2, "version": 23, "entities": [], } ticker.update_from_state() assert_bool(ticker.visible).override_failure_message( diff --git a/client/tests/test_sprint2_proof.gd b/client/tests/test_sprint2_proof.gd index d786be082..b27cedd7f 100644 --- a/client/tests/test_sprint2_proof.gd +++ b/client/tests/test_sprint2_proof.gd @@ -132,7 +132,6 @@ func skip_test_proof_player_moves_and_v2_snapshot() -> void: assert_float(player.y).is_equal_approx(15.5, 0.001) # v4 protocol fields present - assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION) assert_that(snapshot.player_facing).is_equal("North") assert_that(snapshot.game_time).is_not_null() diff --git a/client/tests/test_time_display_sprint17.gd b/client/tests/test_time_display_sprint17.gd index 6d2162045..57f49ea3b 100644 --- a/client/tests/test_time_display_sprint17.gd +++ b/client/tests/test_time_display_sprint17.gd @@ -291,7 +291,7 @@ func test_hud_time_row_updates_after_process() -> void: add_child(instance) GameState.apply_snapshot({ - "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], + "tick": 1, "version": 23, "entities": [], "game_time": {"day": 0, "time_of_day": 720, "day_phase": "Afternoon", "tick_rate": "Full"}, }) instance._process(0.016) diff --git a/client/ui/meta/screens/loading/loading_screen.gd b/client/ui/meta/screens/loading/loading_screen.gd index 240de1d0e..a307e2518 100644 --- a/client/ui/meta/screens/loading/loading_screen.gd +++ b/client/ui/meta/screens/loading/loading_screen.gd @@ -39,13 +39,9 @@ func _build_ui() -> void: _label.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_label) - # client_ver and proto_ver are independent — project.yaml version is the client release, - # Protocol.PROTOCOL_VERSION is the wire protocol. Mismatches between builds are visible - # only to the observer reading the loading-screen label; a future ticket will surface them. var client_ver := _read_client_version() - var proto_ver: int = Protocol.PROTOCOL_VERSION _version_label = Label.new() - _version_label.text = "v%s · protocol %d" % [client_ver, proto_ver] + _version_label.text = "v%s" % [client_ver] _version_label.add_theme_font_size_override("font_size", VERSION_FONT_SIZE) _version_label.add_theme_color_override("font_color", VERSION_COLOR) _version_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER