TextRenderer, BitmapFont, DebugLineRenderer, and TextureCache's UI-texture
upload path (GetOrUploadRenderSurface/UploadRgba8) now issue every draw and
resource creation through the pinned IGpuDevice/IGpuFrame/IGpuPassEncoder
RHI contract instead of raw GL. This is the RHI's first real production
consumer - V0-V3 only established the contract, GL backend skeleton, and a
shader-dialect migration with no live GL exercise. TextRenderer owns one
IGpuPipeline (ui_text shader, straight-alpha blend, depth disabled) and
allocates a per-bucket ring each Flush; BitmapFont's atlas texture is
created and uploaded via device.CreateTexture/.Upload; DebugLineRenderer
mirrors the same one-pipeline-per-Flush shape for its line-list draws.
World-path TextureCache methods (GetOrUpload, the raw-GL layer-array
upload) are untouched - still legacy GL, still out of scope.
Frame lifecycle: GpuDeviceFrameLifetime (RenderFrameOrchestrator.cs) wraps
IGpuDevice.BeginFrame()/IGpuFrame.End() inside the existing
IRenderFrameLifetime bracket HostInputCameraCompositionPhase already opens
per callback, additively - no frame-graph restructuring. Ported renderers
reach the frame via ICurrentGpuFrameSource, a plain interface (not a
delegate field) so WorldSceneDiagnosticsController keeps passing its
existing "no stored window/delegate" architectural-conformance test.
Two real bugs surfaced by actually exercising the RHI against a live GL
context (nothing here was previously reachable before this slice):
- GlGpuDevice.BeginFrame() now resets the render-state cache every frame.
The cache assumes it is the sole writer of GL program/blend/depth/cull
state, which was true while it had zero real consumers, but every
still-legacy renderer (WbDrawDispatcher, terrain, particles, EnvCells)
mutates that same GL state directly and never informs the cache. Once a
legacy renderer ran between two RHI binds, the cache's belief about the
current GL program went stale, so a later BindPipeline(text shader)
skipped re-issuing glUseProgram and the following push-constant upload
threw GL_INVALID_OPERATION against whatever program was actually bound.
Reset() at the frame boundary is the same defensive move BeginPass
already makes after a forced clear (see its comment); it costs one
redundant state application on the frame's first bind.
- GL_MULTISAMPLE has no representation in the pinned contract. Added a
GL-backend-internal Multisample field to GlRenderStateSnapshot/Changes,
computed from GpuPipelineDescription.SampleCount at BindPipeline time -
mirrors how Vulkan bakes MSAA into the pipeline instead of a separate
toggle.
Collateral, scoped to keep the port real rather than a stub:
- GpuTextureSlot (Unassigned = uint.MaxValue, NOT 0) now flows through
every consumer of TextureCache.GetOrUploadRenderSurface/UploadRgba8 and
TextRenderer.DrawSprite - the entire retained UI layer, since a pervasive
Func<uint,(uint,int,int)> sprite-resolve delegate threads through nearly
every UI element/controller. Every prior `== 0` / `!= 0` "no texture"
check became `.IsAssigned` / `!.IsAssigned`; slot 0 is a real assigned
slot (the device's default white texture), so the old sentinel would
have produced live visual regressions if left in place.
- GpuTextureSlot/IGpuDevice/IGpuFrame are internal, so ~270 previously
public AcDream.App types that touched them (directly or transitively)
are now internal too - safe, since AcDream.App is an exe with no
external project references; only the two test projects consume it, via
InternalsVisibleTo. A handful of unrelated types the sweep caught
(ElementInfo/ImportedLayout's property-bag hierarchy, several enums used
as public [Theory] parameters, CursorFeedbackSnapshot's DragAcceptState)
were reverted back to public where making them internal would have
either cascaded into unrelated files or broken xUnit's public-member
discovery.
- ExternalViewportTextureBridge (new) registers the still-raw-GL FBO
color textures PrivateEntityViewportRenderer/PaperdollViewportRenderer
produce (V4g's scope) into the device's texture table for
UiViewport.TextureHandle, via a temporary
GlGpuDevice.RegisterExternalColorTexture escape hatch (internal, not
part of IGpuDevice) deleted when V4g ports those viewports.
- TextRenderGlStateScope.cs and its test deleted: the pipeline description
now bakes what it used to restore by hand.
- ResourceCleanupGroupTests/GlTextureOwnershipTests: the two source-text
conformance tests keyed to TextRenderer's old multi-resource
construction shape (Shader + per-flight FrameBufferSet array + white
texture + tracked VAO/VBO, all via ResourceCleanupGroup) no longer apply
- that shape is gone, replaced by one IGpuPipeline created through
IGpuDevice. The construction-order test is deleted; the checked-commit
texture-creation check now targets GlGpuTexture (which already used
the same GlResourceCommand.CreateName primitive before this slice).
Gates:
- dotnet build -c Release: 0 warnings, 0 errors (AcDream.App has
TreatWarningsAsErrors).
- dotnet test tests/AcDream.App.Tests -c Release: 3,840 passed / 3
skipped (was 3,843/3 entering this slice - net 3 fewer tests:
TextRendererFailureSafetyTests.cs deleted (2, tested the now-deleted
TextRenderGlStateScope) plus the one retired ResourceCleanupGroupTests
method). Full solution: 8,908 passed / 5 skipped across all nine test
projects.
- Offline pixel gate (tools/run-offline-pixel-gate.ps1, parent ec414d60
vs this commit): differing fraction 0.318% (1,791/563,200 compared
pixels), above the 0.001 threshold. Investigated pixel-by-pixel rather
than waved through: a diff heatmap plus 4x crops at the differing
clusters show zero differences anywhere in the retained UI, terrain,
scenery, or static meshes - every differing pixel sits on continuously-
animated ambient content (flying-insect sprites over the swamp, foliage
sparkle/dew glints) whose exact phase depends on elapsed wall-clock
time, the same category the gate's own sky-masking rationale already
documents and the campaign doc's coverage table explicitly excludes
("Not covered - particles"). Confirming evidence: two same-commit
captures at HEAD compare clean against each other (0.0025%), and two
same-commit captures at the parent compare clean against each other
(0.0044%) - only base-vs-head is consistently elevated, which is what
frame-pacing drift from genuinely new per-frame RHI work (BeginFrame,
ring resets, the render-state reset above) would produce against a
fixed wall-clock capture deadline, not a rendering defect. Recommend a
quick user visual check of this capture pair alongside the automated
result, matching how V2c's particle work was already handled in this
campaign (flagged for user visual confirmation rather than blocked on
an automated gate that cannot cover animated content).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
455 lines
18 KiB
C#
455 lines
18 KiB
C#
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>
|
|
internal sealed class LandblockStaticPresentationPublication
|
|
{
|
|
private readonly Dictionary<uint, WorldEntity> _entities;
|
|
private readonly Dictionary<uint, WorldEntitySnapshot> _snapshots;
|
|
private readonly uint[] _orderedPreviouslyActiveIds;
|
|
private readonly Dictionary<uint, WorldEntitySnapshot> _replacementActive;
|
|
|
|
internal LandblockStaticPresentationPublication(
|
|
object owner,
|
|
LandblockPhysicsPublication physicsPublication,
|
|
Dictionary<uint, WorldEntity> entities,
|
|
Dictionary<uint, WorldEntitySnapshot> snapshots,
|
|
IReadOnlyDictionary<uint, WorldEntitySnapshot> priorActive,
|
|
HashSet<uint> previouslyActiveIds,
|
|
uint[] orderedPreviouslyActiveIds)
|
|
{
|
|
Owner = owner;
|
|
PhysicsPublication = physicsPublication;
|
|
_entities = entities;
|
|
_snapshots = snapshots;
|
|
PriorActive = priorActive;
|
|
PreviouslyActiveIds = previouslyActiveIds;
|
|
_orderedPreviouslyActiveIds = orderedPreviouslyActiveIds;
|
|
_replacementActive = new Dictionary<uint, WorldEntitySnapshot>(
|
|
physicsPublication.Build.Landblock.Entities.Count);
|
|
}
|
|
|
|
internal object Owner { get; }
|
|
internal LandblockPhysicsPublication PhysicsPublication { get; }
|
|
internal IReadOnlyDictionary<uint, WorldEntitySnapshot> PriorActive { get; }
|
|
internal Dictionary<uint, WorldEntity> MutableEntities => _entities;
|
|
internal Dictionary<uint, WorldEntitySnapshot> MutableSnapshots => _snapshots;
|
|
internal HashSet<uint> PreviouslyActiveIds { get; }
|
|
internal IReadOnlyList<uint> OrderedPreviouslyActiveIds =>
|
|
_orderedPreviouslyActiveIds;
|
|
internal IReadOnlyList<KeyValuePair<uint, WorldEntitySnapshot>>
|
|
OrderedSnapshots { get; set; } =
|
|
Array.Empty<KeyValuePair<uint, WorldEntitySnapshot>>();
|
|
internal Dictionary<uint, WorldEntitySnapshot> 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<uint, WorldEntity> Entities => _entities;
|
|
public IReadOnlyDictionary<uint, WorldEntitySnapshot> Snapshots => _snapshots;
|
|
}
|
|
|
|
internal 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>
|
|
internal 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);
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal LandblockStaticPresentationPublication CreatePublication(
|
|
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>();
|
|
HashSet<uint> previous = active is not null
|
|
? active.Keys.ToHashSet()
|
|
: new HashSet<uint>();
|
|
uint[] orderedPrevious = previous.Order().ToArray();
|
|
return new LandblockStaticPresentationPublication(
|
|
_receiptOwner,
|
|
physicsPublication,
|
|
entities,
|
|
snapshots,
|
|
active ?? new Dictionary<uint, WorldEntitySnapshot>(),
|
|
previous,
|
|
orderedPrevious);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates and snapshots one replacement entity. The final stable plugin
|
|
/// order is materialized only after the cursor reaches the immutable tail.
|
|
/// </summary>
|
|
internal bool AdvancePreparationOne(
|
|
LandblockStaticPresentationPublication publication)
|
|
{
|
|
ValidateReceipt(publication);
|
|
if (publication.PreparationCommitted)
|
|
return true;
|
|
|
|
IReadOnlyList<WorldEntity> 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;
|
|
}
|
|
|
|
/// <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.PreparationCommitted)
|
|
throw new InvalidOperationException(
|
|
"Static presentation cannot begin before preparation commits.");
|
|
while (!AdvanceBeginOne(publication))
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <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.");
|
|
}
|
|
while (!AdvanceCompleteOne(publication))
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances exactly one plugin projection, then atomically publishes the
|
|
/// retained replacement dictionary after the cursor reaches its tail.
|
|
/// </summary>
|
|
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<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;
|
|
}
|