Files
settled-reach/client/ui/hardware_detector.gd
T
jpmschweitzerandClaude Sonnet 4.6 01b0583265 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>
2026-03-13 10:34:48 +01:00

110 lines
4.4 KiB
GDScript

extends Node
## Hardware detection autoload for AI-Enhanced Dialogue (D-138, #646).
##
## Exposes classification methods for Layer 1 (RAM), Layer 2 (TPT benchmark),
## and Layer 3 (ongoing TPT degradation monitoring).
##
## Loaded as HardwareDetector autoload. Tests access via get_node("/root/HardwareDetector").
## Settings dialog reads classification strings to configure the toggle UI.
const RAM_PASS_THRESHOLD_MB := 2000.0
const RAM_MARGINAL_THRESHOLD_MB := 1600.0
const TPT_GREEN_THRESHOLD := 6.0 # tokens/sec — enable silently
const TPT_YELLOW_THRESHOLD := 3.0 # tokens/sec — partial pre-voicing note
const TPT_DEGRADATION_THRESHOLD := 0.4 # fraction — >40% sustained drop → yellow
const BENCHMARK_CACHE_PATH := "user://ai-dialogue-config.json"
func _ready() -> void:
load_ai_pref()
## Layer 1 — RAM classification.
## Accepts free MB as input; returns "pass" | "marginal" | "fail".
func classify_ram(free_mb: float) -> String:
if free_mb >= RAM_PASS_THRESHOLD_MB:
return "pass"
elif free_mb >= RAM_MARGINAL_THRESHOLD_MB:
return "marginal"
else:
return "fail"
## Layer 2 — TPT benchmark classification.
## Accepts tokens/sec as input; returns "green" | "yellow" | "red".
func classify_tpt(tps: float) -> String:
if tps >= TPT_GREEN_THRESHOLD:
return "green"
elif tps >= TPT_YELLOW_THRESHOLD:
return "yellow"
else:
return "red"
## Layer 3 — Ongoing degradation classification.
## D-138 §8: >40% sustained drop from baseline → yellow (not forced disable).
## Returns "ok" | "yellow".
func classify_degradation(baseline_tps: float, current_avg_tps: float) -> String:
if baseline_tps <= 0.0:
return "ok"
var drop := (baseline_tps - current_avg_tps) / baseline_tps
return "yellow" if drop > TPT_DEGRADATION_THRESHOLD else "ok"
## Query the OS for free RAM and return the classification plus the raw MB value.
## Returns: {classification: String, free_mb: float}
func check_ram() -> Dictionary:
var mem := OS.get_memory_info()
# "available" accounts for reclaimable pages (Linux MemAvailable / Windows ullAvailPhys).
# Fall back to "free" on platforms that don't provide "available".
var free_bytes: int = mem.get("available", mem.get("free", 0))
var free_mb: float = float(free_bytes) / (1024.0 * 1024.0)
return {"classification": classify_ram(free_mb), "free_mb": free_mb}
## Read the cached TPT benchmark result written by the server on first model load.
## Returns: {tps: float, classification: String} or null if no cache file exists.
func read_benchmark_cache() -> Variant:
if not FileAccess.file_exists(BENCHMARK_CACHE_PATH):
return null
var f := FileAccess.open(BENCHMARK_CACHE_PATH, FileAccess.READ)
if f == null:
return null
var json := JSON.new()
if json.parse(f.get_as_text()) != OK:
return null
var data: Variant = json.get_data()
if not data is Dictionary or not data.has("tps"):
return null
var tps: float = float(data["tps"])
return {"tps": tps, "classification": classify_tpt(tps)}
## Load AI dialogue preference from settings.cfg and apply to GameState.
## Called from _ready() to restore the toggle state across session restarts.
## Spec: D-138 §8 — toggle persists via ConfigFile (client-local) + server SQLite (#627).
func load_ai_pref() -> void:
var cfg := ConfigFile.new()
if cfg.load("user://settings.cfg") != OK:
return
var enabled: Variant = cfg.get_value("ai_dialogue", "enabled", null)
if enabled is bool:
GameState.ai_enhanced_dialogue_enabled = enabled
## Compose a human-readable status message for the settings dialog.
## classification: "pass" | "marginal" | "fail" (RAM) or "green" | "yellow" | "red" (TPT).
## Returns empty string when no message is needed.
func status_message(ram_classification: String, tpt_classification: String, free_mb: float, tps: float) -> String:
if ram_classification == "fail":
return "AI-Enhanced Dialogue requires 2 GB of free memory. Your system currently has %.0f MB available. Close other applications and try again, or leave the setting off — the game is complete either way." % free_mb
if ram_classification == "marginal":
return "Only %.0f MB free — performance may vary. You can still enable it." % free_mb
match tpt_classification:
"yellow":
return "Running at %.0f t/s — pre-voicing will work for main characters and key scenes. Background NPCs may show base text until the queue catches up." % tps
"red":
return "Running very slowly at %.0f t/s — we recommend leaving this off, but the choice is yours." % tps
return ""