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 @ 0x00526F30CPhysicsObj::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."); } } }