You are viewing a potentially older version of this package. View all versions.
MidnightMods-ValheimEnforcer-0.20.0 icon

ValheimEnforcer

Make your friends use the agreed mods and stop bringing stuff into the server.

Date uploaded 3 days ago
Version 0.20.0
Download link MidnightMods-ValheimEnforcer-0.20.0.zip
Downloads 89
Dependency string MidnightMods-ValheimEnforcer-0.20.0

This mod requires the following mods to function

denikson-BepInExPack_Valheim-5.4.2333 icon
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.2333
ValheimModding-Jotunn-2.29.2 icon
ValheimModding-Jotunn

Jötunn (/ˈjɔːtʊn/, 'giant'), the Valheim Library was created with the goal of making the lives of mod developers easier. It enables you to create mods for Valheim using an abstracted API so you can focus on the actual content creation.

Preferred version: 2.29.2
ValheimModding-YamlDotNet-16.3.1 icon
ValheimModding-YamlDotNet

Shared version 16.3.0 of YamlDotNet from Antoine Aubry and contributors, net47 package for use in Valheim mods. Maintained by the ValheimModding team.

Preferred version: 16.3.1

README

Valheim Enforcer

Valheim Enforcer is a lightweight Mod Synchronization, and Server sided character progression enforce tool.

This mod is designed to be a drop-in, no maintenance solution for those who are wary of configuration, or those that would rather spend time playing than configuring.

By default this mod will enforce character server saves and require clients to only connect with mods that are installed on the server. All of this is configurable.

Feature Roadmap

The following features are not yet implemented but currently planned:

  • Automatic Mod suggestions/download-links for clients that are missing mods or have incorrect versions
  • Platform ID based 'Moderator' mod list that allows server owners to easily give mod permissions to specific players without making them admins

Got a bug to report or just want to chat about the mod? Drop by the discord or github.

discord logo github logo

Features

Server saved character progression lock. All of the following features are configurable (server authoratative).

  • Character progress is saved on the server
  • Prevents characters from bringing untracked items onto the server
  • Prevents characters from raising skills externally
  • Optionally limits each account to a single character, with an exemption list (One Character Per Account)
  • Imports existing characters from ServerCharacters so players migrate without losing anything (Migrating from ServerCharacters)

Mod Enforcement. All of the following features are configurable (server authoratative).

  • All mods are checked on connection, allows strict version enforcement
  • Prevents users connecting with mods not listed
  • Optional per-mod lists for required, optional, admin-only and server-only mods
  • Optional SHA256 file verification of client plugin DLLs, so a recompiled mod is rejected even when its version string is untouched

Nothing needs configuring for the default behaviour — every mod the server loads becomes a required mod. Mod List covers the file for when you want something else.

Discord notifications. Optional, off until you paste in a webhook URL.

  • Joins, leaves, server startup/shutdown/save, cheat bans, and refused connections
  • Each category can post to a channel of its own, or all of them to one
  • Every message is a template you can rewrite, including role pings (Discord Notifications)

Mod List

The mod list lives in BepInEx/config/ValheimEnforcer/Mods.yaml. Both sides need the mod installed, but only the server's copy decides anything: the only thing a server reads out of a client is the list of plugins that client actually loaded.

You do not have to write this file. Install the mod, start the server, and every plugin the server loaded is now required of everyone. The rest of this section is for when you want something other than "everybody runs exactly what the server runs".

The file is regenerated at startup and re-read within ConfigPollIntervalSeconds (30 by default) of being edited, so you can change it on a running server. Comments you write on their own line are kept across those rewrites and stay attached to the entry below them; a comment sharing a line with a value is not, since that line gets rewritten from scratch.

The five lists

List Who fills it in Client has the mod Client does not
activeMods Generated, every start
requiredMods Auto-populated, then yours allowed rejected
optionalMods You allowed allowed
adminOnlyMods You admins only, everyone else rejected allowed
serverOnlyMods You rejected allowed

Every list is keyed by the mod's BepInEx plugin GUID — Azumatt.AzuCraftyBoxes, not AzuCraftyBoxes. It is the GUID in the plugin's BepInPlugin attribute, and the surest place to read it off is the server's LogOutput.log, where BepInEx lists each plugin as it loads. A mod that appears in none of the lists is rejected.

activeMods is what this machine loaded. It is rebuilt from the running plugins on every start and never read back out of the file, so editing it does nothing. That is deliberate: it is also the list each side reports about itself during the handshake, and a list taken from a text file is a list a player can type whatever they like into.

serverOnlyMods is for mods the server runs and nobody else needs — a map generator, a backup tool, a Discord bridge. It keeps them out of requiredMods without demanding them of anyone. It is not the list for client-side mods: a client that installs a server-only mod is rejected for it, because that mod is on no list that permits it. Client-side mods belong in optionalMods.

An entry

requiredMods:
  Azumatt.AzuCraftyBoxes:
    pluginID: Azumatt.AzuCraftyBoxes
    version: 1.8.13
    name: AzuCraftyBoxes
    enforceVersion: true
Field What it does
pluginID The plugin GUID again. The key above it is what lookups actually use
version The version to compare against, kept current for you
name Human-readable label, for logs and the disconnect screen
enforceVersion When true, a client's version must match exactly. Defaults to false
acceptedHashes, hashSource, thunderstorePackage, hashEnforcement File verification — see Mod File Verification

Version comparison is an exact string match, so 1.0 and 1.0.0 count as a mismatch. Fields sitting at their default are not written out, which is why most entries are three lines. If you find a versionStrictness field in an older file, it does nothing and can be deleted.

What happens when someone connects

Situation Result
Missing a mod from requiredMods Rejected, and told which
Running a mod that is on no list Rejected as a non-allowed mod
Version differs where enforceVersion is set Rejected as a version mismatch, naming the version to install
Running an adminOnlyMods mod without being an admin Rejected

The client runs the same comparison against the server's list and shows the result in the connection error window, but that is only feedback for the player — the server decides, from its own file. With Discord notifications enabled, a rejection is posted with the offending mods listed.

Handled for you

