From fc6c9dc240c4da945e3d22cf51a6b6c5cf7536c1 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 11 Apr 2026 15:38:26 +0200 Subject: [PATCH] diag(app): full CreateObject spawn logging + drop reason breakdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4.7g visual verification turned up several issues and I needed better visibility into what's actually streaming from the server. Removes the "suppress after 10" limit on the spawn log and adds drop-reason counters so I can distinguish "inventory item with no position" (expected, ~16 per login) from "setup dat missing" and "zero mesh refs" (unexpected failures). Findings from a full live run with the new logging: live: summary recv=60 hydrated=44 drops: noPos=16 noSetup=0 setupMissing=0 noMesh=0 Meaning: every positioned CreateObject is hydrating cleanly into IGameState. The only drops are the 16 inventory/equipped items that the server sends without a position (they inherit from their wearer). The foundry statue is NOT being silently dropped at the codec layer — it's in our render list somewhere, probably indistinguishable from generic naked humanoids because we don't decode ObjectDesc yet. User observations from the visual verification: * NPCs + +Acdream visible, but naked (no clothing/armor) * Doors now exist (Phase 4 win over offline-only) * Portals render as black squares * Foundry statue not identifiable (most likely a generic-looking spawn due to missing ObjectDesc) * Holtburg town crier sign half underground (small Z offset) All of the "wrong appearance" findings trace back to the same root cause: CreateObject.TryParse skips past ModelData without extracting the palette swaps, texture changes, and animpart changes that define each entity's unique visual presentation. Base setup mesh renders as-is. Phase 5 work. Next step in this session: port the ModelData parser (primarily the AnimPartChanges list — replacing body parts with armored/statue versions is the single biggest visual improvement for a character model). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 48 +++++++++++++++++++------ 1 file changed, 38 insertions(+), 10 deletions(-) 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}"); } }