// 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 DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; using DatReaderWriter.Types; using SysEnv = System.Environment; using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript; 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); 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(); 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; } if (!dats.TryGet(pesId, out var ps) || 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 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} (no payload — looks at next hook)"); 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(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;