diag(app): full CreateObject spawn logging + drop reason breakdown

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-11 15:38:26 +02:00
parent 713bec256b
commit fc6c9dc240

View file

@ -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<DatReaderWriter.DBObjs.Setup>(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<AcDream.Core.World.MeshRef>();
@ -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}");
}
}