What Controlled by
activeMods rebuilt from the plugins actually loaded always
Any loaded plugin not already on a list is added to requiredMods, with enforceVersion: false AutoAddModsToRequired (on)
A mod's version is corrected in whichever list holds it when you update the mod always
The SHA256 of every plugin the server loads is recorded as its accepted hash RecordHashesForLoadedMods (on)
Mods pinned with a thunderstorePackage are downloaded and hashed ResolveThunderstoreHashes (off)
The file is rewritten with all of the above UpdateLoadedModsOnStartup (on)
Edits are picked up without a restart ConfigPollIntervalSeconds (30)

Updating a mod on the server therefore needs no edit here at all — the version follows it, in whichever list you put it in.

What you write yourself

  • Membership of optionalMods, adminOnlyMods and serverOnlyMods. Nothing is ever added to these automatically; move an entry out of requiredMods by hand.
  • enforceVersion: true. Auto-added mods are always written with it off, so a client that is a patch version behind is not locked out of a server that never asked for exact versions.
  • thunderstorePackage, hashEnforcement, and any Manual hash.

Settings

All of these are server-side and synced to admins, so an admin can change them in-game and the server stays the authority.

Setting Section Default Effect
AutoAddModsToRequired Mods true Adds any loaded plugin that is on no list to requiredMods. Turn it off to curate the file by hand — mods you have not listed are then rejected rather than adopted
UpdateLoadedModsOnStartup Mods true Writes version corrections, auto-added mods and recorded hashes back to the file. With it off, all of that still applies for the session but nothing is saved
HashEnforcement Mods WhenKnown File verification mode — see Mod File Verification
RecordHashesForLoadedMods Mods true Records the hash of every plugin this machine loads. Needs UpdateLoadedModsOnStartup to reach disk
ResolveThunderstoreHashes Mods false Downloads and hashes mods pinned with a thunderstorePackage. Off by default because it makes outbound requests
ConfigPollIntervalSeconds Advanced 30 How often the file is checked for edits
HashComputeTimeoutSeconds Advanced 30 Safety valve for a stalled disk during startup hashing, not a tuning knob
ThunderstoreMaxArchiveMB Advanced 128 Largest package the resolver will download; bigger ones are skipped and logged

Discord.NotifyWrongMods (on) posts a message naming the mods whenever a player is rejected for a mismatch. It can go to a channel of its own, and the wording is yours to change — see Discord Notifications.

Recipes

Lock the pack to exact versions. Set enforceVersion: true on every entry you care about. There is no global switch — it is per mod on purpose, so one mod that is fussy about its version does not force the whole list to be.

Let players use a client-side mod. Move its entry from requiredMods to optionalMods, or add it there if the server does not run it. They can then connect with or without it.

Give admins a tool nobody else may run. Put it in adminOnlyMods. Admin status is read from the server's admin list at connect time, so no client can claim it.

Stop a server-side mod being demanded of clients. Move it to serverOnlyMods. Note that this also means no one may connect with it.

Require a mod the server does not run. Add it to requiredMods by hand with its GUID, version and name. To verify the file as well, give it a thunderstorePackage and turn on ResolveThunderstoreHashes.

Mod File Verification

Version checks only compare the version string a client declares, so somebody who downloads a mod, edits the numbers and rebuilds it — keeping the version the same — passes. File verification closes that by comparing a SHA256 of the DLL each plugin was actually loaded from.

HashEnforcement (server config, Mods section) controls it:

Value Server has a hash for the mod No hash, required/admin mod No hash, optional mod
Off not checked not checked not checked
WhenKnown (default) enforced allowed allowed
Strict enforced rejected allowed

WhenKnown means turning this on breaks nothing: only mods you have actually pinned are enforced. Strict is for a fully pinned server and deliberately fails loudly when a required mod has no hash on file.

Any mod in Mods.yaml can override the server setting with hashEnforcement: Off | WhenKnown | Strict. The usual setup is WhenKnown globally with hashEnforcement: Strict on the handful of mods that actually affect balance.

Getting hashes on file

  • Mods the server loads pin themselves. RecordHashesForLoadedMods (on by default) writes the hash of every plugin the server runs into Mods.yaml at startup.
  • Client-only mods — a UI or QoL plugin the server never loads — need one of:
    • By hand. Put the SHA256 in acceptedHashes and set hashSource: Manual. Get-FileHash -Algorithm SHA256 <file>.dll produces it. Nothing else ever overwrites a Manual entry.
    • From Thunderstore. Set thunderstorePackage: Owner-ModName-Version and enable ResolveThunderstoreHashes. The server downloads that package, hashes the DLLs inside it in memory, records them and discards the download. It re-downloads only when you change the pinned version. Only thunderstore.io and its CDN are ever contacted — arbitrary download URLs are not supported on purpose.
requiredMods:
  shudnal.ExtraSlots:
    pluginID: shudnal.ExtraSlots
    version: 1.1.20
    name: Extra Slots
    thunderstorePackage: shudnal-ExtraSlots-1.1.20
    hashEnforcement: Strict

Things worth knowing

  • Recorded hashes are sent to clients on purpose, so the disconnect screen can name the mod that failed. They are not secrets — anyone can download the package and hash it themselves.
  • A recorded hash pins the version too. A different build of a mod is a different file, so a client on another version fails the file check whether or not enforceVersion is set on that entry. That rejection is reported as a version mismatch, naming the version to install — "modified mod files" is kept for a file whose version matches the server's and whose contents do not, which is the case where reinstalling actually helps.
  • Plugins loaded from memory rather than from a file (BepInEx ScriptEngine, in-game plugin loaders) cannot be verified. They report as dynamic and will be rejected once the server enforces that mod. The client logs a warning about this at startup, before you try to connect.
  • Under Strict, enforcement is deferred for mods whose thunderstorePackage has not resolved yet, but only until the first resolve pass after server start finishes. That window is bounded and logged; it exists so a restart does not lock everyone out for the few seconds the downloads take.
  • BepInEx patchers (BepInEx/patchers/) are not plugins and are not covered by any of this.

Structure validation. Off by default, server authoritative.

  • Catches a client spawning world-generation geometry — dungeon rooms, dvergr towns, ruins — instead of building
  • Catches a piece whose health has been set above what its prefab allows, which is how an indestructible structure is made
  • Blueprint and bulk-building mods cannot trip it, by design (Structure Validation)

