diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index a49f78d..09f7652 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -539,8 +539,9 @@ public sealed class GameWindow : IDisposable // is cheap and gives us exactly one entity's worth of log regardless // of arrival order. bool isStatue = spawn.Name is not null && spawn.Name.Contains("Statue", StringComparison.OrdinalIgnoreCase); - if (isStatue && (texChangeCount > 0 || subPalCount > 0 || animPartCount > 0)) + if (isStatue) { + Console.WriteLine($"live: [STATUE] objScale={spawn.ObjScale?.ToString("F3") ?? "null"}"); if (spawn.TextureChanges is { } tcs) { foreach (var tc in tcs) @@ -557,6 +558,29 @@ public sealed class GameWindow : IDisposable foreach (var apc in apcs) Console.WriteLine($"live: [STATUE] animPart index={apc.PartIndex} newModel=0x{apc.NewModelId:X8}"); } + + // Dump the BASE setup's part list before AnimPartChanges, so we can + // see how many parts the statue's Setup actually has + what their + // default GfxObjs are. The retail statue may have additional parts + // (e.g. a pedestal sub-mesh) that our setup loader is dropping or + // we're rendering with wrong default GfxObjs. + if (spawn.SetupTableId is { } sid && _dats is not null) + { + var baseSetup = _dats.Get(sid); + if (baseSetup is not null) + { + Console.WriteLine($"live: [STATUE] base Setup 0x{sid:X8} has {baseSetup.Parts.Count} parts:"); + for (int pi = 0; pi < baseSetup.Parts.Count; pi++) + { + uint partGfxId = (uint)baseSetup.Parts[pi]; + var pgfx = _dats.Get(partGfxId); + int subCount = pgfx?.Surfaces.Count ?? -1; + Console.WriteLine($"live: [STATUE] part[{pi}] gfxObj=0x{partGfxId:X8} surfaces={subCount}"); + } + // The placement frame the existing flatten logic uses. + Console.WriteLine($"live: [STATUE] placementFrames count={baseSetup.PlacementFrames.Count}"); + } + } } if (_dats is null || _staticMesh is null) return; diff --git a/src/AcDream.Core/Meshing/SetupMesh.cs b/src/AcDream.Core/Meshing/SetupMesh.cs index ac7a8f3..1be9baa 100644 --- a/src/AcDream.Core/Meshing/SetupMesh.cs +++ b/src/AcDream.Core/Meshing/SetupMesh.cs @@ -16,8 +16,22 @@ public static class SetupMesh /// public static IReadOnlyList Flatten(Setup setup) { + // ACViewer's CreateMesh always calls SetPlacementFrame(0x65) = + // Placement.Resting after creating any mesh, regardless of object + // type. For creatures and characters this matters a lot — Default + // is the aggressive battle crouch pose with arms extended forward, + // and Resting is the upright idle pose. Without this preference, + // every drudge/skeleton/character renders mid-attack, including + // the Nullified Statue of a Drudge whose only placement frame is + // keyed by Resting. + // + // Fall back to Default if Resting isn't present (most scenery + // setups only define Default). The user-visible difference is + // most dramatic for creatures; static dat scenery is unaffected. AnimationFrame? defaultAnim = null; - if (setup.PlacementFrames.TryGetValue(Placement.Default, out var af)) + if (setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting)) + defaultAnim = resting; + else if (setup.PlacementFrames.TryGetValue(Placement.Default, out var af)) defaultAnim = af; var result = new List(setup.Parts.Count);