Skip to content

Releases: marthofdoom/APMF

v0.9.10 -- Keep a follower out of the fight, leash it, and play an animation at a target

Choose a tag to compare

@marthofdoom marthofdoom released this 29 Sep 20:03
  • A mod can now keep an NPC out of combat for a while. Claim kIntent_CombatReentryDeny with a number of seconds in APMF_Param.fval (0 means 10, at most 120). While that time runs, the game cannot start a fight for it: when it tries (the NPC sees an enemy, gets hit, an ally is fighting, a script asks), the game's own combat start says no, before it draws a weapon or does anything else.
  • It is the other half of a retreat. The mod stops the fight once with the game's own StopCombat, and the game can no longer pull the NPC straight back in. Harbinger never stops a fight itself. An NPC that is already fighting keeps fighting, but the game cannot add new foes to its fight while the time runs.
  • A mod's own combat entry still works. A kIntent_CombatEntry request for the NPC goes through. When that fight ends, the rest of the window keeps the NPC out again.
  • It ends by itself when the time runs out or the NPC dies, and the log says which. Nothing is undone because nothing was written. The time counts from the mod's own request, so a request that takes over from another mod's gets only what is left of its own time.
  • Harbinger checks its own work. The first time the game starts any fight, the log says the check was seen. If an NPC gets into combat while its window runs anyway, the log says DENY MISSED.
  • [CombatReentryDeny] bCombatReentryDeny is new in APMF.ini, default 1. Set it to 0 and every request is refused.
  • New API revision (v15). It adds the intent and nothing else. Check abiVersion >= 15 before asking for it. An older Harbinger refuses the request. A mod built against an older revision is unaffected.
  • Both runtimes. The spot in the game's combat start was read on the 1.6.1170 and 1.5.97 binaries and is byte-checked at startup. Any other build is refused by name. VR is refused. The startup check now covers 179 addresses.
  • Ran in the 2026-09-28 field session with MFO's retreat. The seat was seen at startup, every deny was logged as refused, and no deny was missed.
  • A mod can now leash an NPC in combat. Claim kIntent_PursuitLeash with an anchor actor in APMF_Param.target (usually the player) and a radius in APMF_Param.fval. While the NPC is farther than the radius from the anchor, the game's own moves toward its target (closing in, chasing, flanking, repositioning, a ranged NPC closing to range) and around its search area when the target is lost are refused when that goal is farther from the anchor still.
  • A chase that already started is stopped too. The game runs a whole chase inside one move, so Harbinger also ends a running move once the NPC crosses the radius, the same way the game ends one whose path failed.
  • The NPC keeps fighting. Attacks, spells, blocks, dodges and every other move run as the game decides. A goal nearer the anchor is always allowed. Harbinger does not walk the NPC back. Past the radius a step back toward the anchor can be refused too, so the NPC may stand where it is.
  • Its own claim. The leash does not share a claim with the combat-action deny, so another mod's attack or cast deny on the same NPC does not replace it.
  • Two moves are not leashed: the game pulling the NPC back into the fight's area, and a search walking to a door. The log and the docs say why.
  • It ends with the claim: release, a save load, a new game, or the NPC unloading. An anchor that is dead, unloaded or in another cell holds nothing back.
  • [PursuitLeash] bPursuitLeash is new in APMF.ini, default 1. Set it to 0 and every leash request is refused.
  • New API revision (v16). It adds the intent and nothing else. Check abiVersion >= 16 before asking for it. An older Harbinger refuses the request. A mod built against an older revision is unaffected. An earlier draft of v16 carried the leash as a combat-action bit, kCombatActionCat_Pursuit. It was never released and is gone from the header.
  • Both runtimes. The fourteen moves and the game's own way of ending one were read on the 1.6.1170 and 1.5.97 binaries and are checked at startup. The startup check now covers 185 addresses.
  • Ran in the 2026-09-28 field session with MFO. Leashes were set and released, and the game's own moves past the radius were refused.
  • A mod can now play a chosen animation at a target. Claim kIntent_Idle with an IDLE record in APMF_Param.form and, if it needs one, the reference to play it at in APMF_Param.target. Harbinger plays it once with the game's own call, the same one the game's scripts use. A follower can pick a lock with IdleLockPick or hand something over with IdleGive.
  • Harbinger plays it and lets go. One play per request. A Repoint with an idle plays it again. The game checks the idle's own conditions and can say no. Then the request ends and the log says why.
  • Release only resets a pose that is still held. A one-shot animation ends by itself. Release sends the reset only when the animation has not ended, so it never pulls the NPC out of whatever it is doing by then. It is never sent to an NPC sitting or lying in furniture.
  • The log says whether the animation really played. Harbinger reads the NPC's behaviour graph at the call and counts a play only when the graph raises one of that idle's own entry events. None within 3 seconds of game time ends the request, and the mod sees the idle did not play. An idle that cannot play on that NPC's graph at all ends it at once. Where Harbinger cannot tell from the graph, the log says UNCONFIRMABLE and the request stays.
  • A crouching NPC stands for the animation. The game has one standing lockpick clip and no kneeling one. The NPC goes back to sneaking when it ends.
  • It ends by itself when the animation cannot be played, when the game refuses it, or when the NPC dies. The log says which. Release, a save load, a new game or the NPC unloading end it too.
  • The old form-free idle (no form) is unchanged.
  • [Idle] bIdleV2 is new in APMF.ini, default 1. Set it to 0 and every request that names an idle is refused.
  • [Idle] bIdleConfirm is new in APMF.ini, default 1. Set it to 0 and Harbinger skips the graph read: every play is logged UNCONFIRMABLE and none is ended for not playing.
  • New API revision (v17). It adds the form and target to the idle and nothing else. Check abiVersion >= 17 before setting a form. An older Harbinger does not refuse it: it plays its old default-pose reset instead.
  • Both runtimes. The game's play call was read on the 1.6.1170 and 1.5.97 binaries and is checked at startup. The startup check now covers 186 addresses.
  • The idle itself ran in the 2026-09-28 field session (IdleGive played and was confirmed). That session also showed the old check calling a lockpick confirmed on unrelated combat events when it never played. The entry-event check above is the fix. It is CI verified only.
  • Harbinger keeps its last five logs. At launch APMF.log moves to APMF.log.1, the older ones move up to APMF.log.5 and the oldest is dropped. Before, every launch wiped the log. A crash or freeze session's log now survives a few relaunches. A log that cannot be moved is noted in the new log and startup goes on. Seen working in the field.
  • A second passive gate probe. When a walk ends blocked, Harbinger now also logs the navmesh under the spot and under the destination, and the doors, activators and obstacles along the straight line between them, with their animation state and collision. It changes nothing. [Travel] bGateProbe2 is new in APMF.ini, default 0 (off). Turn it on only for a gate field test. It ran in the 2026-09-28 field session.
  • A passive probe for the bow a melee follower keeps picking. For an NPC whose equipment a mod governs, Harbinger now logs how the game's combat AI admits a bow or crossbow, what its equip steps try to equip, whether it can reach its target, and which combat moves run while the mod owns the hands. It changes nothing. [Probe] bRangedSelect is new in APMF.ini, default 0 (off). Turn it on only for a field test. Not field-run yet.