Cheat detection (enabled by default, configurable).

  • Automatic log, kick or ban for common cheating utilities
  • ValheimTooler is detected even when injected mid-session (after mod validation) and is always auto-banned
  • Optional Discord notification whenever a player is banned for cheating, routable to a staff-only channel

Clients are checked against a catalog of known cheat tools across three vectors:

Vector What it looks at Why it exists
Process Names of running programs Catches the tool while it is open
Module DLLs loaded into Valheim itself Sees a cheat that already injected and then closed its launcher, and survives renaming the tool
Window Window classes and titles Catches tools renamed to dodge the process check (a "Cheat Engine" window title does not change when you rename the exe)

Detected by default: WeMod / Wand / Infinity, Cheat Engine (including the magic-engine fork and injected speedhack/DBK modules), ArtMoney (SE and Pro), PLITCH, Speed Gear, Squalr, WPE Pro, generic trainers such as FLiNG and Cheat Happens, and the loaders used to deliver Valheim cheats — ValheimTooler, ValHack, Valheim Mod Menu, SharpMonoInjector, Xenos and Extreme Injector.

Tools with no purpose other than cheating (the loaders and injectors above) are banned on sight. Everything else follows ActionOnDetection, which defaults to Kick. The auto-ban decision is made by the server from its own catalog — a client only ever reports what it saw, so a tampered client cannot get another player banned.

Some window signatures are low confidence: Cheat Engine's TfrmMain/TfrmMemView classes are Delphi's default names for forms called frmMain/frmMemView, and plenty of legitimate Delphi software carries them. A low-confidence sighting is reported and shows up in the server log marked (weak), but it is never kicked or banned on its own, regardless of ActionOnDetection — enforcement requires a strong signal (process name, injected module, or window title).

Window titles are ignored on windows that display content rather than run it — browsers and Electron apps, UWP frames, File Explorer, and terminals. A YouTube tab titled "cheat engine tutorial", a Discord channel discussing ArtMoney, or a folder named after a tool will not match, and because those windows are skipped outright, browser tab titles are never sent to the server.

Privacy: only matched entries are sent to the server. A player's full process list never leaves their machine.

False positives: generic framework window classes are logged but never enforced, and browser/Explorer/terminal titles are not matched at all (see above), so neither a Delphi utility in the tray nor a YouTube tab about a cheat tool can get anyone kicked. Developer tools that also read game memory — x64dbg, Process Hacker / System Informer, HxD, ReClass.NET, Frida, Fiddler — are deliberately not detected by default, because modders and streamers use them routinely. Add them to AdditionalCheatProcesses if your server wants them treated as cheats. Aurora, Process Lasso, AutoHotkey, and overlay tools like MSI Afterburner and OBS are excluded on purpose and are not recommended additions; see the config file comments for the reasoning. If something legitimate trips a detection, add it to IgnoredCheatProcesses, which overrides everything else.

Disclaimer: Valheim is client authoratative and without extremely invasive measures, cheating cannot be fully prevented. Process-name detection in particular is a speed bump rather than a wall — renaming Cheat Engine is a documented feature of the tool, and trainer executables are renameable by design. The module and window-title checks exist because they survive a rename, but a client that can cheat can also lie about what it is running. The same applies to mod file verification: the hash is computed and reported by the client, so it stops a recompiled mod, not a patched enforcer. What it changes is the cost — from "edit one file and rebuild" to "reverse engineer and patch the anti-cheat", which is a real barrier to the people who actually do the former and none at all to the people who can do the latter.

What the server does refuse to take on trust is anything it can decide for itself. The sender of every network message is verified against the connection it arrived on, so a modified client cannot act as another player — it cannot run an admin's commands, get someone else banned, or write to another account's character. A character save or inventory delta is only ever accepted for the account and character the connection joined as. The join rules (item confiscation, skill clamping, custom-data reset) are re-run on the server for returning characters, not just applied on the client, and a first save from a brand-new character is held to the new-character rules server-side. These are the parts a client cannot lie its way past; the caveats above are about the parts — what mods it runs, what it has in its inventory this instant — that it still can.

Structure Validation

Off by default. Set EnableStructureValidation to true and the server starts checking the objects clients create, instead of taking every one of them on trust.

It is the answer to a specific report: large structures appearing on a server that show no "Crafted by" on hover, cannot be destroyed, and flattened the terrain where they landed. All three are the same thing — somebody spawning world-generation geometry. A dvergr archway, a crypt room and a stone ruin are ordinary prefabs with ordinary health; what they are not is anything a player can build. There is no craftsman on them because nobody crafted them.

Valheim gives the server nothing to work with here. There is no "place piece" message — a client instantiates the object locally and its data arrives in the same stream as everything else, which the game accepts without checking the prefab, the position, or a single value in it. So this checks it.

The two checks

Check What it looks at Setting
Non-buildable structure A client creates something that is in no build menu DetectNonBuildableStructures
Excessive health A client sets a piece's health above what its prefab allows DetectExcessiveStructureHealth

Blueprint mods are safe, and not because of an allowlist. What makes a prefab placeable is being in a piece table, and every build path uses those tables — the hammer, the hoe, the cultivator, and every blueprint, bulk-build or planned-piece mod, because they all place out of the same menus. Mods register their own pieces into those tables too, so a server's custom content is covered without anybody listing it. A prefab with no table entry is one no build tool can reach.

Repairing is never flagged. Valheim has no invulnerability flag; an unbreakable piece is just an absurd number in the health field. The ceiling is the prefab's own maximum, including any increase from a world modifier, and a full repair writes exactly that.

Nobody is blamed for somebody else's structure. Ownership of an object moves to whichever player is nearest, every couple of seconds. Health that was already too high before a client wrote to it is attributed to no one, so walking past a cheated structure — or hitting it — cannot get an innocent player reported. enforcer-structures-scan is how those get found.

