engine: retain History and wait presentation
This commit is contained in:
@@ -388,6 +388,34 @@ public class AdvTextOpsTests
|
||||
Assert.Single(host.WaitIndicators));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitIndicatorHandlePublishesItsResolvedLayoutBindingToHost()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
static Operand I(long value) => new(0, value);
|
||||
var script = ScriptAssembler.Assemble(table, "WAITMARK_BINDING",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x70, new[] { I(1), I(800), I(160), I(0), I(430) }),
|
||||
(0x79, new[] { I(1), I(100), I(47) }),
|
||||
(0x213, new[] { I(1), I(0xd6d8), I(500) }),
|
||||
(0x212, new[] { I(1), I(0xd674) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
|
||||
new VirtualMachine(script, table, host).Run();
|
||||
|
||||
var retained = Assert.Single(host.WaitIndicatorBindings);
|
||||
Assert.Equal(
|
||||
new AdvTextLayoutPresentationBinding(
|
||||
1, 21, 0xd6d8, 500, 0xd674, 100, 47),
|
||||
retained.Binding);
|
||||
Assert.Equal(
|
||||
new AdvTextLayoutSnapshot(1, 800, 160, 0, 430, 0, 0, 800, 160),
|
||||
retained.Layout);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitIndicatorTerminalFrameIsExclusive()
|
||||
{
|
||||
|
||||
@@ -45,6 +45,10 @@ public class HistoryPresentationOpsTests
|
||||
Assert.Equal((24, 0xffffffL, 0x606060L, 8, "MS 明朝"),
|
||||
(render.Style.PrimaryFontSize, render.Style.TextColor, render.Style.EffectColor,
|
||||
render.Style.LineSpacing, render.Style.FontFace));
|
||||
var retained = Assert.Single(host.RetainedHistoryRenders);
|
||||
Assert.Equal(4, retained.Binding.LayoutSlot);
|
||||
Assert.Equal(0x18, retained.Binding.SourceSurfaceSlot);
|
||||
Assert.Same(render, retained.Batch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Text;
|
||||
|
||||
public class RetainedAdvWaitIndicatorPresentationTests
|
||||
{
|
||||
private static readonly AdvWaitIndicatorConfig Config = new(
|
||||
LayoutSlot: 1,
|
||||
X: 385,
|
||||
Y: 140,
|
||||
SurfaceSlot: 12,
|
||||
SourceX: 0,
|
||||
SourceY: 0,
|
||||
CellWidth: 30,
|
||||
CellHeight: 27,
|
||||
TerminalFrame: 12,
|
||||
FramePeriodMs: 48);
|
||||
|
||||
private static readonly AdvTextLayoutPresentationBinding Binding = new(
|
||||
LayoutSlot: 1,
|
||||
SourceSurfaceSlot: 21,
|
||||
FirstObjectHandle: 0xd6d8,
|
||||
ObjectCapacity: 500,
|
||||
WaitIndicatorObjectHandle: 0xd674,
|
||||
ResetCursorX: 100,
|
||||
ResetCursorY: 47);
|
||||
|
||||
[Fact]
|
||||
public void PublishesAtlasCellAtLayoutRelativePosition()
|
||||
{
|
||||
var presentation = new RetainedAdvWaitIndicatorPresentation(
|
||||
Config, Binding, layoutOriginX: 0, layoutOriginY: 430);
|
||||
var gfx = new GfxState();
|
||||
gfx.SetSurface(12, 0x337c, 0xff00);
|
||||
|
||||
Assert.True(presentation.Update(gfx, elapsedMs: 96, visible: true));
|
||||
|
||||
RenderObject marker = Assert.Single(gfx.SnapshotVisibleObjects());
|
||||
Assert.Equal(
|
||||
(0xd674L, 12, 60, 0, 30, 27, 385, 570),
|
||||
(marker.Handle, gfx.TryGet(marker.Handle)!.SourceSlot, marker.SrcX, marker.SrcY,
|
||||
marker.W, marker.H, marker.DstX, marker.DstY));
|
||||
Assert.Equal(2, presentation.PublishedFrame);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MutatesOnlyWhenFrameChangesAndErasesWhenHidden()
|
||||
{
|
||||
var presentation = new RetainedAdvWaitIndicatorPresentation(
|
||||
Config, Binding, 0, 430);
|
||||
var gfx = new GfxState();
|
||||
|
||||
Assert.True(presentation.Update(gfx, 0, visible: true));
|
||||
Assert.False(presentation.Update(gfx, 47, visible: true));
|
||||
Assert.True(presentation.Update(gfx, 48, visible: true));
|
||||
Assert.Equal(30, Assert.Single(gfx.SnapshotVisibleObjects()).SrcX);
|
||||
|
||||
Assert.True(presentation.Update(gfx, 48, visible: false));
|
||||
Assert.Empty(gfx.SnapshotVisibleObjects());
|
||||
Assert.False(presentation.Update(gfx, 48, visible: false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepublishRestoresAHandleErasedByScript()
|
||||
{
|
||||
var presentation = new RetainedAdvWaitIndicatorPresentation(
|
||||
Config, Binding, 0, 430);
|
||||
var gfx = new GfxState();
|
||||
presentation.Update(gfx, 144, visible: true);
|
||||
gfx.EraseRange(Binding.WaitIndicatorObjectHandle, 1);
|
||||
|
||||
Assert.True(presentation.Republish(gfx, 144, visible: true));
|
||||
|
||||
RenderObject marker = Assert.Single(gfx.SnapshotVisibleObjects());
|
||||
Assert.Equal((0xd674L, 90), (marker.Handle, marker.SrcX));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsMissingHandleAndMismatchedLayout()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new RetainedAdvWaitIndicatorPresentation(
|
||||
Config,
|
||||
Binding with { WaitIndicatorObjectHandle = -1 },
|
||||
0,
|
||||
430));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new RetainedAdvWaitIndicatorPresentation(
|
||||
Config with { LayoutSlot = 2 },
|
||||
Binding,
|
||||
0,
|
||||
430));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ internal class RecordingHost : IHost
|
||||
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<(AdvTextLayoutPresentationBinding Binding, AdvTextHistoryRenderBatch Batch)>
|
||||
RetainedHistoryRenders = new();
|
||||
public readonly Dictionary<int, AdvTextHistoryRenderBatch> ActiveHistoryRenders = new();
|
||||
public int HistoryPresentationEnds;
|
||||
public readonly List<int> ClearedTextLayouts = new();
|
||||
@@ -30,6 +32,8 @@ internal class RecordingHost : IHost
|
||||
public readonly List<(int Slot, int SourceX, int SourceY, int Width, int Height, int X, int Y)>
|
||||
TextureDraws = new();
|
||||
public readonly List<AdvWaitIndicatorConfig> WaitIndicators = new();
|
||||
public readonly List<(AdvTextLayoutPresentationBinding Binding, AdvTextLayoutSnapshot Layout)>
|
||||
WaitIndicatorBindings = new();
|
||||
public readonly List<bool> WaitIndicatorEnabledChanges = new();
|
||||
public readonly List<int> PublishedAdvTextLayouts = new();
|
||||
public readonly List<long> SleptDurations = new();
|
||||
@@ -129,11 +133,21 @@ internal class RecordingHost : IHost
|
||||
HistoryRenders.Add(batch);
|
||||
ActiveHistoryRenders[batch.LayoutSlot] = batch;
|
||||
}
|
||||
public bool RenderTextHistory(
|
||||
GfxState gfx,
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
AdvTextHistoryRenderBatch batch)
|
||||
{
|
||||
RetainedHistoryRenders.Add((binding, batch));
|
||||
RenderTextHistory(batch);
|
||||
return false;
|
||||
}
|
||||
public void EndTextHistoryPresentation()
|
||||
{
|
||||
HistoryPresentationEnds++;
|
||||
ActiveHistoryRenders.Clear();
|
||||
}
|
||||
public void EndTextHistoryPresentation(GfxState gfx) => EndTextHistoryPresentation();
|
||||
public int MessageWindowAlphaSetting { get; set; }
|
||||
public void SetMessageWindowAlphaSetting(int value) => MessageWindowAlphaSetting = value;
|
||||
public void FillSurfaceRect(SurfaceRectFill fill) => SurfaceFills.Add(fill);
|
||||
@@ -141,6 +155,10 @@ internal class RecordingHost : IHost
|
||||
public void PresentObjectRange(GfxState gfx, long firstHandle, long count)
|
||||
=> PresentedRanges.Add((firstHandle, count));
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
||||
public void BindAdvWaitIndicator(
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
AdvTextLayoutSnapshot layout)
|
||||
=> WaitIndicatorBindings.Add((binding, layout));
|
||||
public void SetAdvWaitIndicatorEnabled(bool enabled) => WaitIndicatorEnabledChanges.Add(enabled);
|
||||
public void PublishAdvTextLayout(int layoutSlot) => PublishedAdvTextLayouts.Add(layoutSlot);
|
||||
public void SetAdvPagePresentationSuspended(bool suspended)
|
||||
|
||||
@@ -86,15 +86,28 @@ public interface IHost
|
||||
GfxState gfx, AdvTextLayoutPresentationBinding binding)
|
||||
=> ClearRenderedAdvTextLayout(binding.LayoutSlot);
|
||||
void RenderTextHistory(AdvTextHistoryRenderBatch batch) { }
|
||||
bool RenderTextHistory(
|
||||
GfxState gfx,
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
AdvTextHistoryRenderBatch batch)
|
||||
{
|
||||
RenderTextHistory(batch);
|
||||
return false;
|
||||
}
|
||||
// History render batches are transient bindings, unlike the retained backlog itself. HISTORY.BIN's
|
||||
// recording re-enable at exit ends that presentation and drops every bound target layout.
|
||||
void EndTextHistoryPresentation() { }
|
||||
void EndTextHistoryPresentation(GfxState gfx) => EndTextHistoryPresentation();
|
||||
int MessageWindowAlphaSetting => 0;
|
||||
void SetMessageWindowAlphaSetting(int value) { }
|
||||
void FillSurfaceRect(SurfaceRectFill fill) { }
|
||||
void CopySurfaceRect(SurfaceRectCopy copy) { }
|
||||
void PresentObjectRange(GfxState gfx, long firstHandle, long count) { }
|
||||
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
||||
// Op 0x212 supplies the ordinary retained handle used to publish the configured atlas cell.
|
||||
void BindAdvWaitIndicator(
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
AdvTextLayoutSnapshot layout) { }
|
||||
// Op 0x1ce explicitly starts/stops the same animated marker that op 0x72 starts for an ADV wait.
|
||||
void SetAdvWaitIndicatorEnabled(bool enabled) { }
|
||||
// Op 0x20a republishes one retained ADV text layout and includes the current marker frame when active.
|
||||
|
||||
@@ -207,8 +207,8 @@ public sealed class AdvTextHistory
|
||||
|
||||
/// <summary>
|
||||
/// Resolve layouts whose complete native retained-glyph handle interval is covered by one op-0x1f7
|
||||
/// erase. The Label backend collapses those glyph objects into live text runs, so full native range
|
||||
/// teardown removes the corresponding detached presentation atomically.
|
||||
/// erase. Hosts use this notification to discard fallback metadata or preserve exact transient records
|
||||
/// for a following native op-0x20a republication, according to the active presentation path.
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> LayoutsCoveredByTextObjectErase(long firstHandle, long count)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
|
||||
namespace Age.Engine.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Transient retained-object projection of one ADV layout's animated input-wait atlas.
|
||||
/// The script owns the source surface and object handle; this object owns only the current binding.
|
||||
/// </summary>
|
||||
public sealed class RetainedAdvWaitIndicatorPresentation
|
||||
{
|
||||
private int _publishedFrame = -1;
|
||||
|
||||
public RetainedAdvWaitIndicatorPresentation(
|
||||
AdvWaitIndicatorConfig config,
|
||||
AdvTextLayoutPresentationBinding binding,
|
||||
int layoutOriginX,
|
||||
int layoutOriginY)
|
||||
{
|
||||
if (config.LayoutSlot != binding.LayoutSlot)
|
||||
throw new ArgumentException(
|
||||
"Wait-indicator configuration and retained binding must select the same layout.");
|
||||
if (binding.WaitIndicatorObjectHandle < 0)
|
||||
throw new ArgumentException(
|
||||
"A retained ADV wait indicator requires a nonnegative object handle.",
|
||||
nameof(binding));
|
||||
if (config.SurfaceSlot < 0 || config.CellWidth <= 0 || config.CellHeight <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(config));
|
||||
|
||||
Config = config;
|
||||
Binding = binding;
|
||||
LayoutOriginX = layoutOriginX;
|
||||
LayoutOriginY = layoutOriginY;
|
||||
}
|
||||
|
||||
public AdvWaitIndicatorConfig Config { get; }
|
||||
public AdvTextLayoutPresentationBinding Binding { get; }
|
||||
public int LayoutOriginX { get; }
|
||||
public int LayoutOriginY { get; }
|
||||
public int PublishedFrame => _publishedFrame;
|
||||
public bool IsPublished => _publishedFrame >= 0;
|
||||
|
||||
/// <summary>Publish a changed source cell, or erase the retained handle when inactive.</summary>
|
||||
public bool Update(GfxState gfx, long elapsedMs, bool visible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfx);
|
||||
if (!visible)
|
||||
{
|
||||
if (!IsPublished) return false;
|
||||
Erase(gfx);
|
||||
return true;
|
||||
}
|
||||
|
||||
int frame = Config.FrameAt(elapsedMs);
|
||||
if (frame == _publishedFrame) return false;
|
||||
Bind(gfx, frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Recreate the current frame after script-owned range teardown/publication.</summary>
|
||||
public bool Republish(GfxState gfx, long elapsedMs, bool visible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfx);
|
||||
if (!visible)
|
||||
{
|
||||
if (!IsPublished) return false;
|
||||
Erase(gfx);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bind(gfx, Config.FrameAt(elapsedMs));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Erase(GfxState gfx)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfx);
|
||||
gfx.EraseRange(Binding.WaitIndicatorObjectHandle, 1);
|
||||
_publishedFrame = -1;
|
||||
}
|
||||
|
||||
private void Bind(GfxState gfx, int frame)
|
||||
{
|
||||
gfx.BindDraw(
|
||||
Binding.WaitIndicatorObjectHandle,
|
||||
Config.SurfaceSlot,
|
||||
checked(Config.SourceX + frame * Config.CellWidth),
|
||||
Config.SourceY,
|
||||
Config.CellWidth,
|
||||
Config.CellHeight,
|
||||
checked(LayoutOriginX + Config.X),
|
||||
checked(LayoutOriginY + Config.Y));
|
||||
_publishedFrame = frame;
|
||||
}
|
||||
}
|
||||
@@ -2347,7 +2347,7 @@ public sealed class VirtualMachine
|
||||
{
|
||||
bool enabled = Read(a[0]) == 1;
|
||||
TextHistory.SetRecordingEnabled(enabled);
|
||||
if (enabled) _host.EndTextHistoryPresentation();
|
||||
if (enabled) _host.EndTextHistoryPresentation(Gfx);
|
||||
}
|
||||
return pc + 1;
|
||||
case "append-text-history-metadata": // 0x1d2: (metadata type, value)
|
||||
@@ -2370,11 +2370,17 @@ public sealed class VirtualMachine
|
||||
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 with
|
||||
{
|
||||
batch = batch with
|
||||
{
|
||||
// Native History uses the text manager's current leading, not a retained-record field.
|
||||
Style = batch.Style with { LineSpacing = _advTextStyle.LineSpacing }
|
||||
});
|
||||
};
|
||||
_host.RenderTextHistory(
|
||||
Gfx,
|
||||
TextHistory.GetPresentationBinding(batch.LayoutSlot),
|
||||
batch);
|
||||
}
|
||||
return pc + 1;
|
||||
}
|
||||
case "u0041BB90":
|
||||
@@ -2820,7 +2826,13 @@ public sealed class VirtualMachine
|
||||
return pc + 1;
|
||||
case "set-adv-wait-indicator-handle": // 0x212 (layout)(retained handle)
|
||||
{
|
||||
TextHistory.SetWaitIndicatorObjectHandle((int)Read(a[0]), Read(a[1]));
|
||||
int requestedSlot = (int)Read(a[0]);
|
||||
TextHistory.SetWaitIndicatorObjectHandle(requestedSlot, Read(a[1]));
|
||||
AdvTextLayoutSnapshot layout =
|
||||
TextHistory.GetLayoutSnapshot(requestedSlot);
|
||||
_host.BindAdvWaitIndicator(
|
||||
TextHistory.GetPresentationBinding(layout.Slot),
|
||||
layout);
|
||||
return pc + 1;
|
||||
}
|
||||
case "set-adv-text-object-range": // 0x213 (layout)(first handle)(count)
|
||||
|
||||
Reference in New Issue
Block a user