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.
| Date uploaded | 6 days ago |
| Version | 0.1.5 |
| Download link | CeruleanCutlass-AggroKit-0.1.5.zip |
| Downloads | 90 |
| Dependency string | CeruleanCutlass-AggroKit-0.1.5 |
This mod requires the following mods to function
CeruleanCutlass-ForgeKit
Dependency-free dev-tooling for Outward BepInEx mods: file-driven dev command loop, self-test harness, on-screen toasts, player-ready lifecycle wait, embedded/override table loaders, and a shared dev-verb pack (movement/combat/skill/status probes).
Preferred version: 0.4.10README
AggroKit
📖 Full documentation: AggroKit wiki page
Reusable aggro/threat-control tooling for people writing BepInEx 5 mods for Outward: Definitive Edition. It doesn't add anything a player sees on its own — it's a library dependency other mods (CompanionKit, and anything built on top of it) pull in, and other modders are welcome to build on it too.
Requires: BepInEx 5 (Outward's Mono branch — see Compatibility below) + ForgeKit (hard dependency, installed automatically).
What's in it
| Piece | What it gives you |
|---|---|
AggroTools |
Patch-free primitives for reading/adjusting engine aggro state without a Harmony patch of your own. |
TargetableOverrides |
A per-pair / per-target targetability gate — make one specific character untargetable to one specific other character (or to everyone), without touching the game's global targeting rules. Pair beats for-all. Since 2026-08-22 it also stops AI acquisition (detection, hurt-aggro, squad hand-outs) and skill sub-effects, not just lock-on and melee. |
AggroTools.Protect(target, owner) / Unprotect |
The convenience over the gate: nobody may attack target (AI, player weapons, lock-on, skill sub-effects); existing AI locks on it are released. Re-allow one attacker with TargetableOverrides.Set(attacker, target, true, owner). |
AggroEvents |
Observability hooks (opt-in, off by default) plus an opt-in squad-self-target guard (also off by default — see BlockSquadSelfTarget). |
Installing (for players)
Drop the AggroKit folder into BepInEx/plugins/. It has no effect by itself — install it
because another mod you're using declares it as a dependency.
Configuration
BepInEx/config/cobalt.aggrokit.cfg (generated on first launch):
[Dev]
## Poll BepInEx/config/ak_cmd.txt for dev commands (aggrodump/forcetarget/taunt/calm/...).
EnableCommandFile = true
## Whether DumpKey logs an aggro dump of all AI characters near the player.
EnableDumpKey = true
## Key that logs an aggro dump of all AI characters near the player (needs EnableDumpKey=true).
# Default value: F3
DumpKey = F3
[Research]
## Re-arm AggroEvents' purely-observational Harmony taps that record the event buffer. OFF by
## default so these hot AI-tick paths stay un-patched. With EnablePatches on, the detection-side
## control patches and the targetability override gate are installed regardless of this key — they're
## inert until a verb activates them.
EnableObservation = false
[Fixes]
## Master switch for EVERY Harmony patch AggroKit installs. Off = it patches nothing at all, while
## the library calls and the ak_cmd.txt verbs keep working — so AggroKit can be ruled in or out of a
## mod-conflict bisection without uninstalling it. Applied at load; changing it needs a relaunch.
EnablePatches = true
## Opt-in fix for a squad-contagion self-target assignment in AISCombat.SetPreferredTarget.
## DEFAULT OFF: the case is unreachable in vanilla, and the block is a skipping Harmony prefix, which
## also suppresses any other mod's prefix on the same method. Read live — no relaunch needed.
BlockSquadSelfTarget = false
Dev verbs
AggroKit polls BepInEx/config/ak_cmd.txt (write a line, it runs on the next poll — works while
the game is paused). [Dev] EnableCommandFile = false turns the channel off.
| Verb | What it does |
|---|---|
aggrodump [radius] |
Aggro dump of every AI near the player, plus the player's targetability state |
aistates [name|nearest] |
Dump one AI's state machine |
aggrolog [n|on|off|clear] |
The [AGGRO] event buffer |
watch <name|nearest|off> |
Watch an AI's (state, target) changes |
forcetarget [name|nearest] |
Force an AI onto the player, and auto-watch it |
feud <A> <B> [nooverride|both] |
Make any two AIs fight each other |
taunt [radius] / calm [radius] |
Pull every AI in radius onto the player / calm them |
truce [seconds] [name|nearest] / truceoff [name|nearest|all] / truces |
One AI tolerates the player for N s (your hit on it ends it) / end one or all / list them |
untargetable / targetable |
Toggle the player's QuestNonTargetable |
undetectable / detectable |
Toggle the player's DetectabilityMult |
shieldme / unshieldme |
Toggle incoming-aggro shielding on the player |
stealthme [off] |
Veto AI detection of the player |
decoy <name>|off |
Redirect detections of the player onto another AI |
noaggro [off] |
The player's hits generate no aggro |
clearoverrides |
Clear all targetability overrides |
protect <name|nearest> / unprotect |
Make an AI un-attackable by everyone (AI acquisition, melee, lock-on, skill sub-effects) / undo it |
status |
One glance: player flags, overrides, controls, watches, aggrolog |
restore |
Undo everything — targetable + detectable, all overrides/controls/watches cleared |
selftest |
Zero-interaction environment checks ([SELFTEST] PASS/FAIL … DONE) |
The [Dev] DumpKey bind (default F3) runs aggrodump without the channel. Keys are a
cross-mod resource in this workspace — AggroKit claims its bind through ForgeKit.Keybinds so a
collision with another mod is reported rather than silently firing both.
Using it (for modders)
<!-- your .csproj -->
<ProjectReference Include="path\to\AggroKit\AggroKit.csproj" Private="false" />
[BepInPlugin(GUID, NAME, VERSION)]
[BepInDependency(AggroKit.Plugin.GUID)]
public class Plugin : BaseUnityPlugin
{
// AggroTools / TargetableOverrides / AggroEvents / TauntController / AggroTruce are static
// entry points — call them directly once AggroKit has loaded (declare it as a dependency above).
}
Private="false" matters — it stops MSBuild from copying a second AggroKit.dll into your
mod's own output folder. The kit ships from its own BepInEx/plugins/AggroKit/ folder; your mod
just references it and declares the dependency so BepInEx loads it first.
Compatibility
Outward must be on its Mono Steam beta branch, not the default IL2CPP build — like every
BepInEx 5 mod for this game. If your game runs but no BepInEx mods load and there's no crash log,
this is almost always why (Properties → Betas → select mono in Steam).
License
Apache License 2.0 — see LICENSE in the repository root. You may use, modify, and redistribute
this kit (including in commercial mods) provided you keep the copyright/license notice; see the
license text for the full terms.
CHANGELOG
AggroKit changelog
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