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

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