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>
399 lines
17 KiB
C#
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() ?? "",
|
|
};
|