diff --git a/src/AcDream.App/Plugins/AppAutomationSurface.cs b/src/AcDream.App/Plugins/AppAutomationSurface.cs
index f000d31b..90e929ed 100644
--- a/src/AcDream.App/Plugins/AppAutomationSurface.cs
+++ b/src/AcDream.App/Plugins/AppAutomationSurface.cs
@@ -384,14 +384,27 @@ internal sealed class AppAutomationSurface
}
// ── IMagicCommands ────────────────────────────────────────────────────
+ ///
+ /// True while an action the server has not acknowledged is in flight.
+ ///
+ ///
+ /// Reads the shared busy count, which the cast path increments
+ /// (FreeHandsAndCastSpell @0x00566EF0) and the server's UseDone
+ /// decrements. NOT RuntimeSpellCastState.LastRequestedSpellId: that
+ /// records the last spell requested and is cleared only by Reset at session
+ /// teardown, so using it as a busy flag latches true after the first cast
+ /// and never clears -- which is exactly what stalled MossTank's first buff
+ /// pass at one cast.
+ ///
public bool IsCasting
{
get
{
- RuntimeSpellCastState? cast;
+ GameRuntime? runtime;
lock (_gate)
- cast = _cast;
- return cast?.LastRequestedSpellId is not null;
+ runtime = _runtime;
+ return runtime is not null
+ && runtime.InventoryOwner.Transactions.BusyCount > 0;
}
}
@@ -408,6 +421,8 @@ internal sealed class AppAutomationSurface
return PluginCastGate.Unavailable;
if (!spellbook.Knows(spellId))
return PluginCastGate.NotKnown;
+ if (IsCasting)
+ return PluginCastGate.Busy;
return cast.EvaluateCastGate(spellId) switch
{