Implement system menu show delay opcodes
This commit is contained in:
@@ -61,6 +61,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
| `0x55100` | `adv_skip_service_enabled` | `int` | ADV presentation lifecycle gate: op 0x19b clears it while preserving the persistent Skip toggle; op 0x19c sets it before recomputing active message/read skip |
|
||||
| `0x55104` | `auto_message_enabled` | `int` | ADV Auto mode flag; op 0x1b6 reads, op 0x1b7 writes, adv_input_service_poll consumes |
|
||||
| `0x55110` | `text_history_recording_suppressed` | `uint` | high bit suppresses ADV retained-history index/text/metadata/voice recording; op 0x1bb writes 0x80000000 or zero, and HISTORY.BIN brackets itself with disable/enable |
|
||||
| `0x5511c` | `system_menu_show_delay_ms` | `uint` | op 0x149 replaces the TIMER_SHOWMENU top-edge dwell threshold; reset defaults to 0 and SYSTEM4 sets 1000; op 0x148 reads it |
|
||||
| `0x55120` | `anti_tamper_a` | `int` | anti-tamper checksum operand |
|
||||
| `0x55124` | `anti_tamper_b` | `int` | anti-tamper checksum operand |
|
||||
| `0x5512c` | `anti_tamper_fp` | `int` | anti-tamper (import fn ptr / result) |
|
||||
|
||||
@@ -933,6 +933,47 @@ an `IHost` service: the Godot frontend has no AGERC/native application menu to g
|
||||
as a game-input or persistence switch would broaden its native effect. `SystemMenuActionsEnabled` exposes
|
||||
the retained value for a future equivalent frontend menu.
|
||||
|
||||
### System-menu top-edge reveal delay — opcodes `0x148` / `0x149` (2026-07-29)
|
||||
|
||||
`op_0x149_set_system_menu_show_delay@0x422050` replaces the complete dword at
|
||||
`EngineCtx.system_menu_show_delay_ms` (`ctx+0x5511c`); the paired
|
||||
`op_0x148_get_system_menu_show_delay@0x429c70` writes it to its output operand. Scene reset initializes
|
||||
the field to zero. Himegari never calls the getter and has one setter site: SYSTEM4 writes 1000
|
||||
milliseconds immediately before the unrelated `0x248(128)` tiled-surface configuration.
|
||||
|
||||
The only non-opcode consumer is `system_menu_top_edge_dwell_timer_tick@0x486280`, reached from the main
|
||||
Win32 window procedure's `WM_TIMER` id 1 and repeated top-edge `WM_MOUSEMOVE` path. A pointer at client
|
||||
y=0 or y=1 starts a 100-millisecond polling timer and captures `timeGetTime`; leaving those two rows
|
||||
clears the timestamp and kills the timer. The tick reveals the menu only when:
|
||||
|
||||
- unsigned elapsed time is strictly greater than `system_menu_show_delay_ms`;
|
||||
- neither the left nor right mouse button is held;
|
||||
- `display:ScreenMode` is windowed; and
|
||||
- AGERC host command `-1` reports no active host UI.
|
||||
|
||||
The unsigned comparison preserves the setter's complete dword: a negative script value becomes a very
|
||||
long effective delay rather than being clamped. The independent Alt-key path calls the same reveal worker
|
||||
immediately and does not consult this field.
|
||||
|
||||
`system_menu_strip_show@0x407bd0` prepares/suspends presentation, marks `TIMER_SHOWMENU` active, and calls
|
||||
AGERC host command 4. In the shipped DLL, `agerc_system_menu_strip_dialog_proc@0x10003c20` owns that
|
||||
modeless top strip: it copies the AGE window's title and icon, sizes itself to the screen width and one
|
||||
menu-bar height, refreshes the settings/save action state described for opcode `0x142`, and dispatches its
|
||||
menu commands. `agerc_system_menu_strip_hide@0x10002a40` hides the retained window and clears AGERC bit
|
||||
`0x10`, allowing a later dwell to reveal it again.
|
||||
|
||||
The port has no equivalent host application-menu strip, so this is an isolated scene-context state slice:
|
||||
retain the unsigned value, reset it to zero on root scene reload, expose the exact getter, and leave game
|
||||
input, the script-owned right-click menu, and settings persistence unchanged. The handlers, timer/reveal
|
||||
workers, EngineCtx field, AGERC dispatcher/dialog/hide workers, and key globals are named/commented in the
|
||||
saved Ghidra programs.
|
||||
|
||||
The VM implementation retains the field as a `uint`, sign-extends the getter's dword into the existing
|
||||
script integer-cell representation, and restores zero in the opcode-`0x9` scene reset. It exposes
|
||||
`SystemMenuShowDelayMilliseconds` for a future frontend consumer but deliberately makes no `IHost` call.
|
||||
Focused coverage includes SYSTEM4's 1000-millisecond setting/getter pair, a high-bit dword round trip, and
|
||||
root-reload lifetime.
|
||||
|
||||
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`
|
||||
|
||||
@@ -1281,6 +1281,20 @@ LineEdit, keeps it open on either native validation error, and resumes the worke
|
||||
or cancel. Accept replaces operand 1 and cancel leaves it untouched; operand 2 is never modified.
|
||||
|
||||
|
||||
### 0x148 `get-system-menu-show-delay` (get-system-menu-show-delay, argc 1)
|
||||
- **summary:** (out) - return the current unsigned TIMER_SHOWMENU top-edge dwell threshold paired with opcode 0x149.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x148_get_system_menu_show_delay@0x429c70 writes EngineCtx+0x5511c to operand 1. Scene reset initializes the field to 0, opcode 0x149 replaces it, and the windowed top-edge TIMER_SHOWMENU path consumes it as an unsigned millisecond threshold. This getter is not observed in Himegari's corpus.
|
||||
|
||||
Port status (2026-07-29): implemented with opcode 0x149. The getter writes the retained dword through the VM's signed integer-cell representation so high-bit values preserve the same native 32 bits.
|
||||
|
||||
### 0x149 `set-system-menu-show-delay` (set-system-menu-show-delay, argc 1)
|
||||
- **summary:** (milliseconds) - replace the unsigned dwell threshold before pointer hover at the top two client rows reveals AGERC's modeless system-menu strip. Scene reset defaults to 0; SYSTEM4 sets 1000.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x149_set_system_menu_show_delay@0x422050 directly stores operand 1 at EngineCtx+0x5511c; scene_context_init_reset writes 0 and paired op_0x148_get_system_menu_show_delay@0x429c70 reads it. system_menu_top_edge_dwell_timer_tick@0x486280 compares the field unsigned against timeGetTime elapsed while WM_MOUSEMOVE is in client y=0..1, then requires no left/right button, windowed ScreenMode, and g_pfnAgercHostCallback(-1)==0 before system_menu_strip_show@0x407bd0 invokes AGERC command 4. The handler uses the complete dword. SYSTEM4's sole site sets 1000 immediately before the unrelated tiled-surface edge setting.
|
||||
|
||||
Port status (2026-07-29): implemented as unsigned scene-context state. The setter preserves all 32 operand bits, root scene reload restores zero, and the current Godot frontend deliberately adds no host effect because it has no AGERC application-menu strip.
|
||||
|
||||
### 0x19a `get-message-skip` (u00414E50, argc 1)
|
||||
- **summary:** (out) - return the current all-message skip state set by op 0x88.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -1998,15 +2012,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
- **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table.
|
||||
|
||||
### 0x148 `u004160A0` (u004160A0, argc 1)
|
||||
- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it.
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
- **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table.
|
||||
|
||||
### 0x149 `u0041FCE0` (u0041FCE0, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x14a `u0041FD10` (u0041FD10, argc 7)
|
||||
- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it.
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -1011,6 +1011,40 @@ zero-warning Godot build, clean diff checking, and the Himegari-targeted threade
|
||||
**NEXT:** rerank the final four gaps and investigate SYSTEM4's singleton `0x149`; keep developer-only
|
||||
`0x24d` for its dedicated movie-mask compositor slice.
|
||||
|
||||
**SYSTEM-MENU SHOW DELAY `0x149` INVESTIGATION COMPLETE (2026-07-29):** the opcode replaces the unsigned
|
||||
millisecond threshold at `EngineCtx.system_menu_show_delay_ms`; scene reset defaults it to zero and
|
||||
SYSTEM4's sole call sets 1000. The exact, unobserved paired getter is opcode `0x148`.
|
||||
|
||||
AGE's Win32 window procedure starts a 100-millisecond timer while the pointer dwells in the top two client
|
||||
rows. Once unsigned elapsed time is strictly greater than the configured threshold, with no mouse button
|
||||
held, windowed ScreenMode, and no active AGERC host UI, it reveals AGERC's modeless full-width system-menu
|
||||
strip. Alt reveals the same strip immediately and bypasses the threshold. This is the same settings/save
|
||||
menu whose actions opcode `0x142` guards against CONFIG reentrancy, but it is unrelated to the adjacent
|
||||
`0x248` graphics configuration.
|
||||
|
||||
The current Godot frontend has no equivalent application-menu strip. The bounded implementation is exact
|
||||
state parity: retain the full unsigned dword, reset it to zero with scene context, implement the paired
|
||||
getter, and expose it for a future host strip without changing game input, the script-authored right-click
|
||||
menu, or persistence. This investigation changes metadata only, so the inventory remains 4 opcodes /
|
||||
5 instructions. The `/v2` and `/support/AGERC.DLL` handlers, reveal/timer/dialog paths, typed EngineCtx
|
||||
field, and key globals are annotated and saved.
|
||||
|
||||
**NEXT:** implement the exact `0x148`/`0x149` state pair. Afterwards rerank `0x137`, `0x241`, and the two
|
||||
developer-only `0x24d` sites.
|
||||
|
||||
**SYSTEM-MENU SHOW DELAY `0x148`/`0x149` IMPLEMENTED (2026-07-29):** the VM now retains the complete
|
||||
unsigned threshold, returns the same native dword through signed script integer cells, and restores zero
|
||||
on root scene-context reload. SYSTEM4's 1000-millisecond setup is handled without inventing an `IHost`
|
||||
effect for an AGERC strip the Godot frontend does not have.
|
||||
|
||||
Three focused regressions cover the exact SYSTEM4 setter/getter sequence, high-bit dword preservation, and
|
||||
root-reload lifetime. The shipped setter site is closed; the unobserved getter is also available for
|
||||
broader AGE compatibility. The remaining effectful inventory is 3 opcodes / 4 instructions. Validation
|
||||
passes 501/501 engine tests, opcode and EngineCtx build/lint, a zero-warning Godot build, clean diff
|
||||
checking, and the Himegari-targeted threaded `SELFTEST OK`.
|
||||
|
||||
**NEXT:** rerank `0x137`, `0x241`, and developer-only `0x24d` ×2 before choosing the next slice.
|
||||
|
||||
## Later Phase B breadth
|
||||
|
||||
**INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static
|
||||
|
||||
71
engine/Age.Engine.Tests/SystemMenuShowDelayOpcodeTests.cs
Normal file
71
engine/Age.Engine.Tests/SystemMenuShowDelayOpcodeTests.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
|
||||
public class SystemMenuShowDelayOpcodeTests
|
||||
{
|
||||
private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
private static Operand I(long value) => new(0, value);
|
||||
private static Operand G(long address) => new(3, address);
|
||||
private static (int, Operand[]) Exit() => (0x2, Array.Empty<Operand>());
|
||||
|
||||
[Fact]
|
||||
public void System4DelayRoundTripsThroughThePairedGetter()
|
||||
{
|
||||
Script system4 = ScriptAssembler.Assemble(Table, "SYSTEM4.BIN",
|
||||
[
|
||||
(0x149, [I(1000)]),
|
||||
(0x148, [G(0x100)]),
|
||||
Exit(),
|
||||
], []);
|
||||
var vm = new VirtualMachine(system4, Table, new RecordingHost());
|
||||
|
||||
Assert.Equal(0u, vm.SystemMenuShowDelayMilliseconds);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(1000u, vm.SystemMenuShowDelayMilliseconds);
|
||||
Assert.Equal(1000, vm.Globals[0x100]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetterAndGetterPreserveTheCompleteNativeDword()
|
||||
{
|
||||
Script system4 = ScriptAssembler.Assemble(Table, "SYSTEM4.BIN",
|
||||
[
|
||||
(0x149, [I(0x80000001L)]),
|
||||
(0x148, [G(0x100)]),
|
||||
Exit(),
|
||||
], []);
|
||||
var vm = new VirtualMachine(system4, Table, new RecordingHost());
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(0x80000001u, vm.SystemMenuShowDelayMilliseconds);
|
||||
Assert.Equal(unchecked((int)0x80000001u), vm.Globals[0x100]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RootSceneReloadRestoresTheZeroDefault()
|
||||
{
|
||||
Script reloadedSystem4 = ScriptAssembler.Assemble(Table, "SYSTEM4.BIN",
|
||||
[
|
||||
(0x148, [G(0x100)]),
|
||||
Exit(),
|
||||
], []);
|
||||
Script initial = ScriptAssembler.Assemble(Table, "INITIAL",
|
||||
[
|
||||
(0x149, [I(1000)]),
|
||||
(0x9, Array.Empty<Operand>()),
|
||||
], []);
|
||||
var provider = new MapProvider(new Dictionary<long, Script> { [0] = reloadedSystem4 });
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(initial, Table, host, provider: provider);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(0u, vm.SystemMenuShowDelayMilliseconds);
|
||||
Assert.Equal(0, vm.Globals[0x100]);
|
||||
Assert.Equal(1, host.SceneContextResets);
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,9 @@ public sealed class VirtualMachine
|
||||
// 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;
|
||||
// EngineCtx +0x5511c: unsigned TIMER_SHOWMENU dwell threshold. Native op 0x148
|
||||
// returns the same dword through the VM's signed integer-cell representation.
|
||||
private uint _systemMenuShowDelayMilliseconds;
|
||||
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.
|
||||
@@ -115,6 +118,7 @@ public sealed class VirtualMachine
|
||||
public bool AutoMessageEnabled => _autoMessageEnabled;
|
||||
public bool MessageSkipEnabled => _messageSkipEnabled;
|
||||
public int SystemMenuActionsEnabled => _systemMenuActionsEnabled;
|
||||
public uint SystemMenuShowDelayMilliseconds => _systemMenuShowDelayMilliseconds;
|
||||
public string PendingDiagnosticText => _diagnosticOutput.PendingText;
|
||||
/// <summary>
|
||||
/// Zero-based active-frame cutoff selected by opcode 0x1ad, or null when no surviving marker
|
||||
@@ -786,6 +790,7 @@ public sealed class VirtualMachine
|
||||
_advReadSkipState = false;
|
||||
_advTextStyle = AdvTextStyle.Default;
|
||||
_systemMenuActionsEnabled = 1;
|
||||
_systemMenuShowDelayMilliseconds = 0;
|
||||
TextHistory.SetRecordingEnabled(true);
|
||||
_host.SetMessageSkipActive(false);
|
||||
_host.SetPhysicalMessageSkipActive(false);
|
||||
@@ -2343,6 +2348,14 @@ public sealed class VirtualMachine
|
||||
case "u0041FB10": // pre-reference compatibility
|
||||
_systemMenuActionsEnabled = unchecked((int)Read(a[0]));
|
||||
return pc + 1;
|
||||
case "get-system-menu-show-delay": // 0x148: paired TIMER_SHOWMENU getter
|
||||
case "u004160A0": // pre-reference compatibility
|
||||
Write(a[0], unchecked((int)_systemMenuShowDelayMilliseconds));
|
||||
return pc + 1;
|
||||
case "set-system-menu-show-delay": // 0x149: top-edge dwell threshold in milliseconds
|
||||
case "u0041FCE0": // pre-reference compatibility
|
||||
_systemMenuShowDelayMilliseconds = unchecked((uint)Read(a[0]));
|
||||
return pc + 1;
|
||||
case "get-message-glyph-delay": // 0x7f
|
||||
case "u00414C60":
|
||||
Write(a[0], _messageGlyphDelayMilliseconds); return pc + 1;
|
||||
|
||||
@@ -283,6 +283,11 @@ name = "text_history_recording_suppressed"
|
||||
type = "uint"
|
||||
note = "high bit suppresses ADV retained-history index/text/metadata/voice recording; op 0x1bb writes 0x80000000 or zero, and HISTORY.BIN brackets itself with disable/enable"
|
||||
[[field]]
|
||||
offset = 0x5511c
|
||||
name = "system_menu_show_delay_ms"
|
||||
type = "uint"
|
||||
note = "op 0x149 replaces the TIMER_SHOWMENU top-edge dwell threshold; reset defaults to 0 and SYSTEM4 sets 1000; op 0x148 reads it"
|
||||
[[field]]
|
||||
offset = 0x55120
|
||||
name = "anti_tamper_a"
|
||||
type = "int"
|
||||
|
||||
@@ -3054,23 +3054,24 @@ observed_types = ["l-str"]
|
||||
|
||||
[[opcode]]
|
||||
op = 0x149
|
||||
label = "u0041FCE0"
|
||||
label = "set-system-menu-show-delay"
|
||||
argc = 1
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u0041FCE0"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "set-system-menu-show-delay"
|
||||
category = "input"
|
||||
summary = "(milliseconds) - replace the unsigned dwell threshold before pointer hover at the top two client rows reveals AGERC's modeless system-menu strip. Scene reset defaults to 0; SYSTEM4 sets 1000."
|
||||
details = "Port status (2026-07-29): implemented as unsigned scene-context state. The setter preserves all 32 operand bits, root scene reload restores zero, and the current Godot frontend deliberately adds no host effect because it has no AGERC application-menu strip."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = ""
|
||||
evidence = "Ghidra /v2: op_0x149_set_system_menu_show_delay@0x422050 directly stores operand 1 at EngineCtx+0x5511c; scene_context_init_reset writes 0 and paired op_0x148_get_system_menu_show_delay@0x429c70 reads it. system_menu_top_edge_dwell_timer_tick@0x486280 compares the field unsigned against timeGetTime elapsed while WM_MOUSEMOVE is in client y=0..1, then requires no left/right button, windowed ScreenMode, and g_pfnAgercHostCallback(-1)==0 before system_menu_strip_show@0x407bd0 invokes AGERC command 4. The handler uses the complete dword. SYSTEM4's sole site sets 1000 immediately before the unrelated tiled-surface edge setting."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "unsigned top-edge system-menu dwell threshold in milliseconds"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -8831,20 +8832,25 @@ evidence = "Not observed in Himegari's script corpus; ABI label/argc come from K
|
||||
|
||||
[[opcode]]
|
||||
op = 0x148
|
||||
label = "u004160A0"
|
||||
label = "get-system-menu-show-delay"
|
||||
argc = 1
|
||||
observed_in_himegari = false
|
||||
abi_source = "kelebek"
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u004160A0"
|
||||
category = "unknown"
|
||||
summary = "Broader AGE-catalog compatibility stub; the port currently traces and skips it."
|
||||
name = "get-system-menu-show-delay"
|
||||
category = "input"
|
||||
summary = "(out) - return the current unsigned TIMER_SHOWMENU top-edge dwell threshold paired with opcode 0x149."
|
||||
details = "Port status (2026-07-29): implemented with opcode 0x149. The getter writes the retained dword through the VM's signed integer-cell representation so high-bit values preserve the same native 32 bits."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table."
|
||||
evidence = "Ghidra /v2: op_0x148_get_system_menu_show_delay@0x429c70 writes EngineCtx+0x5511c to operand 1. Scene reset initializes the field to 0, opcode 0x149 replaces it, and the windowed top-edge TIMER_SHOWMENU path consumes it as an unsigned millisecond threshold. This getter is not observed in Himegari's corpus."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = "out: unsigned top-edge system-menu dwell threshold in milliseconds"
|
||||
|
||||
[[opcode]]
|
||||
op = 0x14a
|
||||
|
||||
Reference in New Issue
Block a user