acdream/src/AcDream.App/World/ParentAttachmentState.cs
Erik 69a2ca0c6d refactor(world): extract live projection mechanics
Move appearance rebinding, collision construction, default-pose resolution, local shadow ownership, and exact leave-world presentation into focused owners. Preserve retail parent ordering with staged validation, committed recovery, recursive attached-subtree withdrawal, and retryable exact teardown across parent, pickup, position, unwield, and pose-loss edges.

Co-Authored-By: OpenAI Codex <codex@openai.com>
2026-07-21 16:17:03 +02:00

396 lines
14 KiB
C#

using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
namespace AcDream.App.World;
/// <summary>
/// Update-thread state machine for parent relations that may arrive before
/// either CreateObject. Unaccepted wire events remain in arrival order;
/// committed rollback history contains only relations that passed both the
/// canonical retail timestamp gate and retail <c>PartArray::add_child</c>
/// validation.
/// </summary>
public sealed class ParentAttachmentState
{
private readonly Dictionary<uint, Queue<ParentAttachmentRelation>> _unresolvedByChild = new();
private readonly Dictionary<uint, ParentAttachmentRelation> _stagedByChild = new();
private readonly Dictionary<uint, ParentAttachmentRelation> _recoveryByChild = new();
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
{
_stagedByChild[relation.ChildGuid] = relation;
}
public void Enqueue(ParentEvent.Parsed update)
{
if (!_unresolvedByChild.TryGetValue(update.ChildGuid, out Queue<ParentAttachmentRelation>? queue))
{
queue = new Queue<ParentAttachmentRelation>();
_unresolvedByChild.Add(update.ChildGuid, queue);
}
queue.Enqueue(new ParentAttachmentRelation(
update.ParentGuid,
update.ChildGuid,
update.ParentLocation,
update.PlacementId,
update.ParentInstanceSequence,
update.ChildPositionSequence));
}
/// <summary>
/// Resolves every now-addressable relation for one child in wire order.
/// A future parent generation remains queued; an older generation or a
/// rejected child POSITION_TS is discarded. Every accepted relation
/// supersedes the preceding render projection.
/// </summary>
public void Resolve(
uint childGuid,
Func<uint, bool> isObjectKnown,
Func<uint, ushort?> resolveInstance,
Func<ParentEvent.Parsed, bool> accept)
{
if (_stagedByChild.ContainsKey(childGuid))
return;
if (!_unresolvedByChild.TryGetValue(childGuid, out Queue<ParentAttachmentRelation>? queue))
return;
int candidateCount = queue.Count;
for (int i = 0; i < candidateCount; i++)
{
ParentAttachmentRelation relation = queue.Dequeue();
ushort? parentInstance = resolveInstance(relation.ParentGuid);
if (parentInstance is null)
{
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent });
continue;
}
if (parentInstance.Value != relation.ParentInstanceSequence)
{
// Current parent newer than the packet: discard the stale
// relation. Packet newer than current: retain it until that
// parent generation is constructed.
if (PhysicsTimestampGate.IsNewer(
relation.ParentInstanceSequence,
parentInstance.Value))
{
continue;
}
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent });
continue;
}
if (!isObjectKnown(childGuid))
{
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Child });
continue;
}
var update = new ParentEvent.Parsed(
relation.ParentGuid,
relation.ChildGuid,
relation.ParentLocation,
relation.PlacementId,
relation.ParentInstanceSequence,
relation.ChildPositionSequence);
if (!accept(update))
continue;
_stagedByChild[childGuid] = relation with
{
WaitOwner = ParentAttachmentWaitOwner.Unknown,
};
break;
}
if (queue.Count == 0)
_unresolvedByChild.Remove(childGuid);
}
public bool TryGetProjection(uint childGuid, out ParentAttachmentRelation relation) =>
_stagedByChild.TryGetValue(childGuid, out relation)
|| _recoveryByChild.TryGetValue(childGuid, out relation);
public bool TryGetStagedProjection(
uint childGuid,
out ParentAttachmentRelation relation) =>
_stagedByChild.TryGetValue(childGuid, out relation);
public bool TryGetRecoveryProjection(
uint childGuid,
out ParentAttachmentRelation relation) =>
_recoveryByChild.TryGetValue(childGuid, out relation);
public void CopyPendingProjectionChildrenTo(List<uint> destination)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach (uint childGuid in _stagedByChild.Keys)
destination.Add(childGuid);
foreach (uint childGuid in _recoveryByChild.Keys)
{
if (!_stagedByChild.ContainsKey(childGuid))
destination.Add(childGuid);
}
}
public bool IsCommitted(ParentAttachmentRelation relation) =>
_lastAcceptedByChild.TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation committed)
&& committed == relation;
public bool IsPending(
ParentAttachmentRelation relation,
ParentProjectionCandidateKind kind) =>
(kind is ParentProjectionCandidateKind.Staged
? _stagedByChild
: _recoveryByChild).TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation pending)
&& pending == relation;
public void MarkProjected(
ParentAttachmentRelation relation,
ParentProjectionCandidateKind kind)
{
Dictionary<uint, ParentAttachmentRelation> source =
kind is ParentProjectionCandidateKind.Staged
? _stagedByChild
: _recoveryByChild;
if (source.TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation pending)
&& pending == relation)
{
source.Remove(relation.ChildGuid);
}
}
public bool CommitProjection(ParentAttachmentRelation relation)
{
if (!_stagedByChild.TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation staged)
|| staged != relation)
{
return false;
}
_lastAcceptedByChild[relation.ChildGuid] = relation;
_stagedByChild.Remove(relation.ChildGuid);
_recoveryByChild[relation.ChildGuid] = relation;
return true;
}
public void RejectProjection(ParentAttachmentRelation relation)
{
if (_stagedByChild.TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation staged)
&& staged == relation)
{
_stagedByChild.Remove(relation.ChildGuid);
}
}
public bool RestoreLastAccepted(uint childGuid)
{
if (!_lastAcceptedByChild.TryGetValue(childGuid, out ParentAttachmentRelation relation))
return false;
_recoveryByChild[childGuid] = relation;
return true;
}
public IReadOnlyList<uint> ChildrenWaitingForParent(uint parentGuid)
{
var result = new HashSet<uint>();
foreach ((uint childGuid, ParentAttachmentRelation relation) in _stagedByChild)
{
if (relation.ParentGuid == parentGuid)
result.Add(childGuid);
}
foreach ((uint childGuid, ParentAttachmentRelation relation) in _recoveryByChild)
{
if (relation.ParentGuid == parentGuid)
result.Add(childGuid);
}
foreach ((uint childGuid, Queue<ParentAttachmentRelation> queue) in _unresolvedByChild)
{
if (queue.Any(relation => relation.ParentGuid == parentGuid))
result.Add(childGuid);
}
return result.ToArray();
}
public void RemoveObject(uint guid)
{
_stagedByChild.Remove(guid);
_recoveryByChild.Remove(guid);
_lastAcceptedByChild.Remove(guid);
_unresolvedByChild.Remove(guid);
RemoveParentReferences(_stagedByChild, guid);
RemoveParentReferences(_recoveryByChild, guid);
RemoveParentReferences(_lastAcceptedByChild, guid);
uint[] children = _unresolvedByChild.Keys.ToArray();
for (int i = 0; i < children.Length; i++)
{
uint childGuid = children[i];
Queue<ParentAttachmentRelation> retained = new(
_unresolvedByChild[childGuid].Where(relation => relation.ParentGuid != guid));
if (retained.Count == 0)
_unresolvedByChild.Remove(childGuid);
else
_unresolvedByChild[childGuid] = retained;
}
}
/// <summary>
/// Ends one logical incarnation without discarding unresolved wire events
/// that may explicitly address the replacement/future parent generation.
/// </summary>
public void EndGeneration(uint guid, ushort replacementGeneration)
{
FilterChildCandidates(
guid,
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
_stagedByChild.Remove(guid);
_recoveryByChild.Remove(guid);
_lastAcceptedByChild.Remove(guid);
RemoveParentReferences(_stagedByChild, guid);
RemoveParentReferences(_recoveryByChild, guid);
RemoveParentReferences(_lastAcceptedByChild, guid);
FilterParentCandidates(
guid,
relation => relation.ParentInstanceSequence == replacementGeneration
|| PhysicsTimestampGate.IsNewer(
replacementGeneration,
relation.ParentInstanceSequence));
}
/// <summary>
/// Deletes one exact incarnation. Child-addressed candidates die with the
/// child; candidates addressed to a strictly newer parent incarnation
/// survive for retail's post-Create blob replay.
/// </summary>
public void DeleteGeneration(uint guid, ushort deletedGeneration)
{
FilterChildCandidates(
guid,
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
_stagedByChild.Remove(guid);
_recoveryByChild.Remove(guid);
_lastAcceptedByChild.Remove(guid);
RemoveParentReferences(_stagedByChild, guid);
RemoveParentReferences(_recoveryByChild, guid);
RemoveParentReferences(_lastAcceptedByChild, guid);
FilterParentCandidates(
guid,
relation => PhysicsTimestampGate.IsNewer(
deletedGeneration,
relation.ParentInstanceSequence));
}
/// <summary>
/// Clears an accepted attachment projection after Pickup or a world
/// Position without destroying fresher unresolved ParentEvents.
/// </summary>
public void EndChildProjection(uint childGuid)
{
_stagedByChild.Remove(childGuid);
_recoveryByChild.Remove(childGuid);
_lastAcceptedByChild.Remove(childGuid);
}
public void RemoveChild(uint childGuid)
{
_stagedByChild.Remove(childGuid);
_recoveryByChild.Remove(childGuid);
_lastAcceptedByChild.Remove(childGuid);
_unresolvedByChild.Remove(childGuid);
}
public void Clear()
{
_unresolvedByChild.Clear();
_stagedByChild.Clear();
_recoveryByChild.Clear();
_lastAcceptedByChild.Clear();
}
private static void RemoveParentReferences(
Dictionary<uint, ParentAttachmentRelation> relations,
uint parentGuid)
{
uint[] children = relations
.Where(pair => pair.Value.ParentGuid == parentGuid)
.Select(pair => pair.Key)
.ToArray();
for (int i = 0; i < children.Length; i++)
relations.Remove(children[i]);
}
private void FilterParentCandidates(
uint parentGuid,
Func<ParentAttachmentRelation, bool> retain)
{
uint[] children = _unresolvedByChild.Keys.ToArray();
for (int i = 0; i < children.Length; i++)
{
uint childGuid = children[i];
Queue<ParentAttachmentRelation> queue = _unresolvedByChild[childGuid];
var retained = new Queue<ParentAttachmentRelation>(
queue.Where(relation => relation.ParentGuid != parentGuid || retain(relation)));
if (retained.Count == 0)
_unresolvedByChild.Remove(childGuid);
else
_unresolvedByChild[childGuid] = retained;
}
}
private void FilterChildCandidates(
uint childGuid,
Func<ParentAttachmentRelation, bool> retain)
{
if (!_unresolvedByChild.TryGetValue(
childGuid,
out Queue<ParentAttachmentRelation>? queue))
{
return;
}
var retained = new Queue<ParentAttachmentRelation>(queue.Where(retain));
if (retained.Count == 0)
_unresolvedByChild.Remove(childGuid);
else
_unresolvedByChild[childGuid] = retained;
}
}
public readonly record struct ParentAttachmentRelation(
uint ParentGuid,
uint ChildGuid,
uint ParentLocation,
uint PlacementId,
ushort ParentInstanceSequence,
ushort ChildPositionSequence,
ParentAttachmentWaitOwner WaitOwner = ParentAttachmentWaitOwner.Unknown);
public enum ParentAttachmentWaitOwner
{
Unknown,
Parent,
Child,
}
public enum ParentProjectionCandidateKind
{
Staged,
Recovery,
}