main_menu now connects SimBridge before character_creation opens, gating the transition on first ObserverSnapshot carrying a bookmark_catalog. Loading screen is shown during the connect; on cancel the SimBridge subprocess is torn down and the player returns to main_menu. Catalog is read straight from GameState.bookmark_catalog in W6. Flow (Option A): 1. New Game → SessionManager.new_game() creates save dir 2. main_menu pushes loading_screen via MetaStack with "Connecting to simulation..." message 3. SimBridge.connect_to_sim() spawned; main_menu listens on connection_state_changed, then on snapshot_received for the catalog 4. On catalog arrival: loading_screen closed, scene-transition to character_creation 5. character_creation Cancel → SimBridge.disconnect_from_sim() + scene transition back to main_menu (Tyre's recommendation: clean state per session over warm-start savings) 6. character_creation Start → ConfirmBookmark sent (stubbed for W4 with first catalog entry; real bookmark + location from W6's UI) ESC priority chain in main.gd OPEN_MENU handler: - MetaStack.handle_escape() first — closes the topmost meta overlay - HudGroups.is_implant_active() / close_app() — closes active implant - Fallback: toggle settings_dialog (existing W2 behavior) Files: - sim_bridge.gd: send_named_action(action_name, action_data) helper. Bridges named tag-enum PlayerActions (RequestBookmarkCatalog, ConfirmBookmark) into the existing outbound buffer, parallel to send_input's InputMapper.Action handling. - loading_screen.gd: set_message(text) for the connecting/loading label. - main_menu.gd: full Option A flow rewrite. Tracks _waiting_for_catalog so re-clicking New Game during connect is a no-op. - character_creation.gd: _on_back disconnect path + _on_start ConfirmBookmark stub. MAIN_MENU_SCENE / GAME_SCENE constants. - main.gd: connect_to_sim guard (don't reconnect when Option A leaves it CONNECTED). ESC chain wiring. Verification: - gdlint clean - godot --headless --path client --quit — no SCRIPT ERROR - test_protocol 62/62, test_implant_nav_stack 52/52, test_client_p3 24/24, test_ui_framework_sprint15 54/54 Pre-existing failing suites unchanged: test_sprint2_proof, test_dialogue_sprint18, test_client_p2 (camera-smoothing assertions that pre-date W4 — main.gd has disabled position_smoothing_enabled since #117 / #501 / #117 manual-lerp; tests were stale). Workstream 5 (3-tab restructure of character_creation: Bookmark / Appearance with sub-nav / Skills / Debug) lands next. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
535 lines
18 KiB
GDScript
535 lines
18 KiB
GDScript
extends Node
|
|
|
|
# Signals
|
|
signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState)
|
|
signal snapshot_received(snapshot: Dictionary)
|
|
signal handshake_complete(protocol_version: int)
|
|
signal handshake_failed(reason: String)
|
|
|
|
# Connection states
|
|
enum ConnectionState { DISCONNECTED, CONNECTING, HANDSHAKING, CONNECTED, ERROR }
|
|
|
|
# Connection retry state — handles server startup delay (Critical fix #1)
|
|
const MAX_CONNECT_RETRIES: int = 20 # ~2 seconds at 60fps with 100ms delay
|
|
const CONNECT_RETRY_INTERVAL: float = 0.1 # Seconds between retry attempts
|
|
|
|
# Handshake state (#556)
|
|
const HANDSHAKE_TIMEOUT_USEC: int = 5_000_000 # 5 seconds
|
|
|
|
var state: ConnectionState = ConnectionState.DISCONNECTED
|
|
var test_mode: bool = OS.get_environment("SR_LIVE") != "1" # SR_LIVE=1 connects to real server
|
|
# Type is TestHarness — untyped to avoid autoload parse-order issue.
|
|
var harness = null # Test simulation (D-020: game logic lives outside production client)
|
|
var server_port: int = 9876 # Default matches server's default bind address
|
|
var server_path: String = "" # Path to server binary — set before connect_to_sim()
|
|
|
|
var _last_snapshot: Variant = null # Most recent decoded snapshot (consumed by poll_snapshot)
|
|
var _outbound_buffer: Array[Dictionary] = [] # Raw inputs awaiting batch encode + transport
|
|
|
|
# Transport layer (non-test mode)
|
|
var _bridge: LocalBridge = null
|
|
var _server: ServerProcess = null
|
|
var _connect_retries: int = 0
|
|
var _retry_timer: float = 0.0
|
|
var _handshake_start_usec: int = 0
|
|
|
|
var _test_tick: int:
|
|
get:
|
|
return harness.tick if harness else 0
|
|
set(v):
|
|
if harness:
|
|
harness.tick = v
|
|
|
|
var _test_player_pos: Vector2i:
|
|
get:
|
|
return harness.player_pos if harness else Vector2i.ZERO
|
|
set(v):
|
|
if harness:
|
|
harness.player_pos = v
|
|
|
|
var _test_facing: String:
|
|
get:
|
|
return harness.facing if harness else "North"
|
|
set(v):
|
|
if harness:
|
|
harness.facing = v
|
|
|
|
var _test_in_dialogue: bool:
|
|
get:
|
|
return harness.in_dialogue if harness else false
|
|
set(v):
|
|
if harness:
|
|
harness.in_dialogue = v
|
|
|
|
var _test_gauntlet_mode: bool:
|
|
get:
|
|
return harness.gauntlet_mode if harness else false
|
|
set(v):
|
|
if harness:
|
|
harness.gauntlet_mode = v
|
|
|
|
var _test_npc_relationship: String:
|
|
get:
|
|
return harness.npc_relationship if harness else "Unknown"
|
|
set(v):
|
|
if harness:
|
|
harness.npc_relationship = v
|
|
|
|
var _test_input_queue: Array:
|
|
get:
|
|
return harness.input_queue if harness else []
|
|
|
|
|
|
func _ready() -> void:
|
|
if test_mode:
|
|
harness = load("res://scripts/protocol/test_harness.gd").new()
|
|
print("SimBridge: Running in test mode (dynamic snapshot)")
|
|
|
|
|
|
# -- Test mode proxy API (backward compat for 13+ test files) ------------------
|
|
|
|
|
|
func reset_test_state() -> void:
|
|
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:
|
|
var old_state = state
|
|
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()
|
|
# to allow the server time to bind its port.
|
|
func connect_to_sim() -> void:
|
|
_set_state(ConnectionState.CONNECTING)
|
|
|
|
if test_mode:
|
|
_set_state(ConnectionState.CONNECTED)
|
|
return
|
|
|
|
# Spawn server subprocess
|
|
if not server_path.is_empty():
|
|
_server = ServerProcess.new()
|
|
# Server reads first positional arg as bind address (e.g. "127.0.0.1:9876").
|
|
# D-085 (#258): pass --game-id <id> so server logs use the same session identifier.
|
|
var args := ["127.0.0.1:" + str(server_port)]
|
|
var game_id: String = GameState.current_game_id
|
|
if not game_id.is_empty():
|
|
args.append_array(["--game-id", game_id])
|
|
var pid := _server.start(server_path, args)
|
|
if pid <= 0:
|
|
push_error("SimBridge: failed to start server")
|
|
_set_state(ConnectionState.ERROR)
|
|
return
|
|
|
|
# Defer TCP connection to _process() — server needs time to bind its port
|
|
_connect_retries = 0
|
|
_retry_timer = 0.0
|
|
_bridge = null
|
|
|
|
|
|
# Disconnect from simulation server
|
|
func disconnect_from_sim() -> void:
|
|
if _bridge != null:
|
|
_bridge.disconnect_from_server()
|
|
_bridge = null
|
|
if _server != null:
|
|
_server.stop()
|
|
_server = null
|
|
_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)]
|
|
)
|
|
)
|
|
_bridge = null
|
|
|
|
|
|
# Poll transport layer every frame (non-test mode only)
|
|
func _process(delta: float) -> void: # gdlint:disable=max-returns
|
|
if test_mode:
|
|
return
|
|
|
|
# CONNECTING state: retry TCP connection until server is ready
|
|
if state == ConnectionState.CONNECTING:
|
|
if _bridge == null:
|
|
_retry_timer += delta
|
|
if _retry_timer >= CONNECT_RETRY_INTERVAL or _connect_retries == 0:
|
|
_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
|
|
)
|
|
_set_state(ConnectionState.ERROR)
|
|
return
|
|
_try_connect()
|
|
return
|
|
|
|
# Bridge exists — poll for connection completion
|
|
_bridge.poll()
|
|
match _bridge.get_status():
|
|
StreamPeerTCP.STATUS_CONNECTED:
|
|
_handshake_start_usec = Time.get_ticks_usec()
|
|
_set_state(ConnectionState.HANDSHAKING)
|
|
StreamPeerTCP.STATUS_CONNECTING:
|
|
pass # Still connecting, wait
|
|
StreamPeerTCP.STATUS_ERROR:
|
|
# Connection attempt failed — retry
|
|
_bridge = null
|
|
if _connect_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
|
|
return
|
|
|
|
# HANDSHAKING state: read first framed message, validate HandshakeMessage (#556)
|
|
if state == ConnectionState.HANDSHAKING:
|
|
if _bridge == null:
|
|
_set_state(ConnectionState.ERROR)
|
|
return
|
|
_bridge.poll()
|
|
|
|
# Check connection dropped during handshake
|
|
var bridge_status := _bridge.get_status()
|
|
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)
|
|
_bridge = null
|
|
_set_state(ConnectionState.ERROR)
|
|
return
|
|
|
|
# Check timeout
|
|
if Time.get_ticks_usec() - _handshake_start_usec > HANDSHAKE_TIMEOUT_USEC:
|
|
var reason := "Handshake timeout: no message received within 5 seconds"
|
|
push_error("SimBridge: %s" % reason)
|
|
handshake_failed.emit(reason)
|
|
_bridge.disconnect_from_server()
|
|
_set_state(ConnectionState.ERROR)
|
|
return
|
|
|
|
# Try to read first message
|
|
var msg := _bridge.poll_message()
|
|
if msg.is_empty():
|
|
return # Not ready yet, continue polling
|
|
|
|
# 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")
|
|
):
|
|
var reason := "Handshake decode failed: malformed HandshakeMessage"
|
|
push_error("SimBridge: %s" % reason)
|
|
handshake_failed.emit(reason)
|
|
_bridge.disconnect_from_server()
|
|
_set_state(ConnectionState.ERROR)
|
|
return
|
|
|
|
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]
|
|
)
|
|
push_error("SimBridge: %s" % reason)
|
|
handshake_failed.emit(reason)
|
|
_bridge.disconnect_from_server()
|
|
_set_state(ConnectionState.ERROR)
|
|
return
|
|
|
|
# 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
|
|
)
|
|
if startup_bytes.size() > 0:
|
|
var send_err := _bridge.send_message(startup_bytes)
|
|
if send_err != OK:
|
|
var reason := "Failed to send startup message: %s" % error_string(send_err)
|
|
push_error("SimBridge: %s" % reason)
|
|
handshake_failed.emit(reason)
|
|
_bridge.disconnect_from_server()
|
|
_set_state(ConnectionState.ERROR)
|
|
return
|
|
else:
|
|
var reason := "Failed to encode startup message"
|
|
push_error("SimBridge: %s" % reason)
|
|
handshake_failed.emit(reason)
|
|
_bridge.disconnect_from_server()
|
|
_set_state(ConnectionState.ERROR)
|
|
return
|
|
|
|
handshake_complete.emit(server_version)
|
|
_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",
|
|
}
|
|
)
|
|
)
|
|
return
|
|
|
|
if _bridge == null:
|
|
return
|
|
|
|
_bridge.poll()
|
|
|
|
match _bridge.get_status():
|
|
StreamPeerTCP.STATUS_CONNECTED:
|
|
# Receive: drain all complete messages from the bridge
|
|
var msg := _bridge.poll_message()
|
|
while msg.size() > 0:
|
|
receive_bytes(msg)
|
|
msg = _bridge.poll_message()
|
|
# Send: batch-encode and flush outbound buffer as one frame (Vec<PlayerInput>).
|
|
# Inputs are drained before encoding. On encode failure the inputs are
|
|
# intentionally dropped — re-queuing would retry the same bad data and
|
|
# the server tick has already advanced, making stale inputs invalid.
|
|
var outbound := drain_outbound()
|
|
if outbound.size() > 0:
|
|
var encoded := Protocol.encode_player_inputs(outbound)
|
|
if encoded.size() > 0:
|
|
var err := _bridge.send_message(encoded)
|
|
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()
|
|
)
|
|
StreamPeerTCP.STATUS_CONNECTING:
|
|
pass # Should not happen in CONNECTED state
|
|
StreamPeerTCP.STATUS_ERROR:
|
|
if state != ConnectionState.ERROR:
|
|
push_error("SimBridge: TCP connection error")
|
|
_set_state(ConnectionState.ERROR)
|
|
StreamPeerTCP.STATUS_NONE:
|
|
if state == ConnectionState.CONNECTED:
|
|
push_warning("SimBridge: connection lost")
|
|
_set_state(ConnectionState.DISCONNECTED)
|
|
|
|
|
|
# -- 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.
|
|
# In live mode, encoded and buffered for transport.
|
|
# Returns OK on success, or an error code on failure.
|
|
func send_input(player_input: Dictionary) -> Error:
|
|
if state != ConnectionState.CONNECTED:
|
|
return ERR_CONNECTION_ERROR
|
|
if test_mode:
|
|
var action: int = player_input.get("action", -1)
|
|
var wire_name: String = action_enum_to_wire(action)
|
|
if not wire_name.is_empty():
|
|
if wire_name == "SetFacing":
|
|
var facing: String = ""
|
|
var action_data: Variant = player_input.get("action_data")
|
|
if action_data is Dictionary:
|
|
facing = str(action_data.get("facing", ""))
|
|
if not facing.is_empty():
|
|
harness.process_facing(facing)
|
|
else:
|
|
harness.process_input(wire_name)
|
|
return OK
|
|
var action_name := action_enum_to_wire(player_input.get("action", -1))
|
|
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 action_data: Variant = player_input.get("action_data")
|
|
if action_data != null:
|
|
entry["action_data"] = action_data
|
|
_outbound_buffer.append(entry)
|
|
return OK
|
|
|
|
|
|
## Queue a named PlayerAction by wire string (e.g. "RequestBookmarkCatalog").
|
|
## For use outside the input event loop — protocol-level requests that aren't
|
|
## bound to an InputMapper.Action enum value.
|
|
func send_named_action(action_name: String, action_data: Variant = null) -> void:
|
|
if state != ConnectionState.CONNECTED:
|
|
push_warning("SimBridge.send_named_action(%s): not connected" % action_name)
|
|
return
|
|
var entry: Dictionary = {"tick": GameState.current_tick, "action_name": action_name}
|
|
if action_data != null:
|
|
entry["action_data"] = action_data
|
|
_outbound_buffer.append(entry)
|
|
|
|
|
|
# Poll for snapshot from simulation.
|
|
# In test mode delegates to test harness. In live mode, returns the last decoded snapshot.
|
|
func poll_snapshot() -> Variant:
|
|
if state != ConnectionState.CONNECTED:
|
|
return null
|
|
|
|
if test_mode:
|
|
var snapshot = harness.snapshot()
|
|
snapshot_received.emit(snapshot)
|
|
return snapshot
|
|
|
|
if _last_snapshot != null:
|
|
var snapshot = _last_snapshot
|
|
_last_snapshot = null
|
|
snapshot_received.emit(snapshot)
|
|
return snapshot
|
|
|
|
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
|
|
# so they aren't silently dropped when server ticks faster than client consumes.
|
|
func receive_bytes(bytes: PackedByteArray) -> void:
|
|
var snapshot = Protocol.decode_snapshot(bytes)
|
|
if snapshot == null:
|
|
push_warning("SimBridge: decode_snapshot returned null for %d bytes" % bytes.size())
|
|
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
|
|
):
|
|
snapshot["current_monologue"] = _last_snapshot["current_monologue"]
|
|
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
|
|
):
|
|
snapshot["dialogue_response"] = _last_snapshot["dialogue_response"]
|
|
var old_conv_events: Array = _last_snapshot.get("conversation_events", [])
|
|
if old_conv_events.size() > 0:
|
|
var new_conv_events: Array = snapshot.get("conversation_events", [])
|
|
snapshot["conversation_events"] = old_conv_events + new_conv_events
|
|
var old_conv_ended: Array = _last_snapshot.get("conversation_ended", [])
|
|
if old_conv_ended.size() > 0:
|
|
var new_conv_ended: Array = snapshot.get("conversation_ended", [])
|
|
snapshot["conversation_ended"] = old_conv_ended + new_conv_ended
|
|
# #554: Carry forward save/load result (one-shot, consumed by main.gd)
|
|
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
|
|
):
|
|
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()
|
|
_outbound_buffer.clear()
|
|
return inputs
|
|
|
|
|
|
# -- 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.OPEN_MENU:
|
|
return "" # Client-only action, not part of wire protocol
|
|
InputMapper.Action.BUG_REPORT:
|
|
return "" # Client-only action (#495), not part of wire protocol
|
|
InputMapper.Action.SET_FACING:
|
|
return "SetFacing" # D-054: facing octant update (no movement)
|
|
InputMapper.Action.TELEPORT_HUB:
|
|
return "TeleportToHub" # #501: Gauntlet dev teleport (not production fast-travel)
|
|
InputMapper.Action.SAVE_GAME:
|
|
return "SaveGame" # #554: F5 quicksave (D-085)
|
|
InputMapper.Action.LOAD_GAME:
|
|
return "LoadGame" # #554: F6 quickload (D-085)
|
|
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)
|
|
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
|
|
_:
|
|
push_warning("SimBridge: unknown action enum %s" % action)
|
|
return ""
|