feat(A.5 T14): GpuWorldState RemoveEntitiesFromLandblock + AddEntitiesToExisting
Two new methods on GpuWorldState, used by two-tier streaming (T13): - RemoveEntitiesFromLandblock(id): drop all entities from an LB while keeping the terrain. Used for Near->Far demote (player walks past the inner ring; LB stays loaded but entities leave). - AddEntitiesToExistingLandblock(id, entities): merge new entities into an already-loaded LB record. Used for Far->Near promote (terrain is already on the GPU; just streaming the entity layer in). Falls back to the pending bucket if the LB hasn't loaded yet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
774a7070a8
commit
fb10c3fa8c
2 changed files with 121 additions and 0 deletions
|
|
@ -339,6 +339,51 @@ public sealed class GpuWorldState
|
||||||
bucket.Add(entity);
|
bucket.Add(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drop all entities from a landblock without removing the terrain. Used
|
||||||
|
/// by two-tier streaming when a landblock crosses Near→Far hysteresis.
|
||||||
|
/// Per Phase A.5 spec §4.4.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveEntitiesFromLandblock(uint landblockId)
|
||||||
|
{
|
||||||
|
if (!_loaded.TryGetValue(landblockId, out var lb)) return;
|
||||||
|
if (_wbSpawnAdapter is not null)
|
||||||
|
_wbSpawnAdapter.OnLandblockUnloaded(landblockId);
|
||||||
|
_loaded[landblockId] = new LoadedLandblock(lb.LandblockId, lb.Heightmap, System.Array.Empty<WorldEntity>());
|
||||||
|
_pendingByLandblock.Remove(landblockId);
|
||||||
|
RebuildFlatView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merge entities into an existing-loaded landblock. Used by two-tier
|
||||||
|
/// streaming for the Far→Near promotion case (terrain already loaded;
|
||||||
|
/// entity layer streaming in). Falls back to the pending bucket if the
|
||||||
|
/// landblock isn't loaded yet (handles the rare "promote arrives before
|
||||||
|
/// far load completes" race).
|
||||||
|
/// Per Phase A.5 spec §4.4.
|
||||||
|
/// </summary>
|
||||||
|
public void AddEntitiesToExistingLandblock(uint landblockId, System.Collections.Generic.IReadOnlyList<WorldEntity> entities)
|
||||||
|
{
|
||||||
|
if (!_loaded.TryGetValue(landblockId, out var lb))
|
||||||
|
{
|
||||||
|
// Park as pending — same pattern as AppendLiveEntity for not-yet-loaded LBs.
|
||||||
|
if (!_pendingByLandblock.TryGetValue(landblockId, out var bucket))
|
||||||
|
{
|
||||||
|
bucket = new List<WorldEntity>();
|
||||||
|
_pendingByLandblock[landblockId] = bucket;
|
||||||
|
}
|
||||||
|
bucket.AddRange(entities);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var merged = new List<WorldEntity>(lb.Entities.Count + entities.Count);
|
||||||
|
merged.AddRange(lb.Entities);
|
||||||
|
merged.AddRange(entities);
|
||||||
|
_loaded[landblockId] = new LoadedLandblock(lb.LandblockId, lb.Heightmap, merged);
|
||||||
|
if (_wbSpawnAdapter is not null)
|
||||||
|
_wbSpawnAdapter.OnLandblockLoaded(_loaded[landblockId]);
|
||||||
|
RebuildFlatView();
|
||||||
|
}
|
||||||
|
|
||||||
private void RebuildFlatView()
|
private void RebuildFlatView()
|
||||||
{
|
{
|
||||||
_flatEntities = _loaded.Values.SelectMany(lb => lb.Entities).ToArray();
|
_flatEntities = _loaded.Values.SelectMany(lb => lb.Entities).ToArray();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
using System.Linq;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Streaming;
|
||||||
|
|
||||||
|
public class GpuWorldStateTwoTierTests
|
||||||
|
{
|
||||||
|
private static LoadedLandblock MakeStubLandblock(uint canonicalId, params WorldEntity[] entities)
|
||||||
|
=> new(canonicalId, new LandBlock(), entities);
|
||||||
|
|
||||||
|
private static WorldEntity MakeStubEntity(uint id)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
SourceGfxObjOrSetupId = 0x01000001u,
|
||||||
|
Position = System.Numerics.Vector3.Zero,
|
||||||
|
Rotation = System.Numerics.Quaternion.Identity,
|
||||||
|
MeshRefs = System.Array.Empty<MeshRef>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveEntitiesFromLandblock_KeepsLandblockButDropsEntities()
|
||||||
|
{
|
||||||
|
var state = new GpuWorldState();
|
||||||
|
var lb = MakeStubLandblock(0xAAAAFFFFu,
|
||||||
|
MakeStubEntity(1),
|
||||||
|
MakeStubEntity(2));
|
||||||
|
state.AddLandblock(lb);
|
||||||
|
Assert.Equal(2, state.Entities.Count);
|
||||||
|
|
||||||
|
state.RemoveEntitiesFromLandblock(0xAAAAFFFFu);
|
||||||
|
|
||||||
|
Assert.Empty(state.Entities);
|
||||||
|
Assert.True(state.IsLoaded(0xAAAAFFFFu)); // landblock still resident
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddEntitiesToExistingLandblock_MergesIntoExistingRecord()
|
||||||
|
{
|
||||||
|
var state = new GpuWorldState();
|
||||||
|
var lb = MakeStubLandblock(0xAAAAFFFFu, MakeStubEntity(1));
|
||||||
|
state.AddLandblock(lb);
|
||||||
|
|
||||||
|
state.AddEntitiesToExistingLandblock(0xAAAAFFFFu, new[]
|
||||||
|
{
|
||||||
|
MakeStubEntity(2),
|
||||||
|
MakeStubEntity(3),
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(3, state.Entities.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddEntitiesToExistingLandblock_LandblockNotYetLoaded_ParksInPending()
|
||||||
|
{
|
||||||
|
var state = new GpuWorldState();
|
||||||
|
|
||||||
|
// Landblock not loaded yet.
|
||||||
|
state.AddEntitiesToExistingLandblock(0xAAAAFFFFu, new[]
|
||||||
|
{
|
||||||
|
MakeStubEntity(1),
|
||||||
|
MakeStubEntity(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nothing in the flat view yet.
|
||||||
|
Assert.Empty(state.Entities);
|
||||||
|
Assert.Equal(2, state.PendingLiveEntityCount);
|
||||||
|
|
||||||
|
// Now load the landblock — pending entities should merge in.
|
||||||
|
state.AddLandblock(MakeStubLandblock(0xAAAAFFFFu));
|
||||||
|
Assert.Equal(2, state.Entities.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue