Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
493 lines
17 KiB
C#
493 lines
17 KiB
C#
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;
|
|
using AcDream.Content;
|
|
using System.Numerics;
|
|
|
|
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(IDatReaderWriter dats, RenderStack stack)
|
|
{
|
|
var objects = SampleData.BuildObjectTable();
|
|
var windows = new List<MockupWindow>();
|
|
|
|
if (MountVitals(dats, stack, objects) is { } vitals)
|
|
RecordWindow(windows, "Vitals", vitals, zOrder: 10);
|
|
if (MountToolbar(dats, stack, objects) is { } toolbar)
|
|
RecordWindow(windows, "Toolbar", toolbar, zOrder: 20);
|
|
if (MountCharacter(dats, stack, objects) is { } character)
|
|
RecordWindow(windows, "Character", character, zOrder: 30);
|
|
if (MountInventory(dats, stack, objects) is { } inventory)
|
|
RecordWindow(windows, "Inventory", inventory, zOrder: 40);
|
|
if (MountChat(dats, stack) is { } chat)
|
|
RecordWindow(windows, "Chat", chat, zOrder: 50);
|
|
|
|
MountControls(stack, windows);
|
|
}
|
|
|
|
private static ImportedLayout? Import(IDatReaderWriter dats, uint layoutId, RenderStack stack)
|
|
=> LayoutImporter.Import(dats, layoutId, stack.ResolveChrome, stack.VitalsDatFont,
|
|
fontResolve: stack.ResolveDatFont);
|
|
|
|
private static RetailWindowHandle? MountVitals(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
|
{
|
|
var layout = Import(dats, 0x2100006Cu, stack);
|
|
if (layout is null) return null;
|
|
|
|
IRetainedPanelController? controller =
|
|
FixtureProvider.Populate(0x2100006Cu, layout, stack, objects, dats);
|
|
|
|
return RetailWindowFrame.Mount(
|
|
stack.UiHost.Root,
|
|
layout.Root,
|
|
stack.ResolveChrome,
|
|
new RetailWindowFrame.Options
|
|
{
|
|
WindowName = WindowNames.Vitals,
|
|
Chrome = RetailWindowChrome.Imported,
|
|
Left = 12f,
|
|
Top = 18f,
|
|
ResizeX = true,
|
|
ResizeY = false,
|
|
MinWidth = 40f,
|
|
ContentClickThrough = false,
|
|
Controller = controller,
|
|
});
|
|
}
|
|
|
|
private static RetailWindowHandle? MountToolbar(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
|
{
|
|
var layout = Import(dats, 0x21000016u, stack);
|
|
if (layout is null) return null;
|
|
|
|
IRetainedPanelController? controller =
|
|
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;
|
|
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
|
stack.UiHost.Root,
|
|
toolbarRoot,
|
|
stack.ResolveChrome,
|
|
new RetailWindowFrame.Options
|
|
{
|
|
WindowName = WindowNames.Toolbar,
|
|
Chrome = RetailWindowChrome.CollapsibleNineSlice,
|
|
Left = 12f,
|
|
Top = 220f,
|
|
ContentWidth = contentW,
|
|
ContentHeight = contentH,
|
|
Resizable = false,
|
|
ResizeX = false,
|
|
ResizeY = true,
|
|
ResizableEdges = ResizeEdges.Bottom,
|
|
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right,
|
|
ContentClickThrough = true,
|
|
Controller = controller,
|
|
});
|
|
var frame = (UiCollapsibleFrame)handle.OuterFrame;
|
|
|
|
ConfigureToolbarCollapse(layout, frame, contentH, border);
|
|
return handle;
|
|
}
|
|
|
|
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 RetailWindowHandle? MountCharacter(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
|
{
|
|
var layout = Import(dats, 0x2100002Eu, stack);
|
|
if (layout is null) return null;
|
|
|
|
IRetainedPanelController? controller =
|
|
FixtureProvider.Populate(0x2100002Eu, layout, stack, objects, dats);
|
|
|
|
return RetailWindowFrame.Mount(
|
|
stack.UiHost.Root,
|
|
layout.Root,
|
|
stack.ResolveChrome,
|
|
new RetailWindowFrame.Options
|
|
{
|
|
WindowName = WindowNames.Character,
|
|
Chrome = RetailWindowChrome.NineSlice,
|
|
Left = 540f,
|
|
Top = 18f,
|
|
MaxHeight = 760f,
|
|
ResizeX = false,
|
|
ResizeY = true,
|
|
ResizableEdges = ResizeEdges.Bottom,
|
|
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
|
ContentClickThrough = false,
|
|
Controller = controller,
|
|
});
|
|
}
|
|
|
|
private static RetailWindowHandle? MountInventory(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
|
{
|
|
var layout = Import(dats, 0x21000023u, stack);
|
|
if (layout is null) return null;
|
|
|
|
IRetainedPanelController? controller =
|
|
FixtureProvider.Populate(0x21000023u, layout, stack, objects, dats);
|
|
|
|
var inventoryRoot = layout.Root;
|
|
float contentW = inventoryRoot.Width;
|
|
float contentH = inventoryRoot.Height;
|
|
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
|
stack.UiHost.Root,
|
|
inventoryRoot,
|
|
stack.ResolveChrome,
|
|
new RetailWindowFrame.Options
|
|
{
|
|
WindowName = WindowNames.Inventory,
|
|
Chrome = RetailWindowChrome.NineSlice,
|
|
Left = 900f,
|
|
Top = 18f,
|
|
ContentWidth = contentW,
|
|
ContentHeight = contentH,
|
|
ResizeX = false,
|
|
ResizeY = true,
|
|
ResizableEdges = ResizeEdges.Bottom,
|
|
MaxHeight = Math.Max(
|
|
contentH + 2f * RetailChromeSprites.Border,
|
|
stack.UiHost.Root.Height - 18f),
|
|
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
|
Controller = controller,
|
|
});
|
|
|
|
StretchV(layout, 0x100001D0u);
|
|
StretchV(layout, 0x100001CFu);
|
|
StretchV(layout, 0x100001C6u);
|
|
StretchV(layout, 0x100001C7u);
|
|
PinTopLeft(layout, 0x100001CDu);
|
|
PinTopLeft(layout, 0x100001CEu);
|
|
|
|
return handle;
|
|
}
|
|
|
|
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 RetailWindowHandle? MountChat(IDatReaderWriter dats, RenderStack stack)
|
|
{
|
|
var rootInfo = LayoutImporter.ImportInfos(dats, ChatWindowController.LayoutId);
|
|
if (rootInfo is null) return null;
|
|
|
|
var strings = new DatStringResolver(dats);
|
|
var layout = LayoutImporter.Build(rootInfo, stack.ResolveChrome, stack.VitalsDatFont,
|
|
fontResolve: stack.ResolveDatFont,
|
|
stringResolve: strings.Resolve);
|
|
|
|
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 null;
|
|
|
|
var chatRoot = controller.Root;
|
|
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
|
stack.UiHost.Root,
|
|
chatRoot,
|
|
stack.ResolveChrome,
|
|
new RetailWindowFrame.Options
|
|
{
|
|
WindowName = WindowNames.Chat,
|
|
Chrome = RetailWindowChrome.NineSlice,
|
|
Left = 12f,
|
|
Top = 390f,
|
|
ContentWidth = 490f,
|
|
ContentHeight = chatRoot.Height,
|
|
RebaseContentLayout = true,
|
|
DatConstraintSource = controller.DatWindowInfo,
|
|
MinWidth = 200f,
|
|
ResizeX = true,
|
|
ResizeY = true,
|
|
Opacity = 0.75f,
|
|
StateController = controller,
|
|
});
|
|
controller.AttachWindow(handle);
|
|
|
|
stack.UiHost.Root.DefaultTextInput = controller.Input;
|
|
return handle;
|
|
}
|
|
|
|
private static void RecordWindow(
|
|
List<MockupWindow> windows,
|
|
string title,
|
|
RetailWindowHandle handle,
|
|
int zOrder)
|
|
{
|
|
UiElement element = handle.OuterFrame;
|
|
element.ZOrder = zOrder;
|
|
windows.Add(new MockupWindow(
|
|
handle.Name,
|
|
title,
|
|
element,
|
|
new MockupRect(element.Left, element.Top, element.Width, element.Height),
|
|
zOrder));
|
|
}
|
|
|
|
private static void MountControls(RenderStack stack, IReadOnlyList<MockupWindow> windows)
|
|
{
|
|
if (windows.Count == 0) return;
|
|
|
|
var font = stack.VitalsDatFont;
|
|
var panel = new MockupControlPanel
|
|
{
|
|
Name = "mockup-controls",
|
|
Left = 1010f,
|
|
Top = 430f,
|
|
Width = 258f,
|
|
Height = 242f,
|
|
Anchors = AnchorEdges.None,
|
|
Draggable = true,
|
|
Resizable = false,
|
|
BackgroundColor = new Vector4(0.03f, 0.035f, 0.04f, 0.82f),
|
|
BorderColor = new Vector4(0.55f, 0.45f, 0.27f, 0.95f),
|
|
BorderThickness = 1f,
|
|
ZOrder = 1_000,
|
|
};
|
|
|
|
panel.AddChild(new MockupLabel(font)
|
|
{
|
|
Left = 12f,
|
|
Top = 10f,
|
|
Width = 220f,
|
|
Height = 18f,
|
|
Text = "Mockup",
|
|
TextColor = new Vector4(0.95f, 0.86f, 0.62f, 1f),
|
|
});
|
|
|
|
var toggles = new List<(MockupWindow Window, MockupButton Button)>();
|
|
panel.AddChild(MakeButton(font, "Arrange", 12f, 34f, 108f, 24f, () =>
|
|
{
|
|
Arrange(windows, showAll: false);
|
|
RefreshToggles(toggles);
|
|
}));
|
|
panel.AddChild(MakeButton(font, "Reset", 132f, 34f, 108f, 24f, () =>
|
|
{
|
|
Arrange(windows, showAll: true);
|
|
RefreshToggles(toggles);
|
|
}));
|
|
|
|
float y = 70f;
|
|
foreach (var window in windows)
|
|
{
|
|
var button = MakeButton(font, "", 12f, y, 228f, 24f, () =>
|
|
{
|
|
stack.UiHost.ToggleWindow(window.Name);
|
|
RefreshToggles(toggles);
|
|
});
|
|
toggles.Add((window, button));
|
|
panel.AddChild(button);
|
|
y += 30f;
|
|
}
|
|
|
|
RefreshToggles(toggles);
|
|
stack.UiHost.Root.AddChild(panel);
|
|
}
|
|
|
|
private static MockupButton MakeButton(UiDatFont? font, string text,
|
|
float left, float top, float width, float height, Action onClick)
|
|
{
|
|
var button = new MockupButton(font)
|
|
{
|
|
Left = left,
|
|
Top = top,
|
|
Width = width,
|
|
Height = height,
|
|
Text = text,
|
|
};
|
|
button.Click += onClick;
|
|
return button;
|
|
}
|
|
|
|
private static void Arrange(IReadOnlyList<MockupWindow> windows, bool showAll)
|
|
{
|
|
foreach (var window in windows)
|
|
{
|
|
var element = window.Element;
|
|
element.Left = window.DefaultRect.Left;
|
|
element.Top = window.DefaultRect.Top;
|
|
element.Width = window.DefaultRect.Width;
|
|
element.Height = window.DefaultRect.Height;
|
|
element.ZOrder = window.DefaultZOrder;
|
|
if (showAll)
|
|
element.Visible = true;
|
|
}
|
|
}
|
|
|
|
private static void RefreshToggles(IReadOnlyList<(MockupWindow Window, MockupButton Button)> toggles)
|
|
{
|
|
foreach (var (window, button) in toggles)
|
|
{
|
|
bool visible = window.Element.Visible;
|
|
button.Text = visible ? $"{window.Title} on" : $"{window.Title} off";
|
|
button.TextColor = visible
|
|
? new Vector4(0.96f, 0.93f, 0.82f, 1f)
|
|
: new Vector4(0.62f, 0.62f, 0.62f, 1f);
|
|
button.BackgroundColor = visible
|
|
? new Vector4(0.14f, 0.13f, 0.10f, 0.94f)
|
|
: new Vector4(0.06f, 0.065f, 0.07f, 0.82f);
|
|
}
|
|
}
|
|
|
|
private sealed record MockupWindow(
|
|
string Name,
|
|
string Title,
|
|
UiElement Element,
|
|
MockupRect DefaultRect,
|
|
int DefaultZOrder);
|
|
|
|
private readonly record struct MockupRect(float Left, float Top, float Width, float Height);
|
|
|
|
private sealed class MockupControlPanel : UiPanel
|
|
{
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
if (BackgroundColor.W > 0f)
|
|
ctx.DrawFill(0f, 0f, Width, Height, BackgroundColor);
|
|
if (BorderColor.W > 0f && BorderThickness > 0f)
|
|
ctx.DrawRectOutline(0f, 0f, Width, Height, BorderColor, BorderThickness);
|
|
}
|
|
|
|
protected override void OnTick(double deltaSeconds)
|
|
{
|
|
base.OnTick(deltaSeconds);
|
|
if (Parent is null) return;
|
|
|
|
int top = ZOrder;
|
|
foreach (var sibling in Parent.Children)
|
|
{
|
|
if (!ReferenceEquals(sibling, this))
|
|
top = Math.Max(top, sibling.ZOrder + 1);
|
|
}
|
|
ZOrder = top;
|
|
}
|
|
}
|
|
|
|
private sealed class MockupLabel : UiElement
|
|
{
|
|
private readonly UiDatFont? _font;
|
|
|
|
public string Text { get; set; } = string.Empty;
|
|
public Vector4 TextColor { get; set; } = Vector4.One;
|
|
|
|
public MockupLabel(UiDatFont? font)
|
|
{
|
|
_font = font;
|
|
ClickThrough = true;
|
|
}
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
if (_font is null || Text.Length == 0) return;
|
|
ctx.DrawStringDat(_font, Text, 0f, 0f, TextColor);
|
|
}
|
|
}
|
|
|
|
private sealed class MockupButton : UiPanel
|
|
{
|
|
private readonly UiDatFont? _font;
|
|
|
|
public event Action? Click;
|
|
public string Text { get; set; } = string.Empty;
|
|
public Vector4 TextColor { get; set; } = new(0.96f, 0.93f, 0.82f, 1f);
|
|
|
|
public MockupButton(UiDatFont? font)
|
|
{
|
|
_font = font;
|
|
BackgroundColor = new Vector4(0.14f, 0.13f, 0.10f, 0.94f);
|
|
BorderColor = new Vector4(0.45f, 0.38f, 0.24f, 1f);
|
|
BorderThickness = 1f;
|
|
}
|
|
|
|
public override bool HandlesClick => true;
|
|
|
|
public override bool OnEvent(in UiEvent e)
|
|
{
|
|
if (e.Type != UiEventType.Click || !Enabled) return false;
|
|
Click?.Invoke();
|
|
return true;
|
|
}
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
if (BackgroundColor.W > 0f)
|
|
ctx.DrawFill(0f, 0f, Width, Height, BackgroundColor);
|
|
if (BorderColor.W > 0f && BorderThickness > 0f)
|
|
ctx.DrawRectOutline(0f, 0f, Width, Height, BorderColor, BorderThickness);
|
|
|
|
if (_font is null || Text.Length == 0) return;
|
|
|
|
float textW = _font.MeasureWidth(Text);
|
|
float tx = MathF.Max(4f, (Width - textW) * 0.5f);
|
|
float ty = (Height - _font.LineHeight) * 0.5f;
|
|
ctx.DrawStringDat(_font, Text, tx, ty, TextColor);
|
|
}
|
|
}
|
|
}
|