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:
@@ -13,7 +13,8 @@ extends Node
|
||||
## 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).
|
||||
const PROTOCOL_VERSION: int = 21
|
||||
## v23: adds bookmark_catalog field to ObserverSnapshot (#614).
|
||||
const PROTOCOL_VERSION: int = 23
|
||||
|
||||
# -- Decode: bytes from server → GDScript types --------------------------------
|
||||
|
||||
@@ -379,6 +380,41 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
"category": str(raw_ticker.get("category", "")),
|
||||
}
|
||||
|
||||
# v23: bookmark_catalog (#614) — one-shot response to RequestBookmarkCatalog.
|
||||
# {bookmarks: [{id, title, subtitle, flavor, default_location, allowed_locations,
|
||||
# allowed_locations_cultures, career, starting_capital_tractus}]} or null.
|
||||
var bookmark_catalog: Variant = null
|
||||
var raw_bmc: Variant = raw.get("bookmark_catalog")
|
||||
if raw_bmc is Dictionary and raw_bmc.get("bookmarks") is Array:
|
||||
var bm_entries: Array = []
|
||||
for raw_bm in raw_bmc["bookmarks"]:
|
||||
if not raw_bm is Dictionary or not raw_bm.has("id"):
|
||||
continue
|
||||
var al: Array = []
|
||||
var raw_al: Variant = raw_bm.get("allowed_locations")
|
||||
if raw_al is Array:
|
||||
for loc in raw_al:
|
||||
al.append(str(loc))
|
||||
var alc: Array = []
|
||||
var raw_alc: Variant = raw_bm.get("allowed_locations_cultures")
|
||||
if raw_alc is Array:
|
||||
for cul in raw_alc:
|
||||
alc.append(str(cul))
|
||||
bm_entries.append(
|
||||
{
|
||||
"id": str(raw_bm["id"]),
|
||||
"title": str(raw_bm.get("title", "")),
|
||||
"subtitle": str(raw_bm.get("subtitle", "")),
|
||||
"flavor": str(raw_bm.get("flavor", "")),
|
||||
"default_location": str(raw_bm.get("default_location", "")),
|
||||
"allowed_locations": al,
|
||||
"allowed_locations_cultures": alc,
|
||||
"career": str(raw_bm.get("career", "tycoon")),
|
||||
"starting_capital_tractus": int(raw_bm.get("starting_capital_tractus", 0)),
|
||||
}
|
||||
)
|
||||
bookmark_catalog = {"bookmarks": bm_entries}
|
||||
|
||||
# TODO(server): Send stationary_ticks in ObserverSnapshot (D-071, D-020).
|
||||
# Server already tracks this in ListeningFocus component (server/src/simulation/listening.rs).
|
||||
# When server populates this field, client-side accumulation fallback in game_state.gd
|
||||
@@ -471,6 +507,7 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
"triangle_crisis_events": triangle_crisis_events,
|
||||
"current_ticker": current_ticker,
|
||||
"settings_response": settings_response,
|
||||
"bookmark_catalog": bookmark_catalog,
|
||||
}
|
||||
|
||||
|
||||
@@ -699,6 +736,34 @@ static func encode_change_settings(enabled: bool) -> PackedByteArray:
|
||||
return result.value
|
||||
|
||||
|
||||
## Encode a RequestBookmarkCatalog action (#614).
|
||||
## Unit variant — no payload. Server responds with bookmark_catalog in the next snapshot.
|
||||
static func encode_request_bookmark_catalog() -> PackedByteArray:
|
||||
var entries: Array = [{"tick": 0, "action_name": "RequestBookmarkCatalog", "action_data": null}]
|
||||
var result = Messagepack.encode(entries)
|
||||
if result.status != null:
|
||||
push_error("Protocol: encode_request_bookmark_catalog failed: %s" % result.status)
|
||||
return PackedByteArray()
|
||||
return result.value
|
||||
|
||||
|
||||
## Encode a ConfirmBookmark action (#614, #680).
|
||||
## Struct variant with bookmark_id and starting_location_id.
|
||||
static func encode_confirm_bookmark(bookmark_id: String, starting_location_id: String) -> PackedByteArray:
|
||||
var entries: Array = [
|
||||
{
|
||||
"tick": 0,
|
||||
"action_name": "ConfirmBookmark",
|
||||
"action_data": {"bookmark_id": bookmark_id, "starting_location_id": starting_location_id},
|
||||
}
|
||||
]
|
||||
var result = Messagepack.encode(entries)
|
||||
if result.status != null:
|
||||
push_error("Protocol: encode_confirm_bookmark failed: %s" % result.status)
|
||||
return PackedByteArray()
|
||||
return result.value
|
||||
|
||||
|
||||
## 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:
|
||||
|
||||
Reference in New Issue
Block a user