Restore ADV wait indicator

This commit is contained in:
gamer147
2026-07-11 21:12:24 -04:00
parent 0bf6b43a32
commit 5275ec5f4f
10 changed files with 216 additions and 35 deletions

View File

@@ -1050,6 +1050,29 @@ Matching evidence: `build/native-adv-text-trace.jsonl` and Godot timeline captur
non-overlapping name/dialogue bands at y=447468, 478500, and 507530. 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

View File

@@ -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

View File

@@ -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.

View File

@@ -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));
}
}

View File

@@ -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() { }

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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}"

View File

@@ -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]]