249 lines
10 KiB
C#
249 lines
10 KiB
C#
// 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 <setupId hex, e.g. 0x02000123> [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<Setup>(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<GfxObj>(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<Animation>(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<MotionTable>(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;
|