Commit graph

2 commits

Author SHA1 Message Date
Erik
1ba22a01a8 fix(chargen): Campaign CC CC6b-PRE review fix round — F1-F7 + F11 concession rewrite
F1 (BLOCKING, doc-only) — the idle-by-default rationale rested on an unsound
"uninitialized C++ member defaults to 0" argument (heap operator-new memory
is indeterminate, not zero). Verified and replaced with the real evidence:
gmCGAppearancePage::InitializePage @0x0047FDD0 writes an EXPLICIT
this->m_bZoomedIn = 0; at 0x004802C3, immediately after that same function
points the camera at the zoomed-IN per-heritage eye (0x00480286-0x0048029E).
Fixed in all three places: the register's TS-83 retirement clause,
ChargenPreviewAnimator's class doc, ChargenPreviewZoomController.IsZoomedIn's
doc. Recorded the retail quirk this implies: the character starts framed
close-up while not-zoomed-in, so the first Zoom In click (once mounted)
tweens close-eye->close-eye (visually null) while still freezing the
animation — the port reproduces this faithfully.

F2 — ChargenPreviewZoomController and ChargenPreviewAnimator kept
independent _zoomedIn bools synced only via a nullable animator parameter,
risking desync. Retail's m_bZoomedIn is a single field gating both camera
and animation, so the fix makes the animator the sole state owner:
ChargenPreviewZoomController now takes its ChargenPreviewAnimator as a
required constructor dependency, IsZoomedIn reads straight through to it,
and ZoomIn/ZoomOut no longer take a parameter at all — there is no second
bool left to disagree.

F3 — documented the DoRotation counter-clockwise branch's x87-stack
decompiler artifact (BN renders x87_r7_1 = x87_r6_3 at 0x0047CAEB, which
would store delta-degrees instead of the timestamp for CCW only); the port
already stores "now" in both branches, cited against
feedback_bn_decomp_field_names.md.

F4 — ChargenPreviewAnimator.ApplyIdleFrame now double-buffers two
List<MeshRef> instead of allocating fresh every 30fps tick.

F5 — filed docs/ISSUES.md #402 tracking the RetailAnimationCyclePlayback /
LiveEntityAnimationPresenter duplication as an owned post-CC follow-up,
referenced from the new type's own doc.

F6 — reworded the ChargenPreviewEntityBuilder.TryBuild "byte-identical"
claim to result-identical (TryBuildAnimated now also resolves the idle DID
and loads the idle Animation before the wrapper discards them).

F7 — added the missing clockwise >360 clamp test (readable decomp
polarity, unlike F3's CCW artifact).

ALSO — rewrote the CC6b ledger row's m_alternateSetupID MUST-COVER note per
the reviewer's F11 concession: all five write sites belong to gmBarberUI
(the post-creation barber shop), not gmCGAppearancePage, which has no
option-checkbox-equivalent field at all. Added the enclosing-function
citations and an explicit directive that CC6b-mount must NOT build a
crown/no-flame checkbox on the Appearance page.

Tests: ChargenPreviewRotationControllerTests +1 (10 total),
ChargenPreviewZoomControllerTests +2 and every case rewritten for the
required-animator constructor (9 total). Core.Tests 4786/1 skip (unchanged),
Content.Tests 147/0, App.Tests 5152/6 skips (+3) — zero failures in
isolation, full solution Release build green. Two pre-existing flakes
observed across repeated full-solution runs, neither caused by this round
and neither reproducing standalone: Core.Net.Tests' NakEmissionTests loss
soak, and Content.Tests' DecodedTextureCacheTests concurrency race.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 19:32:48 +02:00
Erik
8dfee1118f feat(chargen): Campaign CC slice CC6b-PRE — idle loop, rotation, zoom (mount-independent half)
Idle animation loop: decomp re-read of gmCGAppearancePage::Update's trailing
StartAnimation/StopAnimation gate (~0x0047EF01-0x0047EF12) plus the ctor
evidence that m_bZoomedIn is a decompiler-elided bool (never explicitly set
away from its zero default, unlike its two sibling bools) establishes that
retail's chargen preview defaults to the idle loop PLAYING, not the frozen
rest pose CC6a shipped as a deliberate simplification (TS-83) — the rest pose
only appears once Zoom In fires. New Core primitive
RetailAnimationCyclePlayback ports CPhysicsObj::set_sequence_animation's
advance-with-wrap + lerp/slerp effect (the same algorithm
LiveEntityAnimationPresenter's legacy NPC-idle branch already carries inline;
not consolidated this round — out of blast radius for a preview-only
feature, noted in the new type's own doc). New ChargenPreviewAnimator drives
the per-tick swap; ChargenPreviewEntityBuilder gained TryBuildAnimated
alongside the byte-behavior-unchanged TryBuild. Olthoi/OlthoiAcid use the
SAME enum key for idle and rest DIDs (decomp-confirmed quirk). TS-83 retired
in the register (§4 count 50->49).

Rotation controller: ChargenPreviewRotationController ports
Rotate/DoRotation (0x0047CB50/0x0047CA80) verbatim — toggle-to-stop,
deltaDegrees = ((now-last)/RotationSecondsPerRevolution)*360, single-pass
+-360 clamp (not a full modulo, matching retail's own tail), the -1.0
invalidation sentinel. Applies to the entity's heading via the existing
MoveToMath.SetHeading port, not the camera, confirming CC6a's own note.

Zoom tween: ChargenPreviewZoomController ports ZoomIn/ZoomOut/
DoZoomAnimation (0x0047CF00/0x0047D050/0x0047C960) — a LINEAR 0.6s tween
(no easing curve in the decomp) between the already-recorded camera eye
profiles, calling into the animator's zoom swap IMMEDIATELY at button-press
time, matching retail's call order exactly.

m_alternateSetupID (research correction): re-reading the decomp
function-by-function found all five m_alternateSetupID write sites —
including the two the CC6a review cited — belong to gmBarberUI (the
post-creation barber shop), not gmCGAppearancePage, which has no
m_pOption1Checkbox-equivalent field and never writes the field. For
character creation the field is always INVALID_DID in retail. TryCompose
still gained a real, decomp-cited alternateSetupIdOverride parameter
(default no-op) implementing gmCG3DView::Update's generic override
precedence, for a future non-chargen consumer.

RetailHeldPose extraction: shared ResolvePoseDid/ComposePartTransform
between RetailPaperdollPoseApplicator and ChargenPreviewEntityBuilder — a
clean mechanical extraction, behavior-identical on the paperdoll side.

Bookkeeping: CC6a ledger row now cites its real commit SHAs (55bfd9ca,
1774d8b2); new CC6b-PRE ledger row records scope done + the page-mount half
still owed.

Tests: RetailAnimationCyclePlaybackTests (10, Core), ChargenAppearanceFactoryTests
(+4), ChargenPreviewRotationControllerTests (9), ChargenPreviewZoomControllerTests
(7), ChargenPreviewAnimatorTests (7, hand-built fixtures), ChargenPreviewEntityBuilderTests
(+5, installed-DAT). Core.Tests 4786/1 skip, Content.Tests 147/0, App.Tests
5149/6 skips — zero failures, full solution Release build green. One
pre-existing, unrelated flake noted: Core.Net.Tests' NakEmissionTests loss
soak failed once in the full-suite run, passed 1/1 isolated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 19:05:56 +02:00