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>
110 lines
4.4 KiB
GDScript
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 ""
|