v0.9.9 -- Start a fight and pin the target

Choose a tag to compare

@marthofdoom marthofdoom released this 25 Sep 21:38
  • A mod can now start a fight. Claim kIntent_CombatEntry with the target actor in APMF_Param.form, and Harbinger asks the game once to put the NPC into combat against that target, with the game's own call for starting a fight. The game can say no (a restrained or dead NPC, a target too far away). Then the request ends and the log says why.
  • Harbinger starts it and then lets go. It never starts the fight a second time, and releasing the claim does not stop the fight. The game ends it the way it ends any fight. A mod that wants it over stops it itself.
  • A request never hangs around doing nothing. It ends by itself when the game refuses, when the fight is over, when the target dies, is disabled or unloads, or when the NPC dies. The log says which. To try again, a mod sends a new request.
  • Start the fight and pin the target to make the NPC fight that one. Starting a fight puts the target on the NPC's list of foes. Pinning the same target once the fight has started makes the NPC choose it from that list.
  • What the world does about the fight is the mod's business: crime, guards, allies joining, the music. Harbinger does not stop it and does not undo it.
  • [CombatEntry] bCombatEntry is new in APMF.ini, default 1. Set it to 0 and every request is refused.
  • New API revision (v14). It adds the intent and nothing else. Check abiVersion >= 14 before asking for it. An older Harbinger refuses the request. A mod built against an older revision is unaffected.
  • Both runtimes. The game's call was read on the 1.6.1170 and 1.5.97 binaries, all three of its arguments included, and any other build is refused by name. VR is refused. The startup check now covers 178 addresses.
  • A mod can now pin an NPC's combat target. Claim kIntent_TargetPin with the target actor in APMF_Param.form. While the NPC is fighting, the game asks who it should fight, and Harbinger answers with the mod's target. The game's own pick never reaches the NPC. The NPC's own AI does the fighting.
  • Only among the game's own combat targets. The target must already be one the NPC's side is fighting and can still find. If it is not in the fight yet, nothing changes and the log says so. Harbinger never makes anyone a target.
  • It never starts a fight. If the game has no target for the NPC, Harbinger writes nothing and the pin waits.
  • A pin ends when Harbinger can no longer track the target. If the target is lost, dies, is disabled or unloads, or the NPC itself dies, Harbinger drops the pin and the log says why. The game picks its own targets again. A mod that wants to keep chasing pins again.
  • It pins the target and nothing else. No attacks, spells, equips, movement or aggression are touched. What the world does about the fight is the mod's business, and releasing the pin does not undo it.
  • Harbinger checks its own work. If the NPC ends a combat update aimed at someone else and Harbinger was never asked, the log says so. If something changes the target after Harbinger answered, the log says that too. Another mod that writes the target after the game's update still wins.
  • [TargetPin] bTargetPin is new in APMF.ini, default 1. Set it to 0 and every pin request is refused.
  • New API revision (v13). It adds the intent and nothing else. Check abiVersion >= 13 before asking for it. An older Harbinger refuses the request. A mod built against an older revision is unaffected.
  • Both runtimes. The three hooked spots were read on the 1.6.1170 and 1.5.97 binaries, and any other build is refused by name. VR is refused.
  • Not field-run yet. CI verified only.

