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
+29
View File
@@ -97,6 +97,17 @@ var pending_load_path: String = ""
# Default: "detective" — fallback for legacy saves without character.txt.
var character_archetype: String = "detective"
# #646: AI-Enhanced Dialogue enabled state (D-138).
# Runtime toggle — true means the LLM re-voicing pipeline should run (server-side).
# Default: true (opt-out model per D-138 §8). Hardware detector may disable at startup
# if RAM is insufficient. Persisted to server SQLite via ChangeSettings IPC.
var ai_enhanced_dialogue_enabled: bool = true
# v20 fields (#627, D-138): settings response from server.
# One-shot: {kind: "full", settings: [{key, value}]} or {kind: "ack", success, key, error} or null.
# "full" response hydrates ai_enhanced_dialogue_enabled (server is authoritative for persisted state).
var settings_response: Variant = null
# v7 fields (#431, D-059/D-060)
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
@@ -328,6 +339,24 @@ func apply_snapshot(snapshot: Dictionary) -> void:
else:
debug_response = null
# v20: settings_response (#627, D-138) — one-shot settings ack/dump from server.
# "full" kind → iterate settings array and hydrate matching fields.
if snapshot.has("settings_response") and snapshot.settings_response is Dictionary:
settings_response = snapshot.settings_response
var sr: Dictionary = snapshot.settings_response
if sr.get("kind") == "full":
var sr_settings: Variant = sr.get("settings")
if sr_settings is Array:
for entry in sr_settings:
if not entry is Dictionary:
continue
if entry.get("key") == "ai_dialogue.enabled":
var val: Variant = entry.get("value")
if val is Dictionary and val.has("Bool"):
ai_enhanced_dialogue_enabled = bool(val["Bool"])
else:
settings_response = null
# v14: player_knowledge (#264, D-041) — partial KG dump for journal panel.
# Only update when field is present (null means no change, server sends when KG changes).
if snapshot.has("player_knowledge") and snapshot.player_knowledge is Dictionary:
+3
View File
@@ -25,6 +25,9 @@ enum Action {
SAVE_GAME, # #554: F5 quicksave — sends SaveGame to server with save path
LOAD_GAME, # #554: F6 quickload — sends LoadGame to server with save path
DEBUG_COMMAND, # #581: debug console command dispatch — sends DebugCommandKind to server
CHANGE_SETTINGS, # #646: persist a setting to server SQLite — sends {key, value} to server
REQUEST_ALL_SETTINGS, # #646: request full settings dump from server after handshake (unit variant)
DELETE_SETTING, # #646: delete a setting by key from server SQLite (struct variant)
}
var input_queue: Array[Dictionary] = []
+15
View File
@@ -253,6 +253,12 @@ func _process(delta: float) -> void:
handshake_complete.emit(server_version)
_set_state(ConnectionState.CONNECTED)
# #646: Request full settings dump on connect — hydrates GameState.ai_enhanced_dialogue_enabled
# from server SQLite so the client reflects the authoritative persisted state (D-138).
_outbound_buffer.append({
"tick": 0,
"action_name": "RequestAllSettings",
})
return
if _bridge == null:
@@ -375,6 +381,9 @@ func receive_bytes(bytes: PackedByteArray) -> void:
# #554: Carry forward save/load result (one-shot, consumed by main.gd)
if snapshot.get("save_result") == null and _last_snapshot.get("save_result") != null:
snapshot["save_result"] = _last_snapshot["save_result"]
# #646: Carry forward settings_response (one-shot, consumed by game_state apply_snapshot)
if snapshot.get("settings_response") == null and _last_snapshot.get("settings_response") != null:
snapshot["settings_response"] = _last_snapshot["settings_response"]
_last_snapshot = snapshot
# Drain the outbound buffer. Returns raw input entries for batch encoding.
@@ -418,6 +427,12 @@ static func action_enum_to_wire(action: int) -> String:
return "LoadGame" # #554: F6 quickload (D-085)
InputMapper.Action.DEBUG_COMMAND:
return "DebugCommand" # #581: debug console command dispatch
InputMapper.Action.CHANGE_SETTINGS:
return "ChangeSettings" # #646: persist setting to server SQLite (D-138)
InputMapper.Action.REQUEST_ALL_SETTINGS:
return "RequestAllSettings" # #646: unit variant — server sends full settings dump
InputMapper.Action.DELETE_SETTING:
return "DeleteSetting" # #646: struct variant — delete setting by key
_:
push_warning("SimBridge: unknown action enum %s" % action)
return ""