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>
142 lines
6.8 KiB
C#
142 lines
6.8 KiB
C#
// PesChainAudit — recursively walk weather PES chains, dump every hook,
|
|
// and decode every CreateParticleHook target ParticleEmitter.
|
|
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 };
|
|
|
|
var rainCandidates = new List<(uint pesRoot, uint emitterId, double life, float maxOff, System.Numerics.Vector3 a)>();
|
|
|
|
foreach (var root in roots)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine($"############ ROOT PES 0x{root:X8} ############");
|
|
var visited = new HashSet<uint>();
|
|
WalkPes(root, depth: 0, rootForReport: root);
|
|
|
|
void WalkPes(uint pesId, int depth, uint rootForReport)
|
|
{
|
|
string pad = new string(' ', depth * 2);
|
|
if (!visited.Add(pesId))
|
|
{
|
|
Console.WriteLine($"{pad}PES 0x{pesId:X8} (already visited — cycle skip)");
|
|
return;
|
|
}
|
|
DatPhysicsScript? ps = physicsScripts.LoadPhysicsScript(pesId);
|
|
if (ps is null)
|
|
{
|
|
Console.WriteLine($"{pad}PES 0x{pesId:X8} NOT FOUND");
|
|
return;
|
|
}
|
|
Console.WriteLine($"{pad}PES 0x{pesId:X8} hooks={ps.ScriptData.Count}");
|
|
for (int i = 0; i < ps.ScriptData.Count; i++)
|
|
{
|
|
var entry = ps.ScriptData[i];
|
|
var hook = entry.Hook;
|
|
string head = $"{pad} [{i}] t={entry.StartTime:F3}s {hook.HookType}";
|
|
switch (hook)
|
|
{
|
|
case CallPESHook call:
|
|
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} (unexpected package header-only shape)");
|
|
break;
|
|
case SoundHook sh:
|
|
Console.WriteLine($"{head} sound=0x{(uint)sh.Id:X8}");
|
|
break;
|
|
case SoundTableHook stb:
|
|
Console.WriteLine($"{head} soundType={stb.SoundType}");
|
|
break;
|
|
case SetLightHook _:
|
|
Console.WriteLine($"{head} (set-light)");
|
|
break;
|
|
case ScaleHook sc:
|
|
Console.WriteLine($"{head} end={sc.End} time={sc.Time}");
|
|
break;
|
|
case TransparentHook th:
|
|
Console.WriteLine($"{head} start={th.Start} end={th.End} time={th.Time}");
|
|
break;
|
|
case DefaultScriptHook _:
|
|
case DefaultScriptPartHook _:
|
|
case AnimationDoneHook _:
|
|
case NoDrawHook _:
|
|
Console.WriteLine(head);
|
|
break;
|
|
default:
|
|
Console.WriteLine($"{head} (no decoder)");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void DumpEmitter(uint emitterInfoId, string pad, uint rootForReport)
|
|
{
|
|
if (!dats.TryGet<ParticleEmitter>(emitterInfoId, out var pe) || pe is null)
|
|
{
|
|
Console.WriteLine($"{pad}ParticleEmitter 0x{emitterInfoId:X8} NOT FOUND");
|
|
return;
|
|
}
|
|
Console.WriteLine($"{pad}ParticleEmitter 0x{emitterInfoId:X8}");
|
|
Console.WriteLine($"{pad} EmitterType={pe.EmitterType} ParticleType={pe.ParticleType}");
|
|
Console.WriteLine($"{pad} Gfx=0x{(uint)pe.GfxObjId:X8} HwGfx=0x{(uint)pe.HwGfxObjId:X8}");
|
|
Console.WriteLine($"{pad} Birthrate={pe.Birthrate:F4}s MaxParticles={pe.MaxParticles} Initial={pe.InitialParticles} Total={pe.TotalParticles} TotalSeconds={pe.TotalSeconds:F3}");
|
|
Console.WriteLine($"{pad} Lifespan={pe.Lifespan:F3}s ±{pe.LifespanRand:F3}");
|
|
Console.WriteLine($"{pad} OffsetDir=({pe.OffsetDir.X:F3},{pe.OffsetDir.Y:F3},{pe.OffsetDir.Z:F3}) MinOffset={pe.MinOffset:F2} MaxOffset={pe.MaxOffset:F2}");
|
|
Console.WriteLine($"{pad} A=({pe.A.X:F3},{pe.A.Y:F3},{pe.A.Z:F3}) MinA={pe.MinA:F3} MaxA={pe.MaxA:F3}");
|
|
Console.WriteLine($"{pad} B=({pe.B.X:F3},{pe.B.Y:F3},{pe.B.Z:F3}) MinB={pe.MinB:F3} MaxB={pe.MaxB:F3}");
|
|
Console.WriteLine($"{pad} C=({pe.C.X:F3},{pe.C.Y:F3},{pe.C.Z:F3}) MinC={pe.MinC:F3} MaxC={pe.MaxC:F3}");
|
|
Console.WriteLine($"{pad} Scale start={pe.StartScale:F2} final={pe.FinalScale:F2} rand={pe.ScaleRand:F2}");
|
|
Console.WriteLine($"{pad} Trans start={pe.StartTrans:F2} final={pe.FinalTrans:F2} rand={pe.TransRand:F2} ParentLocal={pe.IsParentLocal}");
|
|
bool rainLike = pe.Lifespan > 0.5
|
|
&& (pe.OffsetDir.Z > 1.0f || pe.MaxOffset > 5.0f || pe.A.Z < -0.1f);
|
|
if (pe.Lifespan > 0.5)
|
|
Console.WriteLine($"{pad} >>> Lifespan>0.5s — possibly rain-like (downward A.Z={pe.A.Z:F2}, MaxOffset={pe.MaxOffset:F2})");
|
|
if (rainLike)
|
|
rainCandidates.Add((rootForReport, emitterInfoId, pe.Lifespan, pe.MaxOffset, pe.A));
|
|
}
|
|
}
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("################ RAIN-LIKE EMITTER SUMMARY ################");
|
|
if (rainCandidates.Count == 0)
|
|
{
|
|
Console.WriteLine("NONE — no emitter discovered (any depth) has Lifespan>0.5s with downward A or large MaxOffset.");
|
|
Console.WriteLine("=> All 5 weather PESes are flash/sound only; no hidden rain emitter exists in these chains.");
|
|
}
|
|
else
|
|
{
|
|
foreach (var c in rainCandidates)
|
|
{
|
|
Console.WriteLine($" rootPES=0x{c.pesRoot:X8} emitter=0x{c.emitterId:X8} life={c.life:F2}s maxOff={c.maxOff:F2} A=({c.a.X:F2},{c.a.Y:F2},{c.a.Z:F2})");
|
|
}
|
|
}
|
|
|
|
return 0;
|