v0.9.8 -- Travel to a point, blocked legs, gait, and a startup address check

Choose a tag to compare

@marthofdoom marthofdoom released this 25 Sep 02:27
  • Casting at a point ships switched off. The follower does not animate when Harbinger casts from a marker, and every action needs a proper animation. So this is a building block for an animated version, not a finished feature, and no mod should ship on it alone. bPositionCast is 0 and every request is refused with that reason in the log. Set it to 1 to try it. Walking a follower to a point does not depend on it.
  • A mod can now cast a spell at a spot on the ground. Send a cast request with kCastFlag_AtPosition and a point in APMF_Param.pos, and Harbinger drops an invisible marker there and casts the spell from it in the follower's name. The follower does not animate and its hands are not touched. The marker is deleted a frame later. It only works for spells that apply their effect at the spot directly. A spell that launches a projectile (a rune, a trap, a lobbed shot) is refused by name, because the game only places that kind of projectile from the player's crosshair.
  • Why the marker casts and not the follower. The game places a Target Location spell cast by an NPC at that NPC's own hand. It never looks at the target it was given, on either runtime, so telling a follower to cast at a marker lands the spell at the follower. A caster that is the marker lands at the marker. The call is the same one the game's own Papyrus Spell.RemoteCast makes.
  • Summons are refused, by name, and that is the game's rule. The game only ever applies a summon to the actor that cast it, so a marker cannot summon anything. An NPC's own summon already lands in front of it, because the game picks that spot itself. Self, Touch, Aimed and Target Actor spells are refused too (use an ordinary cast request with an actor target), and so are concentration spells and the three spell types RemoteCast refuses. Every refusal is one line in the log saying why.
  • A position cast never casts over the owner of the follower's casting. If another cast claim on that follower outranks the request, the request is refused on the spot and the log names the claim. A wrong spell is refused when the cast is about to happen, with the reason in the log. The marker goes in the cell that holds the point, so a point just across a cell border outdoors works, and a point in a cell that is not loaded is refused by name.
  • A position cast is not a claim. It never enters the control map, so none of the cast seats can ever read it as an actor target, and it holds no facet. The handle it returns is a label for the log. IsClaimLive is false for it and Release does nothing. Harbinger charges no magicka for it. That is the mod's call.
  • A mod can now send a follower to a spot on the ground. Travel takes a point as well as an object or a cell. Set kTravel_ToPosition, leave the form empty and put the point in APMF_Param.pos. Harbinger places an invisible marker there and the follower walks to it the same way it walks to anything else: same stopping distance, combat still cancels it, and it still gives up after two minutes of getting nowhere. That is the game's own way to say "go here", since a package cannot hold coordinates.
  • The marker lives exactly as long as the walk. It is deleted when the walk ends for any reason: arrival, combat, release, a new destination, getting stuck, or the follower unloading. Point the walk somewhere new and the old marker goes once the new one is in place. Across a load the walk is not restored (it never was), and the package is pointed back at its default so it cannot aim at a marker from the old world.
  • A save never keeps Harbinger's markers. Harbinger writes down every marker it has placed and not yet deleted, in its own co-save. Load a save made mid-walk or mid-cast and those markers are deleted as the game finishes loading. Only markers the record proves are Harbinger's are touched: the right ID, still an XMarker, standing exactly where it was placed. Anything else is left alone. A marker whose area is not loaded at that moment stays on the list and goes on a later load. A save from 0.9.7 or older has nothing to clean up.
  • Two new questions a mod can ask. They answer and change nothing. FindEmptySpace walks a distance from an actor or a point along a heading and returns a spot with the clearance asked for, snapped to the ground. It refuses a spot behind a wall, over a drop, on top of someone's head or inside a crowd, and says which. FindHostilesInSpace lists the live actors inside a sphere that the game itself considers hostile to a given actor, nearest first, capped at 64. The hostility test is the game's own, the one Papyrus IsHostileToActor calls. There is no faction list in Harbinger.
  • The queries run on the main thread only. Called from anywhere else they do nothing and say so. MFO's main-thread pump runs on that thread, so it calls straight in. SKSE's AddTask is not that thread.
  • [PositionCast] bPositionCast is new in APMF.ini, default 0 (off). Set it to 1 to allow position casts.
  • New API revision (v11). It adds FindEmptySpace and FindHostilesInSpace, their structs, and the kCastFlag_AtPosition and kTravel_ToPosition bits. Check abiVersion >= 11 before calling either query or setting either bit. An older Harbinger ignores the cast bit and makes an ordinary cast claim, and it refuses a travel request with no form. A mod built against an older revision is unaffected.
  • Both runtimes. Every call these paths make was read on the 1.6.1170 and 1.5.97 binaries before it was used, and both features refuse any other build by name. VR is refused.
  • Looking up a form off the main thread is safe now. A mod can call Harbinger from any thread, and several paths look up forms by ID. The library underneath read the game's list of forms without taking its lock. If the game added a form at the same moment (a spawned actor, a placed marker), the read could land on memory the game had just moved and crash, or come back empty. It now holds the game's own lock for the lookup, the same way the game does. Nothing else changes. A lookup only waits while the game is in the middle of adding or removing a form.
  • A walk now ends when the follower is stuck behind something. When the game cannot move an actor along its path, it puts the actor in its own "Movement Blocked" package. A closed lever gate held two followers there for 40 seconds in the field, then the walk gave up after two minutes. Now the walk ends after 3 seconds of Movement Blocked, and the log says where the follower stopped. Only time the game is running counts, so a menu open in the middle cannot end a walk that was only bumping. Short bumps of about a second, which healthy walks have, do not end it, and a one-poll flicker does not restart the count. Harbinger does not fight the game's answer. It stops and lets the mod decide.
  • Harbinger says whether a person was in the way. When a walk ends blocked, Harbinger looks for a live actor right in front of the follower: the player, another follower, or anyone else. The mod gets that actor and its kind. Someone in the way will probably move, so a looter should take another item and come back. Nobody in the way means a closed gate or a wall, and the item should wait until a door or lever changes something. No timer.
  • A mod can now ask why a walk stopped. Harbinger ends a walk but never drops the mod's claim, so a stopped walk looked the same as a package another mod took away. GetTravelLegState answers for an actor: walking, arrived, blocked (with the spot and whoever stood in front), combat, destination gone, stuck, actor gone, failed or released. It also names the claim the walk belongs to, so a mod can tell its own walk from another mod's. It works from any thread and changes nothing.
  • A refused new destination now stops the old walk. If a mod points a walk at something Harbinger cannot walk to, the follower used to keep walking to the old place with nothing to say so. Now the old walk ends and the state reads failed, naming the refused destination.
  • A mod can now pick the walking speed. Set kTravel_SpeedSet and one of Walk, Jog, Run or FastWalk. Harbinger writes it into the walk's package with the flag the game needs before it uses the speed at all. The game reads the speed when the package starts, so a new walk uses it. Changing the speed of a walk already under way takes effect when the package next starts, and the log says so. Without the bit the walk runs, as before.
  • A passive gate probe. When a walk ends blocked, Harbinger logs the doors and activators near the spot, with their flags and the game's own open or closed state. For the next ten minutes it logs open and activate events near that spot, and reads the gate's state again right after and 4 seconds later. It only writes log lines. This is how the next version will learn what a closed gate looks like to the game.
  • New API revision (v12). It adds GetTravelLegState, the APMF_TravelLegInfo struct and the gait bits. Check abiVersion >= 12 before calling it or setting a gait bit. An older Harbinger ignores the gait bits and walks at its own speed without a word. A mod built against an older revision is unaffected.
  • Built on CommonLib mit-3.7 F1b (registry 3.7.0#4). The startup self-check now also verifies the two lock functions on both runtimes, and the two calls the gate probe makes. The log line reads 175/175.
  • Not field-run. CI verified only.

Harbinger (APMF) v0.9.7 -- Travel walks to a body

Choose a tag to compare

@marthofdoom marthofdoom released this 23 Sep 16:17

Beta. Skyrim AE 1.6.1170 or SE 1.5.97. Pairs with MFO v2.0.12.

The archive carries three files: SKSE/Plugins/APMF.dll, SKSE/Plugins/APMF.ini, and APMF.esl (light plugin, must be enabled).

  • A follower sent to a corpse now gets there. Travel ended the walk the moment it saw the destination was dead. A looter's destination is always a body, so almost every loot walk ended a fraction of a second after it started. Now a target that was already dead when it was picked is just a place to walk to. A target that was alive when it was picked and dies on the way still ends the walk.
  • A walk still ends on arrival, on the follower entering combat, or on the destination being deleted or disabled. No API change.

Build provenance

  • APMF.dll sha256 8f3f8c362c0b83c73e25613fa32af530779a7ef975385dd8a55e536fea9faae6

Harbinger (APMF) v0.9.6 -- Travel just works

Choose a tag to compare

@marthofdoom marthofdoom released this 23 Sep 06:32

Beta. Skyrim AE 1.6.1170 or SE 1.5.97. Pairs with MFO v2.0.11.

The archive carries three files: SKSE/Plugins/APMF.dll, SKSE/Plugins/APMF.ini, and APMF.esl (light plugin, must be enabled).

  • Travel moves the follower the moment a mod asks. v0.9.5 shipped travel in an observe mode that logged what it would do and changed nothing. That mode is gone. Harbinger takes commands, so the way to not move a follower is to not ask. A mod that asks gets a follower who walks.
  • A claim is never accepted and then ignored. In observe mode a mod got a live claim back and no behaviour, with no way to tell. Now an accepted claim always means the follower is walking. A refused one says why in the log.
  • bTravelObserveOnly is removed from APMF.ini. bTravel stays, and turning it off refuses travel claims outright.
  • No change to any other facet. No API change.

Build provenance

  • CI run from commit 3479436
  • APMF.dll sha256 869497e49092ba806e8909a022fe16c710110539ee2521dc35af62ddfcf93029
  • APMF.esl sha256 78a3e6a44e5010eb6a1f0471afcb965d553e009cd5cfb532e4e294c483f7be62

Harbinger (APMF) v0.9.5 -- Tell a follower where to go and what to wear

Choose a tag to compare

@marthofdoom marthofdoom released this 22 Sep 20:11

Beta. Skyrim AE 1.6.1170 or SE 1.5.97. Pairs with MFO v2.0.8.

Requirements

  • Skyrim on Windows: Anniversary Edition 1.6.1170 or Special Edition 1.5.97. Any other build is refused by name, including a 1.6.x that is not 1170. VR is refused. 1.7.104 is not supported, there is no address library for it.
  • SKSE64 for that runtime.
  • Address Library for SKSE Plugins (the all in one file). Harbinger resolves engine addresses through it. Without it the plugin stops at load with an error box rather than running.

Install: three files now, not two

The archive root is your Data folder.

  • SKSE/Plugins/APMF.dll
  • SKSE/Plugins/APMF.ini
  • APMF.esl (new, and it has to be enabled)

APMF.esl is the first plugin Harbinger has ever shipped. It carries the eight travel packages the new travel facet hands to the game. It is light flagged so it takes no load order slot, it has one master, it overrides nothing and it is about 2.5 KB. If it is missing or disabled, travel refuses every request and says so once in the log. Nothing else in Harbinger depends on it.

Tell a follower where to go and what to wear

  • A mod can now tell Harbinger "walk this follower over there" and Harbinger does it. Move to a place is a staple, and before this there was no way to ask for it. A mod could claim the combat target facet, but that only records who owns it, and it could offer a package, but only one it shipped in its own plugin. A mod author who wanted to send followers at an enemy ended up calling the game's start-combat function on a timer, which makes a follower search and pace instead of charge, because the game will not path anyone to an enemy they have never seen.
  • What travel does, in full. The follower walks to the destination on real pathing, and Harbinger lets go the moment that follower is in combat. The destination is any object in the world, an enemy, a chest, a marker, anything, and the follower stops about 75 units short of it. A mod can ask for a different distance, between 50 and 512 units.
  • A destination can also be a whole cell. Name a cell instead of an object and the follower walks until they are in it, which is the natural way to say "go inside" or "get to that room". Distance does not apply to a cell, so the stopping distance is ignored for one.
  • The destination does not have to be nearby. A follower will path across cells to something they cannot see yet, the way the game's own travel packages do. Harbinger gives up after two minutes of getting nowhere and says so in the log rather than holding on silently.
  • A destination cannot be a bare set of coordinates, and that is the game's limit rather than a choice. An AI package's destination holds an object or a cell and no coordinates at all. The game's own answer, in every quest Bethesda shipped, is to place an invisible marker and point at the marker. Do the same. A mod that passes coordinates is told no, in the log, instead of having them quietly ignored.
  • What travel does not do, and this is the point. It does not claim the target, does not start a fight, does not hold anyone's attention on anything, and does not pretend anybody has seen anything. A mod that wants the combat target arbitrated asks for that separately, in its own request. One thing per request keeps them from fighting each other. Single calls that bundle several of these together are a good idea and will come later. They are not being ruled out.
  • Harbinger ships a plugin for the first time, and it has to be enabled. APMF.esl carries eight travel packages, one per follower walking somewhere at once. A package's destination lives on the package itself, so two followers walking to two different places need two of them. A ninth at the same time is refused and said so in the log rather than quietly sharing one and sending both to the same place. It is a light plugin, so it takes no load order slot, it has one master, it overrides nothing and it is about 2.5 KB. Without it this one feature is refused and says so. Nothing else in Harbinger depends on it.
  • A mod can now declare a follower's worn set and have it hold. The mod sends the list of items, with a hand for each one if it cares which. Harbinger equips them and refuses the equips the game tries against them: the outfit refresh, the AI's own weapon and armor picks, the re-equip after an item is removed. An item declared for the left hand that the follower holds in the right is moved, and the same weapon can be declared once per hand, so a follower can dual wield through Harbinger. It holds until the mod declares a new set or releases. A follower with no declaration is never touched, and the player is never touched.
  • A mod says which parts of the loadout it owns. Armor, shield, right hand, left hand, ammo, light. Harbinger holds the declared set in those categories and leaves the rest to the game, so a mod that only cares about the hands no longer has to declare the body armor and the arrows to stop the game's own refresh being refused for them. That was the first field run's one real failure. A category can also be denied outright: a follower who must never raise a shield gets the shield category denied, and nothing can equip one there.
  • One rule decides what an item competes for. A shield takes the shield and the left hand. A torch takes light and the left hand. A two hander, a bow or a crossbow takes both hands. A one hander takes the hand the game resolved it for. When the game has not picked a hand yet, it takes both. The same rule runs in the equip check and in the equip pass, so the two cannot disagree.
  • Potions, food, scrolls, ingredients and books are never refused. The equip check only governs armor, weapons, ammo and torches. A follower drinking a potion, or a mod telling one to, passes through untouched.
  • The player can still dress a follower by hand. An equip from the trade or gift menu passes by default, though the mod's next declaration puts back whatever it displaced unless the mod folds the player's choice into that declaration. A Papyrus or console equip passes too, and both checks run before the scope is consulted. A mod that wants the strict form can ask for the player's equips to be refused as well. Unequips are never refused in this version.
  • Bound weapons are equips. A follower's Bound Sword equips through the same check, so a mod that declares a worn set has to include the bound weapon or the follower will cast and hold nothing once enforcement is on.
  • A claim is refused when the equip hook is not installed. With the feature switched off in the ini, on VR, on an unsupported game version, or after a hook conflict, a mod asking for equip authority gets a refusal instead of a claim that does nothing. A mod that stops its own equips on a successful claim would otherwise have equipped nothing all session. The log says why once. One new call answers whether the check is enforcing or only watching.
  • This is the first call-site hook in Harbinger. Every engine equip goes through one internal function that has no virtual entry to hook, so the two places the game calls it are patched instead. Both are checked byte for byte before either is written. If any other mod has touched them the whole feature refuses to install and says so in the log. A conflict is a missing feature, never a crash.
  • The equip log names who is equipping. The first observe run on the Deck left two lines reading Unknown(39637), which turned out to be the AI command dispatcher equipping directly rather than through the two helpers the log already named. Every remaining caller of the equip worker was then checked against both unpacked game images and named: dropping an item, picking one up, a conjured bound weapon equipping itself, the eat package, the re-equip after ammo or pickup, and combat start. Two stay unnamed on purpose, one whose second route could not be pinned and one that nothing in the game references. Each equip check line also names the categories the item competes for, whether the mod owns any of them, whether any is denied, and the slot the game resolved.
  • The cast classify seat and the weapon score seat now run on Skyrim 1.5.97. Both used to refuse anything but 1.6.1170. Every value they read was checked against the 1.5.97 binary and its address library before being placed, and the weapon score seat resolves its four vtables through the address library on both runtimes instead of carrying hard 1.6.1170 addresses. Nothing was guessed.
  • The version gates are exact. Each seat opens on 1.6.1170 and 1.5.97 and refuses any other build by name. A 1.6 build other than 1170 is refused where it used to be admitted unverified. Skyrim 1.7.104 is still unsupported: there is no address library for it, so nothing was placed, and the game does not get that far anyway.
  • One line at startup states what this build placed, before any seat installs. [runtime] <version>: cast-classify open, group-C open, equip-sink open reads the same way for all three, and gated means the version predicate refused that seat. The [apmf][equip-sink] INSTALLED line is still the truth about whether that seat is live.
  • The log now names the Harbinger release a newer mod needs. When a mod asks for an API newer than the installed Harbinger, the refusal line states the installed version and the first release that would satisfy the mod, instead of only the two API numbers.
  • New API revision (v10). Four revisions ship together here: v7 SetEquipSet, v8 SetEquipSetEx and IsEquipAuthorityEnforced, v9 SetEquipScope, and v10 the travel intent. v10 adds no call and no struct change, the new intent rides the request path that was already there. A mod built against any older revision is unaffected.
  • Travel and equip authority both sh...
Read more

Harbinger (APMF) v0.9.4 -- Ships its settings file

Choose a tag to compare

@marthofdoom marthofdoom released this 10 Sep 00:06

Beta. Skyrim SE/AE 1.6.1170. Pairs with MFO v2.0.5.

Requirements

  • Skyrim Special Edition / AE 1.6.1170. Other runtimes are not supported yet.
  • SKSE64 for that runtime.
  • Address Library for SKSE Plugins (the all in one file). Harbinger resolves engine addresses through it. Without it the plugin stops at load with an error box rather than running.

No ESP or ESL. Harbinger is a DLL plus a settings file.

Ships its settings file

  • Data/SKSE/Plugins/APMF.ini is now included. The plugin has always read that file, but no release ever contained it, so every switch fell back to a built in default and there was no way to see what the switches were. Every key is now present and documented.
  • The spell score steer is on. It nudges the game's own scoring so a follower picks the spell a mod asked for instead of its own preference. It was held off until field data proved the underlying hook runs, and that data arrived on 2026-09-09. This is the configuration that was field tested.
  • Deleting the file, or any single line in it, is safe. The plugin falls back to the same built in default.

No code changed from v0.9.3. The DLL differs only by its version stamp.

Build provenance

  • commit 0b61290 · CI run 34418633607
  • APMF.dll sha256 1d05d8d7652c5ccd3f0f6242e0207f258252bf21a95b63263cfdc2430f4ba95b

Harbinger (APMF) v0.9.3 -- Package offers actually engage

Choose a tag to compare

@marthofdoom marthofdoom released this 09 Sep 18:15

Beta. Runs on Skyrim SE/AE 1.6.1170. Pairs with MFO v2.0.2 — deploy the pair together.

Requirements

  • Skyrim Special Edition / AE 1.6.1170. Other runtimes are not supported yet.
  • SKSE64 for that runtime.
  • Address Library for SKSE Plugins (the all in one file). Harbinger resolves engine addresses through it. Without it the plugin stops at load with an error box rather than running.

No ESP or ESL. Harbinger is a DLL only. There is nothing to enable and nothing to configure.

Harbinger lets a mod take control of one thing an NPC is doing without taking over the NPC. It works on any NPC, not just followers.

Package offers actually engage, and a cast claim can be renewed

  • A client's package offer now reaches the game. The nudge telling the engine to re-ask for a package was posted before the claim was published, so the engine asked while the claim was still invisible and kept the package it already had. Nothing a client offered ever ran. This is what was stopping follower loot travel.
  • A cast claim's deadline is a floor that can be renewed, not a hard expiry. A client can now hold a hand through a long cast instead of the claim dying mid-cast.
  • A client can claim a hand purely to deny it. Ships switched off, no client sets it yet.
  • The spell score steer now runs where spells are scored. It was installed on the weapon path, where it could not affect a spell.
  • Fixed a redirect answer remembered past the point it stopped being true.
  • Fixed a claim built from a package refusing its own client's heartbeat.
  • Fixed the winner of a hand being picked before checking that claim was still alive.

What is proven and what is not

Docs/INTEGRATION.md carries a per-facet table with a proof tier on every row. Field-proven facets include the cast facet, combat target (in active production use), combat-action denial, spell selection, gait, stance, disposition and detection. Movement block, weapon draw, dialogue, idle, shout and equipment are built but not battle-tested. Weapon-vs-weapon admission and re-aiming a cast mid-flight are not implemented.

Build provenance

  • commit f18f364 · CI run 34385885272
  • APMF.dll sha256 374a91aeb84786abcfd044ce8cca4f4ac3adf94d40faf448028e872a596724f5

Harbinger (APMF) v0.9.2 -- The equip deny no longer disarms the follower

Choose a tag to compare

@marthofdoom marthofdoom released this 06 Sep 07:22

ℹ️ NOT FIELD-TESTED

.

Fixes the v0.9.1 follower freeze

A follower could stop fighting. While a cast claim stood, the equip deny took every spell and staff the
actor owned, on both hands, for the whole life of the claim. A follower being healed repeatedly lost
their own attack spells and appeared to freeze. The deny is now limited to the claim's own hand, so the
other hand goes back to the NPC's own AI. The claimed spell still wins its slot, which is what makes the
cast work in the first place.

Offense casts now work through the same path as heals

The engine seats were installed on the Restore caster only, so a client claiming a hostile spell got
nothing at all -- the claim stood and no cast ever happened. They now cover the Offensive caster too, still
gated per call on the claim naming that exact actor and that exact spell.

Two safety fixes came with it. A hostile spell is no longer re-classified (the classification fix
exists only because the game cannot describe a heal aimed at someone else; applying it to a hostile spell
would move it to a row the game never uses and could break casting that already worked). And a latent
crash risk was closed: one seat read a value that exists only on the Restore caster's layout and would have
read garbage off the Offensive one.


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 kSelf heal applied at an ally lands on the caster instead. Leave proxy = 0 and Harbinger mints a delivery-flipped copy for you.
  • stopPct is 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.

Harbinger (APMF) v0.9.1 -- The NPC's own AI performs a client's cast

Choose a tag to compare

@marthofdoom marthofdoom released this 06 Sep 04:17

⚠️ KNOWN ISSUE IN 0.9.1 — a follower can stop fighting

While 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=0

The 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
bHealAnimPackage off 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 kSelf heal applied at an ally lands on the caster instead. Leave proxy = 0 and Harbinger mints a delivery-flipped copy for you.
  • stopPct is 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.