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);
}
}