feat(ui): AI-Enhanced Dialogue toggle + hardware detection (#646, D-138)

Implements the full AI-Enhanced Dialogue feature for Sprint 26:

- HardwareDetector autoload (extends Node): three-layer detection —
  Layer 1 RAM classification (pass/marginal/fail, thresholds 2GB/1.6GB),
  Layer 2 TPT benchmark cache (green/yellow/red, thresholds 6/3 t/s),
  Layer 3 degradation monitoring (>40% drop → yellow). load_ai_pref()
  restores toggle state from user://settings.cfg on startup.

- Settings dialog AI section: toggle, colored status dot, status message
  label, testable API (get_ai_dialogue_label_text, set_ai_dialogue_
  hardware_status, is_ai_dialogue_toggle_enabled). Only RAM "fail" greys
  out toggle — player always overrides yellow/red recommendations (D-138).

- GameState.ai_enhanced_dialogue_enabled (default true, opt-out model).
  GameState.settings_response (v20 one-shot settings dump from server).
  apply_snapshot() hydrates ai_enhanced_dialogue_enabled from full dump.

- Protocol v19→v20, settings_response decoding in decode_snapshot().
  Protocol converted to extends Node autoload (enables test has_method).
  encode_change_settings() helper for test inspection.

- InputMapper: CHANGE_SETTINGS, REQUEST_ALL_SETTINGS, DELETE_SETTING.
  SimBridge: wire mappings for all three. RequestAllSettings queued after
  handshake to hydrate client state from server SQLite on connect.
  settings_response carry-forward in receive_bytes().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 10:34:48 +01:00
co-authored by Claude Sonnet 4.6
parent 2f8218400d
commit 01b0583265
9 changed files with 354 additions and 7 deletions
+48 -3
View File
@@ -1,4 +1,4 @@
class_name Protocol
extends Node
## MessagePack codec for the Rust↔Godot wire protocol (D-020).
##
## Encodes/decodes ObserverSnapshot and PlayerInput to match
@@ -11,8 +11,8 @@ class_name Protocol
## Protocol version — must match server PROTOCOL_VERSION in bridge/types.rs.
## Reject snapshots where version != this value.
## v19: adds character_archetype field to StartupMessage (#588, #587).
const PROTOCOL_VERSION: int = 19
## v20: adds settings_response field to ObserverSnapshot (#627, D-138).
const PROTOCOL_VERSION: int = 20
# -- Decode: bytes from server → GDScript types --------------------------------
@@ -270,6 +270,32 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
"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
@@ -362,6 +388,7 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
"zone_id": zone_id,
"triangle_crisis_events": triangle_crisis_events,
"current_ticker": current_ticker,
"settings_response": settings_response,
}
@@ -548,6 +575,24 @@ static func _encode_action(action_name: String, action_data: Variant) -> Variant
return action_name
## Encode a ChangeSettings action for the AI-Enhanced Dialogue toggle (#646, D-138).
## Returns a MessagePack-encoded Vec<PlayerInput> 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 = Messagepack.encode(entries)
if result.status != null:
push_error("Protocol: encode_change_settings 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: