acdream/tools/ProjectileVfxAudit/Program.cs
Erik d53fe30ffe research(vfx): pin retail projectile and effect oracle
Establish the executable-backed PhysicsDesc, sequence-gate, PhysicsScript, CreateBlocking, particle-anchor, projectile, and Hidden-state behavior before changing runtime code. Correct stale blocking/threshold claims and synchronize the project instructions with the current UI architecture and matching retail binary.

Add copyright-safe packet and DAT-container fixtures plus a failing installed-DAT conformance audit for projectile shapes, typed tables, recall motion, default scripts, and raw CreateBlocking inventory.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-13 22:40:42 +02:00

399 lines
17 KiB
C#

using System.Collections;
using System.Buffers.Binary;
using System.Numerics;
using System.Reflection;
using System.Security.Cryptography;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Lib.IO;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
using DatAnimation = DatReaderWriter.DBObjs.Animation;
using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript;
string datDir = System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? @"C:\Turbine\Asheron's Call";
using var dats = new DatCollection(datDir, DatAccessType.Read);
Console.WriteLine($"DAT metadata audit: {datDir}");
var failures = new List<string>();
var representativePes = new HashSet<uint>();
var expectedSetups = new Dictionary<uint, (Vector3 Origin, float Radius, int Length, string Sha256)>
{
[0x02000124u] = (Vector3.Zero, 0.1f, 812, "630D19A9C79137F7BA90D01448B03E8F44D655FF77A4725792CB5660930A079E"),
[0x0200012Au] = (Vector3.Zero, 0.1f, 776, "ED529E834CC1799B5655549200F7D1060FB62A707988E7B7D4FBED2D9C4BA0D1"),
[0x020003F6u] = (Vector3.Zero, 0.1f, 212, "E211E9C0736797D7003CEC029E8978AE7E5BE0AFBEB76811ECB15DC2A637C0A1"),
[0x0200040Du] = (Vector3.Zero, 0.1f, 212, "671BEAA3BEFB1B2A91C102CCD722F3A5843FF39BB83800C5EDE4460DFE062572"),
[0x0200087Du] = (new Vector3(0, -0.165f, 0.1045f), 0.102f, 344, "B30469764673F55BCBB4F123559256299C34D819974EAB42AC476E3FEED6EAE8"),
[0x020003F4u] = (Vector3.Zero, 0.1f, 212, "9BE00CC9844BF8912CE90AD34EA3214095FE4B29C9425AFEE4EFE70302B50FCD"),
[0x02000880u] = (Vector3.Zero, 0.15f, 212, "E25C47ED5EF850128D9114982CED5DA27ABA8343AEEE7D491B499AD69A04ABAC"),
[0x020003FAu] = (Vector3.Zero, 0.1f, 320, "BFC0C65BE287708DAFE1BF0645737FC1B49A32502987768573F8314FA8B8F905"),
[0x020003FCu] = (Vector3.Zero, 0.1f, 320, "387312DD1B71FC9B4631DA065BCBDC4D51C790A106DDA5631FF0F13C52158E3B"),
[0x02000EAEu] = (Vector3.Zero, 0.1f, 320, "B16B316E6F142A5B37D8B07D2DE1C42CBD12C7D74E23E5D916B5323A99854104"),
};
var expectedPhysicsScripts = new Dictionary<uint, (int Length, string Sha256)>
{
[0x33000D77u] = (108, "9F27628BDEA6B05C95CFADD037746AB380542DF8B040265EF881B3366CB8E12E"),
};
var expectedRecallAnimations = new Dictionary<uint, (int Length, string Sha256)>
{
[0x03000047u] = (28224, "FC01ADE01EAAFE4B276685D4927FDCEE53492F1AF30E84A6DD497E8A74343995"),
[0x0300098Fu] = (104680, "77D1A958941B7A31EA885F790986C59F8148E7CAC7C5C8DDF16AE461C39E5BAD"),
[0x030009BFu] = (143560, "21EF99FE5A760216AF48946B9C41D0FA7E4B94297B8DEC8B91C41BB8C7A0D02D"),
[0x03000C34u] = (9576, "FBE05A10C00005BB44ED890A31519E6186F5C5E800780769EFE481BE74BB507B"),
};
uint[] setupIds =
[
0x02000124u, // Arrow
0x0200012Au, // Quarrel / crossbow bolt
0x020003F6u, // Acid Stream
0x0200040Du, // Flame Bolt
0x0200087Du, // Force Bolt
0x020003F4u, // Frost Bolt
0x02000880u, // Lightning Bolt
0x020003FAu, // Shockwave
0x020003FCu, // Whirling Blade
0x02000EAEu, // Tusker Fist
];
Console.WriteLine("\n=== projectile Setup collision metadata ===");
foreach (uint id in setupIds)
{
Setup? setup = SafeGet<Setup>(id);
if (setup is null)
{
Console.WriteLine($"0x{id:X8}: missing");
failures.Add($"required Setup 0x{id:X8} is missing");
continue;
}
Console.WriteLine($"0x{id:X8}: spheres={setup.Spheres.Count}, cylSpheres={setup.CylSpheres.Count}, "
+ $"radius={setup.Radius:R}, height={setup.Height:R}, defaultPES=0x{setup.DefaultScript.DataId:X8}, "
+ $"defaultPETable=0x{(uint)setup.DefaultScriptTable:X8}");
for (int i = 0; i < setup.Spheres.Count; i++)
{
var s = setup.Spheres[i];
Console.WriteLine($" sphere[{i}] origin=({s.Origin.X:R},{s.Origin.Y:R},{s.Origin.Z:R}) radius={s.Radius:R}");
}
for (int i = 0; i < setup.CylSpheres.Count; i++)
{
var s = setup.CylSpheres[i];
Console.WriteLine($" cylinder[{i}] origin=({s.Origin.X:R},{s.Origin.Y:R},{s.Origin.Z:R}) radius={s.Radius:R} height={s.Height:R}");
}
var expected = expectedSetups[id];
if (setup.Spheres.Count != 1 || setup.CylSpheres.Count != 0)
failures.Add($"Setup 0x{id:X8} expected one ordinary sphere and no cylinder spheres");
else if (!NearlyEqualVector(setup.Spheres[0].Origin, expected.Origin) || !NearlyEqualFloat(setup.Spheres[0].Radius, expected.Radius))
failures.Add($"Setup 0x{id:X8} sphere changed: got {setup.Spheres[0].Origin}/{setup.Spheres[0].Radius:R}");
if ((uint)setup.DefaultScript.DataId != 0)
representativePes.Add((uint)setup.DefaultScript.DataId);
PrintRawMetadata(id, expected.Length, expected.Sha256);
}
uint[] tableIds =
[
0x3400002Bu, // arrow/quarrel template
0x34000005u, // flame bolt
0x34000080u, // frost bolt
0x34000081u, // lightning bolt
0x34000082u, // acid stream
];
Console.WriteLine("\n=== representative PhysicsScriptTables (stored order) ===");
var expectedTables = new Dictionary<uint, (int Length, string Sha256)>
{
[0x3400002Bu] = (504, "C97123E43657C6C04812FB9B04C893DB057069695748DCA5F7F71F2575E30F05"),
[0x34000005u] = (88, "86906B0EDDC9AA2242091EAC7A22C88FB2150F0773B6CCA1FA5E4AAEAFF2A386"),
[0x34000080u] = (56, "BF84C0A7C19821D4B9282D0A37F46B9C87E4B59FC9AD29263F7F6DDF5C0F65DA"),
[0x34000081u] = (56, "D65B0B64E1EEA997C7B6AF3DA99A050D32F10738A1393207FB933AC7AE112DFA"),
[0x34000082u] = (56, "BDF28D167425C3835CD9FBE8556D414D3E58A477A7F840D6DC89072676A39A56"),
};
foreach (uint id in tableIds)
{
PhysicsScriptTable? table = SafeGet<PhysicsScriptTable>(id);
Console.WriteLine($"TABLE 0x{id:X8}");
if (table is null)
{
Console.WriteLine(" missing");
failures.Add($"required PhysicsScriptTable 0x{id:X8} is missing");
}
else
{
DumpObject(table, " ", depth: 0, maxDepth: 5);
foreach (var data in table.ScriptTable.Values)
foreach (var entry in data.Scripts)
representativePes.Add((uint)entry.ScriptId);
}
var expected = expectedTables[id];
PrintRawMetadata(id, expected.Length, expected.Sha256);
}
Console.WriteLine("\n=== representative projectile PhysicsScript hooks ===");
var representativeVisited = new HashSet<uint>();
foreach (uint pes in representativePes.Order())
DumpPhysicsScript(pes, representativeVisited, "");
Console.WriteLine("\n=== installed Hidden / UnHide table entries ===");
foreach (uint id in dats.GetAllIdsOfType<PhysicsScriptTable>())
{
PhysicsScriptTable? table = SafeGet<PhysicsScriptTable>(id);
if (table is null) continue;
foreach (var pair in table.ScriptTable)
{
string type = pair.Key.ToString();
if (type is not "Hidden" and not "UnHide") continue;
Console.WriteLine($"TABLE 0x{id:X8} {type}");
DumpObject(pair.Value, " ", depth: 0, maxDepth: 3);
}
}
const int RecallCommand = unchecked((int)0x10000153u);
var recallAnimationIds = new HashSet<uint>();
Console.WriteLine("\n=== local-DAT LifestoneRecall 0x10000153 motion chains ===");
foreach (uint id in dats.GetAllIdsOfType<MotionTable>())
{
MotionTable? table = SafeGet<MotionTable>(id);
if (table is null) continue;
foreach (var outer in table.Links)
{
if (!outer.Value.MotionData.TryGetValue(RecallCommand, out var motionData)) continue;
Console.WriteLine($"MotionTable 0x{id:X8}, link=0x{unchecked((uint)outer.Key):X8}");
CollectAnimationIds(motionData, recallAnimationIds, " ");
}
if (table.Modifiers.TryGetValue(RecallCommand, out var modifier))
{
Console.WriteLine($"MotionTable 0x{id:X8}, modifier");
CollectAnimationIds(modifier, recallAnimationIds, " ");
}
}
Console.WriteLine("\n=== recall Animation hooks and reachable PES roots ===");
var reachablePes = new HashSet<uint>();
foreach (uint expectedId in expectedRecallAnimations.Keys)
if (!recallAnimationIds.Contains(expectedId))
failures.Add($"expected recall Animation 0x{expectedId:X8} was not resolved from motion 0x10000153");
foreach (uint animationId in recallAnimationIds.Order())
{
DumpAnimation(animationId, reachablePes);
if (expectedRecallAnimations.TryGetValue(animationId, out var expected))
PrintRawMetadata(animationId, expected.Length, expected.Sha256);
else
failures.Add($"unmanifested recall Animation 0x{animationId:X8}");
}
Console.WriteLine("\n=== reachable recall PhysicsScripts ===");
var visitedPes = new HashSet<uint>();
foreach (uint pes in reachablePes.Order())
DumpPhysicsScript(pes, visitedPes, "");
Console.WriteLine("\n=== installed CreateBlockingParticle occurrences (raw retail-layout scan) ===");
var blockingPes = FindRawBlockingHooks(dats.GetAllIdsOfType<DatPhysicsScript>());
Console.WriteLine($"PhysicsScripts: {string.Join(", ", blockingPes.Select(x => $"0x{x:X8}"))}");
foreach (uint id in blockingPes) PrintRawMetadata(id);
var blockingAnimations = FindRawBlockingHooks(dats.GetAllIdsOfType<DatAnimation>());
Console.WriteLine($"Animations: {string.Join(", ", blockingAnimations.Select(x => $"0x{x:X8}"))}");
uint[] expectedBlockingPes = Enumerable.Range(0xAEA, 0xF).Select(x => 0x33000000u | (uint)x).ToArray();
if (!blockingPes.SequenceEqual(expectedBlockingPes))
failures.Add($"raw blocking-PES inventory changed: got {string.Join(",", blockingPes.Select(x => $"0x{x:X8}"))}");
if (blockingAnimations.Count != 0)
failures.Add($"expected no raw blocking Animation hooks, got {string.Join(",", blockingAnimations.Select(x => $"0x{x:X8}"))}");
Console.WriteLine();
if (failures.Count == 0)
{
Console.WriteLine("AUDIT PASS: required assets, hashes, shapes, and raw blocking-hook inventory match.");
}
else
{
Console.Error.WriteLine("AUDIT FAILED:");
foreach (string failure in failures.Distinct()) Console.Error.WriteLine($" - {failure}");
System.Environment.ExitCode = 1;
}
return;
T? SafeGet<T>(uint id) where T : class, IDBObj
{
try { return dats.Get<T>(id); }
catch (Exception ex)
{
Console.WriteLine($" parse failure {typeof(T).Name} 0x{id:X8}: {ex.GetType().Name}: {ex.Message}");
failures.Add($"failed to parse {typeof(T).Name} 0x{id:X8}: {ex.GetType().Name}");
return null;
}
}
void PrintRawMetadata(uint id, int? expectedLength = null, string? expectedSha256 = null)
{
try
{
if (!dats.Portal.TryGetFileBytes(id, out byte[]? bytes, true) || bytes is null)
{
Console.WriteLine($" raw 0x{id:X8}: unavailable");
failures.Add($"raw entry 0x{id:X8} is unavailable");
return;
}
string sha256 = Convert.ToHexString(SHA256.HashData(bytes));
Console.WriteLine($" raw 0x{id:X8}: length={bytes.Length}, sha256={sha256}");
if (expectedLength.HasValue && bytes.Length != expectedLength.Value)
failures.Add($"raw entry 0x{id:X8} length changed: expected {expectedLength}, got {bytes.Length}");
if (expectedSha256 is not null && !sha256.Equals(expectedSha256, StringComparison.OrdinalIgnoreCase))
failures.Add($"raw entry 0x{id:X8} hash changed: expected {expectedSha256}, got {sha256}");
}
catch (Exception ex)
{
Console.WriteLine($" raw 0x{id:X8}: {ex.GetType().Name}: {ex.Message}");
failures.Add($"failed to read raw entry 0x{id:X8}: {ex.GetType().Name}");
}
}
List<uint> FindRawBlockingHooks(IEnumerable<uint> ids)
{
var matches = new List<uint>();
foreach (uint id in ids.Order())
{
if (!dats.Portal.TryGetFileBytes(id, out byte[]? bytes, true) || bytes is null)
{
failures.Add($"raw hook scan could not read 0x{id:X8}");
continue;
}
// Retail CreateBlockingParticle is a 48-byte CreateParticle-shaped
// hook: type, direction, 0x32xxxxxx emitter DID, part, Frame, logical
// ID. Scan aligned raw entries so the defective package decoder cannot
// skip or misalign a following hook. The DID prefix plus a valid retail
// direction makes accidental payload matches deterministically rejectable.
for (int offset = 4; offset <= bytes.Length - 48; offset += 4)
{
if (BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(offset)) != 0x1Au)
continue;
uint direction = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(offset + 4));
if (direction is not (0u or 1u or 0xFFFFFFFEu or 0xFFFFFFFFu))
continue;
uint emitterDid = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(offset + 8));
if ((emitterDid & 0xFF000000u) != 0x32000000u)
continue;
matches.Add(id);
break;
}
}
return matches;
}
static bool NearlyEqualVector(Vector3 left, Vector3 right)
=> Vector3.DistanceSquared(left, right) <= 1e-10f;
static bool NearlyEqualFloat(float left, float right)
=> MathF.Abs(left - right) <= 1e-6f;
static void CollectAnimationIds(MotionData motion, HashSet<uint> ids, string indent)
{
foreach (AnimData anim in motion.Anims)
{
uint id = (uint)anim.AnimId;
ids.Add(id);
Console.WriteLine($"{indent}Animation 0x{id:X8}, frames={anim.LowFrame}..{anim.HighFrame}, rate={anim.Framerate:R}");
}
}
void DumpAnimation(uint id, HashSet<uint> pesRoots)
{
DatAnimation? animation = SafeGet<DatAnimation>(id);
if (animation is null) return;
Console.WriteLine($"Animation 0x{id:X8}: frames={animation.PartFrames.Count}");
for (int frameIndex = 0; frameIndex < animation.PartFrames.Count; frameIndex++)
{
foreach (AnimationHook hook in animation.PartFrames[frameIndex].Hooks)
{
Console.WriteLine($" frame[{frameIndex}] {DescribeHook(hook)}");
if (hook is CallPESHook call) pesRoots.Add(call.PES);
}
}
}
void DumpPhysicsScript(uint id, HashSet<uint> visited, string indent)
{
if (!visited.Add(id)) return;
DatPhysicsScript? script = SafeGet<DatPhysicsScript>(id);
if (script is null) return;
Console.WriteLine($"{indent}PES 0x{id:X8}: hooks={script.ScriptData.Count}");
if (expectedPhysicsScripts.TryGetValue(id, out var expected))
PrintRawMetadata(id, expected.Length, expected.Sha256);
else
PrintRawMetadata(id);
foreach (PhysicsScriptData entry in script.ScriptData)
{
Console.WriteLine($"{indent} t={entry.StartTime:R} {DescribeHook(entry.Hook)}");
if (entry.Hook is CallPESHook call)
DumpPhysicsScript(call.PES, visited, indent + " ");
}
}
static string DescribeHook(AnimationHook hook) => hook switch
{
CallPESHook call => $"CallPES PES=0x{call.PES:X8} pause={call.Pause:R}",
CreateParticleHook create => $"CreateParticle emitter=0x{(uint)create.EmitterInfoId:X8} part={create.PartIndex} logical={create.EmitterId} "
+ $"offset=({create.Offset.Origin.X:R},{create.Offset.Origin.Y:R},{create.Offset.Origin.Z:R}) "
+ $"quat=({create.Offset.Orientation.X:R},{create.Offset.Orientation.Y:R},{create.Offset.Orientation.Z:R},{create.Offset.Orientation.W:R})",
CreateBlockingParticleHook => "CreateBlockingParticle (package omits inherited payload)",
_ => hook.HookType.ToString(),
};
static void DumpObject(object? value, string indent, int depth, int maxDepth)
{
if (value is null) { Console.WriteLine(indent + "null"); return; }
if (depth >= maxDepth || value is string || value.GetType().IsPrimitive || value is decimal || value.GetType().IsEnum)
{
Console.WriteLine(indent + FormatScalar(value));
return;
}
if (value is IDictionary dictionary)
{
foreach (DictionaryEntry item in dictionary)
{
Console.WriteLine($"{indent}[{FormatScalar(item.Key)}]");
DumpObject(item.Value, indent + " ", depth + 1, maxDepth);
}
return;
}
if (value is IEnumerable sequence)
{
int index = 0;
foreach (object? item in sequence)
{
Console.WriteLine($"{indent}[{index++}]");
DumpObject(item, indent + " ", depth + 1, maxDepth);
}
return;
}
Type type = value.GetType();
Console.WriteLine($"{indent}{type.Name}");
foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
{
Console.Write($"{indent} {field.Name} = ");
DumpObject(field.GetValue(value), indent + " ", depth + 1, maxDepth);
}
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.GetIndexParameters().Length == 0 && x.Name is not "DBObjType" and not "HeaderFlags"))
{
Console.Write($"{indent} {property.Name} = ");
DumpObject(property.GetValue(value), indent + " ", depth + 1, maxDepth);
}
}
static string FormatScalar(object? value) => value switch
{
null => "null",
uint v => $"0x{v:X8}",
int v => $"0x{unchecked((uint)v):X8}",
_ => value.ToString() ?? "",
};