using System.Numerics; using AcDream.App.World; using Xunit; namespace AcDream.App.Tests.World; /// /// AP-48 cull (holtburger liveness.rs port): visible = 3×3 landblock neighborhood OR ≤384 m; /// evict only after 25 s continuously out-of-visibility (arm-on-leave / clear-on-return). /// public sealed class EntityVisibilityCullerTests { private static readonly EntityCullInfo Player = new(0x50000001u, new Vector3(0, 0, 0), 169, 180, true); // ---- IsVisible ---- [Fact] public void Within384m_isVisible_byDistance_evenInAFarLandblock() => Assert.True(EntityVisibilityCuller.IsVisible( new Vector3(50, 0, 0), 100, 100, true, new Vector3(0, 0, 0), 169, 180)); [Fact] public void AdjacentLandblock_isVisible_evenWhenFarByPosition() => Assert.True(EntityVisibilityCuller.IsVisible( new Vector3(5000, 0, 0), 169, 181, true, new Vector3(0, 0, 0), 169, 180)); [Fact] public void FarAndNotAdjacent_isNotVisible() => Assert.False(EntityVisibilityCuller.IsVisible( new Vector3(5000, 0, 0), 100, 100, true, new Vector3(0, 0, 0), 169, 180)); // ---- Step lifecycle ---- [Fact] public void Step_visible_clearsDeadlineAndKeeps() { var (dl, evict) = EntityVisibilityCuller.Step(visible: true, deadline: 123.0, now: 200.0); Assert.Null(dl); Assert.False(evict); } [Fact] public void Step_notVisible_unarmed_arms() { var (dl, evict) = EntityVisibilityCuller.Step(visible: false, deadline: null, now: 100.0); Assert.Equal(100.0 + EntityVisibilityCuller.DestructionTimeoutSec, dl); Assert.False(evict); } [Fact] public void Step_notVisible_armedNotExpired_keeps() { var (dl, evict) = EntityVisibilityCuller.Step(visible: false, deadline: 125.0, now: 110.0); Assert.Equal(125.0, dl); Assert.False(evict); } [Fact] public void Step_notVisible_armedExpired_evicts() { var (dl, evict) = EntityVisibilityCuller.Step(visible: false, deadline: 125.0, now: 125.0); Assert.Null(dl); Assert.True(evict); } // ---- Tick integration ---- [Fact] public void Tick_neverEvictsTheLocalPlayer() { var c = new EntityVisibilityCuller(); var ents = new[] { Player }; c.Tick(ents, Player, now: 0); c.Tick(ents, Player, now: 1000); Assert.Empty(c.Tick(ents, Player, now: 1_000_000)); Assert.Equal(0, c.ArmedCount); } [Fact] public void Tick_armsThenEvicts_aFarStaleEntity_afterTimeout() { var c = new EntityVisibilityCuller(); var far = new EntityCullInfo(0xABCDu, new Vector3(5000, 0, 0), 10, 10, true); var ents = new[] { far }; Assert.Empty(c.Tick(ents, Player, now: 0)); // arm at 0 + 25 Assert.Equal(1, c.ArmedCount); Assert.Empty(c.Tick(ents, Player, now: 24)); // not expired var evicted = c.Tick(ents, Player, now: 25); // expired → evict Assert.Equal(new[] { 0xABCDu }, evicted); Assert.Equal(0, c.ArmedCount); // deadline cleared on evict } [Fact] public void Tick_clearsDeadline_whenEntityReturnsToVisibility_beforeTimeout() { var c = new EntityVisibilityCuller(); var far = new EntityCullInfo(0xABCDu, new Vector3(5000, 0, 0), 10, 10, true); var near = far with { Pos = new Vector3(50, 0, 0) }; // within 384 m → visible Assert.Empty(c.Tick(new[] { far }, Player, now: 0)); // arm Assert.Equal(1, c.ArmedCount); Assert.Empty(c.Tick(new[] { near }, Player, now: 10)); // returned → clear Assert.Equal(0, c.ArmedCount); Assert.Empty(c.Tick(new[] { near }, Player, now: 100)); // never evicted } }