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
647
src/AcDream.App/World/InboundPhysicsStateController.cs
Normal file
647
src/AcDream.App/World/InboundPhysicsStateController.cs
Normal file
|
|
@ -0,0 +1,647 @@
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Update-thread owner of inbound retail physics timestamps and the latest
|
||||
/// accepted immutable spawn description. This is deliberately narrower than
|
||||
/// <c>LiveEntityRuntime</c>: it owns no renderer, local entity ID, physics body,
|
||||
/// animation, effect, or spatial-bucket resource.
|
||||
/// </summary>
|
||||
public sealed class InboundPhysicsStateController
|
||||
{
|
||||
private readonly Dictionary<uint, PhysicsTimestampGate> _gates = new();
|
||||
private readonly Dictionary<uint, WorldSession.EntitySpawn> _snapshots = new();
|
||||
|
||||
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _snapshots;
|
||||
|
||||
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
|
||||
_snapshots.TryGetValue(guid, out spawn);
|
||||
|
||||
public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming)
|
||||
{
|
||||
if (!_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate))
|
||||
{
|
||||
gate = new PhysicsTimestampGate();
|
||||
_gates.Add(incoming.Guid, gate);
|
||||
}
|
||||
|
||||
PhysicsTimestamps timestamps = SpawnTimestamps(incoming);
|
||||
CreateObjectTimestampDisposition disposition = gate.SeedForCreateObject(
|
||||
timestamps.Position,
|
||||
timestamps.Movement,
|
||||
timestamps.State,
|
||||
timestamps.Vector,
|
||||
timestamps.Teleport,
|
||||
timestamps.ServerControlledMove,
|
||||
timestamps.ForcePosition,
|
||||
timestamps.ObjDesc,
|
||||
timestamps.Instance);
|
||||
|
||||
if (disposition is CreateObjectTimestampDisposition.StaleGeneration)
|
||||
return new InboundCreateResult(disposition, default, null, Current(gate));
|
||||
|
||||
if (disposition is not CreateObjectTimestampDisposition.ExistingGeneration
|
||||
|| !_snapshots.TryGetValue(incoming.Guid, out WorldSession.EntitySpawn retained))
|
||||
{
|
||||
_snapshots[incoming.Guid] = incoming;
|
||||
return new InboundCreateResult(disposition, incoming, null, Current(gate));
|
||||
}
|
||||
|
||||
WorldSession.EntitySpawn merged = MergeUntimestampedCreate(retained, incoming);
|
||||
_snapshots[incoming.Guid] = merged;
|
||||
return new InboundCreateResult(
|
||||
disposition,
|
||||
merged,
|
||||
incoming.Physics is null ? null : BuildSameGenerationEvents(incoming),
|
||||
Current(gate));
|
||||
}
|
||||
|
||||
public bool TryDelete(DeleteObject.Parsed delete, bool isLocalPlayer)
|
||||
{
|
||||
if (!_gates.TryGetValue(delete.Guid, out PhysicsTimestampGate? gate))
|
||||
return false;
|
||||
|
||||
if (!gate.TryAcceptDeleteEvent(delete.InstanceSequence, isLocalPlayer))
|
||||
return false;
|
||||
|
||||
_gates.Remove(delete.Guid);
|
||||
_snapshots.Remove(delete.Guid);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryApplyObjDesc(
|
||||
ObjDescEvent.Parsed update,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old)
|
||||
|| !gate.TryAcceptObjDescEvent(update.InstanceSequence, update.ObjDescSequence))
|
||||
{
|
||||
accepted = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
PhysicsSpawnData? physics = old.Physics;
|
||||
if (physics is { } desc)
|
||||
physics = desc with
|
||||
{
|
||||
Timestamps = desc.Timestamps with { ObjDesc = update.ObjDescSequence },
|
||||
};
|
||||
|
||||
accepted = old with
|
||||
{
|
||||
AnimPartChanges = update.ModelData.AnimPartChanges,
|
||||
TextureChanges = update.ModelData.TextureChanges,
|
||||
SubPalettes = update.ModelData.SubPalettes,
|
||||
BasePaletteId = update.ModelData.BasePaletteId,
|
||||
Physics = physics,
|
||||
};
|
||||
_snapshots[update.Guid] = accepted;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryApplyPickup(
|
||||
PickupEvent.Parsed update,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old)
|
||||
|| !gate.TryAcceptPositionChannelEvent(
|
||||
update.InstanceSequence, update.PositionSequence))
|
||||
{
|
||||
accepted = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
accepted = ApplyUnparentedPosition(old, null, update.PositionSequence);
|
||||
_snapshots[update.Guid] = accepted;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the parent branch embedded in a same-generation PhysicsDesc.
|
||||
/// Unlike standalone ParentEvent it carries no parent INSTANCE_TS, so only
|
||||
/// the child's shared POSITION_TS participates in freshness.
|
||||
/// </summary>
|
||||
public bool TryApplyCreateParent(
|
||||
CreateParentUpdate update,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
if (!TryGet(update.ChildGuid, out PhysicsTimestampGate? childGate, out WorldSession.EntitySpawn child)
|
||||
|| !childGate.TryAcceptPositionChannelEvent(
|
||||
update.ChildInstanceSequence, update.ChildPositionSequence))
|
||||
{
|
||||
accepted = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
accepted = ApplyParent(
|
||||
child,
|
||||
update.ParentGuid,
|
||||
update.ParentLocation,
|
||||
update.PlacementId,
|
||||
update.ChildPositionSequence);
|
||||
_snapshots[update.ChildGuid] = accepted;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryApplyParent(
|
||||
ParentEvent.Parsed update,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
if (!_gates.TryGetValue(update.ParentGuid, out PhysicsTimestampGate? parentGate)
|
||||
|| !parentGate.IsCurrentInstance(update.ParentInstanceSequence)
|
||||
|| !TryGet(update.ChildGuid, out PhysicsTimestampGate? childGate, out WorldSession.EntitySpawn child)
|
||||
|| !childGate.TryAcceptPositionChannelEvent(
|
||||
childGate.InstanceTimestamp, update.ChildPositionSequence))
|
||||
{
|
||||
accepted = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
accepted = ApplyParent(
|
||||
child,
|
||||
update.ParentGuid,
|
||||
update.ParentLocation,
|
||||
update.PlacementId,
|
||||
update.ChildPositionSequence);
|
||||
_snapshots[update.ChildGuid] = accepted;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryApplyMotion(
|
||||
WorldSession.EntityMotionUpdate update,
|
||||
bool retainPayload,
|
||||
out WorldSession.EntitySpawn accepted,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old))
|
||||
{
|
||||
accepted = default;
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool applyPayload = gate.TryAcceptMovementEvent(
|
||||
update.InstanceSequence,
|
||||
update.MovementSequence,
|
||||
update.ServerControlSequence);
|
||||
timestamps = Current(gate);
|
||||
WorldSession.EntitySpawn stamped = MirrorGateTimestamps(old, gate) with
|
||||
{
|
||||
MovementSequence = gate.MovementTimestamp,
|
||||
ServerControlSequence = gate.ServerControlledMoveTimestamp,
|
||||
};
|
||||
_snapshots[update.Guid] = stamped;
|
||||
|
||||
// Retail consumes MOVEMENT_TS before it discovers that the
|
||||
// SERVER_CONTROLLED_MOVE_TS is stale. Preserve that timestamp-only
|
||||
// mutation in the canonical snapshot even though no motion payload
|
||||
// is applied.
|
||||
if (!applyPayload)
|
||||
{
|
||||
accepted = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!retainPayload)
|
||||
{
|
||||
accepted = stamped;
|
||||
return true;
|
||||
}
|
||||
|
||||
PhysicsSpawnData? physics = stamped.Physics;
|
||||
if (physics is { } desc)
|
||||
physics = desc with
|
||||
{
|
||||
Movement = new PhysicsMovementData(
|
||||
ReadOnlyMemory<byte>.Empty,
|
||||
update.MotionState,
|
||||
update.IsAutonomous),
|
||||
Timestamps = desc.Timestamps with
|
||||
{
|
||||
Movement = gate.MovementTimestamp,
|
||||
ServerControlledMove = gate.ServerControlledMoveTimestamp,
|
||||
},
|
||||
};
|
||||
|
||||
accepted = stamped with
|
||||
{
|
||||
MotionState = update.MotionState,
|
||||
Physics = physics,
|
||||
};
|
||||
_snapshots[update.Guid] = accepted;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryApplyVector(
|
||||
VectorUpdate.Parsed update,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old)
|
||||
|| !gate.TryAcceptVectorEvent(update.InstanceSequence, update.VectorSequence))
|
||||
{
|
||||
accepted = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
PhysicsSpawnData? physics = old.Physics;
|
||||
if (physics is { } desc)
|
||||
physics = desc with
|
||||
{
|
||||
Velocity = update.Velocity,
|
||||
AngularVelocity = update.Omega,
|
||||
Timestamps = desc.Timestamps with { Vector = update.VectorSequence },
|
||||
};
|
||||
|
||||
accepted = old with { Physics = physics };
|
||||
_snapshots[update.Guid] = accepted;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryApplyState(
|
||||
SetState.Parsed update,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old)
|
||||
|| !gate.TryAcceptStateEvent(update.InstanceSequence, update.StateSequence))
|
||||
{
|
||||
accepted = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
PhysicsSpawnData? physics = old.Physics;
|
||||
if (physics is { } desc)
|
||||
physics = desc with
|
||||
{
|
||||
RawState = update.PhysicsState,
|
||||
Timestamps = desc.Timestamps with { State = update.StateSequence },
|
||||
};
|
||||
|
||||
accepted = old with
|
||||
{
|
||||
PhysicsState = update.PhysicsState,
|
||||
Physics = physics,
|
||||
};
|
||||
_snapshots[update.Guid] = accepted;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the addressed live incarnation exists, even when the
|
||||
/// position payload is rejected. This lets callers publish a freshly
|
||||
/// consumed FORCE_POSITION_TS without applying a stale pose.
|
||||
/// </summary>
|
||||
public bool TryApplyPosition(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
bool isLocalPlayer,
|
||||
System.Numerics.Quaternion? forcePositionRotation,
|
||||
System.Numerics.Vector3? currentLocalVelocity,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out WorldSession.EntitySpawn accepted,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old))
|
||||
{
|
||||
disposition = PositionTimestampDisposition.Rejected;
|
||||
accepted = default;
|
||||
timestamps = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool advancesTeleport = PhysicsTimestampGate.IsNewer(
|
||||
gate.TeleportTimestamp,
|
||||
update.TeleportSequence);
|
||||
disposition = gate.TryAcceptPositionEvent(
|
||||
update.InstanceSequence,
|
||||
update.PositionSequence,
|
||||
update.TeleportSequence,
|
||||
update.ForcePositionSequence,
|
||||
isLocalPlayer);
|
||||
timestamps = Current(gate);
|
||||
if (disposition is PositionTimestampDisposition.Rejected)
|
||||
{
|
||||
accepted = MirrorGateTimestamps(old, gate);
|
||||
_snapshots[update.Guid] = accepted;
|
||||
return true;
|
||||
}
|
||||
|
||||
CreateObject.ServerPosition appliedPosition = update.Position;
|
||||
if (disposition is PositionTimestampDisposition.ForcePosition
|
||||
&& forcePositionRotation is { } preserved)
|
||||
{
|
||||
appliedPosition = update.Position with
|
||||
{
|
||||
RotationW = preserved.W,
|
||||
RotationX = preserved.X,
|
||||
RotationY = preserved.Y,
|
||||
RotationZ = preserved.Z,
|
||||
};
|
||||
}
|
||||
|
||||
// PositionPack::UnPack (0x00516740) initializes an absent placement
|
||||
// id to zero; HandleReceivedPosition (0x00453FD0) forwards that exact
|
||||
// value to SetPlacementFrame on a normal accepted update.
|
||||
uint? appliedPlacement = disposition is PositionTimestampDisposition.Apply
|
||||
? update.PlacementId ?? 0u
|
||||
: old.PlacementId;
|
||||
|
||||
System.Numerics.Vector3? appliedVelocity = disposition switch
|
||||
{
|
||||
// ForcePosition returns immediately after BlipPlayer and retains
|
||||
// the local physics object's velocity.
|
||||
PositionTimestampDisposition.ForcePosition =>
|
||||
currentLocalVelocity ?? old.Physics?.Velocity,
|
||||
|
||||
// A fresh local teleport explicitly installs zero velocity. A
|
||||
// normal local correction does not consume PositionPack velocity.
|
||||
PositionTimestampDisposition.Apply when isLocalPlayer =>
|
||||
advancesTeleport
|
||||
? System.Numerics.Vector3.Zero
|
||||
: currentLocalVelocity ?? old.Physics?.Velocity,
|
||||
|
||||
// Remote MoveOrTeleport receives the unpacked vector; an absent
|
||||
// PositionPack velocity is initialized to zero by retail.
|
||||
PositionTimestampDisposition.Apply =>
|
||||
update.Velocity ?? System.Numerics.Vector3.Zero,
|
||||
|
||||
_ => old.Physics?.Velocity,
|
||||
};
|
||||
PhysicsSpawnData? physics = old.Physics;
|
||||
if (physics is { } desc)
|
||||
physics = desc with
|
||||
{
|
||||
Position = appliedPosition,
|
||||
AnimationFrame = appliedPlacement,
|
||||
Velocity = appliedVelocity,
|
||||
Parent = null,
|
||||
Timestamps = desc.Timestamps with
|
||||
{
|
||||
Position = gate.PositionTimestamp,
|
||||
Teleport = gate.TeleportTimestamp,
|
||||
ForcePosition = gate.ForcePositionTimestamp,
|
||||
},
|
||||
};
|
||||
|
||||
accepted = old with
|
||||
{
|
||||
Position = appliedPosition,
|
||||
PositionSequence = gate.PositionTimestamp,
|
||||
ParentGuid = null,
|
||||
ParentLocation = null,
|
||||
PlacementId = appliedPlacement,
|
||||
Physics = physics,
|
||||
};
|
||||
_snapshots[update.Guid] = accepted;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F751 is a notification gate only. Retail compares it to TELEPORT_TS but
|
||||
/// advances that timestamp later, with the accepted Position packet.
|
||||
/// </summary>
|
||||
public bool IsFreshTeleportStart(uint localPlayerGuid, ushort teleportSequence) =>
|
||||
_gates.TryGetValue(localPlayerGuid, out PhysicsTimestampGate? gate)
|
||||
&& gate.IsFreshTeleportStart(teleportSequence);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_gates.Clear();
|
||||
_snapshots.Clear();
|
||||
}
|
||||
|
||||
private bool TryGet(
|
||||
uint guid,
|
||||
out PhysicsTimestampGate gate,
|
||||
out WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
if (_gates.TryGetValue(guid, out gate!)
|
||||
&& _snapshots.TryGetValue(guid, out spawn))
|
||||
return true;
|
||||
|
||||
gate = null!;
|
||||
spawn = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static PhysicsTimestamps SpawnTimestamps(WorldSession.EntitySpawn spawn) =>
|
||||
spawn.Physics?.Timestamps
|
||||
?? new PhysicsTimestamps(
|
||||
spawn.PositionSequence,
|
||||
spawn.MovementSequence,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
spawn.ServerControlSequence,
|
||||
0,
|
||||
0,
|
||||
spawn.InstanceSequence);
|
||||
|
||||
private static AcceptedPhysicsTimestamps Current(PhysicsTimestampGate gate) => new(
|
||||
gate.InstanceTimestamp,
|
||||
gate.ServerControlledMoveTimestamp,
|
||||
gate.TeleportTimestamp,
|
||||
gate.ForcePositionTimestamp);
|
||||
|
||||
private static WorldSession.EntitySpawn MirrorGateTimestamps(
|
||||
WorldSession.EntitySpawn spawn,
|
||||
PhysicsTimestampGate gate)
|
||||
{
|
||||
if (spawn.Physics is not { } desc)
|
||||
return spawn;
|
||||
|
||||
return spawn with
|
||||
{
|
||||
Physics = desc with
|
||||
{
|
||||
Timestamps = new PhysicsTimestamps(
|
||||
gate.PositionTimestamp,
|
||||
gate.MovementTimestamp,
|
||||
gate.StateTimestamp,
|
||||
gate.VectorTimestamp,
|
||||
gate.TeleportTimestamp,
|
||||
gate.ServerControlledMoveTimestamp,
|
||||
gate.ForcePositionTimestamp,
|
||||
gate.ObjDescTimestamp,
|
||||
gate.InstanceTimestamp),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn MergeUntimestampedCreate(
|
||||
WorldSession.EntitySpawn retained,
|
||||
WorldSession.EntitySpawn incoming) =>
|
||||
incoming with
|
||||
{
|
||||
Position = retained.Position,
|
||||
SetupTableId = retained.SetupTableId,
|
||||
AnimPartChanges = retained.AnimPartChanges,
|
||||
TextureChanges = retained.TextureChanges,
|
||||
SubPalettes = retained.SubPalettes,
|
||||
BasePaletteId = retained.BasePaletteId,
|
||||
ObjScale = retained.ObjScale,
|
||||
MotionState = retained.MotionState,
|
||||
MotionTableId = retained.MotionTableId,
|
||||
PhysicsState = retained.PhysicsState,
|
||||
Friction = retained.Friction,
|
||||
Elasticity = retained.Elasticity,
|
||||
InstanceSequence = retained.InstanceSequence,
|
||||
MovementSequence = retained.MovementSequence,
|
||||
ServerControlSequence = retained.ServerControlSequence,
|
||||
PositionSequence = retained.PositionSequence,
|
||||
ParentGuid = retained.ParentGuid,
|
||||
ParentLocation = retained.ParentLocation,
|
||||
PlacementId = retained.PlacementId,
|
||||
Physics = retained.Physics,
|
||||
};
|
||||
|
||||
private static SameGenerationCreateObjectEvents BuildSameGenerationEvents(
|
||||
WorldSession.EntitySpawn incoming)
|
||||
{
|
||||
PhysicsSpawnData physics = incoming.Physics!.Value;
|
||||
PhysicsTimestamps ts = physics.Timestamps;
|
||||
|
||||
CreateParentUpdate? parent = physics.Parent is { } attachment
|
||||
? new CreateParentUpdate(
|
||||
incoming.Guid,
|
||||
attachment.Guid,
|
||||
attachment.LocationId,
|
||||
physics.AnimationFrame ?? 0u,
|
||||
ts.Instance,
|
||||
ts.Position)
|
||||
: null;
|
||||
WorldSession.EntityPositionUpdate? position = parent is null
|
||||
&& physics.Position is { } p
|
||||
&& p.LandblockId != 0
|
||||
? new WorldSession.EntityPositionUpdate(
|
||||
incoming.Guid,
|
||||
p,
|
||||
physics.Velocity,
|
||||
physics.AnimationFrame,
|
||||
IsGrounded: true,
|
||||
ts.Instance,
|
||||
ts.Position,
|
||||
ts.Teleport,
|
||||
ts.ForcePosition)
|
||||
: null;
|
||||
PickupEvent.Parsed? pickup = parent is null && position is null
|
||||
? new PickupEvent.Parsed(incoming.Guid, ts.Instance, ts.Position)
|
||||
: null;
|
||||
WorldSession.EntityMotionUpdate? movement =
|
||||
physics.Movement is { } movementData
|
||||
&& !movementData.RawData.IsEmpty
|
||||
&& movementData.MotionState is { } motionState
|
||||
? new WorldSession.EntityMotionUpdate(
|
||||
incoming.Guid,
|
||||
motionState,
|
||||
ts.Instance,
|
||||
ts.Movement,
|
||||
ts.ServerControlledMove,
|
||||
movementData.IsAutonomous is true)
|
||||
: null;
|
||||
|
||||
return new SameGenerationCreateObjectEvents(
|
||||
new ObjDescEvent.Parsed(
|
||||
incoming.Guid,
|
||||
new CreateObject.ModelData(
|
||||
incoming.BasePaletteId,
|
||||
incoming.SubPalettes,
|
||||
incoming.TextureChanges,
|
||||
incoming.AnimPartChanges),
|
||||
ts.Instance,
|
||||
ts.ObjDesc),
|
||||
parent,
|
||||
position,
|
||||
pickup,
|
||||
movement,
|
||||
new SetState.Parsed(incoming.Guid, physics.RawState, ts.Instance, ts.State),
|
||||
new VectorUpdate.Parsed(
|
||||
incoming.Guid,
|
||||
physics.Velocity ?? System.Numerics.Vector3.Zero,
|
||||
physics.AngularVelocity ?? System.Numerics.Vector3.Zero,
|
||||
ts.Instance,
|
||||
ts.Vector));
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn ApplyUnparentedPosition(
|
||||
WorldSession.EntitySpawn old,
|
||||
CreateObject.ServerPosition? position,
|
||||
ushort positionSequence)
|
||||
{
|
||||
PhysicsSpawnData? physics = old.Physics;
|
||||
if (physics is { } desc)
|
||||
physics = desc with
|
||||
{
|
||||
Position = position,
|
||||
Parent = null,
|
||||
Timestamps = desc.Timestamps with { Position = positionSequence },
|
||||
};
|
||||
|
||||
return old with
|
||||
{
|
||||
Position = position,
|
||||
ParentGuid = null,
|
||||
ParentLocation = null,
|
||||
PositionSequence = positionSequence,
|
||||
Physics = physics,
|
||||
};
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn ApplyParent(
|
||||
WorldSession.EntitySpawn old,
|
||||
uint parentGuid,
|
||||
uint parentLocation,
|
||||
uint placementId,
|
||||
ushort positionSequence)
|
||||
{
|
||||
PhysicsSpawnData? physics = old.Physics;
|
||||
if (physics is { } desc)
|
||||
physics = desc with
|
||||
{
|
||||
Position = null,
|
||||
Parent = new PhysicsAttachment(parentGuid, parentLocation),
|
||||
AnimationFrame = placementId,
|
||||
Timestamps = desc.Timestamps with { Position = positionSequence },
|
||||
};
|
||||
|
||||
return old with
|
||||
{
|
||||
Position = null,
|
||||
ParentGuid = parentGuid,
|
||||
ParentLocation = parentLocation,
|
||||
PlacementId = placementId,
|
||||
PositionSequence = positionSequence,
|
||||
Physics = physics,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct AcceptedPhysicsTimestamps(
|
||||
ushort Instance,
|
||||
ushort ServerControlledMove,
|
||||
ushort Teleport,
|
||||
ushort ForcePosition);
|
||||
|
||||
public readonly record struct CreateParentUpdate(
|
||||
uint ChildGuid,
|
||||
uint ParentGuid,
|
||||
uint ParentLocation,
|
||||
uint PlacementId,
|
||||
ushort ChildInstanceSequence,
|
||||
ushort ChildPositionSequence);
|
||||
|
||||
public readonly record struct SameGenerationCreateObjectEvents(
|
||||
ObjDescEvent.Parsed Appearance,
|
||||
CreateParentUpdate? Parent,
|
||||
WorldSession.EntityPositionUpdate? Position,
|
||||
PickupEvent.Parsed? Pickup,
|
||||
WorldSession.EntityMotionUpdate? Movement,
|
||||
SetState.Parsed State,
|
||||
VectorUpdate.Parsed Vector);
|
||||
|
||||
public readonly record struct InboundCreateResult(
|
||||
CreateObjectTimestampDisposition Disposition,
|
||||
WorldSession.EntitySpawn Snapshot,
|
||||
SameGenerationCreateObjectEvents? SameGenerationEvents,
|
||||
AcceptedPhysicsTimestamps Timestamps);
|
||||
Loading…
Add table
Add a link
Reference in a new issue