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>
This commit is contained in:
parent
1774d8b298
commit
8dfee1118f
18 changed files with 1657 additions and 108 deletions
|
|
@ -12,14 +12,20 @@ namespace AcDream.Core.CharGen;
|
|||
/// The body Setup dat id (0x02......) to build the preview mesh from —
|
||||
/// <c>gender.SetupId</c>, overridden by the selected hair style's
|
||||
/// <c>AlternateSetup</c> when it is neither 0 nor retail's <c>INVALID_DID</c>
|
||||
/// (0xFFFFFFFF — Gear Knight / Undead / Tumerok body variants), falling back
|
||||
/// to <see cref="ChargenAppearanceFactory.HumanSetupId"/> when the resolved
|
||||
/// id is 0 OR <c>INVALID_DID</c> (retail: <c>CharGenState::GetSetupID @
|
||||
/// 0x005C5B22</c> and <c>gmCG3DView::Update</c>'s own check at
|
||||
/// ~0x004EEA51/0x004EEA5F both test against <c>INVALID_DID</c>, not zero —
|
||||
/// <c>acclient.h:39909</c> types the field as <c>IDClass</c>, whose "unset"
|
||||
/// value is 0xFFFFFFFF; <c>CPhysicsObj::makeObject(setupId)</c>'s own
|
||||
/// HUMAN_SETUP_ID fallback, <c>gmCG3DView</c> ctor pseudo-C ~0x004EE79D).
|
||||
/// (0xFFFFFFFF — Gear Knight / Undead / Tumerok body variants), in turn
|
||||
/// overridden outright by <see cref="ChargenAppearanceFactory.TryCompose"/>'s
|
||||
/// own <c>alternateSetupIdOverride</c> parameter when THAT is not
|
||||
/// <c>INVALID_DID</c> (<c>gmCG3DView::Update</c>'s own
|
||||
/// <c>m_alternateSetupID</c> resolution, ~0x004EEA46-0x004EEA53 — see that
|
||||
/// parameter's doc for why chargen's own Appearance page never actually sets
|
||||
/// it), falling back to <see cref="ChargenAppearanceFactory.HumanSetupId"/>
|
||||
/// when the resolved id is STILL 0 OR <c>INVALID_DID</c> after all three
|
||||
/// tiers (retail: <c>CharGenState::GetSetupID @ 0x005C5B22</c> and
|
||||
/// <c>gmCG3DView::Update</c>'s own check at ~0x004EEA5F both test against
|
||||
/// <c>INVALID_DID</c>, not zero — <c>acclient.h:39909</c> types the field as
|
||||
/// <c>IDClass</c>, whose "unset" value is 0xFFFFFFFF;
|
||||
/// <c>CPhysicsObj::makeObject(setupId)</c>'s own HUMAN_SETUP_ID fallback,
|
||||
/// <c>gmCG3DView</c> ctor pseudo-C ~0x004EE79D).
|
||||
/// </param>
|
||||
/// <param name="BasePaletteId">
|
||||
/// <c>gender.BasePaletteId</c> (retail <c>Sex_CG.BasePalette</c>) — the
|
||||
|
|
@ -136,6 +142,30 @@ public static class ChargenAppearanceFactory
|
|||
/// contribution is skipped, matching retail's own "hash miss → no-op,
|
||||
/// caller never checks BuildObjDesc's return value" behavior.
|
||||
/// </summary>
|
||||
/// <param name="alternateSetupIdOverride">
|
||||
/// Retail's SECOND body-Setup-override source — <c>gmCG3DView</c>'s
|
||||
/// <c>m_alternateSetupID</c> field (default <c>INVALID_DID</c>, read at
|
||||
/// <c>gmCG3DView::Update @ ~0x004EEA46-0x004EEA53</c>) — which, when set
|
||||
/// to anything other than <c>INVALID_DID</c>, REPLACES the hairstyle/
|
||||
/// gender-resolved Setup id outright rather than combining with it.
|
||||
/// <b>Decomp-verified NOT to be a character-creation-time mechanism:</b>
|
||||
/// every write site for <c>m_alternateSetupID</c> (the Penumbraen-crown
|
||||
/// and Undead-no-flame variants, ~0x004DFB3F/0x004E0C54/0x004E0D42/
|
||||
/// 0x004E0DB1) lives on <c>gmBarberUI</c> — the POST-CREATION barber-
|
||||
/// shop appearance-editing screen, a wholly separate UI class from
|
||||
/// character creation's <c>gmCGAppearancePage</c>, which has no
|
||||
/// <c>m_pOption1Checkbox</c>-equivalent field and never writes
|
||||
/// <c>m_alternateSetupID</c> anywhere in its own methods (confirmed
|
||||
/// against every field on <c>gmCGAppearancePage</c>,
|
||||
/// <c>acclient.h:56373-56428</c>). For chargen's own preview,
|
||||
/// <c>m_alternateSetupID</c> is therefore ALWAYS <c>INVALID_DID</c> in
|
||||
/// retail, and this parameter's default (<see cref="InvalidDid"/>)
|
||||
/// reproduces that exactly — a real, decomp-verified precedence tier is
|
||||
/// threaded through so a future non-chargen consumer of this same
|
||||
/// factory (e.g. a barber-shop feature, out of Campaign CC's scope) can
|
||||
/// supply one, without inventing a UI source chargen's own Appearance
|
||||
/// page doesn't have.
|
||||
/// </param>
|
||||
public static bool TryCompose(
|
||||
ChargenOptions options,
|
||||
uint heritageId,
|
||||
|
|
@ -143,7 +173,8 @@ public static class ChargenAppearanceFactory
|
|||
ChargenAppearanceSelection selection,
|
||||
IChargenPalSetSource palSets,
|
||||
IChargenClothingTableSource clothingTables,
|
||||
out ChargenAppearanceResult result)
|
||||
out ChargenAppearanceResult result,
|
||||
uint alternateSetupIdOverride = InvalidDid)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(palSets);
|
||||
|
|
@ -170,6 +201,14 @@ public static class ChargenAppearanceFactory
|
|||
if (hairStyle.AlternateSetup != 0 && hairStyle.AlternateSetup != InvalidDid)
|
||||
setupId = hairStyle.AlternateSetup;
|
||||
}
|
||||
|
||||
// gmCG3DView::Update @ ~0x004EEA46-0x004EEA53: m_alternateSetupID,
|
||||
// when set, REPLACES the hairstyle/gender-resolved id outright — it
|
||||
// does not combine with it. See alternateSetupIdOverride's own doc
|
||||
// for why chargen's own Appearance page never actually supplies one.
|
||||
if (alternateSetupIdOverride != InvalidDid)
|
||||
setupId = alternateSetupIdOverride;
|
||||
|
||||
if (setupId == 0 || setupId == InvalidDid)
|
||||
setupId = HumanSetupId;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue