using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Vfx; using AcDream.Core.Vfx; using AcDream.Core.World; using DatReaderWriter.Types; using Xunit; using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript; namespace AcDream.App.Tests.Rendering; /// /// Production sky default-script lifecycle per /// GameSky::CreateDeletePhysicsObjects @0x005073C0: a slot keeps its /// running script while (gfx id, properties) are unchanged — across day-group /// swaps included — and a mismatch or window exit stops it. Research: /// docs/research/2026-08-23-sky-default-script-port.md. /// public sealed class SkyPesFrameControllerTests { private const uint AuroraSetup = 0x02000714u; private const uint AuroraScript = 0x330007DBu; private sealed class Harness { public readonly List ResolvedScriptIds = []; public readonly List Diagnostics = []; public readonly PhysicsScriptRunner Runner; public readonly SkyPesFrameController Controller; public Harness() { var registry = new EmitterDescRegistry(); var system = new ParticleSystem(registry, new Random(42)); var poses = new EntityEffectPoseRegistry(); var sink = new ParticleHookSink(system, poses); Runner = new PhysicsScriptRunner( id => { ResolvedScriptIds.Add(id); var script = new DatPhysicsScript(); script.ScriptData.Add(new PhysicsScriptData { StartTime = 0.0, Hook = new SoundHook(), }); return script; }, sink); Controller = new SkyPesFrameController( Runner, sink, poses, effects: null, Diagnostics.Add); } } private static SkyObjectData Carrier( uint gfxObjId = AuroraSetup, uint scriptId = AuroraScript, uint pesColumn = AuroraScript, uint properties = 0u, float begin = 0f, float end = 0f) => new() { GfxObjId = gfxObjId, DefaultScriptId = scriptId, PesObjectId = pesColumn, Properties = properties, BeginTime = begin, EndTime = end, }; private static DayGroupData Group(params SkyObjectData[] objects) => new() { Name = "Test", SkyObjects = objects, }; [Fact] public void PlaysOncePerSlotAndPersistsAcrossEquivalentDayGroups() { var h = new Harness(); h.Controller.Update(0.1f, Group(Carrier()), Vector3.Zero); Assert.Equal([AuroraScript], h.ResolvedScriptIds); Assert.Equal(1, h.Runner.ActiveScriptCount); // A different DayGroupData instance with the SAME slot identity is // retail's midnight day-group swap keeping the aurora carrier: the // running script must survive, not restart. The runner caches // resolved scripts, so the replay probe is ActiveScriptCount — // PlayDirect stacks one scheduled script per play. h.Controller.Update(0.9f, Group(Carrier()), Vector3.One * 10f); Assert.Equal(1, h.Runner.ActiveScriptCount); } [Fact] public void SlotIdentityChangeStopsTheOldScriptAndPlaysTheNew() { var h = new Harness(); h.Controller.Update(0.1f, Group(Carrier()), Vector3.Zero); h.Controller.Update( 0.1f, Group(Carrier(gfxObjId: 0x02000589u, scriptId: 0x3300042Cu, pesColumn: 0x3300042Cu)), Vector3.Zero); Assert.Equal([AuroraScript, 0x3300042Cu], h.ResolvedScriptIds); Assert.Equal(1, h.Runner.ActiveScriptCount); } [Fact] public void LeavingTheVisibilityWindowStopsTheScriptAndReentryReplays() { var h = new Harness(); SkyObjectData windowed() => Carrier(begin: 0.2f, end: 0.4f); h.Controller.Update(0.3f, Group(windowed()), Vector3.Zero); Assert.Equal(1, h.Runner.ActiveScriptCount); h.Controller.Update(0.5f, Group(windowed()), Vector3.Zero); Assert.Equal(0, h.Runner.ActiveScriptCount); // Re-entry replays (the runner's script cache absorbs the resolver // call; the fresh scheduled script is the observable). h.Controller.Update(0.25f, Group(windowed()), Vector3.Zero); Assert.Equal(1, h.Runner.ActiveScriptCount); } [Fact] public void SetupDefaultScriptIsTheSourceAndAMismatchedPesColumnLogsOnce() { var h = new Harness(); var group = Group(Carrier(scriptId: AuroraScript, pesColumn: 0x33000999u)); h.Controller.Update(0.1f, group, Vector3.Zero); h.Controller.Update(0.2f, group, Vector3.Zero); Assert.Equal([AuroraScript], h.ResolvedScriptIds); string diagnostic = Assert.Single(h.Diagnostics); Assert.Contains("0x330007DB", diagnostic, StringComparison.Ordinal); Assert.Contains("0x33000999", diagnostic, StringComparison.Ordinal); } [Fact] public void CarriersWithoutADefaultScriptNeverPlay() { var h = new Harness(); h.Controller.Update( 0.1f, Group(Carrier(gfxObjId: 0x010015F0u, scriptId: 0u, pesColumn: 0u)), Vector3.Zero); Assert.Empty(h.ResolvedScriptIds); Assert.Equal(0, h.Runner.ActiveScriptCount); } [Fact] public void NullDayGroupStopsEverything() { var h = new Harness(); h.Controller.Update(0.1f, Group(Carrier()), Vector3.Zero); Assert.Equal(1, h.Runner.ActiveScriptCount); h.Controller.Update(0.1f, null, Vector3.Zero); Assert.Equal(0, h.Runner.ActiveScriptCount); } }