Commit graph

908 commits

Author SHA1 Message Date
Erik
94a9f4537a docs(render): VM7 closeout — performance matrix PASS on fe56b6cf; #422 row updated (Campaign VM)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 05:25:47 +02:00
Erik
621a0edfc6 docs(render): VM7 automated closeout note + roadmap entry (Campaign VM)
Release gate 15,283/0/0; connected lifecycle/reconnect route PASS pack-off
and High; VM0 invariance on the final binary (robust 5-10 px, lifestone
band); #422 0/40 instrumented runs. Performance-matrix section to follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 04:46:04 +02:00
Erik
c69029bf58 tools(gate): -ExeOverride and -DebuggerScript for the offline pixel gate; #422 characterised over 40 instrumented runs (Campaign VM VM7)
-ExeOverride runs another AcDream.App.exe through the SAME isolated
settings/scene/capture path so two binaries compare without mixing tool
versions (VM0's recorded fieldOfView trap). -DebuggerScript launches the
client under cdb so ntdll's debug heap validates every free and a heap
corruption breaks at the corrupting site with a stack.

#422 (1-in-8 STATUS_HEAP_CORRUPTION exit after a pack-on offline capture):
16 runs attached + 24 runs under the debug heap on 1f151242, all clean
(P ~ 0.5 % at the filed rate). Recorded as not reproducible on the current
binary, carried as a watch item for the owner's VM7 matrix with the loop as
the first step if it recurs; no longer a merge blocker. Evidence under
docs/research/evidence/vm7 alongside the final pack-off-vs-base+normals
robust diff (5-10 px, lifestone band only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 04:32:27 +02:00
Erik
26adf75079 docs(render): VM6 code-complete — foliage-wind pixel proof, repeat-floor apparatus, ledger (Campaign VM)
Closes the automated half of slice VM6 at 754d59d9 and records how it was
proven. The first pixel method (clock pin 0 s vs 3 s, wind-off pair as the
control) is recorded as confounded: the treeline silhouette carries a
bimodal 0-or-~280 px rasterisation churn between runs of the same binary,
and two conclusions drawn from it were retracted. The replacement
apparatus (same pin, wind on vs off, two captures per arm, robust mask =
AND of the four cross pairs minus both repeat pairs, shadows off so only
geometry can move) found the round-4 defect (wind welded to the shadow
gate: 1 m wind = 49-65 px vs a 22 px floor while the CPU state was
correct) and, after round 5, gives 14,993 robust wind pixels vs a 65 px
floor, every one on a treeline tree, hillside tree or shoreline bush, with
the wind-off frame matching the pre-round-4 plain pipeline at mean |d|
0.007 (round 4's eec95535: 1.77, the overhead-sun regression the narrow
review caught).

tools/vm6/wind-pixel-proof.py is the analysis command the closeout names;
evidence overlays under docs/research/evidence/vm6. Plan: VM6 ledger row
CODE-COMPLETE 2026-08-23, owner visual gate owed (section VM6 Acceptance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 04:01:18 +02:00
Erik
a0157693ec docs(vm3): closeout - owner gate brief with the measured curve table; #422 filed; tests pin the shipped defaults
Opus narrow re-review of 51178f7c: APPROVE. Residuals closed: the AR plan
no longer says '<=1 LSB' unqualified (99.99% of pixels; 95 foliage-
silhouette pixels up to 73 LSB, 58 isolated); the campaign doc says the
same; AtmosphericColorPipelineTests now read the SHIPPED exposure/vignette
defaults from BuiltInAtmosphericRenderPack.Descriptor and the graph's named
DefaultVignetteStrengthFallback instead of literals.

Measured for the gate (offline Holtburg hillside, High defaults vs pack
off): mean luminance -17% noon, -44% dusk, p95 unchanged, clip 0.06% both;
neutral High vs pack off: 110,561 px at |d|=1, 95 at >=5 (foliage edges).
Evidence images under docs/research/evidence/vm3/.

#422 filed: one High-default offline capture exited with
STATUS_HEAP_CORRUPTION after a clean managed shutdown; 1 in 8 runs, never
under validation layers. VM7 gate item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 23:53:26 +02:00
Erik
6e76314b08 docs(vm4): every Campaign AR document says what its evidence shows
Dated 'VM4 correction' notes, not silent rewrites:
- AR plan: NoOpRenderPackProductionIntegrationTests is a 2x2 recording-
  device fixture, not a production pin; the real invariance is VM0's.
  Both '14,928/14,928 zero skips' totals qualified: hermetic lane filter.
- Stage-2 connected report: the dense-town CPU figures ran under the
  automation observer (9.8 MB/frame alloc) and say nothing about the
  owner's hitch on the product; clean numbers are VM0's.
- Track A report + findings doc: TerrainUtils.GetNormal only orients
  scenery; the render normals were already smooth; A2 replaced central
  differences with retail's incident-face average (the reviewer's own
  premise error).
- Findings doc: 'retail brightens' is the fallback path only; real
  hardware runs the single-pass lerp (VM2). LandscapeDetailTextures = 0
  answers the landscape open question. VM0 production table added as the
  baseline.
- Completion audit retail-path row and the review's F1/F2/F3/F5 headers
  annotated with their closures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 22:51:59 +02:00
Erik
dcdd102824 docs(vm1): closeout - AP-232 for the translucent detail blend weight; sampler test pins the production constant
Opus narrow re-review of ae651312: APPROVE. Closes its three residuals:
- AP-232 filed: retail's single-pass stage-1 OUTPUT alpha
  (MODULATE(TEXTURE, CURRENT) @0x0059c549) is the blend weight for a
  translucent subset; acdream's two-draw model is exact for opaque
  subsets (fog identity pinned) and a bounded weight difference on
  translucent ones. Distinct from AP-34 (queue order). Owed since
  05970306.
- TerrainAtlas.DetailSamplerDescription names the production sampler
  (WRAP/LINEAR x3 per ACRender::SetDetailSurfaceInternal @0x006b6280);
  the test now asserts that constant's properties instead of a
  test-local copy.
- Plan VM1 section: fragment now described as fogged; VM1 marked CLOSED
  with the Holtburg measurement (+2.17/+0.57/+0.16 vs predicted
  +2.2/+0.66/+0.16) and the detail-on cost (+0.3-0.5 ms CPU at Arwic).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 22:50:06 +02:00
Erik
ae6513126e fix(render): detail overlay is fogged after the combine like retail; VM1 review fixes
Opus dual-lens review of 05970306 + 388457a7 (APPROVE WITH FIXES). Four
items, all landed:

1. FOG (behavioural). Retail's D3D fixed-function fog stage runs AFTER the
   texture-stage pipeline, so the detail contribution must be fogged, not
   just the base. mesh_modern.frag already fogs the base colour
   (applyFog(rgb, vWorldPos)) before mesh_detail's replay draws over it;
   mesh_detail.frag previously emitted raw detail.rgb, understating fog by
   f*a*(fog-detail). Fix: mesh_detail.vert now outputs vWorldPos (mirroring
   mesh_modern.vert); mesh_detail.frag declares the identical SceneLighting
   UBO and applyFog function (copied verbatim, same binding/std140/math) and
   fogs detail.rgb before emitting it. This collapses algebraically to
   retail's fog-after-combine order:
     (1-a)*mix(base,fog,f) + a*mix(detail,fog,f) = mix(lerp(base,detail,a),fog,f)
   RetailDetailTextureContract gains ExpectedFogged(base,detail,opacity,fog,
   fogFactor); RetailDetailTextureContractTests pins the identity across 200
   random samples within 1e-6.

2. EnvCellRenderer.Rhi.cs's DrawEnvCell-category comment still said "apply
   the 10-50 m positive-view-depth fade" — a stale claim from before VM1
   removed the fade. Replaced with the mip-chain attenuation statement that
   mesh_detail.vert's header comment already carries.

3. Added the test the VM1 contract required but never had: TerrainAtlas
   .TryCreateDetailTexture uploads a full mip chain (MipLevelCount ==
   RhiWorldTextureArray.MipLevelsFor(w,h), GenerateMipChain called) and
   registers with the repeat/linear world sampler, not single-level or
   clamped. Drives the private method directly (reflection) against a
   synthetic PFID_A8R8G8B8 RenderSurface through a minimal in-memory
   IDatReaderWriter fake, so the lane stays hermetic (no installed DAT).

4. #226 pseudocode note: noted that retail's stage-1 OUTPUT alpha
   (MODULATE(TEXTURE, CURRENT), 0x0059c549) — the framebuffer blend weight a
   delayed-alpha subset composites with — is not modelled; acdream instead
   draws a second pass weighted by detail.a*diffuseAlpha. Identical for
   opaque subsets, a bounded difference on translucent building/EnvCell
   subsets already covered by the existing AP-34 shared-alpha-queue
   divergence row. Also qualified the tmpmaterial.Diffuse.a = 1f (0x0059cb99)
   citation to name its exact branch (burnedInStaticLights < 0 &&
   *(render_device+0x7e4) == 0); the other branch leaves diffuse FromVertex,
   but the opaque->1 / fading->opacity mapping still holds either way.

Nit also folded in: EnvCellRendererTests' new SubmitRhi instance-alpha test
is now a [Theory] over WbRenderPass.Opaque and .Transparent, pinning the
bind-before-first-draw invariant on both passes.

Regenerated mesh_detail's committed SPIR-V and the shader manifest
(tools/compile-shaders.ps1); no other shader pair changed.

Verified: dotnet build AcDream.slnx -c Release (0 warnings, 0 errors);
dotnet test on AcDream.App.Tests (Release, hermetic lanes) green, including
the shader manifest tests explicitly; AcDream.Core.Tests unaffected/green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 22:39:58 +02:00
Erik
059703066f fix(render): #226 detail overlay uses retail's single-pass combine; drop the dead distance fade (Campaign VM VM1)
VM2's live cdb read against the PDB-paired retail client (GUID
9e847e2f-777c-4bd9-886c-22256bb87f32) proved
m_caps.bCanDoSinglePassDetailing = 1 and trysinglepass = 1 on real hardware,
so D3DPolyRender::RenderMeshSubset (0x0059ca10) never falls back to the
two-pass framebuffer blend the earlier #226 port reproduced. Every loaded
CGfxObj sets use_built_mesh = 1 (CGfxObj::InitLoad 0x005346b0), so buildings
and EnvCells always take the single-pass texture-stage combine set up in
D3DPolyRender::SetSurface (0x0059c4d0):

    result = lerp(base * diffuse, detail.rgb, detail.a * diffuse.a)

RenderMeshSubset lights opaque built-mesh subsets with
tmpmaterial.Diffuse.a = 1, so on the live Dereth category texture
0x06006D58 (mean rgb 0.165, mean alpha 0.132) the combine works out to
~0.868 * base + 0.022 — a mild darkening, the opposite sign of the fallback
DstColor blend's brightening.

Also removes the invented 10 m / 50 m distance fade. Retail's
ACRender::get_alpha_for_z (0x006b6230) is only evaluated in
D3DPolyRender::DrawPolyInternal (0x0059d7c0, the immediate-polygon path)
and only when the static noFadeDetail (0x00820e38, initialised to 1) is 0 —
unreachable for built meshes. Attenuation is the sampler's linear mip chain
converging to the texture mean, not a scripted ramp.

Changes:
- mesh_detail.vert/.frag: drop vDetailFade and its distance term; add
  vDetailOpacity mirroring mesh_modern.vert's InstanceAlphaBuf (binding 7)
  read, and output detail.rgb with alpha = detail.a * vDetailOpacity under
  the corrected pipeline blend.
- VulkanViewportMapping.BlendFactorsOf / GpuEnums.GpuBlendMode.RetailDetail:
  SrcAlpha + OneMinusSrcAlpha instead of DstColor + OneMinusSrcAlpha.
- RetailDetailTextureContract: replaced the distance-fade constants and
  FramebufferFactor with Expected(base, detail, opacity) and IsNeutral,
  matching the lerp; contract tests cover zero-alpha/zero-opacity no-ops,
  the measured darkening on the live category texture, and full-alpha
  replacement.
- Regenerated mesh_detail's committed SPIR-V and the shader manifest
  (tools/compile-shaders.ps1); no other shader pair changed.
- Docs: #226's pseudocode note, the docs/ISSUES.md #226 entry, and the
  retired TS-52 divergence-register row corrected from the two-pass
  DESTCOLOR description to the single-pass path and the darkening
  expectation, each citing the VM2 cdb note.

Verified: dotnet build AcDream.slnx -c Release (0 warnings, 0 errors);
dotnet test on AcDream.App.Tests and AcDream.Core.Tests (Release, hermetic
lanes) both green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 21:58:48 +02:00
Erik
30e72a2af8 research(vm0): Campaign AR's pack-off path IS the pre-campaign renderer - PASS
Exact-pixel and production-perf comparison of 6c79d35c (+ only the A2
normal files, so no terrain mask is needed) against c51b07ef with no pack:
connected as +Acdream, visible window, one isolated config clone per
variant, pinned clocks. Open field: the only differences are idle pose,
mana digits and a passing flyer. Holtburg: same-binary-twice defines the
dynamic mask (9.8%); in the static 90% both self-diffs have ZERO pixels
with |d|>=8 while base+normals vs HEAD-off has 841/729 - all streaks inside
the animated lifestone. Buildings, ground, trees, sky and UI are clean.

Perf (uncapped Release, no automation observer, ACDREAM_FRAME_PROF=1):
Holtburg CPU p50 4.7 -> 4.1 ms, Arwic 6.0 -> 5.2 ms, GPU unchanged,
alloc/frame 574 KB -> 21 KB. No regression; F5b's '27.8 ms retail CPU' was
the observer.

Three false alarms recorded so nobody repeats them: the isolated gate
settings lack fieldOfView (90 vs the real 86.33 -> a 0.952 zoom); the real
%APPDATA% settings still selected acdream.atmospheric/low (pack ON); a
minimized GLFW window is throttled and never settles.

Tools: tools/vm0/capture-visible.ps1 (pre-campaign gate + -Exe/-Live/
-ConfigDir/-CharacterName/-PreCaptureCommand), tools/vm0/perf-run.sh,
and -BuildingDetailTextures on run-offline-pixel-gate.ps1. Baseline
patches under docs/research/evidence/vm0/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 21:40:40 +02:00
Erik
fe085cadaa research(vm2): retail runs SINGLE-PASS detail texturing - the #226 port targets the fallback
Live cdb read on the PDB-paired retail client (owner's AMD GPU):
  m_caps.bCanDoSinglePassDetailing = 1, trysinglepass = 1,
  MaxSimultaneousTextures = 8, bTexOpDotProduct3 = 1,
  LandscapeDetailTextures = 0, EnvironmentDetailTextures = 1,
  landscape/object detail surfaces null, building/environment non-null,
  tiling 4 everywhere.

So retail's detail combine on modern hardware is the texture-stage path in
D3DPolyRender::SetSurface (stage0 alpha PREMODULATE, stage1 colour
BLENDCURRENTALPHA): lerp(base*diffuse, detail.rgb, detail.a*diffuse.a) -
a mild DARKENING (~-10% mid-tones with 0x06006D58), not the
DSTCOLOR+INVSRCALPHA brightening that Campaign AR ported and that the
2026-08-21 findings doc + AR review both analysed. Those described the
fallback for adapters without D3DTEXOPCAPS_PREMODULATE; the reviewer's
guess that consumer drivers rarely advertised it was wrong.

VM1 now carries the re-port (SRCALPHA+INVSRCALPHA, detail.rgb /
detail.a*diffuseAlpha, neutral at a==0) together with the fade removal.
No distance fade exists on either path. Scripts are read-only attaches
with no breakpoints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 18:23:27 +02:00
Erik
4dbbe8fead docs(plans): Campaign VM — VisualMaster plan + the Campaign AR review it closes
Adds the independent Campaign AR review (docs/research/2026-08-22-campaign-
ar-review.md): architecture, technical correctness, performance, coverage.
Findings F1-F8; F1 (default-path invariance proven only on a 2x2 recording-
device oracle) blocks merge; F2 (10 m/50 m detail fade has no retail anchor);
F3 (the port is retail's two-pass fallback; single-pass stage path
undetermined); F4 (post stack runs in gamma space); F5 (four overclaiming
sentences, one premise error the reviewer owns).

Campaign VM plans the closure: VM0 masked pixel + production perf A/B vs
6c79d35c; VM1 removes the fade (retail attenuates by mips); VM2 one cdb read
of m_caps.bCanDoSinglePassDetailing; VM3 linear-light post stack with a
numerically neutral preset; VM4 truthful docs; VM5 volumetric jitter; VM6
weather-driven foliage wind - procedural-scenery (bit-31 id namespace) cutout
subsets only, three motions (lean/branch/flutter) with gusts and per-tree
phase, shadows share the displacement include; VM7 closeout + merge.

Roadmap gains the Campaign VM pointer beside Campaign AR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 18:17:08 +02:00
Erik
9cd429dd73 docs(render): record secret-safe closeout gate 2026-08-22 14:22:35 +02:00
Erik
fa3e7978ec fix(gates): require secret-safe observer credentials 2026-08-22 14:20:14 +02:00
Erik
772526d0cd docs(render): audit Campaign AR completion evidence 2026-08-22 14:18:21 +02:00
Erik
d84edf0ceb docs(render): finalize Campaign AR machine-local gate 2026-08-22 14:14:40 +02:00
Erik
e56ac57486 docs(render): record Campaign AR Stage 2 closeout 2026-08-22 14:01:07 +02:00
Erik
7a5f96ede5 feat(render): implement Campaign AR and terrain fidelity 2026-08-22 13:13:29 +02:00
Erik
99cf26e00c Merge remote-tracking branch 'origin/main' into codex/atmospheric-rendering-campaign 2026-08-22 13:02:43 +02:00
Erik
5ca029d3ea docs(research): terrain detail/normals + atmospheric rendering findings
Measured, not theorised. Records for the next session:

- #226 detail overlay is NOT implemented at 255b0aae, and the
  BuildingDetailTextures Options checkbox is wired to NOTHING (settings +
  UI exist; no consumer in Rendering/). The port must consume that
  existing setting.
- Retail authors detail per terrain entry; Dereth has 33 entries but only
  3 distinct detail textures, one 64x64 backing 88% of them.
- The detail pass is a framebuffer blend, not DOT3: environment uses
  DSTCOLOR+INVSRCALPHA under BLENDOP_ADD, landscape uses SRCALPHA. Decoded
  texture means give factors 1.03-1.20, so retail's own pass BRIGHTENS
  rather than roughens - a real design decision for the port.
- EnvironmentDetailTextures gates buildings+interiors, NOT landscape
  (ChangeRegion passes landscape=0).
- Refuted and recorded so nobody re-chases: highres is NOT an override dat
  (20,684/2,294 ids, zero overlap - we already use it); the detail
  textures are NOT normal maps (24%/8%/15% unit-length); but the engine
  DOES have BumpMap/DotProduct3 machinery.
- Terrain normals are flat per-face with no averaging - whether retail
  smooths is the open parity question.
- Atmospheric rendering want captured: opt-in shader packs, tier table,
  and the shadow constraints (alpha-tested casters, animated casters via
  the N.5 SSBO, indoor gating, streaming-bounded cascades, the #129 bias
  trap, and the shadow_objects naming trap).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:02:27 +02:00
Erik
ba6c82af93 docs: Campaign CT rescoped to complete chat parity, system + GUI
The user set the goal: complete retail parity for the chat system AND the chat
GUI, not just the green clickable name that started the review. Plan restated
around that bar, with a definition of done — every retail behaviour either
implemented or carrying a divergence-register row, every user-visible surface
covered by a test, and the digest/ISSUES describing reality.

Slices regrouped into A (the tagged-text capability, a strict chain where
nothing is visible until A4), B (system behaviours), C (GUI), D (hygiene), plus
the research still owed before specific slices and what is deliberately out of
scope.

Four items joined the plan that the original six lanes did not own, because
they fell between lanes:
  - the /r, /t, /tell text-replacement macro (the commands work; retail's
    VISIBLE expansion to "@tell {LastTeller}, " does not exist)
  - FilterLanguage, which is a decorative toggle: we store it, ship the bit and
    show it in Options, and never actually filter anything
  - the plain-text session chat log retail writes and we do not
  - the option-gated timestamp prefix

Also corrects the CH3 command-registry research note. Its "acdream status"
columns are from before slice CH4 and list 13 verbs as MISSING that have all
since been added — cg, soc, o, co-vassals, fellows, group, party, vassal, ab,
guild, ct, clfg, crp — and its DIVERGENT row for /g is likewise stale: acdream
maps /g to Fellowship, matching retail, confirmed against the live client
today. The retail side of that document is still the authority; only the
columns describing us were wrong. They misled this session's investigation,
which is exactly why the banner says to verify against ChatInputParser.cs.

Nothing implemented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 07:13:55 +02:00
Erik
663129c340 docs: Campaign CT — chat text tags, researched and planned
Six parallel research lanes on retail's chat text and window behaviour, plus a
plan. The headline: the green clickable speaker name is not a chat feature and
not a colour, it is a missing capability in the TEXT stack.

Retail's client sprintfs literal tag markup into the chat line, and the text
element parses the brackets while appending, attaching a ref-counted tag PER
GLYPH. A tagged run is emergent: adjacent glyphs whose tag pointers are equal.
A glyph takes the tag colour (property 0x1D) only when a tag is open and its
type is 0x10000001; otherwise the ordinary line colour (0x1B).

The colour itself was the one thing the decomp could not settle — it is
authored, not runtime-built — so it was MEASURED out of the installed dats
rather than assumed from a screenshot: P0x1D = RGB(0,178,0). That also exposed
a trap: the tag colour is per-ELEMENT and authored while the line colour on the
same element comes from the runtime chat table, so filing "tag green" into the
LogTextType table would put it in the wrong place.

Our own audit found the gap is narrower than feared. UiText ALREADY draws
multi-coloured runs (the character stat panel uses it); the path is just gated
to single-line elements. The draw path needs no renderer work, and HitChar
already resolves a click to line+column. The real blocker is that sender
identity is destroyed before it reaches the renderer: ChatEntry carries
Sender/SenderGuid the whole way, and ChatVM.RecentLinesDetailed drops both.

Two findings beyond the original question. Retail BOUNDS its transcript
(10,000 chars, trimmed to ~7,500 at a newline) and splits auto-scroll from an
unread indicator by sampling "was at bottom" before the line lands — a naive
port auto-scrolls forever and leaks for the life of a session. And the chat-UI
audit turned up an untracked bug: Escape in the chat input does nothing at all,
because UiField has no Escape case and a focused field also suppresses the
input dispatcher's fallback.

Every lane was instructed to write "UNKNOWN — needs X" rather than guess, and
they did; the carried unknowns are listed in the plan rather than papered over.

Seven slices proposed, nothing implemented yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 06:52:05 +02:00
Erik
be324c003c tools(LayoutDump): dump authored colour arrays; measure retail's tag green
The chat deep-dive proved the MECHANISM behind retail's green clickable
speaker name — a glyph tag coloured from property 0x1D rather than the line's
own 0x1B — but not the colour itself: BuildChatColorLookupTable @0x004F31C0
builds only the 0x1B array, so the value is authored rather than runtime-built
and the research correctly returned "UNKNOWN" instead of assuming the green in
a screenshot.

--colors prints the 0x1B/0x1D arrays of every element in a layout, which
measures it out of the installed dats:

    chat 0x2100006F, transcript 0x10000011
      P0x1B [0x00] R=204 G=204 B=204
      P0x1D [0x00] R=  0 G=178 B=  0     <- the green

Recorded in the research note, including the trap it exposes: the tag colour is
per-ELEMENT and authored, while the line colour on that same element comes from
the runtime chat table. Filing "tag green" into the LogTextType colour table
would put it in the wrong place entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 06:49:59 +02:00
Erik
690f21889e docs: handoff for the VTank-class plugin automation milestone
The requirements research from 2026-07-29 was nearly lost: it lives in a
session titled "graphics" because it was one strand of a session about
something else, and could not be found again by any search. The research
itself was committed (6077ce4d) and survived; this makes it findable and
usable by a session picking the work up cold.

Contents: reading order, the architectural conclusion not to relitigate
(VTank's meta FSM, expressions and loot engine are plugin-land, not
host-land; the K2 headless triad is already the right substrate), the
five dependency-ordered steps, every relevant file path, and the project
rules that bind the work.

Two things the handoff adds beyond relaying the research. First, a
changed-since section: three of the research's "gap" rows have closed —
vendor landed 2026-08-08, fellowship 2026-08-12, secure trade 2026-08-14 —
so step 4's substrate is materially stronger than when the plan was
written, and §3's gap table is the part that has aged. Second, every path
is verified against the live tree as of today rather than copied forward:
WorldEntitySnapshot is still exactly four fields, IPluginHost is unchanged,
there is still no enchantment-enumerating view and no point-goal movement
primitive, and IHeadlessBotPolicy is internal to AcDream.Headless so step 1
mirrors its shape rather than re-exporting the type.

All 30 path references were checked programmatically. Also records that
claude-memory/ is a junction that resolves in the main checkout but not
inside a worktree, which would otherwise read as a broken pointer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 15:50:58 +02:00
Erik
53b6841c5a test: remove final campaign labels 2026-08-18 13:50:31 +02:00
Erik
c1a905004a test: separate diagnostic apparatus from release gates 2026-08-18 11:06:08 +02:00
Erik
6ee3d88863 fix(ui): night-round review — F1 real PlaceMarkerOnMap formula
The prior PlaceMarker() reading ("center at markerX0+x") was wrong.
Binary Ninja elides gmMapUI::PlaceMarkerOnMap @0x004a18b0's entire FPU
chain to bare, operand-less _ftol2() calls, so the pseudo-C
under-specifies the function. A capstone disassembly of the raw bytes
in the PDB-paired acclient.exe recovers the real formula: retail
projects the AC display coordinate (range ~-102.4..102.4) onto the
marker-area rect via a fixed-point-style transform, not a raw pixel
add:

  X = m_x0 - w/2 - (int)((m_x1-m_x0+1) * (x*10+1024)        * (-1/2048))
  Y = m_y0 - h/2 - (int)((m_y1-m_y0+1) * (2047-(y*10+1024)) * (-1/2048))

Constants read directly from .rdata: 0x79bac8=10.0, 0x7aac78=1024.0,
0x7aac70=-1/2048, 0x7aac68=2047.0. The Y axis's FSUBR is retail's
north-up flip. w/h halve with truncating integer division (matching
retail's cdq;sub;sar idiom), not float division.

Extracted the pure math into MapPageController.ComputeMarkerPosition
so it's directly testable, and retargeted MapPageControllerTests to
GOLDEN PIXEL values computed independently from the formula (never
from the port's own output): the reviewer's canonical (0,0)->(122,128)
case, a far-west and far-north case, and a real town-table entry
(Arwic's landblock, cross-checked against RadarCoordinates). Applies
to the green ring, house pin, and all 53 static town hotspots, which
all resolve through the same PlaceMarker call.

Corrected the recon doc's "accepted as-is" note, which had mistaken
"the FPU argument-passing is BN-mangled" for a narrow issue instead of
the whole-formula elision it actually was.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 04:19:29 +02:00
Erik
4817c17600 docs: Map/House panel recon — panelId 16/slot 0x1000018C resolved, LandDefs.GidToLcoord reuse, wire enum already present
Saves the overnight-round recon (embedded findings + this session's desk
verification) for auditability before implementation starts. Corrects two
handoff claims: the panel id is 16 (already resolved by the existing FA
campaign's full 16-slot gmPanelUI::SetupChildren dump, not a guess from
{1,2,6,14}), and GameEventType already defines all four House opcodes
(0x0225-0x0228) — what's missing is routing, not the enum. Identifies that
LandDefs.GidToLcoord/LcoordToGid (src/AcDream.Core/Physics/LandDefs.cs) is
an existing tested port of LandDefs::gid_to_lcoord, reusable for both the
Map tab's coordinate math and the House location display — no re-port
needed. Cites the toolbar button (0x1000019A, panel id 16), the 53-entry
s_rgLocations marker table verbatim, the ServerPosition wire struct reuse
for HouseData.Position, and the AuthoredTooltipText/RetailTooltipPresenter
seam that will close register row TS-85's last item (gmMapUI::AddMapNote).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 01:27:28 +02:00
Erik
ff8b1ebc89 docs: Campaign CC connected gate PASSED — campaign CLOSED user-accepted 2026-08-16
The extended gate round (GF-1..16, R2/R3/R4 re-tests, fix batches A-G +
closeout + two re-test rounds) closed with the user's pass on build
1.0.2-cc.o. Plan status and ledger flipped; findings doc carries the
full round history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:13:36 +02:00
Erik
e6acb800cc fix(chargen): Campaign CC gate round 1 re-test 3 — R4-1..R4-4
Four visual residuals from the lead's own live-client captures of
1.0.2-cc.m, all root-caused via decomp + live-DAT evidence:

- R4-1: Skills credits value overlapped mid-caption again. Root cause
  was a missing UiLayoutPolicy raw-edge reflow on UiButton's value-child
  rect (the child is base-inherited across four sibling buttons of
  differing widths, so its baked-in OriginalParentWidth diverges from
  the actual 231px-wide Skills credits button) plus an HJustify.Right
  value child mapped to Center instead of a real far-edge Right.
- R4-2: the single-sprite scrollbar thumb tiled (GL_REPEAT) instead of
  drawing once — DrawTiled was reused for a small fixed marker graphic
  whose native size is far smaller than the track-proportional thumb
  rect. New DrawThumbMarker draws exactly one native-size instance.
- R4-3: the skills info-box formula line clipped past the surrounding
  gold frame's own authored bottom edge (the pane's own raw box is 20px
  taller than the frame that visually contains it) — clamp the pane's
  Height to the frame's bottom (register AD-105, since retail's
  ShowSkillsText has no code relationship to the frame to cite).
- R4-4: the Appearance help text started mid-sentence — the box was
  never touched by its page controller, so it kept UiText's chat-style
  PreserveEndOnLayout=true default; the scroll model's wasAtEnd check is
  vacuously true on its first-ever overflow transition, pinning the
  first render to the bottom. Set PreserveEndOnLayout=false (a static
  top-oriented report, not a transcript) and wired the box's own nested
  authored scrollbar, never wired before.

App suite live-DAT env 5372/3 -> 5379/3 (+7, zero regressions). Runtime
1735/0 unchanged. Full solution 14585/4 skips/1 failure (the documented
Core.Net NakEmission full-solution-only flake, confirmed standalone-pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:05:23 +02:00
Erik
28704db4bf docs: Campaign CC gate round 1 re-test 3 findings R4-1..R4-4 (lead's live captures)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 18:08:13 +02:00
Erik
91f84dec9f test(chargen): Campaign CC gate round 1 re-test 2 — R3-8 name-field exhaustive dump + closeout docs
R3-8: dumped EVERY property present on 0x10000402 (not just P0x17) across
every state, cross-referenced against UIElement_Text::OnSetAttribute's
complete case list (no UIElement_TextInput class exists in retail — the
name field is a plain UIElement_Text/m_filter-bearing field). The full
recognized-property space has no placeholder/prompt mechanism independent
of P0x17. The BaseElement/prototype-inheritance hypothesis is also ruled
out — the existing regression test already probes the fully-merged
ElementInfo (post BaseElement resolution) and finds nothing. The only
StringInfo-kind property present, 0x49, resolves to "Your name can be 32
characters long and cannot contain numbers or symbols." — but 0x49 is
part of the same five-property tooltip family ISSUES #409/GF-16 already
document client-wide (0x48's own DID, 0x21000041, is the EXACT tooltip
popup LayoutDesc #409 cites) — a hover tooltip, not an in-field
placeholder. No code change, per this batch's own "do not invent a
placeholder" contract — third independent negative result on this
question via three different mechanisms. The lead should request a live
retail screenshot before any further investigation.

Also carries the shared live-DAT regression suite for R3-1 through R3-7
(CharacterCreationLiveDatTests.cs holds tests spanning multiple findings
in one file, so they land together) and the RE-TEST 2 findings-doc
closeout writeup for all eight items.

App suite live-DAT env 5358/3 -> 5372/3 (+14, zero regressions). Runtime
1735/0 unchanged (untouched this round). Full solution: 14578 tests / 4
skips / 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:20:42 +02:00
Erik
956b8d5b6b docs: Campaign CC gate round 1 re-test 2 findings R3-1..R3-9
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:02:18 +02:00
Erik
9efcd80e34 docs: Campaign CC gate round 1 closeout — register/ISSUES/ledger bookkeeping (F3, F12, F15)
Doc-only findings from the round review, plus the register rows the
three code commits' own bookkeeping notes reference:

- F3: AP-229 amended with the dialog-as-sibling z-order addendum — the
  same flat-sibling-list mechanism that motivates AP-229's own screen-
  layering row also covers RetailDialogFactory's open dialogs, which was
  GF-15's actual root cause (now fixed, but the underlying divergence —
  dialogs and screens sharing one z-order list at all — remains and
  could reintroduce the same failure class via a future sibling's own
  unconditional per-tick BringToFront).
- F5/F6: AP-230 amended with the second narrow-honor addendum (the
  LayoutImporter carve-out fix landed in the Group 3 code commit); the
  findings doc's "CHAT INPUT" label corrected to "chat transcript" in
  both places it appeared (0x2100006F/0x10000011 is the transcript
  display, not the input textbox).
- F12: the AD section header recounted 77 -> 79 (a direct physical count
  found it undercounted by 2); the AP section header's own "one high"
  drift-direction note corrected to "one low" — verified against the
  actual commit history (Batch A ended with 165 physical rows but a 164
  header; Batch B's recount correctly landed on 164, the header was
  never overcounting).
- F15: ISSUES.md #406 gains the crash-vs-incomplete-shutdown precedence
  sentence — ReportExited's _runFailure check runs first and returns
  immediately, so a crash always wins over a subsequently-failed
  shutdown for the same session's reported reason.
- AP-231 filed (the Group 2 commit's own ComposeFormula connector-text
  approximation — referenced in that commit's message but the register
  row itself was missed until this pass; 161 active AP rows).
- Campaign CC plan ledger gains a "Gate round 1" row with the full
  commit list for batches A-G plus this session's three closeout
  commits, superseding the ledger's stale "sole remaining acceptance
  step" framing (written before the connected gate ran and found the
  GF-1..GF-16 / R2-1..R2-8 findings this whole round fixed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 15:37:46 +02:00
Erik
1f6365d3e5 Merge campaign-cc-batch-g: Batch G — real color wheel mechanism (inert until closeout wiring)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	docs/research/2026-08-16-campaign-cc-gate-round1-findings.md
2026-08-16 14:19:30 +02:00
Erik
0ecd332fb1 Merge campaign-cc-batch-f: Batch F — skills selection, scrollbar, cost text, arrow states
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 14:19:01 +02:00
Erik
8c30aa18ee fix(chargen): Campaign CC gate round 1 Batch F — Skills page buckets, selection, info box, cost text, arrow states
R2-4/review F1-F2 (gmCGSkillsPage): row click (and arrow click, matching
retail's own post-Increase/DecreaseSkillLevel re-select) now selects a
skill, highlights its row name, and writes the info panes' title (name +
score) and a level-gated bonus line; the description/formula halves stay
unported (SkillBase._description/_formula unreachable from this page's
current data surface, documented on RefreshInfoBox). The listbox's own
authored scrollbar link is wired to its Scroll model (live-DAT-confirmed
at 0x100003F8, matching the "+1 from the listbox" hypothesis). Cost text
now matches SetSkillText @0x00480600 exactly: Untrained's down-cost and
Specialized's up-cost are literal "0", unconditional, where the port
previously rendered blank; the 999-blank gate applies to the up-cost
only, never to a down-cost. Arrow Ghosted/Enabled state (0x1000001a/
0x1000001b) is now gated per branch, including bUntrainable/
bUnspecializable re-derived as "this row's own effective cost is
nonzero" — no new data needed since the page already resolves that cost.

R2-4b (the four-bucket sorted model) is NOT implemented — its Useable-
vs-Unuseable-Untrained split reads SkillBase.MinLevel, confirmed present
in the installed dat (SkillTable_MinLevelDistribution_NeverExceedsTrained)
but not threaded through ChargenOptions/ChargenHeritageOptions/
CharacterCreationRuntimeBindings. AP-213 row records the exact channel a
future fix needs. Also live-DAT-pinned: Templates[0]'s header-caption
child (0x100002f6) resolves as a UiButton, not UiText, in the real dat —
the same UIElement_Button-is-DynamicCast(0xc)-compatible-with-Text quirk
already ported for GF-4b's slider labels.

App suite (live-DAT env) 5321/3 -> 5328/3 (+7, zero regressions).
Runtime 1735/0 unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 14:18:13 +02:00
Erik
834c2547a9 fix(chargen): Campaign CC gate round 1 Batch G — real color wheel (DoColorSpots/DoGradDisk color rendering)
R2-5: retail's gmCGAppearancePage::DoColorSpots/SetSelection/DoGradDisk
paint the nine color swatches and the gradient disc with a real,
computed representative color (PalSet-averaged for Hair/Nose+Mouth+
Skin/Headgear/Shirt/Trousers/Footwear at fixed sample indices
0xd0/0xb0/0x520; direct-Palette for Eyes at 0x103), not the static
authored art acdream showed before this batch.

Ports the full palette-to-RGB pipeline: a new pure Core resolver
(ChargenSwatchColorResolver + IChargenPaletteColorSource) backed by a
new ChargenAppearanceCatalog.TryGetColor reading real Palette dat
objects, pinned against the installed EoR dat. CharacterCreationAppearancePage
recomputes all nine swatches + the gradient disc's tint on every
refresh (part/color/heritage change) and paints them through a new
ChargenSwatchColorTile overlay child — a flat-color-fill approximation
of retail's actual recolored-sprite blit, since neither UiButton
(sealed) nor UiDatElement exposes a per-instance sprite tint today.

Two STOPPED items remain outside this batch's file contract before the
mechanism is visually live: (1) wiring PalSetSource/ClothingTableSource/
PaletteColorSource from CharacterCreationUiController.cs (mirrors the
existing PreviewControl seam); (2) a small additive Tint property on
UiButton/UiDatElement for a byte-true recolor instead of the flat fill.
Also ports Nose/Mouth/Skin's single non-interactive representative
swatch, beyond AP-216/AP-217's original six-part scope.

Register AP-216/AP-217 rewritten (not retired — the two STOPPED items
keep them open). Tests: 11 new Core, 6 new Content live-DAT, 8 new
App-layer fixture. App suite 5321/3 -> 5329/3, Runtime 1735/0
unchanged, zero regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 14:16:14 +02:00
Erik
e24ec20882 fix(chargen): Campaign CC gate round 1 Batch E — text origin, caption escapes, value rects, scrollbars, name prefill
R2-1/R2-6 (description-box text clipped left of the frame, regressed from
Batch C's frame un-consume): root cause was never the un-consume change
itself — the Heritage/Profession/Town/Summary description boxes
(0x100003C4/0x100003E0/0x10000409/0x10000404) all author retail's four
independent text-inset margins (dat properties 0x23-0x26,
UIElement_Text::OnSetAttribute cases 0xf-0x12: margL=9/margR=26/margU=15/
margD=15), which this codebase never read at all, before or after Batch C.
Un-consuming the gold-frame children just made the pre-existing missing-
margin bug visible for the first time (the frame's own left border now
draws around the same x=0 origin text always used). Fixed end to end:
ElementInfo.MarginLeft/Right/Top/Bottom (read in
ApplyCanonicalLegacyProjection, propagated in Merge), UiText.MarginLeft/
Right/Top/Bottom (additive with the pre-existing Padding), a new pure
UiText.ContentOffsetX static consumed by the multi-line draw path's
per-line placement, and matching wrap-width shrinkage in
DatRichText.Compose and BuildText's own authored-multiline path. Scoped to
the multi-line (non-OneLine) path only.

R2-2/R2-3 (Attribute\n Credits renders the literal backslash-n; the live
credit value overlaps mid-caption): two stacked gaps. (1) UiButton
captions never escape-normalized the DAT's literal "\n" — centralized the
normalize into DatWidgetFactory's ResolveAuthoredString (the one choke
point every P0x17 resolution already shares) plus a NormalizeEscapes
helper for the per-state caption loop, so every caller normalizes
identically. (2) UiButton.Label only ever drew one line — retail's
UIElement_Button IS a UIElement_Text with OneLine=false on these buttons,
so a caption should word-wrap/stack like any other Type-12 box. Added
UiButton.DrawBlockLabel + the pure, unit-tested WrapBlockLines. The
value-overlap itself: ValueBox was never wrong (live-DAT-measured correct
child rects) — the caption was drawing unconfined across the button's
full width ("Available Skill Credits" measures 193px in a 231px button
whose value box starts at x=116). Fixed by confining the caption's own
drawable width to stop before ValueBox.X whenever a ValueLabel coexists.

R2-7a (Summary overview listbox missing its scrollbar): pure wiring gap —
the listbox authors a linked scrollbar via dat property 0x72
(ScrollbarElementId=0x10000401) that CharacterCreationSummaryPage's
constructor never resolved, unlike every other UiTemplateListBox owner in
the codebase. Fixed with the same resolve-and-wire pattern.

R2-7b (how-to box scrollbar overlaps text, no thumb): traced to a
downstream symptom of R2-1, not an independent bug — UiScrollbar only
paints its thumb when the linked model has overflow, and the pre-fix wrap
width (un-inset) produced fewer/shorter lines than fit the view. Pinned
directly against the real installed strings/font (Aluvian's how-to text)
that the margin-correct width overflows. No UiScrollbar code changed.

R2-8 (name field should show "[ Name ]"): re-checked the one hypothesis
Batch A's GF-15 closure left open — an authored initial-text string on
the field's own P0x17. Confirmed absent on every state in the installed
DAT. No code change; Batch A's closure stands, now pinned as a live-DAT
regression test.

App suite 5334/3 (was 5321/3, +13, zero regressions). Runtime 1735/0
unchanged. Full solution Release build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 14:07:19 +02:00
Erik
2ad805469d docs: Campaign CC gate round 1 re-test findings R2-1..R2-8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 13:24:36 +02:00
Erik
63bf64c934 fix(chargen): Campaign CC gate round 1 Batch D — gmCG3DView environment backdrop
Retail's chargen 3D views (Appearance and Summary) are not black behind
the model: gmCG3DView::Update @0x004EE9D0 constructs a SECOND CPhysicsObj
from the current heritage's HeritageGroup_CG.environmentSetupID field
(acclient.h verbatim struct layout; the decompiler elides the actual field
read, but HeritageGroup_CG::GetSubDataIDs @0x005c05d0 explicitly walks
iconImage/setupID/environmentSetupID by name, confirming the identity) and
adds it to the SAME viewport's creature_mode_objects the player object
lives in, inserted BEFORE the player (whose own re-AddObject happens much
later, at ~0x004ef199, after the full clothing ObjDesc composes). The
backdrop gets no explicit position/orientation/scale — CPhysicsObj::
makeObject(eax_32, 0, 1) leaves it at the scene origin with identity
orientation, same as the player object's own placement. This id was
already parsed as ChargenHeritageOptions.EnvironmentSetupId
(ChargenTableReader.cs) but never consumed anywhere in production (GF-7/
GF-14).

Fixed by:
- ChargenPreviewEntityBuilder.TryBuildBackdrop: builds a plain, unposed
  Setup mesh from the heritage's EnvironmentSetupId, returning null for
  id 0/unset or an unresolvable Setup (retail's own INVALID_DID gate).
- PrivateEntityViewportRenderer: an optional second entity slot
  (SetBackdrop), reserved via a backdropRenderId constructor parameter so
  paperdoll and creature-appraisal — which never pass one — cannot
  acquire a second entity even by accident (SetBackdrop throws without a
  reserved slot). Per-entity mesh-reference/texture-owner lifetime is
  factored into a private EntitySlot helper shared by both the main and
  backdrop slots. Draw-entity assembly is a pure, directly-testable
  helper (BuildDrawEntities) that puts the backdrop first, matching
  retail's own AddObject insertion order.
- ChargenPreviewController.Rebuild: rebuilds the backdrop whenever the
  HERITAGE changes (narrower than the existing camera-eye-reset gate,
  since environmentSetupID is a pure function of heritage, never gender
  or appearance selection).

Both Appearance and Summary get the fix from the same ChargenPreviewRenderer
facade — confirmed both pages call the identical gmCG3DView::Update on
their own gmCG3DView instance, so no page-specific code was needed.
Lighting was independently re-verified against the same function's
SetLight call (DISTANT_LIGHT, intensity 2.0, direction (0.3, 1.9, 0.65),
default white color) and found to already match byte-for-byte what CC6a
shipped.

Also files docs/ISSUES.md #409 for GF-16 (client-wide UI tooltip system),
investigated in the same root-cause pass but explicitly out of this
batch's scope, and marks it DEFERRED in the findings doc.

Tests: 11 new/extended (ChargenPreviewEntityBuilderTests.TryBuildBackdrop_*,
ChargenPreviewControllerTests backdrop rebuild/swap/absent/no-op cases,
PrivateEntityViewportRendererDrawOrderTests pinning the paperdoll/creature-
appraisal single-entity invariant). Live-DAT measurement: all 13 retail
heritages' EnvironmentSetupId resolve to a real, drawable installed Setup.

App suite 5307/3 -> 5321/3 (+14, 0 regressions). Runtime 1735/0 unchanged.
Launcher.Core.Tests 337/0 and Launcher.Tests 67/0 unchanged (first build of
the merged tree carrying the #406 launcher merge). Full solution: 14508
total / 14504 passed / 4 skipped / 0 failed, dotnet test exit code 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 13:05:23 +02:00
Erik
2349f8b4df fix(chargen): Campaign CC gate round 1 Batch C — Summary how-to + scrollbar linkage
Commit 3/3: Summary how-to text + Commit 2's owed scrollbar linkage +
bookkeeping sweep.

- Ports gmCGSummaryPage::SetHowToText @0x0047ae20 into the Summary
  page's how-to box (0x10000404, HowToTextId was declared and unused
  since CC5). Retail concatenates ID_CharGen_SummaryHowTo + a heritage/
  gender-specific name-suggestion list (heritages 1-4 — Aluvian/
  Gharundim/Sho/Viamontian — only; heritages 5-13's cases in the same
  switch decompile to a vtable-slot artifact, the same decompiler-
  mangled-symbol class the Heritage page's own BonusSkillsKeyByHeritage
  table already documents, so no name-suggestion string exists for them
  and none is invented) + ID_CharGen_SummaryHowToEnd, directly
  concatenated (no separator literal) into ONE plain SetText call — no
  per-run font/color argument, unlike Heritage's ...WithFont calls, so
  this routes through DatRichText as a single DefaultColor segment.

- Wires the description boxes' linked scrollbar to actual text
  scrolling — Commit 2 made the scrollbar child (0x100002e7) BUILD as a
  real UiScrollbar; this binds scrollbar.Model = text.Scroll, the exact
  pattern ChatWindowController already uses for the chat transcript.
  Live-DAT-measured: only Heritage's description (0x100003c4) and
  Summary's how-to box (0x10000404) actually author this child —
  Profession/Town's shorter description boxes do not (a genuine retail
  authoring fact, not something to "fix" further).

Register: AP-215/AP-216/AP-217 rewritten (Batch C's Commit 1 already
retired AP-218/AD-103) — no further changes needed this commit; ISSUES
#366 (chat's new-unseen-text indicator, 0x1000048C under the chat
transcript 0x10000011) NARROWED — its own pre-filed "fix shape"
recommendation (a UiText child carve-out mirroring UiMeter's) is
EXACTLY what Commit 2 shipped, confirmed by that commit's own
client-wide sweep; #366 stays open for the still-missing behavioral
half (no controller drives the indicator's visibility/click).

Findings doc updated: GF-2/GF-3/GF-4/GF-6/GF-11a/GF-12/GF-14's text
half all marked FIXED with their own root-cause notes; the two
remaining "suspected shared roots" (frames/labels, rich text) marked
CONFIRMED + CLOSED.

Full App suite (Debug and Release, live-DAT): 5307 passed / 0 failed /
3 skipped (up from 5304 after Commit 2). Runtime suite: 1735/0,
unaffected.

Campaign CC gate round 1 Batch C is CODE-COMPLETE across all three
commits — GF-2, GF-3, GF-4, GF-6, GF-11a, GF-12, and GF-14's text half
are fixed; AP-216/AP-217 partially closed (register-honest about what
shipped vs what needs a palette-to-RGB pipeline this batch didn't add).
Pending the user's visual gate, with chat + the main game UI flagged
for extra attention (Commit 2's client-wide blast radius).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 12:34:38 +02:00
Erik
7d09821fdc fix(chargen): Campaign CC gate round 1 Batch B — authored selection states, label state, zoom/swatch feedback
GF-1/GF-8: UiButton now recognizes retail's custom Unselected/Selected
radio-pair (0x10000016/0x10000017), bypassing the standard Normal/
Highlight machine that never admitted those state names — .Selected now
lights the heritage/template/gender/Face-Clothes rows it was always a
no-op for.

AP-222/GF-11b: per-state label color/outline (dat 0x1B/0x21) now applies
off the REQUESTED retail state id, not the art-gated committed
ActiveState — resolves the Appearance spins' current-part highlight
(text recolors even though no Highlight art exists on either client) and
the Town caption's Normal-to-white swap.

GF-11c: UiButton.LabelBox lets a lifted caption with its own authored
rect draw there instead of the face-relative offset that's only correct
when the label is authored directly on the button (heritage/template
family, unchanged).

GF-9: wires the real nine companion overlay elements (SetColor's
SetVisible mechanism) that swatch clicks were always meant to drive,
retiring AP-215 item 1 (the swatch.Selected substitution was a permanent
no-op — swatches author no Highlight media at all).

GF-10: zoom buttons now set the retail-mirrored mutual-exclusive
Highlight/Normal pair on click; InitializePage carries no initial
SetState for either button, so both stay at "Normal" until first click.

Register: AP-222 retired (mechanism identified and ported), AP-215
narrowed (item 1 retired, item 2 unrelated and unchanged), row count
recount corrected 164 (was already one high before this batch).

App suite 5282/3 (was 5266/3), Runtime 1735/0 unchanged. Fixture + live-
DAT tests only — no graphical client launch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:37:09 +02:00
Erik
1d9de5e095 fix(chargen): Campaign CC gate round 1 Batch A — GF-15 input, GF-5 skills rows, GF-13 GM toggles
GF-15 (the gate blocker): the Summary name field and Finish button were
NOT structurally broken — live repro over the project's own local ACE
test server showed clicks correctly focus the field and land characters.
The real bug only surfaces after the first dialog opens: pressing Finish
empty successfully creates the NoName RetailMessageDialogView (visible,
correct 400x95 geometry) but it renders nothing and silently absorbs
every click across the whole canvas. Root cause: CharacterCreationUiController.Tick
and CharacterManagementUiController.Tick both call UiRoot.BringToFront(Root)
unconditionally every frame (needed so chargen stays above the occluded
management screen, AP-229); a dialog root is a direct sibling under the
same UiRoot, and RetailWindowManager.BringToFront is "highest ZOrder among
siblings + 1" — whichever BringToFront runs last in a frame wins.
RetailDialogFactory.Tick never re-asserted its own dialogs' z-order, so
the next frame's screen Tick buried the dialog behind the screen's opaque
backdrop while it stayed the registered Modal with exclusive input
priority. Fixed by having RetailDialogFactory.Tick re-raise every open
dialog (in open-order) each tick, matching retail's always-on-top dialog
behavior. Live-verified the complete user sequence end to end: click
field, type, press Finish empty, dialog now visibly renders, OK dismisses
cleanly, field still typable afterward. The "[ Name" prefill question is
closed as a non-bug: neither CharGenState::RandomizeCharacter nor
gmCGSummaryPage::InitializePage write text into the field in the decomp;
retail's field is genuinely empty on open, matching acdream already.

GF-5: CharacterCreationSkillsPage.RebuildRows resolved the wrong listbox
template (Templates[0], retail's own 3-child bucket-header row) and
required the root to be a UiButton (it's a plain container). Byte-traced
gmCGSkillsPage::DoSkillRecords + tagSkillRecord's copy-ctor field order
to map every child id in the real row (Templates[1]): name, level/cost
text, and the two real per-row up/down arrow buttons. Wired the arrows to
retail's own plain-click dispatch, retiring (narrowing) AP-213's
click-to-advance/double-click-retreat single-button substitution.

GF-13: dat property 0x3B (Invisible) was never read by the importer.
Elements 0x10000403/0x10000494 ("Non-Admin"/"Non-Envoy") author it true.
A blast-radius sweep found 1,083 elements client-wide author the same
flag, so this fix stays chargen-scoped only (ElementInfo.Invisible /
UiElement.AuthoredInvisible are pure data additions; only
CharacterCreationUiController acts on them, by the authored flag, not a
hardcoded id list). General importer-wide honor filed as ISSUES.md #408;
register row AP-230 records the split.

Gates: solution build green; App 5266/3 skips/0 failed; Runtime 1735/0;
full-solution run 0 failures anywhere. Register: AP-230 filed, AP-213
narrowed. ISSUES: #408 filed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:54:41 +02:00
Erik
6699e0f88c docs: Campaign CC gate round 1 — the six-page findings batch GF-1..GF-16
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 09:27:58 +02:00
Erik
2176ba768e fix(chargen): Campaign CC CC7 review fix round — F1-F9 — REVIEW-CLOSED
Both dual-lens reviewers of `9cf6c522`+`ddcbf1fb` returned PASS-with-items.
This round closes all nine findings:

F1 files AP-229 for the screen-layering divergence (retail destroys/
reconstructs the current UI framework via UIFlow::UseNewMode; acdream
keeps both CharacterManagementUiController and CharacterCreationUiController
mounted for the whole lifetime and reveals/occludes) plus its narrow
residual risk (the shared RetailDialogFactory can hand UiRoot.Modal to a
dialog opened by the still-ticking, occluded management screen on an
inbound CharacterError) and what already matches retail (selection/
world-name persistence, click-through isolation, one coherent Modal
stack).

F2 rewrites the connected-gate script's roster-full step with the exact
`@modifylong max_chars_per_account` recipe and the pending-delete-counts
note. F3 adds AP-221's console-diagnostic lines to the known-gaps
paragraph. F4 adds an empty-name/AP-227 step. F9 notes that a uniform
Random pick over 13 heritages can repeat.

F5 adds an App-layer source-text pin
(GameWindowLiveSessionOwnershipTests.LiveSessionRuntimeFactoryBinds
CharacterCreatedAndCreationFailedToTheStatusWriter) for the delegate
wiring the reviewer proved was deletable without breaking any test — no
practical seam exists to construct LiveSessionRuntimeFactory without a
GameWindow, so this follows the file's own established source-text-pin
pattern; the payload shape is already pinned separately at
SessionStatusWriterTests.

F6 corrects the CC7 ledger's checksum-assertion wording (it is a
round-trip purity check, not an independent golden — the golden is
CharacterCreateTests.ComputeChecksum_ExactRetailAccumulationSet) and
cross-references it from the test's own doc comment.

F7 corrects the CC7 ledger's fixture-ordering claim (it had chargen
constructing first, backwards from RetailUiRuntime.Tick's real
management-then-chargen order) and reorders CharacterScreensFixedCanvas
ArbiterTests to match production, adding ClickThrough/ZOrder assertions
that pin the occlusion the reviewer previously verified only by hand.

F8 records a known flake (RuntimeCollisionReportingStateTests.
WarmedSteadyContactRefreshDoesNotAllocate, allocation-assertion load
sensitivity, pre-existing) seen under full-solution parallel load on
both reviewer runs.

Campaign status: all seven slices (CC1-CC7) are REVIEW-CLOSED; the
campaign is CODE-COMPLETE pending the user's own connected gate.

Runtime 1735/0 (unchanged), App 5257/3 skips (+1: the new F5 pin).
Full Release build: 0 warnings, 0 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 03:13:02 +02:00
Erik
9cf6c52283 feat(chargen): Campaign CC slice CC7 — end-to-end create flow + connected checklist
Create button un-ghosts: retail's exact gate (gmCharacterManagementUI::
UpdateButtons @0x004ec240, roster count < allowed slot count) ported into
RuntimeCharacterSelectionButtons.CanCreate; the button's OnClick opens the
chargen screen through the same CharacterCreationUiController.Open() seam
the ACDREAM_OPEN_CHARGEN=1 dev path already used. Exit/Back confirm on
chargen needed no new return-path code — character-management is never
hidden while chargen is open on top of it — verified end-to-end by a new
cross-controller test rather than left as an inspection claim.

Full-flow test coverage: a new comprehensive test decodes every 0xF656
field (including the trailing checksum, recomputed via the production
CharacterCreate.ComputeChecksum) against a fully populated creation
(heritage/gender/all appearance slots/template/explicit skill command/
town/name); a new Theory drives the remaining six 0xF643 rejection codes
through the real wire decode path, closing the gap between the
already-covered isolated state-machine Theory and an actual WorldSession
round trip.

Launcher payload cycle: two new tests drive a real Runtime create/reject
through the real SessionStatusWriter (wired exactly as
LiveSessionRuntimeFactory/HeadlessSessionHost do in production) and read
the result back with the real Launcher.Core StatusFileTailer/
StatusEventParser — closing the one gap CC2's own per-layer tests never
reached. No gap was found in production wiring itself: GameWindow already
constructs a real, non-null SessionStatusWriter for both hosts.

Also fixes 4 pre-existing LiveSessionControllerTests assertions that
compared a full RuntimeCharacterSelectionButtons record and would have
failed once CanCreate started being computed; corrects register row
AP-211 to reflect that its own predicted resolution (the Create-button
gate landing) has now happened — both layers are intentionally kept as
retail-matching enforcement plus defense-in-depth, not one superseding
the other.

Adds docs/research/2026-08-16-campaign-cc-test-script.md, the user's
connected-gate script covering both the launcher and dev-shortcut launch
paths, the six-page create flow, every Finish outcome, and the known
cosmetic/behavioral divergences (AP-212/213/215/216/217/218/219/220/222/
224/226/228) so they aren't mistaken for new bugs during the gate.

Gates: full solution Release build green; Runtime 1735/0 (was 1726/0,
+9), App 5256/3 skips (was 5254/3, +2), Headless 166/0 (unchanged),
Launcher.Core 324/0, one full-solution pass across every project clean
(no known flakes reproduced this run).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 02:35:21 +02:00
Erik
d39f3098d5 merge: Campaign LA LA11 - automated closeout review-closed
# Conflicts:
#	docs/plans/2026-08-14-launcher-campaign.md
2026-08-15 02:07:39 +02:00
Erik
9f9c116792 fix(launcher): harden LA11 gate evidence 2026-08-15 01:45:15 +02:00
Erik
accd01a008 fix(launcher): harden Campaign LA11 gate evidence 2026-08-15 01:08:41 +02:00