Create AGENTS.md as a thin bridge to CLAUDE.md, fix isolated dat-layout previews by normalizing panel roots to the studio canvas, add a --mockup mode that mounts production-backed UI windows into one UiHost, and thread the per-element dat-font resolver through GameWindow's live imports. Verified with dotnet test and headless studio screenshots for inventory and the mockup desktop.
This commit is contained in:
parent
ca94b479bf
commit
9444a328fa
8 changed files with 405 additions and 44 deletions
|
|
@ -1861,6 +1861,15 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.App.UI.UiDatFont? vitalsDatFont;
|
||||
lock (_datLock)
|
||||
vitalsDatFont = AcDream.App.UI.UiDatFont.Load(_dats!, _textureCache!);
|
||||
var datFontCache = new System.Collections.Concurrent.ConcurrentDictionary<uint, AcDream.App.UI.UiDatFont?>();
|
||||
if (vitalsDatFont is not null)
|
||||
datFontCache.TryAdd(AcDream.App.UI.UiDatFont.DefaultFontId, vitalsDatFont);
|
||||
AcDream.App.UI.UiDatFont? ResolveDatFont(uint fontDid)
|
||||
=> datFontCache.GetOrAdd(fontDid, id =>
|
||||
{
|
||||
lock (_datLock)
|
||||
return AcDream.App.UI.UiDatFont.Load(_dats!, _textureCache!, id);
|
||||
});
|
||||
Console.WriteLine(vitalsDatFont is not null
|
||||
? "[D.2b] vitals dat-font 0x40000000 loaded for numeric overlay."
|
||||
: "[D.2b] vitals dat-font 0x40000000 unavailable — falling back to debug font.");
|
||||
|
|
@ -1872,7 +1881,7 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.App.UI.Layout.ImportedLayout? imported;
|
||||
lock (_datLock)
|
||||
imported = AcDream.App.UI.Layout.LayoutImporter.Import(
|
||||
_dats!, 0x2100006Cu, ResolveChrome, vitalsDatFont);
|
||||
_dats!, 0x2100006Cu, ResolveChrome, vitalsDatFont, ResolveDatFont);
|
||||
if (imported is not null)
|
||||
{
|
||||
AcDream.App.UI.Layout.VitalsController.Bind(imported,
|
||||
|
|
@ -1916,7 +1925,7 @@ public sealed class GameWindow : IDisposable
|
|||
chatRootInfo = AcDream.App.UI.Layout.LayoutImporter.ImportInfos(
|
||||
_dats!, AcDream.App.UI.Layout.ChatWindowController.LayoutId);
|
||||
chatLayout = chatRootInfo is null ? null
|
||||
: AcDream.App.UI.Layout.LayoutImporter.Build(chatRootInfo, ResolveChrome, vitalsDatFont);
|
||||
: AcDream.App.UI.Layout.LayoutImporter.Build(chatRootInfo, ResolveChrome, vitalsDatFont, ResolveDatFont);
|
||||
}
|
||||
if (chatRootInfo is not null && chatLayout is not null)
|
||||
{
|
||||
|
|
@ -2058,7 +2067,7 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.App.UI.Layout.ImportedLayout? toolbarLayout;
|
||||
lock (_datLock)
|
||||
toolbarLayout = AcDream.App.UI.Layout.LayoutImporter.Import(
|
||||
_dats!, 0x21000016u, ResolveChrome, vitalsDatFont);
|
||||
_dats!, 0x21000016u, ResolveChrome, vitalsDatFont, ResolveDatFont);
|
||||
if (toolbarLayout is not null)
|
||||
{
|
||||
_toolbarController = AcDream.App.UI.Layout.ToolbarController.Bind(
|
||||
|
|
@ -2196,7 +2205,7 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.App.UI.Layout.ImportedLayout? invLayout;
|
||||
lock (_datLock)
|
||||
invLayout = AcDream.App.UI.Layout.LayoutImporter.Import(
|
||||
_dats!, 0x21000023u, ResolveChrome, vitalsDatFont);
|
||||
_dats!, 0x21000023u, ResolveChrome, vitalsDatFont, ResolveDatFont);
|
||||
if (invLayout is not null)
|
||||
{
|
||||
var inventoryRoot = invLayout.Root;
|
||||
|
|
|
|||
251
src/AcDream.App/Studio/MockupDesktop.cs
Normal file
251
src/AcDream.App/Studio/MockupDesktop.cs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Builds a multi-window UI desktop for the studio. It mounts several real
|
||||
/// imported panels into one production <see cref="UiHost"/> so drag, resize,
|
||||
/// click, focus, and z-order behavior all run through the same UI root as the
|
||||
/// game.
|
||||
/// </summary>
|
||||
internal static class MockupDesktop
|
||||
{
|
||||
public static void Load(DatCollection dats, RenderStack stack)
|
||||
{
|
||||
var objects = SampleData.BuildObjectTable();
|
||||
|
||||
MountVitals(dats, stack);
|
||||
MountToolbar(dats, stack, objects);
|
||||
MountCharacter(dats, stack, objects);
|
||||
MountInventory(dats, stack, objects);
|
||||
MountChat(dats, stack);
|
||||
}
|
||||
|
||||
private static ImportedLayout? Import(DatCollection dats, uint layoutId, RenderStack stack)
|
||||
=> LayoutImporter.Import(dats, layoutId, stack.ResolveChrome, stack.VitalsDatFont,
|
||||
fontResolve: stack.ResolveDatFont);
|
||||
|
||||
private static void MountVitals(DatCollection dats, RenderStack stack)
|
||||
{
|
||||
var layout = Import(dats, 0x2100006Cu, stack);
|
||||
if (layout is null) return;
|
||||
|
||||
FixtureProvider.Populate(0x2100006Cu, layout, stack, SampleData.BuildObjectTable(), dats);
|
||||
|
||||
var root = layout.Root;
|
||||
root.Left = 12f;
|
||||
root.Top = 18f;
|
||||
root.ClickThrough = false;
|
||||
root.Anchors = AnchorEdges.None;
|
||||
root.Draggable = true;
|
||||
root.Resizable = true;
|
||||
root.ResizeX = true;
|
||||
root.ResizeY = false;
|
||||
root.MinWidth = 40f;
|
||||
stack.UiHost.Root.AddChild(root);
|
||||
}
|
||||
|
||||
private static void MountToolbar(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
||||
{
|
||||
var layout = Import(dats, 0x21000016u, stack);
|
||||
if (layout is null) return;
|
||||
|
||||
FixtureProvider.Populate(0x21000016u, layout, stack, objects, dats);
|
||||
|
||||
const int border = RetailChromeSprites.Border;
|
||||
var toolbarRoot = layout.Root;
|
||||
float contentW = toolbarRoot.Width > 0f ? toolbarRoot.Width : 300f;
|
||||
float contentH = toolbarRoot.Height;
|
||||
var frame = new UiCollapsibleFrame(stack.ResolveChrome)
|
||||
{
|
||||
Left = 12f,
|
||||
Top = 220f,
|
||||
Width = contentW + 2 * border,
|
||||
Height = contentH + 2 * border,
|
||||
Opacity = 1f,
|
||||
};
|
||||
|
||||
toolbarRoot.Left = border;
|
||||
toolbarRoot.Top = border;
|
||||
toolbarRoot.Width = contentW;
|
||||
toolbarRoot.Height = contentH;
|
||||
toolbarRoot.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right;
|
||||
toolbarRoot.ClickThrough = true;
|
||||
toolbarRoot.Draggable = false;
|
||||
toolbarRoot.Resizable = false;
|
||||
frame.AddChild(toolbarRoot);
|
||||
|
||||
ConfigureToolbarCollapse(layout, frame, contentH, border);
|
||||
stack.UiHost.Root.AddChild(frame);
|
||||
}
|
||||
|
||||
private static void ConfigureToolbarCollapse(ImportedLayout layout, UiCollapsibleFrame frame, float contentH, int border)
|
||||
{
|
||||
uint[] row2Ids =
|
||||
{
|
||||
0x100006B6u,
|
||||
0x100006B7u, 0x100006B8u, 0x100006B9u, 0x100006BAu, 0x100006BBu,
|
||||
0x100006BCu, 0x100006BDu, 0x100006BEu, 0x100006BFu,
|
||||
0x100006C0u,
|
||||
};
|
||||
|
||||
var row2 = new List<UiElement>();
|
||||
float minRow2Top = float.MaxValue;
|
||||
foreach (var id in row2Ids)
|
||||
{
|
||||
if (layout.FindElement(id) is not { } element) continue;
|
||||
row2.Add(element);
|
||||
if (element.Top < minRow2Top) minRow2Top = element.Top;
|
||||
}
|
||||
|
||||
if (row2.Count == 0) return;
|
||||
|
||||
float expandedH = contentH + 2 * border;
|
||||
float collapsedH = minRow2Top + 2 * border;
|
||||
frame.CollapsedHeight = collapsedH;
|
||||
frame.ExpandedHeight = expandedH;
|
||||
frame.SecondRow = row2;
|
||||
frame.Resizable = true;
|
||||
frame.ResizableEdges = ResizeEdges.Bottom;
|
||||
frame.MinHeight = collapsedH;
|
||||
frame.MaxHeight = expandedH;
|
||||
}
|
||||
|
||||
private static void MountCharacter(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
||||
{
|
||||
var layout = Import(dats, 0x2100002Eu, stack);
|
||||
if (layout is null) return;
|
||||
|
||||
FixtureProvider.Populate(0x2100002Eu, layout, stack, objects, dats);
|
||||
|
||||
var root = layout.Root;
|
||||
root.Left = 540f;
|
||||
root.Top = 18f;
|
||||
root.Anchors = AnchorEdges.None;
|
||||
root.ClickThrough = false;
|
||||
root.Draggable = true;
|
||||
root.Resizable = true;
|
||||
root.MinWidth = root.Width;
|
||||
root.MinHeight = root.Height;
|
||||
stack.UiHost.Root.AddChild(root);
|
||||
}
|
||||
|
||||
private static void MountInventory(DatCollection dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
||||
{
|
||||
var layout = Import(dats, 0x21000023u, stack);
|
||||
if (layout is null) return;
|
||||
|
||||
FixtureProvider.Populate(0x21000023u, layout, stack, objects, dats);
|
||||
|
||||
const int border = RetailChromeSprites.Border;
|
||||
var inventoryRoot = layout.Root;
|
||||
float contentW = inventoryRoot.Width;
|
||||
float contentH = inventoryRoot.Height;
|
||||
var frame = new UiNineSlicePanel(stack.ResolveChrome)
|
||||
{
|
||||
Left = 900f,
|
||||
Top = 18f,
|
||||
Width = contentW + 2 * border,
|
||||
Height = contentH + 2 * border,
|
||||
Opacity = 1f,
|
||||
Visible = true,
|
||||
Anchors = AnchorEdges.None,
|
||||
Draggable = true,
|
||||
Resizable = true,
|
||||
ResizeX = false,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
MinHeight = contentH + 2 * border,
|
||||
MaxHeight = 560f,
|
||||
};
|
||||
|
||||
inventoryRoot.Left = border;
|
||||
inventoryRoot.Top = border;
|
||||
inventoryRoot.Width = contentW;
|
||||
inventoryRoot.Height = contentH;
|
||||
inventoryRoot.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom;
|
||||
inventoryRoot.Draggable = false;
|
||||
frame.AddChild(inventoryRoot);
|
||||
|
||||
StretchV(layout, 0x100001D0u);
|
||||
StretchV(layout, 0x100001CFu);
|
||||
StretchV(layout, 0x100001C6u);
|
||||
StretchV(layout, 0x100001C7u);
|
||||
PinTopLeft(layout, 0x100001CDu);
|
||||
PinTopLeft(layout, 0x100001CEu);
|
||||
|
||||
stack.UiHost.Root.AddChild(frame);
|
||||
stack.UiHost.RegisterWindow(WindowNames.Inventory, frame);
|
||||
}
|
||||
|
||||
private static void StretchV(ImportedLayout layout, uint id)
|
||||
{
|
||||
if (layout.FindElement(id) is { } element)
|
||||
element.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom;
|
||||
}
|
||||
|
||||
private static void PinTopLeft(ImportedLayout layout, uint id)
|
||||
{
|
||||
if (layout.FindElement(id) is { } element)
|
||||
element.Anchors = AnchorEdges.Left | AnchorEdges.Top;
|
||||
}
|
||||
|
||||
private static void MountChat(DatCollection dats, RenderStack stack)
|
||||
{
|
||||
var rootInfo = LayoutImporter.ImportInfos(dats, ChatWindowController.LayoutId);
|
||||
if (rootInfo is null) return;
|
||||
|
||||
var layout = LayoutImporter.Build(rootInfo, stack.ResolveChrome, stack.VitalsDatFont,
|
||||
fontResolve: stack.ResolveDatFont);
|
||||
|
||||
var chatLog = new ChatLog();
|
||||
chatLog.SetLocalPlayerGuid(SampleData.PlayerGuid);
|
||||
chatLog.OnSystemMessage("Welcome to the acdream UI mockup.", 0);
|
||||
chatLog.OnLocalSpeech("You", "This desktop is running through UiHost.", SampleData.PlayerGuid, isRanged: false);
|
||||
chatLog.OnChannelBroadcast(1, "Caith", "Drag and resize the windows here.", "General");
|
||||
chatLog.OnTellReceived("Alicia", "The old UI is starting to wake up.", 0x5000ABCDu);
|
||||
|
||||
var vm = new ChatVM(chatLog, displayLimit: 200);
|
||||
var controller = ChatWindowController.Bind(
|
||||
rootInfo,
|
||||
layout,
|
||||
vm,
|
||||
() => NullCommandBus.Instance,
|
||||
stack.VitalsDatFont,
|
||||
debugFont: null,
|
||||
stack.ResolveChrome);
|
||||
if (controller is null) return;
|
||||
|
||||
const int border = RetailChromeSprites.Border;
|
||||
var chatRoot = controller.Root;
|
||||
float contentW = 490f;
|
||||
float contentH = chatRoot.Height;
|
||||
var frame = new UiNineSlicePanel(stack.ResolveChrome)
|
||||
{
|
||||
Left = 12f,
|
||||
Top = 390f,
|
||||
Width = contentW + 2 * border,
|
||||
Height = contentH + 2 * border,
|
||||
MinWidth = 200f,
|
||||
MinHeight = 90f,
|
||||
Opacity = 0.75f,
|
||||
};
|
||||
|
||||
chatRoot.Left = border;
|
||||
chatRoot.Top = border;
|
||||
chatRoot.Width = contentW;
|
||||
chatRoot.Height = contentH;
|
||||
chatRoot.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom;
|
||||
chatRoot.Draggable = false;
|
||||
chatRoot.Resizable = false;
|
||||
frame.AddChild(chatRoot);
|
||||
|
||||
stack.UiHost.Root.AddChild(frame);
|
||||
stack.UiHost.Root.DefaultTextInput = controller.Input;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,8 @@ public sealed record StudioOptions(
|
|||
string? MarkupPath,
|
||||
string? DumpSlug = null,
|
||||
string? DumpFile = null,
|
||||
string? ScreenshotPath = null)
|
||||
string? ScreenshotPath = null,
|
||||
bool Mockup = false)
|
||||
{
|
||||
/// <summary>
|
||||
/// Parse studio options from the args that come AFTER the <c>ui-studio</c> token.
|
||||
|
|
@ -39,6 +40,7 @@ public sealed record StudioOptions(
|
|||
string? dumpSlug = null;
|
||||
string? dumpFile = null;
|
||||
string? screenshotPath = null;
|
||||
bool mockup = false;
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
|
|
@ -67,6 +69,10 @@ public sealed record StudioOptions(
|
|||
{
|
||||
screenshotPath = args[++i];
|
||||
}
|
||||
else if (args[i] == "--mockup")
|
||||
{
|
||||
mockup = true;
|
||||
}
|
||||
else if (!args[i].StartsWith('-'))
|
||||
{
|
||||
datDir ??= args[i];
|
||||
|
|
@ -81,10 +87,10 @@ public sealed record StudioOptions(
|
|||
"ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR.");
|
||||
|
||||
// Default layout: vitals (0x2100006C), unless a dump slug or markup is requested.
|
||||
if (layoutId is null && markupPath is null && dumpSlug is null)
|
||||
if (!mockup && layoutId is null && markupPath is null && dumpSlug is null)
|
||||
layoutId = 0x2100006Cu;
|
||||
|
||||
return new StudioOptions(datDir, layoutId, markupPath, dumpSlug, dumpFile, screenshotPath);
|
||||
return new StudioOptions(datDir, layoutId, markupPath, dumpSlug, dumpFile, screenshotPath, mockup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace AcDream.App.Studio;
|
|||
/// (<see cref="RenderBootstrap"/>) and previews a single UI panel
|
||||
/// identified by a <see cref="LayoutSource"/>.
|
||||
///
|
||||
/// Usage: dotnet run -- ui-studio [dat-dir] [--layout 0xNNNN] [--markup path]
|
||||
/// Usage: dotnet run -- ui-studio [dat-dir] [--layout 0xNNNN] [--markup path] [--mockup]
|
||||
///
|
||||
/// Task 3 adds an ImGui IDE on top of the panel FBO:
|
||||
/// <list type="bullet">
|
||||
|
|
@ -124,6 +124,9 @@ public sealed class StudioWindow : IDisposable
|
|||
|
||||
_stack = RenderBootstrap.Create(gl, _dats, new RenderBootstrapOptions(quality));
|
||||
|
||||
if (_opts.Mockup)
|
||||
MockupDesktop.Load(_dats, _stack);
|
||||
|
||||
// Load the panel described by options and add it to the UI tree.
|
||||
// Task 4b: --dump <slug> uses DumpLayout (static retail mockup, no controllers).
|
||||
// All other modes use LayoutSource + FixtureProvider (production path).
|
||||
|
|
@ -141,8 +144,8 @@ public sealed class StudioWindow : IDisposable
|
|||
_dumpSlugs = UiDumpModel.ListSlugs(_dumpFile)
|
||||
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
UiElement? root;
|
||||
if (_opts.DumpSlug is not null)
|
||||
UiElement? root = null;
|
||||
if (!_opts.Mockup && _opts.DumpSlug is not null)
|
||||
{
|
||||
if (_dumpFile is null)
|
||||
{
|
||||
|
|
@ -160,11 +163,13 @@ public sealed class StudioWindow : IDisposable
|
|||
_currentSlug = _opts.DumpSlug;
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!_opts.Mockup)
|
||||
{
|
||||
root = _source.Load(_opts);
|
||||
if (root is null)
|
||||
Console.Error.WriteLine($"[studio] panel load failed: {_source.LastError}");
|
||||
else
|
||||
NormalizeSinglePanelRoot(root);
|
||||
}
|
||||
|
||||
_panelRoot = root;
|
||||
|
|
@ -189,12 +194,18 @@ public sealed class StudioWindow : IDisposable
|
|||
if (_opts.ScreenshotPath is null)
|
||||
{
|
||||
var input = _window!.CreateInput();
|
||||
// Wire KEYBOARD input into UiHost (keyboard coords are not spatial, so no remapping needed).
|
||||
// Do NOT wire mouse here — raw Silk window coords would be offset by the canvas sub-window's
|
||||
// position (tree pane width + ImGui chrome) and land in the wrong panel-local location.
|
||||
// Mouse is forwarded manually from DrawCanvas with correct panel-local mapping below.
|
||||
// Mockup mode draws UiHost directly to the window, so raw Silk mouse coordinates
|
||||
// already match UI pixels. Inspector mode draws UiHost inside an ImGui canvas, so
|
||||
// mouse is forwarded manually from DrawCanvas with canvas-local remapping.
|
||||
if (_opts.Mockup)
|
||||
foreach (var mouse in input.Mice)
|
||||
_stack.UiHost.WireMouse(mouse);
|
||||
foreach (var kb in input.Keyboards)
|
||||
_stack.UiHost.WireKeyboard(kb);
|
||||
|
||||
if (_opts.Mockup)
|
||||
return;
|
||||
|
||||
_imgui = new AcDream.UI.ImGui.ImGuiBootstrapper(gl, _window!, input);
|
||||
_inspector = new StudioInspector();
|
||||
}
|
||||
|
|
@ -215,7 +226,7 @@ public sealed class StudioWindow : IDisposable
|
|||
// Pick render size from the loaded root's bounds (clamped to sane limits).
|
||||
// Fall back to 1280×720 when the root has no explicit size.
|
||||
int w = 1280, h = 720;
|
||||
if (_panelRoot is not null)
|
||||
if (!_opts.Mockup && _panelRoot is not null)
|
||||
{
|
||||
float rw = _panelRoot.Width;
|
||||
float rh = _panelRoot.Height;
|
||||
|
|
@ -264,6 +275,20 @@ public sealed class StudioWindow : IDisposable
|
|||
}
|
||||
|
||||
// ── INTERACTIVE PATH ──────────────────────────────────────────────────────
|
||||
if (_opts.Mockup)
|
||||
{
|
||||
var mockupGl = _stack.Gl;
|
||||
int mockupW = _window!.Size.X;
|
||||
int mockupH = _window!.Size.Y;
|
||||
_stack.UiHost.Tick(dt);
|
||||
mockupGl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
mockupGl.Viewport(0, 0, (uint)mockupW, (uint)mockupH);
|
||||
mockupGl.ClearColor(0.08f, 0.08f, 0.08f, 1f);
|
||||
mockupGl.Clear(ColorBufferBit | DepthBufferBit);
|
||||
_stack.UiHost.Draw(new Vector2(mockupW, mockupH));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_imgui is null || _inspector is null) return;
|
||||
|
||||
var gl = _stack.Gl;
|
||||
|
|
@ -428,6 +453,18 @@ public sealed class StudioWindow : IDisposable
|
|||
_inspector.Selected = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Studio dat-layout previews show one panel in isolation, not at its live-game
|
||||
/// screen position. Some top-level layouts (inventory: x=500,y=138) otherwise
|
||||
/// draw entirely outside the root-sized screenshot FBO.
|
||||
/// </summary>
|
||||
internal static void NormalizeSinglePanelRoot(UiElement root)
|
||||
{
|
||||
root.Left = 0f;
|
||||
root.Top = 0f;
|
||||
root.Anchors = AnchorEdges.None;
|
||||
}
|
||||
|
||||
private void OnClosing()
|
||||
{
|
||||
_imgui?.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue