From e71ed73aa9bb96b9edb2603c2095c937d6dd4b8e Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Apr 2026 10:39:43 +0200 Subject: [PATCH] fix(anim): Phase L.1c spawn-time cycle fallback + diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User reports same "torso on the ground" symptom after 34d7f4d. Likely cause: my fallback only covered the OnLiveMotionUpdated path, not the spawn handler at the CreateObject boundary. If the spawn-time SetCycle requests a (style, motion) pair the MotionTable lacks, ClearCyclicTail wipes the cyclic tail at line 396 of AnimationSequencer.cs and every body part snaps to its setup-default offset until the first OnLiveMotionUpdated UM applies the path's fallback there. Apply the same fallback chain (requested → WalkForward → Ready → no-op-don't-clear) at the spawn handler. Also add a one-line diagnostic dump (under ACDREAM_DUMP_MOTION=1) on both code paths so the next launch confirms whether the fallback is actually firing and what (mtable, style, motion) tuples are missing. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 52 ++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 100ea54..4ea7f0a 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -2111,7 +2111,48 @@ public sealed class GameWindow : IDisposable { seqMotion = AcDream.Core.Physics.MotionCommand.Ready; } - sequencer.SetCycle(seqStyle, seqMotion); + + // Phase L.1c followup (2026-04-28): apply the same + // missing-cycle fallback the OnLiveMotionUpdated path + // uses. Without this, a monster spawned in combat + // stance with the wire's seqMotion absent from its + // MotionTable hits ClearCyclicTail() with no + // replacement enqueue, every body part snaps to its + // setup-default offset, and the visual collapses to + // "torso on the ground" — visible to acdream + // observers when another client is in combat with a + // monster, until the first OnLiveMotionUpdated UM + // applies the same fallback there. + uint spawnCycle = seqMotion; + if (!sequencer.HasCycle(seqStyle, spawnCycle)) + { + uint origCycle = spawnCycle; + // RunForward → WalkForward → Ready + if ((spawnCycle & 0xFFu) == 0x07 + && sequencer.HasCycle(seqStyle, 0x45000005u)) + { + spawnCycle = 0x45000005u; + } + else if (sequencer.HasCycle(seqStyle, 0x41000003u)) + { + spawnCycle = 0x41000003u; + } + else + { + spawnCycle = 0; + } + + if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1") + { + Console.WriteLine( + $"spawn cycle missing for guid=0x{spawn.Guid:X8} mtable=0x{mtableId:X8} " + + $"style=0x{seqStyle:X8} requested=0x{origCycle:X8} " + + $"→ fallback=0x{spawnCycle:X8}"); + } + } + + if (spawnCycle != 0) + sequencer.SetCycle(seqStyle, spawnCycle); } } } @@ -2603,6 +2644,7 @@ public sealed class GameWindow : IDisposable uint cycleToPlay = animCycle; if (!ae.Sequencer.HasCycle(fullStyle, cycleToPlay)) { + uint requested = cycleToPlay; // RunForward (0x44000007) → WalkForward (0x45000005) if ((cycleToPlay & 0xFFu) == 0x07 && ae.Sequencer.HasCycle(fullStyle, 0x45000005u)) @@ -2621,6 +2663,14 @@ public sealed class GameWindow : IDisposable { cycleToPlay = 0; } + + if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1") + { + Console.WriteLine( + $"UM cycle missing for guid=0x{update.Guid:X8} " + + $"style=0x{fullStyle:X8} requested=0x{requested:X8} " + + $"→ fallback=0x{cycleToPlay:X8}"); + } } if (cycleToPlay != 0) ae.Sequencer.SetCycle(fullStyle, cycleToPlay, animSpeed);