feat(simulation): auto-pause sim on implant-fullscreen — inspection substrate (T-970)
D-226 layer 1. World-advancing phases gated on pause: Movement/Storyteller/Knowledge/TickAdvance set-gated via sim_not_paused; Simulation + Economy gated per-system at their registration sites — collect_sound_events and serve_econ_state_query stay unconditioned (transient-buffer clear + paused-allowed query; set-gating Simulation leaked a stale tick-7 footstep into frozen snapshots — caught by golden_suite, fixed without touching the fixture; regression test encodes the bug shape). New PlayerAction::AutoPause/AutoResume + AutoPauseState resource implement Option A reconciliation: auto-resume only fires if auto-pause caused the pause; manual pause and Half rate survive implant open/close. PauseParams SystemParam bundle keeps process_player_input under the 16-param ceiling (BookmarkInputParams precedent). Client: HudGroups.gameplay_occluded now sends AutoPause/AutoResume via send_named_action; 5 gdUnit tests + 7 Rust tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,28 @@ var _active_app: String = "" # currently focused implant app ("" = none)
|
||||
var _active_mode: Mode = Mode.GAMEPLAY
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# T-970 (D-226 layer 1): auto-pause/auto-resume the sim when a fullscreen
|
||||
# implant app occludes gameplay. AutoPause/AutoResume are distinct
|
||||
# PlayerAction variants from the manual Pause/Unpause (Space bar, D-088) —
|
||||
# the server tracks whether ITS OWN auto-pause caused the current pause
|
||||
# (AutoPauseState) so a pre-existing manual pause or Half rate survives
|
||||
# implant open/close untouched (see server/src/simulation/input.rs and
|
||||
# server/src/simulation/time.rs).
|
||||
gameplay_occluded.connect(_on_gameplay_occluded_auto_pause)
|
||||
|
||||
|
||||
## Sends AutoPause/AutoResume via SimBridge's outbound queue. Uses
|
||||
## 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.
|
||||
func _on_gameplay_occluded_auto_pause(occluded: bool) -> void:
|
||||
if occluded:
|
||||
SimBridge.send_named_action("AutoPause")
|
||||
else:
|
||||
SimBridge.send_named_action("AutoResume")
|
||||
|
||||
|
||||
func register(node: CanvasItem, group: String) -> void:
|
||||
if not _groups.has(group):
|
||||
_groups[group] = []
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
class_name TestHudGroupsAutoPause
|
||||
extends GdUnitTestSuite
|
||||
## T-970 (D-226 layer 1): HudGroups.gameplay_occluded must auto-pause/auto-resume
|
||||
## the sim via SimBridge.send_named_action("AutoPause"/"AutoResume").
|
||||
##
|
||||
## AutoPause/AutoResume are protocol-level requests, not InputMapper keybinds —
|
||||
## same mechanism as RequestBookmarkCatalog (send_named_action bypasses
|
||||
## action_enum_to_wire entirely, so there is nothing to map there). This suite
|
||||
## only covers the signal -> outbound-action wiring in hud_groups.gd; the
|
||||
## server-side pause reconciliation (Option A) is covered by Rust tests in
|
||||
## server/src/simulation/input.rs.
|
||||
|
||||
const TEST_APP_PATH := "implant/test_auto_pause"
|
||||
const OTHER_APP_PATH := "implant/test_auto_pause_other"
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
SimBridge.connect_to_sim() # test mode: immediately CONNECTED
|
||||
SimBridge._outbound_buffer.clear()
|
||||
HudGroups._active_app = ""
|
||||
HudGroups._active_mode = HudGroups.Mode.GAMEPLAY
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
HudGroups._active_app = ""
|
||||
HudGroups._active_mode = HudGroups.Mode.GAMEPLAY
|
||||
HudGroups._groups.erase(TEST_APP_PATH)
|
||||
HudGroups._groups.erase(OTHER_APP_PATH)
|
||||
SimBridge.disconnect_from_sim()
|
||||
SimBridge._outbound_buffer.clear()
|
||||
|
||||
|
||||
func _outbound_has_action(action_name: String) -> bool:
|
||||
for entry in SimBridge._outbound_buffer:
|
||||
if entry.get("action_name") == action_name:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func test_opening_fullscreen_app_sends_auto_pause() -> void:
|
||||
HudGroups.open_app(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
assert_bool(_outbound_has_action("AutoPause")).override_failure_message(
|
||||
"Entering a fullscreen implant app must send AutoPause"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_closing_fullscreen_app_sends_auto_resume() -> void:
|
||||
HudGroups.open_app(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
SimBridge._outbound_buffer.clear()
|
||||
HudGroups.close_app()
|
||||
assert_bool(_outbound_has_action("AutoResume")).override_failure_message(
|
||||
"Closing a fullscreen implant app must send AutoResume"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_insert_mode_does_not_send_auto_pause() -> void:
|
||||
# INSERT mode does not occlude gameplay (D-170) — gameplay_occluded never
|
||||
# fires, so no AutoPause should be sent.
|
||||
HudGroups.open_app(TEST_APP_PATH, HudGroups.Mode.INSERT)
|
||||
assert_bool(_outbound_has_action("AutoPause")).override_failure_message(
|
||||
"INSERT mode must not trigger AutoPause — gameplay is not occluded"
|
||||
).is_false()
|
||||
|
||||
|
||||
func test_switching_between_fullscreen_apps_does_not_resend_auto_pause() -> void:
|
||||
# D-170: switching from one fullscreen app to another must not re-emit
|
||||
# gameplay_occluded (occlusion state does not change) — no duplicate AutoPause.
|
||||
HudGroups.open_app(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
SimBridge._outbound_buffer.clear()
|
||||
HudGroups.open_app(OTHER_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
assert_bool(_outbound_has_action("AutoPause")).override_failure_message(
|
||||
"Switching between two fullscreen apps must not re-send AutoPause"
|
||||
).is_false()
|
||||
|
||||
|
||||
func test_toggle_app_closed_from_fullscreen_sends_auto_resume() -> void:
|
||||
# toggle_app(app) when already open calls close_app() internally —
|
||||
# exercises the same gameplay_occluded(false) path via a different entry point.
|
||||
HudGroups.open_app(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
SimBridge._outbound_buffer.clear()
|
||||
HudGroups.toggle_app(TEST_APP_PATH)
|
||||
assert_bool(_outbound_has_action("AutoResume")).override_failure_message(
|
||||
"toggle_app() closing a fullscreen app must send AutoResume"
|
||||
).is_true()
|
||||
Reference in New Issue
Block a user