fix #270 closeout: strip investigation probes; close the issue
User-verified: casting fixed (exhaustion-edge gate) and monster attack animations restored (spawn settle placement + lost-cell retry). Final session evidence: 14/15 spawn settles grounded; Falling-refusal spam collapsed 2,954 -> 15 transient pre-settle lines. Strips the [UM-ACT]/[MT-FAIL]/[SPAWN-PLACE]/[remote-edge] probes, the MotionInterpreter.DiagnosticGuid plumbing, and the two throwaway probe tests (motion-table attack sweep, vitae color dump - both findings are recorded in ISSUES/research). Complete Release suite: 10,030 passed / 5 skips / 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
21b3a3f3d3
commit
bb1640f777
7 changed files with 3 additions and 263 deletions
|
|
@ -100,7 +100,7 @@ Copy this block when adding a new issue:
|
||||||
|
|
||||||
## #270 — Stuck spell animations + intermittently missing monster attack animations
|
## #270 — Stuck spell animations + intermittently missing monster attack animations
|
||||||
|
|
||||||
**Status:** LOCAL CAUSE FIXED 2026-07-30 (pending user re-test); remote
|
**Status:** CLOSED 2026-07-31 — both symptoms user-verified fixed (stuck casts: exhaustion-edge gate `a46c8e65`; missing monster attack animations: spawn settle placement `21b3a3f3` + lost-cell retry `807fdb5f`). Final settle-session log: 14/15 spawn settles grounded; Falling-refusal spam 2,954 → 15 transient pre-settle lines. All #270 probes stripped.
|
||||||
attack-animation misses under investigation with the [remote-edge] probe
|
attack-animation misses under investigation with the [remote-edge] probe
|
||||||
**Severity:** HIGH (combat/casting presentation)
|
**Severity:** HIGH (combat/casting presentation)
|
||||||
**Component:** motion re-dispatch cadence / remote action animations
|
**Component:** motion re-dispatch cadence / remote action animations
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,6 @@ internal sealed class LiveEntityMotionRuntimeController
|
||||||
rm.Sink = new AcDream.Core.Physics.Motion.MotionTableDispatchSink(sequencer);
|
rm.Sink = new AcDream.Core.Physics.Motion.MotionTableDispatchSink(sequencer);
|
||||||
rm.Motion.DefaultSink = rm.Sink;
|
rm.Motion.DefaultSink = rm.Sink;
|
||||||
}
|
}
|
||||||
// #270 probe identity — strip with the probes.
|
|
||||||
rm.Motion.DiagnosticGuid = serverGuid;
|
|
||||||
// #174 (2026-07-05): the RemoveLinkAnimations seam is retail
|
// #174 (2026-07-05): the RemoveLinkAnimations seam is retail
|
||||||
// CPhysicsObj::RemoveLinkAnimations 0x0050fe20 — a TAILCALL to
|
// CPhysicsObj::RemoveLinkAnimations 0x0050fe20 — a TAILCALL to
|
||||||
// CPartArray::HandleEnterWorld 0x00517d70 →
|
// CPartArray::HandleEnterWorld 0x00517d70 →
|
||||||
|
|
|
||||||
|
|
@ -205,13 +205,6 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
body: remote.Body,
|
body: remote.Body,
|
||||||
moverFlags: moverFlags,
|
moverFlags: moverFlags,
|
||||||
movingEntityId: entity.Id);
|
movingEntityId: entity.Id);
|
||||||
if (AcDream.Core.Physics.PhysicsDiagnostics.DumpMotionEnabled)
|
|
||||||
{
|
|
||||||
Console.WriteLine(
|
|
||||||
$"[SPAWN-PLACE] guid={serverGuid:X8} cell=0x{cellId:X8} ok={settle.Ok} "
|
|
||||||
+ $"contact={settle.InContact} walkable={settle.OnWalkable} "
|
|
||||||
+ $"z={worldPos.Z:F3}->{settle.Position.Z:F3}");
|
|
||||||
}
|
|
||||||
if (!settle.Ok || !settle.InContact)
|
if (!settle.Ok || !settle.InContact)
|
||||||
return; // no floor within reach — stays airborne like retail's fall
|
return; // no floor within reach — stays airborne like retail's fall
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -678,13 +678,6 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Action? RemoveLinkAnimations { get; set; }
|
public Action? RemoveLinkAnimations { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// #270 diagnostic identity (2026-07-30): the owning entity's server
|
|
||||||
/// guid, stamped by the binding layer so [UM-ACT]/[MT-FAIL] probe lines
|
|
||||||
/// are attributable (probe-identity-attribution lesson). 0 = unset.
|
|
||||||
/// Strip with the #270 probes.
|
|
||||||
/// </summary>
|
|
||||||
public uint DiagnosticGuid { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// R3-W4 no-op seam standing in for retail
|
/// R3-W4 no-op seam standing in for retail
|
||||||
|
|
@ -2781,16 +2774,6 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
int diff = incoming >= stored ? incoming - stored : stored - incoming;
|
int diff = incoming >= stored ? incoming - stored : stored - incoming;
|
||||||
bool newer = diff <= 0x3FFF ? stored < incoming : incoming < stored;
|
bool newer = diff <= 0x3FFF ? stored < incoming : incoming < stored;
|
||||||
|
|
||||||
// #270 missing-attack investigation (2026-07-30): one line per
|
|
||||||
// wire action item with the gate verdict — rides
|
|
||||||
// ACDREAM_DUMP_MOTION=1. Strip when #270 closes.
|
|
||||||
if (PhysicsDiagnostics.DumpMotionEnabled)
|
|
||||||
{
|
|
||||||
Console.WriteLine(
|
|
||||||
$"[UM-ACT] guid={DiagnosticGuid:X8} cmd=0x{a.Command:X8} stamp={incoming} stored={stored} "
|
|
||||||
+ $"newer={newer} auton={a.Autonomous} localSkip={IsLocalPlayer && a.Autonomous}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newer) continue;
|
if (!newer) continue;
|
||||||
|
|
||||||
// Local player skips its own autonomous echoes (305977).
|
// Local player skips its own autonomous echoes (305977).
|
||||||
|
|
@ -3039,19 +3022,6 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
// non-action" apply-only path below — but WITHOUT writing state.
|
// non-action" apply-only path below — but WITHOUT writing state.
|
||||||
bool dispatchOk = sink?.ApplyMotion(motion, p.Speed) ?? true;
|
bool dispatchOk = sink?.ApplyMotion(motion, p.Speed) ?? true;
|
||||||
|
|
||||||
// #270 missing-attack investigation (2026-07-30): surface FAILED
|
|
||||||
// animation dispatches — a silently-failing sink is the "monster
|
|
||||||
// attacks but the animation never fires" candidate. Rides
|
|
||||||
// ACDREAM_DUMP_MOTION=1. Strip when #270 closes.
|
|
||||||
if (!dispatchOk && PhysicsDiagnostics.DumpMotionEnabled)
|
|
||||||
{
|
|
||||||
Console.WriteLine(
|
|
||||||
$"[MT-FAIL] guid={DiagnosticGuid:X8} motion=0x{motion:X8} speed={p.Speed:F2} "
|
|
||||||
+ $"style=0x{InterpretedState.CurrentStyle:X8} substate=0x{InterpretedState.ForwardCommand:X8} "
|
|
||||||
+ $"contact={PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact)} "
|
|
||||||
+ $"walkable={PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dispatchOk)
|
if (!dispatchOk)
|
||||||
{
|
{
|
||||||
// Retail: `result = CPhysicsObj::DoInterpretedMotion(...)` is
|
// Retail: `result = CPhysicsObj::DoInterpretedMotion(...)` is
|
||||||
|
|
|
||||||
|
|
@ -747,26 +747,6 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
||||||
rm.Body.Position = resolved.Position;
|
rm.Body.Position = resolved.Position;
|
||||||
if (resolved.CellId != 0)
|
if (resolved.CellId != 0)
|
||||||
committedCellId = resolved.CellId;
|
committedCellId = resolved.CellId;
|
||||||
// [remote-edge] probe (stuck-cast/missing-attack investigation,
|
|
||||||
// 2026-07-30): each ground edge drains the mover's pending
|
|
||||||
// action animations (retail HandleEnterWorld) — one line per
|
|
||||||
// edge correlates eaten attack gestures with contact flickers.
|
|
||||||
Action hitGround = rm.Movement.HitGround;
|
|
||||||
Action leaveGround = rm.Motion.LeaveGround;
|
|
||||||
if (AcDream.Core.Physics.PhysicsDiagnostics.DumpMotionEnabled)
|
|
||||||
{
|
|
||||||
uint edgeGuid = record.ServerGuid;
|
|
||||||
hitGround = () =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[remote-edge] guid={edgeGuid:X8} HitGround");
|
|
||||||
rm.Movement.HitGround();
|
|
||||||
};
|
|
||||||
leaveGround = () =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[remote-edge] guid={edgeGuid:X8} LeaveGround");
|
|
||||||
rm.Motion.LeaveGround();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!AcDream.Core.Physics.PhysicsObjUpdate.CommitSetPositionTransition(
|
if (!AcDream.Core.Physics.PhysicsObjUpdate.CommitSetPositionTransition(
|
||||||
rm.Body,
|
rm.Body,
|
||||||
resolved.InContact,
|
resolved.InContact,
|
||||||
|
|
@ -775,8 +755,8 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
||||||
resolved.CollisionNormal,
|
resolved.CollisionNormal,
|
||||||
previousContact,
|
previousContact,
|
||||||
previousOnWalkable,
|
previousOnWalkable,
|
||||||
hitGround,
|
rm.Movement.HitGround,
|
||||||
leaveGround,
|
rm.Motion.LeaveGround,
|
||||||
() => IsCurrentOwner(
|
() => IsCurrentOwner(
|
||||||
record,
|
record,
|
||||||
rm,
|
rm,
|
||||||
|
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using AcDream.App.UI.Layout;
|
|
||||||
using DatReaderWriter;
|
|
||||||
using DatReaderWriter.Options;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace AcDream.App.Tests.UI.Layout;
|
|
||||||
|
|
||||||
// THROWAWAY probe (#268): dump the authored 0x1B font-color arrays for the
|
|
||||||
// character-panel footer labels so we can read retail's vitae parenthetical
|
|
||||||
// color (AppendTextWithFont color index 3, gmSkillUI 0x0049b972). Delete
|
|
||||||
// after the color constant is captured.
|
|
||||||
public sealed class VitaeColorDumpProbe
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Dump_footer_font_color_arrays()
|
|
||||||
{
|
|
||||||
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
|
||||||
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
"Documents", "Asheron's Call");
|
|
||||||
if (!Directory.Exists(datDir)) return;
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
||||||
foreach (uint layoutId in new[] { 0x2100002Eu, 0x2100002Cu })
|
|
||||||
{
|
|
||||||
var root = LayoutImporter.ImportInfos(dats, layoutId);
|
|
||||||
if (root is null) { sb.AppendLine($"layout {layoutId:X8}: NOT FOUND"); continue; }
|
|
||||||
sb.AppendLine($"=== layout {layoutId:X8} ===");
|
|
||||||
Walk(root, sb);
|
|
||||||
}
|
|
||||||
|
|
||||||
var outPath = Path.Combine(AppContext.BaseDirectory, "vitae-color-dump.txt");
|
|
||||||
File.WriteAllText(outPath, sb.ToString());
|
|
||||||
// Also drop a copy next to the repo artifacts when resolvable.
|
|
||||||
var repoCopy = Environment.GetEnvironmentVariable("ACDREAM_PROBE_OUT");
|
|
||||||
if (!string.IsNullOrEmpty(repoCopy))
|
|
||||||
File.WriteAllText(repoCopy, sb.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Walk(ElementInfo e, StringBuilder sb)
|
|
||||||
{
|
|
||||||
if (e.TryGetEffectiveProperty(0x1Bu, out var color))
|
|
||||||
{
|
|
||||||
if (color.Kind == UiPropertyKind.Array && color.ArrayValue.Count > 1)
|
|
||||||
{
|
|
||||||
sb.Append($"element {e.Id:X8} 0x1B array[{color.ArrayValue.Count}]:");
|
|
||||||
for (int i = 0; i < color.ArrayValue.Count; i++)
|
|
||||||
{
|
|
||||||
var v = color.ArrayValue[i];
|
|
||||||
if (v.Kind == UiPropertyKind.Color)
|
|
||||||
{
|
|
||||||
var c = v.ColorValue;
|
|
||||||
sb.Append($" [{i}]=A{c.Alpha:D3},R{c.Red:D3},G{c.Green:D3},B{c.Blue:D3}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sb.Append($" [{i}]=kind:{v.Kind}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.AppendLine();
|
|
||||||
}
|
|
||||||
else if (color.Kind == UiPropertyKind.Color)
|
|
||||||
{
|
|
||||||
var c = color.ColorValue;
|
|
||||||
sb.AppendLine(
|
|
||||||
$"element {e.Id:X8} 0x1B single: A{c.Alpha:D3},R{c.Red:D3},G{c.Green:D3},B{c.Blue:D3}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var child in e.Children)
|
|
||||||
Walk(child, sb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue