merge: bring main (UN-7, #140 filing, D.2b UI rows) into A7 Fix D round-2 branch
Resolves the divergence-register conflict: kept the accurate per-VERTEX AP-35
(Fix A shipped per-vertex; main's row was the stale pre-Fix-A per-pixel text),
kept main's UI rows AP-37..AP-42, and renumbered this branch's torch-gate row
AP-37 -> AP-43 (AP-37 was taken by main's LayoutDesc row). AP count 41 -> 42.
Retargeted the AP-37 references in WbDrawDispatcher + the CHECKPOINT to AP-43.
Marked ISSUES #140 RESOLVED (b7d655b) with the corrected root cause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
c83fd02642
94 changed files with 16216 additions and 199 deletions
|
|
@ -612,6 +612,10 @@ public sealed class GameWindow : IDisposable
|
|||
// when no selection. Spec: docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md
|
||||
private AcDream.App.UI.TargetIndicatorPanel? _targetIndicator;
|
||||
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
|
||||
// Phase D.2b — retail-look UI tree (dormant UiHost wired here). Null unless ACDREAM_RETAIL_UI=1.
|
||||
private AcDream.App.UI.UiHost? _uiHost;
|
||||
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
||||
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
||||
// Phase I.2: ImGui debug panel ViewModel. Lives for as long as
|
||||
// _panelHost does. Self-subscribes to CombatState in its ctor, so
|
||||
// disposing isn't required (panel host holds the only ref).
|
||||
|
|
@ -862,12 +866,14 @@ public sealed class GameWindow : IDisposable
|
|||
private int _liveAnimRejectSingleFrame;
|
||||
private int _liveAnimRejectPartFrames;
|
||||
|
||||
public GameWindow(AcDream.App.RuntimeOptions options, WorldGameState worldGameState, WorldEvents worldEvents)
|
||||
public GameWindow(AcDream.App.RuntimeOptions options, WorldGameState worldGameState, WorldEvents worldEvents,
|
||||
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
|
||||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
_datDir = options.DatDir;
|
||||
_worldGameState = worldGameState;
|
||||
_worldEvents = worldEvents;
|
||||
_uiRegistry = uiRegistry;
|
||||
SpellBook = new AcDream.Core.Spells.Spellbook(SpellTable);
|
||||
LocalPlayer = new AcDream.Core.Player.LocalPlayerState(SpellBook);
|
||||
}
|
||||
|
|
@ -972,8 +978,10 @@ public sealed class GameWindow : IDisposable
|
|||
_kbSource = new AcDream.App.Input.SilkKeyboardSource(firstKb);
|
||||
_mouseSource = new AcDream.App.Input.SilkMouseSource(
|
||||
firstMouse,
|
||||
wantCaptureMouse: () => DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureMouse,
|
||||
wantCaptureKeyboard: () => DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard);
|
||||
wantCaptureMouse: () => (DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureMouse)
|
||||
|| (_uiHost?.Root.WantsMouse ?? false),
|
||||
wantCaptureKeyboard: () => (DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard)
|
||||
|| (_uiHost?.Root.WantsKeyboard ?? false));
|
||||
_mouseSource.ModifierProbe = () => _kbSource.CurrentModifiers;
|
||||
_inputDispatcher = new AcDream.UI.Abstractions.Input.InputDispatcher(
|
||||
_kbSource, _mouseSource, _keyBindings);
|
||||
|
|
@ -1054,7 +1062,8 @@ public sealed class GameWindow : IDisposable
|
|||
// K.1b §E: explicit WantCaptureMouse defense-in-depth on the
|
||||
// surviving direct-mouse handler. Suppresses RMB orbit /
|
||||
// FlyCamera look while ImGui has the mouse focus.
|
||||
if (DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureMouse)
|
||||
if ((DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureMouse)
|
||||
|| (_uiHost?.Root.WantsMouse ?? false))
|
||||
{
|
||||
_lastMouseX = pos.X;
|
||||
_lastMouseY = pos.Y;
|
||||
|
|
@ -1744,6 +1753,175 @@ public sealed class GameWindow : IDisposable
|
|||
// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132.
|
||||
_samplerCache = new SamplerCache(_gl);
|
||||
|
||||
// Phase D.2b — retail-look UI (ACDREAM_RETAIL_UI=1). Wires the existing
|
||||
// UiHost retained-mode tree (dormant until now) + a first vitals panel.
|
||||
// Render-only: UiHost input is NOT yet bridged to the InputDispatcher
|
||||
// (next sub-phase), so the close button + window drag are inert. Coexists
|
||||
// with the ImGui devtools path (ACDREAM_DEVTOOLS=1), which is unchanged.
|
||||
if (_options.RetailUi)
|
||||
{
|
||||
_vitalsVm ??= new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat, LocalPlayer);
|
||||
_uiHost = new AcDream.App.UI.UiHost(_gl, shadersDir, _debugFont);
|
||||
|
||||
// Feed Silk input to the UiRoot tree so windows drag / close / select.
|
||||
// UiRoot consumes UI events; the game InputDispatcher (subscribed to the
|
||||
// same devices) is gated off via WantCaptureMouse/Keyboard above when the
|
||||
// pointer is over a widget — no double-handling.
|
||||
foreach (var m in _input!.Mice) _uiHost.WireMouse(m);
|
||||
foreach (var kb in _input!.Keyboards) _uiHost.WireKeyboard(kb);
|
||||
|
||||
var cache = _textureCache!;
|
||||
(uint, int, int) ResolveChrome(uint id)
|
||||
{
|
||||
uint t = cache.GetOrUploadRenderSurface(id, out int w, out int h);
|
||||
return (t, w, h);
|
||||
}
|
||||
|
||||
// Phase D.2b — optional retail stylesheet. controls.ini lives under
|
||||
// the AC install (ACDREAM_AC_DIR); absent → source-verified fallback.
|
||||
var controls = _options.AcDir is { } acDir
|
||||
? AcDream.App.UI.ControlsIni.Load(System.IO.Path.Combine(acDir, "controls", "controls.ini"))
|
||||
: AcDream.App.UI.ControlsIni.Parse(string.Empty);
|
||||
// Phase D.2b — retail dat-font for the vitals numbers (Font 0x40000000,
|
||||
// Latin-1, 16px, outline atlas). Passed into the importer so the meter
|
||||
// number overlay renders through the dat-font two-pass blit; falls back to
|
||||
// the debug font only if it fails to load. Under _datLock like other reads.
|
||||
AcDream.App.UI.UiDatFont? vitalsDatFont;
|
||||
lock (_datLock)
|
||||
vitalsDatFont = AcDream.App.UI.UiDatFont.Load(_dats!, _textureCache!);
|
||||
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.");
|
||||
|
||||
// Phase D.2b — the vitals window is data-driven from the dat LayoutDesc
|
||||
// (0x2100006C) via the LayoutImporter. The former hand-authored vitals.xml
|
||||
// markup path was retired after the importer proved pixel-identical at the
|
||||
// 2026-06-15 A/B gate. MarkupDocument stays for plugin/custom panels.
|
||||
AcDream.App.UI.Layout.ImportedLayout? imported;
|
||||
lock (_datLock)
|
||||
imported = AcDream.App.UI.Layout.LayoutImporter.Import(
|
||||
_dats!, 0x2100006Cu, ResolveChrome, vitalsDatFont);
|
||||
if (imported is not null)
|
||||
{
|
||||
AcDream.App.UI.Layout.VitalsController.Bind(imported,
|
||||
healthPct: () => _vitalsVm!.HealthPercent,
|
||||
staminaPct: () => _vitalsVm!.StaminaPercent ?? 0f,
|
||||
manaPct: () => _vitalsVm!.ManaPercent ?? 0f,
|
||||
healthText: () => (_vitalsVm!.HealthCurrent, _vitalsVm.HealthMax) is (uint c, uint m) ? $"{c}/{m}" : "",
|
||||
staminaText: () => (_vitalsVm!.StaminaCurrent, _vitalsVm.StaminaMax) is (uint c, uint m) ? $"{c}/{m}" : "",
|
||||
manaText: () => (_vitalsVm!.ManaCurrent, _vitalsVm.ManaMax) is (uint c, uint m) ? $"{c}/{m}" : "");
|
||||
// Top-level retail window: user-positioned (Anchors.None so the per-frame
|
||||
// anchor pass doesn't reset it), movable, and horizontally resizable like
|
||||
// retail. On a width change the dat edge-anchors reflow the pieces
|
||||
// (UIElement::UpdateForParentSizeChange @0x00462640): top/bottom edges +
|
||||
// the three bars stretch, corners stay 5px, the right edge/corners track
|
||||
// the right side. Vertical resize is off (the layout has no vertical stretch).
|
||||
var vitalsRoot = imported.Root;
|
||||
vitalsRoot.Left = 10; vitalsRoot.Top = 30;
|
||||
vitalsRoot.ClickThrough = false;
|
||||
vitalsRoot.Anchors = AcDream.App.UI.AnchorEdges.None;
|
||||
vitalsRoot.Draggable = true;
|
||||
vitalsRoot.Resizable = true;
|
||||
vitalsRoot.ResizeX = true;
|
||||
vitalsRoot.ResizeY = false;
|
||||
vitalsRoot.MinWidth = 40f;
|
||||
_uiHost.Root.AddChild(vitalsRoot);
|
||||
Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C).");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable.");
|
||||
}
|
||||
|
||||
// Retail chat window — data-driven from LayoutDesc 0x21000006 (gmMainChatUI),
|
||||
// the same importer path as vitals. ChatWindowController binds the transcript,
|
||||
// input, scrollbar and channel menu and routes through ChatVM + ChatCommandRouter.
|
||||
var retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200);
|
||||
AcDream.App.UI.Layout.ElementInfo? chatRootInfo;
|
||||
AcDream.App.UI.Layout.ImportedLayout? chatLayout;
|
||||
lock (_datLock)
|
||||
{
|
||||
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);
|
||||
}
|
||||
if (chatRootInfo is not null && chatLayout is not null)
|
||||
{
|
||||
var chatController = AcDream.App.UI.Layout.ChatWindowController.Bind(
|
||||
chatRootInfo, chatLayout, retailChatVm,
|
||||
() => _commandBus ?? (AcDream.UI.Abstractions.ICommandBus)AcDream.UI.Abstractions.NullCommandBus.Instance,
|
||||
vitalsDatFont, _debugFont, ResolveChrome);
|
||||
if (chatController is not null)
|
||||
{
|
||||
// Ctrl+C / Ctrl+A on the transcript + Ctrl+C/X/V/A on the input need the
|
||||
// keyboard for clipboard + modifier (Ctrl/Shift) state. _uiHost.Keyboard
|
||||
// is set by WireKeyboard above — it is non-null here.
|
||||
chatController.Transcript.Keyboard = _uiHost.Keyboard;
|
||||
chatController.Input.Keyboard = _uiHost.Keyboard;
|
||||
// Wrap the dat content in the universal 8-piece beveled window chrome —
|
||||
// the SAME UiNineSlicePanel the vitals window uses. The chat's own dat
|
||||
// layout only carries flat background sprites, so without this the window
|
||||
// has no retail-style border (the user asked for the vitals border). The
|
||||
// nine-slice IS the movable/resizable window; the dat content fills its
|
||||
// interior, inset by the border. The gmMainChatUI content is authored 490
|
||||
// wide (its transcript/input panels) — KEEP that width + the dat-authored
|
||||
// HEIGHT so the content's child anchors (input-bar-at-bottom, transcript-
|
||||
// fills) capture correct margins on first layout; resizing the frame reflows
|
||||
// them correctly from there.
|
||||
const int chatBorder = AcDream.App.UI.RetailChromeSprites.Border;
|
||||
var chatRoot = chatController.Root;
|
||||
float contentW = 490f, contentH = chatRoot.Height; // dat-authored height
|
||||
var chatFrame = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
|
||||
{
|
||||
Left = 10, Top = 440,
|
||||
Width = contentW + 2 * chatBorder, Height = contentH + 2 * chatBorder,
|
||||
MinWidth = 200f, MinHeight = 90f,
|
||||
// Retail chat is translucent — fade the window's backgrounds/chrome
|
||||
// (text stays opaque). Configurable opacity is a later step; 0.75 reads
|
||||
// as see-through-but-readable. (retail SetDefaultOpacity ~0.5 / active 1.0)
|
||||
Opacity = 0.75f,
|
||||
};
|
||||
chatRoot.Left = chatBorder; chatRoot.Top = chatBorder;
|
||||
chatRoot.Width = contentW; chatRoot.Height = contentH;
|
||||
chatRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
|
||||
| AcDream.App.UI.AnchorEdges.Right | AcDream.App.UI.AnchorEdges.Bottom;
|
||||
chatRoot.Draggable = false; chatRoot.Resizable = false;
|
||||
chatFrame.AddChild(chatRoot);
|
||||
_uiHost.Root.AddChild(chatFrame);
|
||||
// Tab / Enter enters "write mode" by focusing this input (retail's chat
|
||||
// activation); a focused input suppresses character movement (see the
|
||||
// WantsKeyboard gate in the movement poll).
|
||||
_uiHost.Root.DefaultTextInput = chatController.Input;
|
||||
Console.WriteLine("[D.2b] retail chat window from LayoutDesc importer (0x21000006).");
|
||||
}
|
||||
else Console.WriteLine("[D.2b] chat: required role elements missing in 0x21000006.");
|
||||
}
|
||||
else Console.WriteLine("[D.2b] chat: LayoutDesc 0x21000006 not found.");
|
||||
|
||||
// Drain plugin-registered markup panels (buffered before the GL
|
||||
// window opened) into the same UiRoot tree. A faulty plugin markup
|
||||
// file is isolated — logged + skipped, never crashes the client.
|
||||
if (_uiRegistry is not null)
|
||||
{
|
||||
foreach (var p in _uiRegistry.Drain())
|
||||
{
|
||||
try
|
||||
{
|
||||
string pluginXml = System.IO.File.ReadAllText(p.MarkupPath);
|
||||
var pluginPanel = AcDream.App.UI.MarkupDocument.Build(
|
||||
pluginXml, p.Binding, ResolveChrome, controls);
|
||||
_uiHost.Root.AddChild(pluginPanel);
|
||||
Console.WriteLine($"[D.2b] plugin UI panel loaded: {p.MarkupPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[D.2b] plugin UI panel '{p.MarkupPath}' failed to load: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase N.4+N.5 — WB rendering pipeline foundation. The modern path is
|
||||
// mandatory as of N.5 ship amendment: WbMeshAdapter + WbDrawDispatcher
|
||||
// always construct.
|
||||
|
|
@ -7099,6 +7277,11 @@ public sealed class GameWindow : IDisposable
|
|||
// this guard adds defense-in-depth for the per-frame IsActionHeld
|
||||
// movement poll below (typing "walk" into a chat field shouldn't
|
||||
// walk).
|
||||
// ImGui dev-tools text fields fully pause game input (incl. autorun) — fine, it's a
|
||||
// debug overlay. The RETAIL chat "write mode" does NOT early-return here: the block
|
||||
// below still runs so AUTORUN keeps driving the character while you type. Held WASD
|
||||
// is silenced at the source instead — InputDispatcher.IsActionHeld returns false
|
||||
// while WantCaptureKeyboard (which includes a focused chat input) is set.
|
||||
bool suppressGameInput =
|
||||
DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard;
|
||||
if (suppressGameInput) return;
|
||||
|
|
@ -8476,6 +8659,16 @@ public sealed class GameWindow : IDisposable
|
|||
SkipWorldGeometry: ;
|
||||
}
|
||||
|
||||
// Phase D.2b — retail-look UI tree (render-only; input integration deferred).
|
||||
// Self-contained 2D pass: UiHost.Draw → TextRenderer.Flush sets its own
|
||||
// blend/depth state and restores. Drawn before ImGui so the devtools
|
||||
// overlay composites on top during development.
|
||||
if (_options.RetailUi && _uiHost is not null)
|
||||
{
|
||||
_uiHost.Tick(deltaSeconds);
|
||||
_uiHost.Draw(new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y));
|
||||
}
|
||||
|
||||
// Phase D.2a — end ImGui frame. Runs AFTER all scene + debug draws
|
||||
// so ImGui composites on top. ImGuiController save/restores the
|
||||
// GL state it touches (blend, scissor, VAO, shader, texture); any
|
||||
|
|
@ -12496,6 +12689,7 @@ public sealed class GameWindow : IDisposable
|
|||
_sceneLightingUbo?.Dispose();
|
||||
_particleRenderer?.Dispose();
|
||||
_debugLines?.Dispose();
|
||||
_uiHost?.Dispose();
|
||||
_textRenderer?.Dispose();
|
||||
_debugFont?.Dispose();
|
||||
_dats?.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue