refactor(streaming): compose landblock presentation transaction

Own render, physics, DAT-static presentation, and retained static-resource rebinds behind retryable receipts so a failed publication resumes its exact unfinished suffix without replaying retail create-time defaults.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-21 21:45:16 +02:00
parent e1110f7e54
commit ea0da8c8ae
15 changed files with 1744 additions and 89 deletions

View file

@ -670,6 +670,13 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
RequiresActivation: false);
}
// Reconcile the only fallible logical-owner edge before consuming any
// pending live/render/tier maps. A retry then sees the exact pending
// suffix even when a resolver or owner callback fails partway through.
landblock = NormalizeLoadedLandblock(landblock);
if (_loaded.TryGetValue(landblock.LandblockId, out LoadedLandblock? displaced))
ReconcileRetainedStaticEntityScripts(displaced, landblock);
// If pending live entities have been waiting for this landblock,
// merge them into the render-thread-owned mutable entity bucket before
// storing. Subsequent live spawns append to that bucket in O(1).
@ -681,11 +688,6 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
landblock = NormalizeLoadedLandblock(landblock, merged);
_pendingByLandblock.Remove(landblock.LandblockId);
}
else
{
landblock = NormalizeLoadedLandblock(landblock);
}
HashSet<ulong>? mergedRenderIds = additionalRenderIds is null
? null
: new HashSet<ulong>(additionalRenderIds);
@ -703,7 +705,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
tier = LandblockStreamTier.Near;
}
if (_loaded.Remove(landblock.LandblockId, out LoadedLandblock? displaced))
if (_loaded.Remove(landblock.LandblockId, out displaced))
{
RemoveLoadedLandblockFromFlatView(displaced);
_animatedIndexByLandblock.Remove(landblock.LandblockId);
@ -717,6 +719,39 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
(IEnumerable<ulong>?)mergedRenderIds ?? Array.Empty<ulong>());
}
/// <summary>
/// A same-landblock rehydrate is an acdream streaming replacement, not a
/// retail logical create/delete edge. Retained static IDs rebind their
/// Setup-owned resources without replaying defaults; omitted IDs perform
/// the same exact teardown used by ordinary retirement.
/// </summary>
private void ReconcileRetainedStaticEntityScripts(
LoadedLandblock displaced,
LoadedLandblock replacement)
{
if (_entityScriptActivator is null)
return;
var replacementsById = new Dictionary<uint, WorldEntity>();
for (int i = 0; i < replacement.Entities.Count; i++)
{
WorldEntity entity = replacement.Entities[i];
if (entity.ServerGuid == 0)
replacementsById.Add(entity.Id, entity);
}
for (int i = 0; i < displaced.Entities.Count; i++)
{
WorldEntity prior = displaced.Entities[i];
if (prior.ServerGuid != 0)
continue;
if (replacementsById.TryGetValue(prior.Id, out WorldEntity? retained))
_entityScriptActivator.OnRebindStatic(prior, retained);
else
_entityScriptActivator.OnRemove(prior);
}
}
internal void ActivateLandblockPresentation(
GpuLandblockSpatialPublication publication)
{

View file

@ -24,6 +24,7 @@ public sealed class LandblockPhysicsPublication
internal object Owner { get; }
internal LandblockBuild Build { get; }
internal bool BeginCommitted { get; set; }
internal bool CompletionCommitted { get; set; }
public uint LandblockId => Build.Landblock.LandblockId;
@ -127,6 +128,20 @@ public sealed class LandblockPhysicsPublisher
/// </summary>
public LandblockPhysicsPublication BeginPublication(
LandblockRenderPublication renderPublication)
{
LandblockPhysicsPublication publication = PreparePublication(
renderPublication);
BeginPublication(publication);
return publication;
}
/// <summary>
/// Captures the exact render receipt before physics mutation begins. The
/// coordinator retains this receipt and can retry the idempotent exact
/// replacement when a cache/backend operation fails.
/// </summary>
public LandblockPhysicsPublication PreparePublication(
LandblockRenderPublication renderPublication)
{
ArgumentNullException.ThrowIfNull(renderPublication);
LandblockBuild build = renderPublication.Build;
@ -134,7 +149,25 @@ public sealed class LandblockPhysicsPublisher
if (!IsFinite(origin))
throw new ArgumentOutOfRangeException(nameof(renderPublication));
return new LandblockPhysicsPublication(
_receiptOwner,
build,
origin);
}
/// <summary>
/// Advances the exact terrain/cell/building physics replacement from a
/// retained receipt.
/// </summary>
public void BeginPublication(LandblockPhysicsPublication publication)
{
ValidateReceipt(publication);
if (publication.BeginCommitted)
return;
long started = Stopwatch.GetTimestamp();
LandblockBuild build = publication.Build;
Vector3 origin = publication.Origin;
LoadedLandblock landblock = build.Landblock;
PhysicsDatBundle datBundle =
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
@ -286,12 +319,9 @@ public sealed class LandblockPhysicsPublisher
_cellSurfaceCount += cellSurfaces.Count;
_portalPlaneCount += portalPlanes.Count;
_buildingCount += buildingCount;
publication.BeginCommitted = true;
_beginCount++;
_basePublishTicks += Stopwatch.GetTimestamp() - started;
return new LandblockPhysicsPublication(
_receiptOwner,
build,
origin);
}
/// <summary>
@ -304,11 +334,10 @@ public sealed class LandblockPhysicsPublisher
LandblockPhysicsPublication publication,
Action<WorldEntity>? beforeStaticCollision = null)
{
ArgumentNullException.ThrowIfNull(publication);
if (!ReferenceEquals(publication.Owner, _receiptOwner))
throw new ArgumentException(
"The physics publication receipt belongs to another publisher.",
nameof(publication));
ValidateReceipt(publication);
if (!publication.BeginCommitted)
throw new InvalidOperationException(
"Physics publication cannot complete before its prefix commits.");
if (publication.CompletionCommitted)
return;
@ -651,4 +680,15 @@ public sealed class LandblockPhysicsPublisher
float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z);
private void ValidateReceipt(LandblockPhysicsPublication publication)
{
ArgumentNullException.ThrowIfNull(publication);
if (!ReferenceEquals(publication.Owner, _receiptOwner))
{
throw new ArgumentException(
"The physics publication receipt belongs to another publisher.",
nameof(publication));
}
}
}

