feat(client): T-1132 standalone Atlas companion shell + make atlas (D-254 SS3)
New atlas_standalone scene/script: attach-or-spawn boot (one REAL
connect_to_sim attempt at SR_PORT-or-9876 — a separate throwaway TCP
probe was proven by live run to kill a pre-accept-loop server via
broken handshake pipe; never abandon a connected socket), else spawn
--port 0 via new ServerProcess.start_with_pipe + LISTENING:{port}
stdout parse, retry against the resolved port. Reader role wired end
to end: protocol.encode_startup_message optional role param (empty
omits the wire key — byte-identical for all existing callers),
sim_bridge.connection_role suppresses the post-handshake
RequestAllSettings auto-send, hud_groups skips AutoPause/AutoResume
sends for readers (all three would otherwise burn Reader violation
strikes per the T-1130 matrix — endorsed by Oscar).
Generic implant host per D-254 SS3: the shell instantiates ALL
registered implant apps; implant_app_manifest gains
available_in_companion (opt-out, default true) and
implant_registry.instantiate_all a standalone filter param (default
preserves hud.gd behavior byte-identically). Boot order is
instantiate_all THEN open_app (reverse renders a permanently black
window — app_changed fires with no listener; matches hud.gd's order).
Owned-server lifecycle: _exit_tree stops a spawned child, attached
servers survive companion close. Known engine limitation documented:
raw SIGTERM bypasses all Godot notifications and orphans a spawned
server; WM close paths verified clean.
Live-verified: spawn-mode (301 systems rendered from systems.db over
the wire), attach-mode, two simultaneous readers, clean shutdown with
zero orphan processes. 14 new gdUnit tests (port/LISTENING parsing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,20 @@ func _ready() -> void:
|
||||
## send_named_action (protocol-level, not bound to an InputMapper.Action
|
||||
## keybind) — the same mechanism as RequestBookmarkCatalog — since occlusion
|
||||
## is a UI-state transition, not a physical input.
|
||||
## D-254 §2: skipped for Reader connections (atlas_standalone.gd's companion
|
||||
## app) — this travels through the same Vec<PlayerInput> pipeline as
|
||||
## MoveNorth/Interact, which the Reader permission matrix marks "no". A
|
||||
## Reader has no character and no pausable gameplay session of its own (the
|
||||
## standalone scene calls HudGroups.open_app() as its normal boot step, not a
|
||||
## player occluding their own gameplay), so there is nothing meaningful to
|
||||
## pause — sending it anyway would just be dropped as a role violation on
|
||||
## every single companion boot (confirmed via a live run against a real
|
||||
## server: "Reader connection ... sent 1 disallowed PlayerInput(s) — dropped
|
||||
## (strike 1/3)" fired from this exact call site the first time the companion
|
||||
## opened the Atlas fullscreen).
|
||||
func _on_gameplay_occluded_auto_pause(occluded: bool) -> void:
|
||||
if SimBridge.connection_role == "Reader":
|
||||
return
|
||||
if occluded:
|
||||
SimBridge.send_named_action("AutoPause")
|
||||
else:
|
||||
|
||||
@@ -25,6 +25,11 @@ var test_mode: bool = OS.get_environment("SR_LIVE") != "1" # SR_LIVE=1 connects
|
||||
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()
|
||||
# D-254 §2/§3: ConnectionRole wire value for the next StartupMessage — "" (default)
|
||||
# omits the "role" key (server defaults to Player, byte-identical to pre-D-254
|
||||
# behavior). atlas_standalone.gd sets this to "Reader" before connect_to_sim();
|
||||
# every existing caller (main.gd, locomotion_sandbox.gd, tests) leaves it unset.
|
||||
var connection_role: String = ""
|
||||
|
||||
var _last_snapshot: Variant = null # Most recent decoded snapshot (consumed by poll_snapshot)
|
||||
var _outbound_buffer: Array[Dictionary] = [] # Raw inputs awaiting batch encode + transport
|
||||
@@ -283,8 +288,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.
|
||||
# D-254 §2: connection_role threads "Reader" for the standalone companion;
|
||||
# every other caller leaves it "" (omitted key, decodes as Player).
|
||||
var startup_bytes := Protocol.encode_startup_message(
|
||||
GameState.world_seed, GameState.character_visual_descriptor
|
||||
GameState.world_seed, GameState.character_visual_descriptor, connection_role
|
||||
)
|
||||
if startup_bytes.size() > 0:
|
||||
var send_err: int = _bridge.send_message(startup_bytes)
|
||||
@@ -307,6 +314,13 @@ 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).
|
||||
# D-254 §2: this travels as a Vec<PlayerInput> entry (same pipeline as
|
||||
# MoveNorth/Interact), which the permission matrix marks "no" for Reader —
|
||||
# a Reader has no settings to hydrate (no character, no per-player state),
|
||||
# so skip the send rather than have the server log-and-drop it on every
|
||||
# companion connection once role enforcement lands server-side.
|
||||
if connection_role == "Reader":
|
||||
return
|
||||
(
|
||||
_outbound_buffer
|
||||
. append(
|
||||
|
||||
Reference in New Issue
Block a user