feat(client): protocol v23 — bookmark_catalog decode + bookmark actions (Workstream 3)
Adds client-side wire support for the bookmark catalog (#614) and the two associated player actions. PROTOCOL_VERSION bumps from 21 to 23: - v22 (server): RequestBookmarkCatalog + ConfirmBookmark player actions - v23 (server): bookmark_catalog field on ObserverSnapshot Decode: - protocol.gd decode_snapshot extracts optional bookmark_catalog. Defensive parse of BookmarkWire fields (id, title, subtitle, flavor, default_location, allowed_locations, allowed_locations_cultures, career, starting_capital_tractus). Missing or malformed → null. - snapshot_handler.gd caches the catalog into GameState.bookmark_catalog on each snapshot (server pushes on tick 0; re-fetchable via RequestBookmarkCatalog). - GameState gains bookmark_catalog: Array = [] (untyped per autoload parse-order discipline; default empty so callers can iterate without null checks). Encode: - encode_request_bookmark_catalog() — unit variant, sent to trigger a re-push if the cached catalog is missing. - encode_confirm_bookmark(bookmark_id, starting_location_id) — struct variant matching server rmp_serde shape. Called from character creation on Start (lands in Workstream 6). Tests: - 5 new cases in test_protocol.gd: hand-built bookmark_catalog decode (all 9 fields asserted), fixture-based decode round-trip, missing- field null behavior, RequestBookmarkCatalog encode roundtrip, ConfirmBookmark encode roundtrip. - All 12 existing snapshot fixtures regenerated from server via `cargo test --test gen_fixtures -- --ignored`. The new snapshot_with_bookmark_catalog.msgpack fixture was generated by the same pass. Verification: - gdlint clean - godot --headless --path client --quit — no SCRIPT ERROR - test_protocol 62/62, test_client_p3 24/24, test_implant_nav_stack 52/52, test_implant_registry 42/42, test_implant_app_lifecycle 36/36 Workstream 4 (Option A sequencing via loading_screen + SimBridge connect) lands next. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -383,3 +383,109 @@ func test_decode_diagonal_fixtures() -> void:
|
||||
assert_that(input.tick).is_equal(100)
|
||||
assert_that(input.action.variant).is_equal(pair[1])
|
||||
assert_that(input.action.data).is_null()
|
||||
|
||||
|
||||
# -- v23: BookmarkCatalog decode -----------------------------------------------
|
||||
|
||||
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,
|
||||
"entities": [],
|
||||
"bookmark_catalog": {
|
||||
"bookmarks": [
|
||||
{
|
||||
"id": "bm_tycoon_arion",
|
||||
"title": "The Arion Run",
|
||||
"subtitle": "Mid-range freight corridor",
|
||||
"flavor": "You have contacts. Use them.",
|
||||
"default_location": "loc_arion_prime",
|
||||
"allowed_locations": ["loc_arion_prime", "loc_vethis_station"],
|
||||
"allowed_locations_cultures": ["arion", "vethis"],
|
||||
"career": "tycoon",
|
||||
"starting_capital_tractus": 50000,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
var encoded: Variant = Messagepack.encode(raw)
|
||||
assert_that(encoded.status).is_null()
|
||||
|
||||
var snapshot: Variant = Protocol.decode_snapshot(encoded.value)
|
||||
assert_that(snapshot).is_not_null()
|
||||
assert_that(snapshot.bookmark_catalog).is_not_null()
|
||||
|
||||
var bmc: Dictionary = snapshot.bookmark_catalog
|
||||
assert_that(bmc.has("bookmarks")).is_true()
|
||||
assert_that(bmc["bookmarks"].size()).is_equal(1)
|
||||
|
||||
var bm: Dictionary = bmc["bookmarks"][0]
|
||||
assert_that(bm["id"]).is_equal("bm_tycoon_arion")
|
||||
assert_that(bm["title"]).is_equal("The Arion Run")
|
||||
assert_that(bm["default_location"]).is_equal("loc_arion_prime")
|
||||
assert_that(bm["allowed_locations"].size()).is_equal(2)
|
||||
assert_that(bm["allowed_locations"][0]).is_equal("loc_arion_prime")
|
||||
assert_that(bm["allowed_locations_cultures"][1]).is_equal("vethis")
|
||||
assert_that(bm["career"]).is_equal("tycoon")
|
||||
assert_that(bm["starting_capital_tractus"]).is_equal(50000)
|
||||
|
||||
|
||||
func test_decode_snapshot_bookmark_catalog_fixture() -> void:
|
||||
# Cross-language round-trip: Rust-generated fixture (#614).
|
||||
var bytes = _load_fixture("snapshot_with_bookmark_catalog")
|
||||
var snapshot: Variant = Protocol.decode_snapshot(bytes)
|
||||
|
||||
assert_that(snapshot).is_not_null()
|
||||
assert_that(snapshot.bookmark_catalog).is_not_null()
|
||||
var bmc: Dictionary = snapshot.bookmark_catalog
|
||||
assert_that(bmc["bookmarks"].size()).is_greater(0)
|
||||
var bm: Dictionary = bmc["bookmarks"][0]
|
||||
assert_that(bm.has("id")).is_true()
|
||||
assert_that(bm.has("title")).is_true()
|
||||
assert_that(bm.has("allowed_locations")).is_true()
|
||||
assert_that(bm["career"]).is_equal("tycoon")
|
||||
|
||||
|
||||
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,
|
||||
"entities": [],
|
||||
}
|
||||
var encoded: Variant = Messagepack.encode(raw)
|
||||
var snapshot: Variant = Protocol.decode_snapshot(encoded.value)
|
||||
assert_that(snapshot).is_not_null()
|
||||
assert_that(snapshot.bookmark_catalog).is_null()
|
||||
|
||||
|
||||
# -- v23: RequestBookmarkCatalog + ConfirmBookmark encoding --------------------
|
||||
|
||||
func test_encode_request_bookmark_catalog_roundtrip() -> void:
|
||||
var bytes := Protocol.encode_request_bookmark_catalog()
|
||||
assert_that(bytes.size()).is_greater(0)
|
||||
|
||||
var raw: Variant = Messagepack.decode(bytes)
|
||||
assert_that(raw.status).is_null()
|
||||
assert_that(raw.value is Array).is_true()
|
||||
assert_that(raw.value.size()).is_equal(1)
|
||||
|
||||
var entry: Dictionary = raw.value[0]
|
||||
assert_that(entry["action_name"]).is_equal("RequestBookmarkCatalog")
|
||||
assert_that(entry.get("action_data")).is_null()
|
||||
|
||||
|
||||
func test_encode_confirm_bookmark_roundtrip() -> void:
|
||||
var bytes := Protocol.encode_confirm_bookmark("bm_tycoon_arion", "loc_arion_prime")
|
||||
assert_that(bytes.size()).is_greater(0)
|
||||
|
||||
var raw: Variant = Messagepack.decode(bytes)
|
||||
assert_that(raw.status).is_null()
|
||||
assert_that(raw.value is Array).is_true()
|
||||
|
||||
var entry: Dictionary = raw.value[0]
|
||||
assert_that(entry["action_name"]).is_equal("ConfirmBookmark")
|
||||
var data: Dictionary = entry["action_data"]
|
||||
assert_that(data["bookmark_id"]).is_equal("bm_tycoon_arion")
|
||||
assert_that(data["starting_location_id"]).is_equal("loc_arion_prime")
|
||||
|
||||
Reference in New Issue
Block a user