acdream/docs/plans/2026-09-06-mosstank-mode-arbitration.md
2026-09-06 19:20:29 +02:00

13 KiB
Raw Blame History

MossTank — combat-mode and caster arbitration for buffing and idle

Date: 2026-09-06 Status: ACTIVE (owner request after the plugin-UI acceptance the same day) Parent: docs/plans/2026-08-26-mosstank-parity-campaign.md (MT3 buff parity / MT1 autocombat); this is the missing arbiter between the two. Owner direction: plan (Fable) → implement (Sonnet) → review (Opus, two lenses).

Owner request (verbatim intent)

  1. When the macro is running and needs to buff or fight, it must use the Items page (the profile of items the macro may equip). With buffing and fighting enabled and the character in peace mode when the macro starts, the macro must enter the right combat mode by itself and fight or buff as needed.
  2. With Peace Mode When Idle on, the macro must return to peace mode when it has no action (no buff, no fight) to perform.

Verified current state (HEAD e61edd946)

Piece Where Fact
Items profile CombatSettings.CombatItemNames (durable exact names) + CombatItemObjectIds (session ids); _noBuffItemNames in MossTankPanel Items page Add / Add (no buffs) / Remove mutate these. no buffs = VTank's "do not cast item enchantments on this weapon"; it does NOT mean "not a buffing caster".
Combat side CombatController.OnTick (:185-345) With a target: TickEquipment picks the rule weapon from the profile (SelectAutomaticWeapon filters by CombatItemNames/ObjectIds), TryEquipIfNeeded drops to Peace to wield (retry budget VitalSettings.DropToPeaceModeRetryCount, then the wand-recovery path), then EnterDefaultMode. With no target and CombatSettings.IdlePeaceMode: EnterMode(Peace) — but ONLY while _settings.Enabled (combat enabled); with combat disabled the tick returns at "Combat disabled" and nobody drops to peace.
Buff side MossTankPanel.StartBuffPass / the pass tick (~3905-3975) / TryCast Builds the queue and casts through Magic.EvaluateGate + Magic.Cast. No combat-mode and no caster handling at all. The combat controller is SetPaused while _running (pass active).
Host gates AppAutomationSurface.EvaluateGate Knows / busy / target-compatibility only; it does not check combat mode.
Server truth ACE Player_Magic.cs:84-95, 275-286 A cast arriving with CombatMode != Magic is DROPPED (SendUseDoneEvent) unless LastCombatMode == Magic. So a buff pass started from peace or melee mode silently casts nothing.
Magic mode entry ACE Player_Combat.cs:778+ (GetEquippedWand), runtime RuntimeCombatModeState.Request(Magic) Magic mode requires a wielded caster; a request without one is denied server-side. Wielding requires Peace mode (retail rule already modelled by TryEquipIfNeeded).
Caster selection precedents VitalRecharge.cs:495-540 (CasterItemType = 0x8000, profile membership), CombatController.SelectRecoveryCaster Both already pick a profiled caster.
Mode requests ICombatAutomation.EnterMode(mode)ModeChangeSent; Snapshot.Mode updates when the server confirms Asynchronous; callers must wait on the snapshot.

Design

Two small, testable owners inside the plugin. No host/API changes; the plugin-facing surface already has everything needed.

A. BuffCasterPreparer — get the character ready to cast

A state machine the buff pass runs BEFORE its first TryCast, ticked every frame while the pass is running, with Ready gating the queue. It is the single owner of "which caster do we buff with and how do we get into Magic mode". States:

  1. Resolve caster. Snapshot Equipment.CaptureOwnedEquipment(). The buffing caster is, in order: the currently wielded caster (ItemType & 0x8000, IsEquipped); else the first profiled caster (membership by CombatItemObjectIds/CombatItemNames, same predicate as VitalRecharge), deterministic order (name, then object id). no buffs membership is irrelevant here. No candidate at all → the pass STOPS with VTank's own error text, announced once through Chat.PostSystemMessage: "You must add at least one wand to your Items profile." (the string CombatController already uses), and the automatic pass does not retry for BuffSettings' next scan interval.
  2. Wield if needed. If the caster is not wielded: if mode ≠ Peace → EnterMode(Peace) and wait; when in Peace → Equipment.Equip(caster); while Equipment.IsBusy wait; if Refused → stop the pass with the refusal in the status line.
  3. Enter Magic. If mode ≠ Magic → EnterMode(Magic); wait until Snapshot.Mode == Magic.
  4. Ready → the existing queue/TryCast loop runs unchanged.

