using System; using System.Collections.Generic; using System.IO; using System.Text; using AcDream.Core.Physics.Motion; using DatReaderWriter; using DatReaderWriter.Options; using Xunit; namespace AcDream.App.Tests.Physics; // THROWAWAY probe (#270): sweep every MotionTable in the installed portal // dat and attempt the EXACT wire dispatch observed in the stuck-attack // session (style 0x8000003C, ready substate, target attack 0x400000D3 at // speed 2.0). A silent GetObjectSequence failure = "monster attacks but the // animation never fires". Delete after #270 closes. public sealed class MotionTableAttackDispatchProbe { private sealed class NullLoader : AcDream.Core.Physics.IAnimationLoader { public DatReaderWriter.DBObjs.Animation? LoadAnimation(uint id) => null; } [Fact] public void Sweep_attack_dispatch_across_all_motion_tables() { var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); if (!Directory.Exists(datDir)) return; using var dats = new DatCollection(datDir, DatAccessType.Read); var loader = new NullLoader(); int total = 0, okCount = 0; var failures = new List(); var styleMisses = new List(); foreach (var entry in dats.Portal.Tree.GetFilesInRange(0x09000000u, 0x09FFFFFFu)) { if (!dats.Portal.TryGet(entry.Id, out var mt) || mt is null) continue; total++; var cmt = new CMotionTable(mt); var state = new MotionState { Style = 0x8000003Cu, Substate = 0x41000003u, }; var seq = new CSequence(loader); // Seed exactly like the live path: install the combat style + // ready substate first (SetDefaultState equivalent would use the // table default; the wire showed the monsters already standing in // style 0x3C ready). bool ok; try { ok = cmt.GetObjectSequence(0x400000D3u, state, seq, 2.0f, out _, stopCall: false); } catch (Exception ex) { failures.Add($"0x{entry.Id:X8} THREW {ex.GetType().Name}"); continue; } if (ok) okCount++; else { // Distinguish "table has no 0x3C style at all" (never a // combat monster) from "style exists but dispatch failed". bool hasStyle = mt.StyleDefaults.ContainsKey( (DatReaderWriter.Enums.MotionCommand)0x8000003Cu); if (hasStyle) failures.Add($"0x{entry.Id:X8} FAILED (has 0x3C style)"); else styleMisses.Add($"0x{entry.Id:X8}"); } } var sb = new StringBuilder(); sb.AppendLine($"total={total} ok={okCount} failedWithStyle={failures.Count} noStyle={styleMisses.Count}"); foreach (var f in failures) sb.AppendLine(f); // For the first few failing tables: dump what IS authored for the // CastSpell substate (0xD3) so "un-authored data" vs "lookup bug" // is decidable offline. int dumped = 0; foreach (var f in failures) { if (dumped >= 3 || !f.Contains("FAILED")) continue; uint id = Convert.ToUInt32(f.Substring(2, 8), 16); if (!dats.Portal.TryGet(id, out var mt) || mt is null) continue; dumped++; sb.AppendLine($"--- table 0x{id:X8} DefaultStyle=0x{(uint)mt.DefaultStyle:X8}"); foreach (var kv in mt.Cycles) { uint key = (uint)kv.Key; if ((key & 0xFFFFFFu) == 0xD3u || (key & 0xFFFFu) == 0xD3u) sb.AppendLine($" cycle key=0x{key:X8}"); } foreach (var kv in mt.Links) { uint key = (uint)kv.Key; sb.AppendLine($" linkFrom key=0x{key:X8} targets={kv.Value.MotionData.Count}"); foreach (var t in kv.Value.MotionData.Keys) { uint tk = (uint)t; if ((tk & 0xFFFFFFu) == 0xD3u) sb.AppendLine($" -> target 0x{tk:X8} (CASTSPELL)"); } } } File.WriteAllText( Path.Combine(AppContext.BaseDirectory, "attack-dispatch-sweep.txt"), sb.ToString()); var outCopy = Environment.GetEnvironmentVariable("ACDREAM_PROBE_OUT"); if (!string.IsNullOrEmpty(outCopy)) File.WriteAllText(outCopy, sb.ToString()); } }