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:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (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.
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user