diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 8636f38..53e7e60 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1238,14 +1238,13 @@ public sealed class GameWindow : IDisposable private void OnUpdate(double dt) { - // Drain any pending live-session traffic. Non-blocking — returns - // immediately if no datagrams are in the kernel buffer. Fires - // EntitySpawned events synchronously on this thread. - _liveSession?.Tick(); - - // Phase A.1: advance the streaming controller. Computes the observer's - // current landblock coordinates and feeds new load/unload diffs to the - // streamer, then drains any completed landblocks into GpuWorldState. + // Phase A.1: advance the streaming controller FIRST so the initial + // landblocks are loaded into GpuWorldState before live-session + // CreateObject events drain. The earlier order (live tick first, + // streaming tick second) caused the initial CreateObject flood from + // login to land before any landblock was loaded; AppendLiveEntity + // is a no-op for unloaded landblocks, so all 40+ NPCs/weenies were + // silently dropped on the first frame and never rendered. if (_streamingController is not null && _cameraController is not null) { int observerCx = _liveCenterX; @@ -1274,6 +1273,12 @@ public sealed class GameWindow : IDisposable _streamingController.Tick(observerCx, observerCy); } + // Drain pending live-session traffic AFTER streaming so any incoming + // CreateObject events find their landblock already loaded in + // GpuWorldState. Non-blocking — returns immediately if no datagrams + // are in the kernel buffer. Fires EntitySpawned events synchronously. + _liveSession?.Tick(); + if (_cameraController is null || _input is null) return; if (!_cameraController.IsFlyMode) return;