diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 7886a86..bf46b46 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -66,6 +66,14 @@ public sealed class GameWindow : IDisposable private int _liveCenterX; private int _liveCenterY; private uint _liveEntityIdCounter = 1_000_000u; // well above any dat-hydrated id + + /// + /// Phase 6.6/6.7: server-guid → local WorldEntity lookup so + /// UpdateMotion and UpdatePosition handlers can find the entity the + /// server is talking about. The sequential + /// keys the render list; this parallel dictionary keys by server guid. + /// + private readonly Dictionary _entitiesByServerGuid = new(); private int _liveSpawnReceived; // diagnostics private int _liveSpawnHydrated; private int _liveDropReasonNoPos; @@ -589,6 +597,8 @@ public sealed class GameWindow : IDisposable Console.WriteLine($"live: connecting to {endpoint} as {user}"); _liveSession = new AcDream.Core.Net.WorldSession(endpoint); _liveSession.EntitySpawned += OnLiveEntitySpawned; + _liveSession.MotionUpdated += OnLiveMotionUpdated; + _liveSession.PositionUpdated += OnLivePositionUpdated; _liveSession.Connect(user, pass); if (_liveSession.Characters is null || _liveSession.Characters.Characters.Count == 0) @@ -932,6 +942,10 @@ public sealed class GameWindow : IDisposable _entities = extended; _liveSpawnHydrated++; + // Phase 6.6/6.7: remember the server-guid → WorldEntity mapping so + // UpdateMotion / UpdatePosition events can reseat this entity by guid. + _entitiesByServerGuid[spawn.Guid] = entity; + // Phase 6.4: register for per-frame playback if we resolved a real // cycle with a non-zero framerate and at least two frames in the // cycle (single-frame poses are static and don't need ticking). @@ -1015,6 +1029,74 @@ public sealed class GameWindow : IDisposable return hx0 * (1 - ty) + hx1 * ty; } + /// + /// Phase 6.6: the server says an entity's motion has changed. Look up + /// the AnimatedEntity for that guid, re-resolve the idle cycle with the + /// new (stance, forward-command) override, and if the cycle is still + /// animated, swap in the new animation/frame range. Entities not in + /// the animated map (static props, entities rejected at spawn time) + /// are simply ignored — there's nothing to tick for them. + /// + private void OnLiveMotionUpdated(AcDream.Core.Net.WorldSession.EntityMotionUpdate update) + { + if (_dats is null) return; + if (!_entitiesByServerGuid.TryGetValue(update.Guid, out var entity)) return; + if (!_animatedEntities.TryGetValue(entity.Id, out var ae)) return; + + // Re-resolve using the new stance/command. Keep the setup and + // motion-table we already know about — the server's motion + // updates override state within the same table, not swap tables. + ushort stance = update.MotionState.Stance; + ushort? command = update.MotionState.ForwardCommand; + + var newCycle = AcDream.Core.Meshing.MotionResolver.GetIdleCycle( + ae.Setup, _dats, + motionTableIdOverride: null, // same table; already burned into ae.Animation + stanceOverride: stance, + commandOverride: command); + + if (newCycle is null || newCycle.Framerate == 0f + || newCycle.HighFrame <= newCycle.LowFrame + || newCycle.Animation.PartFrames.Count <= 1) + { + // New pose is a static one — stop animating and leave the + // entity on its last rendered frame. Removing from the map + // means the tick no longer updates it. + _animatedEntities.Remove(entity.Id); + return; + } + + ae.Animation = newCycle.Animation; + ae.LowFrame = Math.Max(0, newCycle.LowFrame); + ae.HighFrame = Math.Min(newCycle.HighFrame, newCycle.Animation.PartFrames.Count - 1); + ae.Framerate = newCycle.Framerate; + ae.CurrFrame = ae.LowFrame; + } + + /// + /// Phase 6.7: the server says an entity moved. Translate its new + /// landblock-local position into acdream world space (same math as + /// CreateObject hydration) and update the entity's Position/Rotation + /// in place so the next Draw picks up the new transform. + /// + private void OnLivePositionUpdated(AcDream.Core.Net.WorldSession.EntityPositionUpdate update) + { + if (!_entitiesByServerGuid.TryGetValue(update.Guid, out var entity)) return; + + var p = update.Position; + int lbX = (int)((p.LandblockId >> 24) & 0xFFu); + int lbY = (int)((p.LandblockId >> 16) & 0xFFu); + var origin = new System.Numerics.Vector3( + (lbX - _liveCenterX) * 192f, + (lbY - _liveCenterY) * 192f, + 0f); + var worldPos = new System.Numerics.Vector3(p.PositionX, p.PositionY, p.PositionZ) + origin; + var rot = new System.Numerics.Quaternion(p.RotationX, p.RotationY, p.RotationZ, p.RotationW); + + entity.Position = worldPos; + entity.Rotation = rot; + } + private void OnUpdate(double dt) { // Drain any pending live-session traffic. Non-blocking — returns diff --git a/src/AcDream.Core/World/WorldEntity.cs b/src/AcDream.Core/World/WorldEntity.cs index 31441fd..708d825 100644 --- a/src/AcDream.Core/World/WorldEntity.cs +++ b/src/AcDream.Core/World/WorldEntity.cs @@ -6,8 +6,13 @@ public sealed class WorldEntity { public required uint Id { get; init; } public required uint SourceGfxObjOrSetupId { get; init; } - public required Vector3 Position { get; init; } - public required Quaternion Rotation { get; init; } + /// + /// World-space position. Settable so Phase 6.7 position-update events + /// can reseat an existing entity without rebuilding its meshes. + /// + public required Vector3 Position { get; set; } + /// Settable for the same reason as . + public required Quaternion Rotation { get; set; } /// /// Per-part mesh references with their root-relative transforms. /// Mutable so the animation tick can replace it each frame for