🌙 Shadows of Midgard
"You think the dark hides you. The dark hides nothing — it only makes them work for it. Step off the gravel. Wait for dark. Let the rain do the rest, and the draugr will walk past close enough to touch."
Shadows of Midgard replaces Valheim's perception with one that can be reasoned about. A creature has a vision cone with a real blind spot behind it, casts a real line-of-sight ray against the same layer mask the game uses, and hears you at a distance that depends on what you are wearing and how fast you are moving. It remembers where it last saw you, goes and looks, gives up, calls for help, and runs when it is losing. And it does all of that per player — it can be hunting your friend and completely unaware of you, crouched two metres away, in the same frame.
It does not fight the game's AI. It answers the questions vanilla already asks — can I see this, can I hear this — and lets vanilla decide what to do about the answer.
📜 Contents
🔗 Also mine: TortalPortal, a complete portal overhaul, and Mists of Avalor, in open beta on Hexium.
⚔️ Features
🕯️ Light and Shadow Are Real
Your visibility starts from the light actually falling on you, and that light is the sun or the moon. Full daylight makes you easiest to see, a clear moonlit night adds well under half as much, and the dark of a crypt adds nothing. Put a wall, a rock face or a roof between you and the sun or moon and you are harder to see again. Weather joins in — rain and snow cut how far a creature can see you, and fog cuts it harder. None of this is a biome lookup or a time-of-day switch: it is the game's own sun and moon, exactly as bright as the hour and the weather make them. Torches, fires and forges are not counted; a torch in your hand is just one more item you are carrying.
🌿 Cover You Can Actually Stand In
Bushes and tall grass conceal you, sampled from the real vegetation around your feet rather than from what biome the map says you are in. Crouching helps everywhere, and helps considerably more when there is something to crouch in. The three stack: crouched, in grass, inside a bush is a genuinely different proposition from crouched on bare rock.
🔊 You Make Noise
Movement generates noise scaled by how fast you are going, and armour adds to it — plate is loud. Crouching removes most of it. Rain, snow and mist mask a good fraction of whatever is left, so foul weather is the sneak-thief's friend twice over. Hearing is omnidirectional and does not care about walls: a creature that cannot possibly see you can still hear you blunder past in iron.
🎽 Armour Is a Trade
Every equipable item in the game is classified on first launch by weight and material and written to a hand-editable JSON file. Heavy metal is loud and visible. Leather is quieter. Ghillie, forest, swamp, wolf, lox and mist-themed gear grant active camouflage when it matches the biome you are standing in. Modded armour is picked up automatically, and if the classifier gets a piece wrong you can correct it and mark it "Protected": true so your answer survives every future rebuild.
👁️ Creatures Look, and Can Be Blocked
A creature has a configurable vision cone — 120° by default — with a real blind spot behind it, and it casts a line-of-sight ray against the same layer mask vanilla uses. So a closed door works. A rock works. And critically, it keeps working after the creature has noticed you: an alerted creature re-checks line of sight on a bounded interval, which means retreating indoors and shutting the door genuinely loses it. Creatures with mist vision see through mist; everything else does not.
The blind spot is for sneaking, not for fighting. Once a creature is alerted to you it sees all round, as vanilla creatures do, so stepping past its shoulder in a fight does not make you vanish. And a creature that is actually fighting you sees you up close whatever the light: darkness hides you from the hunt, not from the fight. Walls and shut doors still work at any range.
🧠 Four Rungs, and a Memory
Unaware → Suspicious → Alerted → Engaged, with separate gain and decay rates so a momentary break in cover does not reset a chase, and a single glimpse does not instantly commit a mob to you. A creature that loses you walks to your last known position and searches it at a fraction of its chase pace, then gives up on a timer. Badly hurt creatures flee. Alerted creatures call nearby allies, who converge without being handed a target they have no business knowing.
🦌 Animals Count Too
Deer, hares, chickens, boar piglets, wolf cubs and lox calves run on Valheim's AnimalAI rather than MonsterAI, and they are the creatures a stealth player meets first. They get the same sensing model as everything else — the same light, noise, cover and camouflage terms, the same line-of-sight cast — so a crouched, unlit, hidden player can walk up on a deer instead of watching the whole meadow scatter. What they do not get is the behaviour layer, because an animal has no target to be handed and no ally to call; it flees or it does not, and that decision stays vanilla's.
One thing is corrected. Vanilla's calm-down is a timer rather than a re-check, so an alarmed animal keeps fleeing for four full seconds after it has lost you, with no line-of-sight test at all. Break cover properly and it settles on the spot instead — but only when the measurement is fresh, sight and hearing are both negative and the detection ramp has reached zero. An animal you hit is not an animal that has lost you, and that one is left alone.
👥 Every Player Tracked Separately
A creature keeps a separate detection track per nearby player, admitted by distance rather than by roster size, so a ten-player server does not give every creature ten tracks. It can be Engaged with one player and Unaware of another. Tracks are keyed to a stable player identity, so dying and respawning does not wipe what a creature knows about you — and does not let you launder your way out of a chase either.
🖥️ The Eye and the Bar
Two widgets that live inside Valheim's own HUD — they scale with it, hide with it during cutscenes and when you hide the interface, and die with it on world exit. The eye's colour is how aware the world is of you specifically; its size is how visible you are right now, after cover and camouflage. The bar is how much noise you are making. Drag either one wherever you want while a menu is open — the only time the game frees the mouse cursor — and the position is remembered. They never block or swallow a mouse click, in menus or in play. Set AllowDrag off if you would rather lock them in place.
🛡️ It Yields to Other Mods
No forced priorities, no double-asserting postfixes, no reflection into vanilla. The sense patches run at Priority.Low and honour __runOriginal, so anything that wants to win by priority, wins. There is a frozen public API, a per-creature ZDO opt-out any mod can set, and an acknowledgement key so a consumer can tell "SoM says unaware" apart from "SoM never reached this creature".
📊 Stealth Stats (2.0.4)
The server writes per-player stealth statistics to config/ShadowsOfMidgard/barrkbot_stealth.json once a minute — sneak time and distance, times seen and heard, alerts caused, engagements, and undetected kills — in a shape a Discord bot (or anything else that reads JSON) can consume. Counters cover the current server session. Costs nothing on the wire you weren't already paying: each player's counters ride their own replicated data. Turn it off with TelemetryExport.
🍗 Instructions
- Install on the server and every client. Clients without it cannot connect — deliberate, because a client that computes perception differently is a client playing a different game.
- Launch once. The mod classifies every equipable item in the game and writes
BepInEx/config/SoM_ArmorData.json. This takes a moment on first launch only. - Wait for dark, crouch, and go and look at a greydwarf.
The eye tells you how you are doing. Blue is unnoticed; dark red and pulsing means something has you.
🛠️ How-To
🥷 Actually Sneak Past Something
The terms that matter, roughly in order of how much they move the needle:
- Get off the metal. Heavy gear adds more to being seen than daylight does, and plate is loud as well; leather and cloth barely register.
- Crouch. It cuts noise by 60% and adds concealment on top.
- Wait for dark, and keep out of the sun or moon. A clear moonlit night adds well under half the light of noon, and shade takes more off on top.
- Use the weather. Rain, snow and fog cut sight; rain and snow mask sound.
- Break line of sight, then keep it broken. A wall works even against something already hunting you — but it re-checks about twice a second, so a doorway you pass straight back across buys nothing.
- Watch the angle. The cone is 120° by default. Behind a creature that has not noticed you is genuinely behind it — but once it is alerted to you, it sees all round.
🎽 Fix a Mis-Scored Piece of Armour
The classifier reads item names, which is a heuristic, and heuristics are wrong sometimes — a modded chestpiece called Werewolfmail will not match the wolf keyword because matching is by whole word.
Open BepInEx/config/SoM_ArmorData.json, find the entry, fix Weight / Material / Camo, and add "Protected": true. That flag makes your answer permanent: it survives a rebuild, a version migration, and every future update.
To re-derive everything else after installing an armour mod, set RebuildArmorData = true and launch once. It backs the file up, rebuilds, keeps every Protected entry, and switches itself back off.
🖥️ Set Up a Dedicated Server
Drop the DLL in the server's BepInEx/plugins/ alongside every client's. Launch the server once to generate its config, then edit the server's copy — every setting in Server-Synced is pushed from there to all clients and locked to admins.
The whole engine runs headless. There is no local player on a dedicated server, and nothing in the simulation looks for one.
🐛 Read the Log When Something Is Wrong
A healthy launch is two lines:
[SoM/VanillaBind] 25/25 vanilla capabilities bound, every gate open. Patch layer clear to install.
[SoM/Patches] 10 applied, none failed. id=wubarrk.shadowsofmidgard
If a game update moves a method out from under the mod, those lines change and the full table prints by itself — you never have to turn anything on to see a failure. Set ShowPatchSaga = true to see the whole table on a healthy launch too.
Two failure states worth recognising:
- READ-ONLY mode. A capability the perception layer depends on did not bind, so the mod deliberately installed nothing and the game is running vanilla. The table names the exact member that changed.
- DEGRADED. Something failed after startup. The mod stops holding opinions about creatures and hands perception back to vanilla, rather than guessing.
Both are designed outcomes, not crashes. The failure floor is always vanilla.
⚙️ Configuration
BepInEx/config/wubarrk.shadowsofmidgard.cfg. Edit it in-game with Configuration Manager — synced values push live, no restart.
The file carries a schema version and migrates itself. When a setting's meaning changes between releases, only that setting is reset, everything you tuned is left alone, and the file is backed up to .cfg.bak first. Keys that no longer exist in any current version are tidied out automatically, so the file never accumulates dead settings that silently do nothing when you edit them.
🔒 The Lock
| Setting | What it does |
|---|---|
Lock Configuration |
Default on. Leave it on. The server's values overwrite every client's and only admins may change them. Off means any connected client can broadcast its own gameplay settings to everyone on the server. This is the single most important setting in the file. |
Server-Synced (Admin Controlled)
Everything here changes what a creature perceives, so every peer must agree on it.
1 - Systems — master switches
| Setting | What it does |
|---|---|
EnableStealthOverride |
Default on. The master switch. Off makes the mod completely inert and the game byte-for-byte vanilla — the first thing to try when diagnosing whether a problem is us. |
EnableVisibilitySystem |
Default on. Light, shadow, movement, armour and weather affect how visible you are. |
EnableNoiseSystem |
Default on. Movement and armour generate noise. Off means nothing can hear you at all. |
EnableHidingSystem |
Default on. Bushes, tall grass and crouching grant concealment. |
EnableCamoSystem |
Default on. Gear matching the biome grants concealment. |
DriveAnimals |
Default on. Applies the stealth model to AnimalAI creatures — deer, hares, chickens, boar piglets, wolf cubs, lox calves, the greydwarf gem. Off restores pre-2.0.1 behaviour, where passive wildlife used vanilla perception and fled from a fully hidden player. |
TelemetryExport |
Default on. Counts per-player stealth statistics and, on the server, writes them to config/ShadowsOfMidgard/barrkbot_stealth.json once a minute. Counters reset when the server restarts. Off stops the counting and the writes — an already-written file is left behind, so delete it by hand if you opt out for good. |
2 - StealthBrain — the core loop
| Setting | What it does |
|---|---|
VisionThreshold |
0–1, default 0.25. Visibility must exceed this to count as seen. Raise it to be harder to see. |
HearingThreshold |
0–1, default 0.15. Noise must exceed this to count as heard. Lower than the vision threshold on purpose — sound carries past cover that stops sight. |
SuspicionGain |
0–5, default 2.5. Detection gained per second while heard but not clearly seen. |
AlertGain |
0–5, default 3.0. Detection gained per second while clearly seen. |
DetectionDecay |
0–5, default 0.5. Detection lost per second while neither seen nor heard. Deliberately far slower than the gains — a chase should survive you ducking behind one rock, or nothing ever commits. |
SearchDuration |
1–30 s, default 4. How long a creature searches your last known position before giving up on it. |
GiveUpTime |
1–60 s, default 8. How long before it gives up entirely and goes back to what it was doing. |
SearchSpeedFactor |
0.1–1, default 0.5. Search pace as a fraction of the creature's own chase speed. Relative rather than absolute, so a fast creature still searches fast. |
AlertedLosRecheckInterval |
0.1–5 s, default 0.5. How often an already alerted creature re-checks line of sight. This is a rate limit, never a skip — an alerted creature always loses you when you shut a door, this only decides how quickly it notices. Raising it makes creatures slower on the uptake, never blind. |
3 - Visibility
| Setting | What it does |
|---|---|
LightPenalty |
0–2, default 0.35. How much the sun or moon adds. Full daylight adds all of it, a clear moonlit night well under half, pitch dark none. Torches and fires are not counted. |
ShadowBonus |
0–1, default 0.25. How much standing in shadow subtracts. |
GrassBonus |
0–1, default 0.20. How much crouching in grass subtracts from visibility. Separate from the hiding bonus in 5 - Hiding, which is a different term. |
MovementPenalty |
0–3, default 0.8. How much moving adds, scaled by actual speed. Walking is not sprinting. |
ArmorVisibility |
0–3, default 0.6. How much heavy gear adds. Multiplied by the item's classified weight, so plate costs far more than leather. |
WeatherVisReduction |
0–1, default 0.20. How much rain or snow subtracts. |
FogVisReduction |
0–1, default 0.30. How much fog subtracts. Fog is worth more than rain. |
4 - Noise
| Setting | What it does |
|---|---|
MovementNoise |
0–3, default 0.7. Noise from moving, scaled by speed. |
ArmorNoise |
0–3, default 0.6. Noise from heavy armour, multiplied by classified weight. |
CrouchNoiseReduction |
0–1, default 0.6. Fraction of noise removed by crouching. 1.0 makes crouching perfectly silent. Applied as a multiplier, so it composes with the weather reduction rather than stacking into silence — before 2.0.1 the two subtracted and together they pinned your noise at exactly zero. |
WeatherNoiseReduction |
0–1, default 0.4. Fraction masked by rain, snow or mist. |
5 - Hiding and 6 - Camo
| Setting | What it does |
|---|---|
BushBonus |
0–1, default 0.4. Concealment from standing inside a bush. The strongest single piece of cover in the game. |
GrassBonus |
0–1, default 0.25. Concealment from crouching in tall grass. |
CrouchBonus |
0–1, default 0.15. Concealment from crouching anywhere, with nothing to hide behind. |
BiomeCamo |
0–1, default 0.15. Concealment while your gear matches the biome you are standing in. |
ArmorCamo |
0–1, default 0.10. Concealment from inherently camouflaging armour — ghillie, forest, swamp, wolf, lox and the rest. |
7 - Awareness — the ladder. Keep these in ascending order.
| Setting | What it does |
|---|---|
SuspiciousThreshold |
0–1, default 0.15. It knows something is there. |
AlertedThreshold |
0–1, default 0.40. It is actively looking for you. |
EngagedThreshold |
0–1, default 0.75. It has you and is committing. |
8 - Detection Ranges
| Setting | What it does |
|---|---|
MaxVisualRange |
5–100 m, default 40. The furthest a creature can see you under any conditions. |
MaxHearingRange |
5–80 m, default 25. The furthest it can hear you. Omnidirectional, and unaffected by walls. |
VisionConeHalfAngle |
15–180°, default 60. Half the field of view of a creature that has not been alerted to you yet — 60 gives a 120° cone with a 240° blind spot behind. 180 removes the blind spot entirely. Once a creature is alerted to you it sees all round, as vanilla creatures do. Sight only; hearing does not care where a creature is facing. |
CloseQuartersRange |
0–10 m, default 4. Once a creature is fighting you, it sees you within this distance of its body whatever the light, cover or camouflage, so fights in the dark do not go quiet. Walls, doors and mist still block it, and it never sees you where a vanilla creature could not. Get further away than this, or out of its sight, to break off. 0 turns it off. |
MaxTrackedPlayersPerCreature |
1–64, default 10. How many players one creature remembers at once. The main cost lever on a busy server. Synced rather than local, because dropping a track changes what the creature actually perceives. |
Local to Your Game
| Setting | What it does |
|---|---|
EnableStealthUI |
Default on. Shows the stealth gem and noise meter. Off hides both; the simulation is unaffected. |
EyePosX / EyePosY |
Default 80 / 80. Saved gem position, updated when you drag it. Anchored to the bottom-left corner of Valheim's HUD. |
NoisePosX / NoisePosY |
Default 80 / 40. Saved noise meter position, same anchoring. |
AllowDrag |
Default on. Lets you drag the gem and meter with the mouse while a menu is open — the only time the game frees the cursor. The widgets never block or swallow mouse clicks either way. Turn off to lock the HUD in place; the four position values above are always editable by hand. |
EnableVisionDebug |
Default off. Verbose logging of your own visibility and noise, term by term, plus every armour verdict on the classification pass. Useful for tuning, very loud. |
EnableAIDebug |
Default off. Verbose logging of creature state transitions and the decisions behind them. |
ShowPatchSaga |
Default off. Prints the full startup tables even when everything worked — every vanilla member resolved, and every Harmony patch applied. Off because a healthy launch reports both in one line each. Failures are always reported in full regardless of this setting, so leaving it off never hides a problem. |
DebugLogInterval |
0–60 s, default 1. Minimum seconds between repeated debug lines about the same creature. State changes always log regardless. 0 is unthrottled, which at 20 Hz per creature will bury the log. |
RebuildArmorData |
Default off. One-shot. Re-derives every armour entry on the next launch, keeps everything marked "Protected": true, backs the old file up, and switches itself back off. Use after installing an armour mod. |
LogPerformance |
Default off. Periodically logs sweep timing, evaluation counts and budget use. For chasing stutters. |
🔌 For Mod Authors
Shadows of Midgard exposes a frozen public surface — these names, signatures and semantics do not change between releases. Full details, including the migration path for Mists of Avalor and Dvergr Allies, are in MIGRATION-for-mod-authors.md.
Exempt a creature. Set the ZDO key SoMStealthExempt to 1 and SoM will not simulate it — vanilla AI runs untouched, and every one of our patch sites checks the flag.
nview.GetZDO().Set("SoMStealthExempt".GetStableHashCode(), 1); // owner-side
bool exempt = ShadowsOfMidgard.StealthExemption.IsExempt(character);
Read what SoM knows. SoMInterop resolves a creature's detection state without reflection. The SoM_Ack key proves SoM actually reached that creature, so you can tell "SoM says unaware" apart from "SoM never got here". AwarenessSystem.GetData(Character) remains available with its 1.x signature and never returns null.
Per-creature directives. ZDO keys tune a single creature: sense-range multiplier, force alert, ignore line of sight, or apply a named AI profile.
Patching alongside us.
- Harmony id
wubarrk.shadowsofmidgard, unchanged since 1.0 — existingHarmonyBefore/HarmonyAfterattributes still resolve. - Our sense patches are prefix-only, run at
Priority.Low, and honour__runOriginal. Anything that wants to win by priority, wins. - We do not patch
BaseAI.IsAlerted,MonsterAI.UpdateTarget,Character.DamageorCharacter.OnDamaged.
🔄 Upgrading
Your config carries over. The schema migrates automatically and backs up to .cfg.bak first.
From 1.9.x, three things you will notice:
- Armour finally matters. A packaging fault meant the mod could not touch the game's objects at runtime, so the registries stayed empty and every patch fell through to vanilla. Fixed — see the changelog for the full account.
ShowPatchSagais reset to off and now means "print the full startup tables". A healthy launch reports in two lines instead of thirty-seven; failures still print everything.- Your armour catalogue is rebuilt and is roughly a fifth the size. It was cataloguing monster attacks — a fuling's club, an abomination's swing — because vanilla counts those as equipable items. They can never be picked up by a player, so nothing you can wear changes score.
From 1.x, additionally:
- Your HUD widgets move. They used to be anchored to the middle of the screen, which put the eye on your crosshair. They now anchor bottom-left. Drag them where you want them.
- Alerted creatures lose you again. They could previously sense you through walls once alerted.
- Backstab works properly. The bonus is gated on the target not being alerted, and 1.x had that flag frozen off — so backstabs were landing at full multiplier when they should not have been.
Mists of Avalor and Dvergr Allies need no changes.
🧩 Compatibility
| ✅ Armour mods | Classified automatically. Set RebuildArmorData once after installing. |
| ✅ Creature mods | Anything deriving from MonsterAI or AnimalAI is picked up automatically, spawned or pre-existing. Any other BaseAI subclass is driven by sensing alone — it gets the perception model and nothing touches its behaviour. |
✅ AnimalAI |
Covered since 2.0.1. Deer, hares, chickens, boar piglets, wolf cubs and lox calves obey stealth, and drop a lost target immediately rather than fleeing out vanilla's 4-second timer. Turn off with DriveAnimals. |
| ⚠️ Other AI mods | We yield on priority and never double-assert. If something conflicts, turn on ShowPatchSaga to see exactly what we installed. |
📦 Dependencies
⚠️ Requires: BepInEx — and nothing else. No Jotunn, no JSON library, no shared framework. The plugin is a single DLL.
Support: Discord · Published on Thunderstore and Hexium.
License: MIT — use, modify, and redistribute freely, including commercially; just keep the copyright notice. See LICENSE.md for the full text.