Waits carry a bounded budget: each mode request may be re-issued after ModeRetrySeconds = 2.0 and at most VitalSettings.DropToPeaceModeRetryCount times, mirroring TryEquipIfNeeded; exhausting it stops the pass with a status naming the stage ("could not enter magic mode"). Magic.IsCasting true at any stage → wait. The preparer is reset on Stop, on session end, and when the macro stops.

After the pass ends nothing is restored here: the combat controller resumes (it is un-paused) and, with a target, TickEquipment swaps to the rule weapon exactly as today; with no target the idle owner below decides.

B. MacroIdleModeArbiter — one owner for "peace when idle"

Move the idle-peace decision OUT of CombatController.OnTick (delete its _settings.IdlePeaceMode branch at :274-282; keep "Waiting for a target") into one arbiter ticked from MossTankPanel.OnTick after every controller has ticked, so it also works with combat disabled. It requests EnterMode(Peace) when ALL hold:

  • the macro is running;
  • CombatSettings.IdlePeaceMode is on;
  • no buff pass is running (!_running) and !Magic.IsCasting;
  • no controller owned an action this tick (the existing *OwnsAction set, plus _combat.HasTarget false and Equipment.IsBusy false);
  • Snapshot.Mode is not Peace/Unknown.

It re-issues at most once per IdleRetrySeconds = 1.0 while the condition holds and writes the status line "Entering peace mode" / the refusal notice. With IdlePeaceMode off it never touches the mode (VTank stays in combat mode when idle).

C. Macro start from peace mode (owner scenario)

Nothing new is needed beyond A and B: on Run Macro the combat controller already scans for hostiles and, on a target, wields the profiled weapon and enters the default mode; the automatic buff scan (TickAutomaticBuffing) starts a pass which now prepares the caster and enters Magic. Pin the whole scenario in a panel-level test (see Tests, item 6).

Non-goals

  • No new VTank options. IdlePeaceMode, the Items profile, and the buff settings are the only inputs.
  • No host/API change. Do not add combat-mode logic to AppAutomationSurface.
  • Do not change TickEquipment's weapon policy or the recovery-caster path.

Files

  • New: src/AcDream.Plugins.MossTank/BuffCasterPreparer.cs, src/AcDream.Plugins.MossTank/MacroIdleModeArbiter.cs.
  • src/AcDream.Plugins.MossTank/MossTankPanel.cs: construct both; run the preparer at the head of the pass tick and gate TryCast on Ready; reset on Stop/session end/macro stop; tick the arbiter at the end of OnTick with the owns-action set already computed there.
  • src/AcDream.Plugins.MossTank/CombatController.cs: delete the idle-peace branch (single owner), keep the rest.
  • Tests in tests/AcDream.Plugins.MossTank.Tests/ (reuse the fake automation surface in CombatControllerTests.cs; extend it if it lacks equipment or mode-change simulation).

Tests (each shown to fail before its change)

  1. Preparer, wielded caster + Peace mode: requests Magic, not Ready until the snapshot reports Magic, then Ready; exactly one EnterMode call before the snapshot flips.
  2. Preparer, caster in profile but not wielded, mode Melee: requests Peace → equips the caster (only after the snapshot reports Peace) → requests Magic → Ready; order pinned via a recorded call list.
  3. Preparer, no caster anywhere: stops the pass, posts the VTank error once, does not re-post on the next tick.
  4. Preparer, mode request never confirmed: after the retry budget the pass stops with the stage named; the number of EnterMode calls equals the budget.
  5. Arbiter: idle + IdlePeaceMode + Melee → one Peace request; second tick within 1 s → no second request; a running buff pass, a hostile target, a busy equipment swap, or IsCasting each suppress it; IdlePeaceMode off → never requests; combat DISABLED but macro running still requests (the case the old code missed).
  6. Panel scenario: macro started in Peace with buffing + combat enabled, a profiled wand and a profiled melee weapon, one buff due, one hostile in range → observed order: Peace(already) → Equip wand → Magic → cast → (pass ends) → Peace → Equip weapon → default mode → attack. Then, with the hostile gone and IdlePeaceMode on → Peace.
  7. CombatControllerTests: the moved idle-peace expectation is deleted there and re-pinned on the arbiter; every other test stays green.

Acceptance

