Implements docs/plans/2026-09-06-mosstank-mode-arbitration.md.
The buff pass used to fire TryCast with no regard for combat mode or
which caster was wielded. ACE's Player_Magic.cs:84-95 drops any cast
that arrives while CombatMode != Magic, and Player_Combat.cs:778+
(GetEquippedWand) requires a wielded caster before Magic mode can be
entered at all — so a buff pass started from Peace or Melee silently
cast nothing.
Design A: BuffCasterPreparer (new) is the single owner of "which
caster do we buff with and how do we get into Magic mode". It resolves
the wielded caster first, else the first profiled caster (same
membership predicate as VitalRecharge), wields it through Peace when
needed, then requests Magic — gating the buff queue on Ready. A
missing caster stops the pass with VTank's own notice, posted once
per Reset. A stuck mode request retries on a 2s cadence up to
VitalSettings.DropToPeaceModeRetryCount before stopping and naming the
stuck stage. Hosts that don't model combat-mode automation at all
(EnterMode returns Unavailable) bypass the gate rather than deadlock,
matching the existing TickEquipment convention for older/no-window
hosts.
Design B: MacroIdleModeArbiter (new) is the single owner of "Peace
Mode When Idle", deleting CombatController's own idle-peace branch.
The old branch only ran from CombatController's own no-target state,
which a disabled combat policy never reaches — so a running macro
with combat disabled never dropped to peace. The arbiter ticks after
every controller in MossTankPanel.OnTick and covers that case.
Tests 1-7 of the plan: BuffCasterPreparer (wielded-caster fast path,
wield-then-magic ordering, no-caster notice latch, exhausted retry
budget) and MacroIdleModeArbiter (retry gate, suppression, IdlePeaceMode
off, the disabled-combat case) are added to CombatControllerTests.cs,
reusing its FakeAutomation extended with deferred mode/equip
confirmation and a call log. A full buff-then-fight panel scenario is
added to MossTankPanelTests.cs via a new CombatCapableFakeAutomation.
CombatControllerTests' IdlePeaceIsTheNoTargetFallback is deleted and
re-pinned on the arbiter. Every new test was confirmed to fail (by
compile error or by runtime assertion) against the unmodified
production code via a temporary git stash before its fix landed.
337 -> 345 AcDream.Plugins.MossTank.Tests (336 baseline sans the moved
test, plus 9 new); AcDream.App.Tests MossTank/Plugin filter (79 tests,
including MossTankMarkupContractTests) stays green with no markup
changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>