diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index b6d104f..6f6ef34 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -37,6 +37,10 @@ public sealed class GameWindow : IDisposable private uint _liveEntityIdCounter = 1_000_000u; // well above any dat-hydrated id private int _liveSpawnReceived; // diagnostics private int _liveSpawnHydrated; + private int _liveDropReasonNoPos; + private int _liveDropReasonNoSetup; + private int _liveDropReasonSetupDatMissing; + private int _liveDropReasonNoMeshRefs; public GameWindow(string datDir, WorldGameState worldGameState, WorldEvents worldEvents) { @@ -513,12 +517,25 @@ public sealed class GameWindow : IDisposable private void OnLiveEntitySpawned(AcDream.Core.Net.WorldSession.EntitySpawn spawn) { _liveSpawnReceived++; + + // Log every spawn that arrives so we can inventory what the server + // sends (including the ones we can't render yet). The foundry statue + // hunt in Phase 2c / 4.7 is the main reason for this — we want to + // see EVERY guid+setup to find it in the list. + string posStr = spawn.Position is { } sp + ? $"({sp.PositionX:F1},{sp.PositionY:F1},{sp.PositionZ:F1})@0x{sp.LandblockId:X8}" + : "no-pos"; + string setupStr = spawn.SetupTableId is { } su ? $"0x{su:X8}" : "no-setup"; + Console.WriteLine($"live: spawn guid=0x{spawn.Guid:X8} setup={setupStr} pos={posStr}"); + if (_dats is null || _staticMesh is null) return; if (spawn.Position is null || spawn.SetupTableId is null) { // Can't place a mesh without both. Most of these are inventory // items anyway (no position because they're held), which have no // visible world presence. + if (spawn.Position is null) _liveDropReasonNoPos++; + else _liveDropReasonNoSetup++; return; } @@ -541,7 +558,13 @@ public sealed class GameWindow : IDisposable // Hydrate mesh refs from the Setup dat. This is the same code path // used by the static scenery pipeline (see the Setup hydration above). var setup = _dats.Get(spawn.SetupTableId.Value); - if (setup is null) return; + if (setup is null) + { + _liveDropReasonSetupDatMissing++; + Console.WriteLine($"live: DROP setup dat 0x{spawn.SetupTableId.Value:X8} missing " + + $"(guid=0x{spawn.Guid:X8})"); + return; + } var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup); var meshRefs = new List(); @@ -553,7 +576,13 @@ public sealed class GameWindow : IDisposable _staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes); meshRefs.Add(new AcDream.Core.World.MeshRef(mr.GfxObjId, mr.PartTransform)); } - if (meshRefs.Count == 0) return; + if (meshRefs.Count == 0) + { + _liveDropReasonNoMeshRefs++; + Console.WriteLine($"live: DROP no mesh refs from setup 0x{spawn.SetupTableId.Value:X8} " + + $"(guid=0x{spawn.Guid:X8})"); + return; + } var entity = new AcDream.Core.World.WorldEntity { @@ -578,15 +607,14 @@ public sealed class GameWindow : IDisposable _entities = extended; _liveSpawnHydrated++; - // Log the first few so we can confirm position translation is sane. - if (_liveSpawnHydrated <= 10) + // Dump a summary periodically so we can see drop breakdowns without + // waiting for a graceful shutdown. + if (_liveSpawnReceived % 20 == 0) { - Console.WriteLine($"live: spawned guid=0x{spawn.Guid:X8} setup=0x{spawn.SetupTableId:X8} " + - $"world=({worldPos.X:F1},{worldPos.Y:F1},{worldPos.Z:F1})"); - } - if (_liveSpawnHydrated == 10) - { - Console.WriteLine("live: (suppressing further spawn logs)"); + Console.WriteLine( + $"live: summary recv={_liveSpawnReceived} hydrated={_liveSpawnHydrated} " + + $"drops: noPos={_liveDropReasonNoPos} noSetup={_liveDropReasonNoSetup} " + + $"setupMissing={_liveDropReasonSetupDatMissing} noMesh={_liveDropReasonNoMeshRefs}"); } }