There is a second door: SpawnObject, a routed message nothing in the game ever sends, which asks the server to instantiate any prefab by hash — a creature or an item as easily as a structure. BlockSpawnObjectRPC (on) refuses every one of them and, by default, posts the block to your moderation channel; it follows StructureValidationAction for what happens to the player, which defaults to Log, so out of the box it blocks and reports without kicking or banning. A structure spawned this way is reported as a structure detection either way. Turn it off to fall back to refusing only non-buildable structures through SpawnObject, if a mod on your server legitimately uses the call.

Settings

Setting Default What it does
EnableStructureValidation false Master switch. Everything below is inert until this is on
DetectNonBuildableStructures true The build-menu check
BlockSpawnObjectRPC true Refuse every client-sent SpawnObject RPC (all prefabs, not just structures) and post it to the moderation channel
DetectExcessiveStructureHealth true The health-ceiling check
StructureValidationAction Log What happens to the player: Log, Kick or Ban. Detections are logged and posted to Discord regardless
RemoveDetectedStructures false Whether the structure itself is deleted
StructureValidationExemptAdmins true Whether the adminlist is exempt
StructureHealthAllowedMultiplier 1 Headroom on the health ceiling, for mods that raise piece health at runtime rather than on the prefab
IgnoredStructurePrefabs (empty) Prefab names never flagged, matched as a substring

RemoveDetectedStructures is deliberately a separate switch from the action, and starts off. Run with it off first and read the log for a few days: a wrong detection that only writes a line costs you nothing, and a wrong detection that deletes something costs a player their build. IgnoredStructurePrefabs is the fix when you find one — reach for it rather than turning the whole feature off.

Admins are exempt by default, unlike every other exemption in this mod. Spawning a non-buildable prefab is what devcommands is for, and an admin decorating with it should not have to know this feature exists. Set it to false to hold admins to the same rule as everyone else.

Finding what is already there

The live check only sees a structure as it arrives, which is no help to a server that was hit last month. enforcer-structures-scan walks every object in the world and reports the ones that look placed rather than generated — prefab, coordinates, health and crafter — grouped by prefab so the shape of it is visible at a glance.

enforcer-structures-scan scan
enforcer-structures-scan scan dvergrtown
enforcer-structures-scan remove confirm dvergrtown

scan changes nothing. Removal needs the word confirm typed out, because it is the one thing here that cannot be undone. Both forms take an optional prefab filter, matched as a substring, which is how you act on one finding out of a long report. It runs from the server console or from a connected admin's client, and non-admins are refused server side.

The scan runs both checks whatever your Detect* settings say — you asked for a picture of the world, so you get the whole picture. It works in slices across frames, so a large world does not stall the server while it runs.

It never touches generated content. Real dungeons and ruins are non-buildable structures too, so anything inside a zone the world generated a location into is excluded, and the report says how many were skipped that way. The cost is stated plainly: a structure spawned right next to real ruins is excluded along with them. Missing one is recoverable and deleting a dungeon is not — and the live check catches that case anyway, wherever it happens.

Removal also refuses to delete more than 500 objects at once without a prefab filter. A number that large means this server's content classifies differently from vanilla's, not that somebody placed five hundred structures by hand.

Things worth knowing

  • The terrain is not put back. The flattening arrives as separate objects from the structure, so removing the structure leaves the ground as the cheat left it. Re-terraforming is still yours to do.
  • What is detected is a structure. Something with no piece component at all — scenery, a plant, a creature — is outside the first check on purpose. Requiring one is what keeps tombstones, dropped items, arrows and animals out of a detector that can delete things.
  • Detections name the connection, not the character. A character name is whatever a client says it is, and the crafter field on a cheated piece is empty by definition. Structures found by a scan are reported with no player at all, because nothing durable records who created an object.
  • A world-generated piece can be damaged. Locations spawn with their pieces pre-damaged, which is below the ceiling and never flagged.

One Character Per Account

Off by default. Set EnforceCharacterLimit to true and an account may only join with a character this server already has a save for — anyone else is turned away at the connect handshake and told which character to come back as. Nothing about this is retroactive punishment: every character an account already has stays playable, so switching it on locks nobody out. It only stops the next new character.

There is no separate list to maintain. The characters an account "has" are exactly the saves under BepInEx/config/ValheimEnforcer/Characters/<PlatformID>/, which the mod already writes on the first join. So a brand new player joins normally, that character becomes theirs, and a second one is refused. Run enforcer-player-list to see who has what.

Giving someone a fresh start is deleting their character's .yaml from that folder while they are offline. The slot frees itself; the next character they connect with takes it.

Settings

Setting Default What it does
EnforceCharacterLimit false Master switch. Everything below is inert until this is on
MaxCharactersPerAccount 1 How many characters an account may have. Accounts already over it keep what they have
CharacterLimitExemptAccounts (empty) Comma-separated account ids allowed any number of characters
CharacterLimitExemptAdmins false Whether being on the adminlist is itself an exemption
NotifyCharacterRejected true Post refused joins to Discord, if a webhook is configured

Exemptions are deliberately independent of admin rights — an exempt account does not need to be an admin, and an admin is not exempt unless you list them or turn CharacterLimitExemptAdmins on. Ids go in either form: Steam_76561198012345678 or the bare 76561198012345678. Note that this setting syncs to connected clients like every other server setting, so the ids in it are visible to players; if that matters for your server, the alternative is editing it in the config file with the list left empty in-game.

Things worth knowing

  • Identity is the character name. It is the only thing about a character the server learns during the handshake. A player who deletes "Bjorn" locally and makes a new "Bjorn" gets past the check — though since this mod pushes the saved Bjorn's items and skills back on join, it is a poor way to get a clean slate.
  • The save holds the slot, not the player. Delete someone's save while they still have that character locally and it counts as new again next time they join.
  • If the server cannot read its character folder at all, joins are allowed and a warning is logged. A disk problem should not lock out your playerbase.
  • On a player-hosted (listen) server the host never goes through the connect handshake, so the host's own account is not checked. Dedicated servers check everyone.
  • Enforcement is tied to the game's network version. If Valheim ships a new one, the rule stops applying until the mod is rebuilt against it — the check goes quiet rather than guessing at a changed wire format.

Discord Notifications

