style(client): gdformat all 52 GDScript files — zero format warnings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 11:09:10 +02:00
co-authored by Claude Opus 4.6
parent 572e66f026
commit 88c7407cb6
52 changed files with 2323 additions and 1061 deletions
+31 -16
View File
@@ -30,18 +30,24 @@ const BUSES := [BUS_MUSIC, BUS_AMBIENT, BUS_WORLD_SFX, BUS_PLAYER_ACTIONS, BUS_U
# NOTE: listening_focus requires 30+ stationary ticks before activation (D-069/D-071).
# The tick gate is the caller's responsibility — AudioManager only manages bus volumes.
const DIP_SPECS := {
"dialogue": {
"ease_in": 0.3, "ease_out": 0.5,
"buses": { "Ambient": -7.0 },
"dialogue":
{
"ease_in": 0.3,
"ease_out": 0.5,
"buses": {"Ambient": -7.0},
},
"confrontation": {
"ease_in": 0.5, "ease_out": 1.0,
"buses": { "Ambient": -11.0, "WorldSFX": -5.0 },
"confrontation":
{
"ease_in": 0.5,
"ease_out": 1.0,
"buses": {"Ambient": -11.0, "WorldSFX": -5.0},
"filter_hz": 800.0,
},
"listening_focus": {
"ease_in": 0.5, "ease_out": 0.5,
"buses": { "WorldSFX": 2.5 },
"listening_focus":
{
"ease_in": 0.5,
"ease_out": 0.5,
"buses": {"WorldSFX": 2.5},
},
}
@@ -59,10 +65,10 @@ const CROSSFADE_DURATION := 1.8 # D-073: 1.5-2s spec, mid-range
# Note: amb_station_base (D-038 global base hum) plays globally via play_loop()
# at startup — it is not zone-dependent and has no ZONE_ASSETS entry.
const ZONE_ASSETS: Dictionary = {
"hub": "amb_hub_layer",
"hub": "amb_hub_layer",
"workplace": "amb_hub_layer",
"bar": "amb_bar_layer",
"corridor": "amb_corridor_layer",
"bar": "amb_bar_layer",
"corridor": "amb_corridor_layer",
}
const PREFS_PATH := "user://audio_prefs.cfg"
@@ -73,12 +79,12 @@ const PREFS_PATH := "user://audio_prefs.cfg"
# Audio assets per D-038: footstep variants (walk / run), NPC murmur (D-072, #532).
# Missing assets no-op gracefully (D-038 fallback pattern).
const SOUND_EVENT_ASSETS: Dictionary = {
"Footstep": "sfx_footstep_metal_walk",
"FootstepWalk": "sfx_footstep_metal_walk",
"FootstepCareful":"sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands
"Footstep": "sfx_footstep_metal_walk",
"FootstepWalk": "sfx_footstep_metal_walk",
"FootstepCareful": "sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands
"FootstepCrouch": "sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands
"FootstepSprint": "sfx_footstep_metal_run",
"FootstepRun": "sfx_footstep_metal_run",
"FootstepRun": "sfx_footstep_metal_run",
}
# Asset registry: filename stem (e.g. "amb_station_base") → AudioStream
@@ -110,6 +116,7 @@ func _ready() -> void:
# --- Bus setup ---
func _setup_buses() -> void:
for bus_name in BUSES:
if AudioServer.get_bus_index(bus_name) < 0:
@@ -130,6 +137,7 @@ func _setup_buses() -> void:
# --- Asset registry (D-068 directory-scan pattern) ---
func _scan_registry() -> void:
_scan_dir("res://assets/audio/")
print("AudioManager: %d assets registered" % _registry.size())
@@ -166,6 +174,7 @@ func get_registry_size() -> int:
# --- Playback: non-spatial ---
## Play a one-shot sound on the given bus. No-ops if asset not in registry.
func play(asset_key: String, bus: String = BUS_UI_SOUNDS) -> void:
var stream := _get_stream(asset_key)
@@ -227,6 +236,7 @@ func play_sound_event(event_type: String, world_tile_pos: Vector2) -> void:
# --- Playback: spatial (D-018 close-range) ---
## Play a one-shot spatial sound at a world position (pixels).
## Added to current_scene for correct AudioListener2D positioning.
func play_at(asset_key: String, world_position: Vector2, bus: String = BUS_WORLD_SFX) -> void:
@@ -248,6 +258,7 @@ func play_at(asset_key: String, world_position: Vector2, bus: String = BUS_WORLD
# --- Audio dip profiles (D-069) ---
## Apply a dip profile. Interrupts any active dip, restoring unaffected buses.
## Profiles: "dialogue", "confrontation", "listening_focus"
func apply_dip(profile: String) -> void:
@@ -318,6 +329,7 @@ func get_active_dip() -> String:
# --- Volume control (player sliders, D-069 proportional base) ---
## Set the player's volume preference for a bus. During active dip, immediately
## recalculates effective volume (proportional to new base).
func set_volume(bus: String, volume_db: float) -> void:
@@ -341,6 +353,7 @@ func get_volume(bus: String) -> float:
# --- Volume persistence (user://audio_prefs.cfg) ---
func _load_prefs() -> void:
var cfg := ConfigFile.new()
if cfg.load(PREFS_PATH) != OK:
@@ -366,6 +379,7 @@ func _save_prefs() -> void:
# --- Zone crossfade (D-073) ---
## Handle zone transition. Server sends zone_id per tile in ObserverSnapshot.
## Hard boundary trigger with 1.5-2s audio crossfade between ambient layers.
## Interruptible — mid-crossfade zone change tweens from current position.
@@ -415,6 +429,7 @@ func _kill_zone_tweens() -> void:
# --- Internal helpers ---
func _get_stream(asset_key: String) -> AudioStream:
return _registry.get(asset_key) as AudioStream
+13 -11
View File
@@ -7,14 +7,14 @@ extends Node
# Fog texture byte values — visibility and exploration layers.
# Used by fog shader to distinguish visual treatment per tile.
# Test assertions reference these: assert_that(byte).is_equal(FogState.VIS_FORWARD)
const VIS_HIDDEN: int = 0 # Not in LOS — fully fogged
const VIS_HIDDEN: int = 0 # Not in LOS — fully fogged
const VIS_PERIPHERAL: int = 180 # DEPRECATED: peripheral sector removed in Sprint 22 (#569).
# Retained — tests still reference it.
const VIS_FORWARD: int = 255 # In LOS, forward sector — clear vision
const VIS_FORWARD: int = 255 # In LOS, forward sector — clear vision
const EXP_UNEXPLORED: int = 0 # Never seen — total darkness
const EXP_EXPLORED: int = 128 # Previously seen, now out of LOS — deep fog
const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame)
const EXP_UNEXPLORED: int = 0 # Never seen — total darkness
const EXP_EXPLORED: int = 128 # Previously seen, now out of LOS — deep fog
const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame)
# Zone temperature tints (D-059 + D-046, Sprint 22) — keyed by zone_id string from server.
# Matches audio_manager.gd ZONE_ASSETS zone_id strings for consistent zone semantics.
@@ -25,10 +25,10 @@ const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame
# bar: #2a1f15 (warm amber-dark — social, inhabited)
# corridor: #1a1a1a (neutral dark — transitional, maintenance)
const ZONE_TINTS: Dictionary = {
"hub": Color(0.102, 0.122, 0.180), # #1a1f2e — cool blue-dark
"hub": Color(0.102, 0.122, 0.180), # #1a1f2e — cool blue-dark
"workplace": Color(0.102, 0.122, 0.180), # same as hub
"bar": Color(0.165, 0.122, 0.082), # #2a1f15 — warm amber-dark
"corridor": Color(0.102, 0.102, 0.102), # #1a1a1a — neutral dark
"bar": Color(0.165, 0.122, 0.082), # #2a1f15 — warm amber-dark
"corridor": Color(0.102, 0.102, 0.102), # #1a1a1a — neutral dark
}
const ZONE_TINT_DEFAULT: Color = Color(0.102, 0.102, 0.102) # #1a1a1a neutral
@@ -213,7 +213,11 @@ func update_from_state() -> void:
var new_g := int(tint_color.g * 255.0)
var new_b := int(tint_color.b * 255.0)
# Only update if different from current (avoid spurious texture uploads)
if _tint_bytes[tint_idx] != new_r or _tint_bytes[tint_idx + 1] != new_g or _tint_bytes[tint_idx + 2] != new_b:
if (
_tint_bytes[tint_idx] != new_r
or _tint_bytes[tint_idx + 1] != new_g
or _tint_bytes[tint_idx + 2] != new_b
):
_tint_bytes[tint_idx + 0] = new_r
_tint_bytes[tint_idx + 1] = new_g
_tint_bytes[tint_idx + 2] = new_b
@@ -243,5 +247,3 @@ func _grow_bounds_from_positions(positions: Dictionary) -> Rect2i:
if map_bounds.size.x <= 1 and map_bounds.size.y <= 1:
return tile_bounds
return map_bounds.merge(tile_bounds)
+2 -1
View File
@@ -18,7 +18,7 @@ var current_tick: int = 0
var player_position: Vector2 = Vector2.ZERO
var visible_entities: Array = []
var visible_tiles: Array = []
var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups (normal LOS tiles)
var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups (normal LOS tiles)
var boundary_positions: Dictionary = {} # Vector2i -> true, BoundaryWall margin tiles (#585)
# — visible in fog but not explored
@@ -162,6 +162,7 @@ var stationary_ticks: int = 0
# Empty string when zone_id field absent.
var current_zone_id: String = ""
func apply_snapshot(snapshot: Dictionary) -> void:
# Autoload parse-order: class_name types are not registered when autoloads compile.
# load() returns the cached resource after the first call — essentially free per-tick.
+2 -2
View File
@@ -28,8 +28,8 @@ extends Node
## HudGroups.close_app() # returns to gameplay
## HudGroups.toggle_app("implant/map") # open if closed, close if open
var _groups: Dictionary = {} # group_name -> Array[Control]
var _active_app: String = "" # currently open implant/* app ("" = none)
var _groups: Dictionary = {} # group_name -> Array[Control]
var _active_app: String = "" # currently open implant/* app ("" = none)
func register(node: Control, group: String) -> void:
+96 -48
View File
@@ -14,10 +14,21 @@ extends Node
# Server cooldown is authoritative, but the client throttle prevents
# flooding and gives correct movement feel in test mode.
enum Action {
MOVE_NORTH, MOVE_NORTHEAST, MOVE_EAST, MOVE_SOUTHEAST,
MOVE_SOUTH, MOVE_SOUTHWEST, MOVE_WEST, MOVE_NORTHWEST,
INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE, UNPAUSE,
TOGGLE_STANCE_UP, TOGGLE_STANCE_DOWN,
MOVE_NORTH,
MOVE_NORTHEAST,
MOVE_EAST,
MOVE_SOUTHEAST,
MOVE_SOUTH,
MOVE_SOUTHWEST,
MOVE_WEST,
MOVE_NORTHWEST,
INTERACT,
USE_PERCEPTION_MODE,
OPEN_MENU,
PAUSE,
UNPAUSE,
TOGGLE_STANCE_UP,
TOGGLE_STANCE_DOWN,
BUG_REPORT, # #495: F12 WRONG button — client-only, not sent to server
OPEN_JOURNAL, # #264: J key — toggle knowledge journal panel, client-only
SET_FACING, # D-054: facing octant update (no movement)
@@ -25,18 +36,18 @@ 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)
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)
}
# Minimum milliseconds between movement commands, per stance.
# Tuned so Walk feels like walking, Sprint feels fast but readable.
const MOVE_INTERVAL_MS := {
"Sprint": 200, # 5/sec — fast but trackable
"Walk": 400, # 2.5/sec — comfortable walking pace
"Careful": 600, # ~1.7/sec — deliberate, scanning
"Crouch": 800, # 1.25/sec — creeping
"Sprint": 200, # 5/sec — fast but trackable
"Walk": 400, # 2.5/sec — comfortable walking pace
"Careful": 600, # ~1.7/sec — deliberate, scanning
"Crouch": 800, # 1.25/sec — creeping
}
var input_queue: Array[Dictionary] = []
@@ -44,7 +55,7 @@ var input_queue: Array[Dictionary] = []
# D-054: Client-side facing angle (radians). 0=East, -PI/2=North, PI/2=South.
# Updated every frame from mouse position. EntityRenderer reads this for indicator.
var facing_angle: float = -PI / 2.0 # Default: North
var facing_octant: String = "North" # Derived from facing_angle
var facing_octant: String = "North" # Derived from facing_angle
var _last_sent_octant: String = "North" # Track to avoid redundant sends
var _last_move_msec: int = 0
@@ -63,11 +74,16 @@ func _process(_delta: float) -> void:
# D-054: Send facing octant to server when it changes (even without movement)
if facing_octant != _last_sent_octant:
_last_sent_octant = facing_octant
input_queue.append({
"action": Action.SET_FACING,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {"facing": facing_octant},
})
(
input_queue
. append(
{
"action": Action.SET_FACING,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {"facing": facing_octant},
}
)
)
# Poll held WASD keys
var raw_dir := Vector2i.ZERO
@@ -88,10 +104,15 @@ func _process(_delta: float) -> void:
# D-054: Transform WASD input relative to mouse facing
var world_dir := _wasd_to_world_dir(raw_dir)
var action: Action = _dir_to_action(world_dir)
input_queue.append({
"action": action,
"timestamp_msec": now,
})
(
input_queue
. append(
{
"action": action,
"timestamp_msec": now,
}
)
)
# Discrete actions: fire once on key press (not held).
@@ -197,15 +218,24 @@ static func _snap_to_octant_dir(dir: Vector2) -> Vector2i:
# Quantize to nearest 45° (PI/4)
var octant := roundi(angle / (PI / 4.0))
match octant:
0: return Vector2i(1, 0) # East
1: return Vector2i(1, 1) # Southeast
2, -6: return Vector2i(0, 1) # South
3, -5: return Vector2i(-1, 1) # Southwest
4, -4: return Vector2i(-1, 0) # West
-3, 5: return Vector2i(-1, -1) # Northwest
-2: return Vector2i(0, -1) # North
-1: return Vector2i(1, -1) # Northeast
_: return Vector2i.ZERO
0:
return Vector2i(1, 0) # East
1:
return Vector2i(1, 1) # Southeast
2, -6:
return Vector2i(0, 1) # South
3, -5:
return Vector2i(-1, 1) # Southwest
4, -4:
return Vector2i(-1, 0) # West
-3, 5:
return Vector2i(-1, -1) # Northwest
-2:
return Vector2i(0, -1) # North
-1:
return Vector2i(1, -1) # Northeast
_:
return Vector2i.ZERO
# D-054: Convert a facing angle (radians) to the nearest octant name.
@@ -213,27 +243,45 @@ static func _snap_to_octant_dir(dir: Vector2) -> Vector2i:
static func _angle_to_octant(angle: float) -> String:
var octant := roundi(angle / (PI / 4.0))
match octant:
0: return "East"
1: return "Southeast"
2, -6: return "South"
3, -5: return "Southwest"
4, -4: return "West"
-3, 5: return "Northwest"
-2: return "North"
-1: return "Northeast"
_: return "East"
0:
return "East"
1:
return "Southeast"
2, -6:
return "South"
3, -5:
return "Southwest"
4, -4:
return "West"
-3, 5:
return "Northwest"
-2:
return "North"
-1:
return "Northeast"
_:
return "East"
# Map a direction vector to the corresponding movement Action.
# Handles all 8 directions via composite W+D, W+A, etc.
static func _dir_to_action(dir: Vector2i) -> Action:
match dir:
Vector2i(0, -1): return Action.MOVE_NORTH
Vector2i(1, -1): return Action.MOVE_NORTHEAST
Vector2i(1, 0): return Action.MOVE_EAST
Vector2i(1, 1): return Action.MOVE_SOUTHEAST
Vector2i(0, 1): return Action.MOVE_SOUTH
Vector2i(-1, 1): return Action.MOVE_SOUTHWEST
Vector2i(-1, 0): return Action.MOVE_WEST
Vector2i(-1, -1): return Action.MOVE_NORTHWEST
_: return Action.MOVE_NORTH
Vector2i(0, -1):
return Action.MOVE_NORTH
Vector2i(1, -1):
return Action.MOVE_NORTHEAST
Vector2i(1, 0):
return Action.MOVE_EAST
Vector2i(1, 1):
return Action.MOVE_SOUTHEAST
Vector2i(0, 1):
return Action.MOVE_SOUTH
Vector2i(-1, 1):
return Action.MOVE_SOUTHWEST
Vector2i(-1, 0):
return Action.MOVE_WEST
Vector2i(-1, -1):
return Action.MOVE_NORTHWEST
_:
return Action.MOVE_NORTH
+63 -67
View File
@@ -16,28 +16,23 @@ extends Node
## 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)
}
enum PowerProfile { FULL = 0, BATTERY = 1, POWER_SAVER = 2 } # Plugged in (charged, charging, or no battery) — no restrictions # On battery — AI inference should be suspended per D-138 §8 Layer 3 # 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_UNKNOWN := 0
const _POWER_STATE_ON_BATTERY := 1
const _POWER_STATE_NO_BATTERY := 2
const _POWER_STATE_CHARGING := 3
const _POWER_STATE_CHARGED := 4
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
@@ -48,7 +43,6 @@ var raw_power_state: int = 0
## Battery charge percentage (0100). -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.
@@ -57,7 +51,6 @@ 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").
@@ -66,7 +59,6 @@ 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").
@@ -84,7 +76,6 @@ 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").
@@ -99,7 +90,6 @@ 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.
@@ -117,7 +107,6 @@ 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").
@@ -126,7 +115,6 @@ var locale: String = ""
## Language portion of the locale (e.g. "en").
var locale_language: String = ""
# -- File paths ---------------------------------------------------------------
## Base user data directory (user://).
@@ -150,9 +138,9 @@ 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()
@@ -167,12 +155,12 @@ func _ready() -> void:
func _init_paths() -> void:
user_data_dir = OS.get_user_data_dir()
config_dir = user_data_dir
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"
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"):
@@ -184,12 +172,12 @@ func _init_paths() -> void:
func _init_hardware() -> void:
# -- CPU
cpu_name = OS.get_processor_name()
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_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"):
@@ -198,7 +186,7 @@ func _init_hardware() -> void:
# -- Platform identity
platform_name = OS.get_name()
os_version = OS.get_version()
os_version = OS.get_version()
if OS.has_method("get_distribution_name"):
var dist: Variant = OS.callv("get_distribution_name", [])
if dist is String:
@@ -207,14 +195,14 @@ func _init_hardware() -> void:
# -- Display
screen_count = DisplayServer.get_screen_count()
screen_size = DisplayServer.screen_get_size()
screen_dpi = DisplayServer.screen_get_dpi()
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 = OS.get_locale()
locale_language = OS.get_locale_language()
@@ -222,11 +210,16 @@ func _init_hardware() -> void:
## 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"
1:
return "integrated"
2:
return "discrete"
3:
return "virtual"
4:
return "cpu"
_:
return "other"
## Compute display scale for the primary screen.
@@ -241,17 +234,19 @@ func _compute_display_scale() -> float:
# -- 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)
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:
@@ -293,9 +288,9 @@ func _poll_power() -> void:
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
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
@@ -304,45 +299,46 @@ func _poll_power() -> void:
# -- 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,
"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,
"free_memory_mb": free_memory_mb,
"total_memory_mb": total_memory_mb,
# CPU
"cpu_name": cpu_name,
"cpu_logical_cores": cpu_logical_cores,
"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),
"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,
"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_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,
"display_scale": display_scale,
# Locale
"locale": locale,
"locale_language": locale_language,
"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,
"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,
}
+49 -20
View File
@@ -17,18 +17,26 @@ var _quit_dialog: ConfirmationDialog = null
## Returns the new game-id string.
func new_game() -> String:
var now := Time.get_datetime_dict_from_system()
var timestamp := "%04d%02d%02d-%02d%02d%02d" % [
now.year, now.month, now.day,
now.hour, now.minute, now.second,
]
var timestamp := (
"%04d%02d%02d-%02d%02d%02d"
% [
now.year,
now.month,
now.day,
now.hour,
now.minute,
now.second,
]
)
var rng := RandomNumberGenerator.new()
var hex_seed := "%06x" % (rng.randi() & 0xFFFFFF)
var game_id := "%s-%s" % [timestamp, hex_seed]
var save_path := SAVES_DIR + game_id + "/"
var err := DirAccess.make_dir_recursive_absolute(save_path)
if err != OK:
push_error("SessionManager: failed to create save dir %s: %s" % [
save_path, error_string(err)])
push_error(
"SessionManager: failed to create save dir %s: %s" % [save_path, error_string(err)]
)
return ""
GameState.current_game_id = game_id
@@ -71,15 +79,21 @@ func list_game_dirs() -> Array:
var mtime: int = 0
if newest_save != "":
mtime = FileAccess.get_modified_time(dir_path + newest_save)
results.append({
"game_id": entry,
"modified_time": mtime,
"newest_save": newest_save,
})
(
results
. append(
{
"game_id": entry,
"modified_time": mtime,
"newest_save": newest_save,
}
)
)
entry = dir.get_next()
dir.list_dir_end()
results.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a.modified_time > b.modified_time)
results.sort_custom(
func(a: Dictionary, b: Dictionary) -> bool: return a.modified_time > b.modified_time
)
return results
@@ -105,11 +119,16 @@ func _do_quit_to_menu() -> void:
# SimBridge._process() flushes the outbound buffer before teardown.
if not GameState.current_game_id.is_empty():
var path := "user://saves/" + GameState.current_game_id + "/quicksave.sav"
SimBridge.send_input({
"action": InputMapper.Action.SAVE_GAME,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {"path": path},
})
(
SimBridge
. send_input(
{
"action": InputMapper.Action.SAVE_GAME,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {"path": path},
}
)
)
GameState.current_game_id = ""
_navigate_to_menu.call_deferred()
else:
@@ -131,7 +150,12 @@ func _cleanup_quit_dialog() -> void:
func _write_seed_file(save_path: String, seed: int) -> void:
var file := FileAccess.open(save_path + "world_seed", FileAccess.WRITE)
if file == null:
push_error("SessionManager: failed to write seed file: %s" % error_string(FileAccess.get_open_error()))
push_error(
(
"SessionManager: failed to write seed file: %s"
% error_string(FileAccess.get_open_error())
)
)
return
file.store_64(seed)
@@ -152,7 +176,12 @@ func save_character_archetype(game_id: String, archetype: String) -> void:
var save_path := SAVES_DIR + game_id + "/"
var file := FileAccess.open(save_path + "character.txt", FileAccess.WRITE)
if file == null:
push_error("SessionManager: failed to write character.txt: %s" % error_string(FileAccess.get_open_error()))
push_error(
(
"SessionManager: failed to write character.txt: %s"
% error_string(FileAccess.get_open_error())
)
)
return
file.store_string(archetype)
+129 -50
View File
@@ -34,37 +34,50 @@ var _retry_timer: float = 0.0
var _handshake_start_usec: int = 0
var _test_tick: int:
get: return harness.tick if harness else 0
get:
return harness.tick if harness else 0
set(v):
if harness: harness.tick = v
if harness:
harness.tick = v
var _test_player_pos: Vector2i:
get: return harness.player_pos if harness else Vector2i.ZERO
get:
return harness.player_pos if harness else Vector2i.ZERO
set(v):
if harness: harness.player_pos = v
if harness:
harness.player_pos = v
var _test_facing: String:
get: return harness.facing if harness else "North"
get:
return harness.facing if harness else "North"
set(v):
if harness: harness.facing = v
if harness:
harness.facing = v
var _test_in_dialogue: bool:
get: return harness.in_dialogue if harness else false
get:
return harness.in_dialogue if harness else false
set(v):
if harness: harness.in_dialogue = v
if harness:
harness.in_dialogue = v
var _test_gauntlet_mode: bool:
get: return harness.gauntlet_mode if harness else false
get:
return harness.gauntlet_mode if harness else false
set(v):
if harness: harness.gauntlet_mode = v
if harness:
harness.gauntlet_mode = v
var _test_npc_relationship: String:
get: return harness.npc_relationship if harness else "Unknown"
get:
return harness.npc_relationship if harness else "Unknown"
set(v):
if harness: harness.npc_relationship = v
if harness:
harness.npc_relationship = v
var _test_input_queue: Array:
get: return harness.input_queue if harness else []
get:
return harness.input_queue if harness else []
func _ready() -> void:
@@ -75,18 +88,23 @@ func _ready() -> void:
# -- Test mode proxy API (backward compat for 13+ test files) ------------------
func reset_test_state() -> void:
if harness: harness.reset()
if harness:
harness.reset()
func _test_snapshot() -> Dictionary:
return harness.snapshot()
func _test_has_los(from: Vector2i, to: Vector2i) -> bool:
return harness.has_los(from, to)
# -- Connection lifecycle ------------------------------------------------------
# Change connection state and emit signal
func _set_state(new_state: ConnectionState) -> void:
if state != new_state:
@@ -94,6 +112,7 @@ func _set_state(new_state: ConnectionState) -> void:
state = new_state
connection_state_changed.emit(old_state, new_state)
# Connect to simulation server.
# In test mode, immediately transitions to CONNECTED.
# In live mode, spawns server subprocess and defers TCP connection to _process()
@@ -125,6 +144,7 @@ func connect_to_sim() -> void:
_retry_timer = 0.0
_bridge = null
# Disconnect from simulation server
func disconnect_from_sim() -> void:
if _bridge != null:
@@ -136,15 +156,21 @@ func disconnect_from_sim() -> void:
_connect_retries = 0
_set_state(ConnectionState.DISCONNECTED)
# Attempt TCP connection. Called from _process() during CONNECTING state.
func _try_connect() -> void:
_bridge = LocalBridge.new()
var err := _bridge.connect_to_server("127.0.0.1", server_port)
if err != OK:
push_warning("SimBridge: TCP connect attempt %d/%d failed: %s" % [
_connect_retries + 1, MAX_CONNECT_RETRIES, error_string(err)])
push_warning(
(
"SimBridge: TCP connect attempt %d/%d failed: %s"
% [_connect_retries + 1, MAX_CONNECT_RETRIES, error_string(err)]
)
)
_bridge = null
# Poll transport layer every frame (non-test mode only)
func _process(delta: float) -> void: # gdlint:disable=max-returns
if test_mode:
@@ -158,7 +184,9 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
_retry_timer = 0.0
_connect_retries += 1
if _connect_retries > MAX_CONNECT_RETRIES:
push_error("SimBridge: TCP connection failed after %d retries" % MAX_CONNECT_RETRIES)
push_error(
"SimBridge: TCP connection failed after %d retries" % MAX_CONNECT_RETRIES
)
_set_state(ConnectionState.ERROR)
return
_try_connect()
@@ -176,7 +204,9 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
# Connection attempt failed — retry
_bridge = null
if _connect_retries >= MAX_CONNECT_RETRIES:
push_error("SimBridge: TCP connection failed after %d retries" % MAX_CONNECT_RETRIES)
push_error(
"SimBridge: TCP connection failed after %d retries" % MAX_CONNECT_RETRIES
)
_set_state(ConnectionState.ERROR)
StreamPeerTCP.STATUS_NONE:
_bridge = null # Reset and retry
@@ -191,7 +221,10 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
# Check connection dropped during handshake
var bridge_status := _bridge.get_status()
if bridge_status == StreamPeerTCP.STATUS_ERROR or bridge_status == StreamPeerTCP.STATUS_NONE:
if (
bridge_status == StreamPeerTCP.STATUS_ERROR
or bridge_status == StreamPeerTCP.STATUS_NONE
):
var reason := "Connection dropped during handshake"
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
@@ -215,8 +248,11 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
# Decode HandshakeMessage: { "protocol_version": N }
var decoded: Variant = Messagepack.decode(msg)
if decoded.status != null or not (decoded.value is Dictionary) \
or not decoded.value.has("protocol_version"):
if (
decoded.status != null
or not (decoded.value is Dictionary)
or not decoded.value.has("protocol_version")
):
var reason := "Handshake decode failed: malformed HandshakeMessage"
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
@@ -226,8 +262,10 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
var server_version: int = decoded.value["protocol_version"]
if server_version != Protocol.PROTOCOL_VERSION:
var reason := "Protocol version mismatch: server=%d, client=%d" % [
server_version, Protocol.PROTOCOL_VERSION]
var reason := (
"Protocol version mismatch: server=%d, client=%d"
% [server_version, Protocol.PROTOCOL_VERSION]
)
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
_bridge.disconnect_from_server()
@@ -237,7 +275,10 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
# Send startup message with world_seed and character appearance (#175, D-010/D-029, #718).
# Server blocks waiting for this before entering the tick loop.
var startup_bytes := Protocol.encode_startup_message(
GameState.world_seed, GameState.character_archetype, GameState.character_visual_descriptor)
GameState.world_seed,
GameState.character_archetype,
GameState.character_visual_descriptor
)
if startup_bytes.size() > 0:
var send_err := _bridge.send_message(startup_bytes)
if send_err != OK:
@@ -259,10 +300,15 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
_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",
})
(
_outbound_buffer
. append(
{
"tick": 0,
"action_name": "RequestAllSettings",
}
)
)
return
if _bridge == null:
@@ -289,7 +335,9 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
if err != OK:
push_error("SimBridge: failed to send message: %s" % error_string(err))
else:
push_error("SimBridge: failed to batch-encode %d inputs (dropped)" % outbound.size())
push_error(
"SimBridge: failed to batch-encode %d inputs (dropped)" % outbound.size()
)
StreamPeerTCP.STATUS_CONNECTING:
pass # Should not happen in CONNECTED state
StreamPeerTCP.STATUS_ERROR:
@@ -304,6 +352,7 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
# -- Input / snapshot ----------------------------------------------------------
# Send input to simulation server.
# player_input: Dictionary with "action" (int from InputMapper.Action enum) and "timestamp_msec".
# In test mode, inputs are delegated to the test harness.
@@ -330,13 +379,14 @@ func send_input(player_input: Dictionary) -> Error:
if action_name.is_empty():
return ERR_INVALID_PARAMETER
var tick: int = GameState.current_tick
var entry: Dictionary = { "tick": tick, "action_name": action_name }
var entry: Dictionary = {"tick": tick, "action_name": action_name}
var action_data: Variant = player_input.get("action_data")
if action_data != null:
entry["action_data"] = action_data
_outbound_buffer.append(entry)
return OK
# Poll for snapshot from simulation.
# In test mode delegates to test harness. In live mode, returns the last decoded snapshot.
func poll_snapshot() -> Variant:
@@ -356,6 +406,7 @@ func poll_snapshot() -> Variant:
return null
# Called by transport layer when raw bytes arrive from the server.
# Latest-wins for positional state (stale frames are worthless), but one-shot
# events (monologue, dialogue) are carried forward from overwritten snapshots
@@ -367,12 +418,21 @@ func receive_bytes(bytes: PackedByteArray) -> void:
return
if _last_snapshot != null:
# Carry forward one-shot events the client hasn't consumed yet.
if snapshot.get("current_monologue") == null and _last_snapshot.get("current_monologue") != null:
if (
snapshot.get("current_monologue") == null
and _last_snapshot.get("current_monologue") != null
):
snapshot["current_monologue"] = _last_snapshot["current_monologue"]
if snapshot.get("current_dialogue") == null and _last_snapshot.get("current_dialogue") != null:
if (
snapshot.get("current_dialogue") == null
and _last_snapshot.get("current_dialogue") != null
):
snapshot["current_dialogue"] = _last_snapshot["current_dialogue"]
# #535: Carry forward one-shot dialogue events (arrays merge, scalar falls through)
if snapshot.get("dialogue_response") == null and _last_snapshot.get("dialogue_response") != null:
if (
snapshot.get("dialogue_response") == null
and _last_snapshot.get("dialogue_response") != null
):
snapshot["dialogue_response"] = _last_snapshot["dialogue_response"]
var old_conv_events: Array = _last_snapshot.get("conversation_events", [])
if old_conv_events.size() > 0:
@@ -386,10 +446,14 @@ func receive_bytes(bytes: PackedByteArray) -> void:
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:
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.
func drain_outbound() -> Array[Dictionary]:
var inputs = _outbound_buffer.duplicate()
@@ -399,24 +463,39 @@ func drain_outbound() -> Array[Dictionary]:
# -- Wire protocol mapping -----------------------------------------------------
# Map InputMapper.Action enum values to wire-format action names (matching Rust PlayerAction).
# OPEN_MENU is client-only — no Rust equivalent, not sent over the wire.
static func action_enum_to_wire(action: int) -> String:
match action:
InputMapper.Action.MOVE_NORTH: return "MoveNorth"
InputMapper.Action.MOVE_NORTHEAST: return "MoveNortheast"
InputMapper.Action.MOVE_EAST: return "MoveEast"
InputMapper.Action.MOVE_SOUTHEAST: return "MoveSoutheast"
InputMapper.Action.MOVE_SOUTH: return "MoveSouth"
InputMapper.Action.MOVE_SOUTHWEST: return "MoveSouthwest"
InputMapper.Action.MOVE_WEST: return "MoveWest"
InputMapper.Action.MOVE_NORTHWEST: return "MoveNorthwest"
InputMapper.Action.INTERACT: return "Interact"
InputMapper.Action.USE_PERCEPTION_MODE: return "UsePerceptionMode"
InputMapper.Action.PAUSE: return "Pause"
InputMapper.Action.UNPAUSE: return "Unpause"
InputMapper.Action.TOGGLE_STANCE_UP: return "ToggleStanceUp"
InputMapper.Action.TOGGLE_STANCE_DOWN: return "ToggleStanceDown"
InputMapper.Action.MOVE_NORTH:
return "MoveNorth"
InputMapper.Action.MOVE_NORTHEAST:
return "MoveNortheast"
InputMapper.Action.MOVE_EAST:
return "MoveEast"
InputMapper.Action.MOVE_SOUTHEAST:
return "MoveSoutheast"
InputMapper.Action.MOVE_SOUTH:
return "MoveSouth"
InputMapper.Action.MOVE_SOUTHWEST:
return "MoveSouthwest"
InputMapper.Action.MOVE_WEST:
return "MoveWest"
InputMapper.Action.MOVE_NORTHWEST:
return "MoveNorthwest"
InputMapper.Action.INTERACT:
return "Interact"
InputMapper.Action.USE_PERCEPTION_MODE:
return "UsePerceptionMode"
InputMapper.Action.PAUSE:
return "Pause"
InputMapper.Action.UNPAUSE:
return "Unpause"
InputMapper.Action.TOGGLE_STANCE_UP:
return "ToggleStanceUp"
InputMapper.Action.TOGGLE_STANCE_DOWN:
return "ToggleStanceDown"
InputMapper.Action.OPEN_MENU:
return "" # Client-only action, not part of wire protocol
InputMapper.Action.BUG_REPORT:
@@ -432,11 +511,11 @@ static func action_enum_to_wire(action: int) -> String:
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)
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
return "DeleteSetting" # #646: struct variant — delete setting by key
_:
push_warning("SimBridge: unknown action enum %s" % action)
return ""
+7
View File
@@ -9,9 +9,11 @@ const STRINGS_PATH: String = "res://data/ui-strings.yaml"
var _strings: Dictionary = {}
var _loaded: bool = false
func _ready() -> void:
_load_strings()
func _load_strings() -> void:
if not FileAccess.file_exists(STRINGS_PATH):
push_warning("UIStrings: file not found: %s" % STRINGS_PATH)
@@ -26,6 +28,7 @@ func _load_strings() -> void:
_loaded = true
print("UIStrings: loaded %d strings from %s" % [_strings.size(), STRINGS_PATH])
## Get a UI string by dotted key. Returns the key itself if not found.
func get_text(key: String) -> String:
if _strings.has(key):
@@ -33,20 +36,24 @@ func get_text(key: String) -> String:
push_warning("UIStrings: missing key '%s'" % key)
return key
## Check if a key exists.
func has_key(key: String) -> bool:
return _strings.has(key)
## Get all keys.
func get_all_keys() -> PackedStringArray:
return PackedStringArray(_strings.keys())
## Reload from disk (useful for hot-reload during development).
func reload() -> void:
_strings.clear()
_loaded = false
_load_strings()
## Parse YAML with arbitrary nesting depth.
## Returns flat Dictionary with dotted keys: { "section.sub.key": "value" }.
## Delegates to YamlParser.parse_flat() (#560).