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:
parent
e1110f7e54
commit
ea0da8c8ae
15 changed files with 1744 additions and 89 deletions
|
|
@ -153,6 +153,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
private AcDream.App.Streaming.LandblockRetirementCoordinator? _landblockRetirements;
|
||||
private AcDream.App.Streaming.LandblockRenderPublisher? _landblockRenderPublisher;
|
||||
private AcDream.App.Streaming.LandblockPhysicsPublisher? _landblockPhysicsPublisher;
|
||||
private AcDream.App.Streaming.LandblockStaticPresentationPublisher?
|
||||
_landblockStaticPresentationPublisher;
|
||||
private AcDream.App.Rendering.EquippedChildRenderController? _equippedChildRenderer;
|
||||
private AcDream.App.Streaming.StreamingController? _streamingController;
|
||||
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
||||
|
|
@ -2194,7 +2196,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_translucencyFades.ClearEntity(ownerId);
|
||||
},
|
||||
(entity, info) => _staticAnimationScheduler.Register(entity, info),
|
||||
ownerId => _staticAnimationScheduler.Unregister(ownerId));
|
||||
ownerId => _staticAnimationScheduler.Unregister(ownerId),
|
||||
(entity, info) => _staticAnimationScheduler.Rebind(entity, info));
|
||||
_entityScriptActivator = entityScriptActivator;
|
||||
|
||||
// Phase Post-A.5 #53 (Task 12): wire EntityClassificationCache.InvalidateLandblock
|
||||
|
|
@ -2429,6 +2432,12 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
new AcDream.App.Streaming.LandblockPhysicsPublisher(
|
||||
_physicsEngine,
|
||||
_heightTable!);
|
||||
_landblockStaticPresentationPublisher =
|
||||
new AcDream.App.Streaming.LandblockStaticPresentationPublisher(
|
||||
_lightingSink!,
|
||||
_translucencyFades,
|
||||
_worldGameState,
|
||||
_worldEvents);
|
||||
|
||||
_clipFrame ??= ClipFrame.NoClip();
|
||||
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
|
||||
|
|
@ -2520,6 +2529,17 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
});
|
||||
_streamer.Start();
|
||||
|
||||
var landblockPresentationPipeline =
|
||||
new AcDream.App.Streaming.LandblockPresentationPipeline(
|
||||
_landblockRenderPublisher!,
|
||||
_landblockPhysicsPublisher!,
|
||||
_landblockStaticPresentationPublisher!,
|
||||
_worldState,
|
||||
onLandblockLoaded: loadedLandblockId =>
|
||||
_liveEntityHydration!.OnLandblockLoaded(loadedLandblockId),
|
||||
ensureEnvCellMeshes:
|
||||
_landblockRenderPublisher!.PrepareAfterRenderPins,
|
||||
retirementCoordinator: _landblockRetirements);
|
||||
_streamingController = new AcDream.App.Streaming.StreamingController(
|
||||
enqueueLoad: (id, kind, generation) => _streamer.EnqueueLoad(
|
||||
new AcDream.App.Streaming.LandblockBuildRequest(
|
||||
|
|
@ -2542,7 +2562,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
onLandblockLoaded: loadedLandblockId =>
|
||||
_liveEntityHydration!.OnLandblockLoaded(loadedLandblockId),
|
||||
ensureEnvCellMeshes: _landblockRenderPublisher!.PrepareAfterRenderPins,
|
||||
retirementCoordinator: _landblockRetirements);
|
||||
retirementCoordinator: _landblockRetirements,
|
||||
presentationPipeline: landblockPresentationPipeline);
|
||||
// A.5 T22.5: apply max-completions from resolved quality.
|
||||
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
|
||||
_worldReveal = new AcDream.App.Streaming.WorldRevealCoordinator(
|
||||
|
|
@ -3793,19 +3814,28 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
ticket.RunForEachEntity(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.EntityLighting,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
entity => _lightingSink?.UnregisterOwner(entity.Id));
|
||||
entity =>
|
||||
{
|
||||
if (entity.ServerGuid == 0)
|
||||
_landblockStaticPresentationPublisher!.RemoveLighting(entity);
|
||||
else
|
||||
_lightingSink?.UnregisterOwner(entity.Id);
|
||||
});
|
||||
ticket.RunForEachEntity(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.EntityTranslucency,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
entity => _translucencyFades.ClearEntity(entity.Id));
|
||||
entity =>
|
||||
{
|
||||
if (entity.ServerGuid == 0)
|
||||
_landblockStaticPresentationPublisher!.RemoveTranslucency(entity);
|
||||
else
|
||||
_translucencyFades.ClearEntity(entity.Id);
|
||||
});
|
||||
ticket.RunForEachEntity(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.PluginProjection,
|
||||
static entity => entity.ServerGuid == 0,
|
||||
entity =>
|
||||
{
|
||||
_worldGameState.RemoveById(entity.Id);
|
||||
_worldEvents.ForgetEntity(entity.Id);
|
||||
});
|
||||
entity => _landblockStaticPresentationPublisher!
|
||||
.RemovePluginProjection(entity));
|
||||
|
||||
if (ticket.Kind == AcDream.App.Streaming.LandblockRetirementKind.Full)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
|
||||
internal int Count => _owners.Count;
|
||||
|
||||
public void Register(WorldEntity entity, ScriptActivationInfo info)
|
||||
public bool Register(WorldEntity entity, ScriptActivationInfo info)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(info);
|
||||
|
|
@ -86,13 +86,13 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
|| info.Setup is not { } setup
|
||||
|| info.DefaultAnimationId == 0)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_owners.TryGetValue(entity.Id, out Owner? retained))
|
||||
{
|
||||
if (ReferenceEquals(retained.Entity, entity))
|
||||
return;
|
||||
return true;
|
||||
throw new InvalidOperationException(
|
||||
$"DAT-static animation owner 0x{entity.Id:X8} is already registered.");
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
new MotionTable(),
|
||||
_animationLoader);
|
||||
if (!sequencer.HasCurrentNode)
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
int partCount = setup.Parts.Count;
|
||||
|
|
@ -131,6 +131,63 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
SurfaceOverrides = surfaces,
|
||||
PartAvailable = available,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebinds the retained retail static-animation workset member to a fresh
|
||||
/// landblock snapshot without reconstructing its PartArray sequencer or
|
||||
/// restarting Setup.DefaultAnimation.
|
||||
/// </summary>
|
||||
public void Rebind(WorldEntity entity, ScriptActivationInfo info)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(info);
|
||||
if (!_owners.TryGetValue(entity.Id, out Owner? owner))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"DAT-static animation owner 0x{entity.Id:X8} is not registered.");
|
||||
}
|
||||
if (ReferenceEquals(owner.Entity, entity))
|
||||
return;
|
||||
if (entity.ServerGuid != 0
|
||||
|| !info.UsesStaticAnimationWorkset
|
||||
|| info.Setup is null
|
||||
|| info.DefaultAnimationId == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"DAT-static animation owner 0x{entity.Id:X8} cannot be rebound to incompatible Setup data.");
|
||||
}
|
||||
|
||||
int partCount = owner.Setup.Parts.Count;
|
||||
uint[] gfxIds = BuildPartGfxIds(owner.Setup, entity);
|
||||
bool[] available = new bool[partCount];
|
||||
if (info.PartAvailability is { } supplied)
|
||||
{
|
||||
for (int i = 0; i < partCount && i < supplied.Count; i++)
|
||||
available[i] = supplied[i];
|
||||
}
|
||||
|
||||
// Compose publishes the owner's retained lists directly. Detach the
|
||||
// displaced immutable snapshot before those lists continue advancing
|
||||
// under the replacement entity.
|
||||
WorldEntity displaced = owner.Entity;
|
||||
if (ReferenceEquals(displaced.MeshRefs, owner.MeshRefs))
|
||||
displaced.MeshRefs = owner.MeshRefs.ToArray();
|
||||
if (ReferenceEquals(displaced.IndexedPartTransforms, owner.PartPoses)
|
||||
|| ReferenceEquals(displaced.IndexedPartAvailable, owner.PartAvailable))
|
||||
{
|
||||
displaced.SetIndexedPartPoses(
|
||||
owner.PartPoses.ToArray(),
|
||||
owner.PartAvailable.ToArray());
|
||||
}
|
||||
|
||||
owner.Entity = entity;
|
||||
owner.PartGfxIds = gfxIds;
|
||||
owner.SurfaceOverrides = MatchSurfaceOverrides(entity, gfxIds, available);
|
||||
owner.PartAvailable = available;
|
||||
owner.HasPreparedLivePartFrames = false;
|
||||
owner.PreparedLivePartFrames.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ public sealed class EntityScriptActivator
|
|||
public bool ScriptsStopped;
|
||||
public bool EmittersStopped;
|
||||
public bool PoseRemoved;
|
||||
public WorldEntity? PendingReplacement;
|
||||
public ScriptActivationInfo? PendingReplacementInfo;
|
||||
public bool ReplacementPosePublished;
|
||||
public bool ReplacementEffectOwnerUpdated;
|
||||
public bool ReplacementAnimationOwnerUpdated;
|
||||
public bool ReplacementAnchorUpdated;
|
||||
}
|
||||
|
||||
private readonly PhysicsScriptRunner _scriptRunner;
|
||||
|
|
@ -48,8 +54,9 @@ public sealed class EntityScriptActivator
|
|||
private readonly Func<WorldEntity, ScriptActivationInfo?> _resolver;
|
||||
private readonly Action<uint, WorldEntity, EntityEffectProfile>? _registerDatStaticEffectOwner;
|
||||
private readonly Action<uint>? _unregisterDatStaticEffectOwner;
|
||||
private readonly Action<WorldEntity, ScriptActivationInfo>? _registerDatStaticAnimationOwner;
|
||||
private readonly Func<WorldEntity, ScriptActivationInfo, bool>? _registerDatStaticAnimationOwner;
|
||||
private readonly Action<uint>? _unregisterDatStaticAnimationOwner;
|
||||
private readonly Action<WorldEntity, ScriptActivationInfo>? _rebindDatStaticAnimationOwner;
|
||||
private readonly Dictionary<uint, StaticOwnerState> _staticOwners = new();
|
||||
|
||||
public EntityScriptActivator(
|
||||
|
|
@ -59,8 +66,9 @@ public sealed class EntityScriptActivator
|
|||
Func<WorldEntity, ScriptActivationInfo?> resolver,
|
||||
Action<uint, WorldEntity, EntityEffectProfile>? registerDatStaticEffectOwner = null,
|
||||
Action<uint>? unregisterDatStaticEffectOwner = null,
|
||||
Action<WorldEntity, ScriptActivationInfo>? registerDatStaticAnimationOwner = null,
|
||||
Action<uint>? unregisterDatStaticAnimationOwner = null)
|
||||
Func<WorldEntity, ScriptActivationInfo, bool>? registerDatStaticAnimationOwner = null,
|
||||
Action<uint>? unregisterDatStaticAnimationOwner = null,
|
||||
Action<WorldEntity, ScriptActivationInfo>? rebindDatStaticAnimationOwner = null)
|
||||
{
|
||||
_scriptRunner = scriptRunner ?? throw new ArgumentNullException(nameof(scriptRunner));
|
||||
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
|
||||
|
|
@ -70,6 +78,116 @@ public sealed class EntityScriptActivator
|
|||
_unregisterDatStaticEffectOwner = unregisterDatStaticEffectOwner;
|
||||
_registerDatStaticAnimationOwner = registerDatStaticAnimationOwner;
|
||||
_unregisterDatStaticAnimationOwner = unregisterDatStaticAnimationOwner;
|
||||
_rebindDatStaticAnimationOwner = rebindDatStaticAnimationOwner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebinds a retained DAT-static logical owner to the fresh immutable
|
||||
/// <see cref="WorldEntity"/> snapshot produced by a same-landblock
|
||||
/// rehydrate. This is not a retail create edge: default Setup scripts and
|
||||
/// existing emitters keep their logical lifetime and are never replayed.
|
||||
/// </summary>
|
||||
public void OnRebindStatic(WorldEntity previous, WorldEntity replacement)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(previous);
|
||||
ArgumentNullException.ThrowIfNull(replacement);
|
||||
if (previous.ServerGuid != 0
|
||||
|| replacement.ServerGuid != 0
|
||||
|| previous.Id == 0
|
||||
|| previous.Id != replacement.Id)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"DAT-static rebind requires the same nonzero local ID.",
|
||||
nameof(replacement));
|
||||
}
|
||||
|
||||
uint key = previous.Id;
|
||||
if (!_staticOwners.TryGetValue(key, out StaticOwnerState? state))
|
||||
return;
|
||||
if (ReferenceEquals(state.Entity, replacement))
|
||||
return;
|
||||
if (!ReferenceEquals(state.Entity, previous))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"DAT-static owner 0x{key:X8} does not match the retained incarnation.");
|
||||
}
|
||||
if (state.ReleaseStarted)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"DAT-static owner 0x{key:X8} cannot rebind while teardown is pending.");
|
||||
}
|
||||
|
||||
if (state.PendingReplacement is null)
|
||||
{
|
||||
ScriptActivationInfo replacementInfo = _resolver(replacement)
|
||||
?? throw new InvalidOperationException(
|
||||
$"DAT-static owner 0x{key:X8} lost its Setup activation data during rebind.");
|
||||
ValidateLogicalRebind(state, replacement, replacementInfo);
|
||||
state.PendingReplacement = replacement;
|
||||
state.PendingReplacementInfo = replacementInfo;
|
||||
}
|
||||
else if (!ReferenceEquals(state.PendingReplacement, replacement))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"DAT-static owner 0x{key:X8} already has another rebind pending.");
|
||||
}
|
||||
|
||||
ScriptActivationInfo info = state.PendingReplacementInfo!;
|
||||
if (state.PosePublished && !state.ReplacementPosePublished)
|
||||
{
|
||||
_poses.Publish(replacement, info.PartTransforms, info.PartAvailability);
|
||||
state.ReplacementPosePublished = true;
|
||||
}
|
||||
if (state.EffectOwnerRegistered && !state.ReplacementEffectOwnerUpdated)
|
||||
{
|
||||
_registerDatStaticEffectOwner!(key, replacement, info.EffectProfile!);
|
||||
state.ReplacementEffectOwnerUpdated = true;
|
||||
}
|
||||
if (state.AnimationOwnerRegistered && !state.ReplacementAnimationOwnerUpdated)
|
||||
{
|
||||
(_rebindDatStaticAnimationOwner
|
||||
?? throw new InvalidOperationException(
|
||||
"DAT-static animation ownership has no retained-owner rebind callback."))(
|
||||
replacement,
|
||||
info);
|
||||
state.ReplacementAnimationOwnerUpdated = true;
|
||||
}
|
||||
if (!state.ReplacementAnchorUpdated)
|
||||
{
|
||||
_scriptRunner.SetOwnerAnchor(key, replacement.Position);
|
||||
state.ReplacementAnchorUpdated = true;
|
||||
}
|
||||
|
||||
state.Entity = replacement;
|
||||
state.Info = info;
|
||||
state.PendingReplacement = null;
|
||||
state.PendingReplacementInfo = null;
|
||||
state.ReplacementPosePublished = false;
|
||||
state.ReplacementEffectOwnerUpdated = false;
|
||||
state.ReplacementAnimationOwnerUpdated = false;
|
||||
state.ReplacementAnchorUpdated = false;
|
||||
}
|
||||
|
||||
private static void ValidateLogicalRebind(
|
||||
StaticOwnerState state,
|
||||
WorldEntity replacement,
|
||||
ScriptActivationInfo replacementInfo)
|
||||
{
|
||||
ScriptActivationInfo retained = state.Info;
|
||||
bool retainedAnimation = retained.UsesStaticAnimationWorkset
|
||||
&& retained.DefaultAnimationId != 0;
|
||||
bool replacementAnimation = replacementInfo.UsesStaticAnimationWorkset
|
||||
&& replacementInfo.DefaultAnimationId != 0;
|
||||
if (state.Entity.SourceGfxObjOrSetupId != replacement.SourceGfxObjOrSetupId
|
||||
|| retained.ScriptId != replacementInfo.ScriptId
|
||||
|| retainedAnimation != replacementAnimation
|
||||
|| (retainedAnimation
|
||||
&& retained.DefaultAnimationId != replacementInfo.DefaultAnimationId)
|
||||
|| (retained.EffectProfile is null) != (replacementInfo.EffectProfile is null))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"DAT-static owner 0x{state.Entity.Id:X8} changed Setup lifetime during a retained rebind.");
|
||||
}
|
||||
}
|
||||
|
||||
public void OnCreate(WorldEntity entity)
|
||||
|
|
@ -156,8 +274,8 @@ public sealed class EntityScriptActivator
|
|||
&& info.DefaultAnimationId != 0
|
||||
&& _registerDatStaticAnimationOwner is not null)
|
||||
{
|
||||
_registerDatStaticAnimationOwner(entity, info);
|
||||
state.AnimationOwnerRegistered = true;
|
||||
state.AnimationOwnerRegistered =
|
||||
_registerDatStaticAnimationOwner(entity, info);
|
||||
}
|
||||
|
||||
if (!state.ScriptStarted && info.ScriptId != 0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue