acdream accumulated every CreateObject from every town visited and never pruned by distance/time (only on server DeleteObject / respawn de-dup), so the entity tables + the O(N^2) TickAnimations scan grew with each hop and sank FPS (confirmed in Release). Faithful port of holtburger liveness.rs (ACE_DESTRUCTION_TIMEOUT_SECS=25, CONSERVATIVE_VISIBILITY_DISTANCE_M=384): a world entity is evicted only after being >384m AND outside the 3x3 landblock neighborhood for 25s continuous (arm-on-leave / clear-on-return). Logic in a pure, unit-tested EntityVisibilityCuller; GameWindow wires a 1Hz tick that snapshots the world entities + player and tears each evicted guid down through the existing pruner. Player + held/equipped/contained items are excluded (player by guid; inventory items never carry a world position so they never enter the culled map). A re-created object starts fresh (deadline cleared on remove). Skipped during a teleport hold (frozen player position). AD-32 registered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
3.8 KiB
C#
108 lines
3.8 KiB
C#
using System.Numerics;
|
||
using AcDream.App.World;
|
||
using Xunit;
|
||
|
||
namespace AcDream.App.Tests.World;
|
||
|
||
/// <summary>
|
||
/// 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).
|
||
/// </summary>
|
||
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
|
||
}
|
||
}
|