Implement system menu guard opcode

This commit is contained in:
gamer147
2026-07-29 14:25:29 -04:00
parent 0c7d77805e
commit 2065a1c504
8 changed files with 156 additions and 12 deletions

View File

@@ -123,3 +123,4 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) |
| `0xa0ce4` | `run_state_flags` | `uint` | interpreter run-state flags (bit1 sleeping; 0x8000000 skip/fast-forward) |
| `0xa0ce8` | `timed_callback_abort_signal` | `int` | scheduler-polled abort/input signal cleared when op 0xd5 starts; redirects to abort PC when configured |
| `0xa0d10` | `system_menu_actions_enabled` | `int` | op 0x142 replaces this AGERC/IAGEService reentrancy guard; scene reset defaults it to 1, while CONFIG brackets its scripted settings UI with 0 then 1 |

View File

@@ -903,6 +903,36 @@ backing's earlier one-shot-color provenance, so classifying only animated or loo
rebuilt message window opaque. Post-bind static alpha now controls this fresh backing as well; repeated
CONFIG changes therefore take effect immediately while the enclosing ADV wait remains active.
### Native system-menu reentrancy guard — opcode `0x142` (2026-07-29)
`op_0x142_set_system_menu_actions_enabled@0x421fe0` is a direct one-operand setter for
`EngineCtx.system_menu_actions_enabled` (`ctx+0xa0d10`). `scene_context_init_reset@0x40b3b0` initializes
the field to one. CONFIG.BIN contains both shipped calls: `0x142(0)` is its first instruction, before any
settings data or UI is built, and `0x142(1)` runs after its last optional callback and immediately before
retained-object/surface/SFX teardown and exit.
The field is not consumed by the interpreter, renderer, gameplay logic, or persistence. AGE exposes it
through `AGE:IAGEService` virtual method `iage_service_query_system_menu_enabled@0x45f7c0` (vtable offset
`+0xb4`), which returns whether the field is nonzero. The shipped AGERC.DLL obtains that interface through
`GetClassObject("AGE:IAGEService")`. Its `agerc_refresh_system_menu_state@0x10002e10` calls the virtual
getter after refreshing the native menu's settings. A zero result applies `MF_GRAYED` through
`EnableMenuItem` to command ids `0x9c63`, `0x9c64`, `0x9c65`, `0x9c68`, `0x9c6c`, `0x9c6e`, and
`0x9c6f`; those entries launch settings/save-related callbacks and modal services that could otherwise
reenter configuration while CONFIG already owns the scripted settings UI.
The opcode is therefore a host-menu availability guard, not a configuration-persistence switch or input
lock for the game itself. A portable frontend without AGE's native AGERC menu can retain the flag for
lifetime/state parity and gate any future equivalent menu, with no immediate visual or headless effect.
It is an isolated implementation slice and has no dependency on the remaining opcode gaps. The handler,
IAGEService getter, AGERC menu refresher, and typed EngineCtx field are annotated in the saved Ghidra
programs.
The port retains the complete signed dword on `VirtualMachine`, initializes it to one, replaces it on each
`0x142` call, and restores one during opcode-`0x9` root scene-context reset. It deliberately does not call
an `IHost` service: the Godot frontend has no AGERC/native application menu to gray, and treating the flag
as a game-input or persistence switch would broaden its native effect. `SystemMenuActionsEnabled` exposes
the retained value for a future equivalent frontend menu.
The same route exposed an independent publication-atomicity requirement. Each tile step reaches
`FIELD@0x4eb2 -> 0x9225`, which calls `DRAWMAP`, `DRAWOBJ`, and `DRAWMINIMAP`. `DRAWMAP` first erases its
old terrain handle ranges and then rebuilds them; `DRAWMINIMAP` recreates mutable surfaces `0x42..0x44`

View File

@@ -1258,6 +1258,13 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x12e_find_hit_rectangle@0x428ff0 resolves the reference rectangle and three array operands as VM pointers, decodes rectangle/offset values with anti_tamper_a, and starts at incoming_index+1. Each candidate is [left,right,top,bottom]; the worker subtracts its per-entry x/y offsets from the pointer, inclusively intersects it with the reference rectangle, and writes the matched array index or -1 to operand 1. HISTORY.BIN uses decoded local arrays for scrollbar/control regions, close region 8, and visible text rows 9..13.
### 0x142 `set-system-menu-enabled` (set-system-menu-enabled, argc 1)
- **summary:** (enabled) - replace the EngineCtx flag exposed through IAGEService to gate reentrant native AGERC system-menu actions. Scene reset defaults it to 1; CONFIG writes 0 on entry and 1 immediately before teardown and exit.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x142_set_system_menu_actions_enabled@0x421fe0 directly stores operand 1 at EngineCtx+0xa0d10. scene_context_init_reset initializes the field to 1. iage_service_query_system_menu_enabled@0x45f7c0 exposes it as a boolean through the AGE:IAGEService vtable at +0xb4. Shipped /support/AGERC.DLL's agerc_refresh_system_menu_state@0x10002e10 calls that method; zero invokes EnableMenuItem(...,MF_GRAYED) for command ids 0x9c63/64/65/68/6c/6e/6f. CONFIG.BIN's only sites bracket its complete scripted settings UI with 0 then 1.
Port status (2026-07-29): implemented as an EngineCtx-lifetime signed-dword field on VirtualMachine. Root scene reload restores one. The current Godot frontend has no AGERC native system menu, so the value is retained for exact state parity and a future equivalent menu consumer without affecting game input or persisted settings.
### 0x144 `edit-fullwidth-string-dialog` (edit-fullwidth-string-dialog, argc 2)
- **summary:** (result_inout)(initial_text) - synchronously open AGERC's modal full-width text editor. Accept writes at most eight valid CP932 double-byte characters to operand 1; cancel leaves operand 1 unchanged. Operand 2 supplies the initial edit text and is preserved.
- **grounding:** source=investigation, confidence=high
@@ -1976,10 +1983,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting
- **summary:** —
- **grounding:** source=kelebek, confidence=med
### 0x142 `u0041FB10` (u0041FB10, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x145 `u00416040` (u00416040, argc 1)
- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it.
- **grounding:** source=kelebek, confidence=low

