refactor(world): separate live lifetime from spatial buckets
Introduce LiveEntityRuntime as the canonical owner of each accepted server-object incarnation, stable local identity, timestamped state, parent relations, runtime components, and exactly-once teardown. Split logical registration from rebucketing so pending landblocks, equipment attachment, pickup re-entry, and GUID replacement reuse the same entity and effect owners. Keep canonical materialized and visible target/radar views distinct, preserve retail leave_world versus exit_world semantics, gate root simulation while cell-less, and track transitional pre-Create F754 owners through delete and session reset. Remove stale-spawn rehydration and make GpuWorldState spatial-only for live objects. Add lifecycle, generation, pending, unload, attachment, event-publication, local-ID, rollback, and effect-cleanup coverage; update architecture, milestones, memory, and the divergence register. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
8a5d77f7f4
commit
8dd996053d
24 changed files with 2449 additions and 631 deletions
|
|
@ -20,7 +20,7 @@ namespace AcDream.Core.Tests.Streaming;
|
|||
/// 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"/>.
|
||||
/// entities, whose logical activation belongs to LiveEntityRuntime.
|
||||
/// </summary>
|
||||
public sealed class GpuWorldStateActivatorTests
|
||||
{
|
||||
|
|
@ -99,9 +99,9 @@ public sealed class GpuWorldStateActivatorTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void AddLandblock_DoesNotDoubleFire_OnPendingMerge()
|
||||
public void PendingLiveProjection_DoesNotFireLogicalActivator()
|
||||
{
|
||||
// Live entity (ServerGuid!=0) arrives via AppendLiveEntity first —
|
||||
// Live entity (ServerGuid!=0) arrives via spatial projection 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
|
||||
|
|
@ -109,13 +109,12 @@ public sealed class GpuWorldStateActivatorTests
|
|||
var p = BuildPipeline(scriptId: 0xAAu);
|
||||
var live = Live(serverGuid: 0xCAFEu, pos: Vector3.Zero);
|
||||
|
||||
p.State.AppendLiveEntity(0xA9B4FFFFu, live);
|
||||
p.State.PlaceLiveEntityProjection(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);
|
||||
Assert.Empty(p.Recording.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public class GpuWorldStateTests
|
|||
};
|
||||
|
||||
[Fact]
|
||||
public void AppendLiveEntity_LandblockAlreadyLoaded_AppendsImmediately()
|
||||
public void PlaceLiveEntityProjection_LandblockAlreadyLoaded_AppendsImmediately()
|
||||
{
|
||||
var state = new GpuWorldState();
|
||||
state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu));
|
||||
|
|
@ -37,20 +37,20 @@ public class GpuWorldStateTests
|
|||
// Server sends a spawn at landblock 0xA9B40011 — same landblock,
|
||||
// cell index 0x0011. Canonicalize drops the cell index, lookup
|
||||
// succeeds, entity lands in the loaded landblock immediately.
|
||||
state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(42));
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, MakeStubEntity(42));
|
||||
|
||||
Assert.Single(state.Entities);
|
||||
Assert.Equal(0u, (uint)state.PendingLiveEntityCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendLiveEntity_LandblockNotLoaded_ParksInPending()
|
||||
public void PlaceLiveEntityProjection_LandblockNotLoaded_ParksInPending()
|
||||
{
|
||||
var state = new GpuWorldState();
|
||||
|
||||
// No landblock loaded — the spawn must survive instead of being
|
||||
// silently dropped (the bug from Phase A.1's first live run).
|
||||
state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(42));
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, MakeStubEntity(42));
|
||||
|
||||
Assert.Empty(state.Entities); // not visible yet
|
||||
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||
|
|
@ -62,9 +62,9 @@ public class GpuWorldStateTests
|
|||
var state = new GpuWorldState();
|
||||
|
||||
// Three spawns arrive before the landblock loads.
|
||||
state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1));
|
||||
state.AppendLiveEntity(0xA9B40022u, MakeStubEntity(2)); // same landblock, different cell
|
||||
state.AppendLiveEntity(0xA9B40033u, MakeStubEntity(3));
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, MakeStubEntity(1));
|
||||
state.PlaceLiveEntityProjection(0xA9B40022u, MakeStubEntity(2)); // same landblock, different cell
|
||||
state.PlaceLiveEntityProjection(0xA9B40033u, MakeStubEntity(3));
|
||||
Assert.Equal(3, state.PendingLiveEntityCount);
|
||||
Assert.Empty(state.Entities);
|
||||
|
||||
|
|
@ -82,8 +82,8 @@ public class GpuWorldStateTests
|
|||
{
|
||||
var state = new GpuWorldState();
|
||||
|
||||
state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1)); // pending for 0xA9B4FFFF
|
||||
state.AppendLiveEntity(0xAAAA0022u, MakeStubEntity(2)); // pending for 0xAAAAFFFF
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, MakeStubEntity(1)); // pending for 0xA9B4FFFF
|
||||
state.PlaceLiveEntityProjection(0xAAAA0022u, MakeStubEntity(2)); // pending for 0xAAAAFFFF
|
||||
|
||||
// Loading 0xA9B4FFFF only drains its own bucket.
|
||||
state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu));
|
||||
|
|
@ -93,12 +93,12 @@ public class GpuWorldStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void RelocateEntity_StrandedInPending_MovesToLoadedTarget()
|
||||
public void RebucketLiveEntity_StrandedInPending_MovesToLoadedTarget()
|
||||
{
|
||||
// Regression: the cold-spawn / run-out "invisible player" bug
|
||||
// (2026-07-03). A persistent (server-spawned) entity that spawned into a
|
||||
// not-yet-loaded landblock is parked in the pending bucket. The per-frame
|
||||
// RelocateEntity is supposed to keep it homed to its current landblock so
|
||||
// RebucketLiveEntity keeps it homed to its current landblock so
|
||||
// it draws — but the old implementation scanned ONLY _loaded, so it
|
||||
// silently no-op'd on a pending entity and the player stayed hidden
|
||||
// forever. Confirmed live: "[ent] APPEND guid=0x5000000A -> PENDING(hidden)"
|
||||
|
|
@ -118,16 +118,16 @@ public class GpuWorldStateTests
|
|||
state.MarkPersistent(0x5000000Au);
|
||||
|
||||
// Spawned before its landblock streamed in → parked in pending, hidden.
|
||||
state.AppendLiveEntity(0xADAF0011u, player);
|
||||
state.PlaceLiveEntityProjection(0xADAF0011u, player);
|
||||
Assert.Empty(state.Entities);
|
||||
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||
|
||||
// The player's current landblock IS loaded now (the live log shows the
|
||||
// sibling NPCs re-hydrated into it). RelocateEntity — called every frame
|
||||
// sibling NPCs re-hydrated into it). RebucketLiveEntity — called every frame
|
||||
// to keep the player homed to its current landblock — must promote the
|
||||
// stranded pending entity into the loaded bucket so it draws.
|
||||
state.AddLandblock(MakeStubLandblock(0xBBBBFFFFu));
|
||||
state.RelocateEntity(player, 0xBBBB0011u);
|
||||
state.RebucketLiveEntity(player, 0xBBBB0011u);
|
||||
|
||||
Assert.Single(state.Entities); // now drawn
|
||||
Assert.Equal(0, state.PendingLiveEntityCount); // no longer stranded
|
||||
|
|
@ -139,8 +139,8 @@ public class GpuWorldStateTests
|
|||
var state = new GpuWorldState();
|
||||
|
||||
// Spawns for landblock 0xA9B4FFFF arrive while it's pending.
|
||||
state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1));
|
||||
state.AppendLiveEntity(0xA9B40022u, MakeStubEntity(2));
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, MakeStubEntity(1));
|
||||
state.PlaceLiveEntityProjection(0xA9B40022u, MakeStubEntity(2));
|
||||
Assert.Equal(2, state.PendingLiveEntityCount);
|
||||
|
||||
// Player moves away — the streamer says "this landblock is no
|
||||
|
|
@ -159,7 +159,7 @@ public class GpuWorldStateTests
|
|||
var state = new GpuWorldState();
|
||||
|
||||
state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu));
|
||||
state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1));
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, MakeStubEntity(1));
|
||||
Assert.Single(state.Entities);
|
||||
|
||||
state.RemoveLandblock(0xA9B4FFFFu);
|
||||
|
|
@ -172,7 +172,7 @@ public class GpuWorldStateTests
|
|||
{
|
||||
var state = new GpuWorldState();
|
||||
|
||||
state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1));
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, MakeStubEntity(1));
|
||||
Assert.False(state.IsLoaded(0xA9B4FFFFu)); // pending doesn't count
|
||||
|
||||
state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu));
|
||||
|
|
@ -193,7 +193,7 @@ public class GpuWorldStateTests
|
|||
[Fact]
|
||||
public void RemoveLandblock_RescuesPersistentEntity_FromPendingBucket()
|
||||
{
|
||||
// #138 (secondary): the player is re-injected via AppendLiveEntity every
|
||||
// #138 (secondary): the player is re-injected via rebucketing every
|
||||
// frame; right after a teleport its landblock hasn't streamed in, so it
|
||||
// lands in the pending bucket. If that landblock is then unloaded before
|
||||
// it finishes loading, the persistent entity must still be rescued —
|
||||
|
|
@ -203,7 +203,7 @@ public class GpuWorldStateTests
|
|||
state.MarkPersistent(playerGuid);
|
||||
|
||||
var player = MakeServerEntity(id: 1, serverGuid: playerGuid);
|
||||
state.AppendLiveEntity(0xA9B40011u, player); // landblock not loaded → pending
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, player); // landblock not loaded → pending
|
||||
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||
|
||||
state.RemoveLandblock(0xA9B4FFFFu); // unloaded while still pending
|
||||
|
|
@ -212,17 +212,36 @@ public class GpuWorldStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_DoesNotRescue_NonPersistentPendingEntity()
|
||||
public void RemoveLandblock_RetainsNonPersistentLiveProjectionForSameIdentityReload()
|
||||
{
|
||||
// A normal server object (door) in the pending bucket is NOT persistent;
|
||||
// it must still be dropped (and is recoverable via #138 re-hydrate from
|
||||
// the retained inbound spawn snapshot, not via the rescue path).
|
||||
// A normal server object (door) is not rescued with the moving player,
|
||||
// but its exact projection remains pending for this landblock. Reload
|
||||
// must merge that identity instead of reconstructing from CreateObject.
|
||||
var state = new GpuWorldState();
|
||||
var door = MakeServerEntity(id: 2, serverGuid: 0x7A9B4001u); // not marked persistent
|
||||
state.AppendLiveEntity(0xA9B40011u, door);
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, door);
|
||||
|
||||
state.RemoveLandblock(0xA9B4FFFFu);
|
||||
|
||||
Assert.Empty(state.DrainRescued());
|
||||
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||
state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu));
|
||||
Assert.Same(door, Assert.Single(state.Entities));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_LoadedLiveProjectionMovesToPendingAndReloadsSameIdentity()
|
||||
{
|
||||
var state = new GpuWorldState();
|
||||
var door = MakeServerEntity(id: 3, serverGuid: 0x7A9B4002u);
|
||||
state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu));
|
||||
state.PlaceLiveEntityProjection(0xA9B40011u, door);
|
||||
|
||||
state.RemoveLandblock(0xA9B4FFFFu);
|
||||
|
||||
Assert.Empty(state.Entities);
|
||||
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||
state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu));
|
||||
Assert.Same(door, Assert.Single(state.Entities));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,32 @@ public class GpuWorldStateTwoTierTests
|
|||
Assert.True(state.IsLoaded(0xAAAAFFFFu)); // landblock still resident
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveEntitiesFromLandblock_DropsStaticLayerButRetainsLiveProjection()
|
||||
{
|
||||
var state = new GpuWorldState();
|
||||
WorldEntity staticEntity = MakeStubEntity(1);
|
||||
var liveEntity = new WorldEntity
|
||||
{
|
||||
Id = 2,
|
||||
ServerGuid = 0x70000002u,
|
||||
SourceGfxObjOrSetupId = 0x01000001u,
|
||||
Position = System.Numerics.Vector3.Zero,
|
||||
Rotation = System.Numerics.Quaternion.Identity,
|
||||
MeshRefs = System.Array.Empty<MeshRef>(),
|
||||
};
|
||||
state.AddLandblock(MakeStubLandblock(
|
||||
0xAAAAFFFFu,
|
||||
staticEntity,
|
||||
liveEntity));
|
||||
|
||||
state.RemoveEntitiesFromLandblock(0xAAAAFFFFu);
|
||||
|
||||
Assert.Same(liveEntity, Assert.Single(state.Entities));
|
||||
Assert.DoesNotContain(staticEntity, state.Entities);
|
||||
Assert.True(state.IsLoaded(0xAAAAFFFFu));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddEntitiesToExistingLandblock_MergesIntoExistingRecord()
|
||||
{
|
||||
|
|
@ -118,7 +144,6 @@ public class GpuWorldStateTwoTierTests
|
|||
int callCount = 0;
|
||||
var state = new GpuWorldState(
|
||||
wbSpawnAdapter: null,
|
||||
wbEntitySpawnAdapter: null,
|
||||
onLandblockUnloaded: id => { observed = id; callCount++; });
|
||||
|
||||
state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu, MakeStubEntity(1)));
|
||||
|
|
@ -144,7 +169,6 @@ public class GpuWorldStateTwoTierTests
|
|||
int callCount = 0;
|
||||
var state = new GpuWorldState(
|
||||
wbSpawnAdapter: null,
|
||||
wbEntitySpawnAdapter: null,
|
||||
onLandblockUnloaded: _ => callCount++);
|
||||
|
||||
// Landblock never loaded.
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using AcDream.App.Streaming;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// #138 — selection logic for re-hydrating server objects when a landblock
|
||||
/// reloads after a dungeon collapse (or a Near→Far demote). See
|
||||
/// <see cref="LandblockEntityRehydrator"/> for the retail/ACE rationale.
|
||||
/// </summary>
|
||||
public class LandblockEntityRehydratorTests
|
||||
{
|
||||
private const uint PlayerGuid = 0x50000001u;
|
||||
// A door spawned in cell 0x00070123 of dungeon landblock 0x0007; its
|
||||
// cell-resolved spawn id must match the streamed canonical id 0x0007FFFF.
|
||||
private const uint DungeonLb = 0x0007FFFFu;
|
||||
private const uint DoorGuid = 0x7A9B4001u;
|
||||
private const uint DoorSpawnId = 0x00070123u;
|
||||
|
||||
private static LandblockEntityRehydrator.RetainedSpawn Spawn(
|
||||
uint guid, uint spawnLbId, bool hasMesh = true)
|
||||
=> new(guid, spawnLbId, hasMesh);
|
||||
|
||||
[Fact]
|
||||
public void RetainedSpawnInLoadedLandblock_NotPresent_IsSelected()
|
||||
{
|
||||
var result = LandblockEntityRehydrator.SelectGuidsToRehydrate(
|
||||
DungeonLb,
|
||||
new[] { Spawn(DoorGuid, DoorSpawnId) },
|
||||
new HashSet<uint>(),
|
||||
PlayerGuid);
|
||||
|
||||
Assert.Equal(new[] { DoorGuid }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellResolvedSpawnId_MatchesCanonicalLoadedId()
|
||||
{
|
||||
// The streamed landblock id is canonical (0xAAAAFFFF); the spawn id is
|
||||
// cell-resolved (0xAAAA00CC). They name the same landblock, so the door
|
||||
// must be selected — the canonicalization is the load-bearing step.
|
||||
var result = LandblockEntityRehydrator.SelectGuidsToRehydrate(
|
||||
DungeonLb, // 0x0007FFFF
|
||||
new[] { Spawn(DoorGuid, 0x000701A9u) }, // a different cell, same landblock
|
||||
new HashSet<uint>(),
|
||||
PlayerGuid);
|
||||
|
||||
Assert.Equal(new[] { DoorGuid }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpawnInDifferentLandblock_IsNotSelected()
|
||||
{
|
||||
var result = LandblockEntityRehydrator.SelectGuidsToRehydrate(
|
||||
DungeonLb,
|
||||
new[] { Spawn(DoorGuid, 0xA9B30123u) }, // Holtburg, not the loaded dungeon
|
||||
new HashSet<uint>(),
|
||||
PlayerGuid);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlreadyPresentGuid_IsNotSelected()
|
||||
{
|
||||
// Already rendered (server re-sent it, or it was never dropped) — a
|
||||
// re-hydrate would be a redundant mesh rebuild.
|
||||
var result = LandblockEntityRehydrator.SelectGuidsToRehydrate(
|
||||
DungeonLb,
|
||||
new[] { Spawn(DoorGuid, DoorSpawnId) },
|
||||
new HashSet<uint> { DoorGuid },
|
||||
PlayerGuid);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayerGuid_IsNeverSelected()
|
||||
{
|
||||
// The player has a retained spawn too, but the persistent-rescue path
|
||||
// owns it (preserves its live pose). Re-hydrating would reset it.
|
||||
var result = LandblockEntityRehydrator.SelectGuidsToRehydrate(
|
||||
DungeonLb,
|
||||
new[] { Spawn(PlayerGuid, DoorSpawnId) },
|
||||
new HashSet<uint>(),
|
||||
PlayerGuid);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpawnWithoutWorldMesh_IsNotSelected()
|
||||
{
|
||||
// Inventory items / setup-less spawns build no visible entity.
|
||||
var result = LandblockEntityRehydrator.SelectGuidsToRehydrate(
|
||||
DungeonLb,
|
||||
new[] { Spawn(DoorGuid, DoorSpawnId, hasMesh: false) },
|
||||
new HashSet<uint>(),
|
||||
PlayerGuid);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedSet_SelectsOnlyAbsentWorldObjectsInLoadedLandblock()
|
||||
{
|
||||
uint npcGuid = 0x7A9B4002u; // same dungeon landblock, absent → select
|
||||
uint chestGuid = 0x7A9B4003u; // same landblock but already present → skip
|
||||
uint holtburgGuid = 0x7A9B4004u; // different landblock → skip
|
||||
uint heldItemGuid = 0x7A9B4005u; // no world mesh → skip
|
||||
|
||||
var spawns = new[]
|
||||
{
|
||||
Spawn(DoorGuid, DoorSpawnId), // select
|
||||
Spawn(npcGuid, 0x00070177u), // select (same lb, different cell)
|
||||
Spawn(chestGuid, 0x00070123u), // skip (present)
|
||||
Spawn(holtburgGuid, 0xA9B30100u), // skip (other lb)
|
||||
Spawn(heldItemGuid, 0x00070123u, hasMesh: false), // skip (no mesh)
|
||||
Spawn(PlayerGuid, 0x00070100u), // skip (player)
|
||||
};
|
||||
|
||||
var result = LandblockEntityRehydrator.SelectGuidsToRehydrate(
|
||||
DungeonLb, spawns, new HashSet<uint> { chestGuid }, PlayerGuid);
|
||||
|
||||
Assert.Equal(new HashSet<uint> { DoorGuid, npcGuid }, new HashSet<uint>(result));
|
||||
}
|
||||
}
|
||||
|
|
@ -78,19 +78,20 @@ public class StreamingControllerTests
|
|||
// Note: LoadedLandblock's actual fields are LandblockId, Heightmap,
|
||||
// Entities (positional record). Adjust if the first positional arg
|
||||
// name differs.
|
||||
var lb = new LoadedLandblock(0x32320FFEu, new LandBlock(), System.Array.Empty<WorldEntity>());
|
||||
const uint landblockId = 0x3232FFFFu;
|
||||
var lb = new LoadedLandblock(landblockId, new LandBlock(), System.Array.Empty<WorldEntity>());
|
||||
// A.5 T10-T12 follow-up: use a real empty mesh instance instead of
|
||||
// default! so any future test that flows MeshData through the apply
|
||||
// callback gets a non-null reference to inspect rather than an NRE.
|
||||
var stubMesh = new AcDream.Core.Terrain.LandblockMeshData(
|
||||
System.Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
|
||||
System.Array.Empty<uint>());
|
||||
fake.Pending.Enqueue(new LandblockStreamResult.Loaded(0x32320FFEu, LandblockStreamTier.Near, lb, stubMesh));
|
||||
fake.Pending.Enqueue(new LandblockStreamResult.Loaded(landblockId, LandblockStreamTier.Near, lb, stubMesh));
|
||||
|
||||
controller.Tick(50, 50);
|
||||
|
||||
Assert.Single(applied);
|
||||
Assert.True(state.IsLoaded(0x32320FFEu));
|
||||
Assert.True(state.IsLoaded(landblockId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -102,12 +103,13 @@ public class StreamingControllerTests
|
|||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||||
(_, _) => { }, state, nearRadius: 2, farRadius: 2);
|
||||
|
||||
var lb = new LoadedLandblock(0x32320FFEu, new LandBlock(), System.Array.Empty<WorldEntity>());
|
||||
const uint landblockId = 0x3232FFFFu;
|
||||
var lb = new LoadedLandblock(landblockId, new LandBlock(), System.Array.Empty<WorldEntity>());
|
||||
state.AddLandblock(lb);
|
||||
fake.Pending.Enqueue(new LandblockStreamResult.Unloaded(0x32320FFEu));
|
||||
fake.Pending.Enqueue(new LandblockStreamResult.Unloaded(landblockId));
|
||||
|
||||
controller.Tick(50, 50);
|
||||
|
||||
Assert.False(state.IsLoaded(0x32320FFEu));
|
||||
Assert.False(state.IsLoaded(landblockId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue