fix(app,runtime,headless): Campaign CC slice CC4 review-fix round — F1-F12

Dual-lens review of CC4 (0e71d3b8) returned architectural FAIL (F1, F6)
and retail-fidelity PASS-with-reservations (F2, F3, F4), plus LOW
findings F5, F7-F12. F13 (TS-82's merge collision with campaign-cc6a) is
merge mechanics for the orchestrator, not addressed here.

F1 (HIGH, blocking): CharacterCreationUiController never released
UiRoot.FixedCanvasSize, on a FALSE premise that CharacterManagementUi-
Controller does a per-tick set (it does not — it sets once on activation
and nulls on Deactivate/Dispose). Root cause: RuntimeCharacterCreation-
State had no CompleteEnter() analogue to RuntimeCharacterSelectionState's,
so the creation view reported IsActive=true for an entire in-world
session. Added CompleteEnter(), wired at both LiveSessionController
in-world edges (StartCore, EnterHighlightedCore); made Open/Close/
Deactivate/Dispose set/null the canvas symmetrically; corrected the false
comment and ledger claim; added FixedCanvasSize test coverage.

F2 (MEDIUM-HIGH, blocking): the attribute-slider scalar mapping was not
retail's. Fixed display to value/100f (UpdateAttributeValues @
0x0048251d) and the drag inverse to truncate+clamp-low-only, no rescale
(ListenToElementMessage @ 0x004829c0, independently re-verified against
the decomp). Added tests at scalar 0.5/0.0 plus a display-direction test.

F3 (MEDIUM, blocking): ported the unported heritage-button tab-restore
arm (ListenToElementMessage @ 0x004e9450) — SHOW/HIDE id sets independently
re-derived from the decomp, including the genuine Lugian (0x100005f1)
no-restore quirk, reproduced faithfully. Wired via a new HeritagePage
click callback; added restore + quirk tests.

F4 (MEDIUM): ported SetTown's (@ 0x0047c360) separate per-town page-root
state literal (Holtburg->0x10000034 etc.), independently re-derived from
the decomp's tail-merged branches; wired via the existing
IUiDatStateful.TrySetRetailState seam; added a test.

F5 (MEDIUM): softened AD-103's unmeasured pixel-equivalence claim.

F6 (MEDIUM, blocking): DECISION — install ChargenOptions in the headless
content path (chosen over marking headless creation out-of-scope).
HeadlessSessionHost now calls InstallOptions off the shared content
lease's Dats, beside the existing InstallSpellMetadata call.

F7: AP-213 already named the label format and click/double-click
substitution explicitly on inspection — no edit needed.
F8: AP-212 now names all six DoRandom primitives with a known landing site.
F9: AD-101 retirement corrected to precede CC5's Finish un-ghosting.
F10: merged ItemAppraisalTextFormatter's duplicate <summary> block.
F11: fixed TS-82's wrong AP-211 cross-reference.
F12: cached the chargen DatStringResolver once per composition instead of
per ResolveText call.

Runtime 1713/0, App 5125/13 skips (+8 new tests), Headless 165/0, full
solution Release build green. Live-DAT probes 7/7 under
ACDREAM_PROBE_LIVE_MOUNT=1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-15 18:29:49 +02:00
parent 0e71d3b829
commit ec854db045
12 changed files with 419 additions and 30 deletions

View file

@ -915,6 +915,9 @@ public sealed class LiveSessionController
_inWorld = true;
_activeSelection = selection;
CharacterSelectionState.CompleteEnter(selection.CharacterId);
// CC4 review-fix F1: same in-world edge as selection's own
// CompleteEnter above.
CharacterCreationState.CompleteEnter();
host.ApplyEnteredWorld(selection);
if (!IsCurrent(scope, generation))
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
@ -1206,6 +1209,10 @@ public sealed class LiveSessionController
_inWorld = true;
_activeSelection = selection;
CharacterSelectionState.CompleteEnter(character.CharacterId);
// CC4 review-fix F1: covers BOTH callers of this shared core
// (EnterSelectedCore and EnterCreatedCharacterCore) — the same
// in-world edge as selection's own CompleteEnter above.
CharacterCreationState.CompleteEnter();
scope.Host.ApplyEnteredWorld(selection);
if (!IsCurrent(scope, generation))
return CharacterSelectionResult(RuntimeCommandStatus.Inactive);

View file

@ -400,6 +400,39 @@ public sealed class RuntimeCharacterCreationState : IDisposable
Publish(RuntimeCharacterCreationDeltaKind.Reset);
}
/// <summary>
/// Campaign CC slice CC4 review-fix round (F1): the character-creation
/// analogue of <see cref="RuntimeCharacterSelectionState.CompleteEnter"/>.
/// Unlike selection (whose <c>IsActive</c> is computed from a
/// <c>Lifecycle</c> enum that already has an <c>InWorld</c> state),
/// creation has no lifecycle enum — this flips <see cref="_active"/>
/// (and therefore <see cref="RuntimeCharacterCreationSnapshot.IsActive"/>)
/// straight to <see langword="false"/>, mirroring selection's OBSERVABLE
/// effect at the same call sites (<c>LiveSessionController.StartCore</c>
/// and <c>EnterHighlightedCore</c>, both already call
/// <c>CharacterSelectionState.CompleteEnter</c> at the exact point the
/// session transitions in-world). Session field data (heritage/gender/
/// name/etc.) is left untouched — only <see cref="Reset"/> clears it,
/// matching selection's own CompleteEnter, which does not clear its
/// roster either. Before this fix, nothing ever cleared
/// <see cref="_active"/> between <see cref="Begin"/> and the NEXT
/// <see cref="Reset"/>/<see cref="Dispose"/>, so the creation view
/// reported active for an entire in-world session — the CC4 review's
/// F1 finding (a permanently re-pinned <c>UiRoot.FixedCanvasSize</c>
/// once the chargen screen had ever been opened).
/// </summary>
internal void CompleteEnter()
{
lock (_gate)
{
if (_disposed || !_active)
return;
_active = false;
_revision++;
}
Publish(RuntimeCharacterCreationDeltaKind.StateChanged);
}
internal void Reset(RuntimeGenerationToken generation)
{
lock (_gate)