View file

@ -41,6 +41,9 @@ public sealed class LandblockPresentationPipeline
public required uint LandblockId;
public LandblockStreamTier Tier;
public bool PresentationCommitted;
public LandblockRenderPublication? RenderPublication;
public LandblockPhysicsPublication? PhysicsPublication;
public LandblockStaticPresentationPublication? StaticPublication;
public bool SpatialCommitted;
public GpuLandblockSpatialPublication? SpatialPublication;
public bool SpatialPresentationCommitted;
@ -48,7 +51,11 @@ public sealed class LandblockPresentationPipeline
public bool LiveRecoveryCommitted;
}
private readonly Action<LandblockBuild, LandblockMeshData> _publishBeforeSpatialCommit;
private readonly Action<LandblockBuild, LandblockMeshData>?
_publishBeforeSpatialCommit;
private readonly LandblockRenderPublisher? _renderPublisher;
private readonly LandblockPhysicsPublisher? _physicsPublisher;
private readonly LandblockStaticPresentationPublisher? _staticPublisher;
private readonly GpuWorldState _state;
private readonly Action<uint>? _onLandblockLoaded;
private readonly Action<EnvCellLandblockBuild>? _ensureEnvCellMeshes;
@ -79,6 +86,37 @@ public sealed class LandblockPresentationPipeline
demoteNearLayer);
}
/// <summary>
/// Production constructor. The pipeline retains each concrete owner
/// receipt before mutation and resumes only the unfinished exact stage
/// after a failure.
/// </summary>
public LandblockPresentationPipeline(
LandblockRenderPublisher renderPublisher,
LandblockPhysicsPublisher physicsPublisher,
LandblockStaticPresentationPublisher staticPublisher,
GpuWorldState state,
Action<uint>? onLandblockLoaded = null,
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
LandblockRetirementCoordinator? retirementCoordinator = null)
{
_renderPublisher = renderPublisher
?? throw new ArgumentNullException(nameof(renderPublisher));
_physicsPublisher = physicsPublisher
?? throw new ArgumentNullException(nameof(physicsPublisher));
_staticPublisher = staticPublisher
?? throw new ArgumentNullException(nameof(staticPublisher));
_state = state ?? throw new ArgumentNullException(nameof(state));
_onLandblockLoaded = onLandblockLoaded;
_ensureEnvCellMeshes = ensureEnvCellMeshes;
// Until Slice 5F internalizes concrete retirement ownership, a
// concrete publication pipeline must share the production coordinator.
// A legacy no-op retirement suffix would detach spatial state while
// leaking the concrete render/physics/static owners.
_retirements = retirementCoordinator
?? throw new ArgumentNullException(nameof(retirementCoordinator));
}
public int PendingRetirementCount => _retirements.PendingCount;
public int PendingPublicationCount => _publications.Count;
@ -219,22 +257,60 @@ public sealed class LandblockPresentationPipeline
{
if (!transaction.PresentationCommitted)
{
try
if (_renderPublisher is not null
&& _physicsPublisher is not null
&& _staticPublisher is not null)
{
_publishBeforeSpatialCommit(transaction.Build, transaction.MeshData);
transaction.RenderPublication ??=
_renderPublisher.PreparePublication(
transaction.Build,
transaction.MeshData);
transaction.PhysicsPublication ??=
_physicsPublisher.PreparePublication(
transaction.RenderPublication);
transaction.StaticPublication ??=
_staticPublisher.PreparePublication(
transaction.PhysicsPublication);
// Prepare every immutable receipt before the first external
// mutation. Cross-owner identity/data errors therefore block
// the publication without leaving a committed prefix.
_renderPublisher.BeginPublication(transaction.RenderPublication);
_physicsPublisher.BeginPublication(transaction.PhysicsPublication);
// Retail establishes the building/visible-cell suffix before
// each static object publishes lights, collision, and scripts.
_renderPublisher.CompletePublication(
transaction.RenderPublication);
_staticPublisher.BeginPublication(transaction.StaticPublication);
_physicsPublisher.CompletePublication(
transaction.PhysicsPublication,
entity => _staticPublisher.PrepareEntityBeforeCollision(
transaction.StaticPublication,
entity));
_staticPublisher.CompletePublication(
transaction.StaticPublication);
}
catch (Exception error)
else
{
// The legacy GameWindow callback still spans several concrete
// owners and cannot report a committed prefix. Until Slice-5
// checkpoints C-E replace it with individually ledgered
// publishers, its production wrapper conservatively reports a
// committed mutation. Test/compatibility callbacks retain the
// established strong-exception contract unless they explicitly
// report otherwise.
if (error is StreamingMutationException { MutationCommitted: true })
_publications.Remove(result);
throw;
try
{
(_publishBeforeSpatialCommit
?? throw new InvalidOperationException(
"The presentation pipeline has no publication owners."))(
transaction.Build,
transaction.MeshData);
}
catch (Exception error)
{
// Compatibility callbacks cannot report an exact retained
// receipt. Preserve the established fail-fast rule only
// for callbacks that explicitly report committed mutation.
if (error is StreamingMutationException { MutationCommitted: true })
_publications.Remove(result);
throw;
}
}
transaction.PresentationCommitted = true;
}

View file

@ -20,17 +20,32 @@ public sealed class LandblockRenderPublication
internal LandblockRenderPublication(
object owner,
LandblockBuild build,
LandblockMeshData meshData,
Vector3 origin,
Dictionary<uint, LoadedCell> visibilityCells)
Dictionary<uint, LoadedCell> visibilityCells,
Vector3 aabbMin,
Vector3 aabbMax)
{
Owner = owner;
Build = build;
MeshData = meshData;
Origin = origin;
_visibilityCells = new ReadOnlyDictionary<uint, LoadedCell>(visibilityCells);
AabbMin = aabbMin;
AabbMax = aabbMax;
}
internal object Owner { get; }
internal LandblockBuild Build { get; }
internal LandblockMeshData MeshData { get; }
internal Vector3 AabbMin { get; }
internal Vector3 AabbMax { get; }
internal bool TerrainCommitted { get; set; }
internal bool VisibilityCommitted { get; set; }
internal bool AabbCommitted { get; set; }
internal bool BeginCommitted { get; set; }
internal bool BuildingRegistryCommitted { get; set; }
internal bool EnvCellsCommitted { get; set; }
internal bool CompletionCommitted { get; set; }
public uint LandblockId => Build.Landblock.LandblockId;
@ -137,6 +152,22 @@ public sealed class LandblockRenderPublisher
public LandblockRenderPublication BeginPublication(
LandblockBuild build,
LandblockMeshData meshData)
{
LandblockRenderPublication publication = PreparePublication(
build,
meshData);
BeginPublication(publication);
return publication;
}
/// <summary>
/// Validates and captures the immutable render transaction before its
/// first external mutation. A coordinator retains this receipt when an
/// exact-replacement callback fails, then resumes the same publication.
/// </summary>
public LandblockRenderPublication PreparePublication(
LandblockBuild build,
LandblockMeshData meshData)
{
ArgumentNullException.ThrowIfNull(build);
ArgumentNullException.ThrowIfNull(meshData);
@ -145,7 +176,6 @@ public sealed class LandblockRenderPublisher
"Render publication requires the build's captured origin.",
nameof(build));
long beginStarted = Stopwatch.GetTimestamp();
uint landblockId = build.Landblock.LandblockId;
if (build.EnvCells is { } candidateEnvCells &&
candidateEnvCells.LandblockId != landblockId)
@ -157,28 +187,64 @@ public sealed class LandblockRenderPublisher
Vector3 origin = ComputeOrigin(landblockId, build.Origin);
long terrainStarted = Stopwatch.GetTimestamp();
_publishTerrain(landblockId, meshData, origin);
_terrainPublishTicks += Stopwatch.GetTimestamp() - terrainStarted;
var committedCells = new Dictionary<uint, LoadedCell>();
var visibilityCells = new Dictionary<uint, LoadedCell>();
if (build.EnvCells is { } envCells)
{
_cellVisibility.CommitLandblock(landblockId, envCells.VisibilityCells);
foreach (LoadedCell cell in envCells.VisibilityCells)
committedCells[cell.CellId] = cell;
visibilityCells[cell.CellId] = cell;
}
(Vector3 aabbMin, Vector3 aabbMax) = ComputeAabb(meshData, origin);
_worldState.SetLandblockAabb(landblockId, aabbMin, aabbMax);
_beginCount++;
_beginPublishTicks += Stopwatch.GetTimestamp() - beginStarted;
return new LandblockRenderPublication(
_receiptOwner,
build,
meshData,
origin,
committedCells);
visibilityCells,
aabbMin,
aabbMax);
}
/// <summary>
/// Advances the render prefix from a retained receipt. Every operation is
/// exact replacement, so a callback failure can safely retry this stage.
/// </summary>
public void BeginPublication(LandblockRenderPublication publication)
{
ValidateReceipt(publication);
if (publication.BeginCommitted)
return;
long beginStarted = Stopwatch.GetTimestamp();
LandblockBuild build = publication.Build;
uint landblockId = publication.LandblockId;
if (!publication.TerrainCommitted)
{
long terrainStarted = Stopwatch.GetTimestamp();
_publishTerrain(landblockId, publication.MeshData, publication.Origin);
_terrainPublishTicks += Stopwatch.GetTimestamp() - terrainStarted;
publication.TerrainCommitted = true;
}
if (!publication.VisibilityCommitted)
{
if (build.EnvCells is { } envCells)
_cellVisibility.CommitLandblock(landblockId, envCells.VisibilityCells);
publication.VisibilityCommitted = true;
}
if (!publication.AabbCommitted)
{
_worldState.SetLandblockAabb(
landblockId,
publication.AabbMin,
publication.AabbMax);
publication.AabbCommitted = true;
}
publication.BeginCommitted = true;
_beginCount++;
_beginPublishTicks += Stopwatch.GetTimestamp() - beginStarted;
}
/// <summary>
@ -188,28 +254,35 @@ public sealed class LandblockRenderPublisher
/// </summary>
public void CompletePublication(LandblockRenderPublication publication)
{
ArgumentNullException.ThrowIfNull(publication);
if (!ReferenceEquals(publication.Owner, _receiptOwner))
throw new ArgumentException(
"The render publication receipt belongs to another publisher.",
nameof(publication));
ValidateReceipt(publication);
if (!publication.BeginCommitted)
throw new InvalidOperationException(
"Render publication cannot complete before its prefix commits.");
if (publication.CompletionCommitted)
return;
long started = Stopwatch.GetTimestamp();
LandblockBuild build = publication.Build;
uint landblockId = publication.LandblockId;
if (build.Landblock.PhysicsDats?.Info is { } info)
if (!publication.BuildingRegistryCommitted)
{
uint registryKey = landblockId & 0xFFFF0000u;
_buildingRegistries[registryKey] = BuildingLoader.Build(
info,
landblockId,
publication.VisibilityCells);
if (build.Landblock.PhysicsDats?.Info is { } info)
{
uint registryKey = landblockId & 0xFFFF0000u;
_buildingRegistries[registryKey] = BuildingLoader.Build(
info,
landblockId,
publication.VisibilityCells);
}
publication.BuildingRegistryCommitted = true;
}
if (build.EnvCells is { } envCells)
_commitEnvCells?.Invoke(envCells);
if (!publication.EnvCellsCommitted)
{
if (build.EnvCells is { } envCells)
_commitEnvCells?.Invoke(envCells);
publication.EnvCellsCommitted = true;
}
publication.CompletionCommitted = true;
_completeCount++;
@ -287,4 +360,15 @@ public sealed class LandblockRenderPublisher
new Vector3(origin.X, origin.Y, zMin),
new Vector3(origin.X + 192f, origin.Y + 192f, zMax));
}
private void ValidateReceipt(LandblockRenderPublication publication)
{
ArgumentNullException.ThrowIfNull(publication);
if (!ReferenceEquals(publication.Owner, _receiptOwner))
{
throw new ArgumentException(
"The render publication receipt belongs to another publisher.",
nameof(publication));
}
}
}

