feat(vfx): decode retail hooks and typed tables

Replace the incomplete package path with one DatCollection-backed compatibility seam for PhysicsScripts and Animations. Preserve CreateBlockingParticle's inherited payload and following cursor, route every production and audit consumer through the corrected loaders, and apply retail's post-UnPack StartTime ordering.

Add exact stored-order PhysicsScriptTable upper-threshold resolution, high-byte DID and embedded-ID validation, plus live effect profiles with Setup-to-PhysicsDesc precedence across top-level and attached entity lifetimes. Keep blocking execution deferred and narrow TS-11 accordingly.

Pin synthetic malformed/cursor/order fixtures, installed-DAT blocking and recall audits, high-index IDs, IEEE boundaries, profile teardown, and ordinary decoder parity; synchronize architecture, inventory, milestones, roadmap, research, and memory.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-14 08:17:44 +02:00
parent 8dd996053d
commit 363e046112
31 changed files with 1102 additions and 73 deletions

View file

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="C:\Users\erikn\source\repos\acdream\references\DatReaderWriter\DatReaderWriter\DatReaderWriter.csproj" />
<ProjectReference Include="..\..\src\AcDream.Content\AcDream.Content.csproj" />
</ItemGroup>
</Project>

View file

@ -3,18 +3,21 @@
using System;
using System.Collections.Generic;
using System.IO;
using AcDream.Content.Vfx;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
using SysEnv = System.Environment;
using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript;
using RetailBlockingHook = AcDream.Core.Vfx.RetailCreateBlockingParticleHook;
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);
var physicsScripts = new RetailPhysicsScriptLoader(dats);
uint[] roots = { 0x33000453u, 0x33000428u, 0x3300042Cu, 0x33000429u, 0x3300042Du };
@ -35,7 +38,8 @@ foreach (var root in roots)
Console.WriteLine($"{pad}PES 0x{pesId:X8} (already visited — cycle skip)");
return;
}
if (!dats.TryGet<DatPhysicsScript>(pesId, out var ps) || ps is null)
DatPhysicsScript? ps = physicsScripts.LoadPhysicsScript(pesId);
if (ps is null)
{
Console.WriteLine($"{pad}PES 0x{pesId:X8} NOT FOUND");
return;
@ -52,13 +56,18 @@ foreach (var root in roots)
Console.WriteLine($"{head} -> PES=0x{call.PES:X8} pause={call.Pause:F3}");
WalkPes(call.PES, depth + 2, rootForReport);
break;
case RetailBlockingHook blocking:
uint blockingEmitterId = (uint)blocking.EmitterInfoId;
Console.WriteLine($"{head} EmitterInfoId=0x{blockingEmitterId:X8} PartIdx={blocking.PartIndex} EmitterId={blocking.EmitterId} (blocking)");
DumpEmitter(blockingEmitterId, pad + " ", rootForReport);
break;
case CreateParticleHook cp:
uint eid = (uint)cp.EmitterInfoId;
Console.WriteLine($"{head} EmitterInfoId=0x{eid:X8} PartIdx={cp.PartIndex} EmitterId={cp.EmitterId}");
DumpEmitter(eid, pad + " ", rootForReport);
break;
case CreateBlockingParticleHook _:
Console.WriteLine($"{head} (package schema omits retail's inherited CreateParticle payload)");
Console.WriteLine($"{head} (unexpected package header-only shape)");
break;
case SoundHook sh:
Console.WriteLine($"{head} sound=0x{(uint)sh.Id:X8}");

View file

@ -3,6 +3,8 @@ using System.Buffers.Binary;
using System.Numerics;
using System.Reflection;
using System.Security.Cryptography;
using AcDream.Content.Vfx;
using AcDream.Core.Vfx;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Lib.IO;
@ -15,6 +17,8 @@ string datDir = System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? @"C:\Turbine\Asheron's Call";
using var dats = new DatCollection(datDir, DatAccessType.Read);
var physicsScripts = new RetailPhysicsScriptLoader(dats);
var animations = new RetailAnimationLoader(dats);
Console.WriteLine($"DAT metadata audit: {datDir}");
var failures = new List<string>();
var representativePes = new HashSet<uint>();
@ -203,6 +207,17 @@ 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}"))}");
foreach (uint id in expectedBlockingPes)
{
DatPhysicsScript? script = physicsScripts.LoadPhysicsScript(id);
if (script is null)
{
failures.Add($"retail loader could not parse blocking PES 0x{id:X8}");
continue;
}
if (!script.ScriptData.Any(entry => entry.Hook is RetailCreateBlockingParticleHook))
failures.Add($"blocking PES 0x{id:X8} did not retain the retail payload shape");
}
Console.WriteLine();
if (failures.Count == 0)
@ -304,7 +319,7 @@ static void CollectAnimationIds(MotionData motion, HashSet<uint> ids, string ind
void DumpAnimation(uint id, HashSet<uint> pesRoots)
{
DatAnimation? animation = SafeGet<DatAnimation>(id);
DatAnimation? animation = animations.LoadAnimation(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++)
@ -320,7 +335,7 @@ void DumpAnimation(uint id, HashSet<uint> pesRoots)
void DumpPhysicsScript(uint id, HashSet<uint> visited, string indent)
{
if (!visited.Add(id)) return;
DatPhysicsScript? script = SafeGet<DatPhysicsScript>(id);
DatPhysicsScript? script = physicsScripts.LoadPhysicsScript(id);
if (script is null) return;
Console.WriteLine($"{indent}PES 0x{id:X8}: hooks={script.ScriptData.Count}");
if (expectedPhysicsScripts.TryGetValue(id, out var expected))
@ -338,10 +353,13 @@ void DumpPhysicsScript(uint id, HashSet<uint> visited, string indent)
static string DescribeHook(AnimationHook hook) => hook switch
{
CallPESHook call => $"CallPES PES=0x{call.PES:X8} pause={call.Pause:R}",
RetailCreateBlockingParticleHook blocking => $"CreateBlockingParticle emitter=0x{(uint)blocking.EmitterInfoId:X8} part={blocking.PartIndex} logical={blocking.EmitterId} "
+ $"offset=({blocking.Offset.Origin.X:R},{blocking.Offset.Origin.Y:R},{blocking.Offset.Origin.Z:R}) "
+ $"quat=({blocking.Offset.Orientation.X:R},{blocking.Offset.Orientation.Y:R},{blocking.Offset.Orientation.Z:R},{blocking.Offset.Orientation.W: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)",
CreateBlockingParticleHook => "CreateBlockingParticle (unexpected package header-only shape)",
_ => hook.HookType.ToString(),
};

View file

@ -6,6 +6,6 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chorizite.DatReaderWriter" Version="2.1.7" />
<ProjectReference Include="..\..\src\AcDream.Content\AcDream.Content.csproj" />
</ItemGroup>
</Project>