feat(net): Phase 6.6 — parse UpdateMotion (0xF74C) into MotionUpdated event

Server sends UpdateMotion whenever an entity's motion state changes:
NPCs starting a walk cycle, creatures switching to a combat stance,
doors opening, a player waving, etc. Phase 6.1-6.4 already handles
rendering different (stance, forward-command) pairs for the INITIAL
CreateObject, but without this message NPCs freeze in whatever pose
they spawned with and never transition to walking/fighting.

Added UpdateMotion.TryParse with the same ServerMotionState the
CreateObject path uses, reached via a slightly different outer
layout (guid + instance seq + header'd MovementData; the MovementData
starts with the 8-byte sequence/autonomous header this time rather
than being preceded by a length field). Only the (stance, forward-
command) pair is extracted — same subset CreateObject grabs.

WorldSession dispatches MotionUpdated(guid, state) when a 0xF74C
body parses successfully. The App-side wiring (guid→entity lookup
and AnimatedEntity cycle swap) is intentionally deferred to a
separate commit because it touches GameWindow which is currently
being edited by the Phase 9.1 translucent-pass work.

89 Core.Net tests (was 83, +6 for UpdateMotion coverage).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-11 20:33:26 +02:00
parent 4752b8a528
commit a71db90310
12 changed files with 675 additions and 45 deletions

View file

@ -58,6 +58,22 @@ public sealed class WorldSession : IDisposable
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
public event Action<EntitySpawn>? EntitySpawned;
/// <summary>
/// Payload for <see cref="MotionUpdated"/>: the server guid of the entity
/// whose motion changed and its new server-side stance + forward command.
/// The renderer uses these to drive per-entity cycle switching.
/// </summary>
public readonly record struct EntityMotionUpdate(
uint Guid,
CreateObject.ServerMotionState MotionState);
/// <summary>
/// Fires when the session parses a 0xF74C UpdateMotion game message.
/// Subscribers can look up the entity by guid and transition its
/// animation cycle to the new (stance, forward-command) pair.
/// </summary>
public event Action<EntityMotionUpdate>? MotionUpdated;
/// <summary>Raised every time the state machine transitions.</summary>
public event Action<State>? StateChanged;
@ -243,6 +259,22 @@ public sealed class WorldSession : IDisposable
parsed.Value.MotionTableId));
}
}
else if (op == UpdateMotion.Opcode)
{
// Phase 6.6: the server sends UpdateMotion (0xF74C) whenever an
// already-spawned entity changes its motion state — NPCs
// starting a walk cycle, creatures entering combat, doors
// opening, etc. We dispatch a lightweight event with the
// new (stance, forward-command) pair so the animation
// system can swap the entity's cycle.
var motion = UpdateMotion.TryParse(body);
if (motion is not null)
{
MotionUpdated?.Invoke(new EntityMotionUpdate(
motion.Value.Guid,
motion.Value.MotionState));
}
}
}
}