feat(chargen): Campaign CC slice CC6a — index→ObjDesc factory + preview renderer foundation
Delivers the CC6a foundation half of the chargen 3D preview: the missing index->ObjDesc appearance factory the campaign plan's acdream-seams section named, plus a static-pose offscreen renderer following PrivateEntityViewportRenderer's proven paperdoll/appraisal architecture. Page mount, spin/color-wheel controls, and rotate/zoom behavior stay out of scope per the CC4-parallel worktree contract (CC6b, after CC4 merges). Core (src/AcDream.Core/CharGen/, pure, no Chorizite on public surfaces): ChargenAppearanceFactory.TryCompose ports gmCG3DView::Update @0x004EE9D0's ObjDesc rebuild in its exact decompiled order - base body, hair style, clothing in retail's own Headgear/Trousers/Shirt/Footwear order (not the UI tab order or the wire's field order, both of which differ), eyes (bald-aware), nose, mouth, then the unconditional skin subpalette, hair color, eye color. ChargenPalSetMath ports PalSet::GetPaletteID's shade-to-index formula, cross-checked three ways (decomp control flow, ACE's PaletteSet.GetPaletteID "Taken from acclient.c" citation, ACViewer's identical slider math). ChargenPalSet/ChargenClothingTable are pure projections behind IChargenPalSetSource/IChargenClothingTableSource so the factory itself never touches a dat. Content (src/AcDream.Content/CharGen/): ChargenAppearanceCatalog is the cached dat-backed implementation of those two source interfaces, mirroring ChargenTableReader's no-leak discipline. App (src/AcDream.App/Rendering/): ChargenPreviewRenderer is a third facade over PrivateEntityViewportRenderer beside PaperdollViewportRenderer and CreatureAppraisalViewportRenderer - no existing rendering file touched. ChargenPreviewCamera carries the four retail-verbatim per-heritage eye profiles from gmCGAppearancePage::Update @0x0047E8F0 (cross-checked against ZoomIn/ZoomOut's identical literals) plus the recovered rotation (3.0 s/revolution) and zoom-tween (0.6 s, reconstructed from the decompiler's garbled float literals - the plan's own "measure if it matters" note is resolved, not garbled beyond recovery). Rotation applies to the character model, not the camera, per gmCGAppearancePage::DoRotation. ChargenPreviewEntityBuilder resolves Setup/GfxObj/Surface/Animation itself (there is no live entity yet), reusing DatLiveEntityProjectionMaterializer's surface-override algorithm and RetailPaperdollPoseApplicator's held-pose technique, generalized to chargen's per-heritage rest-pose DID. Two register rows filed: TS-83 (the plan-named CC6a static-pose-vs-retail- idle-loop staging, CC6b to retire) and TS-82 (measured, not assumed - the un-ported clothing Setup-substitution fallback chain costs nothing for the 9 standard heritages with clothing UI, but Undead's default gear choices genuinely lack ClothingBaseEffects coverage for Undead's own body Setup). Tests: ChargenPalSetMathTests, ChargenAppearanceFactoryTests (hand-built fixtures), ChargenAppearanceCatalogInstalledDatTests (installed-DAT sweep, all 26 heritage/gender combinations, zero missing PalSet/ClothingTable ids), ChargenPreviewCameraTests, ChargenPreviewEntityBuilderTests (installed-DAT-gated, proves a real 34-part Aluvian mesh resolves). Core.Tests 4767/1 skip, Content.Tests 146/0, App.Tests 5121/6 skips - all pre-existing skips, zero failures, full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3a6b7e3115
commit
55bfd9ca82
16 changed files with 2131 additions and 2 deletions
176
src/AcDream.App/Rendering/ChargenPreviewCamera.cs
Normal file
176
src/AcDream.App/Rendering/ChargenPreviewCamera.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.CharGen;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Heritage-parameterized camera for the chargen 3D preview
|
||||
/// (<c>gmCG3DView</c>, Appearance page viewport <c>0x100003bb</c> / Summary
|
||||
/// <c>0x10000406</c>). Retail-exact eye positions, ported from
|
||||
/// <c>gmCGAppearancePage::Update @ 0x0047E8F0</c> (pseudo-C ~139037-139114,
|
||||
/// which sets <c>m_vectTargPosition</c>/<c>m_vectCurPosition</c> per
|
||||
/// heritage and snaps them together with no tween — CC6a's static preview
|
||||
/// renders that snapped default, the "zoomed-in" framing) and cross-checked
|
||||
/// against the IDENTICAL literals in <c>gmCGAppearancePage::ZoomIn @
|
||||
/// 0x0047CF00</c> (pseudo-C ~137618-137638). Direction is always
|
||||
/// <c>(0,0,0)</c> ⇒ <c>CreatureMode::SetCameraDirection</c> resets the view
|
||||
/// frame to IDENTITY — the SAME zero-yaw/zero-pitch convention
|
||||
/// <see cref="DollCamera"/> already established for the paperdoll (look
|
||||
/// straight down +Y, +Z up); every camera position below is used AS the
|
||||
/// world-space eye directly, matching that camera's approach.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Rotation is NOT a camera property.</b> Retail's continuous-rotation
|
||||
/// button (<c>gmCGAppearancePage::DoRotation @ 0x0047CA80</c>) advances a
|
||||
/// HEADING applied to the preview CHARACTER (<c>CPhysicsObj::set_heading</c>
|
||||
/// inside <c>gmCG3DView::Update</c>, pseudo-C ~242088) — the camera's own
|
||||
/// position/direction never change during a rotation. CC6b's heading
|
||||
/// parameter therefore belongs on the entity builder
|
||||
/// (<see cref="ChargenPreviewEntityBuilder"/>), not here; this class stays a
|
||||
/// fixed-per-heritage eye, exactly like retail's own camera.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ChargenPreviewCamera : ICamera
|
||||
{
|
||||
private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as DollCamera/ChaseCamera.
|
||||
|
||||
private Vector3 _eye;
|
||||
|
||||
public ChargenPreviewCamera(uint heritageId = 0u)
|
||||
{
|
||||
_eye = ResolveDefaultEye(heritageId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The camera's current world-space eye. Settable so CC6b can react to a
|
||||
/// heritage change without reconstructing the camera.
|
||||
/// </summary>
|
||||
public Vector3 Eye
|
||||
{
|
||||
get => _eye;
|
||||
set => _eye = value;
|
||||
}
|
||||
|
||||
/// <summary>Re-derives <see cref="Eye"/> for the given heritage id (retail's <c>mHeritageGroup</c>).</summary>
|
||||
public void SetHeritage(uint heritageId) => _eye = ResolveDefaultEye(heritageId);
|
||||
|
||||
/// <summary>
|
||||
/// Retail default (zoomed-in) camera eye per heritage. All four profiles
|
||||
/// share <c>X=0</c>; only <c>(Y, Z)</c> — the AC world-space forward
|
||||
/// offset and height — vary. FOUR distinct profiles across the 13
|
||||
/// heritages, not five: standard heritages (Aluvian, Gharu'ndim, Sho,
|
||||
/// Viamontian, Shadowbound, Gearknight, Lugian, Empyrean, Penumbraen,
|
||||
/// Undead — everything except Tumerok/Olthoi/OlthoiAcid) share the SAME
|
||||
/// numeric offset as Gearknight's own dedicated branch in the decomp.
|
||||
/// </summary>
|
||||
public static Vector3 ResolveDefaultEye(uint heritageId) => heritageId switch
|
||||
{
|
||||
(uint)ChargenHeritageGroup.Olthoi => new Vector3(0f, -1.85000002f, 1.85000002f),
|
||||
(uint)ChargenHeritageGroup.OlthoiAcid => new Vector3(0f, -3.04999995f, 2.75f),
|
||||
(uint)ChargenHeritageGroup.Tumerok => new Vector3(0f, -0.850000024f, 1.64999998f),
|
||||
_ => new Vector3(0f, -0.550000012f, 1.64999998f),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Retail zoomed-OUT camera eye per heritage
|
||||
/// (<c>gmCGAppearancePage::ZoomOut @ 0x0047D050</c>, pseudo-C
|
||||
/// ~137671-137687). CC6a does not implement the zoom button (CC6b) —
|
||||
/// recorded here as the verified target CC6b's tween will animate
|
||||
/// toward. Olthoi/OlthoiAcid each keep their own dedicated profile;
|
||||
/// every other heritage — INCLUDING Tumerok, whose zoomed-IN profile is
|
||||
/// special-cased but whose zoomed-OUT is not — shares one value.
|
||||
/// </summary>
|
||||
public static Vector3 ResolveZoomedOutEye(uint heritageId) => heritageId switch
|
||||
{
|
||||
(uint)ChargenHeritageGroup.Olthoi => new Vector3(0f, -3.79999995f, 1.14999998f),
|
||||
(uint)ChargenHeritageGroup.OlthoiAcid => new Vector3(0f, -5.69999981f, 1.64999998f),
|
||||
_ => new Vector3(0f, -2.5f, 0.95f),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Seconds per 360° revolution for the continuous-rotation button
|
||||
/// (<c>gmCGAppearancePage::m_dRotationPerSec</c>, ctor pseudo-C
|
||||
/// ~137523-137524 / ~226652-226653: raw double bits low32=0x00000000,
|
||||
/// high32=0x40080000 → exactly 3.0 — the decompiler shows this cleanly,
|
||||
/// no reconstruction needed). Consumed by CC6b's rotation controller as
|
||||
/// <c>360f / RotationDegreesPerSecond</c> — NOT applied here; see this
|
||||
/// class's own doc comment on why rotation is not a camera concern.
|
||||
/// </summary>
|
||||
public const float RotationSecondsPerRevolution = 3.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Zoom tween duration in seconds
|
||||
/// (<c>gmCGAppearancePage::DoZoomAnimation @ 0x0047C960</c>'s
|
||||
/// reset-if-invalid default, cross-confirmed by <c>ZoomIn</c>/<c>ZoomOut</c>'s
|
||||
/// own <c>-0.1</c> sentinel write, which deliberately invalidates
|
||||
/// <c>m_dAnimDuration</c> so the very next <c>DoZoomAnimation</c> tick
|
||||
/// resets it to this same value). The campaign plan flagged this
|
||||
/// constant as decompiler-garbled (both sites split the raw double
|
||||
/// across two 32-bit stores, and the decompiler mis-renders the LOW
|
||||
/// dword's store as a bogus float literal instead of raw bits) — it is
|
||||
/// NOT unrecoverable: reinterpreting each garbled float literal as its
|
||||
/// own raw 32-bit pattern and pairing it with the store's (clean) high
|
||||
/// dword reconstructs an exact IEEE-754 double both times.
|
||||
/// <c>DoZoomAnimation</c>'s own reset path: low32 from
|
||||
/// <c>4.17232506e-08f</c> reinterpreted = <c>0x33333333</c>, high32 =
|
||||
/// <c>0x3fe33333</c> (clean) → exactly <b>0.6</b>. Cross-check via
|
||||
/// <c>ZoomIn</c>/<c>ZoomOut</c>'s sentinel: low32 from
|
||||
/// <c>-1.58818684e-23f</c> reinterpreted = <c>0x9999999A</c>, high32 =
|
||||
/// <c>0xbfb99999</c> (clean) → exactly <b>-0.1</b>, the well-known
|
||||
/// IEEE-754 bit pattern for -0.1 (<c>0xBFB999999999999A</c>) — confirming
|
||||
/// the reconstruction technique itself, not just this one value.
|
||||
/// </summary>
|
||||
public const float ZoomTweenDurationSeconds = 0.6f;
|
||||
|
||||
public float FovRadians { get; set; } = MathF.PI / 4f; // retail CreatureMode default, same as DollCamera.
|
||||
public float Near { get; set; } = 0.1f;
|
||||
public float Far { get; set; } = 50f;
|
||||
public float Aspect { get; set; } = 1f;
|
||||
|
||||
public Matrix4x4 View =>
|
||||
Matrix4x4.CreateLookAt(_eye, _eye + Vector3.UnitY, Up);
|
||||
|
||||
public Matrix4x4 Projection =>
|
||||
Matrix4x4.CreatePerspectiveFieldOfView(FovRadians, Aspect <= 0f ? 1f : Aspect, Near, Far);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal private-viewport adapter, mirroring <c>DollViewportCamera</c>'s
|
||||
/// role for <see cref="ChargenPreviewCamera"/>.
|
||||
/// </summary>
|
||||
internal sealed class ChargenPreviewViewportCamera : IPrivateEntityViewportCamera
|
||||
{
|
||||
private readonly ChargenPreviewCamera _camera;
|
||||
|
||||
public ChargenPreviewViewportCamera(uint heritageId = 0u)
|
||||
{
|
||||
_camera = new ChargenPreviewCamera(heritageId);
|
||||
}
|
||||
|
||||
public void SetHeritage(uint heritageId) => _camera.SetHeritage(heritageId);
|
||||
|
||||
public Vector3 Eye => _camera.Eye;
|
||||
public float FovRadians
|
||||
{
|
||||
get => _camera.FovRadians;
|
||||
set => _camera.FovRadians = value;
|
||||
}
|
||||
public float Near
|
||||
{
|
||||
get => _camera.Near;
|
||||
set => _camera.Near = value;
|
||||
}
|
||||
public float Far
|
||||
{
|
||||
get => _camera.Far;
|
||||
set => _camera.Far = value;
|
||||
}
|
||||
public float Aspect
|
||||
{
|
||||
get => _camera.Aspect;
|
||||
set => _camera.Aspect = value;
|
||||
}
|
||||
public Matrix4x4 View => _camera.View;
|
||||
public Matrix4x4 Projection => _camera.Projection;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue