Commit graph

609 commits

Author SHA1 Message Date
Erik
b506f53633 refactor(D.5.4): rename ItemRepository->ClientObjectTable, ItemInstance->ClientObject
Broaden naming to the data side of every server object (retail weenie_object_table
shape). Pure rename; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:36:16 +02:00
Erik
fb288ad852 fix(D.5.2): effect tint = per-pixel tile copy (surface ReplaceColor overload)
Visual verification (Coldeve, Energy Crystal) showed acdream's Magical blue as a
flat tint vs retail's gradient. Root cause: RenderIcons calls the SURFACE overload
of SurfaceWindow::ReplaceColor (0x004415b0), which copies the textured effect tile
pixel-by-pixel into the icon's pure-white pixels — not the flat color->color overload
(0x00441530) I'd approximated with the tile's mean color. Port the surface overload
exactly (dst[x,y]=src[x,y] where dst==white); confirmed via clean Ghidra decompile +
named decomp. Retires AP-43 (mean-color approximation); IA-18 updated to the surface op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:21:33 +02:00
Erik
40c97a53ac fix(D.5.2): always run effect recolor (effects==0 -> black) to match retail
Visual verification caught it: a no-mana scroll's icon edges are BLACK in retail
but rendered WHITE in acdream. Cause = the effects!=0 gate (registered AP-44) that
skipped retail's effects==0 recolor. Retail's effect tile is non-null even for
effects==0 (the 0x21 SOLID-BLACK fallback 0x060011C5), so RenderIcons recolors
pure-white pixels to black on mundane items and to the effect hue on magical ones.
Remove the gate (always recolor); retire AP-44 (now faithful). TryGetEffectColor
made internal + a golden test pins effects==0 -> ~black.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 22:54:15 +02:00
Erik
702d6e1e90 test(D.5.2): lock effects-clears-to-zero contract (final-review polish)
The 'item with mana vs out of mana' core promise: a draining item whose
UiEffects clears to 0 returns to its base icon. Guards EnrichItem +
UpdateIntProperty unconditional-assign against a future != 0 regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:02:03 +02:00
Erik
73adc3768c docs(D.5.2): retire IA-16, add IA-18/AP-43..45, roadmap + memory
Divergence register:
- Retire IA-16 (item-icon composite PARTIAL — D.5.2 now complete).
- Add IA-18 (effect overlay = ReplaceColor tint SOURCE, faithful retail
  behavior; anti-regression guard — do NOT re-implement as a blit layer;
  cites IconData::RenderIcons 0x0058d180 + ReplaceColor 0x00441530).
- Add AP-43 (effect tint = mean-opaque color; exact retail byte
  decompiler-ambiguous, visual/cdb confirmation pending).
- Add AP-44 (effects==0 black-fallback recolor skipped; regression-risk
  avoidance, pending visual/cdb confirm).
- Add AP-45 (0x02CE sequence byte not honored, latest-wins).
Section header counts updated: IA 15→17, AP 41→44.

Roadmap: mark D.5.2 shipped (419c3ac..2f789da; appraise dropped as no-op;
effect recolor + live 0x02CE).

Tests: update ToolbarControllerTests iconIds lambda arity 4→5 to match the
D.5.2 GetIcon signature change (was caught by the build).

