Port the retail UIElement_Text editable single-line field:
- Focused = "write mode": draws the gold lit field sprite (0x060011AB, the
Normal_focussed state) instead of the flat translucent rect; Enter submits
AND blurs (exits write mode).
- Single-line SELECTION: click-drag, Shift+Arrows, Shift+Home/End, Ctrl+A;
translucent-blue highlight behind the span; typing/Backspace/Delete/Paste
replace the selection first.
- CLIPBOARD: Ctrl+C copy, Ctrl+X cut, Ctrl+V paste at the caret (control chars
stripped — single-line). Wired to the keyboard device for clipboard + Ctrl/
Shift state.
- Held-key AUTO-REPEAT (Silk delivers one KeyDown per press): Backspace /
Delete / Left / Right repeat via a 0.4s-delay, ~25/s OnTick timer.
- Horizontal SCROLL + clip: keeps the caret in the field and draws only the
glyph window that fits inside it, so long input scrolls within the box
instead of spilling past Send into the 3D world.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the talk-focus menu + button to retail (decomp-verified):
- Menu item text is FILL-ONLY (retail UIElement_Text outlines only when
SetOutline(true); the talk-focus items don't) — kills the grey halo. Available
items render white; UNAVAILABLE items render grey (not the salmon colorPink,
which is a chat-MESSAGE color we'd misapplied). Special items (Squelch /
Tell-to-Selected) render white. Labels indent past the baked checkbox in the
row sprite (0600124E empty box / 0600124D white checkmark) instead of
overlapping it.
- The popup is wrapped in the universal 8-piece window bevel (the menu sprite
family has no border) and draws in OnDrawOverlay so the translucent chat
panel no longer greys its right column.
- The button face (0600124D/66, a fixed 46px LED+arrow sprite) is now 3-sliced
(LED cap / stretch / arrow cap) and autosizes to its label via
NaturalButtonWidth, so "Chat" fits in the body instead of running into the
arrow. The status LED (red Normal / green Pressed) is no longer overdrawn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The retail-look render + focus primitives this chat pass builds on:
- TextRenderer: an OVERLAY layer (sprite/rect/text buckets flushed AFTER the
normal layer) so an open popup composites on top of everything incl. rect
panel backgrounds; a DrawFill primitive (solid quad via a 1x1 white texture)
routed through the SPRITE bucket so a panel background draws UNDER its text
instead of being washed by the later rect bucket; and the text pass now
disables SampleAlphaToCoverage + Multisample so glyph alpha edges aren't
dithered into MSAA coverage (the "fuzzy text") — self-contained GL state
per feedback_render_self_contained_gl_state.
- UiRenderContext.DrawStringDat: snap the line baseline to a whole pixel ONCE
then add the integer per-glyph offset (retail DrawCharacter takes an int
pen-Y + schar m_VerticalOffsetBefore) — fixes the "letters dip down" jitter
at a fractional line origin. Outline pass is now opt-in (retail gates it per
element via SetOutline; default off = crisp fill-only). Adds DrawFill +
Begin/EndOverlayLayer.
- UiElement: OnDrawOverlay + DrawOverlays (second traversal), FindRoot (blur
self), ResetAnchorCapture (re-baseline an anchored element after reflow).
- UiRoot: runs the overlay pass after the main tree; Tab/Enter focuses the
DefaultTextInput (write-mode activation); a left click on a non-edit target
blurs the focused input (exit write mode without submitting).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(D.2b): point-sample the dat-font atlas so UI text is pixel-crisp
The font glyph atlas was uploaded with bilinear (Linear) min/mag filtering, which
softens the small dat-font glyphs (the menu/button text "blur"). Add a nearest-filter
path to UploadRgba8/GetOrUploadRenderSurface and use it for the font atlases only
(world + other UI surfaces keep bilinear). Combined with the existing per-glyph
pixel-snap, glyph texels now map 1:1 to screen pixels. Sharpens all dat-font text
(transcript, menu, Send/Chat buttons, vitals numbers).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
fix(D.2b): arrow swap, centered menu text, scrollbar-to-top, Send caption
- scroll arrows: native sprites are opposite (0x06004C6C up / 0x06004C69 down) per live
visual — swap the assignment, drop the V-flip.
- menu labels centered vertically in each 17px row (was top-aligned, looked corrupt).
- scrollbar pulled up to the panel top so the top arrow meets the window border and the
max/min button lines up with it (the 6px dat offset left a gap after the resize-bar reclaim).
- Send button: the dat sprite 0x06001915 is a blank gold frame (export-confirmed), so add a
generic optional Label/LabelFont to UiDatElement and draw "Send" centered on it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
feat(D.2b): data-driven channel menu chrome + greying + scroll-arrow fix
Investigation found the menu popup is fully dat-driven (UIElement_Menu::MakePopup
@0x46d310 reads LayoutDesc 0x21000006 elements 0x1000001C/1D/1E — the "stray" top-level
elements). Render the popup from the real sprites instead of a flat rect:
- panel 0x0600124C, item row 0x0600124E, selected row 0x0600124D; 191x17 rows, 2 cols.
- drawing rows as SPRITES also fixes the z-order (a DrawRect bg composited OVER the
labels; sprites share the labels submission bucket so text lands on top).
- item greying: available channels white, unavailable salmon (colorPink) — static
approximation (Say/General/Trade/LFG) with an AvailabilityProvider hook for live
TurbineChat state; unavailable items are inert on click. Ports ResetAllTalkFocusMenuButtons.
- scroll arrows: both dat sprites point down (export-confirmed); V-flip the top button
so it points up.
Tabs confirmed to have NO digits in retail (blank gold frames) — acdream already matches.
Build + 392 App tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
fix(D.2b): channel menu popup opaque + button label tracks selected target
- the popup inherited the chat window 0.75 opacity so the transcript bled through;
add UiRenderContext.PushAlphaAbsolute and draw the popup at absolute opacity.
- the "Chat" button was hardcoded; it now shows the active talk target (retail
updates it on selection). Exact textured menu-panel sprite is a follow-up (the
popup is a keystone UIElement_Menu construct, not in the chat LayoutDesc).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
feat(D.2b): exact retail chat colors from a live cdb dump
Attached cdb to a live retail acclient (PDB-matched) and read the named RGBAColor
constants at acclient 0x81c4a8+ (colorWhite/colorBrightPurple/colorLightBlue/
colorGreen/colorLightRed/colorGrey), used by ChatInterface::BuildChatColorLookupTable
@0x4f31c0. Replaced the approximated RetailChatColor palette with the ground-truth
values: speech=white, tell=colorBrightPurple(1,.498,1), channel=colorLightBlue
(.247,.749,1), system/popup=colorGreen(.5,1,.498), combat=colorLightRed, emote=colorGrey.
Capture scripts saved under tools/cdb/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
feat(D.2b): chat polish — typing fix, opacity, scrollbar 3-slice, retail channel menu
Visual-iteration batch (decomp-grounded), each fix verified against the retail screenshots:
- typing: UiElement.HitTest aborted on ClickThrough BEFORE walking children, so the
ClickThrough UiDatElement panels blocked hit-testing to the input/transcript inside
them. Check ClickThrough AFTER the child walk (it only gates whether THIS element
claims the hit). Restores input focus + typing.
- opacity: UiElement.Opacity + a UiRenderContext alpha stack applied to sprite/rect
draws (text bypasses it, stays sharp); chat frame Opacity=0.75 → translucent chat.
- brown sliver: grow the transcript panel up 9px to cover the dropped resize-bar strip.
- scrollbar: real 3-slice thumb (caps 0x06004C60/66 + tiled mid) + tiled track.
- max/min: shifted one button-width left of the scrollbar (dat right-anchors collide).
- system text now green (retail ChatMessageType 5; was yellow).
- word-wrap: transcript lines wrap to the panel width (greedy, ports GlyphList::Recalculate).
- channel menu reworked to retail gmMainChatUI::InitTalkFocusMenu: "Chat" button + a
TWO-COLUMN popup of the 14 talk-focus items (Squelch, Tell to Selected, Chat to All,
Tell to Fellows, ...) on a tan panel; channel items set the active outbound channel.
Build + 392 App tests green. Visual confirmation in progress.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
The live session + its LiveCommandBus are created after the retail-UI block in
OnLoad, so binding the bus by value captured NullCommandBus and silently dropped
outbound chat. Pass a Func<ICommandBus> resolved at submit time (mirrors how the
ImGui ChatPanel re-reads the bus each frame).
AP-41: scrollbar thumb drawn as single stretched tile (0x06004C63) instead of
retail's 3-slice top-cap/middle/bottom-cap — acknowledged in UiChatScrollbar.cs:37,
registered per the divergence-register rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hand-authored chat block (UiNineSlicePanel + inline UiChatView
+ local BuildRetailChatLines/RetailChatColor statics) with
ChatWindowController.Bind(LayoutDesc 0x21000006) — the same LayoutImporter
path as the vitals window. The controller places UiChatView (transcript) +
UiChatInput (text entry, on-submit) + UiChatScrollbar + UiChannelMenu inside
the dat-authored chrome. The dead local statics are deleted.
Wired to _commandBus (same LiveCommandBus as the ImGui ChatPanel) so
type+Enter dispatches SendChatCmd server-ward. Transcript keyboard set from
_uiHost.Keyboard (set by WireKeyboard above the chat block) for Ctrl+C/Ctrl+A.
Divergence register: added AD-28 (two-widget split vs UIElement_Text),
AP-38 (no in-element word-wrap), AP-39 (per-line colour vs per-glyph runs),
AP-40 (no opacity fade / shared vitalsDatFont), TS-30 (tab buttons no-op),
TS-31 (no squelch); updated IA-15 to cover both vitals + chat importer paths.
Build: 0 errors/warnings. Tests: 392 passed, 1 skipped (expected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements Task G2: binds the imported chat LayoutDesc (0x21000006) to live
behavior, the acdream analogue of retail ChatInterface + gmMainChatUI::PostInit.
- UiDatElement: add OnClick hook + OnEvent override so Send/max-min buttons
can be wired by a controller without needing a dedicated widget type.
- ChatWindowController.Bind: reads transcript (0x10000011) and input
(0x10000016) rects from the raw ElementInfo tree (factory skips them as
Type-12/no-media), places UiChatView under the transcript panel and
UiChatInput under the input bar; replaces the imported scrollbar track
(0x10000012) with UiChatScrollbar driving UiChatView.Scroll; replaces
the channel menu placeholder (0x10000014) with UiChannelMenu; wires
Send button and max/min toggle via the new OnClick hook.
ChatCommandRouter.Submit routes all input through the existing pipeline.
- 6 smoke tests: Bind returns non-null, Transcript is child of panel,
Input is child of bar, Input.OnSubmit publishes SendChatCmd, channel
change updates submit channel, returns null when panels missing.
Build: 0 errors. Test suite: 392 passed / 1 skipped / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task G1: two gaps blocked chat window static sprite elements from rendering.
Change 1 — DatWidgetFactory: only skip Type-12 elements that have no own
state media (pure style prototypes). A Type-12 element that carries sprites
(e.g. a chat Send button whose derived Type-0 element inherited Type 12 from
its base prototype) now renders as a UiDatElement.
Change 2 — ElementInfo: add DefaultStateName field (string, default "").
Change 3 — LayoutImporter.ToInfo: read ElementDesc.DefaultState.ToString()
into DefaultStateName; normalize Undef/Undefined/0 sentinels to "".
Change 4 — ElementReader.Merge: inherit DefaultStateName (derived wins if
non-empty, else base).
Change 5 — UiDatElement ctor: initialize ActiveState to DefaultStateName
when set; else "Normal" when a Normal-state sprite is present (retail's
implicit default for buttons/tabs); else "" (DirectState). This makes the
Send button, max/min button, and numbered tabs render their default sprite
without requiring explicit state assignment at runtime.
Vitals neutrality: all vitals chrome/grip elements carry DirectState-only
sprites with no "Normal" named state and DefaultStateName="" (Undef in dat),
so their ActiveState stays "" and their existing conformance tests are
unaffected. Vitals text labels (Type 0→12 via Merge, no StateMedia) are
still skipped by the refined Type-12 guard (StateMedia.Count==0).
Tests: 4 new tests (2 in DatWidgetFactoryTests, 3 in UiDatElementTests).
All 386 pass; 387 total (1 pre-existing skip).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port of retail gmMainChatUI::InitTalkFocusMenu @0x4cdc50 + HandleSelection
@0x4cd540 → SetTalkFocus. Button shows active channel label; click opens a
12-item popup that extends UPWARD (chat sits at screen bottom); selecting an
entry calls OnChannelChanged and updates Selected. BitmapFont? Font uses the
fully-qualified type name to match UiChatInput convention. Includes 6 xunit
tests covering channel table shape, default selection, and popup-pick routing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports retail UIElement_Text editable one-line mode (caret = glyph index;
caret pixel-X = sum of glyph advances via UiDatFont) and ChatInterface's
100-entry command history (up/down arrow; sentinel -1 = live line).
Submit (Enter/KeypadEnter) fires OnSubmit callback, clears, pushes history.
Draws via DrawStringDat (dat font) or DrawString (BitmapFont) fallback.
AcceptsFocus=true + IsEditControl=true so UiRoot routes Char/KeyDown to it
and suppresses global hotkeys while typing. 6 new tests, all green.
Decomp refs: UIElement_Text::MoveCursor @0x468d00,
UIElement_Text::FindPixelsFromPos @0x472b40,
ChatInterface::ProcessCommand @0x4f5100
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the right-side chat scrollbar widget. Ports retail
UIElement_Scrollbar::UpdateLayout @0x4710d0 (thumb sizing + placement)
and HandleButtonClick @0x470e90 (step ±1 line, page on track click).
Dat element ids sourced from chat LayoutDesc 0x21000006 (base layout
0x2100003E): up-button sprite 0x06004C69, down-button 0x06004C6C, track
0x06004C5F, thumb middle 0x06004C63. Up/down buttons occupy the top and
bottom ButtonH (16px) regions of the widget height, matching element
positions Y=0 and Y=32 in the base scrollbar template.
Adds 6 pure ThumbRect tests (no GL): sizing, clamping to MinThumb,
position at start/mid/end, no-overflow full-fill.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the ad-hoc _scroll float with a public UiScrollable instance.
OnDraw feeds ContentHeight/ViewHeight/LineHeight into the model each
frame and reads baseY = bottom - contentH + (MaxScroll - ScrollY) —
the (MaxScroll-ScrollY) inversion reconciles UiScrollable's top-origin
convention (0=oldest, MaxScroll=newest) with the visual layout (newest
at bottom). The wheel handler routes through ScrollByLines with a sign
flip so wheel-up still reveals older lines. _pinBottom tracks whether
the view is at the end and calls ScrollToEnd() each draw to auto-scroll
new messages. ClampScroll static method kept — referenced by existing
tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add `DatFont` property (UiDatFont?): when set, OnDraw uses
ctx.DrawStringDat + datFont.MeasureWidth for all transcript lines;
BitmapFont path unchanged as fallback when DatFont is null.
- Cache `_lastDatFont` alongside `_lastFont` so HitChar hit-tests the
same advance source that drew the last frame.
- HitChar prefers `_lastDatFont` (via UiDatFont.GlyphAdvance) over
`_lastFont` (via bf.Advance) for column resolution, keeping
drag-select and Ctrl+C accurate with the dat font.
- Scroll event handler uses DatFont?.LineHeight first, so the wheel
quantum stays correct when the dat font has a different line height.
- WheelLines 3f → 1f: retail UIElement_Text::HandleMouseWheel
(@0x471450) advances one line per notch; our 3-line quantum was
wrong.
- Add UiChatViewDatFontTests: pins GlyphAdvance formula
(Before+Width+After = 10) and CharIndexAt dat-advance integration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both the ImGui devtools ChatPanel and the upcoming retail chat window
now route through ChatCommandRouter.Submit so command handling lives in
one place. The ChatPanel inline block (TryHandleClientCommand / EqAny /
BuildHelpText) is deleted; ChatCommandRouter carries the same logic
verbatim. ChatPanel.Render becomes a 2-line delegate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(D.2b): chat-window re-drive design spec + list-ui-layouts research tool
Plan-2 chat piece of the LayoutDesc importer. Identifies the chat window as
LayoutDesc 0x21000006 (gmMainChatUI, element class 0x10000041) and grounds a
faithful, data-driven re-drive in the named retail decomp (ChatInterface +
gmMainChatUI + UIElement_Text/_Scrollable/_Scrollbar/_Menu) plus a user-provided
retail screenshot.
Design (full-faithful scope, user-approved):
- transcript = UIElement_Text 0x10000011 (dat font, bottom-pinned, 10k behead cap,
pixel scroll, 1 line/wheel-notch)
- scrollbar = right-side track 0x10000012 + thumb 0x1000048c + up/down
- input = editable UIElement_Text 0x10000016 (caret, 100-entry history, Enter/Send)
- channel menu = UIElement_Menu 0x10000014 ("Chat" selector -> active channel)
- shared ChatCommandRouter extracted from ChatPanel
- screenshot correction: the four 0x10000522-525 left-edge elements are the
numbered CHAT TABS (1-4), not scroll buttons (a research-agent inference the
retail screenshot refutes)
- deferred (need non-UI plumbing, each gets a divergence row): tab switching/
filtering, squelch, clickable name-tags, in-element word-wrap, styled runs,
font config, opacity transition
Tooling: AcDream.Cli `list-ui-layouts <datdir> [0xRootType]` — read-only index of
every UI LayoutDesc by root element class + size + element-Type histogram; how the
chat layout was located (root type 0x10000041). Reusable for future panel re-drives.
Spec: docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
DrawStringDat placed each glyph quad at the raw (often fractional) pen/origin.
When a bar resizes to a fractional width, the centered cur/max number lands on a
sub-pixel x and the glyph atlas (linear-filtered) smears — the 'unsharp at certain
sizes' artifact. Round each glyph's destination to whole pixels (the pen keeps its
true fractional advance, so spacing is unaffected) — matches retail blitting glyphs
to integer dest. User-confirmed sharp across resize widths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TextRenderer batched sprites per-texture and drew each texture's whole buffer at
its FIRST-insertion point. The dat-font glyph atlas is one shared texture used by
all three vital numbers; it first appeared at the health bar, so all three numbers
were emitted right after the health bars — then the stamina + mana bar sprites
painted over their own numbers (only health survived). Replaced the per-texture
dictionary with submission-ordered segments (consecutive same-texture quads still
batch); each meter's number now draws after its own bars. The renderer's own
comment had predicted this break once bars became sprites (importer did that).
Removed the temporary UiMeter label diagnostic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToAnchors was inverted vs retail UIElement::UpdateForParentSizeChange @0x00462640:
stretch is RightEdge==1 (not ==2/==4), LeftEdge==2 = track-right. Verified against
all 19 vitals fixture pieces. Enables Resizable/ResizeX on the importer vitals root
(the prior 'dat is fixed-size' conclusion was wrong). At-rest render unchanged
(anchors only fire on resize). Added a 160->200 resize conformance test.
Also fixed DatWidgetFactoryTests.RectAndAnchors_SetFromElementInfo which encoded
the old inverted model (Right=2 expecting Right anchor; corrected to Right=1).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The importer (proven pixel-identical at the 2026-06-15 A/B gate) is now the
default vitals window when ACDREAM_RETAIL_UI=1 — data-driven from LayoutDesc
0x2100006C. Removed: the hand-authored vitals.xml build path, the asset file
(recoverable from git history), and the now-obsolete ACDREAM_RETAIL_UI_IMPORTER
flag (RuntimeOptions param + parse + 2 tests). The window is user-positioned at
(10,30) and movable; resize stays off — the dat stacked-vitals layout is fixed-
size (chrome edges near-pinned), faithful grip/dragbar resize is Plan 2.
MarkupDocument/UiNineSlicePanel remain for the chat window + plugin panels.
AcDream.App builds 0/0; AcDream.App.Tests 352 passed / 1 skipped / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main was 65 commits ahead of this branch's fork point. Only conflict was the
divergence register: both sides appended an 'AP-32' row. Resolved by keeping
main's AP-32..AP-36 (cell-shell lift, look-in cells, alpha deferral, dungeon
streaming, point lights) and renumbering the importer's row to AP-37; AP header
count -> 37. GameWindow.cs auto-merged cleanly. Verified: AcDream.App builds
0/0; AcDream.App.Tests 354 passed / 1 skipped / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Process/quality items from the LayoutDesc-importer final review — no runtime
behavior change.
I1a — amend IA-15: the 8-piece chrome edge/corner→position mapping is no longer
a guess. The LayoutImporter (ACDREAM_RETAIL_UI_IMPORTER) reads real LayoutDesc
dat data and resolves positions + sprite ids directly; locked by the conformance
fixture vitals_2100006C.json. Residual risk trimmed to anchor resolution at
non-800×600 + controls.ini cascade. Pointers added to LayoutImporter.cs and the
format-doc.
I1b — add AP-32: the importer collapses the dat's nested meter structure
(Type-7 → two Type-3 containers → three image-slice grandchildren each) into
UiMeter's programmatic 3-slice fields instead of building those nodes generically
and porting UIElement_Meter::DrawChildren. Standalone Type-0 text elements are
also skipped (Plan 2). Retail oracles: UIElement_Meter::DrawChildren @0x46fbd0,
UIElement_Text::DrawSelf @0x467aa0.
I1c — AP section header 31 → 32.
N1 — ElementReader.cs: comment at the Type-merge line explaining that a derived
Type 0 (text element) inherits the base's Type 12 (style prototype), which
DatWidgetFactory skips; safe for Plan 1 because vitals numbers render via
UiMeter.Label. Format-doc §10: correct the "render as UiDatElement" sentence to
"skipped entirely" (Type-0 → inherits Type-12 via Merge → factory returns null).
N4 — new conformance test VitalsTree_TextLabel_InheritsFontDidFromBaseLayout:
walks the raw ElementInfo tree from the fixture and asserts at least one element
carries FontDid==0x40000000, proving Resolve()'s inheritance merge fired against
real dat data. FixtureLoader gains LoadVitalsInfos() that returns the raw tree
without calling Build.
Tests: 36 pass (was 35), 0 errors, 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: replace 3 copy-paste meter blocks in VitalsTree_MetersHaveExpectedSliceIds
with a single table-driven loop — a 4th meter is now a one-liner and failures
name the failing meter id directly.
Fix 2: FixtureLoader now reads the fixture as bytes and strips the UTF-8 BOM
(EF BB BF) before passing the span to JsonSerializer, so a BOM-bearing fixture
file never causes a spurious JsonReaderException.
Fix 3: add [Trait("Category", "Conformance")] at the class level so conformance
tests are selectable by category filter.
Fix 4: add missing <param name="layoutId"> doc tag to LayoutImporter.ImportInfos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Job 1: extract LayoutImporter.ImportInfos() (public dat-shell half that returns the
resolved ElementInfo tree without building widgets) so fixture generation and
conformance tests can call it directly. Import() now delegates to ImportInfos() +
Build() — existing 32 Layout tests stay green.
Job 2: generate tests/AcDream.App.Tests/UI/Layout/fixtures/vitals_2100006C.json
from the real portal.dat via a throwaway [Fact] generator (deleted, not committed).
System.Text.Json with IncludeFields=true — ValueTuple serializes as Item1/Item2.
Pre-write validation confirmed health meter BackLeft=0x0600747E FrontRight=0x06007483
rect (5,5,150,16). Round-trip deserialization re-validated before writing.
Job 3: FixtureLoader.LoadVitals() deserializes the fixture from the test output
directory (CopyToOutputDirectory item in csproj) and returns ImportedLayout via
LayoutImporter.Build(root, _ => (0,0,0), null) — no dats, no GL.
Job 4: LayoutConformanceTests — 3 golden tests (35 asserts total):
- VitalsTree_HasThreeMetersAtExpectedRects: 3 meters at x=5, w=150, h=16, y=5/21/37
- VitalsTree_MetersHaveExpectedSliceIds: all 18 back+front slice ids health/stamina/mana
- VitalsTree_ChromeCornerHasExpectedSprite: TL corner 0x10000633 → sprite 0x060074C3
Full App suite: 326 pass / 1 skip (pre-existing) / 0 fail. Build: 0 errors, 0 warnings.
Throwaway generator not committed (confirmed via git status).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds RuntimeOptions.RetailUiImporter (ACDREAM_RETAIL_UI_IMPORTER=1) — a new
opt-in flag that runs the LayoutImporter-built vitals window ALONGSIDE the
hand-authored vitals panel for pixel-for-pixel A/B comparison. The importer
window is placed at x=200, y=30 so both render simultaneously within the same
ACDREAM_RETAIL_UI=1 session. The hand-authored path is entirely untouched and
remains the default; the importer path is the eventual switch-over target.
Also adds two RuntimeOptionsRetailUiTests covering the new flag: value "1" →
true, unset/other → false.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: Added a <para> to the VitalsController class summary citing
docs/research/2026-06-15-layoutdesc-format.md §11 as the source of the
three dat element ids, giving a paper trail back to the evidence per
the project's cite-in-comments rule.
Fix 2: Changed FakeLayout in VitalsBindingTests to accept (uint id,
UiElement e) tuples instead of (string idHex, UiElement e), and updated
all three call sites to pass VitalsController.Health/.Stamina/.Mana.
Tests now follow the constants automatically if they ever change rather
than silently passing with stale hex literals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors retail gmVitalsUI::PostInit: grab Health/Stamina/Mana meters from
the imported layout by their dat element ids (0x100000E6 / EC / EE) and
wire Func<float> fill + Func<string> label providers. Missing ids are
silently skipped (no throw). Slice sprites + dat font already set by the
factory — this is pure data wiring, not graphics.
3 TDD tests: single-meter fill+label, all-three distinct providers, missing-id no-throw.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task 5 of the LayoutDesc Importer (Plan 1 — vitals conformance).
Pure layer (BuildFromInfos / Build):
- ImportedLayout result type: UiElement root + O(1) FindElement(uint id) lookup
- BuildWidget dispatches via DatWidgetFactory.Create; skips Type-12 prototypes (null)
- Meters consume their children (DatWidgetFactory already extracted slice ids —
adding the dat children as UiElement nodes would duplicate geometry)
- All other element types recurse children generically via AddChild
Dat shell (Import):
- Loads LayoutDesc from dats; null-safe if layout is absent
- Resolves each top-level ElementDesc to ElementInfo via Resolve():
BaseElement/BaseLayoutId chain with (layoutId,elementId) cycle guard
- ToInfo(): reads ElementDesc scalar fields (uint → float cast) + DirectState +
named States (UIStateId.ToString() as key)
- ReadState(): extracts first MediaDescImage (File + DrawMode) per state +
font DID from Properties[0x1A] → ArrayBaseProperty → DataIdBaseProperty.Value
- Each sibling element gets a fresh base-chain set (siblings don't share guards)
DRW API: all members confirmed from VitalsLayoutDump.cs usings — no
adjustments needed: LayoutDesc in DBObjs; ElementDesc/StateDesc/MediaDescImage/
ArrayBaseProperty/DataIdBaseProperty in Types; DrawModeType/UIStateId in Enums.
Tests (3/3 green):
- BuildFromInfos_HealthMeter_IsUiMeterAtRect — Type-7 child → UiMeter, Left=5, Width=150
- BuildFromInfos_Type12Child_IsSkipped_Type3Present — prototype absent, container present
- BuildFromInfos_MeterWithChildren_MeterPresent_ChildrenNotInTree — meter findable,
both dat-children absent, UiMeter.Children empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: SliceIds now projects the File id during Select rather than calling
TryGetValue twice (once in Where, once in the local File() helper). Added a
comment noting that OrderBy is stable so X-tie order follows insertion order.
Fix 2: BuildMeter emits a [D.2b] Console.WriteLine when the Type-3 container
count is not exactly 2, surfacing malformed or non-vitals meter elements during
Task 8 conformance testing without disturbing the existing solid-color fallback.
Fix 3: Test 5 adds two explicit NotEqual assertions confirming the
ShowDetail-only overlay sprite (OverlayFile = 0x06007490) did not leak into
FrontRight or FrontTile.
5/5 tests pass, 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hybrid factory mapping ElementInfo.Type to a behavioral widget or the
UiDatElement generic fallback. Type 7 (UIElement_Meter) → UiMeter with
back/front 3-slice ids populated from grandchild image elements; Type 12
(style prototypes / BaseElement stores) → null so the importer skips
them; all other types → UiDatElement. Rect + anchors are set on every
returned widget via ElementReader.ToAnchors.
BuildMeter walks two levels of the element tree: the two Type-3 slice
containers ordered by ReadOrder (back behind, front on top), then within
each container the image children that carry a DirectState ("" key)
ordered by X for left-cap/center-tile/right-cap. The expand-detail
overlay (present in the front container with only named ShowDetail/
HideDetail states and no "" entry) is excluded by the TryGetValue("")
filter automatically — no name-matching needed.
Fill/Label providers are intentionally NOT set here; Task 6
(VitalsController) binds them to live stat data.
5 TDD tests: Type7→UiMeter, UnknownType→UiDatElement, Type12→null,
rect+anchors propagation, and meter slice extraction with overlay exclusion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generic fallback widget for every LayoutDesc element type without a
dedicated behavioral widget (chrome corners/edges, drag bars, resize grips).
Holds an ElementInfo + active-state name; draws that state's media by tiling
(UV-repeat on both S+T axes, matching ImgTex::TileCSI). DrawMode constants
documented per format spec §6 (Undefined=0, Normal=1, Overlay=2,
Alphablend=3 — no Stretch mode). Plan 1: all modes render as the same
alpha-blended tiled quad; per-mode branches deferred to Plan 2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Merge: defensive copy `new List<ElementInfo>(derived.Children)` so a
later mutation of the merged result or the input can't corrupt the other
- Merge: add comment on Width/Height 0-sentinel (Plan-1 safe; Plan-2
limitation and float?-upgrade path documented inline)
- Test: replace mid-sentence "Wait —" authoring trace in
EdgeFlagsToAnchors_ValueThree_FallsBackToTopLeft with a clean
conclusion-first summary of the value-3 mapping rule
9/9 ElementReaderTests pass; 0 build errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task 2 of the LayoutDesc Importer (Plan 1 — vitals conformance).
- ElementInfo POCO: GL-free/dat-free snapshot of a resolved layout element.
Shape matches the plan spec exactly (Id, Type as uint, X/Y/Width/Height as
float, raw Left/Top/Right/Bottom uint edge flags, ReadOrder, FontDid, StateMedia
dict, Children list). Tasks 3–6 depend on this shape.
- ElementReader.ToAnchors(uint,uint,uint,uint): maps dat edge-flag values
(0=none, 1=near-pin, 2=far-pin, 3=floating-center, 4=stretch) to AnchorEdges
bit flags. Corrects the plan's stale assumption that value 4 was the only anchor
trigger; the verified format doc §4 shows 1→Left/Top, 2→Right/Bottom, 4→both.
All-zero falls back to Left|Top (default pin top-left).
- ElementReader.Merge(base_, derived): inheritance merge mirroring BaseElement/
BaseLayoutId. Derived scalars win when non-zero; position/edge-flags/ReadOrder
always from derived; StateMedia merged (base defaults, derived overrides);
Children from derived only.
TDD: tests written first (9 tests covering ToAnchors near-pin/far-pin/stretch/
zero/value-3, Merge scalar override/font inheritance/StateMedia merge/children).
All 9 pass; dotnet build 0 errors 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The retail vitals window border is TWO layers, not one: the bevel chrome
(0x060074BF-C6) PLUS a resize-grip overlay on top — gold ridged edge strips
and a square corner stud at each corner. acdream only drew the bevel, so the
border looked plainer than retail and the corners lacked the little square
sprite the user spotted.
The overlay ids come from the vitals LayoutDesc 0x2100006C (elements
0x1000063B-0x10000642): corner stud 0x06006129 (same 5x5 at all four corners),
edge strips 0x0600612A/2C (top/bottom) and 0x0600612B/2D (left/right). They
have transparent gaps so the bevel shows through — both layers are drawn.
UiNineSlicePanel now draws the grip overlay (edges tiled via the existing
UV-repeat, corner studs 1:1) after the bevel, so every retail-chrome window
(vitals + chat) gets it.
Verified the grip sprites + the composited result headlessly: dump-sprite-sheet
(new CLI: composite arbitrary sprite ids magnified) showed 0x06006129 is a gold
stud and 0x0600612A-D are gold ridged strips; render-vitals-mockup now renders
the faithful default window with the overlay.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retail repeats the bar's "fill-tile" graphic at native width (verified:
the dat element 0x100000E9 is literally the fill-tile; the engine fills via
ImgTex::TileCSI; and a widened side-by-side shows retail tiling, not
stretching). acdream was stretching one copy of the middle slice across the
whole span, so the bevel/bead pattern smeared as the window widened.
UiMeter.DrawHBar now UV-repeats each slice at its NATIVE width: caps span one
native width (a single 1:1 copy), the wide middle spans many (it tiles, last
copy UV-cropped). This works because the UI textures are already GL_REPEAT-
wrapped (TextureCache.UploadRgba8) — the exact mechanism UiNineSlicePanel's
chrome border already uses, so the border edges were ALREADY tiling and need
no change. One draw call per slice; composes with the existing fill-fraction
clip (the partial last tile shows a partial bead).
render-vitals-mockup now renders a widened window twice (stretch vs tile) so
the difference is verifiable headless. Confirmed the tile repeats seamlessly
(no seams).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Windows-like selection in the retail chat window: left-click-drag selects
characters, Ctrl-C copies, Ctrl-A selects all. The selected span paints a
translucent highlight behind the text.
- UiElement.CapturesPointerDrag: a per-element opt-out so an interior drag is
delivered to the widget (text selection) instead of moving/resizing the host
window. UiRoot.OnMouseDown honours it AFTER edge-resize (a resizable window
is still resizable from its frame) and BEFORE window-move.
- UiChatView: AcceptsFocus + IsEditControl + CapturesPointerDrag; caches the
OnDraw layout so OnEvent hit-tests the same geometry; HitChar maps a local
point to (line,col) with glyph-midpoint caret snapping; SelectedText joins a
multi-line span with \n; Ctrl-C writes to IKeyboard.ClipboardText (only when
non-empty, so an empty copy never clobbers the clipboard).
- UiHost exposes the wired IKeyboard (clipboard + Ctrl modifier state).
Adversarial-review fix (the 99 tests would have stayed green without it): a
coordinate-frame mismatch between MouseDown and MouseMove. UiRoot.OnMouseDown
dispatched HitTestTopDown's coords, which are relative to the TOP-LEVEL child,
while MouseMove/MouseUp use target.ScreenPosition. For the chat view inset at
(8,8) inside its window the anchor landed ~8px off the click. OnMouseDown now
delivers target-LOCAL coords like the other mouse events. Added a UiRoot
regression test asserting MouseDown and MouseMove share the target-local frame
for a nested child.
Decomp ref: SurfaceWindow text/selection model; clipboard via Silk.NET
IKeyboard.ClipboardText. Built with the chat-select-copy implement->review
workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The vitals cur/max overlay rendered with the consola TTF debug font,
which is wrong for the retail look. Port the retail dat-font render
path so the numbers use Font 0x40000000 (Latin-1, 16px, with outline
atlas) — the same font retail draws on the vitals window.
UiDatFont (new): loads the Font DBObj from the DatCollection and
uploads its two RenderSurface atlases (foreground glyph pixels
0x06005EE5 + background outline 0x06005EE6) through
TextureCache.GetOrUploadRenderSurface — the same direct-RenderSurface
path the D.2b chrome sprites use. Builds a char->FontCharDesc lookup
and exposes MeasureWidth + LineHeight. The per-glyph advance
(HorizontalOffsetBefore + Width + HorizontalOffsetAfter) is a pure
static so the pen math is unit-testable without GL or the dat.
UiRenderContext.DrawStringDat (new): two-pass per-glyph blit mirroring
SurfaceWindow::DrawCharacter (acclient 0x00442bd0) — the BACKGROUND
atlas sub-rect tinted black (outline) first, then the FOREGROUND
sub-rect tinted the text color (fill), with the pen accumulating the
retail advance the way the string loop does at 0x00467ed4. Respects
the UI transform stack. Skips the outline pass for fonts with no
background atlas.
No shader change was needed: the foreground atlas decodes A8 ->
(255,255,255,a), and ui_text.frag's RGBA-sprite path already
MULTIPLIES the texel by the per-vertex tint (texture(uTex,vUv)*vColor),
so tinting white+alpha by a color gives color+alpha (black outline,
text-color fill).
UiMeter: new DatFont property; the label renders via DrawStringDat
(centered with DatFont.MeasureWidth) when set, falling back to the
debug BitmapFont when null.
GameWindow: loads one UiDatFont for the vitals panel (under _datLock)
and assigns it to each UiMeter child; logs + falls back to the debug
font if the Font fails to load (never crashes).
Tests: 6 pure-logic UiDatFontTests for GlyphAdvance + MeasureWidth
(synthetic glyphs, negative bearings, missing chars, empty/null). Full
App UI suite green (84 passed).
DatReaderWriter member names verified via reflection on the 2.1.7
package: Font.{MaxCharHeight,BaselineOffset,ForegroundSurfaceDataId,
BackgroundSurfaceDataId,CharDescs} and FontCharDesc.{Unicode,OffsetX,
OffsetY,Width,Height,HorizontalOffsetBefore,HorizontalOffsetAfter,
VerticalOffsetBefore}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The vitals bars were rendered from the WRONG layout. The ids in vitals.xml
(0x0600113x) belong to LayoutDesc 0x21000014 -- the 800x28 floaty side-vitals
ROW. The stacked vitals window the user sees is LayoutDesc 0x2100006C
(160x58), which uses a different sprite set and geometry. Dumped the real
tree (new dump-vitals-layout CLI, reflective) and ported it:
- Sprites (#2): the stacked-window set 0x0600747E-0x0600748F (health/stamina/
mana, each back+front 3-slice; caps 10px, mid 130px).
- Right cap (#1) + fill model: retail UIElement_Meter::DrawChildren draws the
back 3-slice full then the front 3-slice CLIPPED to the fill fraction (its
own right-cap shows at 100%, the back's shows through when partial). UiMeter
now clips the front per-slice (UV-crop) instead of growing a capless slice.
- Spacing (#5): three flush 150x16 bars at y=5/21/37 in a 160x58 window
(16px pitch, zero gap), per the dat rects -- not the old 20px-apart guess.
- Border (#3): the window is the 8-piece chrome frame (corners 0x060074C3-C6,
edges 0x060074BF-C2, 5px) -- dat-confirmed identical to RetailChromeSprites.
The headless render-vitals-mockup now composites this exact window
(0x2100006C) from the real sprites with the same clipped-fill model, so the
look was verified before launch. Font (#4, dat Font 0x40000000) is the next
commit.
Decomp refs: gmVitalsUI::PostInit @0x4bfce0; UIElement_Meter::DrawChildren
@0x46fbd0 (scissor-fill); geometry from LayoutDesc 0x2100006C.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add UiChatView, a transcript widget for the retail-look UI: renders the
ChatVM tail bottom-pinned (newest at the bottom, like retail) with
mouse-wheel scrollback and whole-line vertical clipping so text stays
inside the frame. Hosted in a draggable/resizable UiNineSlicePanel and
wired into the UiHost next to the vitals window, fed by a dedicated
ChatVM (200-line tail) over the same live ChatLog. Per-ChatKind colour
palette (speech white, tells magenta, channels blue, system yellow,
emotes grey, combat orange).
This is the read-only foundation. The next sub-step adds glScissor
clipping + word-wrap, drag-to-select, and Ctrl+C copy -- the last needs
a CapturesPointerDrag opt-out on UiElement so an interior drag selects
text instead of moving the window (today an interior drag still moves
the window, same as the vitals panel).
Tests: UiChatView.ClampScroll (pin-to-bottom, cap-at-overflow,
never-negative).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render each vital bar as a horizontal 3-slice from the real retail
RenderSurface sprites (authoritative ids from the vitals LayoutDesc
0x21000014 via dump-vitals-bars): a fixed-width bevelled left-cap, a
stretched glassy-gradient middle, and a fixed-width right-cap. The
empty back track draws full width; the coloured front fill grows from
the left to the value (the track owns the right end, so the fill omits
its own right-cap). Replaces the flat single-sprite Alphablend overlay
that read as the old UI - this is the bordered gradient look from the
retail screenshot (red HP / gold stamina / blue mana).
UiMeter gains the six 9-slice ids (BackLeft/Tile/Right +
FrontLeft/Tile/Right) and a DrawHBar helper; MarkupDocument parses the
backleft/backtile/backright/frontleft/fronttile/frontright attrs;
vitals.xml carries the 18 per-vital ids. The temporary
ACDREAM_BAR_PROVEOUT component grid is removed.
Adds AcDream.Cli render-vitals-mockup: a headless ImageSharp composite
that assembles the bars with the SAME DrawHBar logic, so the sprite
assembly can be verified by eye (Read the PNG) without launching the
client + server - the fast UI-iteration loop the user asked for.
export-ui-sprite dumps a single RenderSurface to PNG for HTML mockups.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UiMeter gains SpriteResolve/BackSpriteId/FrontSpriteId; when both are
set, OnDraw draws the empty-track sprite full-width then the colored-fill
sprite UV-cropped to the live fill fraction (left-to-right drain). Falls
back to solid rects when sprite ids are absent, keeping existing behavior
and tests intact.
MarkupDocument.Build() parses `back`/`front` hex attrs on <meter> and
passes `resolve` into every UiMeter. vitals.xml wires the authoritative
LayoutDesc 0x21000014 sprites (Health 0x06005F3C/3D, Stamina 3E/3F,
Mana 40/41). The bar prove-out block in GameWindow.cs was already gone.
If the sprites decode as 1x1 magenta at runtime they are paletted
(INDEX16/P8) — the solid-color fallback will display instead and can be
investigated separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>