diff --git a/client/scripts/autoloads/game_state.gd b/client/scripts/autoloads/game_state.gd index b8fde5743..1ab7a0dd7 100644 --- a/client/scripts/autoloads/game_state.gd +++ b/client/scripts/autoloads/game_state.gd @@ -11,7 +11,7 @@ var visible_tiles: Array = [] var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups # v2 fields (D-015, D-031) -var game_time: Dictionary = {} # {day, time_of_day, day_phase, paused} or empty +var game_time: Dictionary = {} # {day, time_of_day, day_phase, tick_rate} or empty var player_facing: String = "North" # 8-directional facing direction var visibility_sectors: Dictionary = {} # Vector2i -> "Forward"/"Peripheral" diff --git a/client/scripts/autoloads/sim_bridge.gd b/client/scripts/autoloads/sim_bridge.gd index 7a87b4476..72538f0e6 100644 --- a/client/scripts/autoloads/sim_bridge.gd +++ b/client/scripts/autoloads/sim_bridge.gd @@ -302,8 +302,8 @@ func _test_snapshot() -> Dictionary: "entity_type": "Npc", "distance": npc_dist, "verbs": [ - {"kind": "Talk", "label": "Talk", "priority": 0, "available": true}, - {"kind": "ExamineNpc", "label": "Examine", "priority": 1, "available": true}, + {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, + {"kind": "ExamineNpc", "label": "Observe", "priority": 2, "available": true}, ], }) diff --git a/client/tests/test_interaction_prompt.gd b/client/tests/test_interaction_prompt.gd index f3edd42d5..83f7d53b8 100644 --- a/client/tests/test_interaction_prompt.gd +++ b/client/tests/test_interaction_prompt.gd @@ -21,8 +21,8 @@ func test_protocol_decode_v4_with_nearby_interactions() -> void: "entity_type": "Npc", "distance": 1, "verbs": [ - {"kind": "Talk", "label": "Talk", "priority": 0, "available": true}, - {"kind": "ExamineNpc", "label": "Examine", "priority": 1, "available": true}, + {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, + {"kind": "ExamineNpc", "label": "Observe", "priority": 2, "available": true}, ], }], } @@ -39,14 +39,14 @@ func test_protocol_decode_v4_with_nearby_interactions() -> void: assert_that(ni.verbs.size()).is_equal(2) assert_that(ni.verbs[0].kind).is_equal("Talk") assert_that(ni.verbs[0].label).is_equal("Talk") - assert_that(ni.verbs[0].priority).is_equal(0) + assert_that(ni.verbs[0].priority).is_equal(1) assert_that(ni.verbs[0].available).is_true() assert_that(ni.verbs[1].kind).is_equal("ExamineNpc") -func test_protocol_decode_v2_no_nearby_interactions() -> void: +func test_protocol_decode_v4_no_nearby_interactions() -> void: var raw := { "tick": 5, - "version": 2, + "version": 4, "entities": [], } var encoded = Messagepack.encode(raw) @@ -57,6 +57,7 @@ func test_protocol_decode_v2_no_nearby_interactions() -> void: func test_protocol_decode_empty_nearby_interactions() -> void: var raw := { "tick": 1, + "version": 4, "entities": [], "nearby_interactions": [], } @@ -67,6 +68,7 @@ func test_protocol_decode_empty_nearby_interactions() -> void: func test_protocol_decode_interaction_missing_verbs() -> void: var raw := { "tick": 1, + "version": 4, "entities": [], "nearby_interactions": [{"entity_id": 2}], } @@ -77,6 +79,7 @@ func test_protocol_decode_interaction_missing_verbs() -> void: func test_protocol_decode_interaction_empty_verbs() -> void: var raw := { "tick": 1, + "version": 4, "entities": [], "nearby_interactions": [{"entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": []}], } @@ -97,12 +100,22 @@ 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: var ni := [{"entity_id": 2, "entity_type": "Npc", "distance": 1, - "verbs": [{"kind": "Talk", "label": "Talk", "priority": 0, "available": true}]}] + "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}]}] GameState.apply_snapshot({"tick": 1, "entities": [], "nearby_interactions": ni}) assert_that(GameState.nearby_interactions.size()).is_equal(1) assert_that(GameState.nearby_interactions[0].entity_id).is_equal(2) @@ -110,7 +123,7 @@ func test_game_state_stores_nearby_interactions() -> void: func test_game_state_clears_nearby_interactions_when_absent() -> void: var ni := [{"entity_id": 2, "entity_type": "Npc", "distance": 1, - "verbs": [{"kind": "Talk", "label": "Talk", "priority": 0, "available": true}]}] + "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}]}] GameState.apply_snapshot({"tick": 1, "entities": [], "nearby_interactions": ni}) assert_that(GameState.nearby_interactions.size()).is_equal(1) GameState.apply_snapshot({"tick": 2, "entities": []}) @@ -152,8 +165,8 @@ func test_prompt_get_selected_verb_returns_first_kind() -> void: GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ - {"kind": "Talk", "label": "Talk", "priority": 0, "available": true}, - {"kind": "ExamineNpc", "label": "Examine", "priority": 1, "available": true}, + {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, + {"kind": "ExamineNpc", "label": "Observe", "priority": 2, "available": true}, ], }] var prompt = _make_prompt() diff --git a/client/tests/test_local_bridge.gd b/client/tests/test_local_bridge.gd index 166978bcd..08cb729ab 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, "entities": []} + var snapshot_data := {"tick": 42, "version": Protocol.PROTOCOL_VERSION, "entities": []} var encoded: Variant = Messagepack.encode(snapshot_data) assert_that(encoded.status).is_null() diff --git a/client/tests/test_protocol.gd b/client/tests/test_protocol.gd index 172e1be10..6ac1db5ed 100644 --- a/client/tests/test_protocol.gd +++ b/client/tests/test_protocol.gd @@ -188,6 +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, "entities": [ {"entity_id": 1, "x": 5.0, "y": 10.0, "z": 0, "kind": "Npc"}, {"entity_id": 2, "broken": true}, # Missing required fields @@ -321,10 +322,10 @@ func test_multi_entity_visibility_sectors() -> void: assert_that(snapshot.entities[3].visibility).is_equal("Forward") -# -- v1 backward compatibility (no v2 fields → graceful null defaults) -------- +# -- Version enforcement (strict PROTOCOL_VERSION check) -------------------- -func test_decode_v1_snapshot_graceful_defaults() -> void: - # Minimal v1 snapshot — only tick + entities, no v2 fields +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"}, ]} @@ -332,16 +333,17 @@ func test_decode_v1_snapshot_graceful_defaults() -> void: assert_that(encoded.status).is_null() var snapshot: Variant = Protocol.decode_snapshot(encoded.value) - assert_that(snapshot).is_not_null() - assert_that(snapshot.tick).is_equal(10) - assert_that(snapshot.entities.size()).is_equal(1) - # v2 fields should be null/empty, not crash - assert_that(snapshot.version).is_null() - assert_that(snapshot.game_time).is_null() - assert_that(snapshot.player_facing).is_null() - assert_that(snapshot.visible_tiles.size()).is_equal(0) - # Entity should have null visibility - assert_that(snapshot.entities[0].visibility).is_null() + 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) ----------------