Skip to content

Latest commit

 

History

History
307 lines (248 loc) · 25.9 KB

README.md

File metadata and controls

307 lines (248 loc) · 25.9 KB

AI Package Management Framework (APMF)

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:

The problem it solves

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.

What APMF does

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.

Facets

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.

For mod authors

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.

Quickstart

#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.

How it works (short version)

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.

Requirements

  • 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.ini and logs one line.
  • Build: CMake 3.21+, a C++23 compiler, vcpkg with the commonlibsse-ng port (see native/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.

Install

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's bSceneProbe. The key list below is the short version.
  • APMF.esl, at the root, beside SKSE.

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.

Settings

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.

What is in v0.16.0

  • A Papyrus call ranks as the plugin that owns its script, 20 by default, with iPapyrusOwnerFile choosing 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 checks abiVersion >= 32 first.
  • 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.

What is in v0.15.2

  • 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.

What is in v0.15.1

  • 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 UNRESOLVED and CAST 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_StuckTimeout at 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 Release or Repoint ends the hold.
  • What a client sees from the substrate: Harbinger releases the substrate claim when the window closes, GetSubstrateState goes back to none, and the close reason (fired, fired-not-released, released-unconfirmed, tree-outside-gate) is in APMF.log. The API did not change.
  • No ABI change, still ABI v30.

What is in v0.15.0

  • Travel across cells (ABI v30). The kTravel_CrossCells flag 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). GetClientFacetPriority returns 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. bSubstrateMovementHold and fSubstrateTtlSec in [CombatSubstrate]. A passive CLAIM SCORE line shows what the game scored the claimed spell.
  • Needs ABI v30 for the new calls. Older clients keep working.

v0.14.0

  • Place (ABI v28). kIntent_Place moves an NPC beside a reference or to a point in an interior cell, once, through the game's own move call. GetPlaceState says what happened.
  • Equip enforced for every declared category by default, and an equip claim can name one copy of an item (ABI v25). bEquipObserveOnly now defaults to 0 and bEquipEnforceArmor is retired.
  • Equip is two facets on the Harbinger page and in APMF_Sources.ini: EquipWeapon (weapons, shields, torches, ammo) and EquipArmor. 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, bTorchDeny in APMF.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) and IsSubstrateAvailable (ABI v27). [CombatSubstrate] in APMF.ini.
  • Needs ABI v28 for the new calls. Older clients keep working.

v0.13.1

  • 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] in APMF.ini.
  • The combat substrate (ABI v23, on by default). A mod claims kIntent_CombatSubstrate beside its cast claim and the follower casts its spell animated, out of a real fight, against an invisible stand-in. [CombatSubstrate] in APMF.ini.
  • Idle state (ABI v24). GetIdleState says 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.

v0.13.0

  • 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] in APMF_Sources.ini.
  • Spectator seat. A retreating follower no longer stops to watch the fight while a mod's package claim outranks the engine. iSpectatorSeat in [PackageGate] of APMF.ini.
  • Actor value watch. Harbinger names who changes a follower's speed, disposition, sneak noise and detection range values. Watch only. iActorValueSeat in [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.

v0.12.0

  • 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=1 by 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.

Status

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.

License

MIT. See LICENSE.