Paste a webhook URL into Discord.WebhookUrl and the server starts posting: who joined, who left and whether their save was up to date, who was turned away and why, and when the server came up or went down. Nothing else needs configuring.

Everything below is for when you want more than that — a channel per kind of message, different wording, a role ping when somebody gets banned.

One channel or several

Every category falls back to WebhookUrl, so a category URL is only worth setting when you want that traffic somewhere else.

Setting Covers
WebhookUrl Everything, unless a category below overrides it
WebhookUrlPlayerActivity Joins and leaves
WebhookUrlServerStatus Startup, shutdown, world saves
WebhookUrlModeration Cheat bans, character-limit rejections, structure detections
WebhookUrlModMismatch Connections refused over mods

The usual split is join/leave into a busy activity channel, moderation into somewhere only staff can read — those messages name the account behind a ban — and mod mismatches into wherever players ask for help, since the message already lists what they need to fix.

Leaving WebhookUrl empty and setting only one category is fine: that category posts and nothing else does.

Settings

Setting Default What it does
WebhookUrl (empty) Master switch. Empty means no notifications at all
WebhookUrl… (the four above) (empty) Per-category override; empty falls back to WebhookUrl
ServerLabel (empty) Name for this server as {server} in templates. Only useful when several servers share a channel
NotifyServerStartup true Server came online
NotifyServerShutdown true Server going down
NotifyWorldSaved false Every world save. Off on purpose — the autosave fires roughly every 20 minutes, all day
NotifyPlayerJoined true Player joined
NotifyPlayerLeft true Player left, and whether their save was current
NotifyWrongMods true Connection refused over a mod mismatch
NotifyCheaterBanned true Player banned for cheat usage
NotifyCharacterRejected true Connection refused by EnforceCharacterLimit
NotifyStructureFlagged true Structure validation caught a player placing something invalid. At most one post per player per minute, however many objects were involved

These are deliberately not synced to clients — a webhook URL is a password in URL form, and syncing it would hand it to everyone who connects. Edit them in the config file or in Configuration Manager on the server.

Rewriting the messages

What each message looks like lives in BepInEx/config/ValheimEnforcer/Notifications.yaml, written on first start and re-read within ConfigPollIntervalSeconds of being edited. No restart. It ships with the wording this mod has always used, so it changes nothing until you edit it.

Each entry is the message. Not a description of one — the literal body posted to Discord, placeholders and all. Nothing is added to it and nothing is filled in for you.

playerJoined: |
  {
    "embeds": [{
      "title": "Player Joined",
      "color": {colorGreen},
      "timestamp": "{timestamp}",
      "fields": [
        {"name": "Player", "value": "{player}", "inline": true}
      ]
    }]
  }

That means anything Discord accepts works — author, footer, thumbnail, image, url, several embeds in one post. Their webhook reference is the full list, and none of it needs a change to this mod.

A content line is the one place a mention actually pings; Discord never resolves one inside an embed:

cheaterBanned: |
  {
    "content": "<@&123456789012345678> a player was just banned",
    "embeds": [{
      "title": "Cheater Banned",
      "color": {colorRed},
      "fields": [
        {"name": "Player", "value": "{player}", "inline": true},
        {"name": "Detected", "value": "{reason}", "inline": true}
      ]
    }]
  }

Removing things

Because the body is sent exactly as written, anything you delete is simply not in the message. There is no separate switch for turning a piece off.

Delete Effect
"timestamp" No date stamp under the embed
"title" / "description" That line is gone
One entry in "fields" That row is gone
"fields" No rows at all
"color" No coloured stripe down the left edge
"content" No plain-text line above the embed, and no pings
"embeds" A plain-text message and nothing else — needs "content" to survive
The whole event key The built-in default comes back next start

So the playerJoined above, stripped of its timestamp, colour and title, is a bare one-line post:

playerJoined: |
  {
    "embeds": [{
      "fields": [
        {"name": "Player", "value": "{player}", "inline": true}
      ]
    }]
  }

Watch the commas. JSON does not allow one before a } or a ], and a dangling comma is what deleting the last item in a list leaves behind. It is checked for — see below — so this costs you a log line rather than a silent outage.

To stop an event posting at all, turn off its Notify* setting. Emptying its template is not the way: Discord rejects a message with no content and no embeds, so the mod skips it and logs instead.

Placeholders

Written {likeThis}. Available to every event:

Placeholder Value
{server} ServerLabel from the config, empty unless you set it
{world} World name
{onlinePlayers} How many are connected
{timestamp} Current time, in the ISO-8601 form Discord's "timestamp" field wants
{colorGreen} {colorAmber} {colorRed} {colorGrey} The numbers Discord wants for "color"

Colours are offered as placeholders only so the shipped palette is convenient. "color" takes any number, so "color": 3447003 is a perfectly good blue.

Then per event:

Event Placeholders
serverStartup serverShutdown worldSaved (common only)
playerJoined {player} {playerId} {isAdmin}
playerLeft {player} {playerId} {disconnect} {savedData} {deltaWindow} {statusColor}
cheaterBanned {player} {playerId} {reason} {detections} {action}
characterRejected {character} {playerId} {reason} {maxCharacters}
modMismatch {player} {playerId} {summary} {missingMods} {extraMods} {versionMismatches} {adminOnlyMods} {hashMismatches} {unverifiedMods}
structureFlagged {player} {playerId} {prefab} {position} {reason} {creator} {health} {count} {action}

{summary} on a mod mismatch is the whole rejection written out as prose, which is what the default shows. The lists beside it are the same information split up, for when you want to say something specific — ping the mod team only when {hashMismatches} is involved, or post nothing but {missingMods} in a support channel. {versionMismatches} names both versions per mod — com.example.Mod (needs 1.4.2, has 1.3.0) — and a wrong version lands there even when the file check is what caught it, so {hashMismatches} only ever holds a file that fails at the version the server expects.

{statusColor} is green after a clean logout and amber after a crash or timeout. The default playerLeft uses it as its colour, which is how one template covers both.

On structureFlagged, one message covers the whole batch — a cheat tool drops a village in a second, and a post per piece would walk the webhook into Discord's rate limiter. {count} is how many objects were involved and {prefab}, {position}, {health} and {creator} describe the first of them; the server log has the rest.

