Fix 354 gdlint warnings across 65 files: 194 class-definitions-order (reorder declarations), 138 max-line-length (split long lines), 22 code issues (unused args, no-else-return, naming). Update .gdlintrc to exclude addons/ and raise max-public-methods for test files. No logic changes — declaration order, whitespace, and naming only. Ticket: #783 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
349 lines
12 KiB
GDScript
349 lines
12 KiB
GDScript
extends Node
|
||
## Platform abstraction layer — D-138 §8, D-141. Central location for all OS queries.
|
||
## Owned by Stig (UI Developer). Registered FIRST in project.godot so other
|
||
## autoloads can read its properties in their own _ready().
|
||
##
|
||
## Sections:
|
||
## Power: PowerProfile enum, 30s poll timer, power_profile_changed signal.
|
||
## raw_power_state / battery_percent updated each poll.
|
||
## Memory: free_memory_mb / total_memory_mb, refresh_memory() on demand.
|
||
## CPU: cpu_name / cpu_logical_cores — read once in _ready().
|
||
## GPU: gpu_name / gpu_vendor / gpu_api_version / gpu_type / gpu_driver_info.
|
||
## Guarded for headless/server builds where RenderingServer has no device.
|
||
## Platform: platform_name / os_version / distribution / is_sandboxed.
|
||
## Display: screen_count / screen_size / screen_dpi / screen_refresh_rate / display_scale.
|
||
## Locale: locale / locale_language.
|
||
## Paths: Read-once OS path constants, populated in _ready().
|
||
## Diag: get_diagnostics() — flat dict of all properties for bug reports.
|
||
|
||
|
||
# -- Power profile ------------------------------------------------------------
|
||
|
||
## Emitted when the detected power profile changes.
|
||
signal power_profile_changed(old_profile: int, new_profile: int)
|
||
|
||
## High-level power classification. POWER_SAVER reserved for future OS API.
|
||
enum PowerProfile {
|
||
FULL = 0, # Plugged in (charged, charging, or no battery) — no restrictions
|
||
BATTERY = 1, # On battery — AI inference should be suspended per D-138 §8 Layer 3
|
||
POWER_SAVER = 2 # System-level power-saver mode (future: no cross-platform API yet)
|
||
}
|
||
|
||
## How often (seconds) to re-poll OS for power state changes.
|
||
const POWER_POLL_INTERVAL := 30.0
|
||
|
||
# OS power_state integer constants (Godot 4 — same values as POWERSTATE_* enum).
|
||
const _POWER_STATE_UNKNOWN := 0
|
||
const _POWER_STATE_ON_BATTERY := 1
|
||
const _POWER_STATE_NO_BATTERY := 2
|
||
const _POWER_STATE_CHARGING := 3
|
||
const _POWER_STATE_CHARGED := 4
|
||
|
||
## Current power profile. Updated by the 30-second poll timer.
|
||
var power_profile: PowerProfile = PowerProfile.FULL
|
||
|
||
## Raw OS power_state integer from the last poll. 0 = unknown, 1 = on battery, etc.
|
||
var raw_power_state: int = 0
|
||
|
||
## Battery charge percentage (0–100). -1 if not available or not on battery.
|
||
var battery_percent: int = -1
|
||
|
||
|
||
# -- Memory -------------------------------------------------------------------
|
||
|
||
## Free physical RAM in megabytes. Call refresh_memory() before reading if staleness matters.
|
||
var free_memory_mb: float = 0.0
|
||
|
||
## Total physical RAM in megabytes. Populated once in _ready(); doesn't change at runtime.
|
||
var total_memory_mb: float = 0.0
|
||
|
||
|
||
# -- CPU ----------------------------------------------------------------------
|
||
|
||
## Human-readable processor name (e.g. "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz").
|
||
var cpu_name: String = ""
|
||
|
||
## Number of logical CPU cores reported by the OS.
|
||
var cpu_logical_cores: int = 0
|
||
|
||
|
||
# -- GPU ----------------------------------------------------------------------
|
||
|
||
## GPU display name (e.g. "NVIDIA GeForce RTX 3080").
|
||
var gpu_name: String = ""
|
||
|
||
## GPU vendor string (e.g. "NVIDIA Corporation").
|
||
var gpu_vendor: String = ""
|
||
|
||
## Graphics API version string (e.g. "OpenGL 4.6.0 ...").
|
||
var gpu_api_version: String = ""
|
||
|
||
## GPU type classification: "discrete" | "integrated" | "virtual" | "cpu" | "other".
|
||
var gpu_type: String = "other"
|
||
|
||
## Low-level driver info strings from the OS (may be empty on some platforms).
|
||
var gpu_driver_info: PackedStringArray = PackedStringArray()
|
||
|
||
|
||
# -- Platform identity --------------------------------------------------------
|
||
|
||
## OS name reported by Godot (e.g. "Linux", "Windows", "macOS").
|
||
var platform_name: String = ""
|
||
|
||
## Full OS version string (e.g. "Ubuntu 22.04.3 LTS").
|
||
var os_version: String = ""
|
||
|
||
## Linux distribution name (e.g. "Ubuntu"); empty on non-Linux platforms.
|
||
var distribution: String = ""
|
||
|
||
## True if the application is running inside a sandbox (Flatpak, Snap, macOS sandbox, etc.).
|
||
var is_sandboxed: bool = false
|
||
|
||
|
||
# -- Display ------------------------------------------------------------------
|
||
|
||
## Number of connected screens.
|
||
var screen_count: int = 1
|
||
|
||
## Size of the primary screen in pixels.
|
||
var screen_size: Vector2i = Vector2i(1920, 1080)
|
||
|
||
## DPI of the primary screen.
|
||
var screen_dpi: int = 96
|
||
|
||
## Refresh rate of the primary screen in Hz. Falls back to 60.0 if OS reports < 0.
|
||
var screen_refresh_rate: float = 60.0
|
||
|
||
## UI scale factor. Uses OS-reported scale on macOS; falls back to dpi / 96.0 elsewhere.
|
||
var display_scale: float = 1.0
|
||
|
||
|
||
# -- Locale -------------------------------------------------------------------
|
||
|
||
## Full locale string (e.g. "en_US").
|
||
var locale: String = ""
|
||
|
||
## Language portion of the locale (e.g. "en").
|
||
var locale_language: String = ""
|
||
|
||
|
||
# -- File paths ---------------------------------------------------------------
|
||
|
||
## Base user data directory (user://).
|
||
var user_data_dir: String = ""
|
||
|
||
## Directory where settings.cfg is written. Same as user_data_dir in Godot 4.
|
||
var config_dir: String = ""
|
||
|
||
## Full path to AI benchmark cache (ai-dialogue-config.json).
|
||
var benchmark_cache_path: String = ""
|
||
|
||
## Directory where the game binary lives (install location).
|
||
var install_dir: String = ""
|
||
|
||
## Full path to the game executable.
|
||
var executable_path: String = ""
|
||
|
||
## OS-provided cache directory, or user_data_dir/cache as fallback.
|
||
var cache_dir: String = ""
|
||
|
||
## Directory where the bundled Gemma 2 model lives (derived from install_dir).
|
||
var model_dir: String = ""
|
||
|
||
|
||
# -- Lifecycle ----------------------------------------------------------------
|
||
|
||
func _ready() -> void:
|
||
_init_paths()
|
||
_init_hardware()
|
||
refresh_memory()
|
||
_poll_power()
|
||
|
||
var timer := Timer.new()
|
||
timer.wait_time = POWER_POLL_INTERVAL
|
||
timer.autostart = true
|
||
timer.timeout.connect(_poll_power)
|
||
add_child(timer)
|
||
|
||
|
||
func _init_paths() -> void:
|
||
user_data_dir = OS.get_user_data_dir()
|
||
config_dir = user_data_dir
|
||
benchmark_cache_path = "user://ai-dialogue-config.json"
|
||
executable_path = OS.get_executable_path()
|
||
install_dir = executable_path.get_base_dir()
|
||
model_dir = install_dir + "/models"
|
||
# get_cache_dir() may not exist on all Godot builds — use callv for safety.
|
||
var os_cache: String = ""
|
||
if OS.has_method("get_cache_dir"):
|
||
var result: Variant = OS.callv("get_cache_dir", [])
|
||
if result is String:
|
||
os_cache = result
|
||
cache_dir = os_cache if not os_cache.is_empty() else user_data_dir + "/cache"
|
||
|
||
|
||
func _init_hardware() -> void:
|
||
# -- CPU
|
||
cpu_name = OS.get_processor_name()
|
||
cpu_logical_cores = OS.get_processor_count()
|
||
|
||
# -- GPU (guarded: RenderingServer methods may return empty in headless mode)
|
||
gpu_name = RenderingServer.get_video_adapter_name()
|
||
gpu_vendor = RenderingServer.get_video_adapter_vendor()
|
||
gpu_api_version = RenderingServer.get_video_adapter_api_version()
|
||
gpu_driver_info = OS.get_video_adapter_driver_info()
|
||
if RenderingServer.has_method("get_video_adapter_type"):
|
||
var adapter_type: int = RenderingServer.callv("get_video_adapter_type", [])
|
||
gpu_type = _map_adapter_type(adapter_type)
|
||
|
||
# -- Platform identity
|
||
platform_name = OS.get_name()
|
||
os_version = OS.get_version()
|
||
if OS.has_method("get_distribution_name"):
|
||
var dist: Variant = OS.callv("get_distribution_name", [])
|
||
if dist is String:
|
||
distribution = dist
|
||
is_sandboxed = OS.is_sandboxed()
|
||
|
||
# -- Display
|
||
screen_count = DisplayServer.get_screen_count()
|
||
screen_size = DisplayServer.screen_get_size()
|
||
screen_dpi = DisplayServer.screen_get_dpi()
|
||
var raw_rate: float = DisplayServer.screen_get_refresh_rate()
|
||
screen_refresh_rate = raw_rate if raw_rate >= 0.0 else 60.0
|
||
display_scale = _compute_display_scale()
|
||
|
||
# -- Locale
|
||
locale = OS.get_locale()
|
||
locale_language = OS.get_locale_language()
|
||
|
||
|
||
## Map RenderingDevice.DeviceType int to a readable string.
|
||
## Values: 0=OTHER, 1=INTEGRATED, 2=DISCRETE, 3=VIRTUAL, 4=CPU.
|
||
static func _map_adapter_type(adapter_type: int) -> String:
|
||
match adapter_type:
|
||
1: return "integrated"
|
||
2: return "discrete"
|
||
3: return "virtual"
|
||
4: return "cpu"
|
||
_: return "other"
|
||
|
||
|
||
## Compute display scale for the primary screen.
|
||
## macOS reports via screen_get_scale(); all others fall back to dpi / 96.0.
|
||
func _compute_display_scale() -> float:
|
||
if platform_name == "macOS" and DisplayServer.has_method("screen_get_scale"):
|
||
var scale: Variant = DisplayServer.callv("screen_get_scale", [])
|
||
if scale is float and scale > 0.0:
|
||
return scale
|
||
return float(screen_dpi) / 96.0
|
||
|
||
|
||
# -- Memory API ---------------------------------------------------------------
|
||
|
||
## Refresh free/total memory readings from the OS. Call before read if freshness matters.
|
||
func refresh_memory() -> void:
|
||
var mem := OS.get_memory_info()
|
||
# "available" = Linux MemAvailable / Windows ullAvailPhys (includes reclaimable pages).
|
||
# Fall back to "free" on platforms that don't provide "available".
|
||
free_memory_mb = float(mem.get("available", mem.get("free", 0))) / (1024.0 * 1024.0)
|
||
total_memory_mb = float(mem.get("physical", 0)) / (1024.0 * 1024.0)
|
||
|
||
|
||
# -- Power API ----------------------------------------------------------------
|
||
|
||
## Classify an OS power_state integer into a human-readable string.
|
||
## 1 (ON_BATTERY) → "battery", 2/3/4 (plugged) → "plugged", 0 → "unknown".
|
||
static func classify_power_state(power_state: int) -> String:
|
||
match power_state:
|
||
_POWER_STATE_ON_BATTERY:
|
||
return "battery"
|
||
_POWER_STATE_NO_BATTERY, _POWER_STATE_CHARGING, _POWER_STATE_CHARGED:
|
||
return "plugged"
|
||
_:
|
||
return "unknown"
|
||
|
||
|
||
## True only when the OS reports the device is on battery — used by Layer 3 suspend.
|
||
static func should_suspend_inference(power_state: int) -> bool:
|
||
return power_state == _POWER_STATE_ON_BATTERY
|
||
|
||
|
||
## Read current OS power state integer. Returns 0 (UNKNOWN) if unavailable.
|
||
## Uses callv() to defer method resolution to runtime — avoids compile errors on builds
|
||
## where OS.get_power_info() is not available (e.g. headless).
|
||
func current_power_state() -> int:
|
||
# Try Godot 4.x: OS.get_power_info() → {"power_state": int, "percent": int, ...}
|
||
if OS.has_method("get_power_info"):
|
||
var info: Variant = OS.callv("get_power_info", [])
|
||
if info is Dictionary:
|
||
return int(info.get("power_state", _POWER_STATE_UNKNOWN))
|
||
# Fallback: unknown power state → treat as FULL (no inference suspension).
|
||
return _POWER_STATE_UNKNOWN
|
||
|
||
|
||
func _poll_power() -> void:
|
||
# Read raw state + battery percent from OS.
|
||
raw_power_state = _POWER_STATE_UNKNOWN
|
||
battery_percent = -1
|
||
if OS.has_method("get_power_info"):
|
||
var info: Variant = OS.callv("get_power_info", [])
|
||
if info is Dictionary:
|
||
raw_power_state = int(info.get("power_state", _POWER_STATE_UNKNOWN))
|
||
if raw_power_state == _POWER_STATE_ON_BATTERY:
|
||
battery_percent = int(info.get("percent", -1))
|
||
|
||
var new_profile: PowerProfile = PowerProfile.BATTERY \
|
||
if raw_power_state == _POWER_STATE_ON_BATTERY \
|
||
else PowerProfile.FULL
|
||
if new_profile != power_profile:
|
||
var old_profile := power_profile
|
||
power_profile = new_profile
|
||
power_profile_changed.emit(int(old_profile), int(new_profile))
|
||
|
||
|
||
# -- Diagnostics --------------------------------------------------------------
|
||
|
||
## Returns all platform properties as a flat dictionary for inclusion in bug reports.
|
||
func get_diagnostics() -> Dictionary:
|
||
return {
|
||
# Power
|
||
"power_profile": int(power_profile),
|
||
"raw_power_state": raw_power_state,
|
||
"battery_percent": battery_percent,
|
||
# Memory
|
||
"free_memory_mb": free_memory_mb,
|
||
"total_memory_mb": total_memory_mb,
|
||
# CPU
|
||
"cpu_name": cpu_name,
|
||
"cpu_logical_cores": cpu_logical_cores,
|
||
# GPU
|
||
"gpu_name": gpu_name,
|
||
"gpu_vendor": gpu_vendor,
|
||
"gpu_api_version": gpu_api_version,
|
||
"gpu_type": gpu_type,
|
||
"gpu_driver_info": Array(gpu_driver_info),
|
||
# Platform
|
||
"platform_name": platform_name,
|
||
"os_version": os_version,
|
||
"distribution": distribution,
|
||
"is_sandboxed": is_sandboxed,
|
||
# Display
|
||
"screen_count": screen_count,
|
||
"screen_size": {"x": screen_size.x, "y": screen_size.y},
|
||
"screen_dpi": screen_dpi,
|
||
"screen_refresh_rate": screen_refresh_rate,
|
||
"display_scale": display_scale,
|
||
# Locale
|
||
"locale": locale,
|
||
"locale_language": locale_language,
|
||
# Paths
|
||
"user_data_dir": user_data_dir,
|
||
"config_dir": config_dir,
|
||
"benchmark_cache_path": benchmark_cache_path,
|
||
"install_dir": install_dir,
|
||
"executable_path": executable_path,
|
||
"cache_dir": cache_dir,
|
||
"model_dir": model_dir,
|
||
}
|