acdream/src/AcDream.Core/Physics/Motion/FrameOps.cs
Erik f961d70023 feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.

Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-20 09:10:31 +02:00

132 lines
5.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Numerics;
using DatReaderWriter.Types;
namespace AcDream.Core.Physics.Motion;
/// <summary>
/// R1-P3 — verbatim ports of retail's <c>Frame</c> rotation helpers used by
/// the CSequence physics path (oracle: raw decomp reads 2026-07-02).
///
/// <list type="bullet">
/// <item><c>Frame::grotate</c> (0x005357a0, pc:319782): build the
/// axis-angle quaternion from a GLOBAL rotation vector (angle = |v|,
/// axis = v/|v|, half-angle sin/cos) and PREMULTIPLY it onto the frame's
/// orientation — the incremental rotation is applied in world space.
/// Rotations with |v|² &lt; F_EPSILON² are skipped.</item>
/// <item><c>Frame::rotate</c> (0x004525b0, pc:91477): map the LOCAL
/// rotation vector through the frame's local→global rotation
/// (m_fl2gv — our quaternion), then <c>grotate</c>.</item>
/// </list>
/// </summary>
public static class FrameOps
{
/// <summary>Retail F_EPSILON (0.000199999995f); grotate gates on its
/// square against |v|².</summary>
public const float FEpsilon = 0.000199999995f;
/// <summary>
/// Retail <c>Frame::set_rotate</c> (0x00535080). The candidate is
/// normalized in extended precision, then the complete frame is checked
/// by <c>Frame::IsValid</c> (0x00534ED0). If that check fails, retail
/// restores the previous quaternion instead of allowing NaNs to poison
/// the live object transform.
/// </summary>
public static Quaternion SetRotate(
Vector3 frameOrigin,
Quaternion previous,
Quaternion candidate)
{
double lengthSquared =
((double)candidate.W * candidate.W)
+ ((double)candidate.X * candidate.X)
+ ((double)candidate.Y * candidate.Y)
+ ((double)candidate.Z * candidate.Z);
double inverseLength = 1.0 / Math.Sqrt(lengthSquared);
var normalized = new Quaternion(
(float)(candidate.X * inverseLength),
(float)(candidate.Y * inverseLength),
(float)(candidate.Z * inverseLength),
(float)(candidate.W * inverseLength));
if (float.IsNaN(frameOrigin.X)
|| float.IsNaN(frameOrigin.Y)
|| float.IsNaN(frameOrigin.Z)
|| float.IsNaN(normalized.W)
|| float.IsNaN(normalized.X)
|| float.IsNaN(normalized.Y)
|| float.IsNaN(normalized.Z))
{
return previous;
}
float normalizedLengthSquared = normalized.LengthSquared();
return !float.IsNaN(normalizedLengthSquared)
&& MathF.Abs(normalizedLengthSquared - 1f) < FEpsilon * 5f
? normalized
: previous;
}
/// <summary><c>Frame::grotate</c> — incremental WORLD-space rotation.</summary>
public static void GRotate(Frame frame, Vector3 rotationGlobal)
{
float magSq = rotationGlobal.LengthSquared();
if (magSq < FEpsilon * FEpsilon)
return;
float angle = MathF.Sqrt(magSq);
float invMag = 1f / angle;
float half = angle * 0.5f;
float s = MathF.Sin(half);
float c = MathF.Cos(half);
// Retail's set_rotate receives the raw Hamilton product r ⊗ q
// (r = the new axis-angle quat, q = the current orientation) —
// rotation applied in GLOBAL space. System.Numerics
// Quaternion.Multiply(r, q) is that product with
// Transform(v, r*q) == r-applied-after-q. set_rotate renormalizes.
var r = new Quaternion(
rotationGlobal.X * s * invMag,
rotationGlobal.Y * s * invMag,
rotationGlobal.Z * s * invMag,
c);
frame.Orientation = SetRotate(
frame.Origin,
frame.Orientation,
Quaternion.Multiply(r, frame.Orientation));
}
/// <summary><c>Frame::rotate</c> — LOCAL rotation vector, mapped to
/// global through the orientation, then <see cref="GRotate"/>.</summary>
public static void Rotate(Frame frame, Vector3 rotationLocal)
=> GRotate(frame, Vector3.Transform(rotationLocal, frame.Orientation));
/// <summary>
/// <c>Frame::combine</c> as used by the CSequence pose path (ACE
/// <c>AFrame.Combine</c>, verified against the pre-R1 acdream port):
/// apply the pose — origin += rotate(pos.Origin by orientation), then
/// orientation ∘= pos.Orientation.
/// </summary>
public static void Combine(Frame frame, Frame pos)
{
frame.Origin += Vector3.Transform(pos.Origin, frame.Orientation);
frame.Orientation = SetRotate(
frame.Origin,
frame.Orientation,
frame.Orientation * pos.Orientation);
}
/// <summary>
/// <c>Frame::subtract1</c> — un-apply the pose: orientation ∘=
/// conj(pos.Orientation) FIRST, then origin = rotate(pos.Origin by the
/// UPDATED orientation) (inverse order of <see cref="Combine"/>).
/// </summary>
public static void Subtract1(Frame frame, Frame pos)
{
frame.Orientation = SetRotate(
frame.Origin,
frame.Orientation,
frame.Orientation * Quaternion.Conjugate(pos.Orientation));
frame.Origin -= Vector3.Transform(pos.Origin, frame.Orientation);
}
}