diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index 6291c46..5f2f956 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -39,6 +39,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x552e8` | `ret_stack_b` | `void*` | per-frame return stack (companion) | | `0x5f304` | `sleep_timer` | `int` | sleep timer object (op 0xc8; +8 active, +0x14 start-ms, +0x18 duration) | | `0x5f3ac` | `timed_callback_timer` | `int` | elapsed timer object armed by op 0xd5 and sampled by the timed callback scheduler | +| `0x5f64c` | `adv_wait_indicator_timer` | `int` | elapsed timer started by op 0x1ce and ADV wait setup to select the animated input-wait indicator frame | | `0x5f690` | `timed_callback_abort_pc` | `int` | optional local fallback PC supplied to op 0xd5; -1 disables abort redirection | | `0x5f694` | `timed_callback_entries_vector` | `int` | MSVC vector object base for 16-byte schedule entries; begin/end/capacity pointers follow | | `0x5f698` | `timed_callback_entries_begin` | `void*` | begin pointer for 16-byte entries {deadline_ms, field_100, primary_pc, catchup_pc} | @@ -48,8 +49,12 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x5f6a8` | `timed_callback_cursor` | `int` | next schedule entry consumed by timed_callback_sequence_tick; op 0xd3 resets to zero | | `0x5f6b4` | `timed_callback_script_resource_id` | `uint` | active frame script resource retained by op 0xd5 for local callback dispatch safety | | `0x5f6b8` | `timed_callback_script_resource_guard` | `uint` | companion current-script identity checked before redirecting to a scheduled callback | +| `0x5f72c` | `adv_wait_indicator_frame` | `int` | current animated input-wait indicator frame, reset to zero when the service is armed | +| `0x5f730` | `adv_wait_indicator_terminal_frame` | `int` | terminal frame returned by the indicator layout during ADV wait setup | +| `0x5f734` | `adv_wait_indicator_enabled_value` | `int` | raw enable operand last written by op 0x1ce; nonzero arms the ADV wait-indicator service | | `0x6da78` | `message_skip_activation_guard` | `int` | set when op 0x88 enables Skip; suppresses the enabling click from immediately becoming a hotspot activation | | `0x6da80` | `message_skip_cancel_click_phase` | `int` | press/release phase used only when set:CancelMesSkipOnClick is nonzero | +| `0x6da84` | `adv_wait_indicator_layout_slot` | `int` | ADV text layout whose wait indicator is armed or erased by input-wait setup and op 0x1ce | | `0x6da88` | `coroutine_yield_a` | `void*` | op 0x7b yield-state save (op1 -> +ctxidx*4) | | `0x6db28` | `coroutine_yield_b` | `void*` | op 0x7b yield-state save (op2 -> +ctxidx*4) | | `0x6dbc8` | `coroutine_runstate` | `int` | op 0x7c resume gate (run-state bit 0x2000000) | diff --git a/docs/engine-re.md b/docs/engine-re.md index 7dc1041..7c698b1 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -1725,6 +1725,46 @@ saved. --- +### ADV text publication and wait-indicator service -- opcodes `0x1ce` / `0x20a` (2026-07-20) + +The earlier working label "sprite-animation service" was too broad. These operations belong specifically +to ADV text publication and the animated input-wait indicator (the bouncing marker shown after a page +finishes typing). They do not expose a generic retained-object animation channel. + +`op_0x1ce_set_adv_wait_indicator_enabled@0x41f8c0` stores its operand at +`EngineCtx.adv_wait_indicator_enabled_value` (`ctx+0x5f734`). A nonzero value sets run-state bit +`0x40000000`, resets `adv_wait_indicator_frame` (`ctx+0x5f72c`) to zero, and starts +`adv_wait_indicator_timer` (`ctx+0x5f64c`). Zero erases the indicator belonging to +`adv_wait_indicator_layout_slot` (`ctx+0x6da84`) with the worker's frame `-2` mode and clears the run bit. +Every Himegari corpus use is `0x1ce(0)`, normally at entry to a modal script; `HISTORY.BIN@0x3` is the +concrete route relevant here. + +`op_0x20a_publish_adv_text_layout@0x422ce0` first calls +`adv_text_publish_layout@0x450c80`. Slot zero selects the text manager's current layout; a nonzero operand +selects that indexed layout. The worker erases the layout's old retained-object range, walks its 20-byte +text records, and rebinds their draw objects. When run-state bit `0x40000000` is active, the opcode also +calls `adv_text_publish_wait_indicator_frame@0x453120` with the current indicator frame. The latter worker +uses frame `-1` to return/capture the terminal frame, `-2` to erase the indicator object, and a +nonnegative frame to choose and bind the corresponding atlas cell. All corpus uses are `0x20a(1)`; +`HISTORY.BIN@0x13ab` reaches it through the shared ADV redraw callback. + +Opcode `0x72` owns the normal implicit start of the same service: it records the waiting layout, captures +the indicator's terminal frame, resets the active frame to zero, starts the timer, and raises run-state bit +`0x40000000`. Completing the input wait erases the object and clears the bit. In the original interpreter, +return from a nested modal script flows through that shared redraw/wait path and re-arms the parent marker. +The port's host blocks inside the enclosing wait while servicing the nested HISTORY callback, so it restores +the previously enabled parent marker at the callback-return boundary. This is lifecycle equivalence, not a +script-specific seed or global-state workaround. + +The port exposes the two explicit operations as host services: `0x1ce` controls marker eligibility and +`0x20a` requests retained ADV publication. Godot's existing indicator clock/configuration remains the +animation implementation. The real SC0000-to-HISTORY regression proves HISTORY disables the marker on +entry, executes layout-slot-1 publication on return, clears transient History rows, and leaves the enclosing +ADV page wait in place. The expanded 63-field `EngineCtx` and semantic function annotations are applied to +the saved `/v2` image. + +--- + ## Native walls backlog (targets for this loop) - ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script ` = raw SYS4INI file index. diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 3392d82..b3456df 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -80,6 +80,11 @@ - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x1bb_handler@0x41f650 writes 0x80000000 to ctx+0x55110 for operand 0 and zero for operand 1, rejecting other values. Text/layout/metadata/voice paths test or propagate that high bit before appending retained history. HISTORY.BIN disables recording at entry and reenables it at exit so the backlog UI does not record itself. +### 0x1ce `set-adv-wait-indicator-enabled` (u0041B9F0, argc 1) +- **summary:** (enabled) - explicitly start or stop the animated ADV input-wait indicator service. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra op_0x1ce_set_adv_wait_indicator_enabled@0x41f8c0 writes enabled to ctx+0x5f734. A nonzero value sets run bit 0x40000000, resets ctx+0x5f72c to frame zero, and starts the ctx+0x5f64c elapsed timer. Zero erases the indicator through adv_text_publish_wait_indicator_frame@0x453120 using ctx+0x6da84 and frame -2 when necessary, then clears the run bit. The corpus uses 0x1ce(0) on modal/menu entry, including HISTORY.BIN@0x3. + ### 0x1d0 `step-text-history` (step-text-history, argc 3) - **summary:** (out_layout_slot)(out_record_index)(delta) - resolve a cumulative delta from the latest retained ADV boundary and return its layout slot and first record index, or -1 outputs at a boundary. - **grounding:** source=investigation, confidence=high @@ -110,6 +115,11 @@ - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x204_handler@0x422a60 resolves operand 4 as a string, fetches surface/x/y, then calls draw_string_to_surface@0x450150 on text manager ctx+0x14940. The worker validates and locks gfx-manager surface table +0xa590[slot], chooses text_raster_string_uncached@0x459d90 or cached/effect path @0x45b600, rasterizes GDI GetGlyphOutlineA bitmaps through text_blit_glyph_bitmap@0x458c80 using font/color state +0x4d0/+0x458, then unlocks. SC0000 0x9b2 draws the speaker name into 400x30 surface 0xd at (1,1); following 0x1fb binds it to retained object 0xe678 at (74,444). +### 0x20a `publish-adv-text-layout` (u00420CE0, argc 1) +- **summary:** (layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra op_0x20a_publish_adv_text_layout@0x422ce0 passes the requested slot to adv_text_publish_layout@0x450c80, which rebinds the layout's retained text records. If ADV run-state bit 0x40000000 is active, it also calls adv_text_publish_wait_indicator_frame@0x453120 with ctx+0x5f72c. Slot zero resolves the text manager's current layout; the corpus always uses slot one, including HISTORY.BIN@0x13ab in its shared ADV redraw callback. + ### 0x2bd `set-font-bold` (set-font-bold, argc 1) - **summary:** (enabled) - set the current primary text font weight to 700 when enabled or 0 when disabled, then rebuild the native font state. - **grounding:** source=investigation, confidence=high @@ -1068,10 +1078,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=med -### 0x1ce `u0041B9F0` (u0041B9F0, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x1cf `u0041DA10` (u0041DA10, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=low @@ -1092,10 +1098,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0x20a `u00420CE0` (u00420CE0, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x20d `u00420E10` (u00420E10, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index b510cac..daa720b 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -1926,3 +1926,29 @@ slice. Keep `0x1cb` deferred until the global save/profile storage boundary is c Validation: all 205 engine tests pass, including four focused History presentation cases; opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the Godot build has zero warnings, and threaded `SELFTEST OK`. + +### ADV History text publication / wait-indicator service implemented (2026-07-20) + +Native RE corrects the provisional "sprite-animation service" description: `0x1ce` and `0x20a` are the +explicit control points for ADV text publication and its animated input-wait indicator. `0x1ce(enabled)` +starts or stops the indicator's run-state/timer/frame service; Himegari only calls it with zero when entering +modal scripts, including HISTORY. `0x20a(layout_slot)` republishes the selected retained text layout and, +when the service is active, binds its current indicator frame; HISTORY's shared redraw callback uses slot 1. +Normal ADV wait opcode `0x72` implicitly arms the same service. + +The VM now forwards both operations through generic host methods. Godot gates its existing animated marker +on the explicit enabled state and requests recomposition for layout publication. Because the port keeps the +parent wait blocked while a nested raw-input callback executes, it restores a previously enabled parent +marker when that callback returns, matching the native path that re-enters the shared redraw/wait routine. +No script offsets, boot seeds, or persistence assumptions were introduced. + +A focused synthetic regression verifies the two service calls, and the real SC0000-to-HISTORY regression +requires marker disable plus layout-slot-1 publication while preserving the same enclosing ADV wait and +cleaning the modal rows. The canonical opcode and EngineCtx sources are regenerated; the `/v2` handlers and +workers are named/commented, the expanded 63-field `EngineCtx` is applied, and the image is saved. HISTORY +is now 77/78 distinct opcodes and 853/854 instructions handled or safe-noop. The only remaining instruction +is the deliberately deferred `0x1cb` Read-message Skip setting getter, pending a global profile/save backend +decision. + +Validation: all 206 engine tests pass, opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the +Godot build has zero warnings, and threaded `SELFTEST OK`. diff --git a/engine/Age.Engine.Tests/AdvTextOpsTests.cs b/engine/Age.Engine.Tests/AdvTextOpsTests.cs index 27afdf3..36ac045 100644 --- a/engine/Age.Engine.Tests/AdvTextOpsTests.cs +++ b/engine/Age.Engine.Tests/AdvTextOpsTests.cs @@ -53,4 +53,24 @@ public class AdvTextOpsTests Assert.Equal(new Age.Engine.Hosting.AdvWaitIndicatorConfig(1, 385, 140, 12, 0, 0, 30, 27, 12, 48), Assert.Single(host.WaitIndicators)); } + + [Fact] + public void WaitIndicatorToggleAndTextLayoutPublicationReachHost() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var script = ScriptAssembler.Assemble(table, "WAITMARK_SERVICE", + new List<(int, Operand[])> + { + (0x1ce, new[] { new Operand(0, 1) }), + (0x20a, new[] { new Operand(0, 4) }), + (0x1ce, new[] { new Operand(0, 0) }), + (0x2, Array.Empty()), + }, Array.Empty()); + var host = new RecordingHost(); + + new VirtualMachine(script, table, host).Run(); + + Assert.Equal(new[] { true, false }, host.WaitIndicatorEnabledChanges); + Assert.Equal(4, Assert.Single(host.PublishedAdvTextLayouts)); + } } diff --git a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs index 23e4302..1a08ad2 100644 --- a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs @@ -295,6 +295,8 @@ public class HistoryInteractionOpsTests Assert.Equal(1, host.Waits); // the enclosing ADV page was never released or re-entered Assert.Equal(1, host.HistoryPresentationEnds); Assert.Empty(host.ActiveHistoryRenders); + Assert.Contains(false, host.WaitIndicatorEnabledChanges); + Assert.Contains(1, host.PublishedAdvTextLayouts); var historyButtons = host.FirstHistoryFrame .Where(render => render.Handle >= 0xd2fa && render.Handle <= 0xd300) diff --git a/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs b/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs index 49ecd77..6e67e44 100644 --- a/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs @@ -102,5 +102,6 @@ public class HistoryPresentationOpsTests Assert.All(host.HistoryRenders.Where(render => render.Text.Length > 0), render => Assert.Equal(8, render.Style.LineSpacing)); Assert.Contains(host.PresentedRanges, range => range == (0L, 60000L)); + Assert.Contains(false, host.WaitIndicatorEnabledChanges); } } diff --git a/engine/Age.Engine.Tests/TestSupport.cs b/engine/Age.Engine.Tests/TestSupport.cs index bf39825..a63421d 100644 --- a/engine/Age.Engine.Tests/TestSupport.cs +++ b/engine/Age.Engine.Tests/TestSupport.cs @@ -24,6 +24,8 @@ internal class RecordingHost : IHost public readonly List SurfaceFills = new(); public readonly List<(long First, long Count)> PresentedRanges = new(); public readonly List WaitIndicators = new(); + public readonly List WaitIndicatorEnabledChanges = new(); + public readonly List PublishedAdvTextLayouts = new(); public readonly List SleptDurations = new(); public readonly List<(long Resource, int Channel)> SfxLoads = new(); public readonly List Voices = new(); @@ -62,6 +64,8 @@ internal class RecordingHost : IHost public void PresentObjectRange(GfxState gfx, long firstHandle, long count) => PresentedRanges.Add((firstHandle, count)); public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config); + public void SetAdvWaitIndicatorEnabled(bool enabled) => WaitIndicatorEnabledChanges.Add(enabled); + public void PublishAdvTextLayout(int layoutSlot) => PublishedAdvTextLayouts.Add(layoutSlot); public void SetAdvPagePresentationSuspended(bool suspended) => AdvPagePresentationSuspended.Add(suspended); public void WaitForInput() => Waits++; diff --git a/engine/Age.Engine/Hosting/IHost.cs b/engine/Age.Engine/Hosting/IHost.cs index 801f7cb..5cebc91 100644 --- a/engine/Age.Engine/Hosting/IHost.cs +++ b/engine/Age.Engine/Hosting/IHost.cs @@ -31,6 +31,10 @@ public interface IHost void FillSurfaceRect(SurfaceRectFill fill) { } void PresentObjectRange(GfxState gfx, long firstHandle, long count) { } void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { } + // Op 0x1ce explicitly starts/stops the same animated marker that op 0x72 starts for an ADV wait. + void SetAdvWaitIndicatorEnabled(bool enabled) { } + // Op 0x20a republishes one retained ADV text layout and includes the current marker frame when active. + void PublishAdvTextLayout(int layoutSlot) { } // Op 0x199 temporarily yields the active ADV page into its registered hide-window coroutine. // The retained scene continues to render, but the text layout and its wait marker are suspended // until op 0x7c restores the saved page PC. diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 107ab4a..e3b5809 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -624,6 +624,14 @@ public sealed class VirtualMachine (int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7]), (int)Read(a[8]), Read(a[9]))); return pc + 1; + case "u0041B9F0": + case "set-adv-wait-indicator-enabled": // 0x1ce: explicit marker service start/stop + _host.SetAdvWaitIndicatorEnabled(Read(a[0]) != 0); + return pc + 1; + case "u00420CE0": + case "publish-adv-text-layout": // 0x20a: publish layout and current marker frame if active + _host.PublishAdvTextLayout((int)Read(a[0])); + return pc + 1; case "draw-string": // 0x204 (surface slot, x, y, string) _host.DrawStringToSurface((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), ReadStr(a[3]), _advTextStyle); diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index b605fb7..ceb3918 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -43,6 +43,7 @@ public sealed class GodotAdvHost : IHost private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice; private int _activeWaitLayout; private long _waitIndicatorStartedMs; + private bool _waitIndicatorEnabled; private volatile bool _advPagePresentationSuspended; private GfxState? _foregroundGfx; public volatile bool IsWaiting; @@ -196,6 +197,22 @@ public sealed class GodotAdvHost : IHost }); } + public void SetAdvWaitIndicatorEnabled(bool enabled) + { + lock (_textLock) + { + if (enabled && !_waitIndicatorEnabled) _waitIndicatorStartedMs = _clock.NowMs; + _waitIndicatorEnabled = enabled; + } + _timeline?.Event("wait-indicator-enabled", new() { ["enabled"] = enabled }); + } + + public void PublishAdvTextLayout(int layoutSlot) + { + System.Threading.Interlocked.Exchange(ref _presentRequested, 1); + _timeline?.Event("adv-text-layout-publish", new() { ["layout"] = layoutSlot }); + } + public AdvWaitIndicatorSnapshot? SnapshotAdvWaitIndicator() { if (!IsWaiting || _advPagePresentationSuspended) return null; @@ -203,6 +220,7 @@ public sealed class GodotAdvHost : IHost long resourceId; lock (_textLock) { + if (!_waitIndicatorEnabled) return null; if (!_waitIndicators.TryGetValue(_activeWaitLayout, out config)) return null; if (!_surfaceResources.TryGetValue(config.SurfaceSlot, out resourceId)) return null; } @@ -234,8 +252,12 @@ public sealed class GodotAdvHost : IHost // Publish retained mutations accumulated before the wait once. A static input wait is not itself a // reason to rebuild the 800x600 background every frame; ambient channels are queried separately. System.Threading.Interlocked.Exchange(ref _presentRequested, 1); - lock (_textLock) _activeWaitLayout = layoutSlot == 0 ? _currentAdvLayout : layoutSlot; - _waitIndicatorStartedMs = _clock.NowMs; + lock (_textLock) + { + _activeWaitLayout = layoutSlot == 0 ? _currentAdvLayout : layoutSlot; + _waitIndicatorStartedMs = _clock.NowMs; + _waitIndicatorEnabled = true; + } IsWaiting = true; _timeline?.State("input-wait", new() { ["page"] = Pages }); var autoTimer = new AdvAutoAdvanceTimer(); @@ -243,7 +265,24 @@ public sealed class GodotAdvHost : IHost bool messageSkipped = false; while (!_stopping) { - while (serviceInputCallback()) { } + while (true) + { + bool markerWasEnabled; + lock (_textLock) markerWasEnabled = _waitIndicatorEnabled; + bool keepServicing = serviceInputCallback(); + // The native callback returns through the shared ADV redraw/wait path, whose op 0x72 + // re-arms a marker stopped by nested HISTORY. Our blocking host keeps the parent wait + // parked, so restore that parent-owned state at the equivalent callback boundary. + lock (_textLock) + { + if (markerWasEnabled && !_waitIndicatorEnabled) + { + _waitIndicatorEnabled = true; + _waitIndicatorStartedMs = _clock.NowMs; + } + } + if (!keepServicing) break; + } if (_messageSkipActive) { messageSkipped = true; @@ -259,6 +298,7 @@ public sealed class GodotAdvHost : IHost if (_gate.Wait(0)) break; } IsWaiting = false; + lock (_textLock) _waitIndicatorEnabled = false; _timeline?.State("running", new() { ["input"] = messageSkipped ? "message-skip" : autoAdvanced ? "auto" : "user", diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index 43a7419..ebfc01b 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -44,12 +44,14 @@ INFERRED: dict[int, dict] = { 0x1bf: dict(name='call-end', category='marker', noop=True, confidence='med', source='inference', summary='zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker'), 0x1ca: dict(name='set-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text.'), 0x1cb: dict(name='get-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - read the engine setting `message:ReadTextSkip`.'), + 0x1ce: dict(name='set-adv-wait-indicator-enabled', category='adv', noop=False, confidence='high', source='investigation', summary='(enabled) - explicitly start or stop the animated ADV input-wait indicator service.'), 0x1d1: dict(name='render-text-history', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot)(record_index)(flags)(color_a)(color_b) - render retained ADV text records into a selected text layout/surface.'), 0x1d5: dict(name='cond-block', category='marker', noop=True, confidence='high', source='inference', summary='zero-arg; ALWAYS follows jcc — marks conditional body entry'), 0x1f4: dict(name='stmt-begin', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; opens scripts, pairs with stmt-end 0x1f5'), 0x1f5: dict(name='stmt-end', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; precedes exit/next-stmt, pairs with 0x1f4'), 0x1fd: dict(name='gfx-set-scale-current', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the retained object's current scale matrix at obj+0x6c. The handler divides each integer operand by 100.0 before calling matrix4_make_scale; this is distinct from 0x21e's delayed one-shot target scale."), 0x1ff: dict(name='set-current-translation', category='draw', noop=False, confidence='high', source='investigation', summary="0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac."), + 0x20a: dict(name='publish-adv-text-layout', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame.'), 0x21b: dict(name='line-id?', category='marker', noop=True, confidence='med', source='harness', summary='1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control'), 0x258: dict(name='decl?', category='marker', noop=True, confidence='low', source='harness', summary='2 imm; runs in a chain right after script-entry 0x259, enumerating ids — prologue declaration/registration?'), 0x259: dict(name='script-entry', category='marker', noop=True, confidence='low', source='harness', summary='zero-arg; the first instruction of a script (offset 0), opens the decl chain that 0x258 continues — script/prologue entry marker, structural'), diff --git a/vm-map/engine-ctx.toml b/vm-map/engine-ctx.toml index bddd91a..9b0b227 100644 --- a/vm-map/engine-ctx.toml +++ b/vm-map/engine-ctx.toml @@ -173,6 +173,11 @@ name = "timed_callback_timer" type = "int" note = "elapsed timer object armed by op 0xd5 and sampled by the timed callback scheduler" [[field]] +offset = 0x5f64c +name = "adv_wait_indicator_timer" +type = "int" +note = "elapsed timer started by op 0x1ce and ADV wait setup to select the animated input-wait indicator frame" +[[field]] offset = 0x5f690 name = "timed_callback_abort_pc" type = "int" @@ -218,6 +223,21 @@ name = "timed_callback_script_resource_guard" type = "uint" note = "companion current-script identity checked before redirecting to a scheduled callback" [[field]] +offset = 0x5f72c +name = "adv_wait_indicator_frame" +type = "int" +note = "current animated input-wait indicator frame, reset to zero when the service is armed" +[[field]] +offset = 0x5f730 +name = "adv_wait_indicator_terminal_frame" +type = "int" +note = "terminal frame returned by the indicator layout during ADV wait setup" +[[field]] +offset = 0x5f734 +name = "adv_wait_indicator_enabled_value" +type = "int" +note = "raw enable operand last written by op 0x1ce; nonzero arms the ADV wait-indicator service" +[[field]] offset = 0x6da78 name = "message_skip_activation_guard" type = "int" @@ -228,6 +248,11 @@ name = "message_skip_cancel_click_phase" type = "int" note = "press/release phase used only when set:CancelMesSkipOnClick is nonzero" [[field]] +offset = 0x6da84 +name = "adv_wait_indicator_layout_slot" +type = "int" +note = "ADV text layout whose wait indicator is armed or erased by input-wait setup and op 0x1ce" +[[field]] offset = 0x6da88 name = "coroutine_yield_a" type = "void*" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 29d5adf..29dbffb 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -4233,18 +4233,18 @@ argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041B9F0" -category = "unknown" -summary = "" +name = "set-adv-wait-indicator-enabled" +category = "adv" +summary = "(enabled) - explicitly start or stop the animated ADV input-wait indicator service." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra op_0x1ce_set_adv_wait_indicator_enabled@0x41f8c0 writes enabled to ctx+0x5f734. A nonzero value sets run bit 0x40000000, resets ctx+0x5f72c to frame zero, and starts the ctx+0x5f64c elapsed timer. Zero erases the indicator through adv_text_publish_wait_indicator_frame@0x453120 using ctx+0x6da84 and frame -2 when necessary, then clears the run bit. The corpus uses 0x1ce(0) on modal/menu entry, including HISTORY.BIN@0x3." [[opcode.semantics.args]] i = 1 -role = "" +role = "enabled" observed_types = ["imm"] [[opcode]] @@ -5043,18 +5043,18 @@ argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00420CE0" -category = "unknown" -summary = "" +name = "publish-adv-text-layout" +category = "adv" +summary = "(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra op_0x20a_publish_adv_text_layout@0x422ce0 passes the requested slot to adv_text_publish_layout@0x450c80, which rebinds the layout's retained text records. If ADV run-state bit 0x40000000 is active, it also calls adv_text_publish_wait_indicator_frame@0x453120 with ctx+0x5f72c. Slot zero resolves the text manager's current layout; the corpus always uses slot one, including HISTORY.BIN@0x13ab in its shared ADV redraw callback." [[opcode.semantics.args]] i = 1 -role = "" +role = "layout_slot (0 = current)" observed_types = ["imm"] [[opcode]]