View file

@ -0,0 +1,346 @@
using AcDream.Core.Lighting;
using AcDream.Core.Plugins;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Plugin.Abstractions;
namespace AcDream.App.Streaming;
/// <summary>
/// Retained receipt for the DAT-static presentation stages of one landblock
/// publication. The physics publisher calls the light/translucency stage at
/// the existing per-object point immediately before ordinary collision.
/// </summary>
public sealed class LandblockStaticPresentationPublication
{
private readonly IReadOnlyDictionary<uint, WorldEntity> _entities;
private readonly IReadOnlyDictionary<uint, WorldEntitySnapshot> _snapshots;
internal LandblockStaticPresentationPublication(
object owner,
LandblockPhysicsPublication physicsPublication,
Dictionary<uint, WorldEntity> entities,
Dictionary<uint, WorldEntitySnapshot> snapshots,
HashSet<uint> previouslyActiveIds)
{
Owner = owner;
PhysicsPublication = physicsPublication;
_entities = entities;
_snapshots = snapshots;
PreviouslyActiveIds = previouslyActiveIds;
}
internal object Owner { get; }
internal LandblockPhysicsPublication PhysicsPublication { get; }
internal HashSet<uint> PreviouslyActiveIds { get; }
internal int PriorCleanupCursor { get; set; }
internal int PluginCursor { get; set; }
internal bool BeginCommitted { get; set; }
internal bool CompletionCommitted { get; set; }
public uint LandblockId => PhysicsPublication.LandblockId;
public IReadOnlyDictionary<uint, WorldEntity> Entities => _entities;
public IReadOnlyDictionary<uint, WorldEntitySnapshot> Snapshots => _snapshots;
}
public readonly record struct LandblockStaticPresentationDiagnostics(
long BeginCount,
long CompleteCount,
long LightReplacementCount,
long PluginSpawnCount,
long PluginRefreshCount,
long PluginRemovalCount,
int ActiveLandblockCount,
int ActiveEntityCount);
/// <summary>
/// Update-thread owner of DAT-static lights, translucency lifetime, and plugin
/// projection. Setup defaults remain activated exactly once at the existing
/// post-spatial render-pin boundary owned by <see cref="GpuWorldState"/>.
/// </summary>
/// <remarks>
/// Retail constructs each static <c>CPhysicsObj</c> from Setup defaults before
/// <c>CObjCell::init_objects</c> (0x0052B420) inserts/refloods it. The light
/// state follows <c>CPhysicsObj::set_lights</c> (0x0050FCF0); translucency
/// starts from the new object's Setup/material state before later FPHooks.
/// Plugin projection is an acdream adaptation and therefore emits at most one
/// logical spawn per retained static ID, while reapply only refreshes current
/// state.
/// </remarks>
public sealed class LandblockStaticPresentationPublisher
{
private readonly object _receiptOwner = new();
private readonly LightingHookSink _lighting;
private readonly TranslucencyFadeManager _translucency;
private readonly WorldGameState _worldState;
private readonly WorldEvents _worldEvents;
private readonly Dictionary<uint, Dictionary<uint, WorldEntitySnapshot>>
_activeByLandblock = new();
private readonly Dictionary<uint, uint> _landblockByEntityId = new();
private long _beginCount;
private long _completeCount;
private long _lightReplacementCount;
private long _pluginSpawnCount;
private long _pluginRefreshCount;
private long _pluginRemovalCount;
public LandblockStaticPresentationPublisher(
LightingHookSink lighting,
TranslucencyFadeManager translucency,
WorldGameState worldState,
WorldEvents worldEvents)
{
_lighting = lighting ?? throw new ArgumentNullException(nameof(lighting));
_translucency = translucency
?? throw new ArgumentNullException(nameof(translucency));
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
_worldEvents = worldEvents ?? throw new ArgumentNullException(nameof(worldEvents));
}
public LandblockStaticPresentationDiagnostics Diagnostics => new(
_beginCount,
_completeCount,
_lightReplacementCount,
_pluginSpawnCount,
_pluginRefreshCount,
_pluginRemovalCount,
_activeByLandblock.Count,
_landblockByEntityId.Count);
public LandblockStaticPresentationPublication PreparePublication(
LandblockPhysicsPublication physicsPublication)
{
ArgumentNullException.ThrowIfNull(physicsPublication);
uint canonical = Canonicalize(physicsPublication.LandblockId);
_activeByLandblock.TryGetValue(
canonical,
out Dictionary<uint, WorldEntitySnapshot>? active);
var entities = new Dictionary<uint, WorldEntity>();
var snapshots = new Dictionary<uint, WorldEntitySnapshot>();
foreach (WorldEntity entity in physicsPublication.Build.Landblock.Entities)
{
if (entity.ServerGuid != 0)
continue;
if (!entities.TryAdd(entity.Id, entity))
{
throw new InvalidOperationException(
$"Landblock 0x{canonical:X8} contains duplicate DAT-static ID " +
$"0x{entity.Id:X8}.");
}
if (_landblockByEntityId.TryGetValue(entity.Id, out uint owner)
&& owner != canonical)
{
throw new InvalidOperationException(
$"DAT-static ID 0x{entity.Id:X8} is already owned by " +
$"landblock 0x{owner:X8}.");
}
if (active is not null
&& active.TryGetValue(entity.Id, out WorldEntitySnapshot retained)
&& retained.SourceId != entity.SourceGfxObjOrSetupId)
{
throw new InvalidOperationException(
$"Retained DAT-static ID 0x{entity.Id:X8} changed source " +
$"from 0x{retained.SourceId:X8} to " +
$"0x{entity.SourceGfxObjOrSetupId:X8}.");
}
snapshots.Add(entity.Id, Snapshot(entity));
}
HashSet<uint> previous = active is not null
? active.Keys.ToHashSet()
: new HashSet<uint>();
return new LandblockStaticPresentationPublication(
_receiptOwner,
physicsPublication,
entities,
snapshots,
previous);
}
/// <summary>
/// Retires omitted prior projections and detaches prior concrete lights
/// before replacement collision. Retained IDs keep their SetLight latch
/// and FPHook translucency state because rehydrate is not a logical create.
/// </summary>
public void BeginPublication(LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (publication.BeginCommitted)
return;
uint[] priorIds = publication.PreviouslyActiveIds.Order().ToArray();
while (publication.PriorCleanupCursor < priorIds.Length)
{
uint id = priorIds[publication.PriorCleanupCursor];
bool retained = publication.Entities.ContainsKey(id);
_lighting.UnregisterOwner(id, forgetState: !retained);
if (!retained)
{
_translucency.ClearEntity(id);
_worldState.RemoveById(id);
_worldEvents.ForgetEntity(id);
_landblockByEntityId.Remove(id);
_pluginRemovalCount++;
}
publication.PriorCleanupCursor++;
}
publication.BeginCommitted = true;
_beginCount++;
}
/// <summary>
/// Exact replacement called by <see cref="LandblockPhysicsPublisher"/>
/// immediately before registering this object's ordinary collision.
/// </summary>
public void PrepareEntityBeforeCollision(
LandblockStaticPresentationPublication publication,
WorldEntity entity)
{
ValidateReceipt(publication);
ArgumentNullException.ThrowIfNull(entity);
if (!publication.BeginCommitted)
throw new InvalidOperationException(
"Static presentation cannot prepare an entity before begin commits.");
if (entity.ServerGuid != 0)
return;
if (!publication.Entities.TryGetValue(entity.Id, out WorldEntity? expected)
|| !ReferenceEquals(expected, entity))
{
throw new InvalidOperationException(
$"DAT-static entity 0x{entity.Id:X8} does not belong to this publication.");
}
bool retained = publication.PreviouslyActiveIds.Contains(entity.Id);
_lighting.UnregisterOwner(entity.Id, forgetState: !retained);
if (!retained)
_translucency.ClearEntity(entity.Id);
PhysicsDatBundle datBundle = publication.PhysicsPublication.Build.Landblock.PhysicsDats
?? PhysicsDatBundle.Empty;
uint sourceId = entity.SourceGfxObjOrSetupId;
if ((sourceId & 0xFF000000u) == 0x02000000u
&& datBundle.Setups.TryGetValue(sourceId, out var setup))
{
IReadOnlyList<LightSource> lights = LightInfoLoader.Load(
setup,
ownerId: entity.Id,
entityPosition: entity.Position,
entityRotation: entity.Rotation,
cellId: entity.ParentCellId ?? 0u);
for (int i = 0; i < lights.Count; i++)
_lighting.RegisterOwnedLight(lights[i]);
}
_lightReplacementCount++;
}
/// <summary>
/// Publishes the exact current plugin snapshot after collision/reflood.
/// Existing IDs refresh replay state without emitting another logical
/// spawn; new IDs emit once.
/// </summary>
public void CompletePublication(
LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (!publication.BeginCommitted
|| !publication.PhysicsPublication.CompletionCommitted)
{
throw new InvalidOperationException(
"Static presentation requires completed physics publication.");
}
if (publication.CompletionCommitted)
return;
KeyValuePair<uint, WorldEntitySnapshot>[] snapshots = publication.Snapshots
.OrderBy(pair => pair.Key)
.ToArray();
while (publication.PluginCursor < snapshots.Length)
{
(uint id, WorldEntitySnapshot snapshot) = snapshots[publication.PluginCursor];
_worldState.Add(snapshot);
if (publication.PreviouslyActiveIds.Contains(id))
{
_worldEvents.UpsertCurrent(snapshot);
_pluginRefreshCount++;
}
else
{
_worldEvents.FireEntitySpawned(snapshot);
_pluginSpawnCount++;
}
_landblockByEntityId[id] = Canonicalize(publication.LandblockId);
publication.PluginCursor++;
}
uint canonical = Canonicalize(publication.LandblockId);
if (publication.Snapshots.Count == 0)
{
_activeByLandblock.Remove(canonical);
}
else
{
_activeByLandblock[canonical] = publication.Snapshots
.ToDictionary(pair => pair.Key, pair => pair.Value);
}
publication.CompletionCommitted = true;
_completeCount++;
}
public void RemoveLighting(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (entity.ServerGuid == 0)
_lighting.UnregisterOwner(entity.Id);
}
public void RemoveTranslucency(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (entity.ServerGuid == 0)
_translucency.ClearEntity(entity.Id);
}
public void RemovePluginProjection(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (entity.ServerGuid != 0)
return;
_worldState.RemoveById(entity.Id);
_worldEvents.ForgetEntity(entity.Id);
if (_landblockByEntityId.Remove(entity.Id, out uint landblockId)
&& _activeByLandblock.TryGetValue(
landblockId,
out Dictionary<uint, WorldEntitySnapshot>? active))
{
active.Remove(entity.Id);
if (active.Count == 0)
_activeByLandblock.Remove(landblockId);
}
_pluginRemovalCount++;
}
private void ValidateReceipt(LandblockStaticPresentationPublication publication)
{
ArgumentNullException.ThrowIfNull(publication);
if (!ReferenceEquals(publication.Owner, _receiptOwner))
{
throw new ArgumentException(
"The static presentation receipt belongs to another publisher.",
nameof(publication));
}
}
private static WorldEntitySnapshot Snapshot(WorldEntity entity) => new(
Id: entity.Id,
SourceId: entity.SourceGfxObjOrSetupId,
Position: entity.Position,
Rotation: entity.Rotation);
private static uint Canonicalize(uint landblockId) =>
(landblockId & 0xFFFF0000u) | 0xFFFFu;
}

View file

@ -174,21 +174,23 @@ public sealed class StreamingController
Action? clearPendingLoads = null,
Action<uint>? onLandblockLoaded = null,
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
LandblockRetirementCoordinator? retirementCoordinator = null)
LandblockRetirementCoordinator? retirementCoordinator = null,
LandblockPresentationPipeline? presentationPipeline = null)
{
_enqueueLoad = enqueueLoad;
_enqueueUnload = enqueueUnload;
_drainCompletions = drainCompletions;
_clearPendingLoads = clearPendingLoads;
_state = state;
_presentation = new LandblockPresentationPipeline(
applyTerrain,
state,
onLandblockLoaded,
ensureEnvCellMeshes,
retirementCoordinator,
removeTerrain,
demoteNearLayer);
_presentation = presentationPipeline
?? new LandblockPresentationPipeline(
applyTerrain,
state,
onLandblockLoaded,
ensureEnvCellMeshes,
retirementCoordinator,
removeTerrain,
demoteNearLayer);
NearRadius = nearRadius;
FarRadius = farRadius;
}