AggroKit
Reusable aggro/threat control tooling for Outward BepInEx mods: per-pair targetability gates, detection veto/redirect, taunt and force-target primitives. Inert until a mod uses it — a library dependency, installed automatically.AggroKit changelog
0.1.6 — 2026-09-05
- ForgeKit 0.4.13 / DonorKit 0.1.9 / SpawnKit 0.6.1: session-3 fixes
- packaging: raise ForgeKit dep pins to 0.4.12 and StoryKit to 0.1.8 (fresh-install floor)
- Spawn gate abstraction: Lifecycle.IsGameplayLive, StoryKit SpawnPolicy + posture census, WalkSpeed default
0.1.5 — 2026-08-28
- Fix stale dependency pins across the fleet; DonorKit 0.1.7
- Cross-area pet orphan fix + three combat-staging diagnostics
attackstaging verb (aliasaggroonto) —attack <victim> [attacker] [holdSeconds]: make a nearby creature fight a chosen VICTIM, not just the player. Victim is a name substring,uid:xxxx,me, orpet/ally(the nearest player-faction AI that is not you — a companion anchor, hireling or summon; AggroKit stays pet-agnostic). Attacker defaults to the nearest non-player-faction AI in 40 m. It installs a PAIR targetability override only when the faction matrix would veto the lock, alerts the attacker's squad the way a vanilla detection does, and SUSTAINS the pin forholdSeconds(default 20,0= one-shot) throughTauntController, which re-asserts every 0.3 s and so out-paces AISCombat's 0.5 s re-poll and the 50 %ChanceToSwitchTargetOnHurtsteal. Motivation: a live session's one-shot staging left a hostile 2.4 m from a companion still fighting the player, with nothing in the log to say why.- Every force reports a REASON —
AggroTools.ForceTargetDetailedreturns anAttackOutcome(NoAttacker,NoVictim,AttackerHasNoTargeting,VictimNotAlive,VictimHasNoLockingPoint,BlockedByOverride,NoCombatState,LockDidNotHold,Locked) andAttackPlan.Describeturns it into a remedy sentence.attackandforcetargetboth printN retargetedplus the reason, at Message on success and Warning on failure — so a zero can never be read as silence again.AggroTools.ForceTargetitself is unchanged and stays the boolean recipe consumers call. - New pure helpers on
AggroTools:FindCharacter(victims need not have aCharacterAI),FindAlly,FindHostile.AttackPlan(outcome ladder + argument grammar) is System-only and unit-tested intests/AggroKit.Tests/AttackPlanTests.cs. ak_cmd.txtruns the WHOLE file (allLines: true), like every other channel in the family, and honours#comments. It was the last "first non-blank line only" channel: a batched two-line file silently discarded its second line with nothing in the log to say so.- Additive only —
COMPAT_SINCEstays0.1.3.
0.1.4 — 2026-08-23
-
Truce breakers can be resolved LIVE —
AggroTruce.Holdgains an overload taking a dealer-UID predicate thatTruceTableconsults at BREAK time; the UID snapshot stays as the fallback. A consumer whose breaker is a live thing (Beastwhispering's pet anchor, which does not exist yet when the failed-tame truce is armed) registers the predicate instead. (static analysis AO5-1/AO5-7) -
AggroTruce.Enforceable— false when[Fixes] EnablePatcheswas off at boot, i.e. when a held truce is bookkeeping only because none of the blocking prefixes were installed.Holdwarns once instead of asserting a peace that does not exist. (AO5-2) -
AggroTools.ForceTargetnow RELEASES a live truce on the pair it forces (reasonforcetarget) rather than overriding it silently; it never refuses on a truce. (AO5-5, Cobalt's ruling) -
Owner retags are never silent:
OwnerTable.Ownreports the owner it displaced,TruceTable.RecordcarriesDisplacedOwner, andOverrideTablegains an additiveOnRetagsink. (AO5-8) -
TauntControllerwarns once per hold when its re-assert cannot land, with the fields that separate the causes, instead of failing silently for the hold's whole duration. (AO5-9) -
Truce hygiene: the gone-subject strike table is cleared on Tick's idle early-return. (AO5-6)
-
Protected characters (
AggroTools.Protect(target, owner)/Unprotect/IsProtected, dev verbsprotect/unprotect): aTargetableOverrides"targetable=false" entry is now honoured at AI acquisition too — prefixes onAICEnemyDetection.Detected/CharHurtandAISCombat.SetPreferredTargetrefuse a blocked target (the faction-keyed scan could not be asked per Character, so before this a blocked target FLAPPED instead of staying at peace) — and at skill/status sub-effects (SubEffect.IsTargetablepostfix).ForceTargetrefuses a blocked pair.Protectalso releases every AI lock already held on the target. Resolution order pair > for-allvanilla is pure (
OverrideTable, unit-tested);ClearForAllOwnedByis the owner-checked clear. Built for the road merchant the pet's anchor bit on 2026-08-22. Known hole:RaycastProjectilevolleys (faction-keyed). -
AggroTruce— a timed, pair-keyed truce: one AI tolerates one character (no scan or squad acquisition; calmed once if already locked), broken the moment that character — or a consumer-named extra breaker such as a pet's anchor — hits it, at which point vanilla aggro runs untouched. TheCharHurt/Defenseprefixes break it BEFORE the original runs;Detected/SetPreferredTargetread it after the global veto. PureTruceTablebookkeeping, unit-tested. Built for Beastwhispering's failed-tame outcome. -
AggroTools.AlertSquad(ai, target)— the squad half of a vanilla detection (AISquad.AddTarget), kept separate fromForceTargetso TauntController's re-assert never spams it. -
Verbs
truce [seconds] [name|nearest],truceoff [name|nearest|all],truces;statusandrestoreknow about truces; the scene-load sweep clears dev-staged truces only.
0.1.3 — 2026-08-19
- Merge branch 'fix/sa-0815-leafkits' into feature/pet-self-feed
- Static-analysis wave F3c: leaf-kit fixes (A7-1/2/4/8/9/13/14/15/16/20/23/24/25/27)
- Test debt (F4): model every project in the dependency gate, AggroKit's first tests, UsageSpec corpus coverage
- Coercion wave: every ForgeKit integration completes cleanly through the TUI seam
- Forge shell fixes: SSH commands via bash -c (fish login shell); set/cfgdump on every channel mod
- Forge shell: catalog dump + response protocol + set/cfgdump in ForgeKit; forge CLI/REPL + completion packs; wiki-enriched name db
0.1.2 — 2026-08-11
Prompted by a player report that bisected a vanilla-content bug down to AggroKit. The report did not hold up — AggroKit patches nothing on the path it was blamed for — but answering it took five passes over the decompiled game, because AggroKit was effectively unfalsifiable in the field. This release is about that, plus the one real defect the investigation found.
[Fixes] BlockSquadSelfTargetnow defaults OFF. It was the mod's only unconditional change to vanilla AI, and it is implemented as a skipping Harmony prefix — which under HarmonyX suppresses every other mod's prefix onAISCombat.SetPreferredTargetas well as the original. The case it guards is unreachable in vanilla (both callers filter to allies, and an ally is never a valid target), so it was paying a cross-mod cost for a benefit that cannot be demonstrated. Still available for anyone who actually hits the fake-combat stall.- New
[Fixes] EnablePatchesmaster switch (default on). Disables every Harmony patch AggroKit installs while keeping the library primitives and theak_cmd.txtdev verbs working — so AggroKit can be ruled in or out of a mod-conflict bisection without uninstalling it. Five patch classes previously had no gate at all. - AggroKit now says what it patched. A boot-time census of the vanilla methods actually patched
(read back from the Harmony instance, not a hand-maintained list) plus the resolved gate values,
both also surfaced by the
statusverb. - The self-target block reports when it fires, bounded per session — previously it was silent unless the dev-only ring buffer was enabled, which it never is in a shipped install.
HarmonyPriority(Priority.Last)on the three prefixes that can skip the original, so when AggroKit suppresses a method, other mods' patches have already had their say.- The
IsTargetablepostfix — the one patch body doing real work with notry/catch— now routes throws to the same boundedSwallowedreporter as every other patch body. - The self-test is now gate-aware: it asserts each patch's installed state matches its config
rather than asserting it is installed, so
EnablePatches = falseno longer reads as a failure. It also cross-checks the census against the gates. - Docs: the README, wiki page and Thunderstore description no longer describe an "always-on" fix,
and both now document
EnablePatches.
Also in this release:
- AggroKit: trim manifest description to Thunderstore's 250-char limit
- Merge branch 'main' into fix/sa-0808-kits
- Static-analysis 2026-08-08 wave D-kits: P2-13/P2-15 + the kit P3 group
- Static-analysis 2026-08-08 wave 0: nine P1/P2 fixes
- W4: fixes for everything D1 found, plus the join-race P1
- AggroKit: fake-null cleanup for the aggro taps and taunt forensics
- A1: the fake-null accessors, and every Plugin.cs cleared
- A0 pilot: the fake-null rewrite recipe, proven on one file
0.1.1 — 2026-08-02
- Logging: answer "why did nothing happen?" without needing LogVerbose on in advance
- Cleanup: one anchor-collect loop, one ownership side-table
- Docs sweep: archive, condense, and validate the whole documentation tree