dotnet build AcDream.slnx -c Release; dotnet test tests/AcDream.Plugins.MossTank.Tests -c Release all green (337 + new); dotnet test tests/AcDream.App.Tests -c Release unchanged failure set. Then the owner's connected gate: with a wand and a weapon on the Items page, start the macro in peace mode with buffing and combat enabled; watch it wield the wand, enter magic mode, buff, swap to the weapon and fight when a creature appears, and drop to peace when nothing is left and Peace Mode When Idle is on.

Review ledger

  • Implementation commit: c406942ef9c72dbf6744b30d46c0acf70537e298feat(mosstank): buff-caster preparer and single idle-peace arbiter — wield from the Items profile, enter magic/peace mode by itself. Design A (BuffCasterPreparer), Design B (MacroIdleModeArbiter), the MossTankPanel/CombatController wiring, and Tests 1-7 landed. AcDream.Plugins.MossTank.Tests 336 -> 345 (336 baseline sans the moved IdlePeaceIsTheNoTargetFallback, plus 9 new); AcDream.App.Tests MossTank/Plugin filter (79 tests, incl. MossTankMarkupContractTests) stays green with no markup changes. Owner review still owed (Opus, two lenses) plus the connected gate in Acceptance.

Opus review of c406942ef against VTank's decompiled source (2026-09-06)

Verdict PASS-WITH-FIXES; owner direction is knowledge base first, so the fixes are DEFERRED to Campaign VT (VT2 re-judges this commit against docs/research/vtank-kb/02-scheduler-and-actions.md). Oracle citations are into refs/vtank/decompiled/.

VTank facts established (cite these, do not re-derive): IdlePeace is one rule type cm registered LAST after the "END" sentinel (cLogic.cs:576-577); the engine runs strictly the first valid rule per tick (cLogic.cs:220-253); the buff rule fz (cLogic.cs:466) is valid whenever a buff is DUE (fz.cs:69-86), so a due buff suppresses idle-peace even between passes; cm(0) is also a pre-chain on idle loot, corpse approach, target approach and nav route (cLogic.cs:530-570, LogicRulePreChain.cs:52-62) — VTank drops to Peace BEFORE those; fd.cs:129-133 forces Magic in the tight-waypoint case. Caster/mode sequencing is one idempotent per-tick function ga.a (ga.cs:1432-1565): preferred item → wielded item if its class maps to the wanted mode → profile scan in Items-page insertion order (ga.cs:1400-1408, eq.cs:85-93; the "no buffs" spell field is ignored); no caster → one error "You must add at least one wand to your profile." + StopMacro() (ga.cs:1469-1472); retry counter per tick, exhaustion runs the bugged- combat-state wand-use recovery, never a give-up (ga.cs:1502-1524); nothing is restored after buffing — combat re-wields through the same function.

Findings to fix in VT3 (numbers from the review):

  1. BLOCKING — no-caster path re-posts the notice ~1 Hz forever (Stop clears the latch, the auto scan restarts the pass); VTank posts once and stops the macro. Fix: stop the macro (as CombatController.Disable does).
  2. BLOCKING — 2.0 s × DropToPeaceModeRetryCount(34) = 68 s > the pass's 30 s StallTimeoutSeconds, so the exhausted-budget stop is unreachable; and VTank does not stop there anyway — it runs the wand-use recovery.
  3. SHOULD-FIX — arbiter drops to peace while a buff is due (1 s scan gap); add "a buff is due" to the idle set.
  4. SHOULD-FIX — MacroIdleModeArbiter.Status is never displayed.
  5. SHOULD-FIX — profiled-caster order must be Items-page insertion order, not name order (needs an ordered profile list; CombatItemNames is a set).
  6. SHOULD-FIX — equip stage re-issues Equip every frame with no cadence or budget and no wieldability check (ga.cs:1456-1460).
  7. NIT — Mode == Unknown spins the preparer (arbiter treats it as hands-off).
  8. NIT — two caster predicates (& 0x8000 vs exact); share one helper.
  9. NIT — the notice text differs from VTank's ("…to your profile.").
  10. CARRIED — the cm(0) pre-chains and the fd.cs:131 Magic case are unported; ours suppresses idle-peace on nav/loot instead. Also: CombatController.EnterDebuffMode (:1373) and the meta setcombatstate expression can fight the arbiter (no owns-action flag) — add HasPendingItemDebuff and meta activity to the idle set. Tests T3/T4/T5b/T5d are vacuous as written (pass with the defect present); T1/T2/T5a/T5c/T6 are real pins.