Memory: project_d2b_retail_ui.md updated with D.5.2 shipped entry
(via claude-memory symlink to ~/.claude/projects/.../memory/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:52:15 +02:00
Erik
e0dce5aa9f feat(D.5.2): IconComposer 2-stage effect composite + 5-arg GetIcon
Widen the cache key to (typeUnderlay, icon, underlay, overlay, effects).
GetIcon is now a 2-stage composite mirroring retail IconData::RenderIcons
(0x0058d180): Stage 1 builds the drag composite (base + overlay) and,
when effects != 0, ReplaceColorWhite tints it with the effect tile's
mean-opaque color (DR-1: tint SOURCE, not blit; DR-3: zero-effects
black path skipped). Stage 2 blits typeUnderlay + custom underlay +
drag into the final cached GL texture.

Both callers updated: ToolbarController Func arity widened to 6-arg
(passes item.Effects); GameWindow closure and OnLiveEntitySpawned
EnrichItem call pass spawn.UiEffects. Tree builds with 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:40:37 +02:00
Erik
3e019e408a feat(D.5.2): IconComposer effect-color + ReplaceColorWhite helpers
ReplaceColorWhite (retail SurfaceWindow::ReplaceColor 0x00441530):
replaces only pure-white-opaque (RGBA 255,255,255,255) pixels in place.
TryGetEffectColor: resolves the effect tile DID via ResolveEffectDid,
decodes the RenderSurface, and returns the mean-opaque RGB as the tint
color (divergence DR-2: exact retail color byte is decompiler-ambiguous).
TryDecode: shared RenderSurface decode helper for the effect path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:38:51 +02:00
Erik
75ac51ac23 feat(D.5.2): IconComposer.ResolveEffectDid (effect submap 0x10000005)
Add effect-overlay submap resolve: EnsureEffectSubMap walks the portal
MasterMap (0x25000000) → EnumIDMap 0x10000005 → submap 0x25000009;
ResolveEffectDid(effects) maps LowestSetBit(effects)+1 → RenderSurface
DID with fallback to index 0x21. Golden test validates all 6 cases
(Magical/Poisoned/BoostHealth/BoostStamina/Nether/zero) against the
live dat. Retail ref: IconData::RenderIcons 0x0058d180.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:37:40 +02:00
Erik
242bc9286d feat(D.5.2): PublicUpdatePropertyInt (0x02CE) parser
New standalone parser for the server's live PropertyInt update targeting
a VISIBLE object (carries guid). Wire layout: u32 opcode + u8 sequence +
u32 guid + u32 property + i32 value (17 bytes total).

The sequence byte is parsed-past but not honored (latest-wins; DR-4).
The companion PrivateUpdatePropertyInt (0x02CD) targets the player's own
object (no guid) and is not parsed here.

Three tests: uiEffectsUpdate (round-trip guid/prop/value), wrongOpcode
(returns null), truncated (returns null on 16-byte input).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:29:23 +02:00
Erik
8df0b64676 feat(D.5.2): capture UiEffects from CreateObject weenie header
Previously, weenieFlags bit 0x80 (UiEffects) was read + discarded with
`pos += 4`. Now it is captured into `uiEffects` and surfaced as
`Parsed.UiEffects` — the sole wire path for the effect bitfield since
PropertyInt.UiEffects (18) has no [AssessmentProperty] and never appears
in appraise responses.

Test builder gains `uint uiEffects = 0` param; write line updated to use
it. Three new parse tests: UiEffects_Captured, UiEffectsThenIconOverlay
(cursor-arithmetic regression), and NoUiEffectsBit_LeavesUiEffectsZero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:28:31 +02:00
Erik
5a2af61508 refactor(D.5.2): hoist UiEffectsPropertyId to fields + use it in tests (review polish)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:26:28 +02:00
Erik
77f64d7925 feat(D.5.2): ItemInstance.Effects + ItemRepository.UpdateIntProperty
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:23:20 +02:00
Erik
0e7a083da6 chore(D.5.1): remove temp geometry probe + add RestrictionDB-skip parse test
Task 1: remove the [D.5.1 PROBE] bottom-right rect-dump block from the
toolbar mount in GameWindow.cs. The block iterated 7 element ids and
logged ScreenPosition/Width/Height/Type; it was marked temporary and is
now superseded by the chrome window-frame fix. The kept [D.5.1] startup
diagnostic Console.WriteLines (digit arrays, toolbar ready, window from
LayoutDesc) are untouched.

Task 2: add TryParse_HouseRestrictionsSkipped_ThenIconOverlayCaptured to
CreateObjectTests.cs. Exercises the variable-length RestrictionDB skip
(weenieFlags bit 0x04000000: 12-byte fixed header + 4-byte hash-table
header + count*8 entries) followed immediately by IconOverlay (0x40000000)
and IconUnderlay (weenieFlags2 0x01 via IncludesSecondHeader 0x04000000).
Proves the skip lands the cursor at the right position for both capture
fields. 301/301 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:13:46 +02:00
Erik
8a42066192 feat(D.5.1): parse item IconOverlay/IconUnderlay from CreateObject -> faithful icon overlay layer
The CreateObject optional-tail walker previously stopped at UseRadius (~20 fields
before IconOverlay). This left ItemInstance.IconOverlayId/IconUnderlayId always 0,
so IconComposer's underlay/overlay layers were never drawn on toolbar icons.

Exact field order verified against ACE WorldObject_Networking.cs:87-219 (the
serializer is the authority; acdream connects to a local ACE server):
  UseRadius → TargetType(u32) → UiEffects(u32) → CombatUse(sbyte) →
  Structure(u16) → MaxStructure(u16) → StackSize(u16) → MaxStackSize(u16) →
  Container(u32) → Wielder(u32) → ValidLocations(u32) →
  CurrentlyWieldedLocation(u32) → Priority(u32) → RadarBlipColor(u8) →
  RadarBehavior(u8) → PScript(u16) → Workmanship(f32) → Burden(u16) →
  Spell(u16) → HouseOwner(u32) → HouseRestrictions(variable RestrictionDB) →
  HookItemTypes(u32) → Monarch(u32) → HookType(u16) →
  IconOverlay(PackedDwordKnownType) ← CAPTURE →
  IconUnderlay from weenieFlags2 bit 0x01 ← CAPTURE

RestrictionDB handled correctly: Version(u32) + OpenStatus(u32) + MonarchId(u32)
+ count(u16) + numBuckets(u16) + count×8 bytes entries. Length-aware skip, not a
fixed constant.

weenieFlags2 is now CAPTURED (not skipped) when IncludesSecondHeader
(objDescFlags bit 0x04000000) is set, so the IconUnderlay bit can be tested.

The entire extended walk is inside try/catch: truncated packets degrade to
IconOverlayId=0 / IconUnderlayId=0 (no overlay drawn), never corrupting.

Threading: CreateObject.Parsed → WorldSession.EntitySpawn → GameWindow
OnLiveEntitySpawned → Items.EnrichItem — both ids thread through all three
seams. EnrichItem extended with optional iconOverlayId + iconUnderlayId params
(defaulted 0, backward-compatible).

No change to IconComposer or ToolbarController (they already consume the ids).

Tests: 4 new CreateObject tests (IconOverlay only, overlay+underlay, no-overlay
regression, intermediate-fields cursor arithmetic). Full suite: 0 failures,
2636 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:34:47 +02:00
Erik
a7cad5566b fix(D.5.1): occupancy-gated slot numbers (empty=0x1000005e bg digit) + bottom-right rect probe
FIX 1: UIElement_UIItem::SetShortcutNum (decomp 229481) has a three-way source
branch: occupied+peace -> 0x10000042 (peace digit set), occupied+war -> 0x10000043
(war digit set), empty (ItemId==0) -> 0x1000005e (background digit, stance-independent).
acdream previously only had the peace/war pair and drew them regardless of occupancy.

Changes:
- GameWindow.cs: read property 0x1000005e into toolbarEmptyDigits (no fallback;
  null is safe). Logs entry count. Passes emptyDigits to Bind. Adds [D.5.1 probe]
  block logging screen pos + size of 7 bottom-right element ids via ScreenPosition.
- ToolbarController.cs: adds _emptyDigits field, emptyDigits ctor+Bind param (null
  default). RestampShortcutNumbers sets cell.EmptyDigits. Comments cite decomp 229481.
- UiItemSlot.cs: adds EmptyDigits property + ActiveDigitArray() internal testable seam
  (occupied -> peace/war by stance; empty -> EmptyDigits). OnDraw uses it. Comment
  updated with three-way source table.
- Tests: 5 new UiItemSlotTests (ActiveDigitArray occupancy), 2 new
  ToolbarControllerTests (emptyDigits injection + null-safe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:27:27 +02:00
Erik
b2a812d1fa feat(D.5.1): faithful toolbar slot numbers 1-9 (SetShortcutNum digit sprites, peace/war)
Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465) and
gmToolbarUI::RecvNotice_SetCombatMode (196610-196621). The 9 top-row toolbar slots
show digit labels 1-9 at all times (even when empty — confirmed from the user''s
retail screenshot). The digit sprites are real 32x32 PFID_A8R8G8B8 RenderSurfaces
with glyphs baked into the top-left corner (rest alpha=0), drawn Alphablend over
the slot icon/empty sprite.

Digit DID arrays (peace: property 0x10000042, war: 0x10000043) are read at startup
from LayoutDesc 0x21000037 element 0x1000034A under composite 0x10000346 using the
same ArrayBaseProperty{DataIdBaseProperty} pattern as LayoutImporter.ReadState.
A cited-constant fallback (same confirmed dat ids) is used if the dat navigation
fails. The war glyph set (darker/golden glyphs) switches on any combat stance;
peace glyphs (lighter) restore on NonCombat — re-stamped by RestampShortcutNumbers()
called from both Populate() and SetCombatMode().

Changes:
- UiItemSlot: ShortcutNum/ShortcutPeace/PeaceDigits/WarDigits state; SetShortcutNum/
  ClearShortcutNum; OnDraw restructured (no early return) so digit draws after icon.
- ToolbarController: _peaceDigits/_warDigits/_peace fields; Bind() gains peaceDigits/
  warDigits optional params; RestampShortcutNumbers() helper; Populate() and
  SetCombatMode() both call RestampShortcutNumbers().
- GameWindow: reads digit arrays under _datLock from LayoutDesc 0x21000037, passes to
  Bind(); cited constants as fallback.
- Tests: 5 new UiItemSlotTests (SetShortcutNum/ClearShortcutNum state); 4 new
  ToolbarControllerTests (top-row/bottom-row labels, peace/war switch, array injection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:52:50 +02:00
Erik
f21dbfad80 feat(D.5.1): faithful item-icon type-default underlay (EnumIDMap 0x10000004) — opaque icon backing
Retail IconData::RenderIcons (decomp 407524) builds the icon layer stack bottom→top:
type-default underlay (OPAQUE, Blit_Normal) first, then custom underlay, base icon,
custom overlay.  acdream's IconComposer omitted the type-default underlay, leaving
filled toolbar slots with a transparent background.

Resolution via the two-level EnumIDMap chain that retail uses (DBCache::GetDIDFromEnum
0x413940): Portal.Header.MasterMapId (0x25000000) → master[0x10000004] → submap DID
(0x25000008) → submap[LSB(itemType)+1] → 0x06 RenderSurface underlay DID.  Golden
values confirmed against the live dats: MeleeWeapon→0x060011CB, Armor→0x060011CF,
Clothing→0x060011F3, Jewelry→0x060011D5, None(fallback 0x21)→0x060011D4.

Changes:
- IconComposer: add ResolveUnderlayDid(ItemType)/EnsureUnderlaySubMap (memoised);
  widen cache key from (uint,uint,uint)→(uint,uint,uint,uint); GetIcon gains ItemType
  param and prepends the opaque underlay as layer 0 (Compose sizes to it → fully opaque)
- ToolbarController: widen _iconIds Func from 3-arg to 4-arg; Populate passes item.Type
- GameWindow: update toolbar mount lambda to 4-arg form
- Tests: update ToolbarController test stubs to (_,_,_,_); add
  Compose_opaqueUnderlayFirst_resultIsFullyOpaque (dat-free) and
  ResolveUnderlayDid_goldenValues_matchDat (dat-gated, skip when dats absent)

No divergence-register row existed for this omission; none added (fully ported now).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:37:53 +02:00
Erik
bfc452d610 fix(D.5.1): toolbar movable + chrome-grab + peace-only indicator + no prototype square
D1 — Toolbar not movable: toolbarRoot.Anchors = AnchorEdges.None (was Left|Top)
so ApplyAnchor early-returns and doesn't re-pin the window every frame.
Matches the vitalsRoot idiom exactly.

D2 — Cannot grab toolbar by chrome: toolbarRoot.ClickThrough = false
so HitTest succeeds over the UiDatElement chrome and the drag starts.
UiDatElement ctor defaults ClickThrough=true; vitalsRoot already overrides it.

C1 — All four combat-mode indicators visible at once (war/flame stacked on
peace): ports gmToolbarUI::RecvNotice_SetCombatMode
(acclient_2013_pseudo_c.txt:196632-196669). CombatIndicatorIds[] maps
index 0-3 to NonCombat/Melee/Missile/Magic; SetCombatMode shows exactly one
and hides the other three. Default to NonCombat at bind (player always
spawns in peace). Wires CombatState.CombatModeChanged for live updates.
Tests: CombatIndicator_defaultNonCombat_onlyPeaceVisible,
CombatIndicator_setCombatModeMelee_onlyMeleeVisible,
CombatIndicator_liveSignal_updatesWhenCombatStateChanges.

V1 — Blue empty-slot square at top-left (prototype 0x100001B2 materialized):
ImportInfos now skips top-level elements that are (a) referenced as a
BaseElement by another element in the same layout AND (b) have no own state
media. The CollectBaseRefsInDesc walk covers nested children; HasNoOwnMedia
re-uses ToInfo's media extraction. The Resolve path reads BaseElement from the
raw dat via dats.Get<LayoutDesc> — it never depends on the prototype being in
the built widget tree — so the skip is safe. Conformance tests (vitals, chat)
are unaffected (they exercise Build, not ImportInfos).
Test: BuildFromInfos_PrototypeSkipped_DerivedPresent_PrototypeAbsent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:03:07 +02:00
Erik
383a969c70 feat(D.5.1): ToolbarController — bind 18 slots, populate, deferred rebind, click-to-use
Port of gmToolbarUI::PostInit (slot wiring) + UpdateFromPlayerDesc (flush-and-bind
shortcuts from PlayerDescription) + SetDelayedShortcutNum (deferred ItemAdded rebind)
+ UseShortcut (click → useItem callback).

UiItemSlot gains Clicked (Action?) + OnEvent override (MouseDown → Clicked?.Invoke())
matching the retail UIElement_UIItem click dispatch pattern. UiEvent is a positional
record struct so the OnEvent override reads e.Type (int) against UiEventType.MouseDown
(const int 0x201) — confirmed from UiEvent.cs + UiText.cs before writing.

Three tests green (populate bound slot, deferred rebind on ItemAdded, click fires useItem).
Full suite: 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:36:48 +02:00
Erik
9c8db0d577 feat(D.5.1): UiItemList widget + factory branch for class 0x10000031
Ports retail UIElement_ItemList (class 0x10000031) as a behavioral-leaf
container that owns its UiItemSlot children procedurally. Single-cell
default covers every toolbar slot; N-cell grid is deferred to the inventory
phase. OnDraw syncs the cell rect to the list's Width/Height each frame so
the cell is sized and hit-testable from the first rendered frame, even
though the factory sets rect AFTER construction.

Factory: adds `0x10000031u => new UiItemList(resolve)` arm before the
fallback, so all 18 toolbar itemlist slots route to UiItemList instead of
UiDatElement.

Tests: 4 new (IsLeafWidget, StartsWithOneCell, Cell_returnsFirstSlot,
Create_buildsUiItemList_forItemListClassId). All 4 pass; full suite green
(415 pass / 2 skip in App.Tests; 0 fail total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:28:37 +02:00
Erik
28d5837309 test(D.5.1): cover UiItemSlot.Clear (review — hot path in ToolbarController) 2026-06-16 22:25:59 +02:00
Erik
1270596f30 feat(D.5.1): UiItemSlot widget (UIElement_UIItem cell port)
Behavioral leaf widget for the toolbar item cell. Draws the empty-slot
sprite (0x060074CF) when unbound; draws the pre-composited icon texture
when a weenie is bound via SetItem(). ConsumesDatChildren=true prevents
the LayoutImporter from double-building the dat sub-elements. SpriteResolve
is configurable so paperdoll equip slots can swap in per-slot silhouettes
later. No Clicked/OnEvent — that wiring comes in Task 8 (ToolbarController).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:21:21 +02:00
Erik
6e82807863 feat(D.5.1): IconComposer — CPU alpha-over icon composite + cache
Adds IconComposer (AcDream.App.UI) which mirrors retail IconData::RenderIcons
(decomp 407524): decodes each RenderSurface layer directly via SurfaceDecoder,
composites them bottom-to-top with Porter-Duff alpha-over, and uploads the
result to a GL texture via TextureCache. Composited handles are keyed by the
(iconId, underlayId, overlayId) tuple so each unique combo is uploaded once.

Adds a public TextureCache.UploadRgba8(byte[], int, int, bool) wrapper — a
thin shell around the existing private overload — so IconComposer can upload
its CPU-side composite without duplicating any GL state logic.

Pure Compose() path is covered by 2 unit tests (opaque top wins; transparent
top preserves bottom). Dat-decode + GL-upload exercised by the visual gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:09:41 +02:00
Erik
6c485c2f06 feat(D.5.1): persist PlayerDescription shortcuts (were parsed then discarded)
Add optional `onShortcuts` callback to `GameEventWiring.WireAll`; invoke
it with `parsed.Shortcuts` after the inventory/equipped loops in the
PlayerDescription handler.  `GameWindow` holds the list in a new
`Shortcuts` property (initialized to empty) so the toolbar (D.5.1 Task 5)
can read hotbar slots without keeping a parser reference.  Existing callers
compile unchanged — the parameter defaults to null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:01:40 +02:00
Erik
f8da98b67f feat(D.5.1): ItemRepository.EnrichItem (icon/name/type from CreateObject)
Adds EnrichItem(objectId, iconId, name, type) — enriches an existing
stub created from PlayerDescription with the fuller data carried by its
CreateObject message. Returns false when the item isn't tracked yet
(phase 1: enrich-existing only). Raises ItemPropertiesUpdated on success
so bound widgets (the toolbar) re-render.

Two xUnit tests: enrich-existing updates IconId/Name/raises event (true),
unknown-id returns false.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:48:44 +02:00
Erik
da171cb4e3 feat(D.5.1): capture IconId in CreateObject.Parsed (was discarded at cs:516)
ReadPackedDwordOfKnownType at the old line 516 was throwing the icon dat
id away. Declare iconId before the try-block, assign it there, and pass
IconId: iconId in the Parsed initializer so downstream UI (action bar /
equipment panels) can read the 0x06xxxxxx dat id without a separate lookup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:44:01 +02:00
Erik
89626cd400 feat(D.2b): vitals numbers as UiText (widget-generalization Task 8)
The vitals cur/max numbers now render through the generic UiText widget — retail
gmVitalsUI uses UIElement_Text for them, not a meter-internal label. VitalsController
attaches a centered, non-interactive UiText child to each meter and stops the meter
drawing its own label (UiMeter.Label -> null). New UiText.Centered draws the first line
centered H+V with the SAME formula UiMeter's overlay used, so the numbers are
pixel-identical — user-confirmed in the live client.

This completes the D.2b widget-generalization pass: every chat + vitals widget is now
built generically and registered to its retail Type (Button/Field*/Menu/Meter/Scrollbar/
Text), with thin find-by-id controllers. (*Field is controller-placed; Type 3 stays
UiDatElement for chrome.)

Divergence register: AP-37 vitals-numbers-via-UiMeter.Label clause retired. Full suite:
404 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:52:42 +02:00
Erik
ee2e0fafa0 fix(D.2b): do NOT register Type 3 -> UiField (review fix for Task 6)
Task 6 registered Type 3 -> UiField globally, which broke acdream's Type-3 dat
elements: in these layouts Type 3 is sprite-bearing CHROME (the 8-piece bevel
corners, e.g. vitals 0x10000633 -> sprite 0x060074C3) and the transcript/input
CONTAINER panels — NOT editable fields. UiField draws no dat sprite, so the
vitals bevel corners would render empty; the regression was masked by weakening
VitalsTree_ChromeCornerHasExpectedSprite (UiDatElement+sprite -> UiField+exists).

Retail Type 3 IS UIElement_Field, but retail draws those chrome elements as inert
media-bearing Fields, which our UiDatElement reproduces pixel-for-pixel without a
spurious focus/edit affordance. The one true editable field — the chat input
0x10000016 — resolves to Type 12 and is controller-placed as a UiField (Variant B,
kept). So Type 3 stays on the generic fallback; register it as UiField only when a
window carries a factory-built editable Type-3 field (and UiField grows a
background-media draw + an opt-in editable flag then).

Restored the chrome-corner conformance test (asserts UiDatElement + sprite, an
early warning if Type 3 is ever wrongly routed to UiField). Kept the good Task-6
work: UiField rename + the Variant-B input wiring (stray Type-12 placeholder
removed). Full suite: 404 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:53:56 +02:00
Erik
e059a3f6ef feat(D.2b): UiField (Type 3) — editable input as a generic field; remove the stray Type-12 input placeholder (widget-generalization Task 6)
- Rename UiChatInput → UiField (UIElement_Field, RegisterElementClass(3) @ :126190);
  update doc to cite retail's CatchDroppedItem/MouseOverTop drag-drop hooks for
  future item windows. BackgroundColor default → transparent (controller sets
  the translucent 0.35α value explicitly, matching UiText pattern).
- Register Type 3 in DatWidgetFactory.Create: `3 => new UiField()`.
- ChatWindowController.Bind (Variant B): factory now builds 0x10000016 as an
  invisible UiText placeholder (Type 12); Bind removes that placeholder via
  FindElement(InputId).Parent.RemoveChild and places a UiField at the same rect.
  Result: exactly ONE input widget in the input bar, no stray UiText duplicate.
- Input property type changed from UiChatInput to UiField; GameWindow.cs:1861
  UiField.Keyboard assignment compiles unchanged (field exists).
- Tests: UiChatInputTests → UiFieldTests (class + all ctor refs renamed);
  DatWidgetFactoryTests: new Type3_Field_MakesUiField test; ChatWindowControllerTests:
  updated stale "skipped by factory" comments; LayoutConformanceTests: updated
  VitalsTree_ChromeCornerHasExpectedSprite — Type-3 chrome-corner elements are
  now UiField (sprite rendering for Type-3 dat image elements is a known
  limitation, tracked for post-Task-8 UiField.BackgroundSprite follow-up).
- Full suite: 404 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:48:51 +02:00
Erik
cb082b59e4 feat(D.2b): UiText (Type 12) -- generic text + Type-12 flip; transcript factory-built (widget-generalization Task 5)
Rename UiChatView -> UiText (the retail UIElement_Text class,
RegisterElementClass(0xc) @ acclient_2013_pseudo_c.txt:115655).

Factory changes (DatWidgetFactory.cs):
- Remove the Type-12 skip (was: no-media -> null, with-media -> UiDatElement).
- Add Type 12 -> BuildText() -> UiText in the switch.
- BuildText extracts the element's Direct/Normal sprite as BackgroundSprite
  so any dat-media the element carried keeps rendering under the text.

UiText changes (renamed from UiChatView.cs):
- BackgroundColor default: (0,0,0,0.35) -> (0,0,0,0) (transparent).
  An unbound UiText draws nothing; the controller opts in to the translucent bg.
- New BackgroundSprite + SpriteResolve: optional dat state-sprite background
  drawn UNDER DrawFill+text (faithful UIElement_Text media support).

ChatWindowController.cs (Task 5 Step 8):
- Transcript property: UiChatView -> UiText.
- Bind() now uses layout.FindElement(TranscriptId) as UiText (factory-built)
  instead of manually constructing + AddChild-ing a new UiChatView.
- Sets BackgroundColor = (0,0,0,0.35) on the found widget (retail translucent bg).
- Removes the tInfo null-check from the early guard (transcript is factory-built;
  iInfo lookup kept for the input widget which is still manually constructed).
- BuildLines: UiChatView.Line -> UiText.Line throughout.

Vitals frozen: the Type-12 vitals number elements are meter children and are
never recursed by BuildWidget (the `if (w is not UiMeter)` gate), so they are
not built as widgets and keep rendering via UiMeter.Label. Vitals fixture
vitals_2100006C.json unchanged; LayoutConformanceTests + VitalsBindingTests green.

Tests:
- UiChatViewTests.cs -> UiTextTests.cs (class: UiTextTests, all UiChatView.* -> UiText.*)
- UiChatViewDatFontTests.cs -> UiTextDatFontTests.cs (same)
- DatWidgetFactoryTests: delete Type12_StylePrototype_ReturnsNull +
  DatWidgetFactory_Type12WithMedia_Renders; add Type12_Text_MakesUiText +
  DatWidgetFactory_Type12_AlwaysMakesUiText.
- LayoutImporterTests: BuildFromInfos_Type12Child_IsSkipped_Type3Present updated
  to assert IsType<UiText> (element is now in tree, transparent, not skipped).

Divergence register: AP-37 amended -- removed the "standalone Type-0 text
elements skipped / dat-text widget is Plan 2" clause (now shipped as UiText);
kept the meter-collapse clause and the vitals-numbers-via-UiMeter.Label clause.
AP-38/AP-39/AD-28 file references updated UiChatView.cs -> UiText.cs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:39:02 +02:00
Erik
67e5b8cff2 fix(D.2b): UiMenu — controller owns Selected (review fix for Task 4)
Review caught a behavior divergence: the generic UiMenu auto-set its own
Selected on any enabled pick, while the controller's EnabledProvider keeps the
null-payload specials (Squelch / Tell-to-Selected) enabled/white like retail.
So a special-item click set Selected=null and shifted the highlight onto the
deferred placeholders — and the menu tests masked it by using a different
(specials-disabled) gate than the controller ships.

Fix: clean MVC contract mirroring retail UIElement_Menu::NewSelection — the
widget REPORTS the pick via OnSelect; the controller OWNS Selected (it sets it
only for talk-channel payloads). A special-item click now fires OnSelect(null),
the controller ignores it, and the active channel + highlight stay put —
observably identical to the pre-generalization widget, and extensible for when
Squelch lands. Tests realigned to the controller's gate (specials white) and to
the controller-owns-Selected contract.

Full suite: 403 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:27:30 +02:00
Erik
955f7a69a8 feat(D.2b): UiMenu (Type 6) — generic dropdown; channel knowledge moves to controller (widget-generalization Task 4)
UiChannelMenu → UiMenu: removed ChatChannelKind, the 14-item array, the
button-text map, and the availability default. Generic surface: MenuItem
(label + object? Payload), Selected (object?), OnSelect, EnabledProvider,
ButtonLabelProvider, RowsPerColumn/RowHeight/ColumnWidth (all settable).
All draw/event mechanics unchanged — same popup geometry, same click
coordinates, same 8-piece bevel, same 3-slice button face.

ChatWindowController gains ChannelItems[], ChannelButtonLabel(), and
ChannelAvailable() (verbatim from old widget), and populates the
factory-built Type-6 UiMenu via find-by-id rather than constructing a
replacement widget. The Menu property type is now UiMenu. OnChannelChanged
wrap replaced with the generic OnSelect wrap for the ReflowInputRow hook.

DatWidgetFactory registers Type 6 → new UiMenu().

Tests: UiChannelMenuTests → UiMenuTests (10 tests, all green); factory
Type6 test added; ChatWindowControllerTests updated to use OnSelect.
Divergence register: AP-42 added (flat item model vs retail nested-submenu
MakePopup @0x46d310 — latent, unreachable through the chat menu).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:18:27 +02:00
Erik
805ab5f40b feat(D.2b): UiButton (Type 1) — Send + Max/Min as generic buttons (widget-generalization Task 3)
Introduces UiButton: a dedicated dat-widget button that ports UIElement_Button
(RegisterElementClass(1,...) @ acclient_2013_pseudo_c.txt:125828). State selection,
tiled DrawSprite, and label rendering mirror UiDatElement exactly so the chat Send
and Max/Min buttons have zero behavioral change.

DatWidgetFactory now maps Type 1 → UiButton (beside Type 7 → UiMeter, Type 11 →
UiScrollbar). ChatWindowController's Send and Max/Min bind blocks updated from
UiDatElement casts to UiButton casts; ClickThrough=false lines dropped (UiButton
is interactive by construction).

The old UiPanel.cs UiButton (a plain dev-scaffold rect+text button with no dat
sprites) is renamed UiSimpleButton to free the name — no production code
instantiated it.

Full suite: 402 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:07:58 +02:00
Erik
3593d6623d feat(D.2b): UiScrollbar (Type 11) — promote the generic chat scrollbar (widget-generalization Task 2)
- git mv UiChatScrollbar.cs → UiScrollbar.cs; rename class + update doc summary to
  "Generic scrollbar. Ports retail UIElement_Scrollbar (RegisterElementClass(0xb) @
  acclient_2013_pseudo_c.txt:124137); thumb size = trackLen * ThumbRatio (min 8px); step ±1 line."
- git mv UiChatScrollbarTests.cs → UiScrollbarTests.cs; rename test class + replace
  every UiChatScrollbar reference with UiScrollbar (bodies unchanged).
- DatWidgetFactory: register Type 11 → new UiScrollbar() before the _ fallback case.
- ChatWindowController: change Scrollbar property type to UiScrollbar; replace the old
  "construct-remove-add" block with a "find factory-built UiScrollbar and bind in place"
  block (no RemoveChild/AddChild); keep `var track` assignment in scope so the Max/Min
  block's track.Left/track.Width reads still compile against UiElement?.
- AP-41 divergence register: update file:line to UiScrollbar.cs:35; narrow wording to
  "fallback only — single-tile drawn only when cap ids are unset; the chat controller
  passes all three cap ids so the 3-slice path is the active code path."
- Update inline UiChatScrollbar doc-comment references in UiScrollable.cs + UiChatView.cs.
- Full suite: 399 passed, 2 skipped (dat/tower fixture skips), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:02:49 +02:00
Erik
d1b13a7dbf test(D.2b): chat golden fixture + resolved-Type conformance (widget-generalization Task 1)
- Add ChatLayoutFixtureGenerator.cs (Skip-by-default) to regenerate
  chat_21000006.json from the live portal.dat via LayoutImporter.ImportInfos
- Commit generated fixture chat_21000006.json (13 KB, 400 lines) — dat-free,
  auto-copied to test output via existing *.json csproj glob
- Refactor FixtureLoader: extract shared LoadInfos(fileName) helper; add
  LoadChat() + LoadChatInfos() mirroring the vitals pattern; LoadVitalsInfos()
  now delegates to the shared loader (behavior unchanged, vitals tests green)
- Add ChatLayoutConformanceTests: ResolvesKnownElements + ResolvedTypes_MatchRetailRegistry

Confirmed resolved Types from live dat:
  0x10000011 (transcript) → Type 12 (style-prototype, skipped by factory)
  0x10000016 (input)      → Type 12 (style-prototype, skipped by factory)
  0x10000014 (menu)       → Type 6
  0x10000012 (scrollbar)  → Type 11
  0x10000019 (send)       → Type 1
  0x1000046F (max/min)    → Type 1

Also fix pre-existing build break: UiChatInput.MoveCaret(int delta) was made
private in ce848c1 but UiChatInputTests.Backspace_DeletesBeforeCaret called it
as public. Expose a public MoveCaret(int) overload (no-shift) alongside the
private MoveCaret(int,bool) — restores the intended test surface.

Full suite: 398 passed, 2 skipped (generator + pre-existing), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:55:51 +02:00
Erik
2284a376ae feat(D.2b): write-mode movement gate that preserves autorun
In chat write mode the keyboard belongs to the input — typing "swd" must not
walk the character — but AUTORUN must keep going (the user can chat while
running).

- InputDispatcher.IsActionHeld now returns false while WantCaptureKeyboard is
  set (a focused chat input), the polling-path twin of the existing gate on
  Fired actions. This SUPERSEDES the old per-frame OnUpdate early-return, which
  also killed autorun. Gating here instead lets the movement block keep running,
  so autorun — a separate latched bool ORed into Forward at the call site, not a
  polled key — survives. Test updated to encode the new contract.
- GameWindow: the movement suppress-guard reverts to ImGui-devtools-only (the
  retail write mode no longer early-returns); wires DefaultTextInput = the chat
  input (Tab/Enter activation) and Input.Keyboard for clipboard. Drops the
  one-shot UI-scale diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:24:19 +02:00
Erik
bb983ae850 @
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>
@
2026-06-16 11:33:38 +02:00
Erik
1da697ec2a @
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>
@
2026-06-16 09:37:40 +02:00
Erik
0ec36f6197 fix(D.2b): chat input resolves the live command bus lazily (was bound to null) + register thumb-3-slice row
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>
2026-06-15 23:24:44 +02:00
Erik
9d9e036e4c feat(D.2b): ChatWindowController — bind chat LayoutDesc, place widgets, route chat
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>
2026-06-15 23:04:57 +02:00
Erik
6e6339b026 feat(D.2b): importer renders Type-12-with-sprites + carries DefaultState
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>
2026-06-15 22:54:37 +02:00
Erik
c2170ab18f feat(D.2b): UiChannelMenu — channel selector popup (UIElement_Menu port)
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>
2026-06-15 22:42:22 +02:00
Erik
bcc45d668e feat(D.2b): UiChatInput — editable field, caret, 100-entry history (UIElement_Text port)
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>
2026-06-15 22:36:44 +02:00
Erik
2940b4e3b2 feat(D.2b): UiChatScrollbar — track/thumb/buttons driving UiScrollable
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>
2026-06-15 22:31:12 +02:00
Erik
9f273c9343 feat(D.2b): UiScrollable — pixel scroll model (UIElement_Scrollable port)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:19:29 +02:00
Erik
7552dcba39 feat(D.2b): UiChatView dat-font transcript + 1-line wheel quantum
- 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>
2026-06-15 22:14:56 +02:00
Erik
50883e445b feat(D.2b): extract ChatCommandRouter — shared chat submit pipeline
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>
2026-06-15 22:09:27 +02:00
Erik
8aa643f3e0 fix(D.2b): correct edge-anchor mapping (RightEdge==1=stretch) + enable vitals horizontal resize
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>
2026-06-15 17:05:04 +02:00
Erik
bf77a23ad3 feat(D.2b): flip vitals to the LayoutDesc importer; retire hand-authored vitals.xml
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>
2026-06-15 16:30:24 +02:00
Erik
5ac9d8c19c merge: bring main into claude/hopeful-maxwell-214a12 (LayoutDesc importer branch)
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>
2026-06-15 16:19:15 +02:00