View File

@@ -982,6 +982,35 @@ build, clean diff checking, and the Himegari-targeted threaded `SELFTEST OK`.
**NEXT:** investigate CONFIG's two-site `0x142`; keep developer-only `0x24d` for a later dedicated
movie-mask compositor slice.
**SYSTEM-MENU GUARD `0x142` INVESTIGATION COMPLETE (2026-07-29):** the opcode directly replaces
`EngineCtx.system_menu_actions_enabled`. Scene reset defaults it to one. CONFIG's first instruction writes
zero; its second and final call restores one immediately before UI/surface/SFX teardown and exit.
AGE exposes the field through `AGE:IAGEService` virtual offset `+0xb4`. The shipped AGERC menu refresher
calls it and, when false, grays seven settings/save-related native menu actions so they cannot reenter
configuration while CONFIG owns the scripted settings screen. No interpreter, renderer, gameplay, or save
consumer reads the field. The port has no AGERC native menu, so the exact implementation is a small
EngineCtx-lifetime boolean retained for parity and available to gate any future host equivalent; it should
not block game input or change persisted options. The remaining inventory stays 5 opcodes / 7 instructions
until implementation.
**NEXT:** implement `0x142` as the isolated system-menu availability guard. Afterwards rerank the four
remaining opcodes, with SYSTEM4 singleton `0x149` the likely next native investigation.
**SYSTEM-MENU GUARD `0x142` IMPLEMENTED (2026-07-29):** the VM now retains AGE's complete signed-dword
field, starts it at one, replaces it on every call, and restores one on root scene-context reload.
CONFIG's exact `0 -> 1` bracket is handled without routing the flag into game input, options persistence,
or a synthetic host service. The public retained value is available if a future frontend grows an
equivalent application menu.
Three focused regressions cover the native default and signed replacement, CONFIG's exact pair, and
root-reload lifetime. Both shipped sites are closed, reducing the remaining effectful inventory to
4 opcodes / 5 instructions. Validation passes 498/498 engine tests, opcode and EngineCtx build/lint, a
zero-warning Godot build, clean diff checking, and the Himegari-targeted threaded `SELFTEST OK`.
**NEXT:** rerank the final four gaps and investigate SYSTEM4's singleton `0x149`; keep developer-only
`0x24d` for its dedicated movie-mask compositor slice.
## Later Phase B breadth
**INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static

View File

@@ -0,0 +1,66 @@
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
public class SystemMenuGuardOpcodeTests
{
private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson);
private static Operand I(long value) => new(0, value);
private static (int, Operand[]) Exit() => (0x2, Array.Empty<Operand>());
[Fact]
public void SetterDefaultsEnabledAndReplacesTheCompleteNativeDword()
{
Script config = ScriptAssembler.Assemble(Table, "CONFIG.BIN",
[
(0x142, [I(0)]),
(0x142, [I(-7)]),
Exit(),
], []);
var vm = new VirtualMachine(config, Table, new RecordingHost());
Assert.Equal(1, vm.SystemMenuActionsEnabled);
vm.Run();
Assert.Equal(-7, vm.SystemMenuActionsEnabled);
}
[Fact]
public void ConfigEntryAndExitPairRestoresNativeMenuActions()
{
Script config = ScriptAssembler.Assemble(Table, "CONFIG.BIN",
[
(0x142, [I(0)]),
(0x142, [I(1)]),
Exit(),
], []);
var vm = new VirtualMachine(config, Table, new RecordingHost());
vm.Run();
Assert.Equal(1, vm.SystemMenuActionsEnabled);
}
[Fact]
public void RootSceneReloadRestoresTheNativeDefault()
{
Script system4 = ScriptAssembler.Assemble(Table, "SYSTEM4.BIN",
[
Exit(),
], []);
Script config = ScriptAssembler.Assemble(Table, "CONFIG.BIN",
[
(0x142, [I(0)]),
(0x9, Array.Empty<Operand>()),
], []);
var provider = new MapProvider(new Dictionary<long, Script> { [0] = system4 });
var host = new RecordingHost();
var vm = new VirtualMachine(config, Table, host, provider: provider);
vm.Run();
Assert.Equal(1, vm.SystemMenuActionsEnabled);
Assert.Equal(1, host.SceneContextResets);
}
}

