Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
233 lines
7.9 KiB
GDScript
233 lines
7.9 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),
|
|
## Layer 3 (TPT degradation + battery power state monitoring).
|
|
##
|
|
## Consumes PlatformInfo for all OS queries (memory, power state, paths).
|
|
## Loaded as HardwareDetector autoload. Tests access via get_node("/root/HardwareDetector").
|
|
## Settings dialog reads classification strings and power-state methods to configure the 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
|
|
|
|
## Benchmark cache path — reads from PlatformInfo for cross-platform correctness.
|
|
var benchmark_cache_path: String:
|
|
get:
|
|
return PlatformInfo.benchmark_cache_path
|
|
|
|
# -- Battery suspension state -------------------------------------------------
|
|
|
|
## True when AI inference has been auto-suspended due to battery power.
|
|
## Separate from GameState.ai_enhanced_dialogue_enabled (player preference preserved).
|
|
var inference_suspended: bool = false
|
|
|
|
# User's preference at the moment battery suspension activated (to restore on plug-in).
|
|
var _pre_battery_pref: bool = true
|
|
|
|
# -- Lifecycle ----------------------------------------------------------------
|
|
|
|
|
|
func _ready() -> void:
|
|
assert(PlatformInfo != null, "PlatformInfo must load before HardwareDetector")
|
|
load_ai_pref()
|
|
_pre_battery_pref = GameState.ai_enhanced_dialogue_enabled
|
|
PlatformInfo.power_profile_changed.connect(_on_power_profile_changed)
|
|
|
|
|
|
# -- Layer 1: RAM classification ----------------------------------------------
|
|
|
|
|
|
## 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"
|
|
if free_mb >= RAM_MARGINAL_THRESHOLD_MB:
|
|
return "marginal"
|
|
return "fail"
|
|
|
|
|
|
## Query RAM via PlatformInfo and return classification + raw MB value.
|
|
## Returns: {classification: String, free_mb: float}
|
|
func check_ram() -> Dictionary:
|
|
PlatformInfo.refresh_memory()
|
|
return {
|
|
"classification": classify_ram(PlatformInfo.free_memory_mb),
|
|
"free_mb": PlatformInfo.free_memory_mb
|
|
}
|
|
|
|
|
|
# -- Layer 2: TPT benchmark classification ------------------------------------
|
|
|
|
|
|
## 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"
|
|
if tps >= TPT_YELLOW_THRESHOLD:
|
|
return "yellow"
|
|
return "red"
|
|
|
|
|
|
## 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:
|
|
var cache_path: String = PlatformInfo.benchmark_cache_path
|
|
if not FileAccess.file_exists(cache_path):
|
|
return null
|
|
var text: String = FileAccess.get_file_as_string(cache_path)
|
|
if text.is_empty():
|
|
return null
|
|
var json := JSON.new()
|
|
if json.parse(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)}
|
|
|
|
|
|
# -- Layer 3: degradation + battery -------------------------------------------
|
|
|
|
|
|
## Layer 3 — Ongoing TPT 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"
|
|
|
|
|
|
## Layer 3 — Battery power state classification.
|
|
## Delegates to PlatformInfo for the actual integer → string mapping.
|
|
## power_state: OS power_state integer (0=unknown, 1=on_battery, 2=no_battery, 3=charging, 4=charged).
|
|
## Returns "battery" | "plugged" | "unknown".
|
|
func classify_power_state(power_state: int) -> String:
|
|
return PlatformInfo.classify_power_state(power_state)
|
|
|
|
|
|
## True only when the OS reports on-battery power — inference suspend trigger.
|
|
func should_suspend_inference(power_state: int) -> bool:
|
|
return PlatformInfo.should_suspend_inference(power_state)
|
|
|
|
|
|
## Query current OS power state and return {power_state: int, classification: String, should_suspend: bool}.
|
|
func check_power_state() -> Dictionary:
|
|
var state := PlatformInfo.current_power_state()
|
|
return {
|
|
"power_state": state,
|
|
"classification": classify_power_state(state),
|
|
"should_suspend": should_suspend_inference(state),
|
|
}
|
|
|
|
|
|
## True if AI inference is currently suspended due to battery power.
|
|
func is_inference_suspended() -> bool:
|
|
return inference_suspended
|
|
|
|
|
|
# -- Startup pref load --------------------------------------------------------
|
|
|
|
|
|
## 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
|
|
|
|
|
|
# -- Battery suspend/resume ---------------------------------------------------
|
|
|
|
|
|
func _on_power_profile_changed(old_profile: int, new_profile: int) -> void:
|
|
if new_profile == PlatformInfo.PowerProfile.BATTERY:
|
|
_suspend_for_battery()
|
|
elif old_profile == PlatformInfo.PowerProfile.BATTERY:
|
|
_resume_from_battery()
|
|
|
|
|
|
## Suspend AI inference when transitioning to battery power.
|
|
## Preserves the player's preference so it can be restored on plug-in.
|
|
func _suspend_for_battery() -> void:
|
|
if inference_suspended:
|
|
return
|
|
_pre_battery_pref = GameState.ai_enhanced_dialogue_enabled
|
|
inference_suspended = true
|
|
if _pre_battery_pref:
|
|
# Only send to server if inference was actually running — no-op if already disabled.
|
|
_send_settings_change(false)
|
|
|
|
|
|
## Resume AI inference when plugging back in.
|
|
## Restores inference only if the player had it enabled before battery kicked in.
|
|
func _resume_from_battery() -> void:
|
|
if not inference_suspended:
|
|
return
|
|
inference_suspended = false
|
|
if _pre_battery_pref:
|
|
_send_settings_change(true)
|
|
|
|
|
|
func _send_settings_change(enabled: bool) -> void:
|
|
if SimBridge.state != SimBridge.ConnectionState.CONNECTED:
|
|
return
|
|
(
|
|
SimBridge
|
|
. send_input(
|
|
{
|
|
"action": InputMapper.Action.CHANGE_SETTINGS,
|
|
"action_data": {"ai_enhanced_dialogue": enabled},
|
|
"timestamp_msec": Time.get_ticks_msec(),
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
# -- Status message -----------------------------------------------------------
|
|
|
|
|
|
## 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 ""
|