Run enforcer-notify-test playerJoined to post any event with stand-in data and see the result. It works from the server console or from a connected admin's client — in that case the server does the posting and reports back into your console, since the webhook URL is never sent to clients. It ignores the Notify* switches but still needs a webhook. enforcer-notify-test list names the events.

Non-admins are refused server side, so the command is not a way for a player to make your server post to Discord. There is a short cooldown between tests, which keeps a stuck key from walking the webhook into Discord's rate limiter and silencing the real notifications along with it.

Things worth knowing

  • A broken template does not stop notifications. Every template is checked when the file loads. One that is not valid JSON is reported in the log with a line and column, and that event falls back to its built-in default until you fix it — everything else keeps posting. Bad YAML around the templates keeps whatever was already loaded.
  • A broken template is never overwritten. The fallback is in memory only; your text stays in the file exactly as you typed it, so restarting mid-edit does not cost you the version you were fixing.
  • The check is for syntax, not for Discord's rules. It catches dangling commas, unclosed braces, unterminated strings and single quotes. It does not know that an embed title caps at 256 characters or that "colour" is not a field. Those come back as an HTTP status in the log.
  • Values are escaped and truncated for you. A player called Bj"orn cannot break the document, and a long {summary} or {extraMods} is trimmed rather than being allowed to push the post past Discord's limits.
  • A mistyped placeholder is left visible in the message rather than silently blanked, so {playr} arrives as {playr} and tells you what to fix.
  • @everyone and @here work in content. There is no guard against it, and the event you put it on may fire far more often than you expect. Test with a role ping first.
  • Player names go to Discord whenever notifications are on. That is the point of the feature, but worth knowing before pointing it at a public channel.
  • Comments you add are kept. A # note on its own line stays with the entry below it when the mod rewrites the file. One sharing a line with a value is not, because that line gets rewritten.
  • The world-save message means the save started. Valheim writes the world on a background thread, so nothing can honestly report the moment it finished. Skipped saves are not announced at all.
  • Turning an event off costs nothing. The message is never built, so a server that only wants ban alerts does no work for the rest.

Migrating from ServerCharacters

Coming from ServerCharacters? Valheim Enforcer can read the character files it leaves behind, so your players keep their inventories and skills instead of having everything confiscated on their first join.

The two mods cannot run at the same time. They both take over character saving and would fight over every profile, so Enforcer declares ServerCharacters incompatible. Be aware of how BepInEx enforces that: it refuses to load Enforcer, not ServerCharacters. A server with both installed runs with no Enforcer at all — no mod enforcement, no character sync, no anti-cheat — and the only sign is a line in the BepInEx log. So the order matters:

  1. Stop the server.
  2. Uninstall ServerCharacters. Leave its character files alone — they are what gets imported.
  3. Set ImportServerCharacters = true in ValheimEnforcer.cfg.
  4. Start the server and read the log. It reports how many characters were imported, skipped or unreadable.
  5. Optionally set it back to false. Leaving it on is harmless — characters that already have a save are skipped, so the pass does nothing on later starts.

Want to look before you leap? With the server running, an admin can use enforcer-characters-import dryrun, which reports exactly what it would do and writes nothing. enforcer-characters-import import runs it on demand, and adding force overwrites saves that already exist (normally they are left alone).

The importer only ever reads ServerCharacters' files. Nothing is moved, renamed or deleted, so your old setup stays intact if you want to go back.

What comes across

Inventory (including item quality, variants, crafter names and the custom data mods like EpicLoot attach to items), skill levels, and per-player custom data.

Food, guardian power, known recipes/stations/materials, trophies, map data and spawn points do not come across — Enforcer's character store does not model them. In practice players do not notice: ServerCharacters also writes each player's own local character file, so all of that is still on their machine. What the server needs is only enough to recognise their stuff and stop confiscating it.

The exception is a player who has lost their local character file. Under ServerCharacters the server copy was fully authoritative and could restore everything; here they would come back with their items and skills but not their recipes or map. That is a difference between how the two mods store characters, not something the import can fix.

Things worth knowing

  • Files are found automatically in the game's own character folder, which is where ServerCharacters puts them and which follows Valheim's -savedir. Only set ServerCharactersImportPath if you moved them somewhere else.
  • Backups are ignored on purpose — the backups folder, .fch.old, and *_backup_* files. A hardcore character that died is left dead.
  • The character name is taken from inside the profile, not the file name. ServerCharacters lowercases the file name, and its own code misreads names containing an underscore.
  • A corrupt or truncated file is skipped and reported rather than half-imported, and a file written by a newer version of Valheim than this build understands is skipped rather than guessed at.
  • If the import cannot read something, the affected player simply joins as if they were new. It never blocks a connection.

Server Management

Add the mod to your server and to your clients — both sides must run it. Setting up the mod lists is optional; every mod the server loads is required automatically. See Mod List for the file itself, the other three lists, and what is kept up to date for you.

Console Commands

Type enforcer-help for the list, or enforcer-help items for one area of it. Every command names what it did when it finishes — how many characters it found, how many items it moved, how many objects it deleted — so you never have to go and read a log to find out whether it worked.

Command What it does
enforcer-help Lists the commands, grouped by area
enforcer-player-list Every account with a save, and the characters under it
enforcer-items-list What has been confiscated from one character
enforcer-items-return Gives confiscated items back
enforcer-items-clear Deletes confiscated items for good
enforcer-characters-import Imports saves from ServerCharacters (details)
enforcer-notify-test Previews a Discord message (details)
enforcer-structures-scan Finds cheat-placed structures (details)

Everything except enforcer-help needs devcommands, which means being an admin on the server.

They run from the server console and from a connected admin's client alike. From a client, the server does the work and its output comes back into the console you typed in — so a dedicated server, which has no console of its own, is administered entirely from in-game. The server checks admin status itself on arrival, so a client that lies about being one is refused and told so.

Tab completion works past the first argument: tab through the account ids the server actually has, then through that account's characters. EnableTerminalColors (on) colours the output by severity and is a local setting, so it is yours rather than the server's.

Older command names — Enforcer-List-Players, Enforcer-Return-Confiscated and the rest — still work and are listed beside their replacement in enforcer-help.

