Implement native exit request control flow
This commit is contained in:
@@ -278,18 +278,33 @@ been proven; only recording suppression is reset. Focused tests cover a three-fr
|
||||
resolution to `SYSTEM4.BIN`.
|
||||
|
||||
**Frontend exit request is not a root reload (`0x1`, 2026-07-20).** Native
|
||||
`op_0x1_throw_exit_request@0x4162e0` constructs reason value one and raises the engine's non-returning C++
|
||||
control exception (`DAT_005a9710`). Its only two corpus sites establish the intent: TITLE executes it after
|
||||
the fifth main-menu action's sound and delay, while SYSTEM4 executes it after reporting an invalid execution
|
||||
mode. The Windows/frontend catch policy—full exit versus returning to title—is outside this opcode handler
|
||||
and is not implemented in the Godot frontend yet.
|
||||
`op_0x1_throw_exit_request@0x4162e0` constructs a four-byte value-one payload and throws it with
|
||||
`ThrowInfo@0x5a9710`. The catchable-type metadata resolves that object to the named RTTI type
|
||||
`Command_Exit_Exception` (`TypeDescriptor@0x5b13e8`), rather than an undifferentiated integer exception.
|
||||
Its only two corpus sites establish the intent: TITLE executes it after the fifth main-menu action's sound
|
||||
and delay, while SYSTEM4 executes it after reporting an invalid execution mode.
|
||||
|
||||
The catcher is the outer native message/scheduler loop now recovered as
|
||||
`engine_main_tick_with_exception_policy@0x411840`. Its MSVC `FuncInfo@0x5a9750` has a typed catch entry for
|
||||
`Command_Exit_Exception` at `catch_CommandExitException_set_exit_result@0x412648`. That funclet forces the
|
||||
enclosing result to one and returns continuation `0x412961`, which performs loop teardown and returns to the
|
||||
frontend. It never changes `frame_pc`. This matters because the dispatcher itself advances `frame_pc` only
|
||||
after an opcode handler returns; `0x1` throws instead, so neither the dispatcher nor the catcher advances
|
||||
past it. A distinct generic error-dialog policy at `0x412689` proves that fall-through is explicit: result
|
||||
two retries the same instruction, result four adds the decoded instruction length before restarting the
|
||||
loop, and other results exit. `Command_Exit_Exception` bypasses that policy entirely.
|
||||
|
||||
The Windows/frontend policy that follows the returned exit request—full exit versus returning to
|
||||
title—is outside this opcode handler and is not implemented in the Godot frontend yet.
|
||||
|
||||
TITLE happens to contain a developer menu immediately after its `0x1`, including a `call-script` to
|
||||
`DEBUG.BIN`; that code is unreachable in the native flow because the handler never returns. The port still
|
||||
treats unknown `0x1` as a fall-through stub, so selecting the fifth TITLE action would expose that menu by
|
||||
accident. This is a known discrepancy, not a legitimate route for validating `0x9`. End-to-end visual
|
||||
validation of the native `SYSTEM4 -> TITLE -> child -> 0x9 -> SYSTEM4 -> TITLE` history therefore remains
|
||||
deferred until the frontend exit/return-to-title boundary or a natural game-over/completion route exists.
|
||||
`DEBUG.BIN`; that code is unreachable in the native flow because the handler never returns. The port's
|
||||
former unknown-op fallback did expose that menu when selecting TITLE's fifth action, providing a useful
|
||||
visual confirmation of the static mapping but not a legitimate retail route. Opcode `0x1` now propagates a
|
||||
process-exit request through hotspot callbacks and nested script frames and ends the VM session without
|
||||
executing the following bytecode. End-to-end visual validation of the native
|
||||
`SYSTEM4 -> TITLE -> child -> 0x9 -> SYSTEM4 -> TITLE` history remains deferred until the frontend
|
||||
exit/return-to-title boundary or a natural game-over/completion route exists.
|
||||
|
||||
The unreachable developer menu nevertheless records the game's intended debug-scene handoff. Its two ADV
|
||||
viewer choices write `G[0]=1`, `G[0xaba5c]=-1`, `G[0x62ccf]=0`, and a raw script id into `G[0x699]`, then
|
||||
|
||||
@@ -199,9 +199,9 @@ Operand 2 names the base cell itself: a global-bank operand produces a global re
|
||||
## control
|
||||
|
||||
### 0x1 `throw-exit-request` (throw-exit-request, argc 0)
|
||||
- **summary:** () - raise the engine's non-returning exit/fatal-abort control exception with reason value 1. TITLE uses it for the fifth main-menu action; SYSTEM4 uses it after reporting an invalid execution mode.
|
||||
- **summary:** () - raise Command_Exit_Exception with result value 1. The outer engine loop catches it, exits without advancing the VM PC, and returns control to the frontend. TITLE uses it for the fifth main-menu action; SYSTEM4 uses it after reporting an invalid execution mode.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x1_throw_exit_request@0x4162e0 constructs local value 1 and calls __CxxThrowException_8 with type descriptor DAT_005a9710; the handler is non-returning. Corpus has exactly two sites: TITLE@0x393 after the fifth menu action's sound/sleep, and SYSTEM4@0x5b9 after printing 'invalid execution mode'. TITLE bytecode following 0x1 builds a developer debug menu and can only be reached when a port incorrectly treats 0x1 as a fall-through stub. The frontend catch/prompt policy remains a separate unimplemented boundary.
|
||||
- **evidence:** Ghidra /v2: op_0x1_throw_exit_request@0x4162e0 constructs a four-byte payload with value 1 and calls __CxxThrowException_8 with ThrowInfo 0x5a9710. Its CatchableTypeArray resolves to RTTI TypeDescriptor 0x5b13e8, `.?AVCommand_Exit_Exception@@`. The outer engine_main_tick_with_exception_policy@0x411840 has MSVC FuncInfo 0x5a9750; its typed catch entry maps Command_Exit_Exception to catch_CommandExitException_set_exit_result@0x412648, which forces result 1 and returns continuation 0x412961 for loop teardown/return. The dispatcher advances frame_pc only after a handler returns, and this catch never advances it. A separate generic error-dialog result-4 path at 0x412928 explicitly advances the PC, proving fall-through is opt-in and does not apply here. Corpus has exactly two sites: TITLE@0x393 after the fifth menu action's sound/sleep, and SYSTEM4@0x5b9 after printing 'invalid execution mode'. TITLE bytecode following 0x1 builds a developer debug menu and is unreachable in the retail native path. The frontend full-exit/return-to-title prompt remains a separate unimplemented boundary.
|
||||
|
||||
### 0x3 `call-script` (call-script, argc 1)
|
||||
- **summary:** load & call another SYS4 script by id; id = RAW index into the SYS4INI file table (asset-index). Pushes a script frame; returns to caller when the callee ends.
|
||||
|
||||
@@ -148,8 +148,9 @@ to SYSTEM4; op `0x9` performs a tested whole-stack reload of SYSTEM4; selected g
|
||||
survive; and scene-owned presentation/input state is released. Manual validation of a natural gameplay
|
||||
route through the first `0x9` remains deferred: Himegari's readily accessible return-to-title choice belongs
|
||||
to the still-unimplemented frontend exit-request policy, while the other known natural paths require later
|
||||
gameplay, game over, or completion. Do not use TITLE's currently exposed post-`0x1` developer-menu
|
||||
fallthrough as evidence; native `0x1` is non-returning. See `docs/engine-re.md`.
|
||||
gameplay, game over, or completion. Do not use TITLE's post-`0x1` developer menu as evidence; native `0x1`
|
||||
is non-returning, and the port now propagates that exit request instead of falling through into the hidden
|
||||
bytecode. See `docs/engine-re.md`.
|
||||
|
||||
**Godot debug scene launcher (2026-07-20; implemented and manually validated).** The first version
|
||||
is deliberately narrower than arbitrary hot swapping:
|
||||
|
||||
Reference in New Issue
Block a user