refactor(streaming): own presentation retirement in pipeline

Move full and near-tier presentation retirement out of GameWindow and into a concrete pipeline-owned stage mapper. Harden the retry ledger against reentrancy and committed visibility observer failures, enforce a single state/resource ownership graph, and split concrete versus legacy controller construction so callbacks cannot be silently ignored.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-21 22:07:36 +02:00
parent dc39772bf9
commit ea7ffbc186
10 changed files with 860 additions and 140 deletions

View file

@ -0,0 +1,119 @@
using AcDream.Core.Lighting;
using AcDream.Core.Rendering;
using AcDream.Core.World;
namespace AcDream.App.Streaming;
/// <summary>
/// Composes the presentation-owner half of one detach-first landblock
/// retirement. <see cref="LandblockRetirementTicket"/> remains the exact
/// retry ledger; this class only maps each stage to its concrete owner.
/// </summary>
/// <remarks>
/// Retail separates <c>CLandBlock::release_all</c> (0x0052FCF0),
/// <c>CLandBlock::destroy_static_objects</c> (0x0052FA50), and
/// <c>CLandBlock::Destroy</c> (0x0052FAA0). Acdream's asynchronous equivalent
/// detaches spatial reachability first, then advances these concrete owners
/// from the retained ticket without replaying successful stages or entities.
/// </remarks>
public sealed class LandblockPresentationRetirementOwner
{
private readonly LandblockRenderPublisher _render;
private readonly LandblockPhysicsPublisher _physics;
private readonly LandblockStaticPresentationPublisher _staticPresentation;
private readonly LightingHookSink _lighting;
private readonly TranslucencyFadeManager _translucency;
public LandblockPresentationRetirementOwner(
LandblockRenderPublisher render,
LandblockPhysicsPublisher physics,
LandblockStaticPresentationPublisher staticPresentation,
LightingHookSink lighting,
TranslucencyFadeManager translucency)
{
_render = render ?? throw new ArgumentNullException(nameof(render));
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_staticPresentation = staticPresentation
?? throw new ArgumentNullException(nameof(staticPresentation));
_lighting = lighting ?? throw new ArgumentNullException(nameof(lighting));
_translucency = translucency
?? throw new ArgumentNullException(nameof(translucency));
if (!_staticPresentation.MatchesResources(_lighting, _translucency))
{
throw new ArgumentException(
"The retirement resources must be those owned by the static " +
"presentation publisher.",
nameof(staticPresentation));
}
}
internal bool Matches(
LandblockRenderPublisher render,
LandblockPhysicsPublisher physics,
LandblockStaticPresentationPublisher staticPresentation) =>
ReferenceEquals(_render, render)
&& ReferenceEquals(_physics, physics)
&& ReferenceEquals(_staticPresentation, staticPresentation)
&& _staticPresentation.MatchesResources(_lighting, _translucency);
public void Advance(LandblockRetirementTicket ticket)
{
ArgumentNullException.ThrowIfNull(ticket);
bool staticOnly = ticket.Kind == LandblockRetirementKind.NearLayer;
ticket.RunForEachEntity(
LandblockRetirementStage.EntityLighting,
entity => !staticOnly || entity.ServerGuid == 0,
RemoveLighting);
ticket.RunForEachEntity(
LandblockRetirementStage.EntityTranslucency,
entity => !staticOnly || entity.ServerGuid == 0,
RemoveTranslucency);
ticket.RunForEachEntity(
LandblockRetirementStage.PluginProjection,
static entity => entity.ServerGuid == 0,
_staticPresentation.RemovePluginProjection);
if (ticket.Kind == LandblockRetirementKind.Full)
{
ticket.RunOnce(
LandblockRetirementStage.Terrain,
() => _render.RemoveTerrain(ticket.LandblockId));
}
ticket.RunOnce(
LandblockRetirementStage.Physics,
() =>
{
if (ticket.Kind == LandblockRetirementKind.Full)
_physics.RemoveLandblock(ticket.LandblockId);
else
_physics.DemoteToTerrain(ticket.LandblockId);
});
ticket.RunOnce(
LandblockRetirementStage.CellVisibility,
() => _render.RemoveCellVisibility(ticket.LandblockId));
ticket.RunOnce(
LandblockRetirementStage.BuildingRegistry,
() => _render.RemoveBuildingRegistry(ticket.LandblockId));
ticket.RunOnce(
LandblockRetirementStage.EnvironmentCells,
() => _render.RemoveEnvironmentCells(ticket.LandblockId));
}
private void RemoveLighting(WorldEntity entity)
{
if (entity.ServerGuid == 0)
_staticPresentation.RemoveLighting(entity);
else
_lighting.UnregisterOwner(entity.Id);
}
private void RemoveTranslucency(WorldEntity entity)
{
if (entity.ServerGuid == 0)
_staticPresentation.RemoveTranslucency(entity);
else
_translucency.ClearEntity(entity.Id);
}
}