Restoring user Items

Someone brought on their priceless Epicloot Askavin cloak? Some Prestine +InfinitePower Jewels? You can restore confiscated items!

There are two ways to do so.

  1. In-Game commands
    • Run enforcer-player-list to get the player's account ID and character name
    • Run enforcer-items-list AcountID999999 CharacterName to see what they lost
    • Run enforcer-items-return AcountID999999 CharacterName prefabName (just want it all back? use 'all' as the prefab). If they are online the items go straight into their hands; if they are not, they go into their save and are handed over on their next join. Either way the command tells you which of those happened.
  2. Manual config file edits.
    • Ensure the player is offline (server can be running)
    • If you are unsure about the player's account ID, run enforcer-player-list in-game to get the player's account ID and character name
    • Move any item listed under confiscatedItems to the playerItems list in the player's save file. Player save files are located in BepInEx\config\ValheimEnforcer\Characters\<PlatformID>\playername.yaml on the server.

CHANGELOG

0.20.0

- Routed RPC sender verification (EnforceRoutedRpcSender, Advanced, on by default). Valheim's routed RPC
  carries a sender id the sending client writes, which the server now validates, this is applied to every registered RPC.
   - Client-side messages from the server are now verified before being accepted.
- Character names and account ids are validated before being used as save-file paths, and a connection
  whose character name is not a safe file name is refused at the handshake with a clear reason.
- A player's death is now recorded server-side (from the grave the client creates), so a client that skips
  its own death handling can no longer keep its pre-death inventory.
- Skill levels reported by a client are clamped to the game's valid 0-100 range before being stored.
- Adds structure validation: server-side detection of clients placing structures no build tool can
  place, and of pieces whose health is above what their prefab allows
   - Configurable EnableStructureValidation (off by default) to enable this functionality
   - StructureValidationAction determines the automated response to a player triggering this
   - Admins are exempt by default
- Blocks ZNetScene's SpawnObject RPC (BlockSpawnObjectRPC, on by default), an unused routed call that
  otherwise lets any client have the server instantiate any prefab by hash (creatures and items included,
  not just structures). A block posts to the moderation Discord channel by default and follows
  StructureValidationAction (default Log, so it reports without kicking or banning)
- Adds Enforcer-Scan-Structures, which allows finding existing structures like this
- Adds a structureFlagged Discord notification, routed to the moderation webhook
- Console commands improvements
   - Commands now provide a summary back of what their action taken or result was
   - Output goes to the console you typed in, including when the server ran the command for you.
   - Renamed to enforcer-<area>-<verb>: enforcer-player-list, enforcer-items-list/-return/-clear,
     enforcer-characters-import, enforcer-notify-test, enforcer-structures-scan. Every old name still
     works and is shown beside its replacement in the new enforcer-help
   - Adds enforcer-help, and enforcer-items-list
   - Tab completion now works past the first argument, and offers the account ids and character names
     the server actually has
   - Adds EnableTerminalColors (on, local) to colour command output by severity
   - Fixes clearing confiscated items doing nothing at all when the player was offline
   - Naming a character the server has no save for now says so
- Fixes first-join allowing items on in a specific scenario

0.19.0

- Reduces false positive kicks for having applications which could be cheat-engines running
   - Generic window classes are now a low-confidence signal: the sighting is still reported and
     logged on the server
- Kicks and bans from cheat reports now target the reporting connection's platform ID instead of
  the player name it self-reported, so a crafted report cannot hit another player and duplicate
  character names cannot misfire
- A wrong mod version is now reported as a version mismatch instead of a modified file
   - This covers the case where enforceVersion is off and the recorded hash is what caught it
   - Version mismatches now name both versions - "com.example.Mod (needs 1.4.2, has 1.3.0)" - on the
     disconnect screen, in the server log and in the Discord {versionMismatches} field

0.18.0

- Discord notifications can now be split across channels
   - WebhookUrlPlayerActivity, WebhookUrlServerStatus, WebhookUrlModeration and WebhookUrlModMismatch
     each take a webhook of their own; any left empty falls back to WebhookUrl as before
- Every notification is now a template you can edit, in config/ValheimEnforcer/Notifications.yaml
   - Each entry is the literal message body posted to Discord, anything Discord accepts works, including
     author/footer/thumbnail/image
   - Deleting a key deletes that part of the message: drop "timestamp" and no date stamp is sent,
     drop "embeds" and it becomes a plain text post. Nothing is added back for you
   - A 'content' line is the only place a mention pings - use it for role alerts
   - Placeholders like {player}, {playerId}, {reason} and {missingMods}; a mod mismatch also exposes
     its missing/extra/version/hash lists separately instead of one block of prose
- Adds a world save notification (NotifyWorldSaved, off by default - the autosave is every ~20 minutes)
- Adds ServerLabel, exposed to templates as {server}, for several servers sharing one channel
- Adds Enforcer-Test-Notification, which posts any event with sample data so a template can be
  previewed without waiting for the real thing

0.17.0

- Adds character import from the ServerCharacters mod (ImportServerCharacters, off by default)
   - Reads the character files ServerCharacters leaves behind and turns them into enforcer saves, so
     migrating players keep their inventory and skills instead of being confiscated on first join
   - Item quality, variants, crafter names and mod item data (EpicLoot and friends) come across intact,
     as do modded skills
   - Runs once at server start, or on demand with Enforcer-Import-ServerCharacters, which has a dryrun
   - Existing characters are never overwritten unless 'force' is given

0.16.0

- Adds an optional one-character-per-account rule (EnforceCharacterLimit, off by default)
   - An account may only join with a character the server already has a save for, up to
     MaxCharactersPerAccount; anything else is refused at the connect handshake
   - Characters that already exist are never affected, so enabling it locks out no current player
   - Refused players are told which character to rejoin with, instead of a generic connection error
   - CharacterLimitExemptAccounts allows specific accounts any number of characters, whether or not
     they are admins; CharacterLimitExemptAdmins extends that to the whole adminlist

0.15.0

