using System.Numerics;
using AcDream.Core.Physics.Motion;
namespace AcDream.App.Rendering;
///
/// Retail's toggle direction enum
/// (gmBarberUI::ERotateDirection/gmCGAppearancePage::ERotateDirection
/// typedef alias, acclient.h:6848-6852,6960): Invalid=0,
/// Clockwise=1, CounterClockwise=2.
///
internal enum ChargenRotateDirection
{
Invalid = 0,
Clockwise = 1,
CounterClockwise = 2,
}
///
/// Presentation-free port of gmCGAppearancePage::Rotate
/// (0x0047CB50) + DoRotation (0x0047CA80) — the
/// button-toggled continuous rotation retail applies to the preview
/// CHARACTER's heading (CPhysicsObj::set_heading inside
/// gmCG3DView::Update, pseudo-C ~0x0047eecf1), not the camera (see
/// 's own doc comment on why rotation
/// lives here instead). Retail drives once per frame from
/// a global-message-3 tick while is set
/// (gmCGAppearancePage::ListenToGlobalMessage @ 0x0047CED0); the
/// CC6b page-mount half will bind the Rotate Clockwise/Counter-Clockwise
/// buttons to and the render loop to .
///
internal sealed class ChargenPreviewRotationController
{
///
/// Rotate's explicit sentinel write
/// (this->m_dLastRotateTime = -1.0, pseudo-C ~0x0047cba7/0x0047cbb1
/// — the high dword 0xbff00000 paired with a zero low dword is the
/// exact IEEE-754 bit pattern for -1.0) — invalidates the
/// timestamp so the very next resets it to "now"
/// (a zero-length first delta) instead of computing a huge jump from a
/// stale or never-set value.
///
private const double InvalidTimeSentinel = -1.0;
private double _lastRotateTime = InvalidTimeSentinel;
private ChargenRotateDirection _direction = ChargenRotateDirection.Invalid;
private bool _rotating;
public bool IsRotating => _rotating;
public ChargenRotateDirection Direction => _direction;
/// Retail's m_fCurHeading, degrees, ctor default 0 —
/// applied to the preview entity via MoveToMath.SetHeading
/// (CPhysicsObj::set_heading's exact port).
public float HeadingDegrees { get; private set; }
///
/// gmCGAppearancePage::Rotate @ 0x0047CB50: pressing the SAME
/// direction a second time while already rotating STOPS rotation
/// (retail's button-toggle UX); any other press (opposite direction, or
/// starting from stopped) sets that direction and (re)starts,
/// invalidating m_dLastRotateTime per this class's own sentinel
/// doc.
///
public void Toggle(ChargenRotateDirection direction)
{
if (_rotating && direction == _direction)
{
_rotating = false;
return;
}
_direction = direction;
_lastRotateTime = InvalidTimeSentinel;
_rotating = true;
}
///
/// gmCGAppearancePage::DoRotation @ 0x0047CA80: per-tick
/// deltaDegrees = ((now - lastRotateTime) / RotationSecondsPerRevolution)
/// * 360, added for
/// and subtracted for every other direction (pseudo-C ~0x0047cacd:
/// if (m_eRotateDir != ECG_ROTATE_CLOCKWISE) heading -= delta; else
/// heading += delta;), then a SINGLE-PASS clamp back into
/// [0, 360) — not a full modulo loop; retail's own tail only
/// adds/subtracts 360 once (pseudo-C ~0x0047caf3-0x0047cb31), which is
/// exactly enough for any realistic per-frame delta and is reproduced
/// here verbatim rather than "improved" into a `%=`. Fix round F3: Binary
/// Ninja literally renders x87_r7_1 = x87_r6_3 at 0x0047CAEB
/// inside the counter-clockwise branch — reassigning the local that held
/// the "now" timestamp to the just-computed delta-degrees value — which
/// would make the 0x0047CB3D store into m_dLastRotateTime
/// write delta-degrees instead of the timestamp for CCW only; that is an
/// x87-FPU-stack modeling artifact of the decompiler, not real retail
/// behavior (a shipped feature where every counter-clockwise rotation
/// visibly diverges from clockwise is implausible, and
/// claude-memory/feedback_bn_decomp_field_names.md names exactly
/// this x87-stack-register mislabeling as a known decompiler artifact
/// class), so this port stores now into _lastRotateTime
/// unconditionally in BOTH directions.
///
public void Tick(double now)
{
if (!_rotating)
return;
if (_lastRotateTime <= 0d)
_lastRotateTime = now;
double deltaDegrees = ((now - _lastRotateTime) / ChargenPreviewCamera.RotationSecondsPerRevolution) * 360.0;
HeadingDegrees = _direction == ChargenRotateDirection.Clockwise
? HeadingDegrees + (float)deltaDegrees
: HeadingDegrees - (float)deltaDegrees;
if (HeadingDegrees < 0f)
HeadingDegrees += 360f;
if (HeadingDegrees > 360f)
HeadingDegrees -= 360f;
_lastRotateTime = now;
}
/// CPhysicsObj::set_heading's exact quaternion
/// construction — the SAME shared Core primitive retail movement already
/// ports ().
public Quaternion ToOrientation() =>
MoveToMath.SetHeading(Quaternion.Identity, HeadingDegrees);
}