Implement visible ADV History presentation

This commit is contained in:
gamer147
2026-07-18 23:59:26 -04:00
parent ac5437c5cf
commit 32a67b88de
11 changed files with 420 additions and 20 deletions

View File

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

View File

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