- Adds file verification of client plugin DLLs at connect time
   - New HashEnforcement setting: Off / WhenKnown (default) / Strict, overridable per mod in Mods.yaml
   - Mods the server loads pin themselves; client-only mods pin by hand or from a thunderstorePackage
- Fixes a mod with the wrong version being reported as both a version mismatch and a non-allowed mod
- Comments in Mods.yaml now survive the startup rewrite, which used to delete them - a note stays
  attached to the entry it was written above
- Documents the mod list in the README: the five lists, how an entry is structured, what is kept up
  to date for you and what you have to write yourself

0.14.1

- Update Jotunn version

0.14.0

- Greatly expands cheat tool detection
   - Detects the loaders used to deliver Valheim cheats, these are banned on sight
   - generic trainers are also detected, with a configurable moderation action (default ban)
- Ban reasons and Discord notifications now name the specific tool and how it was found

0.13.0

- Fixes item duplication on death edgecases
- Improves compatibility with death mods that change what happens to items on death
- Inventory changes are now tracked as they happen instead of being polled on a timer
   - An idle player sends nothing at all; CharacterDeltaTracker is now a rate limit (default 15s, was a 60s poll)
   - Singleplayer and listen-host sessions now keep their character save current mid-session
- Fixes restored items being dropped on the ground when the player had room for them
- Singleplayer fixed skill progress earned during a session being rolled back on death
- Status effects can no longer carry across a death

0.12.0

- Improves multiplayer disconnect saving for extremely large character saves
- Allows server admin editing of save files to be hot-reloaded (please ensure the player you are editing is logged off first)

0.11.1

- Improves accuracy of saves in singleplayer games

0.11.0

- Server-side character saves and delta updates are now written off the main thread
   - Full/delta saves are deserialized, serialized and written on a background worker with an in-memory cache
   - Repeated writes to the same character are coalesced, so a burst of saves (e.g. every client on a "save player profiles" broadcast) can no longer stall the server or time players out
   - Internal storage mode keeps its existing behavior (registry writes must stay on the main thread)
- Full character saves are now pulled by the server instead of riding the world/profile autosave
   - The server asks connected players for a full save every FullSyncPullIntervalMinutes (default 25)
   - No more than FullSyncMaxConcurrentPlayers upload at once (default 5); larger player counts are staggered into waves so incoming saves never spike bandwidth
   - Removes the client-side full-save timer and the Player.Save trigger; routine changes still stream up incrementally via CharacterDeltaTracker, and join/logout still push a full save

0.10.1

- Forward leads character saves to ensure first round of delta saves are not discarded

0.10.0

- Anti-Cheat now enabled by default
- ValheimTooler detection reworked to be more flexible
   - A confirmed ValheimTooler detection is always auto-banned (when cheat detection is enabled)
- Discord notification when a player is banned for cheat usage (NotifyCheaterBanned, default on, requires seperate webhook)
- Cheat Engine process scan throttled
   - ScanIntervalSeconds default raised to 30 (now only affects the Cheat Engine check)
- Added another user to the global ban list

0.9.1

- Admin only mods now strongly restricted to admins

0.9.0

- Added Automatic ban list, built in known-banned
- Added discord notifications (server side) [Configurable!]
   - Notify on player join
   - Notify on player leave
   - Notify on server start
   - Notify on server shutdown
   - Notify on mod mismatch

0.8.2

- Configurable save sync intervals for full saves and delta saves
- Last disconnect status tracked
   - Allows reduction in strictness of item confiscation
- Added a confiscated timestamp
- Improved item return logic to drop items on the ground if the player does not have room for it

0.8.1

- Null check for status effects which no longer exist when adding to character
- Improves Item return RPC logic to deal with partially valid clients
- Improves compatibility with some custom status effects and saved custom data

0.8.0

- Improved Item, skill, status effect, and custom data consistency
- Added a catchall to persist character data when exiting without saving

0.7.3

- Polling filewatcher for better server side support with unix/hybrid storage (default check interval is 30s, configurable)

0.7.2

- Adds support for status effect tracking between sessions (configurable)
   - Status effects (such as poison) will now be applied when you log back in, with their previous durations etc
   - No more save scumming for a 60s poison tick
   - On the plus side, your rested buff now stays between play sessions!

0.7.1

- Adds a very small amount of variance allowed for float rounding when validating item durability
- Adds extra details to the confiscation reason

0.7.0

- Added a confiscation reason field on items confiscated, field is optional but will be set for all confiscated items
- Removed redundant NewCharacterSkillsCleared setting (replaced by NewCharacterSetSkillsToZero)
   - Set NewCharacterSetSkillsToZero default to false
- Added CheatDetector module (in testing, disabled by default)
   - Client-side scanning for ValheimTooler (loaded assemblies) and Cheat Engine (process name, window class, injected speedhack/DBK modules, debugger, time-drift speedhack)
   - New Anti-Cheat config section; default ActionOnDetection=Log
   - Detections reported to server via new VENFORCE_CHEAT RPC

0.6.4

- Cache busting between player sessions
- Fixes character switching allowances for local only usage
- Add Extraslots compatability (restores items to the correct slots for characters with extraslots)
- Restores equipped status of items when they are returned to the player

0.6.3

- Explicitly requires yaml.net

0.6.2

- Improves item durability save bounding

0.6.1

- Adds item durability validation (configurable through ValidateItemDurability setting, default on)

0.6.0

- Improves custom data validation
- Enables Enforcer- commands for admins to retrieve confiscated items
   - List player saves
   - List confiscated items for a player
   - Retrieve confiscated items (give to admin) from a player save
   - Retrieve confiscated items (give to player) from a player save
- Optional (disabled by default) portable mode which stores all data inside the world

0.5.5

- Enforce quality and custom data consistency for all characters, including new characters on first load
- Added extra safety checks for player data settings

0.5.4

- Defaults to enforcing mod versions for active mods
- Automatically updates mod versions in all lists when the mod is updated on the server
- Fixes inconsistent server save IDs when recieving data from the client

0.5.3

- Fixes character fallback logic to more consistently select a non-mutating ID, prefers steamID and playfabID

0.5.2

- Fixes skill removal for new chracters on first load

0.5.1

- Fixes player custom data loading for new characters on first init

0.5.0

- Initial public beta