using System; using System.Numerics; using AcDream.Core.CharGen; namespace AcDream.App.Rendering; /// /// Heritage-parameterized camera for the chargen 3D preview /// (gmCG3DView, Appearance page viewport 0x100003bb / Summary /// 0x10000406). Retail-exact eye positions, ported from /// gmCGAppearancePage::Update @ 0x0047E8F0 (pseudo-C ~139037-139114, /// which sets m_vectTargPosition/m_vectCurPosition 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 gmCGAppearancePage::ZoomIn @ /// 0x0047CF00 (pseudo-C ~137618-137638). Direction is always /// (0,0,0)CreatureMode::SetCameraDirection resets the view /// frame to IDENTITY — the SAME zero-yaw/zero-pitch convention /// 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. /// /// /// Rotation is NOT a camera property. Retail's continuous-rotation /// button (gmCGAppearancePage::DoRotation @ 0x0047CA80) advances a /// HEADING applied to the preview CHARACTER (CPhysicsObj::set_heading /// inside gmCG3DView::Update, pseudo-C ~242088) — the camera's own /// position/direction never change during a rotation. The heading itself /// lives on (CC6b: the /// DoRotation/Rotate port) and is applied to the entity via /// ChargenPreviewEntityBuilder.TryBuild/TryBuildAnimated's /// heading parameter, not here; this class stays a fixed-per-heritage /// eye, exactly like retail's own camera. /// (CC6b: the ZoomIn/ZoomOut/DoZoomAnimation port) DOES /// mutate this class's — zoom is a camera concern, unlike /// rotation. /// /// 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); } /// /// The camera's current world-space eye. Settable so CC6b can react to a /// heritage change without reconstructing the camera. /// public Vector3 Eye { get => _eye; set => _eye = value; } /// Re-derives for the given heritage id (retail's mHeritageGroup). public void SetHeritage(uint heritageId) => _eye = ResolveDefaultEye(heritageId); /// /// Retail default (zoomed-in) camera eye per heritage. All four profiles /// share X=0; only (Y, Z) — 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. /// 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), }; /// /// Retail zoomed-OUT camera eye per heritage /// (gmCGAppearancePage::ZoomOut @ 0x0047D050, 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. /// 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), }; /// /// Seconds per 360° revolution for the continuous-rotation button /// (gmCGAppearancePage::m_dRotationPerSec, 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). Retail's own per-tick formula /// (gmCGAppearancePage::DoRotation @ 0x0047CA80, pseudo-C /// ~0x0047CAC7): deltaDegrees = ((now - lastRotateTime) / /// RotationSecondsPerRevolution) * 360 — CC6b's rotation controller /// consumes this constant in exactly that shape, not as a /// degrees-per-second rate. NOT applied here; see this class's own doc /// comment on why rotation is not a camera concern. /// public const float RotationSecondsPerRevolution = 3.0f; /// /// Zoom tween duration in seconds /// (gmCGAppearancePage::DoZoomAnimation @ 0x0047C960's /// reset-if-invalid default, cross-confirmed by ZoomIn/ZoomOut's /// own -0.1 sentinel write, which deliberately invalidates /// m_dAnimDuration so the very next DoZoomAnimation 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. /// DoZoomAnimation's own reset path: low32 from /// 4.17232506e-08f reinterpreted = 0x33333333, high32 = /// 0x3fe33333 (clean) → exactly 0.6. Cross-check via /// ZoomIn/ZoomOut's sentinel: low32 from /// -1.58818684e-23f reinterpreted = 0x9999999A, high32 = /// 0xbfb99999 (clean) → exactly -0.1, the well-known /// IEEE-754 bit pattern for -0.1 (0xBFB999999999999A) — confirming /// the reconstruction technique itself, not just this one value. /// 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); } /// /// Internal private-viewport adapter, mirroring DollViewportCamera's /// role for . /// internal sealed class ChargenPreviewViewportCamera : IPrivateEntityViewportCamera { private readonly ChargenPreviewCamera _camera; public ChargenPreviewViewportCamera(uint heritageId = 0u) { _camera = new ChargenPreviewCamera(heritageId); } /// /// CC6b-MOUNT seam: wraps an EXTERNALLY-owned /// instead of constructing a private one. /// needs a settable to tween — the /// other constructor's private _camera field is unreachable from /// outside this class, so the page-mount composition (which owns the /// zoom controller) must supply the SAME camera instance both this /// adapter and the zoom controller mutate/read. /// public ChargenPreviewViewportCamera(ChargenPreviewCamera camera) { _camera = camera ?? throw new ArgumentNullException(nameof(camera)); } 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; }