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

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

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