Restore ADV wait indicator
This commit is contained in:
@@ -1050,6 +1050,29 @@ Matching evidence: `build/native-adv-text-trace.jsonl` and Godot timeline captur
|
||||
non-overlapping name/dialogue bands at y=447–468, 478–500, and 507–530. A manual run progressed 14 pages:
|
||||
11 clicks completed active reveals and 14 later clicks released 14 distinct waits through `0xe0c`.
|
||||
|
||||
#### ADV wait indicator -- ops `0x73` / `0x72` (2026-07-11)
|
||||
|
||||
The small bat marker is a configured ADV-layout sprite, not a glyph or part of SO001. `SYSTEM4.BIN`
|
||||
loads universal raw asset `0x337c` (`SO000.AGF`) into surface slot 12. SO000 is a 390x27 strip of thirteen
|
||||
30x27 bat frames. It then executes op `0x73` with
|
||||
`(layout=1, x=385, y=140, surface=12, src=0,0, cell=30x27, terminalFrame=12, period=48ms)`.
|
||||
Layout 1 begins at screen y=430, so the marker lands at `(385,570)`, matching the original screenshot.
|
||||
|
||||
Native `op_0x73_configure_wait_indicator@0x41e900` writes this descriptor through
|
||||
`adv_text_configure_wait_indicator@0x44ff60` to layout fields `+0x3c..+0x60`; operand 10 configures the
|
||||
animation period through `adv_indicator_set_frame_period@0x44d060`. `op_0x72_handler@0x41e690` later calls
|
||||
the layout renderer with frame `-1` and arms the input-wait flags, which makes the configured animation
|
||||
visible only after show-text has naturally or forcibly completed. The completion click remains consumed;
|
||||
the following click releases the wait.
|
||||
|
||||
**Port implementation:** the single-scene harness now injects SYSTEM4's exact SO000 surface-12 and op-`0x73`
|
||||
configuration alongside its existing SO001 shortcut. The VM forwards all ten operands through the host ABI;
|
||||
`GodotAdvHost` retains configurations by layout and resolves slot 0 to the active Phase-A layout. A separate
|
||||
Godot atlas overlay advances the inclusive frames `0..12` on the 48 ms host clock only while `IsWaiting`,
|
||||
then hides immediately when input releases the wait. Keeping this 30x27 overlay outside the software
|
||||
backbuffer preserves the static-wait compositor optimization. Full SYSTEM4 replay remains Phase-B work;
|
||||
the shortcut carries the same state in the meantime.
|
||||
|
||||
### SC0000 native SFX / BGM-fade family — `0xb4`/`0xb5`/`0xb6`/`0xc2`/`0xd9` (2026-07-11)
|
||||
|
||||
The three SFX opcodes are a retained channel lifecycle, not immediate fire-and-forget calls. Handler
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
|
||||
## adv
|
||||
|
||||
### 0x72 `wait-for-input` (wait-for-input, argc 1)
|
||||
- **summary:** (layout_slot) - arm the ADV input wait after text reveal completes; activates the wait indicator configured for the selected text layout by op 0x73.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x72_handler@0x41e690 fetches operand 1 and calls FUN_00453120(text_manager, layout_slot, -1, &state), then sets the input-wait run-state flags. FUN_00453120 resolves layout slot 0 as current and consumes the indicator descriptor at layout+0x3c configured by op 0x73. SYSTEM4 layout 1 uses SO000's bat strip; the click that completes show-text is consumed before this opcode is reached.
|
||||
|
||||
### 0x73 `configure-adv-wait-indicator` (configure-adv-wait-indicator, argc 10)
|
||||
- **summary:** (layout_slot)(dst_x)(dst_y)(surface_slot)(src_x)(src_y)(cell_w)(cell_h)(terminal_frame)(frame_period_ms) - configure the animated marker shown while the selected ADV layout waits for input.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x73_configure_wait_indicator@0x41e900 passes operands 1..9 to adv_text_configure_wait_indicator@0x44ff60 and operand 10 to adv_indicator_set_frame_period@0x44d060. The worker writes surface/source rect, layout-relative destination, enabled=1, and terminal/column values to the selected layout at +0x3c..+0x60. SYSTEM4 executes `set-texture 0x337c 0xc 0xff00` (raw id 0x337c = SO000.AGF, a 390x27 strip of thirteen 30x27 bat frames), then `0x73 1 385 140 12 0 0 30 27 12 48`; layout 1 begins at y=430, placing the 30x27 marker at screen (385,570), matching the original. Op 0x72 activates this descriptor only after reveal completion.
|
||||
|
||||
### 0x7a `set-adv-text-cursor` (set-adv-text-cursor, argc 3)
|
||||
- **summary:** (layout_slot)(x)(y) - set the cursor in the selected ADV text layout's last 20-byte record. Slot 0 selects the current layout.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -524,14 +534,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
|
||||
|
||||
### 0x72 `wait-for-input` (wait-for-input, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=med
|
||||
|
||||
### 0x73 `u0041AB30` (u0041AB30, argc 10)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x75 `u0041AC30` (u0041AC30, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -1384,6 +1384,18 @@ visible. This AE001D path is a retained scaled/rotating texture; the later `0x23
|
||||
was separately resolved by preserving draw-texture's 200x200 cell and interpreting `(8,4)` as total
|
||||
frames/columns rather than a grid divisor.
|
||||
|
||||
### SC0000 ADV wait indicator implemented (2026-07-11)
|
||||
|
||||
The missing post-reveal bat is `SO000.AGF`, a 390x27 strip of thirteen 30x27 frames. SYSTEM4 loads raw
|
||||
asset `0x337c` into surface slot 12 and configures layout 1 through op `0x73` as a marker at layout-relative
|
||||
`(385,140)`, screen `(385,570)`, with terminal frame 12 and a 48 ms period. Native op `0x72` activates that
|
||||
layout descriptor after reveal completion. The Phase-A bootstrap now injects SO000 into surface 12 and
|
||||
forwards the exact op-`0x73` configuration through `IHost`; `WaitForInput` carries the selected layout and
|
||||
starts the marker clock. Godot presents it as a separate 30x27 atlas overlay, advancing inclusive frames
|
||||
0..12 every 48 ms only during the stable wait and hiding on release. This avoids recompositing the 800x600
|
||||
software backbuffer during otherwise static waits. Validation: engine **147/147**, zero-warning Godot build,
|
||||
threaded `SELFTEST OK`, and a real page-1 capture showing the transparent bat at `(385,570)`.
|
||||
|
||||
### A2b -- SC0000 post-movie AE001H spritesheet correction ✅ DONE (2026-07-11)
|
||||
|
||||
The reported missing cave spirit was the `0x231` path, independent of the earlier AE001D retained glow.
|
||||
|
||||
@@ -30,4 +30,27 @@ public class AdvTextOpsTests
|
||||
Assert.Equal((1, 75, 47), Assert.Single(host.TextCursors));
|
||||
Assert.Equal((13, 1, 1, "speaker"), Assert.Single(host.SurfaceStrings));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitIndicatorConfigurationReachesHost()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "WAITMARK",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x73, new[]
|
||||
{
|
||||
new Operand(0, 1), new Operand(0, 385), new Operand(0, 140), new Operand(0, 12),
|
||||
new Operand(0, 0), new Operand(0, 0), new Operand(0, 30), new Operand(0, 27),
|
||||
new Operand(0, 12), new Operand(0, 48),
|
||||
}),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
|
||||
new VirtualMachine(script, table, host).Run();
|
||||
|
||||
Assert.Equal(new Age.Engine.Hosting.AdvWaitIndicatorConfig(1, 385, 140, 12, 0, 0, 30, 27, 12, 48),
|
||||
Assert.Single(host.WaitIndicators));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ internal sealed class RecordingHost : IHost
|
||||
public readonly List<(int Offset, string Text)> Lines = new();
|
||||
public readonly List<(int Slot, int X, int Y)> TextCursors = new();
|
||||
public readonly List<(int Surface, int X, int Y, string Text)> SurfaceStrings = new();
|
||||
public readonly List<AdvWaitIndicatorConfig> WaitIndicators = new();
|
||||
public readonly List<long> SleptDurations = new();
|
||||
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
||||
public readonly List<int> SfxStarts = new();
|
||||
@@ -26,6 +27,7 @@ internal sealed class RecordingHost : IHost
|
||||
public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
|
||||
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
||||
=> SurfaceStrings.Add((surfaceSlot, x, y, text));
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
||||
public void WaitForInput() => Waits++;
|
||||
public void Sleep(long duration) => SleptDurations.Add(duration);
|
||||
public void FrameYield() { }
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
using Age.Engine.Model;
|
||||
|
||||
namespace Age.Engine.Hosting;
|
||||
|
||||
public readonly record struct AdvWaitIndicatorConfig(
|
||||
int LayoutSlot, int X, int Y, int SurfaceSlot,
|
||||
int SourceX, int SourceY, int CellWidth, int CellHeight,
|
||||
int TerminalFrame, long FramePeriodMs);
|
||||
|
||||
public interface IHost
|
||||
{
|
||||
void ShowText(int offset, string text);
|
||||
@@ -8,7 +14,9 @@ public interface IHost
|
||||
// op 0x204 rasterizes a string into a numbered surface before 0x1fb binds that surface.
|
||||
void SetAdvTextCursor(int layoutSlot, int x, int y) { }
|
||||
void DrawStringToSurface(int surfaceSlot, int x, int y, string text) { }
|
||||
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
||||
void WaitForInput();
|
||||
void WaitForInput(int layoutSlot) => WaitForInput();
|
||||
void Sleep(long duration);
|
||||
void FrameYield();
|
||||
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive
|
||||
|
||||
@@ -283,13 +283,19 @@ public sealed class VirtualMachine
|
||||
return pc + 1;
|
||||
case "set-adv-text-cursor": // 0x7a (layout slot, x, y); slot 0 means current natively
|
||||
_host.SetAdvTextCursor((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2])); return pc + 1;
|
||||
case "configure-adv-wait-indicator": // 0x73: per-layout animated input-wait marker
|
||||
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
||||
(int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
|
||||
(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 "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]));
|
||||
return pc + 1;
|
||||
case "wait-for-input":
|
||||
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
|
||||
if (_o.HaltAtWaitForInput) { HaltReason ??= "wait-for-input"; return HALT; }
|
||||
_host.WaitForInput(); return pc + 1;
|
||||
_host.WaitForInput((int)Read(a[0])); return pc + 1;
|
||||
case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing.
|
||||
_host.Sleep(Read(a[0])); return pc + 1;
|
||||
case "get-message-skip": // 0x1c7: Ctrl/message fast-forward run-state bit
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly string _scene; // e.g. "SC0000" — for section_base
|
||||
private readonly object _imageLock = new();
|
||||
private readonly Dictionary<int, RgbaImage?> _images = new(); // raw catalog id -> decoded pixels
|
||||
private readonly Dictionary<int, long> _surfaceResources = new(); // surface slot -> scene/raw resource id
|
||||
private readonly Dictionary<long, (RgbaImage Image, string Name, int RawIndex)> _movieFrames = new();
|
||||
private readonly Dictionary<int, long> _movieBySurface = new();
|
||||
private readonly HashSet<long> _completedMovies = new();
|
||||
@@ -27,8 +28,12 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly Dictionary<int, SurfaceTextDraw> _surfaceText = new();
|
||||
private string _advText = "";
|
||||
private int _advTextX = 100, _advTextY = 47;
|
||||
private int _currentAdvLayout = 1; // SYSTEM4's ordinary SC0000 ADV layout
|
||||
private long _advTextStartedMs;
|
||||
private bool _advTextForceComplete;
|
||||
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
||||
private int _activeWaitLayout;
|
||||
private long _waitIndicatorStartedMs;
|
||||
private GfxState? _foregroundGfx;
|
||||
public volatile bool IsWaiting;
|
||||
public volatile bool IsTransitionWaiting;
|
||||
@@ -75,7 +80,12 @@ public sealed class GodotAdvHost : IHost
|
||||
|
||||
public void SetAdvTextCursor(int layoutSlot, int x, int y)
|
||||
{
|
||||
lock (_textLock) { _advTextX = x; _advTextY = y; }
|
||||
lock (_textLock)
|
||||
{
|
||||
if (layoutSlot != 0) _currentAdvLayout = layoutSlot;
|
||||
_advTextX = x;
|
||||
_advTextY = y;
|
||||
}
|
||||
_timeline?.Event("text-cursor", new() { ["slot"] = layoutSlot, ["x"] = x, ["y"] = y });
|
||||
}
|
||||
|
||||
@@ -99,15 +109,50 @@ public sealed class GodotAdvHost : IHost
|
||||
public bool TryGetSurfaceText(int surfaceSlot, out SurfaceTextDraw draw)
|
||||
{ lock (_textLock) return _surfaceText.TryGetValue(surfaceSlot, out draw); }
|
||||
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
|
||||
{
|
||||
lock (_textLock) _waitIndicators[config.LayoutSlot] = config;
|
||||
_timeline?.Event("wait-indicator-config", new()
|
||||
{
|
||||
["layout"] = config.LayoutSlot, ["x"] = config.X, ["y"] = config.Y,
|
||||
["surface"] = config.SurfaceSlot, ["cell_w"] = config.CellWidth,
|
||||
["cell_h"] = config.CellHeight, ["terminal_frame"] = config.TerminalFrame,
|
||||
["period_ms"] = config.FramePeriodMs,
|
||||
});
|
||||
}
|
||||
|
||||
public AdvWaitIndicatorSnapshot? SnapshotAdvWaitIndicator()
|
||||
{
|
||||
if (!IsWaiting) return null;
|
||||
AdvWaitIndicatorConfig config;
|
||||
long resourceId;
|
||||
lock (_textLock)
|
||||
{
|
||||
if (!_waitIndicators.TryGetValue(_activeWaitLayout, out config)) return null;
|
||||
if (!_surfaceResources.TryGetValue(config.SurfaceSlot, out resourceId)) return null;
|
||||
}
|
||||
var asset = _res.ResolveTexture(_scene, resourceId);
|
||||
var image = asset != null ? Decode(asset) : null;
|
||||
if (asset == null || image == null || config.CellWidth <= 0 || config.CellHeight <= 0) return null;
|
||||
int frames = System.Math.Max(1, config.TerminalFrame + 1);
|
||||
long period = System.Math.Max(1, config.FramePeriodMs);
|
||||
int frame = (int)((_clock.NowMs - _waitIndicatorStartedMs) / period % frames);
|
||||
return new AdvWaitIndicatorSnapshot(image, asset.Name, asset.RawIndex, config, frame);
|
||||
}
|
||||
|
||||
public volatile int Pages; // VM-thread page counter (incremented before IsWaiting so shot-gating can't race)
|
||||
|
||||
public void WaitForInput()
|
||||
public void WaitForInput() => WaitForInput(0);
|
||||
|
||||
public void WaitForInput(int layoutSlot)
|
||||
{
|
||||
Pages++;
|
||||
_main.CallDeferred("PageBreak");
|
||||
// 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;
|
||||
IsWaiting = true;
|
||||
_timeline?.State("input-wait", new() { ["page"] = Pages });
|
||||
_gate.Wait();
|
||||
@@ -244,13 +289,18 @@ public sealed class GodotAdvHost : IHost
|
||||
public void CreateTexture(int slot, int width, int height)
|
||||
{
|
||||
lock (_textLock) _surfaceText.Remove(slot);
|
||||
lock (_textLock) _surfaceResources.Remove(slot);
|
||||
_slotDims[slot] = (width, height);
|
||||
if (TraceOps) Godot.GD.Print($"[op] create-texture slot={slot} {width}x{height}");
|
||||
}
|
||||
|
||||
public void SetTexture(long resourceId, int slot)
|
||||
{
|
||||
lock (_textLock) _surfaceText.Remove(slot);
|
||||
lock (_textLock)
|
||||
{
|
||||
_surfaceText.Remove(slot);
|
||||
_surfaceResources[slot] = resourceId;
|
||||
}
|
||||
var asset = _res.ResolveTexture(_scene, resourceId);
|
||||
var image = asset != null ? Decode(asset) : null;
|
||||
_slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0);
|
||||
@@ -428,3 +478,5 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
|
||||
public readonly record struct SurfaceTextDraw(int X, int Y, string Text);
|
||||
public readonly record struct AdvWaitIndicatorSnapshot(
|
||||
RgbaImage Image, string Name, int AssetId, AdvWaitIndicatorConfig Config, int Frame);
|
||||
|
||||
@@ -17,6 +17,10 @@ public partial class Main : Godot.Control
|
||||
private TextureRect _screenView = null!; // shows the composited screen backbuffer
|
||||
private Image _screen = null!; // 800x600 immediate-mode canvas
|
||||
private ImageTexture _screenTex = null!;
|
||||
private TextureRect _waitIndicator = null!;
|
||||
private ImageTexture? _waitIndicatorSheet;
|
||||
private AtlasTexture? _waitIndicatorAtlas;
|
||||
private int _waitIndicatorAssetId = -1;
|
||||
// One managed composition target for the entire frame. Layer helpers mutate it in place; only the
|
||||
// completed frame crosses the Godot Image boundary, avoiding a full GetData/SetData round-trip per layer.
|
||||
private readonly byte[] _screenPixels = new byte[ScreenWidth * ScreenHeight * 4];
|
||||
@@ -72,6 +76,17 @@ public partial class Main : Godot.Control
|
||||
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
AddChild(_screenView); // added first -> draws behind the text/status labels
|
||||
|
||||
// Native ADV wait marker: a tiny independently animated atlas region. Keeping it separate from the
|
||||
// 800x600 software backbuffer avoids recompositing the entire retained scene throughout static waits.
|
||||
_waitIndicator = new TextureRect
|
||||
{
|
||||
Visible = false,
|
||||
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
|
||||
StretchMode = TextureRect.StretchModeEnum.Keep,
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
};
|
||||
AddChild(_waitIndicator);
|
||||
|
||||
_text = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = MouseFilterEnum.Ignore };
|
||||
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
AddChild(_text);
|
||||
@@ -181,6 +196,16 @@ public partial class Main : Godot.Control
|
||||
_host.SetTexture(systemChrome.RawIndex, 0x11);
|
||||
_vm.Gfx.SetSurface(0x11, systemChrome.RawIndex, 0);
|
||||
}
|
||||
// SYSTEM4 also loads SO000 and configures op 0x73 before entering scene code. The Phase-A
|
||||
// single-scene harness does not replay those graphics side effects, so inject their exact state
|
||||
// alongside the existing SO001 bootstrap until Phase B runs the complete SYSTEM4 entrypoint.
|
||||
if (!_selftest && resources.ResolveName("SO000.AGF") is { } waitIndicator)
|
||||
{
|
||||
_host.SetTexture(waitIndicator.RawIndex, 0x0c);
|
||||
_vm.Gfx.SetSurface(0x0c, waitIndicator.RawIndex, 0xff00);
|
||||
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
||||
1, 385, 140, 0x0c, 0, 0, 30, 27, 12, 48));
|
||||
}
|
||||
// --boot: run SYSTEM4's state prefix (INITCONFIG/INIT2/INIT) so the scene sees boot state — chiefly
|
||||
// INIT2's gfx handle array 0x62455.. (skips the UI scripts LOGO/OP/TITLE). State carries via globals.
|
||||
if (boot && !_selftest)
|
||||
@@ -230,6 +255,7 @@ public partial class Main : Godot.Control
|
||||
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
|
||||
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
||||
if (!_selftest && _host != null) UpdateAdvTextPresentation();
|
||||
if (!_selftest && _host != null) UpdateAdvWaitIndicatorPresentation();
|
||||
// --shot-sequence: dump one PNG per frame across the opening so a time-based (paced) effect can be
|
||||
// verified as distinct frames, not just the final state. Captures after Recomposite; quits when full.
|
||||
if (_seqDir != null && _seqIdx < _seqFrames && !_done)
|
||||
@@ -397,6 +423,33 @@ public partial class Main : Godot.Control
|
||||
_text.Text = count == 0 ? "" : t.Text[..count];
|
||||
}
|
||||
|
||||
private void UpdateAdvWaitIndicatorPresentation()
|
||||
{
|
||||
var snapshot = _host.SnapshotAdvWaitIndicator();
|
||||
if (snapshot == null)
|
||||
{
|
||||
_waitIndicator.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var s = snapshot.Value;
|
||||
if (_waitIndicatorAssetId != s.AssetId)
|
||||
{
|
||||
var image = Image.CreateFromData(s.Image.Width, s.Image.Height, false, Image.Format.Rgba8, s.Image.Pixels);
|
||||
_waitIndicatorSheet = ImageTexture.CreateFromImage(image);
|
||||
_waitIndicatorAtlas = new AtlasTexture { Atlas = _waitIndicatorSheet };
|
||||
_waitIndicator.Texture = _waitIndicatorAtlas;
|
||||
_waitIndicatorAssetId = s.AssetId;
|
||||
}
|
||||
|
||||
var c = s.Config;
|
||||
_waitIndicatorAtlas!.Region = new Rect2(
|
||||
c.SourceX + s.Frame * c.CellWidth, c.SourceY, c.CellWidth, c.CellHeight);
|
||||
_waitIndicator.Position = new Vector2(c.X, 430 + c.Y);
|
||||
_waitIndicator.Size = new Vector2(c.CellWidth, c.CellHeight);
|
||||
_waitIndicator.Visible = true;
|
||||
}
|
||||
|
||||
private static string ColorTimeline(Age.Engine.Model.ColorTransitionState? state)
|
||||
=> state is { } c
|
||||
? $" color=0x{c.Current:x8}->0x{c.Target:x8} colorProgress={c.Progress:0.000}"
|
||||
|
||||
@@ -1014,83 +1014,83 @@ abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "wait-for-input"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
category = "adv"
|
||||
summary = "(layout_slot) - arm the ADV input wait after text reveal completes; activates the wait indicator configured for the selected text layout by op 0x73."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "med"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = ""
|
||||
evidence = "Ghidra /v2: op_0x72_handler@0x41e690 fetches operand 1 and calls FUN_00453120(text_manager, layout_slot, -1, &state), then sets the input-wait run-state flags. FUN_00453120 resolves layout slot 0 as current and consumes the indicator descriptor at layout+0x3c configured by op 0x73. SYSTEM4 layout 1 uses SO000's bat strip; the click that completes show-text is consumed before this opcode is reached."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "layout slot (0 = current)"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
op = 0x73
|
||||
label = "u0041AB30"
|
||||
label = "configure-adv-wait-indicator"
|
||||
argc = 10
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u0041AB30"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "configure-adv-wait-indicator"
|
||||
category = "adv"
|
||||
summary = "(layout_slot)(dst_x)(dst_y)(surface_slot)(src_x)(src_y)(cell_w)(cell_h)(terminal_frame)(frame_period_ms) - configure the animated marker shown while the selected ADV layout waits for input."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = ""
|
||||
evidence = "Ghidra /v2: op_0x73_configure_wait_indicator@0x41e900 passes operands 1..9 to adv_text_configure_wait_indicator@0x44ff60 and operand 10 to adv_indicator_set_frame_period@0x44d060. The worker writes surface/source rect, layout-relative destination, enabled=1, and terminal/column values to the selected layout at +0x3c..+0x60. SYSTEM4 executes `set-texture 0x337c 0xc 0xff00` (raw id 0x337c = SO000.AGF, a 390x27 strip of thirteen 30x27 bat frames), then `0x73 1 385 140 12 0 0 30 27 12 48`; layout 1 begins at y=430, placing the 30x27 marker at screen (385,570), matching the original. Op 0x72 activates this descriptor only after reveal completion."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "layout slot (0 = current)"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "destination x, layout-relative"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 3
|
||||
role = ""
|
||||
role = "destination y, layout-relative"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 4
|
||||
role = ""
|
||||
role = "source surface slot"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 5
|
||||
role = ""
|
||||
role = "source x"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 6
|
||||
role = ""
|
||||
role = "source y"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 7
|
||||
role = ""
|
||||
role = "cell width"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 8
|
||||
role = ""
|
||||
role = "cell height"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 9
|
||||
role = ""
|
||||
role = "terminal frame index / sheet columns"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 10
|
||||
role = ""
|
||||
role = "frame period (milliseconds)"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
|
||||
Reference in New Issue
Block a user