acdream/tests/AcDream.Core.Net.Tests/Messages/UpdatePositionTests.cs
Erik 333a7c197a 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>
2026-04-11 20:37:32 +02:00

174 lines
6 KiB
C#

using System;
using System.Buffers.Binary;
using AcDream.Core.Net.Messages;
using Xunit;
namespace AcDream.Core.Net.Tests.Messages;
/// <summary>
/// Covers <see cref="UpdatePosition.TryParse"/> — the 0xF748 GameMessage
/// the server sends whenever an entity's world position changes. The
/// layout relies on conditional field presence driven by PositionFlags,
/// so the tests exercise both the full-rotation and missing-component
/// paths plus the optional velocity/placement fields.
/// </summary>
public class UpdatePositionTests
{
[Fact]
public void RejectsWrongOpcode()
{
var body = new byte[64];
BinaryPrimitives.WriteUInt32LittleEndian(body, 0xCAFEBABE);
Assert.Null(UpdatePosition.TryParse(body));
}
[Fact]
public void RejectsTruncated()
{
Assert.Null(UpdatePosition.TryParse(Array.Empty<byte>()));
Assert.Null(UpdatePosition.TryParse(new byte[3]));
}
[Fact]
public void ParsesAllFourRotationComponentsPresent()
{
// Full rotation, no velocity, no placement.
var body = BuildBody(
guid: 0x12345678u,
flags: 0,
cellId: 0xA9B4001Au,
px: 10f, py: 20f, pz: 30f,
rw: 1f, rx: 0f, ry: 0f, rz: 0f);
var result = UpdatePosition.TryParse(body);
Assert.NotNull(result);
Assert.Equal(0x12345678u, result!.Value.Guid);
Assert.Equal(0xA9B4001Au, result.Value.Position.LandblockId);
Assert.Equal(10f, result.Value.Position.PositionX);
Assert.Equal(20f, result.Value.Position.PositionY);
Assert.Equal(30f, result.Value.Position.PositionZ);
Assert.Equal(1f, result.Value.Position.RotationW);
Assert.Null(result.Value.Velocity);
Assert.Null(result.Value.PlacementId);
}
[Fact]
public void ParsesWithMissingRotationComponents()
{
// OrientationHasNoX | OrientationHasNoY | OrientationHasNoZ — only
// W is present. The missing components default to 0, which is the
// convention the server assumes.
uint flags = 0x10 | 0x20 | 0x40;
var body = BuildBodyPartial(
guid: 0xDEADBEEF,
flags: flags,
cellId: 0x01BB0001,
px: 1f, py: 2f, pz: 3f,
rotationComponents: new float[] { 0.707f });
var result = UpdatePosition.TryParse(body);
Assert.NotNull(result);
Assert.Equal(0.707f, result!.Value.Position.RotationW, precision: 4);
Assert.Equal(0f, result.Value.Position.RotationX);
Assert.Equal(0f, result.Value.Position.RotationY);
Assert.Equal(0f, result.Value.Position.RotationZ);
}
[Fact]
public void ParsesHasVelocityFlag()
{
// HasVelocity(0x01) + all four rotation components present.
uint flags = 0x01;
using var ms = new System.IO.MemoryStream();
using var bw = new System.IO.BinaryWriter(ms);
bw.Write(UpdatePosition.Opcode);
bw.Write(0xAABBCCDDu);
bw.Write(flags);
bw.Write(0x01020003u);
bw.Write(5f); bw.Write(6f); bw.Write(7f);
bw.Write(1f); bw.Write(0f); bw.Write(0f); bw.Write(0f);
bw.Write(100f); bw.Write(200f); bw.Write(300f); // velocity
var result = UpdatePosition.TryParse(ms.ToArray());
Assert.NotNull(result);
Assert.NotNull(result!.Value.Velocity);
Assert.Equal(100f, result.Value.Velocity!.Value.X);
Assert.Equal(200f, result.Value.Velocity.Value.Y);
Assert.Equal(300f, result.Value.Velocity.Value.Z);
}
[Fact]
public void ParsesHasPlacementIdFlag()
{
// HasPlacementID(0x02) — placement id u32 follows rotation (no velocity).
uint flags = 0x02;
using var ms = new System.IO.MemoryStream();
using var bw = new System.IO.BinaryWriter(ms);
bw.Write(UpdatePosition.Opcode);
bw.Write(0x11111111u);
bw.Write(flags);
bw.Write(0x00000001u);
bw.Write(0f); bw.Write(0f); bw.Write(0f);
bw.Write(1f); bw.Write(0f); bw.Write(0f); bw.Write(0f);
bw.Write((uint)0x65); // Placement.Resting
var result = UpdatePosition.TryParse(ms.ToArray());
Assert.NotNull(result);
Assert.Equal((uint)0x65, result!.Value.PlacementId);
}
[Fact]
public void ParsesBothVelocityAndPlacement()
{
uint flags = 0x01 | 0x02;
using var ms = new System.IO.MemoryStream();
using var bw = new System.IO.BinaryWriter(ms);
bw.Write(UpdatePosition.Opcode);
bw.Write(0x22222222u);
bw.Write(flags);
bw.Write(0x04050607u);
bw.Write(10f); bw.Write(20f); bw.Write(30f);
bw.Write(1f); bw.Write(0f); bw.Write(0f); bw.Write(0f);
bw.Write(1f); bw.Write(2f); bw.Write(3f);
bw.Write((uint)42u);
var result = UpdatePosition.TryParse(ms.ToArray());
Assert.NotNull(result);
Assert.NotNull(result!.Value.Velocity);
Assert.Equal(42u, result.Value.PlacementId);
}
// ---- helpers ----
private static byte[] BuildBody(
uint guid, uint flags, uint cellId,
float px, float py, float pz,
float rw, float rx, float ry, float rz)
{
using var ms = new System.IO.MemoryStream();
using var bw = new System.IO.BinaryWriter(ms);
bw.Write(UpdatePosition.Opcode);
bw.Write(guid);
bw.Write(flags);
bw.Write(cellId);
bw.Write(px); bw.Write(py); bw.Write(pz);
bw.Write(rw); bw.Write(rx); bw.Write(ry); bw.Write(rz);
return ms.ToArray();
}
private static byte[] BuildBodyPartial(
uint guid, uint flags, uint cellId,
float px, float py, float pz,
float[] rotationComponents)
{
using var ms = new System.IO.MemoryStream();
using var bw = new System.IO.BinaryWriter(ms);
bw.Write(UpdatePosition.Opcode);
bw.Write(guid);
bw.Write(flags);
bw.Write(cellId);
bw.Write(px); bw.Write(py); bw.Write(pz);
foreach (var c in rotationComponents) bw.Write(c);
return ms.ToArray();
}
}