feat(net): Phase 6.7 — parse UpdatePosition (0xF748) into PositionUpdated event

Companion to the Phase 6.6 UpdateMotion parser. Without this, every
server-spawned entity stays frozen at its CreateObject origin forever
— NPCs don't patrol, creatures don't hunt, other players don't walk
past. UpdatePosition is the per-entity position delta the server sends
on every movement tick.

The wire format is straightforward but fiddly:
  u32 opcode | u32 guid | u32 flags | u32 cellId | 3xf32 pos
  (0..4) conditional f32 rotation components, present iff the
  corresponding OrientationHasNo* flag is CLEAR
  optional 3xf32 velocity iff HasVelocity
  optional u32 placementId iff HasPlacementID
  four u16 sequence numbers (consumed but not used)

Layout ported from references/ACE/Source/ACE.Server/Network/Structure/
PositionPack.cs::Write and ACE.Entity/Enum/PositionFlags.cs.

WorldSession dispatches PositionUpdated(guid, position, velocity) on
a successful parse. GameWindow wiring (guid → WorldEntity lookup and
transform swap) is deferred to the same follow-up commit that lands
Phase 6.6 wiring, after the in-flight Phase 9.1 translucent-pass work
merges so we don't step on GameWindow.cs edits.

96 Core.Net tests (was 89, +7 for UpdatePosition coverage).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-11 20:37:32 +02:00
parent a71db90310
commit 333a7c197a
3 changed files with 371 additions and 0 deletions

View file

@ -74,6 +74,23 @@ public sealed class WorldSession : IDisposable
/// </summary>
public event Action<EntityMotionUpdate>? MotionUpdated;
/// <summary>
/// Payload for <see cref="PositionUpdated"/>: the server guid plus a
/// full <see cref="CreateObject.ServerPosition"/> describing the
/// entity's new world position and rotation. Subscribers translate
/// the landblock-local position into acdream world space and reseat
/// the corresponding <c>WorldEntity</c>.
/// </summary>
public readonly record struct EntityPositionUpdate(
uint Guid,
CreateObject.ServerPosition Position,
System.Numerics.Vector3? Velocity);
/// <summary>
/// Fires when the session parses a 0xF748 UpdatePosition game message.
/// </summary>
public event Action<EntityPositionUpdate>? PositionUpdated;
/// <summary>Raised every time the state machine transitions.</summary>
public event Action<State>? StateChanged;
@ -275,6 +292,22 @@ public sealed class WorldSession : IDisposable
motion.Value.MotionState));
}
}
else if (op == UpdatePosition.Opcode)
{
// Phase 6.7: the server sends UpdatePosition (0xF748) every
// time an entity moves through the world — NPC patrols,
// creatures hunting, other players walking past, projectiles
// tracking. Without this, everything stays at its
// CreateObject spawn point forever.
var posUpdate = UpdatePosition.TryParse(body);
if (posUpdate is not null)
{
PositionUpdated?.Invoke(new EntityPositionUpdate(
posUpdate.Value.Guid,
posUpdate.Value.Position,
posUpdate.Value.Velocity));
}
}
}
}