The new value type collided with DatReaderWriter.Types.Frame (used in physics-adjacent code like ShadowShapeBuilder), which the structural fix (per-file using-aliases across 6 files) would have re-incurred in every later physics slice. Renamed the TYPE to CellFrame; the Position.Frame MEMBER keeps retail's name. Restored the 5 alias-only files to their pre-Slice-1 state; synced spec + plan. Core 1522 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
169 lines
6.2 KiB
C#
169 lines
6.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Vfx;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
using Xunit;
|
|
using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript;
|
|
|
|
namespace AcDream.Core.Tests.Streaming;
|
|
|
|
/// <summary>
|
|
/// Phase C.1.5b: verifies <see cref="GpuWorldState"/> fires
|
|
/// <see cref="EntityScriptActivator.OnCreate"/> /
|
|
/// <see cref="EntityScriptActivator.OnRemove"/> from the four
|
|
/// dat-hydration paths (AddLandblock, AddEntitiesToExistingLandblock,
|
|
/// RemoveLandblock, RemoveEntitiesFromLandblock), and that the
|
|
/// pending-bucket merge in AddLandblock does NOT double-fire for live
|
|
/// entities that already had OnCreate at <see cref="GpuWorldState.AppendLiveEntity"/>.
|
|
/// </summary>
|
|
public sealed class GpuWorldStateActivatorTests
|
|
{
|
|
private sealed class RecordingSink : IAnimationHookSink
|
|
{
|
|
public List<(uint EntityId, Vector3 Pos, AnimationHook Hook)> Calls = new();
|
|
public void OnHook(uint entityId, Vector3 worldPos, AnimationHook hook)
|
|
=> Calls.Add((entityId, worldPos, hook));
|
|
}
|
|
|
|
private sealed record Pipeline(
|
|
GpuWorldState State,
|
|
PhysicsScriptRunner Runner,
|
|
ParticleHookSink Sink,
|
|
RecordingSink Recording);
|
|
|
|
private static Pipeline BuildPipeline(uint scriptId)
|
|
{
|
|
var script = new DatPhysicsScript();
|
|
script.ScriptData.Add(new PhysicsScriptData
|
|
{
|
|
StartTime = 0.0,
|
|
Hook = new CreateParticleHook { EmitterInfoId = 100u, Offset = new Frame() },
|
|
});
|
|
var table = new Dictionary<uint, DatPhysicsScript> { [scriptId] = script };
|
|
|
|
var registry = new EmitterDescRegistry();
|
|
var system = new ParticleSystem(registry);
|
|
var sink = new ParticleHookSink(system);
|
|
var recording = new RecordingSink();
|
|
var runner = new PhysicsScriptRunner(id => table.TryGetValue(id, out var s) ? s : null, recording);
|
|
var activator = new EntityScriptActivator(runner, sink,
|
|
_ => new ScriptActivationInfo(scriptId, Array.Empty<Matrix4x4>()));
|
|
|
|
var state = new GpuWorldState(entityScriptActivator: activator);
|
|
return new Pipeline(state, runner, sink, recording);
|
|
}
|
|
|
|
private static LoadedLandblock MakeStubLandblock(uint canonicalId, params WorldEntity[] entities)
|
|
=> new(canonicalId, new LandBlock(), entities);
|
|
|
|
private static WorldEntity DatHydrated(uint id, Vector3 pos) => new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = 0u,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = pos,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
private static WorldEntity Live(uint serverGuid, Vector3 pos) => new()
|
|
{
|
|
Id = serverGuid,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = pos,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
[Fact]
|
|
public void AddLandblock_FiresActivatorForDatHydratedEntity()
|
|
{
|
|
var p = BuildPipeline(scriptId: 0xAAu);
|
|
var entity = DatHydrated(id: 0x40A9B401u, pos: new Vector3(1, 2, 3));
|
|
var lb = MakeStubLandblock(0xA9B4FFFFu, entity);
|
|
|
|
p.State.AddLandblock(lb);
|
|
|
|
// Tick fires the CreateParticleHook into RecordingSink.
|
|
p.Runner.Tick(0.001f);
|
|
Assert.Single(p.Recording.Calls);
|
|
Assert.Equal(0x40A9B401u, p.Recording.Calls[0].EntityId);
|
|
Assert.Equal(new Vector3(1, 2, 3), p.Recording.Calls[0].Pos);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddLandblock_DoesNotDoubleFire_OnPendingMerge()
|
|
{
|
|
// Live entity (ServerGuid!=0) arrives via AppendLiveEntity first —
|
|
// OnCreate fires once at that point. Then AddLandblock for the
|
|
// same canonical id pulls the pending entity into the loaded list.
|
|
// The new fire-site MUST NOT call OnCreate again (the live entity
|
|
// is filtered out by ServerGuid != 0).
|
|
var p = BuildPipeline(scriptId: 0xAAu);
|
|
var live = Live(serverGuid: 0xCAFEu, pos: Vector3.Zero);
|
|
|
|
p.State.AppendLiveEntity(0xA9B4FFFFu, live);
|
|
var emptyLb = MakeStubLandblock(0xA9B4FFFFu);
|
|
p.State.AddLandblock(emptyLb);
|
|
|
|
p.Runner.Tick(0.001f);
|
|
Assert.Single(p.Recording.Calls); // exactly one — no double-fire
|
|
Assert.Equal(0xCAFEu, p.Recording.Calls[0].EntityId);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveLandblock_FiresOnRemoveForDatHydratedEntity()
|
|
{
|
|
var p = BuildPipeline(scriptId: 0xAAu);
|
|
var entity = DatHydrated(id: 0x40A9B401u, pos: Vector3.Zero);
|
|
var lb = MakeStubLandblock(0xA9B4FFFFu, entity);
|
|
|
|
p.State.AddLandblock(lb);
|
|
// Don't Tick: Play queued the script in _active immediately; ticking
|
|
// would fire its single StartTime=0 hook and self-remove the script
|
|
// before we can observe RemoveLandblock cleaning it up.
|
|
Assert.Equal(1, p.Runner.ActiveScriptCount);
|
|
|
|
p.State.RemoveLandblock(0xA9B4FFFFu);
|
|
Assert.Equal(0, p.Runner.ActiveScriptCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddEntitiesToExistingLandblock_FiresActivatorForEachPromoted()
|
|
{
|
|
var p = BuildPipeline(scriptId: 0xAAu);
|
|
var emptyLb = MakeStubLandblock(0xA9B4FFFFu);
|
|
p.State.AddLandblock(emptyLb);
|
|
|
|
var promoted = new[]
|
|
{
|
|
DatHydrated(id: 0x40A9B401u, pos: Vector3.Zero),
|
|
DatHydrated(id: 0x40A9B402u, pos: Vector3.UnitX),
|
|
};
|
|
p.State.AddEntitiesToExistingLandblock(0xA9B4FFFFu, promoted);
|
|
|
|
p.Runner.Tick(0.001f);
|
|
Assert.Equal(2, p.Recording.Calls.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveEntitiesFromLandblock_FiresOnRemoveForDatHydratedEntities()
|
|
{
|
|
var p = BuildPipeline(scriptId: 0xAAu);
|
|
var entity = DatHydrated(id: 0x40A9B401u, pos: Vector3.Zero);
|
|
var lb = MakeStubLandblock(0xA9B4FFFFu, entity);
|
|
p.State.AddLandblock(lb);
|
|
// Don't Tick: see comment in RemoveLandblock_FiresOnRemoveForDatHydratedEntity.
|
|
Assert.Equal(1, p.Runner.ActiveScriptCount);
|
|
|
|
p.State.RemoveEntitiesFromLandblock(0xA9B4FFFFu);
|
|
Assert.Equal(0, p.Runner.ActiveScriptCount);
|
|
}
|
|
}
|