feat(client): upgrade protocol decoder and GameState for v2 snapshot
Server shipped ObserverSnapshot v2 with game_time, player_facing, visible_tiles (with visibility sectors), and per-entity visibility. Protocol decoder was silently ignoring these fields. Now extracts all v2 data with null defaults for backward compatibility. GameState gains game_time, player_facing, visibility_sectors vars. Derives visible_positions from visible_tiles when present (for real server mode). Test snapshot updated with v2 fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,11 @@ var visible_entities: Array = []
|
||||
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 player_facing: String = "North" # 8-directional facing direction
|
||||
var visibility_sectors: Dictionary = {} # Vector2i -> "Forward"/"Peripheral"
|
||||
|
||||
# Player entity ID — the first entity is assumed to be the player (will be
|
||||
# refined when the server assigns explicit player entity IDs).
|
||||
var player_entity_id: int = 1
|
||||
@@ -40,3 +45,25 @@ func apply_snapshot(snapshot: Dictionary) -> void:
|
||||
visible_positions.clear()
|
||||
for pos in snapshot.visible_positions:
|
||||
visible_positions[Vector2i(pos.x, pos.y)] = true
|
||||
|
||||
# v2: game_time (D-031)
|
||||
if snapshot.has("game_time") and snapshot.game_time is Dictionary:
|
||||
game_time = snapshot.game_time
|
||||
|
||||
# v2: player_facing (D-015)
|
||||
if snapshot.has("player_facing") and snapshot.player_facing is String:
|
||||
player_facing = snapshot.player_facing
|
||||
|
||||
# v2: visible_tiles with visibility sectors
|
||||
# Derives visible_positions when not explicitly provided (real server mode)
|
||||
if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0:
|
||||
visibility_sectors.clear()
|
||||
var has_explicit_positions := snapshot.has("visible_positions")
|
||||
if not has_explicit_positions:
|
||||
visible_positions.clear()
|
||||
for vtile in snapshot.visible_tiles:
|
||||
var pos := Vector2i(vtile.x, vtile.y)
|
||||
if vtile.has("visibility"):
|
||||
visibility_sectors[pos] = vtile.visibility
|
||||
if not has_explicit_positions:
|
||||
visible_positions[pos] = true
|
||||
|
||||
@@ -240,6 +240,14 @@ func _test_snapshot() -> Dictionary:
|
||||
_test_tick += 1
|
||||
return {
|
||||
"tick": _test_tick,
|
||||
"version": 2,
|
||||
"game_time": {
|
||||
"day": 0,
|
||||
"time_of_day": 0,
|
||||
"day_phase": "Morning",
|
||||
"paused": false,
|
||||
},
|
||||
"player_facing": "North",
|
||||
"entities": [
|
||||
{
|
||||
"entity_id": 1,
|
||||
@@ -247,6 +255,7 @@ func _test_snapshot() -> Dictionary:
|
||||
"y": 10.0,
|
||||
"z": 0,
|
||||
"kind": { "variant": "Player", "data": null },
|
||||
"visibility": "Forward",
|
||||
},
|
||||
{
|
||||
"entity_id": 2,
|
||||
@@ -254,9 +263,11 @@ func _test_snapshot() -> Dictionary:
|
||||
"y": 10.0,
|
||||
"z": 0,
|
||||
"kind": { "variant": "Npc", "data": null },
|
||||
"visibility": "Peripheral",
|
||||
},
|
||||
],
|
||||
"tiles": _test_tiles(),
|
||||
"visible_tiles": _test_visible_tiles(),
|
||||
"visible_positions": _test_visible_positions(),
|
||||
}
|
||||
|
||||
@@ -292,6 +303,27 @@ func _test_tiles() -> Array:
|
||||
|
||||
return tiles
|
||||
|
||||
# Test visible tiles with visibility sectors (v2 format)
|
||||
# Tiles ahead of the player (y <= player_y) are Forward, others Peripheral.
|
||||
func _test_visible_tiles() -> Array:
|
||||
var vtiles: Array = []
|
||||
var player_x := 10
|
||||
var player_y := 10
|
||||
var radius := 4
|
||||
var room_x := 7
|
||||
var room_y := 7
|
||||
var room_w := 8
|
||||
var room_h := 8
|
||||
|
||||
for x in range(player_x - radius, player_x + radius + 1):
|
||||
for y in range(player_y - radius, player_y + radius + 1):
|
||||
var dist := absf(x - player_x) + absf(y - player_y)
|
||||
if dist <= radius:
|
||||
if x >= room_x and x < room_x + room_w and y >= room_y and y < room_y + room_h:
|
||||
var sector: String = "Forward" if y <= player_y else "Peripheral"
|
||||
vtiles.append({"x": x, "y": y, "z": 0, "visibility": sector})
|
||||
return vtiles
|
||||
|
||||
# Test visibility: player at (10,10) can see tiles within radius 4, blocked by walls
|
||||
func _test_visible_positions() -> Array:
|
||||
var positions: Array = []
|
||||
|
||||
@@ -13,7 +13,9 @@ class_name Protocol
|
||||
# -- Decode: bytes from server → GDScript types --------------------------------
|
||||
|
||||
## Decode an ObserverSnapshot from MessagePack bytes.
|
||||
## Returns { "tick": int, "entities": Array[Dictionary] } or null on error.
|
||||
## Returns decoded snapshot Dictionary or null on error.
|
||||
## v2 fields (version, game_time, player_facing, visible_tiles) default to null/empty
|
||||
## when decoding v1 snapshots for backward compatibility.
|
||||
static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
var result = Messagepack.decode(bytes)
|
||||
if result.status != null:
|
||||
@@ -41,10 +43,41 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
# GDScript int is signed 64-bit. Rust tick is u64 but will not exceed 2^63
|
||||
# in any realistic scenario (would require ~29 billion years at 10 ticks/game-minute per D-031).
|
||||
var tick: int = raw["tick"]
|
||||
|
||||
# v2 fields — optional for backward compatibility
|
||||
var version: Variant = raw.get("version")
|
||||
var game_time: Variant = raw.get("game_time")
|
||||
|
||||
# player_facing: FacingDirection is a unit enum → bare string in rmp_serde
|
||||
var player_facing: Variant = null
|
||||
var raw_facing: Variant = raw.get("player_facing")
|
||||
if raw_facing is String:
|
||||
player_facing = raw_facing
|
||||
|
||||
# visible_tiles: Array of {x, y, z, visibility}
|
||||
var visible_tiles: Array = []
|
||||
var raw_vtiles: Variant = raw.get("visible_tiles")
|
||||
if raw_vtiles is Array:
|
||||
for raw_tile in raw_vtiles:
|
||||
if raw_tile is Dictionary and raw_tile.has("x") and raw_tile.has("y") and raw_tile.has("z"):
|
||||
var tile_entry := {
|
||||
"x": int(raw_tile["x"]),
|
||||
"y": int(raw_tile["y"]),
|
||||
"z": int(raw_tile["z"]),
|
||||
}
|
||||
var vis: Variant = raw_tile.get("visibility")
|
||||
if vis is String:
|
||||
tile_entry["visibility"] = vis
|
||||
visible_tiles.append(tile_entry)
|
||||
|
||||
return {
|
||||
"tick": tick,
|
||||
"entities": entities,
|
||||
"decode_errors": dropped,
|
||||
"version": version,
|
||||
"game_time": game_time,
|
||||
"player_facing": player_facing,
|
||||
"visible_tiles": visible_tiles,
|
||||
}
|
||||
|
||||
|
||||
@@ -56,12 +89,20 @@ static func _decode_entity(raw: Dictionary) -> Variant:
|
||||
return null
|
||||
|
||||
var entity_id: int = raw["entity_id"]
|
||||
|
||||
# v2: visibility sector (Forward/Peripheral). null for v1 entities.
|
||||
var visibility: Variant = null
|
||||
var raw_vis: Variant = raw.get("visibility")
|
||||
if raw_vis is String:
|
||||
visibility = raw_vis
|
||||
|
||||
return {
|
||||
"entity_id": entity_id,
|
||||
"x": float(raw["x"]),
|
||||
"y": float(raw["y"]),
|
||||
"z": int(raw["z"]),
|
||||
"kind": _decode_enum_variant(raw["kind"]),
|
||||
"visibility": visibility,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -271,6 +271,57 @@ func test_encode_player_inputs_empty() -> void:
|
||||
assert_that(raw.value.size()).is_equal(0)
|
||||
|
||||
|
||||
# -- v2 snapshot decoding -------------------------------------------------------
|
||||
|
||||
func test_decode_snapshot_v2_full() -> void:
|
||||
var bytes = _load_fixture("snapshot_v2_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
|
||||
assert_that(snapshot).is_not_null()
|
||||
assert_that(snapshot.tick).is_equal(500)
|
||||
assert_that(snapshot.version).is_equal(2)
|
||||
|
||||
# game_time
|
||||
assert_that(snapshot.game_time).is_not_null()
|
||||
assert_that(snapshot.game_time.day).is_equal(1)
|
||||
assert_that(snapshot.game_time.time_of_day).is_equal(720)
|
||||
assert_that(snapshot.game_time.paused).is_false()
|
||||
|
||||
# player_facing (unit enum → bare string)
|
||||
assert_that(snapshot.player_facing).is_equal("Southeast")
|
||||
|
||||
# entities with visibility sector
|
||||
assert_that(snapshot.entities.size()).is_equal(1)
|
||||
assert_that(snapshot.entities[0].visibility).is_equal("Forward")
|
||||
|
||||
# visible_tiles
|
||||
assert_that(snapshot.visible_tiles.size()).is_equal(3)
|
||||
assert_that(snapshot.visible_tiles[0].x).is_equal(10)
|
||||
assert_that(snapshot.visible_tiles[0].visibility).is_equal("Forward")
|
||||
assert_that(snapshot.visible_tiles[1].visibility).is_equal("Peripheral")
|
||||
|
||||
|
||||
func test_existing_fixtures_have_v2_fields() -> void:
|
||||
# All fixtures are generated by v2 fixture_snapshot() — verify decoder extracts v2 fields
|
||||
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(2)
|
||||
assert_that(snapshot.player_facing).is_equal("North")
|
||||
assert_that(snapshot.game_time).is_not_null()
|
||||
|
||||
|
||||
func test_multi_entity_visibility_sectors() -> void:
|
||||
# snapshot_multi_entity has: Player=Forward, Npc=Peripheral, Object=Forward, Terrain=Forward
|
||||
var bytes = _load_fixture("snapshot_multi_entity")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.entities[0].visibility).is_equal("Forward")
|
||||
assert_that(snapshot.entities[1].visibility).is_equal("Peripheral")
|
||||
assert_that(snapshot.entities[2].visibility).is_equal("Forward")
|
||||
assert_that(snapshot.entities[3].visibility).is_equal("Forward")
|
||||
|
||||
|
||||
# -- Batch input fixture (D-030 Layer 1 bidirectional symmetry) ----------------
|
||||
|
||||
func test_decode_batch_input_fixture() -> void:
|
||||
|
||||
Reference in New Issue
Block a user