Implement system menu guard opcode
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
66
engine/Age.Engine.Tests/SystemMenuGuardOpcodeTests.cs
Normal file
66
engine/Age.Engine.Tests/SystemMenuGuardOpcodeTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user