APMF ("Harbinger") is an SKSE framework for Skyrim on Windows. Its cast seats open on Anniversary Edition 1.6.1170, Special Edition 1.5.97 and 1.7.104 (Steam), behind exact-version gates that refuse any other build by name, and VR is refused outright. (This line said AE only until v0.9.5, which placed the verified 1.5.97 values.) It's a scalpel, not a takeover. A mod claims exactly the facet of an NPC it needs, gets it without a fight, and everything else about that NPC (movement, other packages, the rest of its AI) keeps running untouched. When the mod releases the facet, the actor's own AI takes it straight back.
It's for SKSE plugin authors whose mod directs NPCs and wants to stop fighting other mods over who owns the actor.
Start here:
- Docs/INTEGRATION.md: the getting-started quickstart for mod authors. Read this first.
- Docs/GUIDE.md: the guide. What a facet is, how claims are ranked, how a claim lives and dies, threading, binding.
- Docs/FACETS.md: the facet reference. Every facet, its arguments, what it enforces, its lifetime, and examples.
- Docs/RECIPES.md: worked scenarios that combine facets, with complete code.
- Docs/GLOSSARY.md: every term, defined once.
- Docs/CHANNEL-MAP.md: the full facet map, with an honest proof tier per facet.
- native/APMF_API.h: the header you build against. The whole public surface, one file.
- Docs/INI-REFERENCE.md: every key in
APMF.iniand the format ofAPMF_Sources.ini. - Docs/HARBINGER-MENU.md: a short guide to the Harbinger page in the SKSE Menu Framework.
- CHANGELOG.md: what changed in each release.
Skyrim gives one actor to one package at a time. Whoever claims the actor at the highest priority wins, and everyone else is locked out. A custom follower framework that holds a follower blocks any other mod from moving or directing them. Mods that want to share control end up fighting over the same actor, and one of them loses.
APMF is a MODERATOR. It arbitrates who controls each facet of an actor and denies the losing sources, so the winner's own behavior reaches the actor. It never generates behavior itself. It calls no combat, cast, or movement command. A client mod doesn't claim a package or fight over priority: it tells APMF which facet it's taking, on whatever basis it chooses, and executes the behavior with its own proven mechanisms. APMF just makes it win and releases when the client is done.
Control is granular, per facet. A client can claim a follower's cast decision, so its own AI casts the client's chosen spell, fully animated, while the follower keeps moving under its own control, because only the cast facet was claimed.
Unaware mods and vanilla packages keep working. APMF never lies to them. It reports the actor's true state, so a package that's waiting to reach a spot simply waits, exactly as it would if the walk were slow. When APMF gives control back, that package finishes on its own.
Any aspect of AI control can be directed, not only movement: stance and sneak, weapon draw, combat targeting, headtracking, idle animations, aggression, dialogue availability, and more. See Docs/CHANNEL-MAP.md for the full list and how proven each one is.
A facet is one part of an NPC's AI that a mod can claim. Each has its own intent in the API, its own rules, and a section in Docs/FACETS.md that covers what it is for, the exact arguments, what Harbinger enforces, how long a claim lives, who wins a conflict, and short C++ examples.
| Facet | What it is for | Reference |
|---|---|---|
| Movement block (ch.1) | Stand the actor still | ch.1 |
| Gait (ch.1a) | Halve the actor's speed | ch.1a |
| Stance (ch.3) | Make the actor sneak | ch.3 |
| Weapon drawn (ch.4) | Draw the weapon, sheathe on release | ch.4 |
| Headtrack (ch.5) | Make the actor look straight up (a test facet) | ch.5 |
| Combat target (ch.6) | Record who owns the combat target | ch.6 |
| Combat action (ch.7) | Deny attack or cast behaviour | ch.7 |
| Select spell (ch.8) | Limit the spells the AI may cast | ch.8 |
| Cast (ch.8b) | Make the NPC's own AI cast a spell at a target, animated | ch.8b |
| Position cast | Cast a Target Location spell at a point (off by default) | position cast |
| Offer package (ch.9) | Hand the actor one AI package | ch.9 |
| Dialogue (ch.10) | Pause the actor's own dialogue once | ch.10 |
| Disposition (ch.11) | Make the actor aggressive and loyal | ch.11 |
| Idle (ch.12) | Play an animation, optionally at a target | ch.12 |
| Shout and power (ch.14) | Record who owns the shout facet | ch.14 |
| Equipment (ch.15) | Gate spell and staff re-arm | ch.15 |
| Detection (ch.16) | Silent movement, longer detect range | ch.16 |
| Equip authority (ch.17) | Declare what the NPC wears and refuse the rest | ch.17 |
| Travel (ch.19) | Walk the NPC to an object, cell or point, through doors to an NPC in another cell (ABI v30) | ch.19 |
| Target pin (ch.20) | Hold the combat target on yours | ch.20 |
| Combat entry (ch.21) | Start a fight | ch.21 |
| Combat re-entry deny (ch.22) | Keep the NPC out of combat for a while | ch.22 |
| Pursuit leash (ch.23) | Stop the NPC chasing too far from an anchor | ch.23 |
| Combat approach (ch.24) | Close on an actor while fighting | ch.24 |
| Cast guard (ch.25) | Hold attack spells through friends, bound channels | ch.25 |
| Equip preference (ch.26) | Name the weapons, shields, torches or staves the NPC should favour in combat | ch.26 |
| Combat substrate (ch.27) | Put the NPC into combat against Harbinger's own stand-in so an out of combat cast is animated | ch.27 |
| Place | Move an NPC beside a reference or to a point in an interior cell, once (ABI v28) | place |
| Space queries | Ask for a clear spot or the hostiles in a sphere | queries |
| Line of sight | Harbinger's own ray between two actors | queries |
| Awareness | Does this actor sense that one | queries |
| Observe-only seats | Who changes an actor value, the seat census, package sources | observe only |
Four read-only queries need no claim. GetIdleState says what became of an idle claim (ABI v24), GetSubstrateState says whether an actor's combat is a substrate (ABI v26), IsSubstrateAvailable says whether the substrate installed (ABI v27), and GetClientFacetPriority returns the calling mod's own priority on a facet, 0 meaning off (ABI v29). Each is in Docs/APMF_API-REFERENCE.md.
Some facets are field-proven and some are built but not yet seen working in game. FACETS.md says which for each one.
You build against one header, native/APMF_API.h. Start with Docs/INTEGRATION.md for how to find the interface, check the version, register as a source and run the claim, heartbeat and release pattern. Docs/GUIDE.md is the model behind it. Then use Docs/FACETS.md as the reference for each facet and Docs/RECIPES.md for scenarios that combine them. MFO is the worked example, and its native/apmf/ folder is the one to read.
#include "APMF_API.h"
// The newest struct is APMF_API_v29 (Harbinger reports ABI 30, since ABI 30 added a flag and no slot).
// APMF_API.h is canonical, never hardcode the number in your own docs.
const APMF_API::APMF_API_v29* g_apmf = nullptr;
// Once, from kPostLoad/kDataLoaded:
if (HMODULE h = GetModuleHandleA("APMF.dll")) {
auto fn = reinterpret_cast<APMF_API::GetInterface_t>(
GetProcAddress(h, APMF_API::kGetInterfaceExport));
if (fn) {
// fn() returns nullptr if the installed APMF is OLDER than the version you ask for,
// and the newest interface for any version at or below its own. Ask for the LOWEST
// ABI you can live with, then gate every newer slot on base->abiVersion.
if (auto* base = fn(/*lowest ABI you need*/ 5)) {
if (base->abiVersion >= 5)
g_apmf = reinterpret_cast<const APMF_API::APMF_API_v29*>(base); // call a newer slot only after checking abiVersion
}
}
}
// Claim a facet, drive it yourself, release when done:
if (g_apmf) {
auto claim = g_apmf->Request(actorFormID, APMF_API::kIntent_MovementBlock, /*basis=*/50.0f);
// ... your own logic while you hold it ...
g_apmf->Release(claim);
}The full walkthrough, with RequestEx, Repoint, the pending-claim rule, threading and the
claim, heartbeat and release pattern, is in Docs/INTEGRATION.md. Every facet is in
Docs/FACETS.md.
One central hook on the actor update covers every NPC from a single install. It's a virtual function hook, so it stays stable across game versions instead of breaking on each update. Control is executed at the right layer for each facet, so a client can direct an actor without overriding the package that owns it.
Full design is in design.md.
- Skyrim on Windows: Anniversary Edition 1.6.1170, Special Edition 1.5.97, or 1.7.104 (Steam). Every hook and every engine value Harbinger uses was checked on each of these three builds, and each opens on all three. Any other build is refused by name, including a 1.6.x that isn't 1170, so nothing runs unverified. VR is refused at install throughout.
- On 1.7.104 there is no Address Library, and none is needed. The id table for 1.7.104 is built into Harbinger's DLL by the CommonLibSSE-NG fork it is built on, so there is nothing extra to download or install.
- Optional: SKSE Menu Framework 3.x for the Harbinger menu page. Without it Harbinger runs from
APMF_Sources.iniand logs one line. - Build: CMake 3.21+, a C++23 compiler, vcpkg with the
commonlibsse-ngport (seenative/vcpkg.json/native/vcpkg-configuration.json). - A client mod links only against
native/APMF_API.h, a plain C-ABI header with no CommonLib dependency of its own.
The archive's root is your Data folder, so a mod manager installs it with nothing to
place by hand. Three files come out of it, and all three have to be there. A fourth file,
SKSE/Plugins/APMF_Sources.ini, is optional and is written by the menu page when you
change a priority (format in Docs/INI-REFERENCE.md):
SKSE/Plugins/APMF.dll, the plugin itself.SKSE/Plugins/APMF.ini, the settings file. Every key is documented in it and set to the configuration that was tested. Deleting the file, or any line in it, is safe. Every key is also listed in Docs/INI-REFERENCE.md, including the[Harbinger]section'sbSceneProbe. The key list below is the short version.APMF.esl, at the root, besideSKSE.
APMF.esl is new in v0.9.5 and it has to be enabled in your load order like any
other plugin. It's ESL flagged, so it takes no regular load-order slot (it uses one of
the 4096 light slots), it has one master (Skyrim.esm), it overrides nothing, and it's
about 2.5 KB. It carries the travel packages the travel facet (ch.19) hands the engine,
and it is generated from APMF_GenerateESL.py rather than hand-authored.
If it's missing or disabled, APMF refuses every kIntent_Travel claim and names the
reason once in the log, so a client's own degrade path runs. Nothing else breaks. Every
other facet behaves exactly the same without it.
Every key is in Docs/INI-REFERENCE.md. These are the ones added or changed from v0.13.1 on, with the default each has in code and in the shipped APMF.ini. Set a key to 0 to switch that part off, and delete the file or any line to get the defaults.
| Section | Key | Default | What it does |
|---|---|---|---|
[EquipAuthority] |
bEquipObserveOnly |
0 | 0 enforces every declared equip category (weapons, shields, torches, ammo, armor). 1 only logs what would be refused. bEquipEnforceArmor is retired and ignored |
[EquipAuthority] |
bTorchDeny |
1 | While a mod's equip claim owns the torch, the game's own torch logic is off for that follower |
[HandBlock] |
bEquipmentWeaponAdmission |
1 | The weapon rule at the combat equip seat (ch.15) |
[HandBlock] |
bEquipmentWeaponObserveOnly |
0 | 1 makes the weapon rule log only |
[CombatSubstrate] |
bCombatSubstrate |
1 | The combat substrate (ch.27). 0 refuses every substrate claim |
[CombatSubstrate] |
bSubstrateMovementHold |
1 | Hold the movement half of the follower's combat tree instead of failing it. A hold that did not arm refuses the substrate |
[CombatSubstrate] |
fSubstrateTtlSec |
6 | How long a substrate stays open, counted from the tree's first run (1 to 15) |
[CombatSubstrate] |
bSubstrateEquip |
1 | The game's own AI equips the claimed spell. Weapon, shield and torch are stowed first and put back as the same copies |
[CombatSubstrate] |
bSubstrateOverrideDeny |
1 | Deny the enter-combat override package while the follower is held. If its address check fails the substrate is refused |
[CombatSubstrate] |
bSubstrateMusicDeny, bSubstrateBarkDeny, bSubstrateAggressorClear, bSubstrateJoinGuard |
1 | Battle music, the combat-start bark, the aggressor bit, and a no-target join into a substrate group |
[CombatSubstrate] |
bSubstrateBitReassert |
0 | Built, off until it is field proven |
[CombatSubstrate] |
bSubstrateBystanderCensus, bSubstrateEntryTrace, bSubstrateProbes |
1 | Passive log lines only |
The facet priorities (EquipWeapon, EquipArmor and the rest) live in APMF_Sources.ini, written by the Harbinger page. A priority of 0 turns that mod off for that facet.
- A Papyrus call ranks as the plugin that owns its script, 20 by default, with
iPapyrusOwnerFilechoosing the winning override (0) or the defining plugin (1). A vanilla quest script's equip is refused by default (Skyrim.esm at 20). - The menu says what each column counts: Uncontested, Contested: blocked, Contested: won and Own. A client's own cast start and agreed target pick count in Own.
- A follower's out of combat cast no longer makes the town panic. A Spectator package that watches a held substrate holder is refused.
- The cast guard can hold for bystanders (ABI v32,
kCastGuard_Bystanders). Bow, crossbow, melee swing and Self area holds for actors that are not hostile, not the player and not on the party's side. It is an opt-in bit and adds no slot. A client checksabiVersion >= 32first. - The attack probe, ch.7 and ch.23 accept only a real actor, which fixes a field crash with
bAttackSelect=1. - ABI v32. Older mods keep working.
- A claimed spell the game scores as zero for its own data reasons is lifted. Candlelight in Requiem has a magnitude of 0 and a style can give a category a 0 multiplier, so the game never picked such a spell even when a mod claimed it. Harbinger now proves the zero from the row terms and lifts it, and a tie of zeros is classified by the spell's main (costliest) effect. This comes with the two Light overrides below, and together they let followers cast Candlelight and other Light spells animated, in lit towns and outside combat. Field tested on LoreRim.
- A claimed Light spell equips in lit places and while the group is not searching. The game only lights the caster in the dark, and only while the combat group is searching. Harbinger answers both tests for a claim's driven Light spell and leaves the other checks (fleeing, cost, already lit) to the game. Not on 1.7.104.
- A follower that cannot reach its target drops the fight, and a mod can ask why a claim ended (ABI v31,
GetClaimEndReason). Engaging the same target again restarts the watchdogs. - A probe names the script behind a Papyrus equip (
bPapyrusCaller, off by default). The combat behaviour tree catalog is now clean-room. - ABI v31. Older mods keep working.
- The substrate closes as fired only when the spell is cast. A close on the start of the release cut casts short. It now waits for the game's spell cast event, and a release that never confirms closes loudly. New passive lines
LEAF GATE,GATE UNRESOLVEDandCAST BUILT. - The cast proxy carries the spell's name and menu icon. A buff cast through the substrate shows its real name instead of an unnamed effect. The proxy keeps the slot the game gives a new spell, either hand.
- A travel walk that ends short is sent again, up to 3 times, on a fresh package slot when the destination has moved. After that the leg reads
kLeg_StuckTimeoutat once. - A blocked or stuck walk keeps the follower on the travel package while the mod's claim stands. The state reads the same as before. Only the mod's
ReleaseorRepointends the hold. - What a client sees from the substrate: Harbinger releases the substrate claim when the window closes,
GetSubstrateStategoes back to none, and the close reason (fired,fired-not-released,released-unconfirmed,tree-outside-gate) is inAPMF.log. The API did not change. - No ABI change, still ABI v30.
- Travel across cells (ABI v30). The
kTravel_CrossCellsflag on a travel claim to a reference makes the follower walk through doors to that NPC, the game's own way, and follow them if they move. It arrives in the same interior cell or the same outdoor worldspace and inside the radius, then is held until the mod releases it. Without the flag nothing changes. - Client facet priority (ABI v29).
GetClientFacetPriorityreturns the calling mod's own priority on a facet. 0 means off. - The combat substrate holds the movement half of the follower's combat tree instead of failing it, and its time limit runs from the tree's start.
bSubstrateMovementHoldandfSubstrateTtlSecin[CombatSubstrate]. A passiveCLAIM SCOREline shows what the game scored the claimed spell. - Needs ABI v30 for the new calls. Older clients keep working.
- Place (ABI v28).
kIntent_Placemoves an NPC beside a reference or to a point in an interior cell, once, through the game's own move call.GetPlaceStatesays what happened. - Equip enforced for every declared category by default, and an equip claim can name one copy of an item (ABI v25).
bEquipObserveOnlynow defaults to 0 andbEquipEnforceArmoris retired. - Equip is two facets on the Harbinger page and in
APMF_Sources.ini:EquipWeapon(weapons, shields, torches, ammo) andEquipArmor. An old[Equip]section is still read where the new ones have no value. - The Harbinger page has honest counters. New Blocked, Passed and Observed columns beside Served and Hits, and the winner is the highest priority source active in the last 60 seconds. See Docs/HARBINGER-MENU.md.
- Package priorities are enforced, and a mod's queued equip is ranked once as the mod that asked.
- Weapon rule (ch.15) at the combat equip seat, with a stand down when the claimed weapon is not one the AI could pick. Torch seat (ch.17) switches off the game's own torch logic while a claim owns the torch.
bEquipmentWeaponAdmission,bTorchDenyinAPMF.ini. - Travel (ch.19). A leg to a cell arrives by parent cell and holds the actor until the mod releases or re-points it. A re-point takes a fresh package slot. A passive DONE-WHILE-WALKING line names a walk that ended early.
- The combat substrate (ch.27) installs on 1.7.104, lets the game's AI equip the claimed spell, denies the enter-combat override package, battle music and barks, and reports its state with
GetSubstrateState(ABI v26) andIsSubstrateAvailable(ABI v27).[CombatSubstrate]inAPMF.ini. - Needs ABI v28 for the new calls. Older clients keep working.
- Held engagements. A fight a mod starts or pins is held until the target is dead, disabled or unloaded, or the mod lets go. The follower chases the target the game's way, walks to a downed one, walks back from the edge of its leash, and gives up with a logged anomaly if it makes no progress.
[EngagementHold]and[PursuitLeash]inAPMF.ini. - The combat substrate (ABI v23, on by default). A mod claims
kIntent_CombatSubstratebeside its cast claim and the follower casts its spell animated, out of a real fight, against an invisible stand-in.[CombatSubstrate]inAPMF.ini. - Idle state (ABI v24).
GetIdleStatesays what became of an idle claim's play. - Equip preference (ABI v22). A mod names the weapons, shields, torches or staves it prefers in combat and Harbinger lifts them in the game's own scoring.
- Equip refusals follow the priorities, and heals look past friends.
- Needs ABI v24 for the new calls. Older clients keep working.
- Harbinger names who changes the AI, in more places. Combat entry, combat actions, combat approach, spell choice and cast claims now say whose answer they replaced, and a claim stands only while it outranks that source in
APMF_Sources.ini. With the default priorities nothing changes. - Unknown sources checkbox. Each facet has "Unknown sources can be overridden" on the Harbinger page. Off by default.
[UnknownSources]inAPMF_Sources.ini. - Spectator seat. A retreating follower no longer stops to watch the fight while a mod's package claim outranks the engine.
iSpectatorSeatin[PackageGate]ofAPMF.ini. - Actor value watch. Harbinger names who changes a follower's speed, disposition, sneak noise and detection range values. Watch only.
iActorValueSeatin[ActorValueObserve]. - Loaded plugin list. The Harbinger page lists every loaded SKSE plugin, including ones that have not written anything yet.
- No ABI change. Still ABI v21.
- Cast guard (ch.25, ABI v21). A mod can ask Harbinger to hold an NPC's attack spell while a friend stands in the line of fire or the blast, and to keep the NPC's own concentration spells off one hand or both. Harbinger measures the line every frame.
Docs/FACETS.md, "ch.25 Cast guard". - The controller (Phase 1a). Harbinger keeps a registry of every source that writes an AI facet: client mods, other mods' hook DLLs, plugins, the engine, Papyrus and console calls. Each has a priority from 0 to 100 per facet, set in
APMF_Sources.ini. Defaults are 20 for other mods, 50 for clients and 75 for direct calls. A change Harbinger cannot trace is allowed and alerted, never denied.Docs/FACETS.md, "Basis and priority". - Target restore. When a named source is outranked on the combat target, Harbinger puts the target back through the engine's own call.
- Seat census. Every engine seat Harbinger hooks is re checked at each load stage and every 60 seconds, so a mod that hooks over it later is named in the log and on the menu.
- Equip caller recovery. The mod behind an engine equip is found by a stack scan. An equip Harbinger cannot attribute is never denied.
- The Harbinger menu page. A page in the SKSE Menu Framework shows the sources per facet, the winner, alerts and the census, and edits priorities live.
Docs/HARBINGER-MENU.md. - Armor enforced for clients.
bEquipEnforceArmor=1by default: an outfit or the game can no longer replace the armor a client chose, and a stripped follower gets it back within seconds. (From v0.14.0 every category is enforced and this key is retired.) - Own line of sight from the eyes, a heal approach goal kept inside the pursuit leash, and a disarmed actor's weapon search left alone by the approach bound.
Phase 1 built, now at v0.16.0, ABI v32 (see Docs/STATUS.md). The central
Actor::Update (0xAD) hook drives a multi-NPC control map (keyed by FormID,
scales to hundreds, an uncontrolled NPC pays one hash-miss), a real
inter-plugin C-ABI client API (native/APMF_API.h), and the full documented
channel catalog (movement full-block, disposition, casting selection,
headtrack, combat-target steer, equipment, gait, detection, and more) as a
first-release baseline. Persisted actor-value overrides are co-saved, so
nothing strands across a save/load.
This is a framework for other mods to build on, not a standalone gameplay mod. MFO (marth's Follower Overhaul) is the worked example, the first real client integrating against it.
MIT. See LICENSE.