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

@ -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)