Implement visible ADV History presentation
This commit is contained in:
@@ -1310,11 +1310,11 @@ future numbered-save integration seam, not a reason to choose a shared-profile b
|
||||
History button can be complete first, while save/load restoration stays deferred with the wider storage
|
||||
architecture decision.
|
||||
|
||||
The remaining supporting gaps are ordinary presentation operations: primary/ruby font sizes (`0x75/0x197`),
|
||||
font weight (`0x2bd`), colors/effect mode/offset (`0x76/0x77/0x78/0x1a4`), layout origin (`0x198`), surface
|
||||
rectangle fill (`0x20b`), message-window alpha (`0x131`), and retained-object presentation (`0x222`). The
|
||||
`0xd3/0xd4/0xd5` callback-sequence family drives the smooth scrollbar interpolation and is the main
|
||||
remaining scheduler detail; omitting it would affect motion fidelity, not the backlog data model.
|
||||
History's display support consists of the ordinary presentation operations: primary/ruby font sizes
|
||||
(`0x75/0x197`), font weight (`0x2bd`), colors/effect mode/offset (`0x76/0x77/0x78/0x1a4`), layout origin
|
||||
(`0x198`), surface rectangle fill (`0x20b`), message-window alpha (`0x131`), and retained-object presentation
|
||||
(`0x222`). The `0xd3/0xd4/0xd5` callback-sequence family drives the smooth scrollbar interpolation; omitting
|
||||
it affects motion fidelity, not the backlog data model.
|
||||
|
||||
The first port slice now implements an engine/session-owned `AdvTextHistory` service. `GameSession` carries
|
||||
one instance across VM scene runs, while a standalone VM owns its instance for the full top-level and nested
|
||||
@@ -1334,6 +1334,26 @@ and does not mutate it. Query scans stop at the next group-start record; their t
|
||||
the native helpers. History's remaining work is therefore presentation and interaction rather than backlog
|
||||
data access.
|
||||
|
||||
The third port slice implements visible History presentation without moving canonical records into Godot.
|
||||
Op `0x1d1` asks `AdvTextHistory` to select one retained group, skip metadata/voice records, and build a
|
||||
host-facing render batch using the target layout configured by `0x70/0x71`, positioned by `0x198`, and
|
||||
seeded by `0x7a`. `HISTORY.BIN` passes zero for flags and both color overrides, so this is its exact ordinary
|
||||
binding path; the native flag-4 raster-without-binding and flag-8 side-effect modes remain outside the live
|
||||
game call site. Godot owns only replaceable labels keyed by target layout, applies the retained font/color/
|
||||
effect snapshot, and clears stale labels whenever `0x71` resets that layout.
|
||||
|
||||
The same slice implements the support calls at their natural seams. Op `0x131` reads a host configuration
|
||||
property (currently defaulting to zero; choosing a profile/config persistence backend remains deferred),
|
||||
`0x20b` clears the addressed region of the mutable name-strip text surface, and `0x222` requests one retained
|
||||
recomposition boundary. Surface text is now a list rather than one draw per slot: HISTORY's five 600x30
|
||||
source rows therefore retain five independent speaker names, and compositor source-rectangle clipping maps
|
||||
each one into its bound object. A real-script regression retains SC0000 page one, runs unmodified
|
||||
`HISTORY.BIN`, observes a non-empty rendered row, and reaches its `(0,60000)` presentation call.
|
||||
|
||||
History's remaining work is interaction fidelity: `0x12e` rectangle hover/hit selection, stored voice replay
|
||||
through `0x1bd`, and the `0xd3/0xd4/0xd5` smooth-scroll callback scheduler. None changes backlog ownership or
|
||||
requires choosing a save/profile backend.
|
||||
|
||||
The original dependency order was **Hide Window first** to establish reusable callback/coroutine input, then
|
||||
Read-message Skip, then History after both the input layer and message-completion seam exist. Hide Window is
|
||||
now complete. Read-message Skip's semantics remain understood, but its implementation is deferred until the
|
||||
|
||||
@@ -321,7 +321,7 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit
|
||||
### 0x20b `fill-surface-rect` (fill-surface-rect, argc 7)
|
||||
- **summary:** (surface_slot)(x)(y)(width)(height)(alpha)(rgb) - fill a clipped rectangle on a graphics surface with the supplied color and alpha.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x20b_handler@0x422d50 converts x/y/width/height to an exclusive rectangle, clamps alpha to 255, repacks RGB to native ARGB, and calls the surface manager's rectangle-fill path at 0x4790e0. HISTORY.BIN clears each 400x30 name strip on temporary surface 0xc1 before draw-string.
|
||||
- **evidence:** Ghidra /v2: op_0x20b_handler@0x422d50 converts x/y/width/height to an exclusive rectangle, clamps alpha to 255, repacks RGB to native ARGB, and calls the surface manager's rectangle-fill path at 0x4790e0. HISTORY.BIN clears each 600x30 name strip on temporary surface 0xc1 before draw-string.
|
||||
|
||||
### 0x20c `present-frame` (present-frame, argc 0)
|
||||
- **summary:** Present the composited frame; label_1235a uses this on the read/message-skip branch to expose the completed foreground endpoint immediately.
|
||||
|
||||
@@ -1728,3 +1728,27 @@ remaining effectful gaps total 21 instructions.
|
||||
**Next:** implement History's display slice around `0x1d1`, layout origin `0x198`, message-window alpha
|
||||
`0x131`, surface fill `0x20b`, and retained-object presentation `0x222`. After visible backlog rows render,
|
||||
finish rectangle hover/interaction, voice replay, and the `0xd3/0xd4/0xd5` smooth-scroll scheduler details.
|
||||
|
||||
### ADV History visible display implemented (2026-07-18)
|
||||
|
||||
The third bounded slice is complete. Op `0x1d1` now selects one retained group and produces an engine-owned
|
||||
render batch for the requested target text layout. `0x198` updates that layout's origin, `0x71` clears stale
|
||||
host presentation, and the existing cursor/style setters provide the target baseline and retained font,
|
||||
color, outline, and effect state. The live `HISTORY.BIN` call passes flags/colors as zero, so the port renders
|
||||
its ordinary bound-text path without inventing behavior for unused native special modes.
|
||||
|
||||
Godot maintains per-layout History labels while the engine remains the canonical backlog owner. The surface-
|
||||
text path was generalized from one string per surface to clipped lists, allowing all five speaker-name rows
|
||||
drawn into surface `0xc1` to coexist. Op `0x20b` clears each reused 600x30 name region, op `0x131` reads a
|
||||
host configuration seam with a backend-neutral default, and op `0x222` explicitly publishes the retained
|
||||
rebuild. No persistence, RT.DAT, or port-owned profile format was introduced.
|
||||
|
||||
Four focused regressions cover target origin/cursor/style, alpha/fill/present operand shaping, layout reset,
|
||||
and an end-to-end real-script path: real SC0000 page-one records are shared into unmodified `HISTORY.BIN`,
|
||||
which produces a non-empty visible row and reaches `(0,60000)` presentation without a step-cap halt.
|
||||
Validation: engine 194/194, zero-warning Godot build, threaded `SELFTEST OK`. HISTORY is now 68/78 distinct
|
||||
opcodes and 840/854 instructions handled or safe-noop; its ten remaining effectful gaps total 14 instructions.
|
||||
|
||||
**Next:** implement the basic History interaction slice around rectangle hit/hover (`0x12e`) and clean exit
|
||||
using the existing callback layer. Then add stored voice replay (`0x1bd`); leave `0xd3/0xd4/0xd5` smooth
|
||||
scroll interpolation as a separate fidelity slice.
|
||||
|
||||
102
engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs
Normal file
102
engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
|
||||
public class HistoryPresentationOpsTests
|
||||
{
|
||||
private const int T_IMM = 0, T_GINT = 3;
|
||||
private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
private static Operand I(long value) => new(T_IMM, value);
|
||||
private static Operand G(int address) => new(T_GINT, address);
|
||||
|
||||
[Fact]
|
||||
public void RenderHistoryUsesTheSelectedTargetLayoutAndOneRetainedGroup()
|
||||
{
|
||||
var history = new AdvTextHistory();
|
||||
history.DefineLayout(1, 640, 160, 80, 430);
|
||||
history.AppendMetadata(9, 2, AdvTextStyle.Default);
|
||||
history.AppendText(0, 10, "retained dialogue", new AdvTextStyle(24, 8, false,
|
||||
0xffffff, 0x606060, 3, 1, 1));
|
||||
history.ResetLayout(1);
|
||||
history.AppendText(0, 11, "next group", AdvTextStyle.Default);
|
||||
history.SetRecordingEnabled(false);
|
||||
history.DefineLayout(4, 600, 150, 0, 0);
|
||||
history.SetRecordingEnabled(true);
|
||||
|
||||
var script = ScriptAssembler.Assemble(Table, "HISTORY_RENDER",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x198, new[] { I(4), I(65), I(150) }),
|
||||
(0x7a, new[] { I(4), I(45), I(42) }),
|
||||
(0x1d1, new[] { I(4), I(0), I(0), I(0), I(0) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(script, Table, host, textHistory: history);
|
||||
|
||||
vm.Run();
|
||||
|
||||
var render = Assert.Single(host.HistoryRenders);
|
||||
Assert.Equal((4, 0, "retained dialogue"),
|
||||
(render.LayoutSlot, render.FirstRecordIndex, render.Text));
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(4, 600, 150, 65, 150, 45, 42), render.Layout);
|
||||
Assert.Equal((24, 0xffffffL, 0x606060L),
|
||||
(render.Style.PrimaryFontSize, render.Style.TextColor, render.Style.EffectColor));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HistorySupportingPresentationOpsReachTheHostWithNativeShaping()
|
||||
{
|
||||
var script = ScriptAssembler.Assemble(Table, "HISTORY_PRESENT_SUPPORT",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x131, new[] { G(0x100) }),
|
||||
(0x20b, new[] { I(0xc1), I(0), I(30), I(600), I(30), I(999), I(0x123456) }),
|
||||
(0x222, new[] { I(0), I(60000) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost { MessageWindowAlphaSetting = 6 };
|
||||
var vm = new VirtualMachine(script, Table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(6, vm.Globals[0x100]);
|
||||
Assert.Equal(new SurfaceRectFill(0xc1, 0, 30, 600, 30, 255, 0x123456),
|
||||
Assert.Single(host.SurfaceFills));
|
||||
Assert.Equal((0L, 60000L), Assert.Single(host.PresentedRanges));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetLayoutClearsItsPreviouslyBoundHostPresentation()
|
||||
{
|
||||
var script = ScriptAssembler.Assemble(Table, "HISTORY_CLEAR_RENDER",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x71, new[] { I(5) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
|
||||
new VirtualMachine(script, Table, host).Run();
|
||||
|
||||
Assert.Equal(5, Assert.Single(host.ClearedTextLayouts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealHistoryScriptBuildsVisibleRowsFromARealSc0000Page()
|
||||
{
|
||||
var scripts = Sys4ScriptProvider.Load(Table);
|
||||
var session = new GameSession();
|
||||
session.RunScene(scripts.RequireByName("SC0000.BIN"), Table, new RecordingHost(),
|
||||
new VmOptions(MaxSteps: 1_000_000, HaltAtWaitForInput: true), scripts);
|
||||
var host = new RecordingHost();
|
||||
|
||||
var result = session.RunScene(scripts.RequireByName("HISTORY.BIN"), Table, host,
|
||||
new VmOptions(MaxSteps: 2_000_000, HaltAtWaitForInput: true), scripts);
|
||||
|
||||
Assert.NotEqual("step-cap", result.Halt);
|
||||
Assert.Contains(host.HistoryRenders, render => render.Text.Length > 0);
|
||||
Assert.Contains(host.PresentedRanges, range => range == (0L, 60000L));
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ internal 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<AdvTextHistoryRenderBatch> HistoryRenders = new();
|
||||
public readonly List<int> ClearedTextLayouts = new();
|
||||
public readonly List<SurfaceRectFill> SurfaceFills = new();
|
||||
public readonly List<(long First, long Count)> PresentedRanges = new();
|
||||
public readonly List<AdvWaitIndicatorConfig> WaitIndicators = new();
|
||||
public readonly List<long> SleptDurations = new();
|
||||
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
||||
@@ -32,6 +36,14 @@ internal 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 DrawStringToSurface(int surfaceSlot, int x, int y, string text, AdvTextStyle style)
|
||||
=> SurfaceStrings.Add((surfaceSlot, x, y, text));
|
||||
public void ClearRenderedAdvTextLayout(int layoutSlot) => ClearedTextLayouts.Add(layoutSlot);
|
||||
public void RenderTextHistory(AdvTextHistoryRenderBatch batch) => HistoryRenders.Add(batch);
|
||||
public int MessageWindowAlphaSetting { get; set; }
|
||||
public void FillSurfaceRect(SurfaceRectFill fill) => SurfaceFills.Add(fill);
|
||||
public void PresentObjectRange(GfxState gfx, long firstHandle, long count)
|
||||
=> PresentedRanges.Add((firstHandle, count));
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
||||
public void SetAdvPagePresentationSuspended(bool suspended)
|
||||
=> AdvPagePresentationSuspended.Add(suspended);
|
||||
|
||||
@@ -10,6 +10,9 @@ public readonly record struct AdvWaitIndicatorConfig(
|
||||
public readonly record struct AdvAutoWaitState(
|
||||
bool Enabled, bool VoicePending, long PostVoiceDelayMs, long UnvoicedDelayMs);
|
||||
|
||||
public readonly record struct SurfaceRectFill(
|
||||
int SurfaceSlot, int X, int Y, int Width, int Height, int Alpha, long Rgb);
|
||||
|
||||
public interface IHost
|
||||
{
|
||||
void ShowText(int offset, string text);
|
||||
@@ -17,6 +20,13 @@ 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 DrawStringToSurface(int surfaceSlot, int x, int y, string text, AdvTextStyle style)
|
||||
=> DrawStringToSurface(surfaceSlot, x, y, text);
|
||||
void ClearRenderedAdvTextLayout(int layoutSlot) { }
|
||||
void RenderTextHistory(AdvTextHistoryRenderBatch batch) { }
|
||||
int MessageWindowAlphaSetting => 0;
|
||||
void FillSurfaceRect(SurfaceRectFill fill) { }
|
||||
void PresentObjectRange(GfxState gfx, long firstHandle, long count) { }
|
||||
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
||||
// 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
|
||||
|
||||
@@ -58,6 +58,15 @@ public sealed record AdvTextHistoryRecord(
|
||||
/// <summary>AGE's logical 8-byte history index entry.</summary>
|
||||
public readonly record struct AdvTextHistoryEntry(int LayoutSlot, int FirstRecordIndex);
|
||||
|
||||
/// <summary>The host-facing result of rendering one retained history group into a target ADV layout.</summary>
|
||||
public sealed record AdvTextHistoryRenderBatch(
|
||||
int LayoutSlot,
|
||||
int FirstRecordIndex,
|
||||
int Flags,
|
||||
AdvTextLayoutSnapshot Layout,
|
||||
string Text,
|
||||
AdvTextStyle Style);
|
||||
|
||||
/// <summary>
|
||||
/// Engine-owned retained ADV backlog. It deliberately has no persistence behavior: native numbered-save
|
||||
/// restoration belongs to the future unified save architecture, while live HISTORY.BIN reads this model.
|
||||
@@ -115,6 +124,14 @@ public sealed class AdvTextHistory
|
||||
layout.CursorY = y;
|
||||
}
|
||||
|
||||
public void SetLayoutOrigin(int requestedSlot, int x, int y)
|
||||
{
|
||||
int slot = ResolveLayout(requestedSlot);
|
||||
var layout = GetOrCreateLayout(slot);
|
||||
layout.OriginX = x;
|
||||
layout.OriginY = y;
|
||||
}
|
||||
|
||||
public void AppendText(int requestedSlot, int sourceOffset, string text, AdvTextStyle style,
|
||||
AdvTextHistoryRecordFlags flags = AdvTextHistoryRecordFlags.None)
|
||||
{
|
||||
@@ -214,6 +231,42 @@ public sealed class AdvTextHistory
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the ordinary bound-text result of native op 0x1d1. The game-facing HISTORY path passes flags
|
||||
/// and colors as zero: metadata and voice records are skipped, while adjacent text records in the same
|
||||
/// group are emitted continuously into the selected target layout.
|
||||
/// </summary>
|
||||
public bool TryBuildRenderBatch(int requestedLayoutSlot, int firstRecordIndex, int flags,
|
||||
long overrideTextColor, long overrideEffectColor,
|
||||
out AdvTextHistoryRenderBatch batch)
|
||||
{
|
||||
batch = null!;
|
||||
if ((uint)firstRecordIndex >= (uint)_records.Count) return false;
|
||||
|
||||
int slot = ResolveLayout(requestedLayoutSlot);
|
||||
var target = SnapshotLayout(slot);
|
||||
var text = new System.Text.StringBuilder();
|
||||
AdvTextStyle style = AdvTextStyle.Default;
|
||||
bool haveStyle = false;
|
||||
foreach (var record in EnumerateGroup(firstRecordIndex))
|
||||
{
|
||||
if (record.Flags.HasFlag(AdvTextHistoryRecordFlags.NavigationFiltered) && (flags & 1) == 0)
|
||||
break;
|
||||
if (record.Kind != AdvTextHistoryRecordKind.Text) continue;
|
||||
if (!haveStyle)
|
||||
{
|
||||
style = record.Style;
|
||||
haveStyle = true;
|
||||
}
|
||||
text.Append(record.Text);
|
||||
}
|
||||
|
||||
if ((flags & 2) != 0)
|
||||
style = style with { TextColor = overrideTextColor, EffectColor = overrideEffectColor };
|
||||
batch = new AdvTextHistoryRenderBatch(slot, firstRecordIndex, flags, target, text.ToString(), style);
|
||||
return true;
|
||||
}
|
||||
|
||||
private int SelectLayout(int requestedSlot)
|
||||
{
|
||||
int slot = requestedSlot == 0 ? CurrentLayoutSlot : requestedSlot;
|
||||
@@ -233,6 +286,13 @@ public sealed class AdvTextHistory
|
||||
return layout;
|
||||
}
|
||||
|
||||
private AdvTextLayoutSnapshot SnapshotLayout(int slot)
|
||||
{
|
||||
var layout = GetOrCreateLayout(slot);
|
||||
return new AdvTextLayoutSnapshot(slot, layout.Width, layout.Height,
|
||||
layout.OriginX, layout.OriginY, layout.CursorX, layout.CursorY);
|
||||
}
|
||||
|
||||
private void AppendBoundary(int slot)
|
||||
{
|
||||
if (RecordingSuppressed) return;
|
||||
|
||||
@@ -499,6 +499,7 @@ public sealed class VirtualMachine
|
||||
return pc + 1;
|
||||
case "reset-adv-text-layout": // 0x71: reset layout and begin the next logical retained group
|
||||
TextHistory.ResetLayout((int)Read(a[0]));
|
||||
_host.ClearRenderedAdvTextLayout((int)Read(a[0]));
|
||||
return pc + 1;
|
||||
case "set-adv-text-cursor": // 0x7a (layout slot, x, y); slot 0 means current natively
|
||||
TextHistory.SetCursor((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]));
|
||||
@@ -510,7 +511,8 @@ public sealed class VirtualMachine
|
||||
(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]));
|
||||
_host.DrawStringToSurface((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), ReadStr(a[3]),
|
||||
_advTextStyle);
|
||||
return pc + 1;
|
||||
case "wait-for-input":
|
||||
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
|
||||
@@ -680,6 +682,15 @@ public sealed class VirtualMachine
|
||||
Write(a[1], -1);
|
||||
}
|
||||
return pc + 1;
|
||||
case "render-text-history": // 0x1d1: rasterize/bind one retained group to a target layout
|
||||
case "u0041BAE0":
|
||||
{
|
||||
int flags = (int)Read(a[2]);
|
||||
if ((flags & 4) == 0 && TextHistory.TryBuildRenderBatch(
|
||||
(int)Read(a[0]), (int)Read(a[1]), flags, Read(a[3]), Read(a[4]), out var batch))
|
||||
_host.RenderTextHistory(batch);
|
||||
return pc + 1;
|
||||
}
|
||||
case "u0041BB90":
|
||||
case "find-text-history-value": // 0x1d3: operand 3 is accepted but ignored natively
|
||||
{
|
||||
@@ -715,6 +726,13 @@ public sealed class VirtualMachine
|
||||
EffectOffsetY = (int)Read(a[1])
|
||||
};
|
||||
return pc + 1;
|
||||
case "set-adv-text-layout-origin": // 0x198; slot 0 selects the current layout
|
||||
case "u0041B540":
|
||||
TextHistory.SetLayoutOrigin((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]));
|
||||
return pc + 1;
|
||||
case "get-message-window-alpha": // 0x131: host profile/config seam; default host currently supplies 0
|
||||
case "u00415F70":
|
||||
Write(a[0], _host.MessageWindowAlphaSetting); return pc + 1;
|
||||
case "u00415BF0":
|
||||
case "reset-message-skip-input": // 0x101 clears transient input/run bits, not op 0x88 state
|
||||
return pc + 1;
|
||||
@@ -743,6 +761,12 @@ public sealed class VirtualMachine
|
||||
Write(a[1], gw); Write(a[2], gh);
|
||||
return pc + 1;
|
||||
}
|
||||
case "fill-surface-rect": // 0x20b: clipped alpha/RGB fill of a mutable surface
|
||||
case "u00420D50":
|
||||
_host.FillSurfaceRect(new SurfaceRectFill(
|
||||
(int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[4]),
|
||||
(int)System.Math.Min(Read(a[5]), 255), Read(a[6]) & 0x00ff_ffff));
|
||||
return pc + 1;
|
||||
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
|
||||
case "play-voice":
|
||||
_autoVoicePending = true;
|
||||
@@ -879,6 +903,9 @@ public sealed class VirtualMachine
|
||||
_host.WaitForForegroundTransition(Gfx); return pc + 1;
|
||||
case "clear-gfx-command-queue": // 0x224: retained compositor does not use this native queue
|
||||
return pc + 1;
|
||||
case "present-gfx-object-range": // 0x222: publish pending retained changes in the selected range
|
||||
case "u004216C0":
|
||||
_host.PresentObjectRange(Gfx, Read(a[0]), Read(a[1])); return pc + 1;
|
||||
default:
|
||||
// Stub is per-instruction frequency (the VM handles ~30 ops; the rest hit here, e.g.
|
||||
// 0x258/0x259 stmt markers appear en masse), so gate it with Step — else --trace floods.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using Age.Engine.Hosting;
|
||||
@@ -30,7 +31,8 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly System.Threading.AutoResetEvent _frameSignal = new(false);
|
||||
private volatile bool _stopping;
|
||||
private readonly object _textLock = new();
|
||||
private readonly Dictionary<int, SurfaceTextDraw> _surfaceText = new();
|
||||
private readonly Dictionary<int, List<SurfaceTextDraw>> _surfaceText = new();
|
||||
private readonly Dictionary<int, AdvTextHistoryRenderBatch> _historyText = new();
|
||||
private string _advText = "";
|
||||
private int _advTextX = 100, _advTextY = 47;
|
||||
private int _currentAdvLayout = 1; // SYSTEM4's ordinary SC0000 ADV layout
|
||||
@@ -99,8 +101,17 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
|
||||
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
||||
=> DrawStringToSurface(surfaceSlot, x, y, text, AdvTextStyle.Default);
|
||||
|
||||
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text, AdvTextStyle style)
|
||||
{
|
||||
lock (_textLock) _surfaceText[surfaceSlot] = new SurfaceTextDraw(x, y, text);
|
||||
lock (_textLock)
|
||||
{
|
||||
if (!_surfaceText.TryGetValue(surfaceSlot, out var draws))
|
||||
_surfaceText[surfaceSlot] = draws = new List<SurfaceTextDraw>();
|
||||
draws.RemoveAll(draw => draw.X == x && draw.Y == y);
|
||||
draws.Add(new SurfaceTextDraw(x, y, text, style));
|
||||
}
|
||||
_timeline?.Event("draw-string", new() { ["surface"] = surfaceSlot, ["x"] = x, ["y"] = y, ["text"] = text });
|
||||
}
|
||||
|
||||
@@ -115,8 +126,57 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetSurfaceText(int surfaceSlot, out SurfaceTextDraw draw)
|
||||
{ lock (_textLock) return _surfaceText.TryGetValue(surfaceSlot, out draw); }
|
||||
public IReadOnlyList<SurfaceTextDraw> SnapshotSurfaceText(int surfaceSlot)
|
||||
{
|
||||
lock (_textLock)
|
||||
return _surfaceText.TryGetValue(surfaceSlot, out var draws) ? draws.ToArray() : Array.Empty<SurfaceTextDraw>();
|
||||
}
|
||||
|
||||
public void ClearRenderedAdvTextLayout(int layoutSlot)
|
||||
{
|
||||
lock (_textLock) _historyText.Remove(layoutSlot == 0 ? _currentAdvLayout : layoutSlot);
|
||||
}
|
||||
|
||||
public void RenderTextHistory(AdvTextHistoryRenderBatch batch)
|
||||
{
|
||||
lock (_textLock) _historyText[batch.LayoutSlot] = batch;
|
||||
_timeline?.Event("history-render", new()
|
||||
{
|
||||
["layout"] = batch.LayoutSlot, ["record"] = batch.FirstRecordIndex,
|
||||
["x"] = batch.Layout.OriginX + batch.Layout.CursorX,
|
||||
["y"] = batch.Layout.OriginY + batch.Layout.CursorY,
|
||||
["text"] = batch.Text,
|
||||
});
|
||||
}
|
||||
|
||||
public IReadOnlyList<AdvTextHistoryRenderBatch> SnapshotRenderedTextHistory()
|
||||
{
|
||||
lock (_textLock) return _historyText.Values.OrderBy(batch => batch.LayoutSlot).ToArray();
|
||||
}
|
||||
|
||||
public int MessageWindowAlphaSetting => 0;
|
||||
|
||||
public void FillSurfaceRect(SurfaceRectFill fill)
|
||||
{
|
||||
lock (_textLock)
|
||||
{
|
||||
if (!_surfaceText.TryGetValue(fill.SurfaceSlot, out var draws)) return;
|
||||
int right = fill.X + System.Math.Max(0, fill.Width);
|
||||
int bottom = fill.Y + System.Math.Max(0, fill.Height);
|
||||
draws.RemoveAll(draw => draw.X >= fill.X && draw.X < right && draw.Y >= fill.Y && draw.Y < bottom);
|
||||
}
|
||||
_timeline?.Event("surface-fill", new()
|
||||
{
|
||||
["surface"] = fill.SurfaceSlot, ["x"] = fill.X, ["y"] = fill.Y,
|
||||
["w"] = fill.Width, ["h"] = fill.Height, ["alpha"] = fill.Alpha, ["rgb"] = fill.Rgb,
|
||||
});
|
||||
}
|
||||
|
||||
public void PresentObjectRange(GfxState gfx, long firstHandle, long count)
|
||||
{
|
||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||
_timeline?.Event("present-object-range", new() { ["first"] = firstHandle, ["count"] = count });
|
||||
}
|
||||
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
|
||||
{
|
||||
@@ -608,6 +668,6 @@ 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, AdvTextStyle Style);
|
||||
public readonly record struct AdvWaitIndicatorSnapshot(
|
||||
RgbaImage Image, string Name, int AssetId, AdvWaitIndicatorConfig Config, int Frame);
|
||||
|
||||
@@ -27,6 +27,10 @@ public partial class Main : Godot.Control
|
||||
private readonly byte[] _screenPixels = new byte[ScreenWidth * ScreenHeight * 4];
|
||||
private Label _text = null!;
|
||||
private Label _speaker = null!;
|
||||
private readonly System.Collections.Generic.List<Label> _surfaceTextLabels = new();
|
||||
private readonly System.Collections.Generic.Dictionary<int, Label> _historyTextLabels = new();
|
||||
private Font? _presentationRegularFont;
|
||||
private FontVariation? _presentationBoldFont;
|
||||
private Label _status = null!;
|
||||
private Label _locatorHud = null!;
|
||||
private AudioStreamPlayer _bgm = null!; // looping background music
|
||||
@@ -100,6 +104,7 @@ public partial class Main : Godot.Control
|
||||
_speaker = new Label { MouseFilter = MouseFilterEnum.Ignore, Visible = false };
|
||||
_speaker.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
AddChild(_speaker);
|
||||
_surfaceTextLabels.Add(_speaker);
|
||||
_status = new Label();
|
||||
_status.SetAnchorsAndOffsetsPreset(LayoutPreset.BottomWide);
|
||||
_status.OffsetLeft = 40; _status.OffsetTop = -60;
|
||||
@@ -285,6 +290,7 @@ public partial class Main : Godot.Control
|
||||
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
||||
if (!_selftest && _host != null) UpdateAdvTextPresentation();
|
||||
if (!_selftest && _host != null) UpdateAdvWaitIndicatorPresentation();
|
||||
if (!_selftest && _host != null) UpdateHistoryTextPresentation();
|
||||
// --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)
|
||||
@@ -445,7 +451,8 @@ public partial class Main : Godot.Control
|
||||
private void Recomposite()
|
||||
{
|
||||
System.Array.Clear(_screenPixels);
|
||||
_speaker.Visible = false;
|
||||
foreach (var label in _surfaceTextLabels) label.Visible = false;
|
||||
int surfaceTextLabelIndex = 0;
|
||||
System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null;
|
||||
int z = 0;
|
||||
var visible = _vm.Gfx.SnapshotVisibleObjects(_clock.NowMs); // one synchronized sample for objects + ranges
|
||||
@@ -506,13 +513,21 @@ public partial class Main : Godot.Control
|
||||
}
|
||||
decisions?.Add(v.Handle, $"z{z} {outcome}");
|
||||
var rawObject = _vm.Gfx.TryGet(v.Handle);
|
||||
if (rawObject != null && _host.TryGetSurfaceText(rawObject.SourceSlot, out var surfaceText))
|
||||
if (rawObject != null)
|
||||
{
|
||||
var textPos = localToDest.Apply(surfaceText.X, surfaceText.Y);
|
||||
_speaker.Position = new Vector2((float)textPos.X, (float)textPos.Y);
|
||||
_speaker.Size = new Vector2(System.Math.Max(1, v.W - surfaceText.X), System.Math.Max(1, v.H - surfaceText.Y));
|
||||
_speaker.Text = surfaceText.Text;
|
||||
_speaker.Visible = true;
|
||||
foreach (var surfaceText in _host.SnapshotSurfaceText(rawObject.SourceSlot))
|
||||
{
|
||||
if (surfaceText.X < v.SrcX || surfaceText.X >= v.SrcX + v.W ||
|
||||
surfaceText.Y < v.SrcY || surfaceText.Y >= v.SrcY + v.H) continue;
|
||||
var textPos = localToDest.Apply(surfaceText.X - v.SrcX, surfaceText.Y - v.SrcY);
|
||||
var label = GetSurfaceTextLabel(surfaceTextLabelIndex++);
|
||||
label.Position = new Vector2((float)textPos.X, (float)textPos.Y);
|
||||
label.Size = new Vector2(System.Math.Max(1, v.W - (surfaceText.X - v.SrcX)),
|
||||
System.Math.Max(1, v.H - (surfaceText.Y - v.SrcY)));
|
||||
label.Text = surfaceText.Text;
|
||||
ApplyAdvTextStyle(label, surfaceText.Style);
|
||||
label.Visible = true;
|
||||
}
|
||||
}
|
||||
z++;
|
||||
}
|
||||
@@ -532,6 +547,76 @@ public partial class Main : Godot.Control
|
||||
_text.Text = count == 0 ? "" : t.Text[..count];
|
||||
}
|
||||
|
||||
private void UpdateHistoryTextPresentation()
|
||||
{
|
||||
foreach (var label in _historyTextLabels.Values) label.Visible = false;
|
||||
foreach (var batch in _host.SnapshotRenderedTextHistory())
|
||||
{
|
||||
if (batch.Text.Length == 0) continue;
|
||||
if (!_historyTextLabels.TryGetValue(batch.LayoutSlot, out var label))
|
||||
{
|
||||
label = CreateAdvPresentationLabel();
|
||||
_historyTextLabels.Add(batch.LayoutSlot, label);
|
||||
}
|
||||
var layout = batch.Layout;
|
||||
label.Position = new Vector2(layout.OriginX + layout.CursorX, layout.OriginY + layout.CursorY);
|
||||
label.Size = new Vector2(System.Math.Max(1, layout.Width - layout.CursorX),
|
||||
System.Math.Max(1, layout.Height - layout.CursorY));
|
||||
label.Text = batch.Text;
|
||||
ApplyAdvTextStyle(label, batch.Style);
|
||||
label.Visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
private Label GetSurfaceTextLabel(int index)
|
||||
{
|
||||
while (_surfaceTextLabels.Count <= index) _surfaceTextLabels.Add(CreateAdvPresentationLabel());
|
||||
return _surfaceTextLabels[index];
|
||||
}
|
||||
|
||||
private Label CreateAdvPresentationLabel()
|
||||
{
|
||||
var label = new Label
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
Visible = false,
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
||||
ClipText = true,
|
||||
};
|
||||
label.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
label.AddThemeFontOverride("font", _text.GetThemeFont("font"));
|
||||
AddChild(label);
|
||||
return label;
|
||||
}
|
||||
|
||||
private void ApplyAdvTextStyle(Label label, AdvTextStyle style)
|
||||
{
|
||||
int fontSize = style.PrimaryFontSize > 0 ? style.PrimaryFontSize : 24;
|
||||
_presentationRegularFont ??= _text.GetThemeFont("font");
|
||||
if (style.Bold)
|
||||
{
|
||||
_presentationBoldFont ??= new FontVariation
|
||||
{
|
||||
BaseFont = _presentationRegularFont,
|
||||
VariationEmbolden = 1.2f,
|
||||
};
|
||||
label.AddThemeFontOverride("font", _presentationBoldFont);
|
||||
}
|
||||
else label.AddThemeFontOverride("font", _presentationRegularFont);
|
||||
label.AddThemeFontSizeOverride("font_size", fontSize);
|
||||
label.AddThemeColorOverride("font_color", RgbColor(style.TextColor, Colors.White));
|
||||
label.AddThemeColorOverride("font_outline_color", RgbColor(style.EffectColor, new Color(0.38f, 0.38f, 0.38f)));
|
||||
int outline = style.RenderMode == 0 ? 0 : System.Math.Max(1,
|
||||
System.Math.Max(System.Math.Abs(style.EffectOffsetX), System.Math.Abs(style.EffectOffsetY)));
|
||||
label.AddThemeConstantOverride("outline_size", outline);
|
||||
}
|
||||
|
||||
private static Color RgbColor(long rgb, Color fallback)
|
||||
{
|
||||
if ((rgb & 0x00ff_ffff) == 0) return fallback;
|
||||
return new Color(((rgb >> 16) & 0xff) / 255f, ((rgb >> 8) & 0xff) / 255f, (rgb & 0xff) / 255f, 1);
|
||||
}
|
||||
|
||||
private void UpdateAdvWaitIndicatorPresentation()
|
||||
{
|
||||
var snapshot = _host.SnapshotAdvWaitIndicator();
|
||||
|
||||
@@ -5070,7 +5070,7 @@ noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra /v2: op_0x20b_handler@0x422d50 converts x/y/width/height to an exclusive rectangle, clamps alpha to 255, repacks RGB to native ARGB, and calls the surface manager's rectangle-fill path at 0x4790e0. HISTORY.BIN clears each 400x30 name strip on temporary surface 0xc1 before draw-string."
|
||||
evidence = "Ghidra /v2: op_0x20b_handler@0x422d50 converts x/y/width/height to an exclusive rectangle, clamps alpha to 255, repacks RGB to native ARGB, and calls the surface manager's rectangle-fill path at 0x4790e0. HISTORY.BIN clears each 600x30 name strip on temporary surface 0xc1 before draw-string."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
|
||||
Reference in New Issue
Block a user