View File

@@ -88,6 +88,9 @@ public sealed class VirtualMachine
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
private int _messageWindowAlphaSetting;
private int _messageGlyphDelayMilliseconds;
// EngineCtx +0xa0d10: AGERC queries this through IAGEService to gray its native
// settings/save menu actions while CONFIG owns the scripted settings screen.
private int _systemMenuActionsEnabled = 1;
private readonly Dictionary<string, int> _valueSwitchTargets = new(StringComparer.Ordinal);
// Native EngineCtx owns 11 lazily allocated integer FIFOs at +0x55130. ATSEEK/MVSEEK use
// slot zero as their packed-coordinate flood-fill worklist; op 0x132 replaces a slot.
@@ -111,6 +114,7 @@ public sealed class VirtualMachine
public long Steps { get; private set; }
public bool AutoMessageEnabled => _autoMessageEnabled;
public bool MessageSkipEnabled => _messageSkipEnabled;
public int SystemMenuActionsEnabled => _systemMenuActionsEnabled;
public string PendingDiagnosticText => _diagnosticOutput.PendingText;
/// <summary>
/// Zero-based active-frame cutoff selected by opcode 0x1ad, or null when no surviving marker
@@ -781,6 +785,7 @@ public sealed class VirtualMachine
_advSkipServiceEnabled = false;
_advReadSkipState = false;
_advTextStyle = AdvTextStyle.Default;
_systemMenuActionsEnabled = 1;
TextHistory.SetRecordingEnabled(true);
_host.SetMessageSkipActive(false);
_host.SetPhysicalMessageSkipActive(false);
@@ -2334,6 +2339,10 @@ public sealed class VirtualMachine
_messageWindowAlphaSetting = (int)Read(a[0]);
_host.SetMessageWindowAlphaSetting(_messageWindowAlphaSetting);
return pc + 1;
case "set-system-menu-enabled": // 0x142: native AGERC menu reentrancy guard
case "u0041FB10": // pre-reference compatibility
_systemMenuActionsEnabled = unchecked((int)Read(a[0]));
return pc + 1;
case "get-message-glyph-delay": // 0x7f
case "u00414C60":
Write(a[0], _messageGlyphDelayMilliseconds); return pc + 1;

View File

@@ -592,3 +592,8 @@ offset = 0xa0ce8
name = "timed_callback_abort_signal"
type = "int"
note = "scheduler-polled abort/input signal cleared when op 0xd5 starts; redirects to abort PC when configured"
[[field]]
offset = 0xa0d10
name = "system_menu_actions_enabled"
type = "int"
note = "op 0x142 replaces this AGERC/IAGEService reentrancy guard; scene reset defaults it to 1, while CONFIG brackets its scripted settings UI with 0 then 1"

View File

@@ -2964,23 +2964,24 @@ observed_types = ["l-int"]
[[opcode]]
op = 0x142
label = "u0041FB10"
label = "set-system-menu-enabled"
argc = 1
abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "u0041FB10"
category = "unknown"
summary = ""
name = "set-system-menu-enabled"
category = "input"
summary = "(enabled) - replace the EngineCtx flag exposed through IAGEService to gate reentrant native AGERC system-menu actions. Scene reset defaults it to 1; CONFIG writes 0 on entry and 1 immediately before teardown and exit."
details = "Port status (2026-07-29): implemented as an EngineCtx-lifetime signed-dword field on VirtualMachine. Root scene reload restores one. The current Godot frontend has no AGERC native system menu, so the value is retained for exact state parity and a future equivalent menu consumer without affecting game input or persisted settings."
noop_headless = false
source = "kelebek"
confidence = "low"
source = "investigation"
confidence = "high"
depends_on = []
evidence = ""
evidence = "Ghidra /v2: op_0x142_set_system_menu_actions_enabled@0x421fe0 directly stores operand 1 at EngineCtx+0xa0d10. scene_context_init_reset initializes the field to 1. iage_service_query_system_menu_enabled@0x45f7c0 exposes it as a boolean through the AGE:IAGEService vtable at +0xb4. Shipped /support/AGERC.DLL's agerc_refresh_system_menu_state@0x10002e10 calls that method; zero invokes EnableMenuItem(...,MF_GRAYED) for command ids 0x9c63/64/65/68/6c/6e/6f. CONFIG.BIN's only sites bracket its complete scripted settings UI with 0 then 1."
[[opcode.semantics.args]]
i = 1
role = ""
role = "native system-menu actions enabled (zero disables; nonzero enables)"
observed_types = ["imm"]
[[opcode]]