fix(chargen): Campaign CC gate round 1 Batch D — gmCG3DView environment backdrop
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>
This commit is contained in:
parent
0b05b58514
commit
63bf64c934
9 changed files with 866 additions and 103 deletions
|
|
@ -104,6 +104,19 @@ internal static class ChargenPreviewEntityBuilder
|
|||
/// doc comment.</summary>
|
||||
public const uint PreviewRenderId = 0xDA11_D032u;
|
||||
|
||||
/// <summary>Reserved synthetic guid for the chargen preview's ENVIRONMENT
|
||||
/// backdrop (GF-7/GF-14 fix) — next slot in the same 0xDA11D03x chargen
|
||||
/// family as <see cref="PreviewServerGuid"/>.</summary>
|
||||
public const uint PreviewBackdropServerGuid = 0xDA11_D033u;
|
||||
|
||||
/// <summary>Reserved render-local entity id for the backdrop object,
|
||||
/// passed in <c>animatedEntityIds</c> alongside <see cref="PreviewRenderId"/>
|
||||
/// so a heritage switch's new environment Setup also bypasses the
|
||||
/// classification cache — same reasoning as <see cref="PreviewRenderId"/>'s
|
||||
/// own doc comment, applied to retail's SECOND <c>creature_mode_objects</c>
|
||||
/// member (<c>gmCG3DView::m_pbgObject</c>).</summary>
|
||||
public const uint PreviewBackdropRenderId = 0xDA11_D034u;
|
||||
|
||||
/// <summary>
|
||||
/// Retail's held-pose (REST) animation DID enum key, resolved through
|
||||
/// master map slot 7 exactly like <c>RetailPaperdollPoseApplicator.ResolvePoseDid</c>
|
||||
|
|
@ -301,6 +314,92 @@ internal static class ChargenPreviewEntityBuilder
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the chargen preview's ENVIRONMENT BACKDROP entity — the fix for
|
||||
/// GF-7/GF-14 (preview backdrop black on Appearance and Summary).
|
||||
///
|
||||
/// <para>
|
||||
/// Decomp-cited: <c>gmCG3DView::Update @0x004EE9D0</c>
|
||||
/// (~0x004eecd3-0x004eed44) constructs a SECOND <c>CPhysicsObj</c> from
|
||||
/// <c>m_bgSetupID</c> and adds it to the SAME viewport's
|
||||
/// <c>creature_mode_objects</c> the player object lives in — <b>BEFORE</b>
|
||||
/// the player is re-added (the player's own re-<c>AddObject</c> happens
|
||||
/// much later, at ~0x004ef199, after the full clothing ObjDesc is
|
||||
/// composed), so retail's own draw-list order is backdrop first, player
|
||||
/// second. <c>m_bgSetupID</c> is compared against a freshly-read value the
|
||||
/// decompiler elides (<c>var_b8</c>/<c>eax_32</c>, an unresolved-call
|
||||
/// artifact — see <c>claude-memory/feedback_bn_decomp_field_names.md</c>)
|
||||
/// immediately after <c>ACCharGenData::GetHG(charGenData, mHeritageGroup)</c>
|
||||
/// (0x004eea1a) resolves the current heritage's <c>HeritageGroup_CG</c>;
|
||||
/// <c>acclient.h</c>'s verbatim struct layout
|
||||
/// (<c>HeritageGroup_CG.environmentSetupID</c>, right after
|
||||
/// <c>setupID</c>) confirms the elided value IS that field — i.e. THE
|
||||
/// SAME id this codebase already parses as
|
||||
/// <see cref="AcDream.Core.CharGen.ChargenHeritageOptions.EnvironmentSetupId"/>
|
||||
/// (<c>ChargenTableReader.cs</c>) but never consumed. The backdrop object
|
||||
/// gets NO explicit position/orientation/scale anywhere in the function —
|
||||
/// <c>CPhysicsObj::makeObject(eax_32, 0, 1)</c> (0x004eed2f) leaves it at
|
||||
/// its physics-object default (origin, identity), exactly like the player
|
||||
/// object's own placement in this same private scene. This method mirrors
|
||||
/// that: a plain, unposed, unpalette-overridden Setup mesh at the origin.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Both the Appearance page (<c>gmCGAppearancePage</c>) and the Summary
|
||||
/// page (<c>gmCGSummaryPage</c>) call this SAME <c>gmCG3DView::Update</c>
|
||||
/// function on their own <c>gmCG3DView</c> instance (confirmed at
|
||||
/// pseudo-C ~0x0047bbf0/~0x0047c92c for Summary and ~0x0047c840/
|
||||
/// ~0x0047eee1 for Appearance) — so the backdrop mechanism is identical
|
||||
/// for both viewports, not page-specific.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="environmentSetupId">
|
||||
/// <see cref="AcDream.Core.CharGen.ChargenHeritageOptions.EnvironmentSetupId"/>.
|
||||
/// Zero (unset/no environment authored for this heritage) returns null —
|
||||
/// matches retail's own <c>if (eax_32 != INVALID_DID.id)</c> gate at
|
||||
/// 0x004eed29, which skips <c>makeObject</c>/<c>AddObject</c> entirely
|
||||
/// when the heritage has no environment Setup.
|
||||
/// </param>
|
||||
public static WorldEntity? TryBuildBackdrop(
|
||||
IDatReaderWriter dats,
|
||||
uint environmentSetupId,
|
||||
object datLock)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(datLock);
|
||||
|
||||
if (environmentSetupId == 0u)
|
||||
return null;
|
||||
|
||||
lock (datLock)
|
||||
{
|
||||
Setup? setup = dats.Get<Setup>(environmentSetupId);
|
||||
if (setup is null)
|
||||
return null;
|
||||
|
||||
var flattened = SetupMesh.Flatten(setup);
|
||||
var drawable = new List<MeshRef>(flattened.Count);
|
||||
foreach (MeshRef part in flattened)
|
||||
{
|
||||
if (dats.Get<GfxObj>(part.GfxObjId) is not null)
|
||||
drawable.Add(part);
|
||||
}
|
||||
if (drawable.Count == 0)
|
||||
return null;
|
||||
|
||||
return new WorldEntity
|
||||
{
|
||||
Id = PreviewBackdropRenderId,
|
||||
ServerGuid = PreviewBackdropServerGuid,
|
||||
SourceGfxObjOrSetupId = environmentSetupId,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = drawable,
|
||||
ParentCellId = null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>No dat access — pure projection of the already-composed
|
||||
/// ObjDesc's subpalettes, safe to call outside <c>datLock</c>.</summary>
|
||||
private static PaletteOverride? BuildPaletteOverride(ChargenAppearanceResult appearance)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue