Skip to content

Static Effects (Plugin Quests)

Static effects are pre-allocated slots that exist on every tracked actor. They are updated on a timer by the framework, making them more efficient than dynamic effects for always-on conditions.

This requires implementing a plugin quest script that extends sla_PluginBase.

Plugin lifecycle

Your quest script extends sla_PluginBase and overrides the lifecycle functions:

Scriptname MyMod_SLAPlugin extends sla_PluginBase

; Called on every periodic update. actors = all tracked actors, nakedActors = subset that are naked.
function Update(Actor[] actors, Actor[] nakedActors)
    int i = actors.Length - 1
    while i >= 0
        UpdateActor(actors[i], true)
        i -= 1
    endWhile
endFunction

; Called per-actor. fullUpdate = true means a complete recalculation is expected.
function UpdateActor(Actor who, bool fullUpdate)
    float exposure = ComputeExposure(who)
    SetArousalEffectValue(who, _exposureEffectIdx, exposure)
endFunction

; Called when an actor is removed from tracking (e.g. leaves the area).
function ClearActor(Actor who)
    SetArousalEffectValue(who, _exposureEffectIdx, 0.0)
endFunction

; Return true when all dependencies are present.
bool function CheckDependencies()
    return (Game.GetFormFromFile(0x800, "MyMod.esp") as Quest) != none
endFunction

; Called when the plugin transitions to "Installed" state.
function EnablePlugin()
    RegisterForPerodicUpdates()  ; required, or Update() / UpdateActor() never fire
    _exposureEffectIdx = RegisterEffect("MyMod_Exposure", "Exposure", "Arousal from nearby nudity")
endFunction

; Called on every game load while ALREADY "Installed". Re-register your
; load-fragile registrations here — see "Plugin state machine" below for why.
function ReassertSubscriptions()
    RegisterForPerodicUpdates()                          ; match EnablePlugin()'s subscriptions
    RegisterForModEvent("SomeMod_Event", "OnSomeEvent")  ; and any external mod-event hooks
endFunction

; Called when the plugin transitions out of "Installed" state.
function DisablePlugin()
    parent.DisablePlugin()  ; tears down periodic/LOS registrations
    UnregisterEffect("MyMod_Exposure")
endFunction

int _exposureEffectIdx = -1

Plugin state machine

The base class manages a two-state machine: "" (not installed) and "Installed". On every game load, CheckDependencies() is called:

  • If it returns true and the state is not "Installed" → calls EnablePlugin() then enters "Installed"
  • If it returns true and the state is already "Installed" (the usual game-load case) → calls ReassertSubscriptions() to re-assert load-fragile registrations
  • If it returns false and the state is "Installed" → calls DisablePlugin() then leaves "Installed"

OnInstalled() and OnUninstalled() are called automatically to register/unregister your plugin with the framework. Override EnablePlugin/DisablePlugin for your setup/teardown, not these events.

Re-assert load-fragile registrations in ReassertSubscriptions()

EnablePlugin() only runs on the first install transition — on a normal load your plugin is already "Installed", so EnablePlugin() does not run again. Two kinds of registration it makes can be missing on a later load:

  • Periodic/LOS subscriptions are stored in the save but not rebuilt from scratch on load; they can desync (most often after a mod version upgrade), leaving a plugin "installed" yet silently not receiving UpdateActor() / UpdateObserver() — effects freeze at their last value while event-driven effects keep working.
  • RegisterForModEvent hooks persist only in the SKSE co-save (the MCBR record). That file is separate from the .ess save and is a common thing players delete to "clean" a save — doing so drops every mod-event registration, so your event handlers go dead with no other symptom.

ReassertSubscriptions() runs on every load to heal both. Re-make exactly the RegisterForPerodicUpdates() / RegisterForLOSUpdates() and RegisterForModEvent(...) calls your EnablePlugin() made — all are idempotent (re-registering overwrites the same registration), so it's a safe no-op when already registered. Do not re-register effects (RegisterEffect) or re-run other setup here — the C++ effect registry survives the save, and re-running full setup can clobber live state. A plugin that makes none of these registrations can leave this as the inherited no-op.

Registering and using static effects

; In EnablePlugin():
int effectIdx = RegisterEffect("MyMod_Exposure", "Exposure", "From nearby nudity")
; effectIdx is the slot index. Store it — you need it for all subsequent calls.

; In UpdateActor():
SetArousalEffectValue(who, effectIdx, 35.0)       ; set absolute value
; or
ModArousalEffectValue(who, effectIdx, 5.0, 100.0) ; add 5, cap at 100

; To apply a built-in timed function to a static effect:
SetArousalEffectFunction(who, effectIdx, 1, 4.0 / 24.0, 0.0) ; decay, half-life 4h
; Convenience wrappers:
SetLinearArousalEffect(who, effectIdx, 50.0 * 24.0, 100.0)   ; +50/hour, cap 100
SetArousalDecayEffect(who, effectIdx, 2.0 / 24.0, 0.0)       ; half-life 2h, floor 0

; To read back values:
float val   = GetArousalEffectValue(who, effectIdx)
float param = GetArousalEffectFncParam(who, effectIdx)
float limit = GetArousalEffectFncLimit(who, effectIdx)
int   aux   = GetArousalEffectFncAux(who, effectIdx)

; To disable (zero out) an effect:
DisableArousalEffect(who, effectIdx)

; Force immediate recalculation (normally updates are batched):
ForceUpdateArousal(who)

Auxiliary storage

Each static effect slot carries two auxiliary fields for plugin use: one int and one float. These are not part of the arousal sum — they are free storage for your plugin state.

; Store per-actor state alongside the effect:
slaInternalModules.SetStaticAuxillaryInt(who, effectIdx, someState)
slaInternalModules.SetStaticAuxillaryFloat(who, effectIdx, someTimestamp)

int state = slaInternalModules.GetStaticEffectAux(who, effectIdx)

LOS (line-of-sight) updates

If your plugin needs to react when an actor sees another, register for LOS events:

function EnablePlugin()
    ...
    RegisterForLOSUpdates()
endFunction

function ReassertSubscriptions()
    ...
    RegisterForLOSUpdates()  ; re-assert on load alongside RegisterForPerodicUpdates()
endFunction

; Called when observer gains LOS on observed:
function UpdateObserver(Actor observer, Actor observed)
    ModArousalEffectValue(observer, _exposureEffectIdx, 2.0, 50.0)
endFunction

The base DisablePlugin() calls UnregisterForLOSUpdates() (and stops periodic updates) for you — but Papyrus does not chain to the parent automatically, so your override must call parent.DisablePlugin() (as shown in the lifecycle example above) for that teardown to run.

MCM options

Plugins can expose options to the SLA MCM:

function AddOptions()
    AddToggleOption("General", "Enable Exposure", "Count nearby nudity", true)
    AddOptionEx("General", "Rate", "Arousal per nude actor", 5.0, 0.0, 20.0, 1.0, "{0}")
endFunction

float function GetOptionValue(int optionId)
    if optionId == 0
        return _enabled as Float
    elseIf optionId == 1
        return _rate
    endIf
    return 0.0
endFunction

function OnUpdateOption(int optionId, float value)
    if optionId == 0
        _enabled = (value as int) as bool
    elseIf optionId == 1
        _rate = value
    endIf
endFunction

Read the shipped plugins

sla_sexlabplugin.psc and sla_ddplugin.psc are complete, real plugins built on this base class. See sla_PluginBase.psc for the full wrapper API and the Papyrus API Reference for the underlying native functions, including effect groups.