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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>