using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
namespace AcDream.Content.Tests;
///
/// Pins the data the ambient flyers (circling birds, flitting butterflies)
/// depend on, against the installed retail DATs.
///
///
///
/// A Static object whose Setup declares a DefaultAnimation joins retail's
/// CPhysics::static_animating_objects workset
/// (CPhysicsObj::InitDefaults @ 0x00513A7B) and is driven by
/// animate_static_object @ 0x00513DF0, whose only motion step is
/// Frame::grotate(&m_position.frame, &m_omegaVector). That
/// vector is written by exactly one thing:
/// SetOmegaHook::Execute @ 0x00526F30 → CPhysicsObj::set_omega
/// @ 0x0050F6D0.
///
///
/// So these objects move ONLY if the SetOmega hook is honoured. acdream
/// decoded the hook and then ignored it, leaving m_omegaVector at zero: the
/// wings animated and nothing flew.
///
///
/// This test exists because the fix rests on two claims about shipped data
/// that are invisible from the code: that the authored omegas are pure YAW,
/// and that the meshes hang far off the axis they spin about. Rotation only
/// reads as flight because of the second one — a bird spinning about its own
/// centre would just pirouette. If a dat ever contradicts either, the fix is
/// wrong and this should say so rather than the behaviour quietly changing.
///
///
[Trait("Lane", "InstalledDat")]
public sealed class InstalledStaticAnimatingOmegaTests
{
[Fact]
public void EverySetOmegaAnimationIsPureYawOnAMeshOffsetFromItsAxis()
{
string? datDir = ContentConformanceDats.ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
var found = new List<(uint Setup, float Yaw, double Radius)>();
foreach (uint id in dats.Portal.Tree.Select(e => e.Id).Where(i => (i >> 24) == 0x02))
{
if (!dats.Portal.TryGet(id, out Setup? setup) || setup is null)
continue;
uint animId = setup.DefaultAnimation?.DataId ?? 0u;
if (animId == 0
|| !dats.Portal.TryGet(animId, out Animation? anim)
|| anim is null)
{
continue;
}
foreach (var frame in anim.PartFrames)
foreach (var hook in frame.Hooks)
{
if (hook is not SetOmegaHook set)
continue;
// Pure yaw: a non-zero X or Y would tumble the object, and
// the circle-tracing reading of this mechanism would fail.
Assert.Equal(0f, set.Axis.X, 5);
Assert.Equal(0f, set.Axis.Y, 5);
Assert.NotEqual(0f, set.Axis.Z);
double radius = 0d;
foreach (var placement in setup.PlacementFrames.Values)
foreach (var af in placement.Frames)
radius = Math.Max(
radius,
Math.Sqrt((af.Origin.X * af.Origin.X)
+ (af.Origin.Y * af.Origin.Y)));
found.Add((id, set.Axis.Z, radius));
}
}
Assert.NotEmpty(found);
// Every one of them hangs metres off its own spin axis — that offset IS
// the flight radius. One metre would be a pirouette, not a circuit.
foreach ((uint setupId, _, double radius) in found)
{
Assert.True(
radius > 1d,
$"setup 0x{setupId:X8} spins about an axis only {radius:0.###}m "
+ "from its mesh; rotation would not read as flight.");
}
}
}