using AcDream.Core.Lighting;
using AcDream.Core.Plugins;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Plugin.Abstractions;
namespace AcDream.App.Streaming;
///
/// 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.
///
public sealed class LandblockStaticPresentationPublication
{
private readonly Dictionary _entities;
private readonly Dictionary _snapshots;
private readonly uint[] _orderedPreviouslyActiveIds;
private readonly Dictionary _replacementActive;
internal LandblockStaticPresentationPublication(
object owner,
LandblockPhysicsPublication physicsPublication,
Dictionary entities,
Dictionary snapshots,
IReadOnlyDictionary priorActive,
HashSet previouslyActiveIds,
uint[] orderedPreviouslyActiveIds)
{
Owner = owner;
PhysicsPublication = physicsPublication;
_entities = entities;
_snapshots = snapshots;
PriorActive = priorActive;
PreviouslyActiveIds = previouslyActiveIds;
_orderedPreviouslyActiveIds = orderedPreviouslyActiveIds;
_replacementActive = new Dictionary(
physicsPublication.Build.Landblock.Entities.Count);
}
internal object Owner { get; }
internal LandblockPhysicsPublication PhysicsPublication { get; }
internal IReadOnlyDictionary PriorActive { get; }
internal Dictionary MutableEntities => _entities;
internal Dictionary MutableSnapshots => _snapshots;
internal HashSet PreviouslyActiveIds { get; }
internal IReadOnlyList OrderedPreviouslyActiveIds =>
_orderedPreviouslyActiveIds;
internal IReadOnlyList>
OrderedSnapshots { get; set; } =
Array.Empty>();
internal Dictionary ReplacementActive =>
_replacementActive;
internal int PreparationCursor { get; set; }
internal bool PreparationCommitted { get; set; }
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 Entities => _entities;
public IReadOnlyDictionary Snapshots => _snapshots;
}
public readonly record struct LandblockStaticPresentationDiagnostics(
long BeginCount,
long CompleteCount,
long LightReplacementCount,
long PluginSpawnCount,
long PluginRefreshCount,
long PluginRemovalCount,
int ActiveLandblockCount,
int ActiveEntityCount);
///
/// 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 .
///
///
/// Retail constructs each static CPhysicsObj from Setup defaults before
/// CObjCell::init_objects (0x0052B420) inserts/refloods it. The light
/// state follows CPhysicsObj::set_lights (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.
///
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>
_activeByLandblock = new();
private readonly Dictionary _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);
internal bool MatchesResources(
LightingHookSink lighting,
TranslucencyFadeManager translucency) =>
ReferenceEquals(_lighting, lighting)
&& ReferenceEquals(_translucency, translucency);
public LandblockStaticPresentationPublication PreparePublication(
LandblockPhysicsPublication physicsPublication)
{
LandblockStaticPresentationPublication publication =
CreatePublication(physicsPublication);
while (!AdvancePreparationOne(publication))
{
}
return publication;
}
///
/// Captures prior ownership without validating the replacement entity
/// list. The concrete pipeline advances replacement validation one entity
/// at a time before any render/physics owner mutates external state.
///
internal LandblockStaticPresentationPublication CreatePublication(
LandblockPhysicsPublication physicsPublication)
{
ArgumentNullException.ThrowIfNull(physicsPublication);
uint canonical = Canonicalize(physicsPublication.LandblockId);
_activeByLandblock.TryGetValue(
canonical,
out Dictionary? active);
var entities = new Dictionary();
var snapshots = new Dictionary();
HashSet previous = active is not null
? active.Keys.ToHashSet()
: new HashSet();
uint[] orderedPrevious = previous.Order().ToArray();
return new LandblockStaticPresentationPublication(
_receiptOwner,
physicsPublication,
entities,
snapshots,
active ?? new Dictionary(),
previous,
orderedPrevious);
}
///
/// Validates and snapshots one replacement entity. The final stable plugin
/// order is materialized only after the cursor reaches the immutable tail.
///
internal bool AdvancePreparationOne(
LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (publication.PreparationCommitted)
return true;
IReadOnlyList source =
publication.PhysicsPublication.Build.Landblock.Entities;
uint canonical = Canonicalize(publication.LandblockId);
if (publication.PreparationCursor < source.Count)
{
WorldEntity entity = source[publication.PreparationCursor];
if (entity.ServerGuid != 0)
{
publication.PreparationCursor++;
return false;
}
if (publication.Entities.ContainsKey(entity.Id))
{
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 (publication.PriorActive.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}.");
}
publication.MutableEntities.Add(entity.Id, entity);
publication.MutableSnapshots.Add(entity.Id, Snapshot(entity));
publication.PreparationCursor++;
return false;
}
publication.OrderedSnapshots = publication.Snapshots
.OrderBy(static pair => pair.Key)
.ToArray();
publication.PreparationCommitted = true;
return true;
}
///
/// 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.
///
public void BeginPublication(LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (!publication.PreparationCommitted)
throw new InvalidOperationException(
"Static presentation cannot begin before preparation commits.");
while (!AdvanceBeginOne(publication))
{
}
}
///
/// Advances exactly one retained prior-owner cleanup. The cursor advances
/// only after the complete owner operation succeeds, so a failure retries
/// the same ID without rebuilding or reordering the work list.
///
internal bool AdvanceBeginOne(
LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (publication.BeginCommitted)
return true;
if (publication.PriorCleanupCursor
< publication.OrderedPreviouslyActiveIds.Count)
{
uint id = publication.OrderedPreviouslyActiveIds[
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++;
return false;
}
publication.BeginCommitted = true;
_beginCount++;
return true;
}
///
/// Exact replacement called by
/// immediately before registering this object's ordinary collision.
///
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 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++;
}
///
/// Publishes the exact current plugin snapshot after collision/reflood.
/// Existing IDs refresh replay state without emitting another logical
/// spawn; new IDs emit once.
///
public void CompletePublication(
LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (!publication.BeginCommitted
|| !publication.PhysicsPublication.CompletionCommitted)
{
throw new InvalidOperationException(
"Static presentation requires completed physics publication.");
}
while (!AdvanceCompleteOne(publication))
{
}
}
///
/// Advances exactly one plugin projection, then atomically publishes the
/// retained replacement dictionary after the cursor reaches its tail.
///
internal bool AdvanceCompleteOne(
LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (!publication.BeginCommitted
|| !publication.PhysicsPublication.CompletionCommitted)
{
throw new InvalidOperationException(
"Static presentation requires completed physics publication.");
}
if (publication.CompletionCommitted)
return true;
if (publication.PluginCursor < publication.OrderedSnapshots.Count)
{
(uint id, WorldEntitySnapshot snapshot) =
publication.OrderedSnapshots[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.ReplacementActive[id] = snapshot;
publication.PluginCursor++;
return false;
}
uint canonical = Canonicalize(publication.LandblockId);
if (publication.ReplacementActive.Count == 0)
_activeByLandblock.Remove(canonical);
else
_activeByLandblock[canonical] = publication.ReplacementActive;
publication.CompletionCommitted = true;
_completeCount++;
return true;
}
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? 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;
}