feat(net): port retail physics spawn and event timestamps
Parse the complete PhysicsDesc plus F754/F755 packets, correct every PhysicsState bit, and gate all nine retail update channels with generation-safe immutable snapshots. Preserve ForcePosition, teleport, placement, velocity, parent, pickup, delete, and same-generation CreateObject ordering from the named client. Separate accepted logical lifecycle notifications from retained UI qualities, make GUID replacement and session reset clear every projection exactly once, and add packet, wraparound, malformed-input, parent FIFO, canonical-position, reconnect, and GUID-reuse conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
d53fe30ffe
commit
8a5d77f7f4
50 changed files with 3809 additions and 649 deletions
288
src/AcDream.App/Rendering/ParentAttachmentState.cs
Normal file
288
src/AcDream.App/Rendering/ParentAttachmentState.cs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Update-thread state machine for parent relations that may arrive before
|
||||
/// either CreateObject. Unaccepted wire events remain in arrival order;
|
||||
/// rollback history contains only relations that passed the canonical retail
|
||||
/// timestamp gate.
|
||||
/// </summary>
|
||||
public sealed class ParentAttachmentState
|
||||
{
|
||||
private readonly Dictionary<uint, Queue<ParentAttachmentRelation>> _unresolvedByChild = new();
|
||||
private readonly Dictionary<uint, ParentAttachmentRelation> _projectionByChild = new();
|
||||
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
|
||||
|
||||
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
|
||||
{
|
||||
_projectionByChild[relation.ChildGuid] = relation;
|
||||
_lastAcceptedByChild[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 (!_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;
|
||||
|
||||
_projectionByChild[childGuid] = relation;
|
||||
_lastAcceptedByChild[childGuid] = relation;
|
||||
}
|
||||
|
||||
if (queue.Count == 0)
|
||||
_unresolvedByChild.Remove(childGuid);
|
||||
}
|
||||
|
||||
public bool TryGetProjection(uint childGuid, out ParentAttachmentRelation relation) =>
|
||||
_projectionByChild.TryGetValue(childGuid, out relation);
|
||||
|
||||
public void MarkProjected(uint childGuid) => _projectionByChild.Remove(childGuid);
|
||||
|
||||
public bool RestoreLastAccepted(uint childGuid)
|
||||
{
|
||||
if (!_lastAcceptedByChild.TryGetValue(childGuid, out ParentAttachmentRelation relation))
|
||||
return false;
|
||||
_projectionByChild[childGuid] = relation;
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<uint> ChildrenWaitingForParent(uint parentGuid)
|
||||
{
|
||||
var result = new HashSet<uint>();
|
||||
foreach ((uint childGuid, ParentAttachmentRelation relation) in _projectionByChild)
|
||||
{
|
||||
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)
|
||||
{
|
||||
_projectionByChild.Remove(guid);
|
||||
_lastAcceptedByChild.Remove(guid);
|
||||
_unresolvedByChild.Remove(guid);
|
||||
|
||||
RemoveParentReferences(_projectionByChild, 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);
|
||||
_projectionByChild.Remove(guid);
|
||||
_lastAcceptedByChild.Remove(guid);
|
||||
RemoveParentReferences(_projectionByChild, 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);
|
||||
_projectionByChild.Remove(guid);
|
||||
_lastAcceptedByChild.Remove(guid);
|
||||
RemoveParentReferences(_projectionByChild, 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)
|
||||
{
|
||||
_projectionByChild.Remove(childGuid);
|
||||
_lastAcceptedByChild.Remove(childGuid);
|
||||
}
|
||||
|
||||
public void RemoveChild(uint childGuid)
|
||||
{
|
||||
_projectionByChild.Remove(childGuid);
|
||||
_lastAcceptedByChild.Remove(childGuid);
|
||||
_unresolvedByChild.Remove(childGuid);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_unresolvedByChild.Clear();
|
||||
_projectionByChild.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,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue