CC4 re-review returned NOT CLOSED: R1 (MEDIUM, blocking) is a new residual the F1 fix itself introduced, plus three LOW riders (R2, R3, R4). R1 — nulling UiRoot.FixedCanvasSize on chargen Close() stripped it from character-management, which stays active underneath and only sets the canvas on its own activation edge. Root cause (reviewer-named): two controllers writing one host-global with no owner. Fixed with the root-cause shape (reviewer's option (c)): UiRoot.DeclareFixedCanvas(owner, size)/RevokeFixedCanvas(owner), an owner-scoped arbiter — every declarer must agree on the canvas size (a mismatch throws instead of silently last-writer-wins), and the canvas nulls only once EVERY declarer has revoked. Both CharacterCreationUiController and CharacterManagementUi- Controller now declare/revoke instead of writing FixedCanvasSize directly; grepped for stragglers, none remain in production code (the raw setter stays public only for UiRootFixedCanvasTests' isolated scale-math coverage). New test (reviewer-specified): CharacterScreensFixedCanvasArbiterTests — two controllers sharing one UiRoot, proving the canvas stays set through chargen's Exit-confirm Close while char-management is still active, nulling only once char-management also deactivates, plus the original F1 defect's own covering case (both revoke together at world entry). R3 — HeadlessSessionHostTests.ContentLease_InstallsRealChargenOptions_ SelectHeritageIsAccepted proves F6's install actually opens the gate: a content lease carrying a real hand-built DatCharGen heritage (not ChargenOptions.Empty) is installed, and TrySelectHeritage for it succeeds. R2 — filed docs/ISSUES.md #402 for the pre-existing Streaming.LandblockBuildFactoryTests.Build_UsesTheSuppliedSharedReaderGate full-suite flake (unrelated to Campaign CC). R4 — fixed "unchached" -> "uncached" typo in InteractionRetainedUiComposition.cs. Runtime 1713/0, App 5127/13 skips (+2), Headless 166/0 (+1), full solution Release build green. Live-DAT probes 7/7 under ACDREAM_PROBE_LIVE_MOUNT=1. The known #402 flake did not fire across 3 consecutive full-suite runs this session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
48 KiB
Campaign CC — retail character creation
Status: ACTIVE (started 2026-08-15)
Goal (user-set): the full retail creation flow against local ACE — Create
button through a new character entering the world, 3D preview live, rejections
showing retail's dialogs — then stop for the user gate.
Branch: claude/acdream-launcher-credentials-4d2f7c
Process: Campaign LA's, binding (Sonnet implements, Opus dual-lens reviews
per slice, retail decomp is the oracle, register rows with deviations,
build+test green per slice, commits tagged Campaign CC).
This plan embeds the 2026-08-15 recon facts (three parallel sweeps: retail
gmCG UI, chargen data+wire, acdream seams) so slices and future sessions need
no transcript access. references/ACE and references/holtburger are NOT in
this worktree (gitignored) — read them from the main checkout at
C:\Users\erikn\source\repos\acdream\references\.
Retail ground truth (recon summary — cite these in code)
Flow. Create button (0x100003A0) → QueueUIMode(0x1000000b) →
gmCharGenMainUI (acclient.h:56232): ONE root layout, enum 0x10000039 via
GetDIDByEnum table 5 (our generic RetailDataIdResolver handles this), pages
as children. ECGProgress: Heritage=1 → Profession=2 → Skills=3 →
Appearance=4 → Town=5 → Summary=6. Nav dispatch
gmCharGenMainUI::ListenToElementMessage@237025: Back 0x100003c6 (at
Heritage → DoExit), Next 0x100003c7, Finish 0x100003c8 (Summary only),
Help 0x100003c9, Exit 0x100003ca (→ ID_CharGen_ExitWarning confirm),
Random 0x100003cb (on Summary → randomize warning first). Tab buttons
0x100003ef..f4 jump pages freely (not validation-gated). Page roots:
Heritage 0x100003d1, Profession 0x100003d2, Skills 0x100003d3,
Appearance 0x100003d4, Town 0x100003d5, Summary 0x100003d6; progress
bar 0x100003ce, master page 0x100003d0. Per-page child ids are in the
recon-cited ctors: Heritage InitializePage@143731 (13 race buttons + text
0x100003c4), Profession @143010 (6 attribute sliders 0x100003e6..eb,
avail/health/stam/mana 0x100003e2..e5, template buttons resolved in
UpdateProfession@142180: Custom 0x100003d9, Bowhunter/Swashbuckler/
Lifecaster/Warmage/Wayfarer/Soldier 0x100003da..df), Skills @141911
(listbox 0x100003f7, credits 0x100002f3, info 0x100003fb/fc),
Appearance @140032 (gender 0x100003a7/a8, spins hair/eyes/nose/mouth/skin
0x100003af..b3, headgear/shirt/trousers/footwear 0x100003b5..b8, zoom
0x10000325/26, rotate 0x10000323/24, color wheel family
0x1000030e..0x10000321, viewport 0x100003bb), Town @137120 (Sanamar
0x1000040b, Holtburg 0x1000040d, Yaraq 0x1000040e, Shoushi
0x1000040f), Summary @136566 (list 0x10000400, name text 0x10000402
with NameInputFilter, viewport 0x10000406).
CharGenState (acclient.h:40074): the model our Runtime owner mirrors — heritage/gender, appearance strips+styles+colors+shades (f64 shades), template + 6 attributes + credit budgets + per-attribute locks, 55-slot skill advancement array + skill credits, name[33], startArea, setupID, verificationState. Writers per page in the recon (SetHeritageGroup recomputes budgets + ApplyTemplate + RandomizeStartArea; SetGender reapplies clothing and UpdateTrueFacePal).
Finish (DoFinish(this, arg2)@236864): trim+set name → empty name →
ID_CharGen_NoNameWarning, abort. CORRECTED at the CC3 review-fix round
(F3) — the original line here (remainingAtrbCredits > 0 → abort, "retail
FORCES full spend") was WRONG; retail does NOT force a full spend. The
real gate is arg2 != 0 && remainingAtrbCredits > 0: the ordinary
Finish-button click passes arg2 = 1 (@0x004E9579), and on unspent
credits shows MakeCreditWarningDialog and returns WITHOUT sending
(@0x004E91F2-0x004E9210) — but that dialog's own confirm handler
re-invokes DoFinish(this, 0) (@0x004E98BB), which SKIPS the credit check
entirely (arg2 == 0) and sends with the credits still unspent. ACE
accepts this — ValidateAttributeCredits only rejects a total that
EXCEEDS the max, never an under-spend. Then: verification state must be
UNDEF (no double submit) → set PENDING → Proto_UI::SendCharGenResult@0x00546A70.
Wire 0xF656 (ACCharGenResult::CG_Pack@0x005C7200, byte-identical to
ACE's CharacterCreateInfo.Unpack): account String16L FIRST (outside the
body), then u32 constant 1, u32 heritage, u32 gender, u32×3 eyes/nose/mouth
strips, u32×2 hairColor/eyeColor, u32 hairStyle, u32×2 headgearStyle/Color,
u32×2 shirt, u32×2 trousers, u32×2 footwear, f64×6 skin/hair/headgear/shirt/
trousers/footwear shades, u32 templateNum, u32×6 attributes
(str/end/coord/quick/focus/self), u32 slot, u32 classID, u32 numSkills +
numSkills×u32 advancement classes (MUST be exactly 55 — ACE TERMINATES the
session on mismatch), String16L name, u32 startArea, u32 isAdmin, u32
isEnvoy(=ACE IsSentinel), u32 trailing checksum = sum of
heritage+gender+strips(3)+hairColor+eyeColor+hairStyle+headgearStyle+
shirtStyle+trousersStyle+footwearStyle+template+6 attributes (ACE never
reads it; we send it for byte fidelity). holtburger cross-check:
character/types.rs:236 (stops before the checksum).
Response 0xF643 (shared opcode with restore — LA7a's conditional parse is
reusable): codes Undef=0 Ok=1 Pending=2 NameInUse=3 NameBanned=4 Corrupt=5
DatabaseDown=6 AdminPrivilegeDenied=7. On Ok the payload is a
CharacterIdentity (guid, String16L name, u32 secondsGreyedOut) and NOBODY
sends a fresh CharacterList — retail appends the identity to its local
roster (Handle_CharGenVerificationResponse@0x0055E8B0 case 1 →
CharacterSet::AddIdentity) and gmCharGenMainUI::Update@236161 then
watches the set and calls CPlayerSystem::LogOnCharacter DIRECTLY when the
new name appears (logs straight in; only falls back to char management if it
never appears). Error dialogs: NameInUse→ID_Character_Err_NameReserved,
NameBanned→ID_Character_Err_NameBanned, Corrupt/DatabaseDown→
ID_Character_Err_NameDBDown, AdminPrivilegeDenied→
ID_Character_Err_NameAdminDenied, Pending/Undef→silent state reset (ACE
sends Pending for a disabled-Olthoi rejection — retail swallows it; port
as-is, register-note the quirk).
Chargen DAT table 0x0E000002: readable TODAY via the
Chorizite.DatReaderWriter package (dats.Get<CharGen>) — zero in-tree
readers exist. ACE loaders (ACE.DatLoader.FileTypes.CharGen +
HeritageGroupCG/SexCG/TemplateCG) and retail serializers
(ACCharGenData::Serialize@0x005C36D0, HeritageGroup_CG@0x005C2100,
Sex_CG@0x005C1600, Template_CG@0x005C0450) define the shape: per
heritage → name/icon/setup/EnvironmentSetup/attribute+skill credits/start
areas/skills(costs)/templates(attrs+skills)/genders; per sex → scale, setup,
base palette, skin palset, base ObjDesc, and the option LISTS (hair styles/
colors, eye colors, eye/nose/mouth strips, headgear/shirt/pants/footwear,
clothing colors).
3D preview (gmCG3DView, Appearance 0x100003bb + Summary 0x10000406
ONLY — the other four pages have no viewport): preview body
CPhysicsObj::makeObject(setupId) (fallback HUMAN_SETUP_ID), rebuild on
change via ObjDesc (ClothingTable::BuildObjDesc per clothing slot + strips
- PalSet skin/hair/eye subpalettes) applied with
DoObjDescChangesFromDefault@242308, one DISTANT_LIGHT (intensity 2.0), idle animation loop at 30fps (set_sequence_animation), rest-pose freeze on zoom-in, BUTTON-toggled continuous rotation (DoRotation@137337, 3.0 s/revolution, per-frame global-message-3 tick), zoom tween between per-heritage camera positions (Update@138974hard-codes Olthoi vs human-form camera offsets).
acdream seams (build on these, do not reinvent)
- Layout mount:
RetailDataIdResolver.Resolve(dats, 0x10000039, 5)+LayoutImporter— fully generic.DatWidgetFactoryalready maps dat type 0xD →UiViewport. The char-management controller REFUSES viewports by local policy (:212) — chargen gets its OWN controller; cloneCharacterManagementUiMountCoordinator+ the bindings-record pattern. - Fixed canvas: chargen is the same 800×600 flow screen — mount at authored
extent,
UiRoot.FixedCanvasSizeon activate (AD-98), dialogs center onEffectiveCanvasSize. Live-DAT probe tests sweep ALL media ids (CharacterManagementLiveDatTestspattern) and pin authored justify/anchors. - Preview pipeline:
PrivateEntityViewportRenderer(offscreen target → texture table →UiViewportsprite) is proven by paperdoll + appraisal; cameras there are FIXED — chargen needs a heading-capable camera. NOTE:GlGpuDevice.RegisterExternalColorTextureis a DELETED API that survives only in stale doc comments — do not cite it. Appearance building:DollEntityBuilder.Buildis index-agnostic and pure (setup + resolved palette/part ids), but the only existing factory reads a LIVE entity — chargen needs a new index→dat→ObjDesc factory (SexCG.BaseObjDesc + strip overlays + PalSet.GetPaletteID hues). Pose: paperdoll holds a static final frame; retail chargen plays a live idle loop — see slice CC6 for the staged approach. - Runtime owner: mirror
RuntimeCharacterSelectionStateexactly (lifecycle/ snapshot/delta records, borrow-only view, generation-gated commands, one mutable owner, no App types). Command family lands besideIGameRuntimeCommands.CharacterSelection. Enter-after-create hooks the existingLiveSessionController.BeginEnter/CompleteEnter. - Wire plumbing:
WorldSession's dispatch chain routes EVERY 0xF643 throughCharacterRestore.Parsetoday with no request correlation — the KNOWN LANDMINE. Creation requires an awaiting-request latch (create vs restore) BEFORE its response arm lands. Outbound mirrorsSendRestoreCharacter@2223. Status writer: addcharacterCreated/creationFailedevents (update the pinned §LA1 contract text + the Launcher.Core tailer + tests in lockstep).
Slices
| Slice | Deliverable | Depends |
|---|---|---|
| CC1 | Chargen data layer: CharGen table reader → typed options model (heritages/sexes/appearance lists/templates/skills+costs/budgets/towns), Content/Core, live-DAT probes |
— |
| CC2 | Wire: CharacterCreate 0xF656 builder (byte-exact incl. checksum), shared verification-response type (refactor from CharacterRestore), WorldSession request-correlation for 0xF643, send seam, status events + contract/tailer update |
— |
| CC3 | RuntimeCharacterCreationState: full CharGenState mirror, per-page commands, retail client gates (full-spend, name, 55-slot invariant, client-side slot cap), verification latch, Ok → roster append + retail log-straight-in |
CC1, CC2 |
| CC4 | Screen shell + form pages (App): mount (enum 0x10000039), master nav/tabs/progress, dialogs, Heritage + Profession + Skills + Town pages | CC1, CC3 |
| CC5 | Summary page: name input (NameInputFilter, ID_CharGen_NameTooLong), summary listbox, static summary viewport, Finish gates + full response/dialog handling |
CC3, CC4 |
| CC6 | Appearance page + preview: index→ObjDesc factory, chargen preview renderer (offscreen, heading camera, rotate/zoom buttons), spin controls + color wheels; staged: CC6a static-pose preview (paperdoll-style held frame, register row for the missing idle loop), CC6b idle animation + zoom rest-freeze (retire the row) | CC1, CC4 |
| CC7 | End-to-end: Create button un-ghosts, full flow vs ACE shapes in tests, launcher payload cycle, connected checklist doc | all |
Parallelism: CC1 ∥ CC2 (disjoint: Content/Core vs Core.Net; separate worktrees). CC4 ∥ CC6a after CC3. CC5 last before CC7.
Risks / open items (from recon Unknowns)
- 0xF643 create/restore correlation (CC2's first job; the restore doc comment already warns).
- 55-slot skill array: ACE terminates the session on mismatch — CC2/CC3 must make it structurally impossible to send anything else.
- Slot cap is client-enforced only (ACE never checks on create) — honor
slotCountlike retail's UI did. - Color-wheel/gradient widgets (
tagColorWheel, GradCircle0x1000030e, shade scroll) may need new widget types inDatWidgetFactory— CC6 scouts the authored layout first. - Retail unknowns to resolve during slices, never guess: the chargen
please-wait dialog context (decompiler-mislabeled field), the
AppearancePage gender-flip-on-init oddity (@140355 — verify live before
porting),
Method_CGenums are empty in the header, ZoomIn tween duration constant is decompiler-garbled (measure against retail if it matters). - Viewport inside the fixed canvas: the offscreen target's pixel size vs the canvas-scaled on-screen rect (render at scaled size for crispness or authored size for fidelity) — decide in CC6a with the user gate as arbiter.
references/*absent in worktrees (except WorldBuilder, uninitialized submodule) — agents read ACE/holtburger from the MAIN checkout path.- CC7 landmine (found in the CC1 review fix round, 2026-08-15): ACE's
PlayerFactory.CreatePlayerheritage-override branch (references/ACE/Source/ACE.Server/Factories/PlayerFactory.cs:184-211) over-deducts skill credits when specializing a skill the active heritage's own list prices. For a skill priced ONLY by the global SkillTable, ACE correctly computes the incremental specialize cost viaSkillBase.UpgradeCostFromTrainedToSpecialized(=SpecializedCost - TrainedCost) and chargesTrainSkill(trainedCost) + SpecializeSkill(incrementalCost)= the field's TOTAL, matching retail. But when the heritage's own list has an entry, ACE setsspecializedCost = skillGroup.PrimaryCostdirectly —PrimaryCostis already the TOTAL cost to reach Specialized (acdream's ownChargenSkillCost.PrimaryCostconvention, confirmed against retail) — and then still chargesTrainSkill(NormalCost) + SpecializeSkill(PrimaryCost), over-deducting by an extraNormalCostcredits versus what retail's client computed and what the player agreed to spend. Practical impact for CC7's connected gate: a retail-legal character build that specializes a skill the ACTIVE HERITAGE prices (every one of the 13 installed heritages has exactly one such skill — seeChargenTableReaderInstalledDatTests.InstalledHeritages_SkillCostFallbackCoversTheKnownUncostableSkillSet) may be REJECTED by local ACE withFailedToSpecializeSkilleven though acdream sent the byte-correct 0xF656 body. If CC7's gate hits this, it is an ACE-side bug reproduced from its own source, NOT an acdream wire or math defect — do not "fix" acdream's cost math to match ACE's over-deduction. MEASURED 2026-08-15 (user-prompted — downgrades this landmine to LATENT): dumping the installed EoR DAT shows every one of the 13 heritages' single override is skill 14 (Arcane Lore) at NormalCost=0 / PrimaryCost=2, versus global TrainedCost=4 / SpecializedCost=6. ACE's over-deduction equals NormalCost — which is ZERO for the only heritage-priced skill — so ACE charges 0+2=2 and retail's client computes 2: they AGREE, and no character build can trigger the rejection with end-of-retail data. The formula bug in ACE's heritage-override branch is real but unfireable here; it only matters if a custom server ships a DAT whose heritage override has a nonzero NormalCost. The earlier "may be REJECTED" inference was made from code without measuring the data — the C4 closeout's observe-don't-infer lesson, again. Register: file an AD row if CC7 needs a documented workaround (e.g. picking a Specialized skill combination that avoids the heritage-priced skill for the connected gate) rather than silently adjusting acdream's send.
Review protocol
Per slice: implement → Opus dual-lens (architectural + retail fidelity — this campaign is retail-heavy everywhere) → fixes → narrow re-review → DONE in ledger. CC2's review adds wire-byte scrutiny (the LA7a precedent: the reviewer decodes the binary); CC6's adds the visual-fidelity lens ahead of the user gate.
Ledger
| Slice | Status | Commits | Review | Notes |
|---|---|---|---|---|
| CC1 | REVIEW-CLOSED 2026-08-15 | 04450041, cb4703e8 |
CLOSED (fix round + narrow re-review; every citation independently re-derived) | Core model (no Chorizite leak) + Content projector; 31 math units + 6 installed-DAT gates (13 heritages). FINDING for CC3: each human heritage's "Adventurer" template IS retail's Custom entry point — attributes at the 10-floor (60/330), a real TemplateCG row, not a UI special case. Review fix round (cb4703e8): F1 doc corrected — Custom IS template index 0 (the Adventurer row), per gmCGProfessionPage::UpdateProfession @ 0x004821b0 (case 0 → button 0x100003d9 / ID_CharGen_CustomText) and CharGenState::SetTemplate @ 0x005C5A60 (commits via CharGenState::ApplyTemplate @ 0x005C5080, i.e. selecting Custom resets sliders to the floor spread, it does not bypass templates); F2 two-tier skill-cost fallback implemented (ChargenOptions.GlobalSkillCostsBySkillId from portal.dat 0x0E000004, ChargenSkillCreditMath checks heritage list then global list) + installed-DAT completeness assertion recording reality: the global SkillTable prices 38/54 advancement skill ids, every one of the 13 heritages ships EXACTLY one heritage-specific override (always also present in the global table), and 16 skill ids are genuinely uncostable in both tiers (retail's -1 case) — see ChargenTableReaderInstalledDatTests.InstalledHeritages_SkillCostFallbackCoversTheKnownUncostableSkillSet; F3 every ChargenTableReader collection is now frozen at projection (ToFrozenDictionary/ToArray, matching MagicCatalog's pattern) including both ChargenOptions.Empty dictionaries; F4 a reflection guard test (ChargenNoChoriziteLeakTests) pins the no-Chorizite-leak contract by walking every public AcDream.Core.CharGen member; F5 HasAnyAppearanceOptions's doc reworded to state precisely what it proves (an OR across eight lists, omitting the three color lists) + a new installed-DAT gate records per-list reality — found COMPLETE, every gender of every heritage has non-empty lists across all eight plus the three color lists, even the sparse Gear Knight/Olthoi variants; F6 TryGetHeritage/TryGetStarterArea annotated [MaybeNullWhen(false)] (matching the house EmptyDatReaderWriter pattern), all affected call sites (more than the originally estimated five) fixed across both test projects. Filed CC7 risk item 8: ACE's PlayerFactory heritage-override branch over-deducts skill credits when specializing a heritage-priced skill (references/ACE/Source/ACE.Server/Factories/PlayerFactory.cs:184-211) — a retail-legal build may be rejected by local ACE at the CC7 connected gate; this is an ACE bug, not an acdream defect. Narrow re-review CLOSED: the reviewer retro-graded F2 to HIGH (under the base commit 37 of 38 costable skills were charged zero) and confirmed the SkillBase.SpecializedCost->PrimaryCost mapping dodged the UpgradeCostFromTrainedToSpecialized trap. Residuals: R1 retail refunds +1 credit on a both-tier miss (port charges 0; unreachable via retail’s own skills listbox — NOTE FOR CC3 if any path ever exposes the 16 uncostable ids); R2 list downcast-mutability and R3 field-walking in the leak guard CLOSED at the merge-closeout commit (Array.AsReadOnly at every projection seam; GetFields walk added). Decomp fact for CC4: ApplyTemplate force-sets template_=0 for heritage 0xc/0xd — both Olthoi variants are hard-locked to Custom/template 0. |
| CC2 | REVIEW-CLOSED, MERGED 2026-08-15 (55fc51ed) |
5eaad2c8, e77ebf10, 95e95bb6 |
PASS then CLOSED (fix round: F1 latch-scope narrowing + overwrite pin test, F2 register AD-100, F3 ACE double-NameInUse note, F4 creationFailed{code,reason,name}, F5 pointer, retail-discriminator citations) | Byte-exact 0xF656 (19-term checksum vs CG_Pack accumulator), shared 0xF643 type, correlation latch, status events + contract amendment. Core.Net 993 / Runtime 1667 / Launcher.Core 323, Windows+WSL |
| CC3 | REVIEW-CLOSED 2026-08-15 | 9a84230c, 397ccd62, + the R1 closeout commit |
CLOSED (dual-lens: retail fidelity PASS, architectural FAIL → F1-F16 fix round 397ccd62 → narrow re-review CLOSED, both lenses PASS. Re-review residual R1 — the cached wire count is stale by creates-since-last-CharacterList, so a SECOND create after a rejected enter got wire slot N instead of N+1 — fixed in the closeout commit: LiveSessionController._createsSinceCharacterList (reset on every fresh wire CharacterList apply + generation reset; applied only to the cached-wire branch — the display-roster fallback already counts prior appends), regression test SecondCreate_AfterRejectedEnter_GetsTheNextWireSlot drives create→Ok→rejected guid-enter→ReturnToSelection→second create and pins slots 0/1/2/3. R2: fix-round sha recorded here.) |
RuntimeCharacterCreationState (new, src/AcDream.Runtime/Session/): full CharGenState mirror (heritage/gender/appearance/template/six attributes+locks/55-slot skill set/name/startArea/slot/verification state), mirroring RuntimeCharacterSelectionState's exact pattern (snapshot/delta/event-stream/borrow-only view, generation-gated Try* internals). Ports SetHeritageGroup, SetGender, SetTemplate/ApplyTemplate (Custom = template 0, Olthoi force-lock), the six attribute setters + GetAbsRemainingCredits + BalanceAttributes (retail's literal str/end/coord/quick/focus/self round-robin order, cursor-based fairness), SetSkillLevel + ResetSkillLevels' three-way free-skill baseline (both two-tier cost lookups reuse CC1's ChargenSkillCreditMath/ChargenSkillCost verbatim — no duplicated math), RandomizeStartArea, and DoFinish's complete gate sequence (empty name / unspent attribute credits [see F3 below] / already-Pending / client-side roster-vs-slotCount cap). LiveSessionController gained a sibling IRuntimeCharacterCreationCommands implementation (command family lands beside IRuntimeCharacterSelectionCommands, IGameRuntimeCommands.CharacterCreation added with the same default-throw shape as CharacterSelection), a CharacterCreationState property, ILiveSessionOperations.CreateCharacter (default method → WorldSession.SendCharacterCreation), and a HandleCharacterCreationResponse wire handler subscribed to WorldSession.CharacterCreateResponseReceived alongside the existing character-selection bindings. ILiveSessionLifecycleHost gained ApplyCharacterCreated/ApplyCreationFailed as DEFAULT interface methods (no-op) so AcDream.App's existing host implementations keep compiling unchanged — wiring them to SessionStatusWriter.CharacterCreated/CreationFailed is left to CC4 (Runtime calls the hooks; the App-side forward is a future host-construction change; F14: zero production call sites exist for these hooks until then — a headless bot cannot observe a create yet). Review fix round (this commit): F1 (HIGH, blocking) the post-create log-straight-in no longer enters by roster INDEX — WorldSession gained a guid-based EnterWorld(uint characterGuid, string accountName, TimeSpan?) overload (refactored to share EnterWorldCore with the index-based overload) plus ILiveSessionOperations.EnterWorldByGuid (default method); LiveSessionController factored EnterSelectedCore/the new EnterCreatedCharacterCore through a shared EnterHighlightedCore(sendEnterWorld) — the cached wire CharacterList is stale for a just-created character by ACE design (ACE appends server-side and replies Ok with no CharacterList resend — references/ACE/.../CharacterHandler.cs:170-172), so an index-derived enter could throw (0 pre-existing characters) or enter the WRONG character (N pre-existing, display order ≠ wire order). F2 (HIGH, blocking) the post-create roster append no longer round-trips through ApplyRoster (which re-derives EVERY entry's ActiveIndex — a wire contract ACE indexes for delete, CharacterHandler.cs:297 — from display/name-sort order); RuntimeCharacterSelectionState gained a real AppendCreatedCharacter(characterId, name, wireIndex) primitive that preserves every existing entry's ActiveIndex untouched and assigns the new entry's from the pre-create wire CharacterList.Characters.Count (0-based, read from the same cached source the index-enter path uses). F3 (MEDIUM-HIGH, blocking) the credit gate was NOT retail — DoFinish(this, arg2)'s real gate is arg2 != 0 && remainingAtrbCredits > 0: the ordinary click (arg2=1) warns-and-refuses, but the warning dialog's own confirm re-invokes DoFinish(this, 0), which skips the check and sends with credits unspent (ACE accepts this). TryBeginFinish/LiveSessionController.Finish/IRuntimeCharacterCreationCommands.Finish gained a confirmedUnspentCredits/confirmUnspentCredits parameter (default false = retail's arg2=1) — the plan doc's own "retail FORCES full spend" line above (§Retail ground truth, Finish) was corrected in the same round. F4 (MEDIUM, blocking) a stale out-of-range template index surviving a heritage switch to a heritage with fewer templates now clears to TemplateUnset in ApplyTemplateLocked, mirroring ConstrainAllByHeritage @ 0x005C65CC's template_ >= count → template_ = 0xffffffff clamp (previously it just returned, leaving the stale index to reach the wire). F5 (MEDIUM) AP-207's anchor was wrong (SetAttribValue never calls FitTemplateToCharacter) — corrected to the four real call sites, including a fourth the original filing also missed (UpdateToDefaultAttributes @ 0x00482860). F6 (MEDIUM) ApplyCreationResponse's Pending/Undef branch no longer publishes from inside lock(_gate) — every branch now sets kind and a single Publish runs after the lock releases, matching every sibling method. F7 (MEDIUM) two new tests pin BalanceAttributes' persistent cursor: successive overspends absorb from different attributes, and the Self→Strength wrap. F8 (LOW) ResetSkillLevels' doc corrected — retail's real gate is BOTH costs >= 0 (not "either tier"); the dictionary-presence equivalence is a CC1-established, installed-DAT-gated invariant, cited precisely. F9 (LOW) the Slot doc corrected — retail DOES assign it (gmCharacterManagementUI::SelectCharacter @ 0x004EC160 → SetSlot(GetSlot(...))), just semantically stale (the last-selected PRE-EXISTING character's slot); conclusion (send 0) unchanged. F10 (LOW) AP-209's classID citation completed with the three heritage-dependent branch ids (ordinary/Olthoi/OlthoiAcid) plus admin variants. F11 the integration test fixture no longer stubs EnterWorld to a bare counter — it captures guid-based calls and the fixture now has two pre-existing characters whose wire order deliberately differs from alphabetical order, so the roster-preservation assertion actually exercises F2 instead of coinciding with it by accident. F12 filed register row AP-211 for the client-side RosterFull slot-cap refusal (acdream-side gate, no retail DoFinish-layer counterpart — same-commit rule). F13 LiveSessionController.Finish's bare catch {} narrowed to InvalidOperationException/SocketException and _scope bound to a local after validation. F15 RandomizeStartAreaLocked now leaves _startArea unchanged on an empty list (matching retail's if (var_9c > 0) guard) instead of forcing -1. Filed register rows AP-207 (FitTemplateToCharacter's FPU-unrecoverable auto-detect skipped — ACE only reads TemplateOption for title text; anchor corrected this round), AP-208 (per-style color-count approximated by the shared gender-wide ClothingColors list — CC1's model has no per-style palette data), AP-209 (classID sent as a placeholder 0 — DAT DID lookup unavailable in Core, ACE ignores the field; branch table added this round), AP-210 (ApplyTemplate's per-attribute guarded sequential set approximated as one atomic replace), AP-211 (this round — the RosterFull client-side slot-cap refusal). Tests: tests/AcDream.Runtime.Tests/CharGen/RuntimeCharacterCreationStateTests.cs (34 cases — every Finish gate including the F3 confirmed-credits path, the F4 stale-template clamp, the F7 cursor-advance/wrap pair, Ok/each-rejection-code response mapping, duplicate-NameInUse tolerance, Olthoi template lock, attribute-lock/balance interaction, uncostable-skill rejection, generation reset) + .../Session/LiveSessionControllerCharacterCreationTests.cs (5 cases — wire-send exactly 55 skill slots via a REAL WorldSession + GameMessageCapture, decoded byte-for-byte; the full Ok round trip via WorldSession.ProcessDatagram reflection asserting F1's guid-based enter + F2's ActiveIndex-preserving roster append + ApplyCharacterCreated; the NameInUse round trip asserting ApplyCreationFailed + no roster/enter side effect; the local-refusal-never-touches-the-wire gate; the F3 confirmed-unspent-credits send). Runtime 1706/0 (was 1701, was 1667), Core.Net unchanged at 994/0, full solution Release build green. OPEN for CC4+: RuntimeCharacterCreationState's ChargenOptions currently defaults to ChargenOptions.Empty — threading the installed DAT's loaded options through GameRuntime/App startup is unresolved; the Slot field's real assignment source (which caller picks the target roster slot) has no decomp citation (ACE ignores it, non-load-bearing); classID's real DAT-DID resolution (AP-209) if a non-ACE server ever needs it; the F14 zero-call-site status hooks. |
| CC4 | CODE-COMPLETE 2026-08-15 | original + fix-round, both "this commit" | Dual-lens review returned architectural FAIL (F1, F6) + retail-fidelity PASS-with-reservations (F2, F3, F4) + LOW findings F5/F7-F12 (F13 is a merge-mechanics note for the orchestrator, not an acdream defect). Fix round applied same-session (see the "Review fix round" paragraph at the end of this row); re-review status owed to the orchestrator. | Screen shell + form pages (App layer). Mount: CharacterCreationUiController/CharacterCreationUiMountCoordinator (src/AcDream.App/UI/Layout/) clone CharacterManagementUiController's recipe — enum 0x10000039 via RetailDataIdResolver.Resolve(dats, ..., 5u), root 0x100003CC (decomp-verified: gmCharGenMainUI::gmCharGenMainUI @ 0x004e7eb0, NOT the plan doc's earlier 0x100003cc-adjacent guesses — confirmed live against the installed DAT, [CC4-DAT] enum=0x10000039 -> DID=0x21000038), fixed-canvas AD-98 treatment shared with char-management. CORRECTED at the review fix round (2026-08-15, F1) — the original claim above was FALSE: CharacterManagementUiController does NOT do a per-tick set; it writes UiRoot.FixedCanvasSize ONCE on its own activation edge and NULLS it in both Deactivate() and Dispose(). This controller now matches that exact shape: Open() sets the canvas once, Close()/Deactivate()/Dispose() null it symmetrically. The un-nulled canvas was a real bug: RuntimeCharacterCreationState had no CompleteEnter() analogue to RuntimeCharacterSelectionState's (added this round, wired at both LiveSessionController in-world edges), so the chargen view reported IsActive=true for an entire in-world session, and since RetailUiRuntime.Tick ticks char-management BEFORE chargen, chargen's un-nulled canvas would silently re-pin an 800x600 scale over the in-world UI forever once the screen had ever been opened (dormant at defaults, armed under ACDREAM_OPEN_CHARGEN=1). Master shell: progress bar 0x100003ce, master page 0x100003d0 (state 0x10000025+page-1), 6 page roots, 6 free-navigation tabs (0x100003ef..f4), nav buttons 0x100003c6..cb — full decomp port of gmCharGenMainUI::ListenToElementMessage @ 0x004e9450 (Back-at-Heritage→DoExit, Next capped at Summary, Finish Summary-only) and SetProgressState @ 0x004e7a10 (the Olthoi Profession/Skills/Town tab-hide + forward/backward page redirect, keyed off the LIVE snapshot heritage id every call). Exit confirmation via RetailDialogFactory.MakeConfirmation + ID_CharGen_ExitWarning (table 0x23000002, matching DoExit @ 0x004e8650); on confirm the screen just closes (visibility only — see AD-99's sibling precedent) rather than porting gmEpilogueUI. Heritage page (CharacterCreationHeritagePage.cs, decomp InitializePage @ 0x00483a10 + the EXACT button-id→heritage-id map read off ListenToElementMessage @ 0x00483860, which is NOT numeric-order — e.g. 0x100005e8→Tumerok(7)): all 13 buttons, composed description text (ID_CharGen_Heritage_StartingSkills_Header/Body, ID_CharGen_Heritage_BonusSkills_Trained_Header + per-heritage body — Shadowbound/Penumbraen share one string per the decomp's case 5: case 0xa:; Lugian/Olthoi/OlthoiAcid have no bonus-skills string in the retail table at all, confirmed by string-key absence, not guessed). Selecting a heritage ALSO auto-selects its lowest gender key (AD-101 — Appearance's real gender buttons are CC6b's). Profession page (CharacterCreationProfessionPage.cs, InitializePage @ 0x00482d50 + UpdateProfession @ 0x004821b0's template map, cited already on ChargenTemplate): 7 template buttons (Custom=index 0, the six presets NOT in id order), 6 attribute sliders with the exact e6/e7/e9/e8/ea/eb id↔attribute-id mapping (the documented 3/4 swap), avail/health/stamina/mana. Live-DAT probe found TWO widget-mapping surprises the decomp's DynamicCast calls don't predict: the slider's value display (0x100002ef) imports as UiField not UiText (retail's NumberInputFilter, @0x00482e36) — wired for direct numeric entry via OnSubmit, not just display; and all four avail/health/stamina/mana containers (and the Skills credits meter) author as UIElement_Button whose Type-12 value child is swallowed by UiButton.ConsumesDatChildren before ever becoming an addressable widget — substituted with the button's own .Label (AD-103). Health/Stamina/Mana formulas ported from UpdateAttributeValues @ 0x00482450: Health=Endurance/2 (int truncation — the decompiler elides the FPU divide at _ftol2 @0x0048262b, so the exact MSVC rounding mode is UNVERIFIED beyond well-established AC convention; flagged, not guessed-and-hidden), Stamina=Endurance, Mana=Self; Available=RemainingAttributeCredits directly (UpdateCreditsMeter-style, no formula). Skills page (CharacterCreationSkillsPage.cs, InitializePage @ 0x00481dd0): ONE flat listbox (AP-213, retail's four-bucket sorted InsertEntrySorted/UpdateSkillEntry model not ported) driven by CC3's TrainSkill/SpecializeSkill/UntrainSkill + the SAME two-tier TryGetSkillCost presence gate RuntimeCharacterCreationState uses (16 uncostable ids never listed, matching retail); credits meter via the AD-103 button-Label substitution; info panes 0x100003fb/fc unbound (no info-pane content source this round). Town page (CharacterCreationTownPage.cs, InitializePage @ 0x0047c6d0 + SetTown @ 0x0047c360's literal index map): the four buttons map to LITERAL startArea indices (Sanamar→3, Holtburg→0, Yaraq→2, Shoushi→1 — not id order), composed "How To" + per-town description text. Random (0x100003cb, DoRandom @ 0x004e7d70): Heritage/Profession/Town approximated with a uniform pick over every valid option (AP-212 — no RandomizeHeritageGroup/RandomizeTemplate primitives exist); disabled outright on Skills (no RandomizeSkills primitive), Appearance (placeholder), Summary (CC5's warning dialog). Options threading: RuntimeCharacterCreationState.InstallOptions(ChargenOptions) (new, mirrors RuntimeCharacterState.InstallSpellMetadata→Spellbook.InstallMetadata's "install immutable DAT metadata after construction, throw if already active" pattern) called from ContentEffectsAudioCompositionPhase.Compose (new ChargenOptionsInstalled composition point, right after SpellMetadataInstalled) via IContentEffectsAudioCompositionFactory.LoadChargenOptions/InstallChargenOptions — ChargenTableReader.Load(dats) threaded through the SAME DAT-open composition sequence spell metadata uses, always well before any session's Begin(). CORRECTED at the review fix round (2026-08-15, F6): the original claim that headless was unaffected left a dead end — HeadlessSessionHost wired the CharacterCreated/CreationFailed status hooks (closing CC3's F14) but never installed ChargenOptions, so a content-bearing headless host could observe a create but never actually issue one (every chargen command silently refused against ChargenOptions.Empty). Fixed by installing options directly beside the existing InstallSpellMetadata call, off the same HeadlessProcessContentLease.Dats, whenever contentLease is non-null; a content-less headless host (a validated-legal configuration — see the R9 note near _contentLease's other reads) still cannot issue chargen commands, matching its existing inability to resolve spell/collision data either. Status hooks: LiveSessionLifecycleBindings gained optional CharacterCreated/CreationFailed delegates (default null — every pre-CC4 construction site keeps compiling); LiveSessionLifecycleHost now overrides both ILiveSessionLifecycleHost methods to forward them; LiveSessionHostBindings gained matching optional fields threaded through LiveSessionHost's constructor; both LiveSessionRuntimeFactory.Create (App/graphical) and HeadlessSessionHost wire them to SessionStatusWriter.CharacterCreated/CreationFailed, closing CC3's F14 (zero call sites). Deferred command seam: IGameRuntimeView.CharacterCreation (new default-throw member, mirrors CharacterSelection), GameRuntime.CharacterCreation (passthrough to Session.CharacterCreation), CurrentGameRuntimeAdapter's new CharacterCreationProjection (IsActive-gated view+command wrapper, mirrors CharacterSelectionProjection), DeferredGameRuntimeStateCommands's new CharacterCreation view getter + 9 generation-capturing wrapper methods, and CharacterCreationRuntimeBindings wired in InteractionRetainedUiComposition.cs (CharacterCreation: sibling of CharacterSelection:, ResolveText backed by a DatStringResolver cached once per composition (characterCreationStrings, review fix round F12 — a fresh resolver per call was allocating + re-locking on every Heritage/Town description lookup, several times per page switch) and locked under d.DatLock only around each .Resolve call, OpenOnStart from the new RuntimeOptions.OpenCharacterCreationOnStart / ACDREAM_OPEN_CHARGEN=1 env flag — the interim open seam since Create stays ghosted). Widget types added to DatWidgetFactory: NONE — every id resolves through EXISTING factory mappings (Button=1, Text/Field=12, Scrollbar=11, ListBox=5); the two "new" findings (editable-Field slider value, button-consumed credits/vitals children) are AUTHORED-DATA-DRIVEN outcomes of the existing factory logic, not new widget classes. Register rows filed (same commit): AD-101 (Heritage-page auto-gender-select interim default), AD-102 (Viamontian/Sanamar ToD-account-ownership gate omitted — acdream has no account/DLC signal), AD-103 (avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays), AP-212 (Random button's uniform-pick approximation), AP-213 (Skills page flat-listbox simplification), TS-82 (Appearance/Summary placeholder pages, reachable via free tab nav, content-inert pending CC5/CC6a/CC6b). Tests: tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs (7 cases, ACDREAM_PROBE_LIVE_MOUNT=1-gated — sweeps every master-shell/page id against the installed DAT and pins the two widget-mapping surprises above) + CharacterCreationUiControllerTests.cs (16 cases — hand-built layout fixture, no DAT: page switching, Olthoi tab-hide+redirect, Back/Exit/Random gating, exit-confirm/cancel, per-page command dispatch including the slider/field/skill-row/town-button paths) + tests/AcDream.Runtime.Tests/CharGen/RuntimeCharacterCreationStateTests.cs (+4 InstallOptions cases) + tests/AcDream.Runtime.Tests/Session/LiveSessionLifecycleHostTests.cs (+2 status-hook forwarding cases). Runtime 1713/0 (was 1707), App 5117/13 skips (was 5101/6, +16 new +7 gated-skip), Headless 165/0 unaffected, full solution Release build green. OPEN for CC5/CC6a/CC6b: the real Appearance-page gender buttons must retire AD-101's auto-select; Summary's Finish gate, name input, and randomize-warning dialog (currently Finish/Random both hard-disabled); Skills page info-panes 0x100003fb/fc have no content source wired yet; the four-bucket sorted skill list (AP-213) and retail's exact Random algorithms (AP-212) remain unported if a future gate demands byte-exact parity; the Health/Stamina/Mana rounding-mode residual (see above) would need a live cdb byte trace to fully pin. Review fix round (this commit, 2026-08-15): F1 (HIGH, blocking, architectural) — see the corrected FixedCanvasSize paragraph above; added RuntimeCharacterCreationState.CompleteEnter() (mirrors RuntimeCharacterSelectionState's own, wired at both LiveSessionController in-world edges: StartCore and the shared EnterHighlightedCore) and made CharacterCreationUiController.Open/Close/Deactivate/Dispose set/null UiRoot.FixedCanvasSize symmetrically with CharacterManagementUiController's real (not per-tick) shape; added FixedCanvasSize coverage to CharacterCreationUiControllerTests. F2 (MEDIUM-HIGH, blocking, fidelity) — the attribute-slider scalar mapping was NOT retail's: fixed the display scalar to value/100f (UpdateAttributeValues @ 0x0048251d) and the drag inverse to Math.Max(10, (int)(scalar*100f)) — truncate, clamp low only, no rescale (ListenToElementMessage @ 0x004829c0's scrollbar-drag case, independently re-derived against the decomp and confirmed byte-for-byte); added tests at scalar 0.5 and 0.0 (the previous single scalar=1f test coincidentally agreed with both the old wrong formula and the new correct one). F3 (MEDIUM, blocking, fidelity) — ported ListenToElementMessage @ 0x004e9450's heritage-button tab-restore arm (independently re-derived from the decomp: SHOW ids 0x100003bf/c1/c2/c3/10000590/91/100005a9/bf/c4/e8, HIDE ids 0x100005c7/c8, with Lugian 0x100005f1 genuinely absent from both switch cases — a real retail quirk, reproduced faithfully) as CharacterCreationUiController.ApplyHeritageTabRestore, invoked synchronously from a new CharacterCreationHeritagePage ctor callback on every button click; added restore-after-Olthoi-hide and Lugian-no-restore tests. F4 (MEDIUM, fidelity, blocks the user gate) — gmCGTownPage::SetTown @ 0x0047c360 also sets the TOWN PAGE's own retail state (a separate literal map from the master page's per-page-index cycling: Holtburg->0x10000034, Shoushi->0x10000037, Yaraq->0x10000036, Sanamar->0x10000035, re-asserted directly at the Sanamar-click site @0x0047c518) — independently re-derived from the decomp's tail-merged-branch pattern and ported to CharacterCreationTownPage.Refresh via the existing IUiDatStateful.TrySetRetailState seam; added a test. F5 (MEDIUM) — AD-103's "composited pixel result unchanged" claim was asserted, not measured; softened to state the equivalence is unverified rather than building a rect/justify comparison probe this round. F6 (MEDIUM, blocking, architectural) — decision: install ChargenOptions in the headless content path (option (a) of the two offered), not the deferred/out-of-scope alternative — HeadlessSessionHost now calls RuntimeCharacterCreationState.InstallOptions(ChargenTableReader.Load(content.Dats)) beside the existing InstallSpellMetadata call whenever contentLease is non-null, closing the gap where CC3's F14 status hooks were wired but no content-bearing headless host could ever produce a create to observe. F7 (LOW-MEDIUM) — AP-213 already named the label format and the click/double-click substitution explicitly on inspection; no row edit needed. F8 (LOW) — AP-212 now names all SIX of DoRandom's decompiled primitives (added the three the original row omitted: RandomizeAppearance @ 0x005c4f10, RandomizeClothing @ 0x005c6770, RandomizeCharacter @ 0x005c6d80, independently verified against the decomp alongside the three already-cited ones) and states the known landing site (Runtime, beside CC3's CharGenState ports). F9 (LOW) — AD-101's retirement condition corrected: must happen before CC5's Finish un-ghosts, not merely "at CC6b" (CC5 precedes CC6b in the slice order; shipping Finish first would let a create complete on an implicit gender default). F10 (LOW) — merged ItemAppraisalTextFormatter.SkillName's two consecutive <summary> blocks into one. F11 (LOW) — TS-82's "see AP-211's sibling gate" cross-reference was wrong (AP-211 is the unrelated roster-slot-cap refusal); corrected to point at TS-82's own CC5 dependency. F12 (LOW) — cached the chargen DatStringResolver once per composition (characterCreationStrings in InteractionRetainedUiComposition.CreateRetainedUi) instead of constructing + DAT-locking fresh on every ResolveText call; the LinesProvider per-Refresh closure allocation already matched the house pattern used throughout CharacterStatController.cs and elsewhere, so it was left as-is. F13 is a merge-mechanics note (TS-82 collides with campaign-cc6a's TS-82/83) for the orchestrator at merge time — no acdream-side action taken. CC4 re-review round (ec854db0's own fix round, 2026-08-15) — R1 (MEDIUM, blocking, architectural, NEW residual introduced by the F1 fix above): the F1 fix's raw _host.FixedCanvasSize = null in Close() was STILL a bug — character-creation can be simultaneously active on top of character-management (which stays active underneath, ticking its own roster), and nulling the shared host-global from either screen without regard for the OTHER screen's own active declaration strips it out from under whichever screen is still open (the exact AD-98 gate-round-2 misalignment defect resurfacing one layer up: char-select renders unstretched with dialogs centered against the raw window). Root cause per the reviewer (agreed): TWO controllers writing ONE host-global with no owner. Fix — the root-cause shape, no workaround: UiRoot gained a single arbiter, DeclareFixedCanvas(object owner, Vector2 size)/RevokeFixedCanvas(object owner) (see AD-98's own register row for the mechanism detail); both CharacterCreationUiController and CharacterManagementUiController now declare on their activation edge and revoke on close/deactivate/dispose instead of writing FixedCanvasSize directly — grepped for stragglers, none remain in production code; the raw property setter stays public only for UiRootFixedCanvasTests' isolated scale-math coverage. Test (reviewer-specified): tests/AcDream.App.Tests/UI/Layout/CharacterScreensFixedCanvasArbiterTests.cs — two controllers sharing ONE UiRoot, asserting the canvas across the full sequence (char-mgmt active → chargen Open → chargen Exit-confirm Close, canvas STAYS SET because char-mgmt is still active → char-mgmt deactivate, NOW it nulls) plus the original F1 defect's own covering case (both screens revoke together at world entry). R3 (LOW): tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs's new ContentLease_InstallsRealChargenOptions_SelectHeritageIsAccepted proves F6's install actually opens the gate — a HeadlessSessionHost built with a content lease carrying a REAL hand-built DatCharGen heritage (not ChargenOptions.Empty) has that heritage present in CharacterCreationState.Options, and TrySelectHeritage for it succeeds once Begin is called (both called directly via this project's existing InternalsVisibleTo on AcDream.Runtime, isolating the F6 wiring from the unrelated real-network handshake needed to reach the same session state through the normal command gate). R2 (LOW): filed docs/ISSUES.md #402 for the pre-existing Streaming.LandblockBuildFactoryTests.Build_UsesTheSuppliedSharedReaderGate full-suite flake (passes isolated, fails ~2/5 full-suite runs, last touched 82f8d4f8 2026-07-25 — unrelated to Campaign CC) so it stops being re-discovered. R4 (LOW): fixed the "unchached" → "uncached" typo in InteractionRetainedUiComposition.cs's F12 comment. Runtime 1713/0 (unchanged), App 5127/13 skips (+2 new: 2 CharacterScreensFixedCanvasArbiterTests cases), Headless 166/0 (+1 new: R3's test), full solution Release build green. |
| CC5 | — | |||
| CC6a | — | |||
| CC6b | — | |||
| CC7 | — |