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:
|
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`.
|
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)
|
### 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
|
The three SFX opcodes are a retained channel lifecycle, not immediate fire-and-forget calls. Handler
|
||||||
|
|||||||
@@ -5,6 +5,16 @@
|
|||||||
|
|
||||||
## adv
|
## 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)
|
### 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.
|
- **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
|
- **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:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **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)
|
### 0x75 `u0041AC30` (u0041AC30, argc 1)
|
||||||
- **summary:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **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
|
was separately resolved by preserving draw-texture's 200x200 cell and interpreting `(8,4)` as total
|
||||||
frames/columns rather than a grid divisor.
|
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)
|
### 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.
|
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((1, 75, 47), Assert.Single(host.TextCursors));
|
||||||
Assert.Equal((13, 1, 1, "speaker"), Assert.Single(host.SurfaceStrings));
|
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 Offset, string Text)> Lines = new();
|
||||||
public readonly List<(int Slot, int X, int Y)> TextCursors = 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<(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> SleptDurations = new();
|
||||||
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
||||||
public readonly List<int> SfxStarts = 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 SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
|
||||||
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
||||||
=> SurfaceStrings.Add((surfaceSlot, x, y, text));
|
=> SurfaceStrings.Add((surfaceSlot, x, y, text));
|
||||||
|
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
||||||
public void WaitForInput() => Waits++;
|
public void WaitForInput() => Waits++;
|
||||||
public void Sleep(long duration) => SleptDurations.Add(duration);
|
public void Sleep(long duration) => SleptDurations.Add(duration);
|
||||||
public void FrameYield() { }
|
public void FrameYield() { }
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
using Age.Engine.Model;
|
using Age.Engine.Model;
|
||||||
|
|
||||||
namespace Age.Engine.Hosting;
|
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
|
public interface IHost
|
||||||
{
|
{
|
||||||
void ShowText(int offset, string text);
|
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.
|
// op 0x204 rasterizes a string into a numbered surface before 0x1fb binds that surface.
|
||||||
void SetAdvTextCursor(int layoutSlot, int x, int y) { }
|
void SetAdvTextCursor(int layoutSlot, int x, int y) { }
|
||||||
void DrawStringToSurface(int surfaceSlot, int x, int y, string text) { }
|
void DrawStringToSurface(int surfaceSlot, int x, int y, string text) { }
|
||||||
|
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
||||||
void WaitForInput();
|
void WaitForInput();
|
||||||
|
void WaitForInput(int layoutSlot) => WaitForInput();
|
||||||
void Sleep(long duration);
|
void Sleep(long duration);
|
||||||
void FrameYield();
|
void FrameYield();
|
||||||
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive
|
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive
|
||||||
|
|||||||
@@ -283,13 +283,19 @@ public sealed class VirtualMachine
|
|||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "set-adv-text-cursor": // 0x7a (layout slot, x, y); slot 0 means current natively
|
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;
|
_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)
|
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]));
|
_host.DrawStringToSurface((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), ReadStr(a[3]));
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "wait-for-input":
|
case "wait-for-input":
|
||||||
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
|
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
|
||||||
if (_o.HaltAtWaitForInput) { HaltReason ??= "wait-for-input"; return HALT; }
|
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.
|
case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing.
|
||||||
_host.Sleep(Read(a[0])); return pc + 1;
|
_host.Sleep(Read(a[0])); return pc + 1;
|
||||||
case "get-message-skip": // 0x1c7: Ctrl/message fast-forward run-state bit
|
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 string _scene; // e.g. "SC0000" — for section_base
|
||||||
private readonly object _imageLock = new();
|
private readonly object _imageLock = new();
|
||||||
private readonly Dictionary<int, RgbaImage?> _images = new(); // raw catalog id -> decoded pixels
|
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<long, (RgbaImage Image, string Name, int RawIndex)> _movieFrames = new();
|
||||||
private readonly Dictionary<int, long> _movieBySurface = new();
|
private readonly Dictionary<int, long> _movieBySurface = new();
|
||||||
private readonly HashSet<long> _completedMovies = new();
|
private readonly HashSet<long> _completedMovies = new();
|
||||||
@@ -27,8 +28,12 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private readonly Dictionary<int, SurfaceTextDraw> _surfaceText = new();
|
private readonly Dictionary<int, SurfaceTextDraw> _surfaceText = new();
|
||||||
private string _advText = "";
|
private string _advText = "";
|
||||||
private int _advTextX = 100, _advTextY = 47;
|
private int _advTextX = 100, _advTextY = 47;
|
||||||
|
private int _currentAdvLayout = 1; // SYSTEM4's ordinary SC0000 ADV layout
|
||||||
private long _advTextStartedMs;
|
private long _advTextStartedMs;
|
||||||
private bool _advTextForceComplete;
|
private bool _advTextForceComplete;
|
||||||
|
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
||||||
|
private int _activeWaitLayout;
|
||||||
|
private long _waitIndicatorStartedMs;
|
||||||
private GfxState? _foregroundGfx;
|
private GfxState? _foregroundGfx;
|
||||||
public volatile bool IsWaiting;
|
public volatile bool IsWaiting;
|
||||||
public volatile bool IsTransitionWaiting;
|
public volatile bool IsTransitionWaiting;
|
||||||
@@ -75,7 +80,12 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
public void SetAdvTextCursor(int layoutSlot, int x, int y)
|
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 });
|
_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)
|
public bool TryGetSurfaceText(int surfaceSlot, out SurfaceTextDraw draw)
|
||||||
{ lock (_textLock) return _surfaceText.TryGetValue(surfaceSlot, out 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 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++;
|
Pages++;
|
||||||
_main.CallDeferred("PageBreak");
|
_main.CallDeferred("PageBreak");
|
||||||
// Publish retained mutations accumulated before the wait once. A static input wait is not itself a
|
// 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.
|
// reason to rebuild the 800x600 background every frame; ambient channels are queried separately.
|
||||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
|
lock (_textLock) _activeWaitLayout = layoutSlot == 0 ? _currentAdvLayout : layoutSlot;
|
||||||
|
_waitIndicatorStartedMs = _clock.NowMs;
|
||||||
IsWaiting = true;
|
IsWaiting = true;
|
||||||
_timeline?.State("input-wait", new() { ["page"] = Pages });
|
_timeline?.State("input-wait", new() { ["page"] = Pages });
|
||||||
_gate.Wait();
|
_gate.Wait();
|
||||||
@@ -244,13 +289,18 @@ public sealed class GodotAdvHost : IHost
|
|||||||
public void CreateTexture(int slot, int width, int height)
|
public void CreateTexture(int slot, int width, int height)
|
||||||
{
|
{
|
||||||
lock (_textLock) _surfaceText.Remove(slot);
|
lock (_textLock) _surfaceText.Remove(slot);
|
||||||
|
lock (_textLock) _surfaceResources.Remove(slot);
|
||||||
_slotDims[slot] = (width, height);
|
_slotDims[slot] = (width, height);
|
||||||
if (TraceOps) Godot.GD.Print($"[op] create-texture slot={slot} {width}x{height}");
|
if (TraceOps) Godot.GD.Print($"[op] create-texture slot={slot} {width}x{height}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTexture(long resourceId, int slot)
|
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 asset = _res.ResolveTexture(_scene, resourceId);
|
||||||
var image = asset != null ? Decode(asset) : null;
|
var image = asset != null ? Decode(asset) : null;
|
||||||
_slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0);
|
_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 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 TextureRect _screenView = null!; // shows the composited screen backbuffer
|
||||||
private Image _screen = null!; // 800x600 immediate-mode canvas
|
private Image _screen = null!; // 800x600 immediate-mode canvas
|
||||||
private ImageTexture _screenTex = null!;
|
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
|
// 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.
|
// 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];
|
private readonly byte[] _screenPixels = new byte[ScreenWidth * ScreenHeight * 4];
|
||||||
@@ -72,6 +76,17 @@ public partial class Main : Godot.Control
|
|||||||
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||||
AddChild(_screenView); // added first -> draws behind the text/status labels
|
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 = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = MouseFilterEnum.Ignore };
|
||||||
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||||
AddChild(_text);
|
AddChild(_text);
|
||||||
@@ -181,6 +196,16 @@ public partial class Main : Godot.Control
|
|||||||
_host.SetTexture(systemChrome.RawIndex, 0x11);
|
_host.SetTexture(systemChrome.RawIndex, 0x11);
|
||||||
_vm.Gfx.SetSurface(0x11, systemChrome.RawIndex, 0);
|
_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
|
// --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.
|
// INIT2's gfx handle array 0x62455.. (skips the UI scripts LOGO/OP/TITLE). State carries via globals.
|
||||||
if (boot && !_selftest)
|
if (boot && !_selftest)
|
||||||
@@ -230,6 +255,7 @@ public partial class Main : Godot.Control
|
|||||||
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
|
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
|
||||||
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
||||||
if (!_selftest && _host != null) UpdateAdvTextPresentation();
|
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
|
// --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.
|
// verified as distinct frames, not just the final state. Captures after Recomposite; quits when full.
|
||||||
if (_seqDir != null && _seqIdx < _seqFrames && !_done)
|
if (_seqDir != null && _seqIdx < _seqFrames && !_done)
|
||||||
@@ -397,6 +423,33 @@ public partial class Main : Godot.Control
|
|||||||
_text.Text = count == 0 ? "" : t.Text[..count];
|
_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)
|
private static string ColorTimeline(Age.Engine.Model.ColorTransitionState? state)
|
||||||
=> state is { } c
|
=> state is { } c
|
||||||
? $" color=0x{c.Current:x8}->0x{c.Target:x8} colorProgress={c.Progress:0.000}"
|
? $" 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]
|
[opcode.semantics]
|
||||||
name = "wait-for-input"
|
name = "wait-for-input"
|
||||||
category = "unknown"
|
category = "adv"
|
||||||
summary = ""
|
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
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "med"
|
confidence = "high"
|
||||||
depends_on = []
|
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]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
role = ""
|
role = "layout slot (0 = current)"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0x73
|
op = 0x73
|
||||||
label = "u0041AB30"
|
label = "configure-adv-wait-indicator"
|
||||||
argc = 10
|
argc = 10
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "u0041AB30"
|
name = "configure-adv-wait-indicator"
|
||||||
category = "unknown"
|
category = "adv"
|
||||||
summary = ""
|
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
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "low"
|
confidence = "high"
|
||||||
depends_on = []
|
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]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
role = ""
|
role = "layout slot (0 = current)"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 2
|
i = 2
|
||||||
role = ""
|
role = "destination x, layout-relative"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 3
|
i = 3
|
||||||
role = ""
|
role = "destination y, layout-relative"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 4
|
i = 4
|
||||||
role = ""
|
role = "source surface slot"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 5
|
i = 5
|
||||||
role = ""
|
role = "source x"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 6
|
i = 6
|
||||||
role = ""
|
role = "source y"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 7
|
i = 7
|
||||||
role = ""
|
role = "cell width"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 8
|
i = 8
|
||||||
role = ""
|
role = "cell height"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 9
|
i = 9
|
||||||
role = ""
|
role = "terminal frame index / sheet columns"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 10
|
i = 10
|
||||||
role = ""
|
role = "frame period (milliseconds)"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
|
|||||||
Reference in New Issue
Block a user