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>
Adds `AcDream.Cli dump-vitals-bars <datDir>` subcommand that:
- Scans all 101 LayoutDesc objects in client_local_English.dat
- Finds the vitals window layout (0x21000014) by locating the Health
meter element id 0x100000E6 (from gmVitalsUI::PostInit decomp)
- Walks each meter's sub-element tree (typed access via ElementDesc.Children,
ElementDesc.States, ElementDesc.StateDesc, StateDesc.Media, MediaDescImage.File)
- Prints every RenderSurface DataId (0x06xxxxxx) per vital
Authoritative output:
HEALTH (0x100000E6): front-bar fill 0x06005F3D / track fill 0x06005F3C
E8/E9/EA pieces: 0x06001131/32/33, 0x06001141/40/3F
STAMINA (0x100000EC): front-bar fill 0x06005F3F / track fill 0x06005F3E
E8/E9/EA pieces: 0x06001137/38/39, 0x06001147/46/45
MANA (0x100000EE): front-bar fill 0x06005F41 / track fill 0x06005F40
E8/E9/EA pieces: 0x06001134/35/36, 0x06001144/43/42
LayoutDesc shape discovered: Fields Width, Height, Elements (HashTable<uint,ElementDesc>).
ElementDesc shape: ElementId, Type, BaseElement, BaseLayoutId, DefaultState,
X/Y/Width/Height/ZLevel, LeftEdge/TopEdge/RightEdge/BottomEdge,
States (Dictionary<UIStateId,StateDesc>), Children (Dictionary<uint,ElementDesc>),
StateDesc (direct single state).
StateDesc shape: StateId, PassToChildren, IncorporationFlags,
Properties (Dictionary<uint,BaseProperty>), Media (List<MediaDesc>).
MediaDescImage shape: File (uint DataId), DrawMode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The anchor pass added in f911b5f runs on every element's children — including
UiRoot's children, which are the top-level WINDOWS. With the default Left|Top
anchor, ApplyAnchor reset each window's Left/Top/Width/Height back to its
captured design rect EVERY frame, so user move/resize was undone instantly ("I
can't resize or move it"). A window is user-positioned, so it must not be
anchor-managed by its parent: set UiNineSlicePanel.Anchors = None. Children
INSIDE the window still anchor to it (the bars keep stretching with width).
Regression tests: UiNineSlicePanel.Anchors == None; ApplyAnchor(None) is a no-op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add AnchorEdges [Flags] enum and Anchors property (default Left|Top, so
all existing elements are unchanged) to UiElement. ApplyAnchor() captures
the design-time margins on first call then recomputes Left/Top/Width/Height
each frame; DrawSelfAndChildren drives it for every child before painting.
ComputeAnchoredRect is public + static so it can be unit-tested without a
running frame loop.
MarkupDocument.Build gains a private Anchor() CSV parser and threads it
into the <meter> initializer via the anchor= attribute.
vitals.xml: remove title="Vitals" (retail vitals has no heading) and add
anchor="left,top,right" to all three meter bars so they stretch when the
panel is dragged wider.
Two new xUnit tests in UiRootInputTests: Left+Right stretches width;
Left+Top only keeps fixed size. All 19 App.Tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ResizeX/ResizeY bool properties to UiElement (both true by default).
HitEdges() in UiRoot masks out locked axes after edge detection, so a
locked edge falls through to window-move behaviour — matching retail,
where the vitals bar height is fixed and only widens.
MarkupDocument.Build() parses an optional resize="x|y|both|none"
attribute on <panel>; vitals.xml gets resize="x" to enforce the
horizontal-only constraint in all instances of the panel.
Two new tests: HitEdges_RespectsResizeAxisLock (UiRootInputTests) and
Build_ResizeAttrX_SetsHorizontalOnly (MarkupDocumentTests). 11/11 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TextRenderer.Flush batched by primitive type and flushed rects -> text ->
sprites LAST, so the 8-piece chrome (incl. the center fill) painted OVER the
vital bars + numbers ("the window is drawn in front of the bars"). Reorder to
sprites -> rects -> text so chrome composites behind widget fills + text.
Correct while bars are solid rects; when bars become gradient SPRITES this must
move to true submission/painter order (sprite-on-sprite z) — noted inline as the
D.2b follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add parallel resize mode to the UiRoot retained-mode input state machine.
A left-drag starting within ResizeGrip=5px of a Resizable window's edge or
corner resizes it (min-size clamped); interior drags on a Draggable window
still reposition it.
Changes:
- UiElement: Resizable, MinWidth, MinHeight properties
- UiRoot: ResizeEdges flags enum; _resizeTarget state fields; FindWindow
(replaces FindDraggable, matches Draggable||Resizable); HitEdges (static,
internal, testable); ResizeRect (static, public, testable); OnMouseDown
checks edge-grip before move; OnMouseMove resize branch precedes move;
OnMouseUp clears _resizeTarget
- UiNineSlicePanel: Resizable = true (retail windows are resizable)
- UiRootInputTests: 4 new tests — ResizeRect_RightBottom, ResizeRect_LeftTop
(min-clamp + origin shift), HitEdges_DetectsCornerAndInteriorNone,
EdgeDrag_ResizesPanel_InteriorDragMoves (full integration path)
Note on test coordinate: right-edge grab uses x=298 (2px inside the panel's
hit-test boundary) rather than x=300 (exactly at edge, misses OnHitTest's
strict `<` check). This is intentional — the grip zone extends inward from
the edge boundary, so a click 2px inside correctly lands in both the
hit-test rect AND the resize-grip zone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- UiElement: add Draggable flag; left-drag on a draggable element repositions
it as a floating window instead of starting a drag-drop sequence.
- UiRoot: add WantsMouse/WantsKeyboard properties (mirrors ImGui's WantCaptureMouse
pattern); add FindDraggable helper; inject _windowDragTarget state machine into
OnMouseDown/OnMouseMove/OnMouseUp so draggable windows track the pointer offset.
- UiNineSlicePanel: set Draggable=true so retail window frames are movable by default.
- GameWindow: OR _uiHost?.Root.WantsMouse|WantsKeyboard into the SilkMouseSource
wantCaptureMouse/wantCaptureKeyboard delegates and the direct MouseMove gate so
game actions (movement, world-pick) are suppressed while the pointer is over a
retail window — no double-handling with the InputDispatcher.
- GameWindow: wire all Silk Mice/Keyboards to UiHost after construction so the
UiRoot tree receives live input.
- Tests: 3 new UiRootInputTests covering WantsMouse hit-test, window-drag
reposition, and non-draggable panel immobility.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the plugin-facing UI registration surface (Task 9, final D.2b task).
Plugins call host.Ui.AddMarkupPanel(path, binding) from Enable(); calls are
buffered in BufferedUiRegistry before the GL window opens, then drained into
UiHost.Root in GameWindow.OnLoad inside the RetailUi block after the first-
party vitals panel. Faulty plugin markup is isolated (try/catch per panel,
logged + skipped). IPluginHost.Ui added; AppPluginHost wired; StubHost in
Core.Tests updated; BufferedUiRegistryTests confirms drain-once semantics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements Task 8 of the D.2b retail-UI plan. MarkupDocument.Build() parses
KSML-style panel markup into a live UiNineSlicePanel subtree, resolving
{Binding} attribute expressions against a supplied object via reflection.
Color format is #AARRGGBB (alpha-first, matching controls.ini). Handles
<panel> root (geometry + optional title label) and <meter> children (fill,
label, bar color). Future element kinds (label, button, image) extend the
switch without touching existing code.
vitals.xml encodes the just-approved vitals panel layout (health red #FFC70D0D,
stamina gold #FFD49E1F, mana blue #FF1F33D9); ships next to the binary via
PreserveNewest csproj rule. GameWindow.cs drops the 35-line hand-built panel
block in favour of a 4-line File.ReadAllText + MarkupDocument.Build call —
identical tree, identical render, now data-driven.
2 new tests (Build_CreatesPanelWithMeterFillLabelAndGeometry,
Build_NullBindingValuesYieldNullFillAndLabel) + 11 total targeted green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds ControlsIni — a minimal flat-INI reader for retail's controls.ini
(#AARRGGBB alpha-first color tokens; case-insensitive section/key lookup;
missing file returns an empty sheet with no throw). Wires the [title]
color token into the vitals panel's UiLabel in GameWindow.OnLoad, with
hardcoded white as the fallback. Visually a no-op (retail's [title] color
is white), but proves the stylesheet plumbing end-to-end (D.2b §7).
Three unit tests cover section parsing, #AARRGGBB decode, and graceful
missing-file handling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the dormant AcDream.App/UI retained-mode tree into GameWindow under
ACDREAM_RETAIL_UI=1: an 8-piece dat-sprite UiNineSlicePanel framing three
UiMeter vital bars bound to the existing VitalsVM. Render-only (UiHost input not
yet bridged to the InputDispatcher — next sub-phase). Coexists with the ImGui
devtools path; no regression there.
Visually verified against a live retail client: the bars match retail's vitals
structure (three stacked horizontal bars, current/max numbers centered) — so the
earlier "orbs" assumption was wrong (retail vitals ARE bars), and stamina is GOLD
not cyan (the #10F0F0 research note was wrong). UiMeter gains a centered numeric
Label (stub debug font for now). Spec §8 + the markup example corrected to match.
Bookkeeping: retired divergence row TS-30 (flat-rect panels -> real dat chrome)
and added IA-15 (our UiHost/markup engine vs keystone.dll's LayoutDesc tree).
Remaining polish (filed, §15): glassy gradient bar fill sprite + the retail dat
font for the numbers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds UiMeter, the horizontal vital-bar widget for the D.2b retail-look
UI toolkit. Solid-color fill for Spec 1; the retail orb sprite + scissor
crop path is reserved for a later sub-phase. Five unit tests (1 Fact +
4 Theory) cover half-fill geometry and clamping at -1/0/1/2 fractions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the retail floating-window bevel as a UiPanel subclass using
RetailChromeSprites: 4 tiled edges + 4 stretched corners + tiled center fill,
matching the 8-piece border layout confirmed by the D.2b Step-0 prove-out.
Resolver delegate keeps GL out of unit tests. Geometry verified by
ComputeFrameRects_PlacesCornersEdgesAndCenter (1/1 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step-0 prove-out result: retail UI chrome sprites are RenderSurface objects
(0x06xxxxxx) that must be decoded DIRECTLY, not via the Surface->SurfaceTexture
chain GetOrUpload uses for world materials (which produced 1x1 magenta/garbage).
Added TextureCache.GetOrUploadRenderSurface(id, out w, out h) — Portal/HighRes
TryGet<RenderSurface> -> DecodeRenderSurface(palette:null) -> upload, separately
cached. This is the path UI chrome + (later) dat fonts use.
Confirmed the universal floating-window bevel is an 8-piece border + center fill:
center 0x06004CC2 (48x48)
edges 0x060074BF/C1 (10x5 horiz) 0x060074C0/C2 (5x10 vert)
corners 0x060074C3..C6 (5x5)
Recorded in RetailChromeSprites.cs (edge/corner->position mapping is a best
guess pending the LayoutDesc 0x21000040 parse; visually confirmed at panel
render). The memory-note ids were right; only the decode path was wrong.
Temporary prove-out harness (added to GameWindow.OnRender) removed. proveout*.log
gitignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Base1Solid (or OrigTextureId==0) Surface can carry a null ColorValue;
DecodeSolidColor dereferenced it (color.Alpha) and threw NullReferenceException.
It is called directly from TextureCache.DecodeFromDats, OUTSIDE
DecodeRenderSurface's try/catch, so the NRE crashed the whole client. Surfaced
by the D.2b chrome prove-out feeding UI surface ids. Guard null -> Magenta
(the decoder's existing "undecodable" sentinel). Test added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add uUseTexture==2 (RGBA modulate) branch to ui_text.frag so dat sprites
can be drawn through the existing 2D batcher without touching the font path.
TextRenderer gains _spriteBufs (per-GL-handle List<float>), DrawSprite(), and
a Flush block that issues one draw call per distinct texture with uUseTexture=2.
Also adds DepthMask(false) in the state-save block (restored to true after) to
prevent the transparent-quad pass from writing depth and corrupting the 3D scene
if the UI is flushed mid-frame.
TextureCache gains GetOrUpload(surfaceId, out width, out height) — caches pixel
dimensions alongside the GL handle so UI 9-slice geometry can compute slice UVs
from the source image size without a second decode.
UiRenderContext gains a DrawSprite forwarder that applies the current 2D
translate stack, matching the DrawRect / DrawRectOutline pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds two startup-time env toggles that Phase D.2b's retail-UI panel
frame will read:
- ACDREAM_RETAIL_UI=1 → opts.RetailUi (bool, default false)
- ACDREAM_AC_DIR=<path> → opts.AcDir (string?, default null)
Both follow the existing helper conventions (IsExactlyOne / NullIfEmpty).
No call sites broke because the only construction site in RuntimeOptions.cs
already uses named arguments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The instrumented capture (cottage-112-capture1.log) + dat replay pinned
the transparent-cottage mechanism end to end:
1. The A9B3 cottage's entry cell 0x104 is a 0.22 m-wide THRESHOLD band
(x 184.68->184.46 at y~82). A running player (~13-16 cm/tick at
30 Hz) can cross it BETWEEN two physics ticks - the tick where the
centre is inside 0x104 never happens.
2. Our outdoor-seed branch ran CheckBuildingTransit over a landcell
snapshot and STOPPED - building-admitted entry cells were never
expanded. The tick after the skip (centre in 0x100, a deep room not
building-portal-adjacent) found no containing candidate -> the pick
kept the outdoor landcell FOREVER (absorbing): the user walked the
whole interior classified outdoor (render faithfully drew an outdoor
frame = transparent walls), promoting only on touching
portal-adjacent 0x102's own volume minutes later (captured:
0xA9B3003C -> 0xA9B30102 with no transitions in between).
3. Retail cannot strand: CObjCell::find_cell_list (0x0052b4e0) runs ONE
growing-array walk for EVERY seed (0052b576-0052b5ab,
cells[i]->find_transit_cells vtable dispatch over the GROWING array)
- the landcell's building bridge admits 0x104 (the foot sphere still
overlaps the band one tick after the skip) and the walk expands
0x104's portals to 0x100 where containment wins. Recovery fires one
tick after any skip.
Fix: BuildCellSetAndPickContaining now runs retail's single growing
walk for both seeds with per-cell-type dispatch (landcells ->
CLandCell::find_transit_cells 0x00533800 -> CSortCell 0x00534060 ->
check_building_transit 0x0052c5d0; envcells -> FindTransitCellsSphere
with the straddle gate + once-per-walk outside add). The old indoor
branch behavior is preserved (seed at index 0, hysteresis, straddle-
gated outdoor pick); the outdoor branch gains the expansion + the
indoor branch gains the retail landcell bridge dispatch for
straddle-admitted landcells.
Pins (dat-backed, Issue112MembershipTests): tick-skip recovery one tick
past the threshold (RED pre-fix); run-speed entry replay across tick
phases never strands outdoor; threshold-gap outdoor-seed keeps outdoor
(over-fix guard); entry-walk replay diagnostic prints the full
promotion chain (0x3C -> 0x104 -> 0x100 -> 0x103 -> 0x100 -> 0x102).
Suites: App 246+1skip / Core 1438+2skip / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 1ca412d part-offset expansion fixed the staircase but still rested
on the 5 m promise one level down: a SINGLE part whose mesh extends
more than 5 m from its own origin (offset 0 -> box +-5 m) keeps the
gaze-dependent vanish. Per the user's mandate ("it must work for every
case"), the bound now derives from the dat VERTEX data - the same
vertices that get drawn - so no synthetic containment promise remains.
Oracle context (read this session): retail has NO whole-entity
visibility volume - CPhysicsPart::Draw (0x0050d7a0) viewcone-checks
each part's dat-authored CGfxObj.drawing_sphere at the part's own
world position (RenderDeviceD3D::DrawMesh 0x005a0860). Retail's bound
IS data; ours was a promise. Our per-ENTITY granularity stays (a
deliberate batching-era choice, WB-owned per the inventory) but the
volume is now data-derived and conservative: visually identical by
construction, never culls what retail would draw.
- GfxObjBounds: per-GfxObj vertex AABB, cached by id (parts repeat
heavily); LocalBoundsAccumulator: union of part-transformed AABB
corners (conservative-correct under any affine transform).
- WorldEntity.SetLocalBounds + RefreshAabb preferred path: rotate the
root-local bounds' 8 corners into world axes + DefaultAabbRadius
margin (absorbs animated-pose drift vs the rest-pose bounds; keeps
small objects at their historical box size). Offset heuristic stays
as the fallback for boundless fixtures.
- All four hydration sites wired (outdoor stabs, scenery incl. baked
scale, interior cell statics, server live spawns).
Tests: tall-single-part coverage (the case 1ca412d could not see),
rotation-following, accumulator union. Suites: App 246+1skip / Core
1434+2skip / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User re-gate after 2163308/987313a: run-from-town stairs FIXED, barrel
GONE - but the stairs still vanish by VIEWING ANGLE (visible climbing
down, gone climbing up; same at the tower top). The gate3 probe data
exonerates everything downstream: the entity always draws with correct
batches when it reaches the dispatcher (cache hit:119, restZ correct,
zero WALK-REJECTs, never clip-culled) - so the vanish lives in the one
gaze-dependent gate the probe cannot see: the bounds-based cullers.
WorldEntity.RefreshAabb was a fixed +-5 m box around the entity ANCHOR.
The staircase's 43 parts spiral 15 m ABOVE the anchor, and BOTH
visibility gates derive from the box: the dispatcher's per-entity
frustum cull AND RetailPViewRenderer.EntitySphere (the viewcone sphere
= this box's bounding sphere). Looking up the spiral put the anchor's
neighborhood out of view -> the whole entity culled while 15 m of it
stood in front of the camera; looking down kept the anchor in view ->
visible. Exactly the reported asymmetry.
Fix: expand the box by the largest MeshRef part-translation magnitude
(rotation-invariant, so entity.Rotation needs no handling; identity-
part entities get offset 0 - behavior unchanged; scenery scale is
already baked into the part transforms).
Suites: App 246+1skip / Core 1431+2skip / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports retail ACRender::polyClipFinish (0x006b6d00, pc:702749) near-eye
semantics into PortalProjection.ProjectToClip - the fundamental fix for
the in-plane portal clip family (climb strobes, tower-top roof/floor
flap while turning; live-corroborated this session: [viewer-diff]
0xAAB30108 strobing 27x mid-climb, whole interior dropping at the top).
Pseudocode: docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md.
Three legs, all decomp-driven:
1. ProjectToClip clips at w >= 0 EXACTLY (was EyePlaneW=1e-4), with
retail's any-negative-w gate. Boundary intersections land at w == 0
(homogeneous directions), so a portal the eye is CROSSING yields the
correct unbounded half-region that the bounded view-region clip cuts
to the screen. A w=0 vertex cannot survive a bounded region clip
into the divide (direction fails some edge of any bounded convex
region); the measure-zero corner case is guarded non-finite->empty.
2. CellView.CanonicalKey keys ALL-COLLINEAR (zero-area) views as their
snapped segment ("L:" + extremes) instead of rejecting them - retail
PROPAGATES degenerate views (ClipPortals decomp:433651-433711
forwards any count!=0 GetClip output, no area gate anywhere), keeping
the cell behind an exactly-in-plane portal in the draw list (cells
draw whole; onward floods die naturally). Rejection dropped the
whole chain for the frame - the parked-eye knife-edge band. Finite
key space unchanged -> dedup + strict-growth convergence intact.
3. The EyeInsidePortalOpening rescue is DELETED (the T2-documented
compensation for the 1e-4 divergence) along with EyeStandingPerpDist
+ PointInPoly2D. Empty clip = no flood, period (retail's rule).
CornerFloodReplay - the gate that REFUTED the previous deletion
attempt - passes WITHOUT the rescue under the W=0 port.
Harness criterion corrected to retail's rules (it codified the rescue):
cells fully BEHIND the camera are not required (all-behind portals clip
empty in retail); monotone area holds per root regime; the two
manufactured exact-on-plane steps assert root-only (boundary root pick
is ambiguous; the in-plane portal there is ~perpendicular to the gaze =
genuinely off-screen). Build_CollapsedInteriorPortalNearEye test
inverted to pin the retail empty-clip rule (it pinned the rescue).
New pins: eye-crossing portal -> w==0 boundary verts + half-region (not
sliver); gaze-along-plane degenerate view accepted + segment-key dedup;
non-finite guard. Replay harnesses (CornerFloodReplay, Issue120,
TowerAscent, HouseExit, Issue127) all green.
Suites: App 246+1skip / Core 1430+2skip / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decisive probe (3cf6bcc) caught it live in ONE session: a 43-part
staircase entity (src=0x020003F2, healthy MeshRefs tZ=[0.35..15.15])
drew with cache=hit:3 restZero=3 - THREE batches belonging to a 1-part
entity - then under a different hint the correct hit:119. Two
compounding bugs:
1. interiorIdBase = 0x40000000 | (landblockId & 0x00FFFF00) resolved to
0x40YYFF00 for landblock keys 0xXXYYFFFF - the landblock X byte
DISCARDED. Every landblock in a map Y-row shared one id space:
Holtburg town A9B3's 9th interior stab == the AAB3 tower's spiral
staircase, both 0x40B3FF09. Fixed to 0x40000000|(lbX<<16)|(lbY<<8)
(the scenery 0x80XXYY## scheme).
2. The Tier-1 classification cache's #53 tuple key (EntityId,
LandblockHint) was fed the PLAYER's landblock at bucket-draw time
(RetailPViewRenderer.DrawEntityBucket fabricates its tuple with
ctx.PlayerLandblockId), so colliding ids from different landblocks
shared a key: whichever entity classified first under a hint won,
and the loser wore its batches all session (static fast path never
re-classifies). Also: bucket-hinted entries were never swept by
InvalidateLandblock(owner) - stale entries survived owner unload.
Fixed: ResolveCacheLandblockHint derives the hint from the entity's
owning cell (ParentCellId landblock, canonical 0xXXYYFFFF), falling
back to the tuple id for ownerless paths (outdoor stabs/scenery,
where the tuple IS the owner).
Explains the session-shaped repro exactly: town-login + run to the
tower hydrates/classifies town interiors first -> the tower staircase
cache-hits the town twin's batches (stairs missing/partial + a wrong
object near the floor - the "water barrel"); login-inside classifies
the tower first -> usually clean. meshMissing=0 / entSeen==entDrawn
both ways (everything draws, wrong batches). Likely also feeds #113's
distance-dependent phantom staircase (the town twin wearing the
tower's staircase batches).
3 new cache tests pin the collision contract + hint derivation.
Suites: App green / Core 1430+2skip / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The broken-state log (user-session-capture2.log) shows meshMissing=0 /
entSeen==entDrawn WHILE broken stairs are on screen - the staircase is
DRAWN WRONG, not missing. This probe discriminates the three live
hypotheses in ONE launch (handoff 2026-06-11 s4):
- HYDRATE dump (GameWindow.BuildInteriorEntitiesForStreaming): per-part
placement-frame translations + dropped-part accounting at the MOMENT
MeshRefs are constructed. H-A (SetupMesh.Flatten identity fallback /
silent gfx-null part drops under degraded dat reads) shows here as
zero translations or built<43.
- DRAW dump (WbDrawDispatcher, first tuple per entity): live MeshRefs
translation summary + per-part loaded flags + Tier-1 classification
cache state (batch count + RestPose translation summary), re-emitted
compactly on signature change. H-B (partial/stale cached batch set)
shows as correct translations + odd batch count.
- WALK-REJECT lines (rate-limited): attributes 'entity never reaches
the draw loop' to the specific gate (visibleCellIds/frustum).
- Correct everything -> H-C (draw-side compose), instrument next.
Targets: ACDREAM_DUMP_ENTITY=0x020003F2,0x020005D8 (the 43-part spiral
staircase Setup + the wall barrels; H-A predicts the user's 'barrel' IS
the collapsed staircase). Probe is inert when the env var is unset.
Parser in RenderingDiagnostics (diagnostic-owner pattern) + 5 unit tests.
Suites: App 242+1skip / Core 1427+2skip / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The registration-time re-arm was insufficient and the user proved it
(ran back from the lifestone -> broken stairs + exposed barrel again):
a preparation cancelled by landblock churn AFTER the last registration
event has no later event to re-fire it - crossing blocks loads/unloads
them repeatedly behind the player, so the cancel-after-last-register
window is routinely hit on any cross-country run.
The structural fix: the draw dispatcher touches every
missing-but-referenced mesh every frame (the meshMissing slow path) -
THAT is the one site a retry can never be missed from. Both miss paths
(per-MeshRef and per-Setup-part) now call WbMeshAdapter.EnsureLoaded
(idempotent passthrough to PrepareMeshDataAsync, which early-outs on
existing data and dedups pending tasks), deduped per Draw pass.
Retail-equivalence: retail loads synchronously - geometry is never
permanently absent; this converges the async pipeline to the same
guarantee regardless of cancellation/eviction timing.
Also fixes the #53-one-level-deeper hole found en route: a missing
SETUP PART did not mark the entity incomplete, so a partial batch set
could cache permanently for Setup-shaped render data.
New apparatus: [mesh-miss] once-per-id line under ACDREAM_WB_DIAG=1 -
any future missing mesh names itself instead of needing a live repro.
Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user caught the process failure: two snap fixes were written without
reading retails restore code. The named decomp settles it -
CPhysicsObj::SetPositionInternal (0x00515bd0, pc:283892-283945) treats
the supplied Position as INPUT: AdjustPosition resolves which cell
CONTAINS it, CheckPositionInternal/find_valid_position VALIDATES it
through the collision transition, and the no-cell case goes
store_position + GotoLostCell. There is NO terrain or surface
re-grounding anywhere in the restore path. Trust + validate.
Both prior shapes diverged: grounding outdoor claims to terrainZ warped
a roof-deck logout (ACEs authoritative z=127.2 on the AAB3 tower)
through the roof into the building volume -> the transparent-interior
spawn on every login; the cell-walkable scan that replaced it missed
shell-geometry decks entirely (no EnvCell owns the deck surface) and
failed silently - the user logged in transparent at the tower bottom
again.
Fix: a zero-delta outdoor restore above terrain commits the claims Z
verbatim ([snap] line says so); the first physics tick validates and
settles against the REAL collision world (the BR-7 building channel
covers the deck). max(terrain, z) stays as the under-terrain sanity
bound - our recoverable stand-in for retails lost-cell machinery
(documented divergence, same class as the #107 demote).
Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A zero-delta RESTORE of an outdoor claim standing far above terrain
(logged out on a building roof deck - the AAB3 tower 0x010A slab at
z=127.2 over terrain 112) was grounded to TERRAIN unconditionally,
warping the player through the roof into the building interior,
outdoor-classified -> the transparent-interior spawn the user hit on
every login while the save sat on the roof. Retail restores settle via
AdjustPosition onto real surfaces, not the heightmap.
Fix: the snap outdoor branch, zero-delta shape only, when the claim z
exceeds terrain by more than step height: ground to the nearest CELL
WALKABLE at/below the claim z (the #111 WalkableFloorZNearest query -
real floors only, never the ceiling soup), keeping the outdoor cell id
(honest: a deck-stander center sits above the slab cell BSP - the same
state the user played in all afternoon). GfxObj-shell roofs without
cells not covered - file if a real case shows.
Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live capture pinned it end to end. BuildInteriorEntitiesForStreaming
lifts the render-side cell transform +0.02 m Z (shell z-fighting vs
terrain - a DRAW concern) and passed that LIFTED transform to
BuildLoadedCell, so every plane in the visibility graph sat 2 cm high.
The portal side test's in-plane window is +-10 mm: an eye standing ON a
floor containing a HORIZONTAL portal (the tower's deck lip 010A->0107,
stair landings, cellar mouths) sits 0-10 mm above the TRUE plane = 10-20
mm BELOW the lifted plane -> outside the window -> the cell behind the
portal side-culled out of the flood. Captured live at the stair top:
root=0xAAB3010A eye z=126.803 vs the portal plane at 126.80, flood=1,
0x0107 (the whole tower interior incl. the staircase) dropped WHILE THE
GAZE LOOKED STRAIGHT AT IT - "stairs disappear and you can walk on
them", and the roof/edge flap as the gaze swung the marginal admissions.
Vertical doorways were immune (the lift slides their planes along
themselves) - exactly why this hit stairs/decks/floors and not doors.
Chase chain (the apparatus did all the work): [viewer] print-on-change
probe with eye@mm -> the user's climb capture -> [viewer-diff] naming
the dropped cells per flip -> headless replay of the exact captured
(eye,fwd) frame: healthy UNLIFTED, reproduces ONLY with the production
lift -> gate-by-gate diagnostic (side test dot=+0.003 unlifted vs
-0.017 lifted; clip + rescue exonerated; knife-edge z-sweep all-stable,
killing the float-chaos theory).
Fix: BuildLoadedCell receives the PHYSICS (unlifted) transform; the
drawn shells keep their lift. The seal/punch fans (which read the
visibility LoadedCell's WorldTransform) now stamp TRUE depth - MORE
consistent with the unlifted terrain they protect.
Pins: CapturedTopOfStairs_MainCellStaysInFlood - arm 1 (unlifted =
post-fix production) asserts the main cell admitted at the captured
frame; arm 2 (lifted) is the mechanism canary asserting the drop, with
instructions if it ever starts passing. Plus the gate-by-gate
diagnostic + knife-edge sweep as the investigation record.
Also this session: Issue127FloodFlipReplayTests (the captured 4 cm
outdoor flip pair replays STABLE across fovs/pre-gate arms - the
outdoor churn is NOT the flood math; remaining #127 = distant-building
admission churn, lower priority now that the tower-cell drops are
explained by the lift), and the [viewer-diff] probe (per-flip added/
removed cell naming - keep, it found this).
Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The users tower capture (tower-viewer-capture.log, 551 [viewer] lines)
decodes into three distinct issues:
- #126 (HIGH, #107/#111 family): an OUTDOOR spawn claim on the tower
roof (z=127.2) is grounded to TERRAIN z=112 - the player is warped
through the roof into the tower interior, outdoor-classified ->
the transparent-interior spawn. The snap outdoor branch must ground
to the nearest WALKABLE surface (roofs/GfxObj floors), not terrain.
- #127 (HIGH, the flap mechanism): per-building flood admissions are
BISTABLE per frame under the outdoor root - flood size oscillates
+-1-3 cells at millimetre eye deltas (45<->52 standing on the roof,
including a byte-static eye flip). Every oscillation = building
interiors dropping in/out -> the roof/edge flap; running past a
building = #123. Interior side shows the same family (flood 1<->3,
outPolys 0<->1 during the climb).
- #128: the staircase was invisible the WHOLE climb under a HEALTHY
interior root (0xAAB30107 FullScreen views - the cone cannot cull a
root-cell static), while the SAME build rendered it perfectly in a
different session (diag spawn + screenshot, meshMissing=0).
Session-sticky nondeterminism; the barrel tracks this bug (a
partial subset of staircase parts), NOT dat content (user axiom:
no barrel in retail). Needs a diag-instrumented repro of the users
session shape.
The [viewer] probe now logs the camera forward (fwd=) so the next
capture can be replayed headlessly - Build clip results depend on the
view-projection, not just the eye.
Suites: App 238+1skip, Core 1422+2skip, UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>