Custom Effects
A modder's tool: adds a new library of custom SideLoader effects for XML mods.Overview
A standalone SideLoader library for mod authors. It adds SL_UseSkill, SL_LearnSkillHidden, SL_Banish, SL_PlayVFXOnWeapon, SL_PlayVFXOnChar, SL_PlayVFXPulsed, SL_PlayTimedSound and SL_ReliableVisuals you can use from XML.
SL_UseSkill
Makes a character cast a skill they already know.
<SL_Effect xsi:type="SL_UseSkill">
<Delay>0</Delay>
<SyncType>OwnerSync</SyncType>
<OverrideCategory>None</OverrideCategory>
<SkillID>8100102</SkillID>
<Target>Owner</Target>
<MaxWait>3</MaxWait>
</SL_Effect>
| Field | Meaning |
|---|---|
SkillID |
Item ID of the skill. The target must already know it |
Target |
Owner (the caster) or AffectedCharacter |
MaxWait |
Seconds to wait for the character to be free before giving up |
It goes through Item.TryQuickSlotUse() — the same path as pressing a quickslot key — so the game
handles requirements, cooldown, costs, animation and networking.
SL_LearnSkillHidden
Teaches a skill permanently without it appearing in the skill menu. For skills your mod triggers itself rather than ones the player casts.
<SL_Effect xsi:type="SL_LearnSkillHidden">
<Delay>0</Delay>
<SyncType>OwnerSync</SyncType>
<OverrideCategory>None</OverrideCategory>
<SkillID>-88002</SkillID>
</SL_Effect>
The skill is genuinely learned, castable and networked — it simply never shows in the menu. It survives saves and reloads.
Why you need this.
SL_UseSkill cannot cast a skill the character has not learned — Item.TryQuickSlotUse refuses one
outright. Without a way to teach invisibly, a mod that fires its own skill must either put it in the
player's menu or not work at all.
The learning is vanilla's TryUnlockSkill. Only the display lists are patched; the authoritative
knowledge is left alone. So the skill is genuinely learned, castable and networked — it simply is not
listed, and skill-limit mods, which count those same display lists, do not see it.
Nothing is persisted: the display lists are rebuilt from scratch every session, which is why uninstalling this library just makes the skill visible again rather than corrupting a save.
SL_Banish
Sends a character beyond the veil (inert, untouchable, untargetable and unseen) for a duration, then returns it exactly as it was.
<SL_Effect xsi:type="SL_Banish">
<Delay>0</Delay>
<SyncType>MasterSync</SyncType>
<OverrideCategory>None</OverrideCategory>
<Duration>10</Duration>
</SL_Effect>
Use MasterSync on enemies. Ends early if the character dies, restoring it as a lootable corpse.
A banished character is silent, and cannot start an attack while it is away. An attack it had already begun is not cancelled — it is held exactly as it was and resolves normally when the banish ends.
Loading a save mid-banish releases the creature immediately rather than starting the duration over, and in co-op a client that joins while a banish is running follows the host's rather than running a clock of its own.
Why you need this.
Vanilla has no equivalent. SL_Stun is cancelled by almost any state change, and SL_Petrify
makes the target count as dead and revivable. Neither gives you a timed removal from the fight
that returns the character untouched.
There are no toggles: Duration is the only field.
SL_PlayVFXOnWeapon
SL_PlayVFX, but able to land on weapons the vanilla hook silently refuses.
<SL_Effect xsi:type="SL_PlayVFXOnWeapon">
<Delay>0</Delay>
<SyncType>OwnerSync</SyncType>
<OverrideCategory>None</OverrideCategory>
<VFXPrefab>VFXRunicBlade</VFXPrefab>
<HitPos>false</HitPos>
<ParentMode>This</ParentMode>
<DontInstantiateNew>false</DontInstantiateNew>
<AllowSkinnedMesh>true</AllowSkinnedMesh>
<Slot>Keep</Slot>
</SL_Effect>
| Field | Meaning |
|---|---|
VFXPrefab |
Same list as SL_PlayVFX |
AllowSkinnedMesh |
Let the emitters find a SkinnedMeshRenderer. Default true |
Slot |
Keep (the prefab's own value), MainHand or OffHand |
Everything else behaves exactly as SL_PlayVFX.
Why you need this.
A weapon VFX like VFXRunicBlade has no position of its own. Its particle systems each carry a
VFXParticlesOnWeapon, which does not follow the hand — it takes the weapon's mesh and makes it the
particle emission shape.
Two things make it give up, and both are silent. When it gives up, the particle system keeps
whatever shape the prefab was authored with and the effect still plays — just in the wrong place
and the wrong form, with nothing logged. That fallback looks exactly like a positioning bug, which
it is not: no Position, Rotation, ParentMode or Delay can affect it, because position was
never the mechanism.
This effect sets those two fields on its own copy of the VFX before the game clones it. Everything
else is left to vanilla PlayVFX.
SL_PlayVFXOnChar
SL_PlayVFX, but it puts the VFX on the affected character instead of wherever the prefab expects to hang from.
<SL_Effect xsi:type="SL_PlayVFXOnChar">
<Delay>0</Delay>
<SyncType>OwnerSync</SyncType>
<OverrideCategory>None</OverrideCategory>
<VFXPrefab>VFXBoonEthereal</VFXPrefab>
<HitPos>false</HitPos>
<ParentMode>This</ParentMode>
<DontInstantiateNew>false</DontInstantiateNew>
<Offset>
<x>0</x><y>0</y><z>0</z>
</Offset>
<Detach>Character</Detach>
<DetachLifetime>5</DetachLifetime>
</SL_Effect>
| Field | Meaning |
|---|---|
VFXPrefab |
Same list as SL_PlayVFX |
Offset |
Added to the character's position. Zero puts it on their root, at the feet |
Detach |
Where the VFX goes when the status ends: None, Character or World |
DetachLifetime |
Seconds before an outliving VFX is destroyed. Ignored when Detach is None |
Everything else behaves exactly as SL_PlayVFX.
Why you need this.
A status effect's transforms hang below StatusManager, which sits a metre above the character's
root. A prefab carrying its own vertical offset stacks the two and plays two metres up, with the
second metre inside the prefab itself. Nothing is logged and nothing errors; the effect simply plays
in the wrong place.
You cannot fix it from stock XML. SL_PlayVFX has no Position field, and ParentMode offers only
This and FXWorld — neither anchors to a character. Adding the vanilla VFXPositionOnChar hook
does not work either: it answers the broadcast VFXSystem.Play sends and reads the character passed
to it, and that character is null here, so it does nothing. This effect places the VFX itself.
Detach solves a separate problem. A VFX near the end of a status forces you to extend the status
Lifespan to give it room — and that extends every other effect too. Character gives the VFX
its own clock while it keeps riding the character; World leaves it where it stood, for a VFX that
marks a place rather than a creature.
SL_PlayVFXPulsed
SL_PlayVFX that can blink its VFX off and on again at given times, so a long-lived effect can mark
moments instead of merely existing.
<SL_Effect xsi:type="SL_PlayVFXPulsed">
<Delay>0</Delay>
<SyncType>OwnerSync</SyncType>
<OverrideCategory>None</OverrideCategory>
<VFXPrefab>HexHauntedVFX</VFXPrefab>
<HitPos>false</HitPos>
<ParentMode>This</ParentMode>
<DontInstantiateNew>false</DontInstantiateNew>
<PulseAt>
<float>8.68</float>
<float>15.4</float>
<float>19.98</float>
</PulseAt>
<PulseLength>0.4</PulseLength>
<HardClear>true</HardClear>
<StopAt>0</StopAt>
</SL_Effect>
| Field | Meaning |
|---|---|
PulseAt |
Seconds after this effect activates at which the VFX blinks off. Order does not matter |
PulseLength |
How long each blink stays dark. Default 0.3 |
HardClear |
Destroy the particles already in the air instead of letting them fade. Default true |
StopAt |
Seconds after activation at which the VFX stops for good. Zero means never |
The first four fields are the same as SL_PlayVFX.
If PulseAt is empty in the log, the entries were dropped — each one must be a <float> element.
Why you need this.
Two separate things make a flicker impossible from stock XML:
- Every
PlayVFXinstantiates its own copy of the prefab and keeps it in a private field. Two effects pointed at the sameVFXPrefabtherefore produce two independent VFX, and neither can stop the other. StackingSL_PlayTimedVFXentries at intervals does not pulse one effect — it spawns several, each stopping on its own clock VFXSystem.Stop()callsParticleSystem.Stop()with no arguments, which meansStopEmitting. Emission halts, but every particle already in the air lives out itsstartLifetimefirst
This effect owns one instance for its whole life and drives it from a single coroutine. HardClear
uses StopEmittingAndClear to destroy the live particles rather than let them fade, which is what
makes a short blink actually read as a blink.
PulseAt is relative to activation, not to the status. Effect.Delay is applied before the
effect activates, so subtract it: a mark at 9.83s on an effect with <Delay>1.15</Delay> is 8.68.
SL_PlayTimedSound
SL_PlaySoundEffect that stops after a given time, instead of running until its clip ends or the status does.
<SL_Effect xsi:type="SL_PlayTimedSound">
<Delay>0</Delay>
<SyncType>OwnerSync</SyncType>
<OverrideCategory>None</OverrideCategory>
<Sounds>
<Sounds>ENV_MagicSourceLoop</Sounds>
</Sounds>
<Follow>true</Follow>
<MinPitch>1</MinPitch>
<MaxPitch>1</MaxPitch>
<StopAfter>10</StopAfter>
<FadeIn>0</FadeIn>
<FadeOut>0.15</FadeOut>
</SL_Effect>
| Field | Meaning |
|---|---|
Sounds |
Same list as SL_PlaySoundEffect; one is chosen, as vanilla does |
StopAfter |
Seconds after activation at which the stop begins. Zero means never |
FadeIn |
Seconds to fade in. Zero starts at full volume |
FadeOut |
Seconds to fade, the sound is silent at StopAfter + FadeOut. Zero cuts at StopAfter |
Follow, MinPitch and MaxPitch behave exactly as SL_PlaySoundEffect.
Why you need this.
SL_PlaySoundEffect fires and forgets. The audio manager hands back a sound source and the vanilla
effect discards it, so there is no handle to stop — which makes the status Lifespan the only
control you have over when a sound ends. Extend a Lifespan for any other reason and every sound in
the status is dragged along with it.
This plays the sound itself so it can keep that handle. Everything else was checked against the decompiled vanilla effect rather than assumed, so swapping one for the other changes nothing except that it now stops.
SL_ReliableVisuals
A marker. It does nothing on its own. Put one on a status effect and that status VFX and sounds play on every client, not only on the one that owns the affected character.
<SL_Effect xsi:type="SL_ReliableVisuals">
<Delay>0</Delay>
<SyncType>OwnerSync</SyncType>
<OverrideCategory>None</OverrideCategory>
<IncludeBlasts>false</IncludeBlasts>
</SL_Effect>
| Field | Meaning |
|---|---|
IncludeBlasts |
Also play SL_ShootBlast. Default false |
WARNING:
IncludeBlastschanges gameplay, not just visuals.A blast normally deals damage, and playing one on a client that does not own the character is a gameplay change rather than a visual. Turn this on only for a blast you have deliberately made inert —
RefreshTime -1,HitOnShoot false— and are using purely for its particles.
Why you need this.
A status applied to an AI is owned by the master client, so only the master runs its effects locally. Every other client depends on this:
if (m_statuses.TryGetValue((UID)_statusUID, out status))
status.OnReceiveActivatedEffects(_affectedCharacter, _activatedEffectsInfos);
The receiving client must already hold that exact status UID — and every application mints a new one, so an announcement that arrives before the status itself is discarded silently.
This acts when the status arrives, which happens reliably on every client, rather than when its effects are announced. Nothing is sent, so nothing can be lost: both clients hold the same XML and replay it themselves. If the announcement does arrive afterwards it is suppressed, so nothing plays twice.
Limits:
- Conditions are evaluated locally: an effect gated on state a remote client cannot read
correctly may decide differently there. Avoid
EffectConditionson effects you mark - Timing is per-client: each client times from its own status arrival, so a long sequence can sit tens of milliseconds apart between screens. Invisible for visuals; do not build gameplay on it
- It reports itself in the log —
played N visual effects, and(N FAILED)with a warning naming the effect if any throws
Notes
- All eight effects are verified in single-player and co-op
Contacts
- Join our Outward modding community discord server
- If you have any bug to report or you want to give feedback, here's my discord workshop channel
