class_name BrowseProtocol ## BrowseRequest/BrowseResponse wire codec (T-1131/T-1133, D-254 §4) — the ## six-entity data browser proxy (star systems, bodies, stations, ## corporations, commodities, trait templates). ## ## Split out of protocol.gd (not folded in) purely to stay under gdlint's ## max-file-lines — protocol.gd's own static functions (encode_browse_request/ ## browse_response_from_raw/decode_browse_response) delegate here. Every ## caller still goes through Protocol.* — this file is an implementation ## detail, not a second public API surface. ## ## `browse: true` is the mandatory discriminator field, same convention as ## star_map/city_names — decode_inbound's demux is at its documented ## practical ceiling of shape-sniffing probes, so BrowseRequest is ONE new ## shape carrying an internal kind/query split, not six. ## ## Wire shapes (confirmed against Oscar's T-1131 contract, 2026-07-17): ## kind: a BrowseEntityKind bare string ("StarSystem"|"Body"|"Station"| ## "Corporation"|"Commodity"|"TraitTemplate") — all-unit-variant enum, ## same shape as AtlasLayerRequest's up_to: CascadeLayer. ## query: BrowseQuery's two variants both carry named fields (struct ## variants), so each encodes as a single-key map: ## {"Index": {"filter_system_id": null | "GJ-1"}} ## {"Detail": {"id": "GJ1c"}} ## filter_system_id only means anything for kind == "Body" (bodies filter ## by containing SYSTEM, not by another body — renamed from ## filter_body_id on Oscar's side before shipping). ## status: BrowseStatus reuses the exact Ready|NotFound|Error(String) shape ## as AtlasLayerStatus/StarMapStatus/CityNamesStatus, decoded by the same ## bare-string-or-single-key-map rule protocol.gd's _decode_status_field ## already implements for those three — duplicated here as ## _decode_status_field (not shared via a Callable) to keep this file ## genuinely standalone; the shape is a stable, tiny, three-line rule. static func _decode_status_field(status_raw: Variant) -> Dictionary: if status_raw is String: return {"status": status_raw, "error": ""} if status_raw is Dictionary and status_raw.has("Error"): return {"status": "Error", "error": str(status_raw["Error"])} return {"status": "", "error": ""} ## Encode a BrowseRequest. `mp` is the loaded messagepack.gd module (passed ## in rather than reloaded here — protocol.gd's _mp() already owns that ## load()). kind is a BrowseEntityKind bare string. query_kind is "Index" or ## "Detail"; filter_value is the containing system_id (Index, Body only) or ## the entity's own id (Detail). static func encode_browse_request( mp, kind: String, query_kind: String, filter_value: String = "" ) -> PackedByteArray: var query: Dictionary if query_kind == "Detail": query = {"Detail": {"id": filter_value}} else: query = { "Index": {"filter_system_id": filter_value if not filter_value.is_empty() else null} } var msg := {"browse": true, "kind": kind, "query": query} var result = mp.encode(msg) if result.status != null: push_error("BrowseProtocol: encode_browse_request failed: %s" % result.status) return PackedByteArray() return result.value ## Build a BrowseResponse from an already-decoded raw value. Returns null ## unless it carries "kind" AND "status" AND ("index" or "detail") — "kind" + ## "status" alone doesn't disambiguate from AtlasLayerResponse (also has ## "status", never "kind"). "index"/"detail" pass through as raw decoded ## values — BrowserAdapter (client/ui/implant/apps/browser/browser_adapter.gd) ## owns interpreting their per-kind shape, matching how atlas_response_from_raw's ## layer1/district_grid/etc. fields also just pass through unshaped. static func browse_response_from_raw(raw: Variant) -> Variant: if not raw is Dictionary or not raw.has("kind") or not raw.has("status"): return null if not raw.has("index") and not raw.has("detail"): return null var decoded_status := _decode_status_field(raw.get("status")) return { "kind": str(raw.get("kind", "")), "status": decoded_status["status"], "error": decoded_status["error"], "index": raw.get("index"), "detail": raw.get("detail"), }