From 18a18820fe331d32dab66a738f809be92872d5eb Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 10 Jul 2026 09:54:03 -0500 Subject: [PATCH] =?UTF-8?q?feat(client):=20net=20primitives=20=E2=80=94=20?= =?UTF-8?q?DmResponse,=20NarrateResult,=20DmTransport,=20ProxyConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- client/scripts/net/dm_response.gd | 25 +++++++++ client/scripts/net/dm_response.gd.uid | 1 + client/scripts/net/dm_transport.gd | 10 ++++ client/scripts/net/dm_transport.gd.uid | 1 + client/scripts/net/narrate_result.gd | 18 +++++++ client/scripts/net/narrate_result.gd.uid | 1 + client/scripts/net/proxy_config.gd | 14 +++++ client/scripts/net/proxy_config.gd.uid | 1 + client/tests/unit/test_net_primitives.gd | 54 ++++++++++++++++++++ client/tests/unit/test_net_primitives.gd.uid | 1 + 10 files changed, 126 insertions(+) create mode 100644 client/scripts/net/dm_response.gd create mode 100644 client/scripts/net/dm_response.gd.uid create mode 100644 client/scripts/net/dm_transport.gd create mode 100644 client/scripts/net/dm_transport.gd.uid create mode 100644 client/scripts/net/narrate_result.gd create mode 100644 client/scripts/net/narrate_result.gd.uid create mode 100644 client/scripts/net/proxy_config.gd create mode 100644 client/scripts/net/proxy_config.gd.uid create mode 100644 client/tests/unit/test_net_primitives.gd create mode 100644 client/tests/unit/test_net_primitives.gd.uid diff --git a/client/scripts/net/dm_response.gd b/client/scripts/net/dm_response.gd new file mode 100644 index 0000000..05aedf4 --- /dev/null +++ b/client/scripts/net/dm_response.gd @@ -0,0 +1,25 @@ +class_name DmResponse +extends RefCounted +## The raw outcome of one HTTP attempt, independent of any game meaning. +## `status` is 0 when the transport never reached the server; `body` is the +## parsed JSON dict, or null when the body was absent or not JSON. + +var status: int +var body # Variant: parsed JSON (Dictionary) or null +var transport_ok: bool +var error: String + + +func _init(p_status := 0, p_body = null, p_transport_ok := false, p_error := "") -> void: + status = p_status + body = p_body + transport_ok = p_transport_ok + error = p_error + + +static func ok(p_status: int, p_body) -> DmResponse: + return DmResponse.new(p_status, p_body, true, "") + + +static func failed(p_error: String) -> DmResponse: + return DmResponse.new(0, null, false, p_error) diff --git a/client/scripts/net/dm_response.gd.uid b/client/scripts/net/dm_response.gd.uid new file mode 100644 index 0000000..021a85a --- /dev/null +++ b/client/scripts/net/dm_response.gd.uid @@ -0,0 +1 @@ +uid://bdy2347pcqm4v diff --git a/client/scripts/net/dm_transport.gd b/client/scripts/net/dm_transport.gd new file mode 100644 index 0000000..aadc8c9 --- /dev/null +++ b/client/scripts/net/dm_transport.gd @@ -0,0 +1,10 @@ +class_name DmTransport +extends RefCounted +## The network seam. DmService talks to this, never to HTTPRequest directly, so +## the loop logic is testable with an injected fake. Concrete transports +## override post_json. The base returns a failed response (rather than assert()) +## so a missing override is a loud, catchable failure in tests. + +func post_json(_path: String, _body: Dictionary) -> DmResponse: + push_error("DmTransport.post_json must be overridden") + return DmResponse.failed("transport not implemented") diff --git a/client/scripts/net/dm_transport.gd.uid b/client/scripts/net/dm_transport.gd.uid new file mode 100644 index 0000000..22b5b8b --- /dev/null +++ b/client/scripts/net/dm_transport.gd.uid @@ -0,0 +1 @@ +uid://crgmujan6vj2l diff --git a/client/scripts/net/narrate_result.gd b/client/scripts/net/narrate_result.gd new file mode 100644 index 0000000..f66e554 --- /dev/null +++ b/client/scripts/net/narrate_result.gd @@ -0,0 +1,18 @@ +class_name NarrateResult +extends RefCounted +## The game-meaningful result of one narrate call. `facts` are the [FACT:] +## strings the caller applies to the canon log; empty when degraded. + +var display_text: String +var facts: Array +var degraded: bool + + +func _init(p_display_text := "", p_facts := [], p_degraded := false) -> void: + display_text = p_display_text + facts = p_facts + degraded = p_degraded + + +static func fallback(line: String) -> NarrateResult: + return NarrateResult.new(line, [], true) diff --git a/client/scripts/net/narrate_result.gd.uid b/client/scripts/net/narrate_result.gd.uid new file mode 100644 index 0000000..70d1fb1 --- /dev/null +++ b/client/scripts/net/narrate_result.gd.uid @@ -0,0 +1 @@ +uid://dye0nd5h05cks diff --git a/client/scripts/net/proxy_config.gd b/client/scripts/net/proxy_config.gd new file mode 100644 index 0000000..7573518 --- /dev/null +++ b/client/scripts/net/proxy_config.gd @@ -0,0 +1,14 @@ +class_name ProxyConfig +extends RefCounted +## The client's one piece of network config: the proxy base URL. Never a key, +## never a model name (charter ยง4). Overridable per environment via project +## settings; defaults to the dev proxy. + +const SETTING := "coc_rpg/proxy_base_url" +const DEFAULT := "http://localhost:8000" + + +static func base_url() -> String: + var v = ProjectSettings.get_setting(SETTING, DEFAULT) + var s := str(v) + return s if s != "" else DEFAULT diff --git a/client/scripts/net/proxy_config.gd.uid b/client/scripts/net/proxy_config.gd.uid new file mode 100644 index 0000000..3d18a0e --- /dev/null +++ b/client/scripts/net/proxy_config.gd.uid @@ -0,0 +1 @@ +uid://n7p287j05isf diff --git a/client/tests/unit/test_net_primitives.gd b/client/tests/unit/test_net_primitives.gd new file mode 100644 index 0000000..3c74103 --- /dev/null +++ b/client/tests/unit/test_net_primitives.gd @@ -0,0 +1,54 @@ +extends "res://addons/gut/test.gd" + +const DmResponse = preload("res://scripts/net/dm_response.gd") +const NarrateResult = preload("res://scripts/net/narrate_result.gd") +const DmTransport = preload("res://scripts/net/dm_transport.gd") +const ProxyConfig = preload("res://scripts/net/proxy_config.gd") + + +func test_dm_response_ok(): + var r = DmResponse.ok(200, {"prose": "hi"}) + assert_eq(r.status, 200) + assert_true(r.transport_ok) + assert_eq(r.body, {"prose": "hi"}) + assert_eq(r.error, "") + + +func test_dm_response_failed(): + var r = DmResponse.failed("boom") + assert_false(r.transport_ok) + assert_eq(r.status, 0) + assert_eq(r.body, null) + assert_eq(r.error, "boom") + + +func test_narrate_result_basic(): + var r = NarrateResult.new("text", ["a"], false) + assert_eq(r.display_text, "text") + assert_eq(r.facts, ["a"]) + assert_false(r.degraded) + + +func test_narrate_result_fallback(): + var r = NarrateResult.fallback("cold chamber") + assert_eq(r.display_text, "cold chamber") + assert_eq(r.facts, []) + assert_true(r.degraded) + + +func test_transport_base_returns_not_implemented(): + var t = DmTransport.new() + var r = await t.post_json("/x", {}) + assert_false(r.transport_ok) + assert_eq(r.error, "transport not implemented") + + +func test_proxy_config_default(): + ProjectSettings.set_setting(ProxyConfig.SETTING, null) + assert_eq(ProxyConfig.base_url(), ProxyConfig.DEFAULT) + + +func test_proxy_config_override(): + ProjectSettings.set_setting(ProxyConfig.SETTING, "http://example.test:9000") + assert_eq(ProxyConfig.base_url(), "http://example.test:9000") + ProjectSettings.set_setting(ProxyConfig.SETTING, null) diff --git a/client/tests/unit/test_net_primitives.gd.uid b/client/tests/unit/test_net_primitives.gd.uid new file mode 100644 index 0000000..b5e83dc --- /dev/null +++ b/client/tests/unit/test_net_primitives.gd.uid @@ -0,0 +1 @@ +uid://cpihryriwysay