From bb1640f777fc66bab889a458990bbe14c37f0907 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 31 Jul 2026 07:26:57 +0200 Subject: [PATCH] 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 --- docs/ISSUES.md | 2 +- .../LiveEntityMotionRuntimeController.cs | 2 - .../LiveEntityNetworkUpdateController.cs | 7 - src/AcDream.Core/Physics/MotionInterpreter.cs | 30 ----- .../Physics/RuntimeRemotePhysicsUpdater.cs | 24 +--- .../Physics/MotionTableAttackDispatchProbe.cs | 124 ------------------ .../UI/Layout/VitaeColorDumpProbe.cs | 77 ----------- 7 files changed, 3 insertions(+), 263 deletions(-) delete mode 100644 tests/AcDream.App.Tests/Physics/MotionTableAttackDispatchProbe.cs delete mode 100644 tests/AcDream.App.Tests/UI/Layout/VitaeColorDumpProbe.cs diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 3b74a38f..49ecb19a 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -100,7 +100,7 @@ Copy this block when adding a new issue: ## #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 **Severity:** HIGH (combat/casting presentation) **Component:** motion re-dispatch cadence / remote action animations diff --git a/src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs b/src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs index 37d631ab..db8a635b 100644 --- a/src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs +++ b/src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs @@ -45,8 +45,6 @@ internal sealed class LiveEntityMotionRuntimeController rm.Sink = new AcDream.Core.Physics.Motion.MotionTableDispatchSink(sequencer); 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 // CPhysicsObj::RemoveLinkAnimations 0x0050fe20 — a TAILCALL to // CPartArray::HandleEnterWorld 0x00517d70 → diff --git a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs index 866becb7..8ea2bffb 100644 --- a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs +++ b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs @@ -205,13 +205,6 @@ internal sealed class LiveEntityNetworkUpdateController body: remote.Body, moverFlags: moverFlags, 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) return; // no floor within reach — stays airborne like retail's fall diff --git a/src/AcDream.Core/Physics/MotionInterpreter.cs b/src/AcDream.Core/Physics/MotionInterpreter.cs index 0e6ed006..5aed4aec 100644 --- a/src/AcDream.Core/Physics/MotionInterpreter.cs +++ b/src/AcDream.Core/Physics/MotionInterpreter.cs @@ -678,13 +678,6 @@ public sealed class MotionInterpreter : IMotionDoneSink /// public Action? RemoveLinkAnimations { get; set; } - /// - /// #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. - /// - public uint DiagnosticGuid { get; set; } /// /// 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; 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; // 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. 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) { // Retail: `result = CPhysicsObj::DoInterpretedMotion(...)` is diff --git a/src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs b/src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs index 3f0adf7a..c6da0114 100644 --- a/src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs +++ b/src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs @@ -747,26 +747,6 @@ internal sealed class RuntimeRemotePhysicsUpdater rm.Body.Position = resolved.Position; if (resolved.CellId != 0) 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( rm.Body, resolved.InContact, @@ -775,8 +755,8 @@ internal sealed class RuntimeRemotePhysicsUpdater resolved.CollisionNormal, previousContact, previousOnWalkable, - hitGround, - leaveGround, + rm.Movement.HitGround, + rm.Motion.LeaveGround, () => IsCurrentOwner( record, rm, diff --git a/tests/AcDream.App.Tests/Physics/MotionTableAttackDispatchProbe.cs b/tests/AcDream.App.Tests/Physics/MotionTableAttackDispatchProbe.cs deleted file mode 100644 index 2b14baea..00000000 --- a/tests/AcDream.App.Tests/Physics/MotionTableAttackDispatchProbe.cs +++ /dev/null @@ -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(); - 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()); - } -} diff --git a/tests/AcDream.App.Tests/UI/Layout/VitaeColorDumpProbe.cs b/tests/AcDream.App.Tests/UI/Layout/VitaeColorDumpProbe.cs deleted file mode 100644 index 4c3aade8..00000000 --- a/tests/AcDream.App.Tests/UI/Layout/VitaeColorDumpProbe.cs +++ /dev/null @@ -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); - } -}