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(); var representativePes = new HashSet(); var expectedSetups = new Dictionary { [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 { [0x33000D77u] = (108, "9F27628BDEA6B05C95CFADD037746AB380542DF8B040265EF881B3366CB8E12E"), }; var expectedRecallAnimations = new Dictionary { [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(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 { [0x3400002Bu] = (504, "C97123E43657C6C04812FB9B04C893DB057069695748DCA5F7F71F2575E30F05"), [0x34000005u] = (88, "86906B0EDDC9AA2242091EAC7A22C88FB2150F0773B6CCA1FA5E4AAEAFF2A386"), [0x34000080u] = (56, "BF84C0A7C19821D4B9282D0A37F46B9C87E4B59FC9AD29263F7F6DDF5C0F65DA"), [0x34000081u] = (56, "D65B0B64E1EEA997C7B6AF3DA99A050D32F10738A1393207FB933AC7AE112DFA"), [0x34000082u] = (56, "BDF28D167425C3835CD9FBE8556D414D3E58A477A7F840D6DC89072676A39A56"), }; foreach (uint id in tableIds) { PhysicsScriptTable? table = SafeGet(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(); foreach (uint pes in representativePes.Order()) DumpPhysicsScript(pes, representativeVisited, ""); Console.WriteLine("\n=== installed Hidden / UnHide table entries ==="); foreach (uint id in dats.GetAllIdsOfType()) { PhysicsScriptTable? table = SafeGet(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(); Console.WriteLine("\n=== local-DAT LifestoneRecall 0x10000153 motion chains ==="); foreach (uint id in dats.GetAllIdsOfType()) { MotionTable? table = SafeGet(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(); 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(); foreach (uint pes in reachablePes.Order()) DumpPhysicsScript(pes, visitedPes, ""); Console.WriteLine("\n=== installed CreateBlockingParticle occurrences (raw retail-layout scan) ==="); var blockingPes = FindRawBlockingHooks(dats.GetAllIdsOfType()); Console.WriteLine($"PhysicsScripts: {string.Join(", ", blockingPes.Select(x => $"0x{x:X8}"))}"); foreach (uint id in blockingPes) PrintRawMetadata(id); var blockingAnimations = FindRawBlockingHooks(dats.GetAllIdsOfType()); 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(uint id) where T : class, IDBObj { try { return dats.Get(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 FindRawBlockingHooks(IEnumerable ids) { var matches = new List(); 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 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 pesRoots) { DatAnimation? animation = SafeGet(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 visited, string indent) { if (!visited.Add(id)) return; DatPhysicsScript? script = SafeGet(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() ?? "", };