extends Node ## MessagePack codec for the Rust↔Godot wire protocol (D-020). ## ## Encodes/decodes ObserverSnapshot and PlayerInput to match ## rmp_serde's named-field encoding of server/src/bridge/types.rs. ## ## Wire format notes (rmp_serde with to_vec_named): ## Structs → msgpack maps with string keys ## Unit enum variants (no data) → bare strings ("MoveNorth", "Npc") ## Data enum variants → single-element maps ({"UsePerceptionMode": "thermal"}) static func _mp(): return load("res://addons/messagepack/messagepack.gd") ## BrowseRequest/BrowseResponse codec (T-1131/T-1133) — factored into its own ## file to stay under gdlint's max-file-lines, load()'d here (not referenced ## as a bare class_name) per the autoload parse-order rule (CLAUDE.md): ## Protocol is an autoload, and autoload scripts compile before global ## class_name scripts are registered — a top-level class_name reference would ## fail to parse. By the time any caller actually runs (always post-boot), ## load() returns the already-cached resource with no reload cost. static func _bp(): return load("res://scripts/protocol/browse_protocol.gd") ## AtlasLayerRequest/Response + StarMapRequest/Response + CityNamesRequest/Response ## codec (T-1118) — same load()-by-path rationale as _bp() above. static func _amp(): return load("res://scripts/protocol/atlas_map_protocol.gd") # -- Decode: bytes from server → GDScript types -------------------------------- ## Decode a raw MessagePack frame to its top-level value (or null on error). ## #960: lets receive_bytes decode a frame once and branch by shape before ## committing to the heavier snapshot decode. static func decode_raw(bytes: PackedByteArray) -> Variant: var result = _mp().decode(bytes) if result.status != null: push_error("Protocol: msgpack decode failed: %s" % result.status) return null return result.value ## Decode an ObserverSnapshot from MessagePack bytes. ## 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 raw = decode_raw(bytes) if raw == null: return null return _decode_snapshot_from_raw(raw) ## Build an ObserverSnapshot from an already-decoded raw value (the snapshot ## body, shared by decode_snapshot and the receive-side classifier). static func _decode_snapshot_from_raw(raw: Variant) -> Variant: if not raw is Dictionary or not raw.has("tick") or not raw.has("entities"): push_error("Protocol: snapshot missing required fields") return null var entities: Array[Dictionary] = [] var raw_entities: Array = raw["entities"] var dropped := 0 for raw_entity in raw_entities: var entity = _decode_entity(raw_entity) if entity != null: entities.append(entity) else: dropped += 1 if dropped > 0: push_error( ( "Protocol: %d/%d entities failed to decode (D-010 information boundary violation)" % [dropped, raw_entities.size()] ) ) # 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"] # 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 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, tile_kind} 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 # tile_kind: Floor/Wall/Door/Object — map to client tile type strings var kind: Variant = raw_tile.get("tile_kind") if kind is String: tile_entry["type"] = kind.to_lower() else: tile_entry["type"] = "floor" visible_tiles.append(tile_entry) # v4: nearby_interactions (#404/#405) var nearby_interactions: Array = [] var raw_interactions: Variant = raw.get("nearby_interactions") if raw_interactions is Array: for raw_ni in raw_interactions: var ni = _decode_nearby_interaction(raw_ni) if ni != null: nearby_interactions.append(ni) # v5: current_monologue (#414) var current_monologue: Variant = null var raw_monologue: Variant = raw.get("current_monologue") if raw_monologue is Dictionary and raw_monologue.has("text"): current_monologue = { "id": str(raw_monologue.get("id", "")), "text": str(raw_monologue["text"]), "duration_seconds": float(raw_monologue.get("duration_seconds", 5.0)), } # v6: player_stance (#449, D-053) — unit enum → bare string var player_stance: String = "Walk" var raw_stance: Variant = raw.get("player_stance") if raw_stance is String: player_stance = raw_stance # v6: player_inventory (#449, D-065) — array of {item_id, name, slot} var player_inventory: Array = [] var raw_inventory: Variant = raw.get("player_inventory") if raw_inventory is Array: for raw_item in raw_inventory: if raw_item is Dictionary and raw_item.has("item_id") and raw_item.has("name"): ( player_inventory . append( { "item_id": int(raw_item["item_id"]), "name": str(raw_item["name"]), "slot": int(raw_item.get("slot", 0)), } ) ) # v7: pending_recognitions (#431, D-059/D-060) — cognitive delay fog entities # Bounded: server sends at most ~50 pending recognitions per snapshot (practical limit # given perception range). Excessive arrays are truncated to prevent allocation abuse. const MAX_PENDING_RECOGNITIONS: int = 64 var pending_recognitions: Array = [] var raw_recognitions: Variant = raw.get("pending_recognitions") if raw_recognitions is Array: var count := 0 for raw_pr in raw_recognitions: if count >= MAX_PENDING_RECOGNITIONS: push_warning( ( "Protocol: pending_recognitions truncated at %d entries" % MAX_PENDING_RECOGNITIONS ) ) break if ( raw_pr is Dictionary and raw_pr.has("entity_id") and raw_pr.has("x") and raw_pr.has("y") ): ( pending_recognitions . append( { "entity_id": int(raw_pr["entity_id"]), "x": float(raw_pr["x"]), "y": float(raw_pr["y"]), "z": int(raw_pr.get("z", 0)), "remaining_ticks": int(raw_pr.get("remaining_ticks", 0)), "total_delay_ticks": int(raw_pr.get("total_delay_ticks", 1)), } ) ) count += 1 # v7: current_dialogue (#435, D-061/D-062) — NPC speech + player response options # Options carry response_id for server round-trip and priority for display ordering. # Locked options are invisible (D-062): server filters before sending. var current_dialogue: Variant = null var raw_dialogue: Variant = raw.get("current_dialogue") if raw_dialogue is Dictionary and raw_dialogue.has("speech"): var dialogue_options: Array = [] var raw_options: Variant = raw_dialogue.get("options") if raw_options is Array: for raw_opt in raw_options: if raw_opt is Dictionary and raw_opt.has("text"): ( dialogue_options . append( { "text": str(raw_opt["text"]), "response_id": str(raw_opt.get("response_id", "")), "priority": int(raw_opt.get("priority", 0)), "confrontation": bool(raw_opt.get("confrontation", false)), } ) ) current_dialogue = { "npc_name": str(raw_dialogue.get("npc_name", "")), "npc_entity_id": int(raw_dialogue.get("npc_entity_id", -1)), "speech": str(raw_dialogue["speech"]), "options": dialogue_options, } # v8: dialogue_response (#305, D-028) — NPC's spoken dialogue line from server # Separate from current_dialogue (client-side rich dialogue state). var dialogue_response: Variant = null var raw_dr: Variant = raw.get("dialogue_response") if raw_dr is Dictionary and raw_dr.has("text"): dialogue_response = { "line_id": str(raw_dr.get("line_id", "")), "text": str(raw_dr["text"]), "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 = [] var raw_conv_events: Variant = raw.get("conversation_events") if raw_conv_events is Array: for raw_ce in raw_conv_events: if raw_ce is Dictionary and raw_ce.has("occluded_line"): ( conversation_events . append( { "speaker_id": int(raw_ce.get("speaker_id", 0)), "target_id": int(raw_ce.get("target_id", 0)), "speaker_name": str(raw_ce.get("speaker_name", "")), "target_name": str(raw_ce.get("target_name", "")), "occluded_line": str(raw_ce["occluded_line"]), } ) ) # v9: conversation_ended (#535, D-078) — pairs whose conversation ended this tick. var conversation_ended: Array = [] var raw_conv_ended: Variant = raw.get("conversation_ended") if raw_conv_ended is Array: for raw_end in raw_conv_ended: if raw_end is Dictionary: ( conversation_ended . append( { "speaker_id": int(raw_end.get("speaker_id", 0)), "target_id": int(raw_end.get("target_id", 0)), } ) ) # v14: poi_list (#151) — discovered POIs for minimap rendering. # Each entry: {poi_id, name, x, y, z, poi_category}. Positions in sim tile coords. var poi_list: Array = [] var raw_pois: Variant = raw.get("poi_list") if raw_pois is Array: for raw_poi in raw_pois: if ( raw_poi is Dictionary and raw_poi.has("poi_id") and raw_poi.has("x") and raw_poi.has("y") ): ( poi_list . append( { "poi_id": str(raw_poi["poi_id"]), "name": str(raw_poi.get("name", "")), "x": int(raw_poi["x"]), "y": int(raw_poi["y"]), "z": int(raw_poi.get("z", 0)), "poi_category": str(raw_poi.get("poi_category", raw_poi.get("category", "Location"))), } ) ) # v14: examine_result (#174, #242) — character-filtered observation text. # {entity_id, text, confidence} or null. Auto-dismisses on client after 4-6 seconds. var examine_result: Variant = null var raw_examine: Variant = raw.get("examine_result") if raw_examine is Dictionary and raw_examine.has("text"): examine_result = { "entity_id": int(raw_examine.get("entity_id", 0)), "text": str(raw_examine["text"]), "confidence": str(raw_examine.get("confidence", "KnowsOf")), } # v15: save_result (#554, D-085) — one-shot save/load operation result. # {success: bool, kind: "save"|"load", error: String|null} var save_result: Variant = null var raw_save: Variant = raw.get("save_result") if raw_save is Dictionary: save_result = { "success": bool(raw_save.get("success", false)), "kind": str(raw_save.get("kind", "")), "error": raw_save.get("error"), } # v18: debug_response (#580) — debug console command result. # {command: String, text: String, success: bool} var debug_response: Variant = null var raw_debug: Variant = raw.get("debug_response") if raw_debug is Dictionary: debug_response = { "command": str(raw_debug.get("command", "")), "text": str(raw_debug.get("text", "")), "success": bool(raw_debug.get("success", false)), } # v19: triangle_crisis_events (#590, D-072/D-089) — one-shot activation events. # Each entry: {triangle_id: int}. Client deduplicates by triangle_id across ticks. # v0.1 intentional omissions: role_assignments, trigger_npc_id, tick are not decoded # here — the client has no use for them in v0.1 (no overlay, no entity targeting). # Add when #593+ requires richer client-side event handling. var triangle_crisis_events: Array = [] var raw_tce: Variant = raw.get("triangle_crisis_events") if raw_tce is Array: for raw_ev in raw_tce: if raw_ev is Dictionary and raw_ev.has("triangle_id"): ( triangle_crisis_events . append( { "triangle_id": int(raw_ev["triangle_id"]), } ) ) # v20: settings_response (#627, D-138) — server ack after ChangeSettings / full settings dump # after RequestAllSettings. kind = "full"|"ack". "full": settings array [{key, value}]. # "ack": {success: bool, key: String, error: String|null}. var settings_response: Variant = null var raw_sr: Variant = raw.get("settings_response") if raw_sr is Dictionary: var sr_kind: String = str(raw_sr.get("kind", "")) if sr_kind == "full": var sr_settings: Array = [] var raw_sr_settings: Variant = raw_sr.get("settings") if raw_sr_settings is Array: for raw_s in raw_sr_settings: if raw_s is Dictionary and raw_s.has("key"): ( sr_settings . append( { "key": str(raw_s["key"]), "value": raw_s.get("value"), } ) ) settings_response = {"kind": "full", "settings": sr_settings} elif sr_kind == "ack": settings_response = { "kind": "ack", "success": bool(raw_sr.get("success", false)), "key": str(raw_sr.get("key", "")), "error": raw_sr.get("error"), } # v19: current_ticker (#592) — scrolling news headline when in The Last Shift zone. # {id: String, text: String, category: String} or null when player outside bar zone. var current_ticker: Variant = null var raw_ticker: Variant = raw.get("current_ticker") if raw_ticker is Dictionary and raw_ticker.has("text"): current_ticker = { "id": str(raw_ticker.get("id", "")), "text": str(raw_ticker["text"]), "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", "")), "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 # can be removed — apply_snapshot() should contain only direct field assignments. var stationary_ticks: Variant = null var raw_st: Variant = raw.get("stationary_ticks") if raw_st != null: stationary_ticks = int(raw_st) # TODO(server): Send top-level zone_id string in ObserverSnapshot (D-073, D-020). # Server sends zone_id per VisibleTile but not as a top-level snapshot field. # When server populates this, client-side tile iteration fallback in game_state.gd # can be removed — apply_snapshot() should contain only direct field assignments. var zone_id: Variant = null var raw_zid: Variant = raw.get("zone_id") if raw_zid is String: zone_id = raw_zid # v14: player_knowledge (#264, D-041) — partial KG dump for journal panel. # {entities: [{entity_id, name, confidence, source, state, relationship, last_observed_tick}], # facts: [{fact_id, confidence, source, state, acquired_tick}]} var player_knowledge: Variant = null var raw_pk: Variant = raw.get("player_knowledge") if raw_pk is Dictionary: var kg_entities: Array = [] var raw_kg_entities: Variant = raw_pk.get("entities") if raw_kg_entities is Array: for raw_ke in raw_kg_entities: if raw_ke is Dictionary and raw_ke.has("entity_id"): ( kg_entities . append( { "entity_id": int(raw_ke["entity_id"]), "name": str(raw_ke.get("name", "Unknown")), "confidence": str(raw_ke.get("confidence", "Suspects")), "source": str(raw_ke.get("source", "")), "state": str(raw_ke.get("state", "Active")), "relationship": str(raw_ke.get("relationship", "Unknown")), "last_observed_tick": int(raw_ke.get("last_observed_tick", 0)), } ) ) var kg_facts: Array = [] var raw_kg_facts: Variant = raw_pk.get("facts") if raw_kg_facts is Array: for raw_kf in raw_kg_facts: if raw_kf is Dictionary and raw_kf.has("fact_id"): ( kg_facts . append( { "fact_id": str(raw_kf["fact_id"]), "confidence": str(raw_kf.get("confidence", "Suspects")), "source": str(raw_kf.get("source", "")), "state": str(raw_kf.get("state", "Active")), "acquired_tick": int(raw_kf.get("acquired_tick", 0)), } ) ) player_knowledge = { "entities": kg_entities, "facts": kg_facts, } return { "tick": tick, "entities": entities, "decode_errors": dropped, "game_time": game_time, "player_facing": player_facing, "player_stance": player_stance, "player_inventory": player_inventory, "visible_tiles": visible_tiles, "nearby_interactions": nearby_interactions, "current_monologue": current_monologue, "current_dialogue": current_dialogue, "dialogue_response": dialogue_response, "pending_recognitions": pending_recognitions, "conversation_events": conversation_events, "conversation_ended": conversation_ended, "poi_list": poi_list, "examine_result": examine_result, "player_knowledge": player_knowledge, "save_result": save_result, "debug_response": debug_response, "stationary_ticks": stationary_ticks, "zone_id": zone_id, "triangle_crisis_events": triangle_crisis_events, "current_ticker": current_ticker, "settings_response": settings_response, "bookmark_catalog": bookmark_catalog, "gauntlet_mode": gauntlet_mode, "room_id": room_id, } ## Decode a single VisibleEntity from a raw msgpack map. static func _decode_entity(raw: Dictionary) -> Variant: if ( not raw.has("entity_id") or not raw.has("x") or not raw.has("y") or not raw.has("z") or not raw.has("kind") ): push_warning("Protocol: entity missing required fields: %s" % str(raw.keys())) 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 # v4: relationship (D-033) and observation state var relationship: String = "Unknown" var raw_rel: Variant = raw.get("relationship") if raw_rel is String: relationship = raw_rel var observation: Variant = "Visible" var raw_obs: Variant = raw.get("observation") if raw_obs is String: observation = raw_obs elif raw_obs is Dictionary: observation = _decode_enum_variant(raw_obs) 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, "relationship": relationship, "observation": observation, } ## Decode a NearbyInteraction from a raw msgpack map. ## Returns {entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]} or null. static func _decode_nearby_interaction(raw) -> Variant: if not raw is Dictionary: return null if not raw.has("entity_id") or not raw.has("verbs"): push_warning("Protocol: nearby_interaction missing required fields: %s" % str(raw.keys())) return null var verbs: Array[Dictionary] = [] var raw_verbs: Variant = raw.get("verbs") if raw_verbs is Array: for rv in raw_verbs: var verb = _decode_verb_option(rv) if verb != null: verbs.append(verb) if verbs.is_empty(): return null var entity_type: String = "" var raw_et: Variant = raw.get("entity_type") if raw_et is String: entity_type = raw_et return { "entity_id": int(raw["entity_id"]), "entity_type": entity_type, "distance": int(raw.get("distance", 0)), "verbs": verbs, } ## Decode a VerbOption from a raw msgpack map. ## Returns {kind, label, priority, available} or null. static func _decode_verb_option(raw) -> Variant: if not raw is Dictionary or not raw.has("label"): return null var kind: String = "" var raw_kind: Variant = raw.get("kind") if raw_kind is String: kind = raw_kind return { "kind": kind, "label": str(raw["label"]), "priority": int(raw.get("priority", 0)), "available": bool(raw.get("available", true)), } ## Decode an enum variant from rmp_serde's encoding. ## Unit variants are bare strings, data variants are single-element maps. ## Returns { "variant": String, "data": Variant } in both cases. static func _decode_enum_variant(raw) -> Dictionary: if raw is String: return {"variant": raw, "data": null} if raw is Dictionary and raw.size() == 1: var variant_name: String = raw.keys()[0] return {"variant": variant_name, "data": raw[variant_name]} push_warning("Protocol: unexpected enum encoding: %s" % str(raw)) return {"variant": "Unknown", "data": raw} # -- Encode: GDScript types → bytes to server ---------------------------------- ## Encode a StartupMessage to MessagePack bytes (#175, #718). ## Sent by the client immediately after handshake validation. ## Server reads this to initialize SimRng (D-010, D-029). ## character_visual: optional CharacterVisualDescriptor — included as "character_visual_descriptor" dict. ## role: D-254 §2 ConnectionRole wire value — "" (default, every caller before ## D-254) omits the "role" key entirely, matching server StartupMessage::role's ## #[serde(default)] and decoding as ConnectionRole::Player — byte-identical to ## pre-D-254 output. atlas_standalone.gd is the one caller that passes "Reader" ## (unit enum variant, bare string per this file's header wire-format note — ## same encoding as PlayerAction unit variants like "MoveNorth"). static func encode_startup_message( world_seed: int, character_visual: Variant = null, role: String = "" ) -> PackedByteArray: var msg := { "world_seed": world_seed, } if character_visual != null and character_visual.has_method("to_dict"): msg["character_visual_descriptor"] = character_visual.to_dict() if not role.is_empty(): msg["role"] = role var result = _mp().encode(msg) if result.status != null: push_error("Protocol: startup message encode failed: %s" % result.status) return PackedByteArray() return result.value ## Encode a PlayerInput to MessagePack bytes. ## action_name: one of "MoveNorth", "MoveSouth", "MoveEast", "MoveWest", ## "Interact", "UsePerceptionMode", "Pause", "Unpause" ## action_data: null for unit variants, String for UsePerceptionMode static func encode_player_input( tick: int, action_name: String, action_data: Variant = null ) -> PackedByteArray: var action_value: Variant = _encode_action(action_name, action_data) var input := { "tick": tick, "action": action_value, } var result = _mp().encode(input) if result.status != null: push_error("Protocol: msgpack encode failed: %s" % result.status) return PackedByteArray() return result.value ## Encode an array of PlayerInputs to MessagePack bytes (Vec wire format). ## Server expects one framed message per tick containing all inputs as a msgpack array. ## Each entry: { "tick": int, "action_name": String, "action_data": Variant (optional) } static func encode_player_inputs(inputs: Array) -> PackedByteArray: var wire_inputs: Array = [] for input in inputs: var action_name: String = input["action_name"] var action_data: Variant = input.get("action_data") ( wire_inputs . append( { "tick": input["tick"], "action": _encode_action(action_name, action_data), } ) ) var result = _mp().encode(wire_inputs) if result.status != null: push_error("Protocol: msgpack encode failed: %s" % result.status) return PackedByteArray() return result.value ## Encode a PlayerAction for the wire. ## Struct variants (Interact) always need their fields even when null. ## Unit variants (MoveNorth, Pause, etc.) encode as bare strings. static func _encode_action(action_name: String, action_data: Variant) -> Variant: if action_data != null: return {action_name: action_data} # Interact is a struct variant — server expects named fields, not a bare string if action_name == "Interact": return {"Interact": {"target_entity_id": null, "verb": null}} return action_name ## Encode a ChangeSettings action for the AI-Enhanced Dialogue toggle (#646, D-138). ## Returns a MessagePack-encoded Vec in buffer-entry format ## (action_name + action_data), suitable for inspection in tests and for ## queuing via SimBridge._outbound_buffer. ## The server receives this as a PlayerAction::ChangeSettings variant. static func encode_change_settings(enabled: bool) -> PackedByteArray: var entries: Array = [ { "tick": 0, "action_name": "ChangeSettings", "action_data": {"ai_enhanced_dialogue": enabled}, } ] var result = _mp().encode(entries) if result.status != null: push_error("Protocol: encode_change_settings failed: %s" % result.status) return 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 = _mp().encode(entries) if result.status != null: push_error("Protocol: encode_request_bookmark_catalog failed: %s" % result.status) return PackedByteArray() return result.value ## AtlasLayerRequest/Response + StarMapRequest/Response + CityNamesRequest/ ## Response codec — factored into atlas_map_protocol.gd (T-1118) to stay ## under gdlint's max-file-lines, same rationale as _bp()/browse_protocol.gd ## above. Every function below is a thin delegate under its ORIGINAL public ## name — external callers (sim_bridge.gd, test_atlas_overlays.gd, ## test_atlas_data_delivery.gd) are unaffected by the move. ## window_center/window_n (T-1138, D-226 T-1124 amendment §1): optional ## windowed district-resolution regional-map query — see ## atlas_map_protocol.gd's encode_atlas_layer_request doc for the wire shape. ## Omitted callers (every whole-body-layer call site predating T-1138) are ## byte-unchanged. window_granularity/window_min_wl_m (T-1150): same ## byte-compatibility contract, see atlas_map_protocol.gd. static func encode_atlas_layer_request( body_id: String, up_to: String = "Topography", window_center: Variant = null, window_n: int = 0, window_granularity: int = 0, window_min_wl_m: int = 0 ) -> PackedByteArray: return _amp().encode_atlas_layer_request( _mp(), body_id, up_to, window_center, window_n, window_granularity, window_min_wl_m ) ## Decode an AtlasLayerResponse (#969, D-225). Returns a Dictionary ## {body_id, status, error, layer1, district_grid, road_graph, settlements, ## region_grid, quarter_footprints, district_window}, or null if the bytes are ## not an atlas response (no "status" key — e.g. an ObserverSnapshot). static func decode_atlas_layer_response(bytes: PackedByteArray) -> Variant: return atlas_response_from_raw(decode_raw(bytes)) ## Build an AtlasLayerResponse from an already-decoded raw value. See ## atlas_map_protocol.gd for the full field-by-field wire-shape rationale. static func atlas_response_from_raw(raw: Variant) -> Variant: return _amp().atlas_response_from_raw(raw) ## Encode a StarMapRequest (T-949, D-010) for the Reach-level star-map proxy. static func encode_star_map_request() -> PackedByteArray: return _amp().encode_star_map_request(_mp()) ## Build a StarMapResponse from an already-decoded raw value. See ## atlas_map_protocol.gd for the data-unwrapping rationale. static func star_map_response_from_raw(raw: Variant) -> Variant: return _amp().star_map_response_from_raw(raw) ## Decode a StarMapResponse from MessagePack bytes. See star_map_response_from_raw. static func decode_star_map_response(bytes: PackedByteArray) -> Variant: return star_map_response_from_raw(decode_raw(bytes)) ## Encode a CityNamesRequest (T-949, D-223/D-236) for one body's atlas ## city-name pool. static func encode_city_names_request(body_id: String) -> PackedByteArray: return _amp().encode_city_names_request(_mp(), body_id) ## Build a CityNamesResponse from an already-decoded raw value. See ## atlas_map_protocol.gd for the SolExcluded/cities-shape rationale. static func city_names_response_from_raw(raw: Variant) -> Variant: return _amp().city_names_response_from_raw(raw) ## Decode a CityNamesResponse from MessagePack bytes. See city_names_response_from_raw. static func decode_city_names_response(bytes: PackedByteArray) -> Variant: return city_names_response_from_raw(decode_raw(bytes)) ## Encode a BrowseRequest (T-1131/T-1133, D-254 §4) for the six-entity data ## browser proxy. Delegates to browse_protocol.gd — kept out of this file to ## stay under gdlint's max-file-lines; see that file for the full wire-shape ## rationale (discriminator field, kind/query split, filter_system_id). static func encode_browse_request( kind: String, query_kind: String, filter_value: String = "" ) -> PackedByteArray: return _bp().encode_browse_request(_mp(), kind, query_kind, filter_value) ## Build a BrowseResponse from an already-decoded raw value. See ## browse_protocol.gd for the full shape/disambiguation rationale. static func browse_response_from_raw(raw: Variant) -> Variant: return _bp().browse_response_from_raw(raw) ## Decode a BrowseResponse from MessagePack bytes. See browse_response_from_raw. static func decode_browse_response(bytes: PackedByteArray) -> Variant: return browse_response_from_raw(decode_raw(bytes)) ## Decode + classify one inbound frame (#960, D-225; T-949 adds starmap/ ## citynames; T-1131/T-1133 adds browse). Returns {kind, value}, kind one of ## "snapshot"|"atlas"|"starmap"|"citynames"|"browse"|"unknown" — all msgpack ## maps, told apart by field, most-specific-first: StarMapResponse is the ## only kind with "data" and no "body_id"; CityNamesResponse the only one ## with "cities"; BrowseResponse the only one with its OWN "kind" field ## alongside "status"; anything else carrying "status" is AtlasLayerResponse. ## Lets receive_bytes decode the frame ONCE instead of double-decoding the ## 20 Hz snapshot path. static func decode_inbound(bytes: PackedByteArray) -> Dictionary: var raw = decode_raw(bytes) if not raw is Dictionary: return {"kind": "unknown", "value": null} if raw.has("entities"): return {"kind": "snapshot", "value": _decode_snapshot_from_raw(raw)} if raw.has("data") and not raw.has("body_id"): return {"kind": "starmap", "value": star_map_response_from_raw(raw)} if raw.has("cities"): return {"kind": "citynames", "value": city_names_response_from_raw(raw)} if raw.has("kind") and raw.has("status"): return {"kind": "browse", "value": browse_response_from_raw(raw)} if raw.has("status"): return {"kind": "atlas", "value": atlas_response_from_raw(raw)} return {"kind": "snapshot", "value": _decode_snapshot_from_raw(raw)} ## 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 = _mp().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: var result = _mp().decode(bytes) if result.status != null: push_error("Protocol: msgpack decode failed: %s" % result.status) return null var raw = result.value if not raw is Dictionary or not raw.has("tick") or not raw.has("action"): push_error("Protocol: player_input missing required fields") return null var tick: int = raw["tick"] return { "tick": tick, "action": _decode_enum_variant(raw["action"]), }