The [MT-FAIL] probe caught combat-stance monsters constantly failing to dispatch 0x40000015 (Falling): their bodies were airborne-flagged while standing. contact_allows_move (0x00528dd0) requires Contact+OnWalkable and silently refuses every action animation for an airborne mover - a spawned-standing monster's swings never played until it first moved. Retail never has this state: CreateObject spawns run the placement transition (CPhysicsObj::SetPosition -> SetPositionInternal 0x00515330), which establishes CONTACT/ON_WALKABLE from the floor at spawn. Our remote creation seeded a raw position with no placement. SeedRemoteSpawnPlacement mirrors RemoteTeleportPlacement: engine placement resolve (Setup-derived cylinder, TS-46) + the verbatim CommitSetPositionTransition, wired at BOTH RemoteMotion creation sites (UM-triggered creation - so a first-ever-UM attack animates in the same packet - and ordinary first-UP creation). Unplaceable results leave the body airborne exactly like a failed retail placement. Also adds the [UM-ACT] (wire action items + stamp-gate verdict) and [MT-FAIL] (refused animation dispatches) probes, riding ACDREAM_DUMP_MOTION=1, which are what convicted the body state. Complete Release suite: 10,032 passed / 5 skips / 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
4.8 KiB
C#
124 lines
4.8 KiB
C#
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<string>();
|
|
var styleMisses = new List<string>();
|
|
foreach (var entry in dats.Portal.Tree.GetFilesInRange(0x09000000u, 0x09FFFFFFu))
|
|
{
|
|
if (!dats.Portal.TryGet<DatReaderWriter.DBObjs.MotionTable>(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<DatReaderWriter.DBObjs.MotionTable>(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());
|
|
}
|
|
}
|