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
|
|
@ -1,5 +1,6 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
|
|
@ -10,8 +11,9 @@ namespace AcDream.Core.Net.Messages;
|
|||
/// weenies like the Holtburg foundry statue) in the client's loaded area.
|
||||
///
|
||||
/// <para>
|
||||
/// acdream's parser extracts only the fields needed to hand the spawn off
|
||||
/// to <c>IGameState</c>:
|
||||
/// The parser preserves the complete PhysicsDesc needed to construct a retail
|
||||
/// physics object, while retaining legacy convenience fields during the
|
||||
/// LiveEntityRuntime migration:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><b>GUID</b> — always at the start of the body (after the opcode).</item>
|
||||
|
|
@ -25,13 +27,9 @@ namespace AcDream.Core.Net.Messages;
|
|||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// Most other fields (extended weenie header, object description, motion tables,
|
||||
/// palettes, texture overrides, animation frames, velocity, ...) are
|
||||
/// consumed-but-ignored so the parse position ends up wherever the
|
||||
/// client-side caller wanted — a <c>Parse</c> call doesn't need to reach
|
||||
/// the end of the body to return useful output. We read through the fixed
|
||||
/// WeenieHeader prefix for Name/ItemType, then stop before optional header
|
||||
/// tails.
|
||||
/// Every PhysicsDesc field is captured, including presence-preserving zero
|
||||
/// values and all nine timestamps. The PublicWeenieDesc parser continues to
|
||||
/// retain its supported presentation/gameplay subset.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -120,7 +118,7 @@ public static class CreateObject
|
|||
ushort ServerControlSequence = 0,
|
||||
ushort ForcePositionSequence = 0,
|
||||
// L.2g S1 (DEV-6): ObjectMovement stamp (timestamp block index 1)
|
||||
// seeds MotionSequenceGate's MOVEMENT_TS at spawn.
|
||||
// seeds PhysicsTimestampGate's MOVEMENT_TS at spawn.
|
||||
ushort MovementSequence = 0,
|
||||
// Parent/placement bootstrap for equipped child objects. These are
|
||||
// the CreateObject equivalents of ParentEvent 0xF749.
|
||||
|
|
@ -208,7 +206,11 @@ public static class CreateObject
|
|||
uint? PetOwnerId = null,
|
||||
// PublicWeenieDesc._ammoType, gated by WeenieHeader flag 0x100.
|
||||
// AMMO_NONE is the explicit wire value zero; null means absent.
|
||||
ushort? AmmoType = null);
|
||||
ushort? AmmoType = null,
|
||||
// Complete immutable PhysicsDesc projection. Kept alongside the
|
||||
// legacy convenience fields while live-entity ownership migrates to
|
||||
// LiveEntityRuntime in Step 2.
|
||||
PhysicsSpawnData? Physics = null);
|
||||
|
||||
/// <summary>
|
||||
/// The relevant subset of the server-sent <c>MovementData</c> /
|
||||
|
|
@ -479,8 +481,8 @@ public static class CreateObject
|
|||
/// Parse a reassembled CreateObject body. <paramref name="body"/> must
|
||||
/// start with the 4-byte opcode. Returns <c>null</c> if the body is
|
||||
/// malformed (truncated field); returns a populated <see cref="Parsed"/>
|
||||
/// on success. The parser stops at the end of PhysicsData; subsequent
|
||||
/// weenie-header fields are deliberately not consumed.
|
||||
/// on success after consuming the supported PhysicsDesc and public
|
||||
/// WeenieHeader fields.
|
||||
/// </summary>
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
|
|
@ -492,6 +494,17 @@ public static class CreateObject
|
|||
float? objScale = null;
|
||||
ServerMotionState? motionState = null;
|
||||
uint? motionTableId = null;
|
||||
PhysicsMovementData? movement = null;
|
||||
uint? soundTableId = null;
|
||||
uint? physicsScriptTableId = null;
|
||||
ReadOnlyMemory<PhysicsAttachment>? children = null;
|
||||
float? translucency = null;
|
||||
Vector3? velocity = null;
|
||||
Vector3? acceleration = null;
|
||||
Vector3? angularVelocity = null;
|
||||
uint? defaultScriptType = null;
|
||||
float? defaultScriptIntensity = null;
|
||||
PhysicsSpawnData? physics = null;
|
||||
// Commit A 2026-04-29 — live-entity collision plumbing. PhysicsState
|
||||
// (acclient.h:2815) was previously skipped at line ~337; the PWD
|
||||
// _bitfield (acclient.h:6431-6463) was previously discarded as
|
||||
|
|
@ -547,11 +560,16 @@ public static class CreateObject
|
|||
{
|
||||
if (body.Length - pos < (int)movementLen) return null;
|
||||
int movementStart = pos;
|
||||
motionState = TryParseMovementData(body.Slice(movementStart, (int)movementLen));
|
||||
ReadOnlySpan<byte> movementBytes = body.Slice(movementStart, (int)movementLen);
|
||||
motionState = TryParseMovementData(movementBytes);
|
||||
pos = movementStart + (int)movementLen;
|
||||
if (body.Length - pos < 4) return null;
|
||||
pos += 4; // isAutonomous u32
|
||||
bool isAutonomous = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)) != 0;
|
||||
pos += 4;
|
||||
movement = new PhysicsMovementData(movementBytes.ToArray(), motionState, isAutonomous);
|
||||
}
|
||||
else
|
||||
movement = new PhysicsMovementData(ReadOnlyMemory<byte>.Empty, null, null);
|
||||
}
|
||||
else if ((physicsFlags & PhysicsDescriptionFlag.AnimationFrame) != 0)
|
||||
{
|
||||
|
|
@ -585,12 +603,14 @@ public static class CreateObject
|
|||
if ((physicsFlags & PhysicsDescriptionFlag.STable) != 0)
|
||||
{
|
||||
if (body.Length - pos < 4) return null;
|
||||
soundTableId = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.PeTable) != 0)
|
||||
{
|
||||
if (body.Length - pos < 4) return null;
|
||||
physicsScriptTableId = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
|
|
@ -616,19 +636,27 @@ public static class CreateObject
|
|||
if (body.Length - pos < 4) return null;
|
||||
int childCount = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
if (childCount < 0 || childCount > 1024) return PartialResult();
|
||||
if (childCount < 0 || childCount > 1024) return null;
|
||||
if (body.Length - pos < childCount * 8) return null;
|
||||
pos += childCount * 8; // each child = guid u32 + locationId u32
|
||||
var parsedChildren = new PhysicsAttachment[childCount];
|
||||
for (int i = 0; i < parsedChildren.Length; i++)
|
||||
{
|
||||
parsedChildren[i] = new PhysicsAttachment(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos + 4)));
|
||||
pos += 8;
|
||||
}
|
||||
children = parsedChildren;
|
||||
}
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.ObjScale) != 0)
|
||||
{
|
||||
if (body.Length - pos < 4) return PartialResult();
|
||||
if (body.Length - pos < 4) return null;
|
||||
objScale = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Friction) != 0)
|
||||
{
|
||||
if (body.Length - pos < 4) return PartialResult();
|
||||
if (body.Length - pos < 4) return null;
|
||||
friction = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
|
|
@ -641,31 +669,89 @@ public static class CreateObject
|
|||
// Was previously dropped — every object got the default
|
||||
// 0.05f, so server-set bouncier surfaces felt identical to
|
||||
// walls.
|
||||
if (body.Length - pos < 4) return PartialResult();
|
||||
if (body.Length - pos < 4) return null;
|
||||
elasticity = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Translucency) != 0) pos += 4;
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Velocity) != 0) pos += 12; // vec3
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Acceleration) != 0) pos += 12;
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Omega) != 0) pos += 12;
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScript) != 0) pos += 4;
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScriptIntensity) != 0) pos += 4;
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Translucency) != 0)
|
||||
{
|
||||
if (body.Length - pos < 4) return null;
|
||||
translucency = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Velocity) != 0)
|
||||
{
|
||||
if (!TryReadVector3(body, ref pos, out Vector3 value)) return null;
|
||||
velocity = value;
|
||||
}
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Acceleration) != 0)
|
||||
{
|
||||
if (!TryReadVector3(body, ref pos, out Vector3 value)) return null;
|
||||
acceleration = value;
|
||||
}
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Omega) != 0)
|
||||
{
|
||||
if (!TryReadVector3(body, ref pos, out Vector3 value)) return null;
|
||||
angularVelocity = value;
|
||||
}
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScript) != 0)
|
||||
{
|
||||
if (body.Length - pos < 4) return null;
|
||||
defaultScriptType = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScriptIntensity) != 0)
|
||||
{
|
||||
if (body.Length - pos < 4) return null;
|
||||
defaultScriptIntensity = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
// 9 sequence timestamps, always present at end of PhysicsData.
|
||||
// PhysicsTimeStamp enum order (acclient.h:6084; ACE
|
||||
// WorldObject_Networking.cs:411-420): 0=position, 1=movement,
|
||||
// 4=teleport, 5=serverControl, 6=forcePosition, 8=instance.
|
||||
if (body.Length - pos < 9 * 2) return PartialResult();
|
||||
if (body.Length - pos < 9 * 2) return null;
|
||||
var seqSpan = body.Slice(pos, 9 * 2);
|
||||
ushort instanceSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(8 * 2));
|
||||
ushort positionSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(0 * 2));
|
||||
ushort movementSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(1 * 2));
|
||||
ushort stateSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(2 * 2));
|
||||
ushort vectorSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(3 * 2));
|
||||
ushort teleportSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(4 * 2));
|
||||
ushort serverControlSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(5 * 2));
|
||||
ushort forcePositionSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(6 * 2));
|
||||
ushort objDescSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(7 * 2));
|
||||
ushort instanceSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(8 * 2));
|
||||
pos += 9 * 2;
|
||||
AlignTo4(ref pos);
|
||||
if (pos > body.Length) return null;
|
||||
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
positionSeq, movementSeq, stateSeq, vectorSeq, teleportSeq,
|
||||
serverControlSeq, forcePositionSeq, objDescSeq, instanceSeq);
|
||||
physics = new PhysicsSpawnData(
|
||||
RawState: physicsState.Value,
|
||||
Position: position,
|
||||
Movement: movement,
|
||||
AnimationFrame: placementId,
|
||||
SetupTableId: setupTableId,
|
||||
MotionTableId: motionTableId,
|
||||
SoundTableId: soundTableId,
|
||||
PhysicsScriptTableId: physicsScriptTableId,
|
||||
Parent: parentGuid.HasValue && parentLocation.HasValue
|
||||
? new PhysicsAttachment(parentGuid.Value, parentLocation.Value)
|
||||
: null,
|
||||
Children: children,
|
||||
Scale: objScale,
|
||||
Friction: friction,
|
||||
Elasticity: elasticity,
|
||||
Translucency: translucency,
|
||||
Velocity: velocity,
|
||||
Acceleration: acceleration,
|
||||
AngularVelocity: angularVelocity,
|
||||
DefaultScriptType: defaultScriptType,
|
||||
DefaultScriptIntensity: defaultScriptIntensity,
|
||||
Timestamps: timestamps);
|
||||
|
||||
// --- WeenieHeader: read the fixed prefix fields we need. ---
|
||||
// ACE WorldObject_Networking.SerializeCreateObject writes:
|
||||
|
|
@ -1026,17 +1112,8 @@ public static class CreateObject
|
|||
CombatUse: combatUse,
|
||||
PluralName: pluralName,
|
||||
PetOwnerId: petOwnerId,
|
||||
AmmoType: ammoType);
|
||||
|
||||
// Local helper: if we ran out of fields past PhysicsData, still
|
||||
// return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion).
|
||||
Parsed PartialResult() => new(
|
||||
guid, position, setupTableId, animParts,
|
||||
textureChanges, subPalettes, basePaletteId, objScale, null, null, motionState, motionTableId,
|
||||
PhysicsState: physicsState,
|
||||
ObjectDescriptionFlags: objectDescriptionFlags,
|
||||
Friction: friction,
|
||||
Elasticity: elasticity);
|
||||
AmmoType: ammoType,
|
||||
Physics: physics);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -1391,4 +1468,20 @@ public static class CreateObject
|
|||
runRate = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadVector3(ReadOnlySpan<byte> body, ref int pos, out Vector3 value)
|
||||
{
|
||||
if (body.Length - pos < 12)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 4)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 8)));
|
||||
pos += 12;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue