Harbinger (APMF) v0.9.1 -- The NPC's own AI performs a client's cast
Pre-release
⚠️ KNOWN ISSUE IN 0.9.1 — a follower can stop fightingWhile a cast claim is live, the equip deny is too broad: it denies every other spell and staff the
actor owns, on both hands, for the whole life of the claim. A follower being healed repeatedly can
therefore be stripped of their own attack spells and appear to freeze or stand idle.➡️ FIXED IN v0.9.2 — use that instead of this build.
If you must stay on 0.9.1, the workarounds are:
1. Turn the deny off — in
Data/SKSE/Plugins/APMF.ini:[EquipGate] EnableEquipDenyComplete=0The follower behaves normally again. Note that animated casting stops working, and because the claim
still succeeds the client may not fall back to its own casting, so casts may simply not happen.2. Stop the client from claiming at all — the cleanest fully-playable state. With MFO, turn
bHealAnimPackageoff in the MCM and run MFO v2.0.1. Instant heals work as before and the
follower fights normally.The whole cast path can also be disabled outright with
[CastSeats] EnableSeat0Classify=0.
API: ABI v5 (APMF_API_v5), intent kIntent_Cast (ch.8b), call RequestCast
📘 API guide -- Docs/INTEGRATION.md -- start here. See "Making an NPC cast (ABI v5)".
📄 APMF_API.h -- the single header to copy into your plugin.
🗺️ Channel map -- every facet and its hook site.
Harbinger (APMF) is a per-facet AI arbitration layer for Skyrim NPCs. A client mod claims one facet of an actor, Harbinger routes that facet to the winner and denies everyone else, and the rest of the NPC keeps running normally. No package substitution, no frozen bodies.
What is new in 0.9.1
A client can ask Harbinger to make an NPC cast a chosen spell at a chosen target, and the NPC's OWN combat AI performs it. Harbinger makes no equip call, no animation call and no cast call. It answers the questions the engine's own cast logic asks, so the charge, the aim, the animation style and the channel are all the game's own.
This makes a heal-other cast possible for the first time. The vanilla combat AI cannot classify a healing spell aimed at someone else, so it never builds one as a candidate and never considers casting it. That is why followers have only ever healed themselves through the game's AI. Harbinger supplies the one classification decision the engine is missing and the engine does the rest.
Using it
Copy native/APMF_API.h from this repo into your plugin. It is a single header, byte-shared and append-only. Keep your copy identical to this one.
1. Get the interface once, after SKSE load
Harbinger exports one undecorated C function. Fetch it in kPostLoad or kDataLoaded and keep the pointer.
#include "APMF_API.h"
const APMF_API::APMF_API_v5* g_apmf = nullptr;
if (HMODULE h = GetModuleHandleA("APMF.dll")) {
auto fn = reinterpret_cast<APMF_API::GetInterface_t>(
GetProcAddress(h, APMF_API::kGetInterfaceExport)); // "APMF_GetInterface"
if (fn) {
if (auto* base = fn(APMF_API::kABIVersion)) { // nullptr on ABI mismatch
if (base->abiVersion >= 5) // RequestCast is a v5 slot
g_apmf = reinterpret_cast<const APMF_API::APMF_API_v5*>(base);
}
}
}g_apmf null means Harbinger is absent or too old. Guard every call and fall back to whatever you did before. Never read past the end of an older interface struct.
2. Claim the cast
APMF_API::APMF_CastRequest req{};
req.spell = spellFormID; // the spell you want cast
req.proxy = 0; // 0 = let APMF mint a delivery-flip proxy if the spell is Self-delivery
req.target = targetActorID; // 0 = self. Load-bearing: this is where the cast lands.
req.flags = APMF_API::kCastFlag_LeftHand // put it in the left hand
| APMF_API::kCastFlag_Concentration // set for a held/channelled stream
| APMF_API::MakeStopPct(80); // stop a channel at 80% of the target's AV
req.ttlMs = 4000;
APMF_API::Handle h = g_apmf->RequestCast(actorFormID, basis, &req);
if (h == APMF_API::kInvalidHandle) {
// lost arbitration, or the cast channel is not registered -> do your own thing
}req is copied synchronously inside the call, so a stack temporary is fine. RequestCast is safe to call from any thread.
3. Hold it, then release it
The claim is always bounded. ttlMs = 0 means the 4000 ms default, and any value is clamped to the maximum, so a crashed or forgetful client can never leave an NPC stuck. Re-request while you still want the cast, and call g_apmf->Release(h) as soon as you do not.
Field notes
- Hand policy. If the NPC is holding a weapon from another source, put the spell in the left hand. Contesting the weapon hand means the other source re-equips over you a moment later and the cast dies.
- Self-delivery spells. A
kSelfheal applied at an ally lands on the caster instead. Leaveproxy = 0and Harbinger mints a delivery-flipped copy for you. stopPctis optional. Left at 0 a channel stops at full restoration. Without it the engine stops where its own combat style says, which for a heal is roughly a quarter-second pulse.- Failure is not masked. If the engine refuses the cast (no magicka, hand busy, spell unknown) nothing happens and nothing is faked. That is deliberate. Check
APMF.log.
Requirements and limits
- Skyrim AE 1.6.1170. The cast path refuses to install on other runtimes and on VR rather than guessing at offsets.
- Kill switches in
Data/SKSE/Plugins/APMF.ini:[CastSeats] EnableSeat0Classify,[EquipGate] EnableEquipDenyComplete. - Beta. Field-proven for heal-other on a follower. Offense casts through this path are still being ported in the reference client.
- Reference client: MFO v2.0.1. Broader framework docs:
Docs/INTEGRATION.md,Docs/CHANNEL-MAP.md,design.md.