Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
DeathRoulette
When a player dies, a random event fires for everyone. Server-side only, clients install nothing.
| Date uploaded | a month ago |
| Version | 1.0.0 |
| Download link | Geckuss-DeathRoulette-1.0.0.zip |
| Downloads | 30 |
| Dependency string | Geckuss-DeathRoulette-1.0.0 |
This mod requires the following mods to function
denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2350JereKuusela-Server_devcommands
Enables devcommands and utilities for server admins.
Preferred version: 1.113.0README
DeathRoulette
When a player dies on the server, a random event fires for everyone.
Server-side only. Players install nothing — install it on a dedicated server and every connected vanilla client sees the effects.
Built and verified against game build l-1.0.12 (network 40, world 41), BepInEx 5.4.23.5.
Source: https://github.com/Geckuss/valheim-deathroulette
How death is detected
Player.OnDeath() starts with:
if (!m_nview.IsOwner()) { Debug.Log("OnDeath call but not the owner"); return; }
A player's ZNetView is owned by that player's own client, so Player.OnDeath never runs on
a dedicated server. Patching it server-side compiles, loads, and silently does nothing.
What the server does see is the broadcast made from inside OnDeath():
m_nview.InvokeRPC(ZNetView.Everybody, "OnDeath"); // Everybody == 0
That is a routed RPC. ZRoutedRpc.RPC_RoutedRPC on the server passes anything addressed to
peer 0 through HandleRoutedRPC before relaying it to the remaining peers, so a postfix on
ZRoutedRpc.HandleRoutedRPC sees every player death, with the dying player's ZDOID in
RoutedRPCData.m_targetZDO. Name and position come off the ZDO (ZDOVars.s_playerName,
zdo.GetPosition()).
"OnDeath" is a generic RPC name, so the watcher also checks
zdo.GetPrefab() == "Player".GetStableHashCode().
What a server can do to vanilla clients
Everything in ServerActions drives RPCs the stock client already registers:
| Action | RPC | Registered in |
|---|---|---|
| Chat announcement (Shout, not distance-culled) | ChatMessage |
Chat |
| Status effect on a player | RPC_AddStatusEffect |
SEMan |
| Heal a player | RPC_Heal |
Character |
| Damage a player | RPC_Damage |
Character |
| Stagger a player | RPC_Stagger |
Character |
| Animation hit-stop | RPC_FreezeFrame |
Character |
| Make a player loud | RPC_AddNoise |
Character |
| Teleport a player | RPC_TeleportTo |
Character |
| Play a VFX/SFX prefab | SpawnObject |
ZNetScene |
| Start a real raid | SetEvent via RandEventSystem.SetRandomEventByName |
RandEventSystem |
| Skip to morning | EnvMan.SkipToMorning (sets net time) |
— |
| World state | ZoneSystem.SetGlobalKey |
— |
ZNet.IsDedicated() is a lie
In build l-1.0.12 it returns a hardcoded false, so it cannot be used to detect a dedicated
server. IsServer() returns the real m_isServer and is what everything here uses.
Sounds and visual effects do work server-side
ZNetScene.RPC_SpawnObject just calls Instantiate(prefab, pos, rot) with no owner guard, so
broadcasting SpawnObject makes every client render its own copy — which is exactly right
for non-networked VFX/SFX prefabs. So the server can play sounds and spawn effects without a
client mod. The limit is that it can only name assets the client already ships; custom
audio or VFX would still need a client-side mod, as would HUD text and screen shake.
The duplicate-spawn trap
The same broadcast is wrong for anything with a ZNetView: N players would each instantiate
their own creature, giving N copies with N ZDOs. ServerActions.SpawnCreature therefore
targets exactly one peer. ServerActions.PlayEffect broadcasts. Keep the two apart.
Per-ZDO RPCs are safe to broadcast
RPC_Heal, RPC_AddStatusEffect and RPC_AddNoise each begin with their own IsOwner()
check, so broadcasting to Everybody with a target ZDO lets the owning client act and every
other client no-op. That avoids having to resolve the owning peer. RPC_Stagger and
RPC_FreezeFrame are deliberately not owner-guarded — vanilla broadcasts them too, because
the animation should play on every client that can see the character.
Testing it
The death hook is confirmed working in production -- a real death on a live server produced
Thorkell died -> 'loud_grief' in the log. What is worth checking by hand is whether each
individual effect actually lands visibly, since several resolve names on the client that the
server cannot validate.
Use dev/dr.sh, which needs no client mod and no restart:
./dev/dr.sh list # every event with its weight
./dev/dr.sh status # master switch, cooldown, who is online and where
./dev/dr.sh fire thunderclap # trigger one event
./dev/dr.sh roll # roll the table as if someone died
./dev/dr.sh reload # re-read the config file in place
Stand in game, fire an event, and see whether anything happens. fire refuses when nobody is
online, because every effect needs a player to act on. A manual fire sets DeathContext.IsTest,
which makes events that normally act on "everyone except the corpse" act on everyone instead --
otherwise an admin testing alone is the stand-in victim, the survivor list is empty, and
blood_tax, second_wind, scatter and summon_mourners all decline with nothing to show.
Some events need two or more players online to show their real behaviour: summon_mourners
teleports a lone test firer to their own position, swap_places declines outright, and
grave_guardian can only prove that a spawn happens, not that it happens once (the duplicate
it guards against needs two peers). If an event announces but nothing visibly happens, the
effect name is wrong rather than the plumbing -- see Name accuracy.
dev/ holds personal deployment and testing scripts (deploy.sh, dr.sh, setup-libs.sh).
They are configured through environment variables (VALHEIM_SSH_HOST, VALHEIM_CONTAINER,
VALHEIM_BEPINEX, VALHEIM_PLUGIN_DIR, VALHEIM_DIR) and are not part of the published
package.
Why commands are a file and not chat
Chat was tried first and cannot work for this. Two independent reasons:
Chat.InputText does text = text[0] != '/' ? "say " + text : text.Substring(1) and then runs
the result as a local console command, so anything typed with a leading slash never leaves
the client.
Worse, chat never reaches a dedicated server when the admin is alone. Both Chat.SendText and
Talker.Say fan out one RPC per online player through
CheckPermissionsAndSendChatMessageRPCsAsync, each addressed to that player's own peer. And
InvokeRoutedRPC handles a message addressed to your own id locally while explicitly declining
to route it:
if (targetPeerID == m_id || targetPeerID == 0L) HandleRoutedRPC(data);
if (targetPeerID != m_id) RouteRPC(data); // own id -> never leaves the client
A dedicated server has no local player and never appears in the player list, so with one player online there is nobody else to address and nothing is sent at all.
So the server polls bepinex/deathroulette.cfg's directory for a deathroulette.cmd file once
a second (a timer check plus one File.Exists), runs each line, and writes the reply to
deathroulette.out. dr.sh wraps that over SSH.
Announcements
These go to MessageHud, the same place a raid announces itself, via the routed
ShowMessage RPC:
- Center -- the event's display name, in the big four-second crossfade banner. The "The ground is shaking" slot.
- TopLeft -- who died, and the flavour line. These queue rather than replace each other,
and each is written to the player's message log by
AddLog.
Death and effect are separate lines. The death fires on every death, before the enabled and cooldown checks, so deaths are still named when the roulette is off or cooling down.
Events return their flavour text rather than announcing it themselves, which keeps phrasing in one place; returning null is how an event declines and triggers a re-roll.
Chat cannot carry these, at all
An earlier version shouted announcements into chat and every one was silently discarded client-side, with nothing in the server log. Two independent gates:
// Chat.OnNewChatMessage -> RelationsManager.CheckPermissionAsync
if (!user.IsValid) { completedHandler(Error); return; } // not granted -> dropped
// Terminal.AddString
if (!ZNet.TryGetPlayerByPlatformUserID(user, out var playerInfo)) return; // dropped
string text2 = ...playerInfo.m_name... // a custom sender name is never used
A chat message must be attributed to a real connected player, and the displayed name comes from
the player list rather than from the UserInfo you send. A dedicated server is neither a player
nor a valid platform user. MessageHud.RPC_ShowMessage, by contrast, is just
ShowMessage((MessageType)type, text) -- no owner check, no permission check, no sender lookup,
and it runs the text through Localization.
The event table
34 events. Cut any of them by setting its weight to 0 in the config; no rebuild needed.
| Id | Flavour | Weight | What it does |
|---|---|---|---|
taunt |
Harmless | 9 | Odin passes comment. Nothing else. |
bossstone_omen |
Harmless | 7 | Boss-stone activation effect at everyone's feet. |
gjall_lament |
Harmless | 7 | A gjall's taunt echoes over every player. |
moment_of_silence |
Harmless | 7 | Everyone's animation hitches briefly. |
phantom_numbers |
Harmless | 6 | Floating text appears over every player. Cosmetic. |
forsaken_gift |
Kind | 7 | A random forsaken power (GP_*) for everyone. |
windfall |
Kind | 7 | Scatters a pile of materials on the ground at the corpse. |
second_wind |
Kind | 6 | Survivors healed to full. |
rested |
Kind | 4 | Rested buff for everyone. |
dawn_mercy |
Kind | 4 | Time skips to the next morning. |
revealed_path |
Kind | 4 | Pins the nearest landmark on each map, faces them to it. |
gods_relent |
Kind | 4 | Ends the raid currently running. Declines if none. |
feather_fall |
Kind | 4 | Slow Fall on everyone: no fall damage for a while. |
adrenaline_rush |
Kind | 4 | A surge of adrenaline for everyone. |
mistveil |
Kind | 3 | Demister on everyone: the mist parts. |
sheltered |
Kind | 3 | Shelter and Campfire comfort on everyone. |
thunderclap |
Mean | 7 | Lightning strikes every player for a little damage. |
shared_pain |
Mean | 5 | Random harmful status on everyone. |
blood_tax |
Mean | 5 | Survivors lose up to 25% of current health. Never lethal. |
stagger_all |
Mean | 5 | Everyone staggered, brief loss of control. |
loud_grief |
Mean | 4 | Everyone becomes loud, attracting creatures. |
deep_chill |
Mean | 4 | Cold and Freezing on everyone. |
tar_soaked |
Mean | 4 | Tared and Slimed on everyone: slow and clumsy. |
pyre |
Mean | 4 | Burning plus fire damage on everyone. |
plague |
Mean | 4 | Poison plus poison damage on everyone. |
soaked |
Mean | 3 | Everyone gets Wet. |
smoked_out |
Mean | 3 | Smoked on everyone: no resting until it clears. |
raid |
Brutal | 10 | A real raid: music, banner, spawners. Gated by default. |
summon_mourners |
Brutal | 5 | Survivors teleported to the corpse, wherever it is. |
swarm |
Brutal | 5 | A handful of small creatures spawned on the corpse. |
scatter |
Brutal | 4 | Survivors thrown 20-60m in a random direction. |
grave_guardian |
Brutal | 4 | One creature spawned to guard the corpse. |
swap_places |
Brutal | 3 | Two random players trade positions. Needs two online. |
pilgrimage |
Brutal | 1 | Everyone sent to the last defeated boss's altar, boss woken. |
Two safety rails: blood_tax and thunderclap read ZDOVars.s_health and cap damage at a
fraction of current health, so a rolled event can never itself be lethal and recurse; and a
global cooldown (default 30s) stops a party wipe firing five raids at once.
Events that cannot apply — nobody else online for second_wind, no raid running for
gods_relent, no boss defeated yet for pilgrimage — return null and are re-rolled rather
than wasting the death.
Progression gating
The raid event has two modes, chosen by [Raids] UseVanillaGating (default on):
- Gated (default). The mod asks vanilla which raids are valid right now and where, so progression (global keys), biome and base checks all apply -- a Meadows base never gets an Ashlands raid.
- Ungated.
RandEventSystem.SetRandomEventByNamesets the event directly and does not run those checks, so anything in[Raids] Poolfires regardless of progress. Use this for chaos.
Boss summons (boss_*, hildirboss1..3) are never offered by the raid event: vanilla marks
them m_random = false and the gating honours that. The pilgrimage event summons a boss
deliberately, and only the last one the world has already defeated.
Name accuracy
Three name pools are resolved at different places, which matters when tuning:
- Raid names are defined in game prefabs, which vary by world and version.
[Raids] Poolis left empty by default, meaning "whatever vanilla currently permits", which is the simplest correct setting with gating on. To pin a specific list, read the world's real names off the running server (logged on startup byStartupPatch) and set them in config, not code. - Effect prefabs are resolved by the client's
ZNetScene. 1892 real names extracted from the build's asset manifest are in reference/effect-prefab-names.txt; the ones used here were taken from that list. - Status effects are resolved by the client's
ObjectDB, which the server has no copy of, so a wrong name fails silently with no log. TheGP_*powers were verified against the manifest; the vanilla status names used here (Burning,Frost,Poison,Wet,Smoked,Rested,Cold,Freezing,Tared,Slimed,SlowFall,Demister,Shelter,CampFire) are long-standing but not statically verified. Confirm them in play.
Layout
src/Plugin.cs BepInEx entry point, config, command-file poll
src/DeathWatcher.cs Harmony patch that detects death server-side
src/ServerActions.cs server -> vanilla client primitives
src/RouletteEvents.cs the weighted event table
src/StartupPatch.cs logs the world's real raid names on start
src/Announce.cs the two announcements (who died, what fired)
src/CommandFile.cs polled admin command channel
src/EffectSpec.cs effect-prefab layering parser
lib/ reference assemblies (game + BepInEx), not redistributed
reference/ effect prefab names extracted from the build
dev/ personal deploy/test scripts, not part of the package
Build
dotnet build -c Release
netstandard2.1, matching BepInEx 5 on Unity Mono. No assembly publicizer needed:
ZRoutedRpc.RoutedRPCData is public, and Harmony patches private methods by name.
The build needs the reference assemblies in lib/, which are copyrighted game and BepInEx
binaries and are gitignored rather than committed. Populate them from a local install and your
server with dev/setup-libs.sh (see the script for the env vars it reads).
Install
Drop DeathRoulette.dll into your dedicated server's
BepInEx/plugins/ directory and restart. Clients need nothing. On Thunderstore/Hexium the
package installs into BepInEx/plugins/Geckuss-DeathRoulette/.
dev/deploy.sh automates the build-ship-restart loop for a dockerised server; it is env-var
driven (VALHEIM_SSH_HOST, VALHEIM_CONTAINER, VALHEIM_PLUGIN_DIR) and --force skips the
check that refuses to restart while players are connected. A restart kicks everyone online.
Config
BepInEx/config/geckuss.deathroulette.cfg, generated on first run.
[General] — Enabled, AnnounceDeaths, AnnounceEffects, CooldownSeconds (default 30),
VerboseLogging (logs every death and roll).
[Weights] — one entry per event id, described and defaulted as in the table above. 0
disables.
[Raids] — UseVanillaGating (default on), Pool (empty by default; see Name accuracy).
[Effects] — override the prefab lists for Thunderclap, AncientOmen, Lament, and the
ThunderclapDamage. Effects layer with + (played together) and vary with | (one picked at
random).
[Events] — PilgrimageSummonsBoss (default on): turn off to keep the mass teleport without
respawning the boss.
CHANGELOG
Changelog
1.6.0
- Packaging:
manifest.jsonlists theServer_devcommandsdependency again, so a fresh install from Thunderstore or Hexium brings the in-game admin console the package description and the README both promise. - Packaging: dropped the stray UTF-8 BOMs from
manifest.json,thunderstore.toml,DeathRoulette.csprojandsrc/Plugin.cs. A BOM in the manifest breaks the JSON parse on upload. - New
publish.ps1: builds the upload zip and pushes it to Thunderstore and Hexium with the officialtcliCLI, reading the API tokens from the gitignoredapikeysfile. Run./publish.ps1after bumping the four version fields.
1.5.0
- The death announcement now names the cause when the server can see it. A dedicated server
only ever executes server-owned damage (falling trees and rocks, carts, ballistae/turrets,
creature projectiles, environment AOEs), which all flow through
Character.Damage-- a newDeathCauseHarmony prefix records the last such hit per player and the death line becomes e.g. "X has died — crushed by a falling tree" or "X has died — killed by a Fuling". Most causes (melee hits, falls, DoTs) are computed on the victim's own client and stay generic. Tunable withDeathCauseWindowSeconds(default 5; 0 disables cause attribution). - New
CountdownSecondsconfig (default 3): the rolled effect no longer fires instantly. The death is announced, the raid-banner slot walks down 3, 2, 1, and only then the event fires and announces itself. Set it to 0 for the old instant behaviour. The countdown only runs whenAnnounceEffectsis on, since the countdown is the effect's reveal.
1.4.0
-
Removed the
lead_feetevent. -
Removed the
smoked_outevent. -
Removed the
skeweredevent. -
pilgrimagenow remembers where everyone stood, watches the summoned boss, ends the stuck boss-event music the moment the boss dies (vanilla never clears it for a bare spawned boss), and teleports everyone back home 20 seconds after the kill. -
Removed the
shelteredevent: its Shelter/CampFire status names do not resolve on a stock client, so the event announced but did nothing. -
Removed the
shamans_graceevent: vanilla'sSE_Greydwarf_shaman_healonly heals non-player creatures, so a player-targeted "heal aura" did nothing. -
magic_barrierandshaman_shieldno longer sustain their status effects: each is applied once and lasts the vanilla default baked into the client asset. -
Removed the
moment_of_silenceevent: its release-formRPC_FreezeFramedoes not hitch animations on a stock client, so the event announced but did nothing. -
Removed the
royaltyevent. -
Removed the
shared_painevent: a random harmful status effect, redundant with the dedicated status events (pyre, deep_chill, slimed, tar_soaked, etc.). -
Fixed the adrenaline event's stamina-regen buff, which silently no-oped since the 1.0.7 mead rework: the
Effects/AdrenalineBuffdefault (and code fallback) is nowPotion_stamina_medium. Existing configs keep the staleStaminaMediumvalue -- reset that one key to pick up the fix. -
New events using status-effect names verified against a 1.0.7 ObjectDB:
- Kind:
fenris_blessing(5 min),berserk_fury(45 s),hold_lessons(2 min),giants_strength(5 min),witches_brews(5 min),wolf_sight(5 min). - Mean:
seasick(20 s),rooted(12 s). - Harmless:
buried_luck(3 min),dvergr_ally(90 s).
- Kind:
-
New events round two: Kind
lightfoot(5 min),mage_luck(3 min),set_bonus(5 min, random set effect); Meantroll_love(2 min); Harmlessfishing_knot(3 min). -
New events round three: Kind
magic_barrier(90 s, staff shield) andshaman_shield(60 s, fuling shaman heal aura).Slimedis already covered bytar_soaked. -
New event
maddened: neutral and tamed creatures near every player turn hostile, using the same server-authoritativeBaseAI.AggravateAllInAreapath as the vanillaaggravateconsole command. Aggravation is normally permanent in vanilla, so the event now tracks the affected creatures and clears the flag again after 60 s (CalmCreatures), so animal pens recover. -
New event
animal_friends: tames up to 4 untouched tameables (boar, wolf, lox, hen) near every player using the vanilla completion pathMonsterAI.MakeTame()(publicCharacter.SetTamed→RPC_SetTamed, server-as-owner writess_tamed). -
tar_soakednow applies only Tared;Slimedis its own event (slimed, 20 s) so the two can be tuned independently. -
loud_griefis substantially meaner and fully configurable under[Events]:- Lasts 60 s by default instead of 20 (
LoudGriefDuration, 0 disables the noise). - Makes everyone genuinely loud through the client's real noise pipeline (RPC_AddNoise →
Character.m_noiseRange/ itss_noiseZDO), which creature AI hears out to their alert range through walls, instead of a cosmetic ping. - Wakes and aggros every live creature within
LoudGriefNoisePowermetres of each player every few seconds via publicBaseAI.Alert(), so anything nearby stops idling and hunts. Tamed animals and players are spared. - Optionally spawns
LoudGriefSpawnCount(default 2) ofLoudGriefSpawnPrefab(defaultGreyling) next to every player, so a quiet biome still gets something.
- Lasts 60 s by default instead of 20 (
-
New
AttractCreatureshelper inServerActions(server-authoritative wake + aggro within a radius), powering the louderloud_grief. -
Fixed
revealed_pathhanding every player copies of the nearest landmark: vanillaFindClosestLocationreturns the same instance to everyone standing near it, so several players could all be pointed at one cave. Each player now gets a different instance of the chosen landmark while any remain, reusing one only when the world has none left. -
hold_lessonsnow applies theSoftDeathbuff exactly once instead of re-applying it every two seconds for two minutes: the buff's own duration is ten minutes, so the refresh loop was pointless. -
witches_brewsnow picks a brew per player and never hands a player the same resistance mead twice in a row (status effects are client-side, so this is the server's best proxy for "a potion you do not already have"). It also applies once instead of refreshing for five minutes. -
The seven armour sets are now separate, callable events --
fenris_blessing,berserk_fury,wolf_set,root_set,troll_set,lox_set,harvester_set-- all shipped disabled (weight 0) so they never roll on their own, but each can be fired directly withserver dr fire <id>. Armour Fitting (set_bonus) now hands everyone one of the seven at random (45 seconds for the berserker set, five minutes for the rest). Existing configs keep their oldfenris_blessing/berserk_furyweights -- zero them in[Weights]to take them out of the roulette pool too. -
witches_brewsis now the single potion event: its pool is every potion the mod uses (the three resistance brews plus lightfoot mead, eitr mead, troll pheromones and the stamina mead), keeping the per-player "not the same drink twice in a row" rule. The standalone potion events --lightfoot,mage_luck,troll_love-- ship disabled (weight 0) like the armour sets, so they can still be fired directly withserver dr fire <id>but never roll on their own.lightfootnow hands the mead out once and lets it run its full ten minutes instead of a five-minute sustain loop. Existing configs keep the oldlightfoot/mage_luck/troll_loveweights -- zero them in[Weights]to take them out of the roulette pool too. -
buried_luckandfishing_knotapply their buff once and accept it lasts until death: vanilla has no server-side way to expire a status whose asset carries no duration (RPC_AddStatusEffectcarries only item level/skill level, and neitherWishbonenorSetEffect_FishingHatdefines a TTL), so the old re-apply loop just leaked a permanent buff. Descriptions now say so instead of promising "three minutes".
1.3.0
- New event
dusk_mercy: time skips to the next nightfall, mirroringdawn_mercyfor the morning. No vanillaSkipToNightexists, so it replicates EnvMan's smoothed fast-forward by setting the private skip fields to the day-fraction whereIsNightturns true. - New event
riktig_fuling: a band ofGoblin_Gem(the genuine, undressed Fulings) spawns on the corpse. Spawns 2-4 like the swarm event, but these are full goblins. - New event
fog_of_war: the map (minimap and full) is hidden from everyone for five minutes via the vanillaNoMapglobal key, which every client honours and gets re-synced to in real time. - New event
no_escape: portals refuse to work for five minutes via the vanillaNoPortalsglobal key;TeleportWorld.Teleportblocks with the stock "$msg_blocked" message and the key is removed again after the timer.
1.2.0
- Status-effect events now last a set time instead of a vanilla-baked instant. The server cannot set a status duration directly (the RPC has no duration and the TTL lives in the client asset), so these are sustained by re-applying on a short cadence: Feather Fall and Mistveil 5 min; Sheltered, Smoked Out and Deep Chill 3 min; Tar-Soaked, Pyre and Plague 20 s.
- Damage-over-time events (Deep Chill, Pyre, Plague) now deal a small, health-capped tick each interval, since environmental damage from a server-named status does not reliably apply.
- Loud Grief now sustains the noise for 20 seconds so it actually draws creatures in.
- Adrenaline reworked: full-ish heal plus a stamina-regen buff (configurable) plus Ashlands adrenaline, so it does something noticeable pre-Ashlands.
- New event
corpse_run: ten seconds after a real death, the fallen player is pulled back to their grave. - Raised weights for Scatter, Swarm and Grave Guardian.
1.1.0
- Added in-game admin commands through the developer console. With
Server Devcommands
(now a dependency) installed on the server and admin client, run
server dr <command>from the F5 console:list,status,fire <id>,roll,fx <prefab>,stopraid,reload,help. Admin-only, enforced by Server Devcommands and the server adminlist. - The roulette no longer fires the same event twice in a row when an alternative exists.
- Cold Rain now applies Wet + Cold with a water-splash effect, instead of a bare Wet debuff.
- Fixed Lament and Ancient Omen, which were silent: they used prefabs that render as nothing
under a bare server spawn. Lament now layers
sfx_gjall_alerted+sfx_gjall_idle_vocals; Ancient Omen usessfx_frostfoundry_activate.
1.0.0
First public release.
When a player dies on a dedicated server, a random event fires for everyone connected. The mod is server-side only: clients run stock Valheim and install nothing.
- 34 weighted events across four flavours (Harmless, Kind, Mean, Brutal), each independently weighted and disableable in config without a rebuild.
- Death detection via a Harmony postfix on
ZRoutedRpc.HandleRoutedRPC, which is the only place a dedicated server sees a player death. - Announcements through
MessageHud(the raid-banner channel), since chat cannot carry server-originated messages to clients. - Two safety rails: self-inflicted damage is capped below lethal, and a global cooldown stops a party wipe firing multiple raids at once.
- Raids are progression-gated by default, honouring vanilla's global-key, biome and base checks.
- Configurable effect prefab lists, thunderclap damage, raid pool/gating, and pilgrimage boss summoning.
- Admin command channel via a polled command file for live testing without a restart.