// SetupInspect — dump a Setup's part/pose truth for placement bugs // (built for the Bind stone floating-crystal investigation, task #32). // // dotnet run --project tools\SetupInspect -- 0x02000xyz // // Prints: parts + gfx ids, parent indices, default scales, every placement // frame's per-part origin/orientation, DefaultAnimation id, and — when set — // the animation's frame count plus per-part origins for the first, middle, // and last frames. Comparing placement-frame origins against animation-frame // origins tells you which pose the client is actually showing. using System; using System.Globalization; using System.IO; using System.Linq; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; using DatReaderWriter.Types; using SysEnv = System.Environment; if (args.Length < 1) { Console.Error.WriteLine("usage: SetupInspect [animId hex override]"); return 2; } static uint ParseHex(string s) => uint.Parse(s.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? s[2..] : s, NumberStyles.HexNumber, CultureInfo.InvariantCulture); uint setupId = ParseHex(args[0]); string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); Console.WriteLine($"datDir = {datDir}"); using var dats = new DatCollection(datDir, DatAccessType.Read); if (!dats.TryGet(setupId, out var setup) || setup is null) { Console.Error.WriteLine($"ERROR: Setup 0x{setupId:X8} not found"); return 1; } Console.WriteLine($"=== Setup 0x{setupId:X8} ==="); Console.WriteLine($"Flags = 0x{(uint)setup.Flags:X8}"); Console.WriteLine($"Radius/Height = {setup.Radius:F3} / {setup.Height:F3}"); Console.WriteLine($"StepUp/StepDown = {setup.StepUpHeight:F3} / {setup.StepDownHeight:F3}"); Console.WriteLine($"Spheres = {setup.Spheres.Count}"); for (int i = 0; i < setup.Spheres.Count; i++) { Sphere sphere = setup.Spheres[i]; Console.WriteLine( $" sphere[{i}] origin=({sphere.Origin.X:R},{sphere.Origin.Y:R},{sphere.Origin.Z:R}) " + $"radius={sphere.Radius:R} " + $"radiusBits=0x{BitConverter.SingleToUInt32Bits(sphere.Radius):X8}"); } Console.WriteLine($"CylSpheres = {setup.CylSpheres.Count}"); for (int i = 0; i < setup.CylSpheres.Count; i++) { CylSphere cylinder = setup.CylSpheres[i]; Console.WriteLine( $" cyl[{i}] origin=({cylinder.Origin.X:R},{cylinder.Origin.Y:R},{cylinder.Origin.Z:R}) " + $"radius={cylinder.Radius:R} height={cylinder.Height:R} " + $"radiusBits=0x{BitConverter.SingleToUInt32Bits(cylinder.Radius):X8}"); } Console.WriteLine($"Parts = {setup.Parts.Count}"); for (int i = 0; i < setup.Parts.Count; i++) { string parent = i < setup.ParentIndex.Count ? setup.ParentIndex[i].ToString(CultureInfo.InvariantCulture) : "-"; string scale = i < setup.DefaultScale.Count ? $"({setup.DefaultScale[i].X:F3},{setup.DefaultScale[i].Y:F3},{setup.DefaultScale[i].Z:F3})" : "-"; Console.WriteLine($" part[{i}] gfx=0x{(uint)setup.Parts[i]:X8} parent={parent} scale={scale}"); } Console.WriteLine("Part GfxObj vertex bounds (object-local before frames):"); foreach (uint gfxId in setup.Parts.Select(p => (uint)p).Distinct()) { if (!dats.TryGet(gfxId, out var gfx) || gfx is null) { Console.WriteLine($" gfx=0x{gfxId:X8} NOT FOUND"); continue; } float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue; float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue; int count = 0; foreach (var vert in gfx.VertexArray.Vertices.Values) { minX = Math.Min(minX, vert.Origin.X); maxX = Math.Max(maxX, vert.Origin.X); minY = Math.Min(minY, vert.Origin.Y); maxY = Math.Max(maxY, vert.Origin.Y); minZ = Math.Min(minZ, vert.Origin.Z); maxZ = Math.Max(maxZ, vert.Origin.Z); count++; } Console.WriteLine( $" gfx=0x{gfxId:X8} verts={count} " + $"x[{minX:F2},{maxX:F2}] y[{minY:F2},{maxY:F2}] z[{minZ:F2},{maxZ:F2}] " + $"sortCenter=({gfx.SortCenter.X:F2},{gfx.SortCenter.Y:F2},{gfx.SortCenter.Z:F2}) " + $"flags=0x{(uint)gfx.Flags:X8} " + $"physicsBsp={(gfx.PhysicsBSP?.Root is null ? "none" : "present")} " + $"physicsPolygons={gfx.PhysicsPolygons.Count}"); } Console.WriteLine($"DefaultAnimation = 0x{(uint)setup.DefaultAnimation:X8}"); Console.WriteLine($"DefaultScript = 0x{(uint)setup.DefaultScript:X8}"); Console.WriteLine($"DefaultMotionTable = 0x{(uint)setup.DefaultMotionTable:X8}"); static void DumpHooks(AnimationFrame frame, string indent) { if (frame.Hooks.Count == 0) return; foreach (AnimationHook hook in frame.Hooks) { string detail = hook switch { TransparentPartHook tp => $"part={tp.PartIndex} start={tp.Start:F3} end={tp.End:F3} time={tp.Time:F3}", TransparentHook t => $"start={t.Start:F3} end={t.End:F3} time={t.Time:F3}", ScaleHook s => $"end={s.End:F3} time={s.Time:F3}", _ => "", }; Console.WriteLine($"{indent}HOOK {hook.GetType().Name} type={hook.HookType} dir={hook.Direction} {detail}"); } } Console.WriteLine($"PlacementFrames = {setup.PlacementFrames.Count}"); foreach (var kvp in setup.PlacementFrames) { Console.WriteLine($" [placement {kvp.Key} (0x{(uint)kvp.Key:X})] frames={kvp.Value.Frames.Count} hooks={kvp.Value.Hooks.Count}"); for (int i = 0; i < kvp.Value.Frames.Count; i++) { Frame f = kvp.Value.Frames[i]; Console.WriteLine( $" part[{i}] origin=({f.Origin.X:F3},{f.Origin.Y:F3},{f.Origin.Z:F3}) " + $"quat=({f.Orientation.W:F3},{f.Orientation.X:F3},{f.Orientation.Y:F3},{f.Orientation.Z:F3})"); } DumpHooks(kvp.Value, " "); } void DumpAnimation(uint id, string label) { if (!dats.TryGet(id, out var anim) || anim is null) { Console.Error.WriteLine($"ERROR: Animation 0x{id:X8} not found"); return; } Console.WriteLine(); Console.WriteLine($"=== Animation 0x{id:X8} ({label}) ==="); Console.WriteLine($"NumParts = {anim.NumParts}"); Console.WriteLine($"PartFrames = {anim.PartFrames.Count}"); if (SysEnv.GetEnvironmentVariable("SETUPINSPECT_TRACK_PART") is { Length: > 0 } trackRaw && int.TryParse(trackRaw, out int trackPart)) { Console.WriteLine($"-- part[{trackPart}] across ALL frames --"); for (int frameIdx = 0; frameIdx < anim.PartFrames.Count; frameIdx++) { var frames = anim.PartFrames[frameIdx].Frames; if (trackPart >= frames.Count) continue; Frame f = frames[trackPart]; Console.WriteLine( $" f[{frameIdx:D2}] origin=({f.Origin.X:F3},{f.Origin.Y:F3},{f.Origin.Z:F3}) " + $"quat=({f.Orientation.W:F3},{f.Orientation.X:F3},{f.Orientation.Y:F3},{f.Orientation.Z:F3})"); } } int[] sample = new[] { 0, anim.PartFrames.Count / 2, anim.PartFrames.Count - 1 } .Distinct().Where(i => i >= 0 && i < anim.PartFrames.Count).ToArray(); foreach (int frameIdx in sample) { AnimationFrame frame = anim.PartFrames[frameIdx]; Console.WriteLine($" frame[{frameIdx}] parts={frame.Frames.Count} hooks={frame.Hooks.Count}"); for (int i = 0; i < frame.Frames.Count; i++) { Frame f = frame.Frames[i]; Console.WriteLine( $" part[{i}] origin=({f.Origin.X:F3},{f.Origin.Y:F3},{f.Origin.Z:F3}) " + $"quat=({f.Orientation.W:F3},{f.Orientation.X:F3},{f.Orientation.Y:F3},{f.Orientation.Z:F3})"); } DumpHooks(frame, " "); } Console.WriteLine("-- hooks on ALL frames --"); for (int frameIdx = 0; frameIdx < anim.PartFrames.Count; frameIdx++) { AnimationFrame frame = anim.PartFrames[frameIdx]; if (frame.Hooks.Count == 0) continue; Console.WriteLine($" frame[{frameIdx}]:"); DumpHooks(frame, " "); } } if ((uint)setup.DefaultAnimation != 0 || args.Length > 1) { uint animId = args.Length > 1 ? ParseHex(args[1]) : (uint)setup.DefaultAnimation; DumpAnimation(animId, "DefaultAnimation/override"); } // Walk the motion table the way MotionResolver.ResolveIdleCycleInternal does: // DefaultStyle -> StyleDefaults[DefaultStyle] -> Cycles[(style << 16) | substate] // -> Anims[0] -> Animation. if ((uint)setup.DefaultMotionTable != 0) { uint mtId = (uint)setup.DefaultMotionTable; if (!dats.TryGet(mtId, out var mtable) || mtable is null) { Console.Error.WriteLine($"ERROR: MotionTable 0x{mtId:X8} not found"); return 1; } Console.WriteLine(); Console.WriteLine($"=== MotionTable 0x{mtId:X8} ==="); Console.WriteLine($"DefaultStyle = 0x{(uint)mtable.DefaultStyle:X} ({mtable.DefaultStyle})"); Console.WriteLine($"StyleDefaults = {mtable.StyleDefaults.Count}"); foreach (var kvp in mtable.StyleDefaults) Console.WriteLine($" style 0x{(uint)kvp.Key:X} ({kvp.Key}) -> substate 0x{(uint)kvp.Value:X} ({kvp.Value})"); Console.WriteLine($"Cycles = {mtable.Cycles.Count}"); foreach (var kvp in mtable.Cycles) { var anims = kvp.Value.Anims; string animsDesc = string.Join(", ", anims.Select(a => $"anim=0x{(uint)a.AnimId:X8} lo={a.LowFrame} hi={a.HighFrame} fps={a.Framerate:F2}")); Console.WriteLine($" cycle key 0x{(uint)kvp.Key:X8}: [{animsDesc}]"); } if (mtable.StyleDefaults.TryGetValue(mtable.DefaultStyle, out var defSub)) { int cycleKey = (int)(((uint)mtable.DefaultStyle << 16) | ((uint)defSub & 0xFFFFFF)); Console.WriteLine($"Default cycle key = 0x{(uint)cycleKey:X8}"); if (mtable.Cycles.TryGetValue(cycleKey, out var motionData) && motionData is not null && motionData.Anims.Count > 0) { DumpAnimation((uint)motionData.Anims[0].AnimId, "default idle cycle"); } else { Console.WriteLine("Default cycle NOT FOUND in Cycles — MotionResolver returns null here " + "and the client falls back to PlacementFrames."); } } else { Console.WriteLine("DefaultStyle has no StyleDefaults entry — MotionResolver returns null here " + "and the client falls back to PlacementFrames."); } } return 0;