Custom Effects
A modder's tool: adds a library of custom SideLoader effects and extensions for XML mods.Overview
No code, more effects.
Custom Effects is a library for mod authors:
ten new SL_Effect types and one SL_ItemExtension,
each one usable straight from your XML.
Effects and extensions
| Name | What it does |
|---|---|
SL_UseSkill |
Casts a skill for a character, as if from their quickslot |
SL_LearnSkillHidden |
Teaches a skill that never shows in the skill menu |
SL_Banish |
Takes a character out of the fight for a set time |
SL_PlayVFXOnWeapon |
Plays a weapon VFX on bows and off-hand weapons too |
SL_PlayVFXOnChar |
Anchors a VFX to the character it affects |
SL_PlayVFXPulsed |
Makes a VFX flicker at chosen moments |
SL_PlayTimedSound |
Plays a sound that ends on your schedule |
SL_ReliableVisuals |
Plays a status's VFX and sounds on every client in co-op |
SL_RemoveStatusParentOwner |
Removes a status from whoever fired or swung, not the one hit |
SL_Blind |
Makes a character's next blow miss entirely |
SL_ItemVFX |
Gives any item a vanilla weapon's particles |
SL_UseSkill
Makes a character cast a skill they already know, either the one who triggered the effect or the one it hit, waiting until they are free to act.
<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.
On a projectile or blast, SL_UseSkill fails part of the time in co-op. To make another player cast a
skill from one, have the hit give them a short hidden status that carries it.
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. Skill-limit mods count those same display lists, so they do not see it
either.
The hiding is not saved: 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. If the character dies while banished, the banish ends early and it
returns 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.
Damage already under way still lands: DoTs applied before the banish keep ticking, and a banished creature can die from them. What it cannot take is a fresh hit — its hitboxes are gone.
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 banish rather than running a clock of its own.
Why you need this
Nothing in stock XML does this. 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 |
Lets it work on skinned weapons such as bows. 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 such as 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, silently: it skips skinned weapons such as bows, and reads only the hand
the prefab was made for. 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, but no Position,
Rotation, ParentMode or Delay will fix it.
This effect fixes both on its own copy of the VFX before the game clones it. Everything else is left
to vanilla PlayVFX.
SL_PlayVFXOnChar
SL_PlayVFX, but anchored to the affected character instead of wherever the prefab would hang.
<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 the VFX lasts after the status ends. 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 reacts to VFXSystem.Play and reads the character that call passes it,
which is null here, so it does nothing. This effect places the VFX itself.
Detach solves a separate problem. A VFX that plays near the end of a status is cut off when the
status ends, so you would have to extend its Lifespan — 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 activation 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 |
Everything else behaves exactly as SL_PlayVFX.
If the log says no PulseAt times and no StopAt were read, or reports 0 pulse times, the entries
were dropped — each 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.83 seconds 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; list several and one plays at random |
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 out; 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.
SL_ReliableVisuals
A marker. It does nothing on its own. Put one on a status effect and that status's 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 visualsA 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 creates a new one, so an announcement that arrives before the status itself is discarded silently.
With this marker, each client plays the effects itself as soon as the status arrives, which it always does. A late announcement is ignored, so nothing plays twice.
Limits:
- Conditions are evaluated locally: an effect whose condition depends on something a remote
client cannot see 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, plus(N FAILED)and a warning naming the effect if any fails
SL_RemoveStatusParentOwner
SL_RemoveStatusEffect, but on the character who fires the blast or projectile, or wields the weapon,
instead of the one it hits.
<SL_Effect xsi:type="SL_RemoveStatusParentOwner">
<Delay>0</Delay>
<SyncType>Everyone</SyncType>
<OverrideCategory>None</OverrideCategory>
<CleanseType>StatusSpecific</CleanseType>
<SelectorValue>RunicStealth</SelectorValue>
<Requires_AffectedChar>true</Requires_AffectedChar>
</SL_Effect>
| Field | Meaning |
|---|---|
CleanseType |
Same as SL_RemoveStatusEffect: StatusSpecific, StatusFamily, StatusType, StatusNameContains or NegativeStatuses |
SelectorValue |
Same as SL_RemoveStatusEffect |
Requires_AffectedChar |
Remove it only when the blast, projectile or weapon actually hit a character. Default true. With false, a projectile removes it on any end: a hit, a wall or running out its lifespan |
Put it in an SL_ShootBlast's BlastEffects, an SL_ShootProjectile's ProjectileEffects, a
weapon's HitEffects or an imbue's effects with OverrideCategory Hit. On a skill's or a status's
own effects it removes from the caster or the bearer, exactly as SL_RemoveStatusEffect would.
Why you need this
Inside a blast, a projectile or a weapon's hit, the affected character is whoever was hit, so
SL_RemoveStatusEffect there strips the status from the target. Nothing in stock XML reaches back
to the shooter or the wielder for a status.
This is vanilla's own RemoveStatusEffect, handed the shooter or the wielder instead of the target,
so every CleanseType behaves exactly as it does there.
SL_Blind
A marker for a status. While a character carries that status, its blows miss entirely: no damage, no knockback, no statuses and nothing to block.
<SL_Effect xsi:type="SL_Blind">
<Delay>0</Delay>
<SyncType>OwnerSync</SyncType>
<OverrideCategory>None</OverrideCategory>
<Consume>true</Consume>
<Grace>0.2</Grace>
<PlayCue>Both</PlayCue>
<CueVFX>NONE</CueVFX>
<CueSounds>
<Sounds>SFX_SKILL_Counter</Sounds>
</CueSounds>
</SL_Effect>
| Field | Meaning |
|---|---|
Consume |
The first blow that misses removes the status. Default true. With false, every blow misses for as long as the status lasts |
Grace |
Seconds from that first miss to the removal, so the rest of the same swing misses too. Default 0.2 |
PlayCue |
What plays on the character when a blow misses: Both, Sound, Visual or None. Default Both |
CueVFX |
The cue's visual, any SL_PlayVFX prefab. Default NONE: the status's own special FX |
CueSounds |
The cue's sounds, any game sounds; list several and one plays at random. Default empty: the status's own SpecialSFX |
Put it in the status's ActivationEffects. It does nothing when it fires; its presence is what counts.
It works on every client in co-op, and on players as well as enemies.
A blow misses only when it is aimed at someone the character could target, so its buffs on itself and its allies still land. A blow that hits nobody does not consume the status, so a character that never connects keeps it until its lifespan runs out. DoTs it dealt before going blind keep ticking. A blow that misses costs no durability on either the attacker's weapon or the target's armour.
The cue plays at most once every 0.5 seconds, so a swing that misses two targets plays it once.
Why you need this
Nothing in stock XML makes a character's own blows miss entirely.
By default the cue is the status's special FX, which a status XML cannot set: VFXPrefab sets only
the FX played when the status starts, and the special FX comes from whichever status you cloned.
CueVFX lets you choose the cue yourself.
SL_ItemVFX
Puts a vanilla weapon's own particles on any item.
It goes in an item's <ItemExtensions>.
<ItemExtensions>
<SL_ItemExtension xsi:type="SL_ItemVFX">
<VFX>
<VFX>Obsidian</VFX>
</VFX>
<Fit>Auto</Fit>
<Offset><x>0</x><y>0</y><z>0</z></Offset>
<Rotation><x>0</x><y>0</y><z>0</z></Rotation>
<Scale>1</Scale>
<SizeMultiplier>1</SizeMultiplier>
<ShowInHand>true</ShowInHand>
<ShowSheathed>true</ShowSheathed>
<ShowOnGround>true</ShowOnGround>
<ShowOnBag>false</ShowOnBag>
</SL_ItemExtension>
</ItemExtensions>
| Field | Meaning |
|---|---|
VFX |
Whose particles: a weapon set or a unique weapon, from the lists below. List several and they all play |
Fit |
How they are fitted to the item: Auto, Stretch, Original, Mesh, Bounds or Blade. Default Auto |
Offset, Rotation |
Fine-tuning, in the item's own space |
Scale |
Enlarges the area the particles come from |
SizeMultiplier |
Enlarges the particles themselves |
ShowInHand, ShowSheathed, ShowOnGround, ShowOnBag |
When they show: drawn, sheathed, lying on the ground, hung on a backpack. Default true, except ShowOnBag |
- Purely visual and local: nothing is sent over the network or saved, and every client shows it for itself
- The log names where each item's particles came from and how they were fitted:
SL_ItemVFX: <item> (<ID>) - Astral from 2010210, 1 emitters, Fit=Auto (Stretch) ratio=0.91.
Weapon sets
The item gets the set's weapon of its own type. For a type the set does not have, it uses the first weapon listed, and by default its particles cover the item's blade or head, or a shield's whole face.
| Entry | Weapons |
|---|---|
Obsidian |
Sword, Axe, Mace, Claymore, Greataxe, Greatmace, Spear, Halberd, Knuckles, Bow, Chakram, Shield, Dagger |
Meteoric |
Sword, Axe, Mace, Claymore, Greataxe, Greatmace, Spear, Halberd, Knuckles, Bow, Chakram, Dagger |
Palladium |
Sword, Axe, Mace, Claymore, Greataxe, Greathammer, Spear, Halberd |
Hailfrost |
Sword, Axe, Mace, Claymore, Greataxe, Hammer, Spear, Halberd, Knuckles, Bow, Dagger, Pistol, Chakram |
Astral |
Sword, Axe, Mace, Claymore, Greataxe, Greatmace, Spear, Halberd, Knuckles, Bow, Shield, Dagger, Chakram, Pistol |
Smoke |
Sword, Axe, Mace, Claymore, Greataxe, Hammer, Spear, Halberd, Knuckles, Bow, Shield |
Calygrey, AncientCalygrey |
Mace, Staff |
JadeLich |
Mace, Staff |
Blood |
Blood Sword, Great BloodSword, BloodSpear |
Staves and halberds share a weapon type in Outward, so a staff gets a set's halberd particles, and a
halberd given Calygrey, AncientCalygrey or JadeLich gets that set's staff particles. Astral
has both and uses its halberd's for both.
Hailfrost's vanilla weapons frost only from a core built into their model, so by default it frosts the
item's whole blade or head instead; set Fit to Stretch for the core look.
Unique weapons
| Entry | Weapon | Entry | Weapon |
|---|---|---|---|
Brand |
Brand | Grind |
Grind |
Maelstrom |
Maelstrom Blade | Shriek |
Shriek |
Sunfall |
Sunfall Axe | Frostburn |
Frostburn Staff |
ButcherCleaver |
Butcher Cleaver | Duty |
Duty |
Skycrown |
Skycrown Mace | Tokebakicit |
Tokebakicit |
MaceOfSeasons |
Mace of Seasons | Murmure |
Murmure |
Starchild |
Starchild Claymore | RedLady |
Red Lady's Dagger |
Kelvin |
Kelvin's Greataxe | Manticore |
Manticore Dagger |
Challenger |
Challenger Greathammer | FrozenChakram |
Frozen Chakram |
Werlig |
Werlig Spear | MysteriousChakram |
Mysterious Chakram |
ThriceWrought |
Thrice-Wrought Halberd | ExperimentalChakram |
Experimental Chakram |
Dreamer |
Dreamer Halberd | DistortedExperiment |
Distorted Experiment |
GhostReaper |
Ghost Reaper | GildedShiver |
Gilded Shiver of Tramontane |
ScholarsStaff |
Scholar's Staff | LionmanMace |
Lionman Electric Mace |
MagesPokingStick |
Mage's Poking Stick | LionmanStaff |
Lionman Frost Staff |
Sandrose |
Sandrose | GiantArchPriest |
Giant Arch Priest Blade Guandao |
SealedMace |
Sealed Mace | MilitiaAxe |
Militia Axe Josef |
CruelPriest |
Scepter of the Cruel Priest | MilitiaClaymore |
Militia Claymore |
MilitiaSpear |
Militia Spear |
Brandstops short of the tip on a sword, or on any blade withStretch, as the vanilla Brand does; setFittoBladeto reach the tipScholarsStaffandMagesPokingStickglow at the item's tip. On the vanilla staves the glow sits in the head, just below the end, and a plain staff has no such head
Fit options
| Value | What it does |
|---|---|
Auto |
The default: Stretch on a weapon of the entry's own type, Blade on any other. Hailfrost always gets Blade |
Stretch |
The source weapon's layout, stretched to the item's blade and centred on it. On swords and daggers, particles always reach down to the item's guard. Falls back to Bounds on an item with no blade, such as a bow |
Original |
Exactly where the source weapon has them |
Mesh |
Emits from the item's whole model, hilt included. Falls back to Bounds when the game does not let particles read the model |
Bounds |
One box around the item's whole model |
Blade |
Emits from the item's blade or head only, not its handle or guard. Falls back to Mesh on a shield, and on an item with no blade or head such as a quarterstaff |
Why you need this
A weapon's own particles are not VFX prefabs: they are built into its visual, the 3D model the game
loads for it. No SL_PlayVFX name reaches them, and nothing in stock XML attaches anything to an
item's visual.
This copies them from the vanilla weapon onto your item's visual and keeps them there as the item is drawn, sheathed, dropped or stored. The game reuses item visuals between copies of an item, so the particles are removed whenever a visual passes to another copy.
Compatibility
- Requires SideLoader
- Mods built on Custom Effects must list it as a dependency, or SideLoader drops their XML
- Tested in single player and in co-op; in co-op every player needs the mod installed
Thanks and credit
- Thanks to my dear friend Rogismund, who sits through the most tedious co-op sessions hunting online bugs with me
- Thanks to Sinai for SideLoader and for all their incredible work
- Thanks to Joh-Benji and Proboina for all the help they have given me!
- Thanks to the entire modding community that has tirelessly made (and is making!) my favourite game go from epic to legendary
Contacts
- Join our Outward modding community Discord server
- If you have any bugs to report or want to give feedback, here's my Discord workshop channel
