Dyrr
A door policy: characters that have played elsewhere do not come in. Off until an admin turns it on.
- Features
- How the join check works
- Character protection
- Idle kick
- Inventory watch
- Installation
- Before you turn Enforce on
- Configuration
- Files Dyrr writes
- Console commands
- Multiplayer
- Known limits
- Troubleshooting
- Status
- Bug reports
- Discord
- Licence
- Part of Longhouse
Dyrr is a join policy for Valheim servers. A character that has played on other worlds, that
has used cheats, or that arrives with mods the server does not permit can be refused when it
connects. Nothing is refused until an admin turns Enforce on.
It also kicks idle players on dedicated servers, reports when a character comes back carrying something it did not leave with, and stops your own client from taking a character into a world it does not belong to.
Features
- Six checks at the join: the character has spawned in another world, the game has flagged it for cheats, it has run a console command the game classes as a cheat, its own records disagree with each other, the client is running mods the server does not permit, or it did not answer at all. Each can be switched off on its own.
Enforceis off by default. Every connection is still judged and the verdict is written to the log, so you can see who would be turned away before anyone actually is.- A refused player is told which rule they broke, on their own screen if they have Longhouse Core, and in their own log either way.
- Character protection on the client: a character is bound to the first world it plays in, and the game refuses to start it anywhere else. This works whether or not the server runs Dyrr.
- Idle kick on dedicated servers, with a chat warning first.
- Inventory watch: a log line when a character's inventory changed while it was offline. This only ever reports. It never refuses or kicks.
- A
dyrrconsole command that prints the standing verdict for everyone currently connected.
How the join check works
Both ends register an RPC as soon as the connection object exists, before either side sends
PeerInfo. The client sends what it knows about its own character: the character name, the
cheats flag and its counter, the name of every console command the character has ever run, the
UIDs of every world it has spawned in, and the BepInEx plugin GUIDs loaded on that machine. The
server does the arithmetic in a prefix on ZNet.RPC_PeerInfo and refuses before the player
spawns, so a refused player never watches the world load first. All of it comes out of
PlayerProfile, which lives on the client's disk, so all of it is self-reported. See
Known limits.
Which console commands count as cheats is decided on the server, from the server's own
Terminal command table, so a cheat command added by some other mod on the server counts for
free. A dedicated server has no guarantee of having built that table when the first player
knocks, so there is a fallback list of the 73 vanilla commands registered with isCheat: true.
The mod check is the one that reaches cheating on a dedicated server.
Console.IsCheatsEnabled() returns ZNet.instance.IsServer(), so a client's own devcommands
does nothing on someone else's server. Anyone cheating there is running a mod that patched
around that line, which makes "what is this client running" the more useful question.
The tamper check does not need the client to be honest, only consistent. The game writes
the same facts in more than one place: m_usedCheats is a bool, PlayerStatType.Cheats is a
counter incremented on the next line, and m_knownWorlds records worlds by name at save time
where m_worldData records them by UID at spawn. A flag that is clear beside a counter above
zero, or more world names than world UIDs, is an edited save. Neither inequality can be
produced by playing the game.
Character protection
Refusing a character at the join arrives too late to help it. Loading a character into any world
writes that world into PlayerProfile.m_worldData permanently, and nothing in the game ever
removes the entry. So the client half refuses the trip instead.
Each character is bound to the first world it is seen in, recorded in
BepInEx/config/dyrr-home.txt. After that:
- Starting a local world with a character bound elsewhere is refused at
FejdStartup.OnWorldStart, with a popup naming both worlds and the file to edit. - Joining a server whose world is not the character's home is dropped inside
RPC_PeerInfo, before the character spawns and before the game writes anything. This works whatever the server does, and whether or not the server runs Dyrr at all. That isProtectOnServers.
Neither asks for confirmation. The damage has no undo, so a confirm dialog would just be a button for doing the unfixable thing.
The character select screen carries the answer under the character's name, in a clone of
vanilla's own "Cloud save" label: Belongs to <world>, Not bound to a world yet,
Has played in <world> and <world> for a character that already has more than one, or
Belongs to a world that is not on this PC when the world is a server's. The line follows
ProtectCharacter and disappears with it.
A character with exactly one world in its own save is bound on the spot even if Dyrr has never watched it play. A character with more than one is left unbound, since there is no single home left to defend, and those are exactly the characters an enforcing server turns away.
Idle kick
On dedicated servers only. A player whose character has not moved 5cm or turned one degree for
IdleMinutes is kicked, after a chat warning IdleWarnMinutes ahead. Position and facing are
read off the character's ZDO every five seconds. Walking, fighting, turning the camera or
sorting a chest all reset the clock.
The kick itself is vanilla's own InternalKick, the same path the console command takes, so the
player gets the ordinary kicked screen with a reason attached. A locally hosted world is never
watched.
Inventory watch
A player's inventory never crosses the wire in vanilla: it lives in their own .fch on their
own disk. So the client sends a tally of what it is carrying every InventoryInterval seconds,
the server keeps the most recent one, and compares the first report of the next session
against it. What that catches is a change made while the character was away, rather than
ordinary play.
The line names what moved, biggest movement first. Item names are the game's own localisation tokens, and anything above quality 1 carries its level:
Inventory changed while away: Balder, +40 $item_iron, +12 $item_silverore, +1 $item_sword_iron*4
Two honest sources of false alarms: a client that crashes stops sending, so the stored snapshot can be up to one interval stale, and Valheim can itself roll a character back to its own last local save. Both look identical to tampering at the moment the report arrives, which is why this only ever writes a log line. Nothing here can refuse, kick or correct anybody.
Installation
Through a mod manager, install
Dyrr and it is done. By hand, put
Dyrr.dll in BepInEx/plugins/Dyrr/.
- Requires BepInEx 5.4.2350. BepInEx 5 API only, not compatible with BepInEx 6.
- Longhouse Core is an optional soft dependency. See Multiplayer.
- Install it on the server and on every client. The facts being judged live on the client,
so a client without Dyrr answers nothing, and
RefuseUnreportedis on by default.
Start the game or the server once and quit. That first run writes
BepInEx/config/ezomic.valheim.dyrr.cfg, which does not exist until the plugin has loaded.
This is the usual reason people think the mod is broken.
Built against Valheim 1.0.7. Version 1.4.0 does not run on pre-1.0 Valheim, and 1.3.0 does not run on 1.0.
Before you turn Enforce on
Read this part. Enforce is the one setting here that can lock people out of a server,
including you.
- The game never removes a world from a character's record. One visit anywhere else is permanent for that character file. Restoring a backup taken before the trip is the only way back in, and it does work: a character refused for having travelled came back from backup and was admitted.
- The cheat flag is the same.
devcommandssets it and nothing clears it. - Being an admin does not exempt you. Dyrr does not consult
adminlist.txtanywhere. Your own character is judged by exactly the same rules as everybody else's. - With
Enforceon, a client without Dyrr is refused byRefuseUnreported. If you are running Core as well, its version check turns that client away first. - Every server you run must enforce. A lenient server is a hole a bound character walks into, and the lenient one is what ruins it. Keep a separate character per server.
The order that works: leave Enforce off, let people connect, run dyrr and read the log, put
the innocent plugins in dyrr-mods.txt, then turn Enforce on.
ProtectCharacter is on while Enforce is off. Refusing other people is a policy somebody
should choose; refusing to let you ruin your own character is not.
Configuration
BepInEx/config/ezomic.valheim.dyrr.cfg. Every entry carries its own comment in the file.
Door (server side)
| Setting | Default | Effect |
|---|---|---|
Enabled |
true |
Off leaves the plugin loaded and judging nothing. Server side only |
Enforce |
false |
On refuses the connection. Off logs what would have been refused |
RefuseOtherWorlds |
true |
Refuse a character that has spawned in any world but this one |
RefuseCheats |
true |
Refuse a character the game has flagged for devcommands use |
RefuseCheatCommands |
true |
Refuse a character that has run a command the game marks as a cheat. The command is named in the log and in the refusal |
RefuseTampered |
true |
Refuse a character whose own records disagree with each other |
RefuseUnreported |
true |
Refuse a connection that answers nothing, or whose profile could not be read |
RefusedMessage |
This server refused this connection. |
Sent to the refused client. The specific reason is appended as a sentence starting "It", so keep this one general |
Protect (client side)
| Setting | Default | Effect |
|---|---|---|
ProtectCharacter |
true |
Refuse to start a local world with a character that belongs to a different one, and record which world each character belongs to |
ProtectOnServers |
true |
Extend that to servers: leave the connection before spawning if the server's world is not this character's home |
Mods (server side)
| Setting | Default | Effect |
|---|---|---|
RefuseMods |
Refuse |
Off, Notice or Refuse. Notice logs what it would have done and lets everyone in. The legacy true and false still parse as Refuse and Off |
ModPolicy |
Allow |
Allow: only what this server runs, plus the allowlist. Deny: anything except DeniedMods |
AllowedMods |
empty | Extra GUIDs a client may run, comma separated. Prefer dyrr-mods.txt, below |
DeniedMods |
empty | GUIDs no client may run, comma separated. Only read under Deny |
Notice exists because Enforce is a single switch over every rule at once. Trialling the mod
list by turning Enforce off also stops refusing cheats and altered records for as long as the
trial runs. Use Notice for a week, read the log, then set Refuse.
The plugins the server itself runs are always permitted and never need listing, so adding a mod
to the server does not refuse everybody the next day. A value that is none of the three words is
read as Refuse and logged, so a typo cannot be the thing that opens a server.
Inventory
| Setting | Default | Effect |
|---|---|---|
WatchInventories |
true |
Report when a character comes back carrying something it did not leave with |
InventoryInterval |
60 |
Seconds between a client's inventory reports. Floored at 5 |
InventoryDetail |
6 |
How many changed items to name before the rest are counted |
InventoryNamesIds |
false |
Put the character's player id beside the name in the log line |
Idle (dedicated servers)
| Setting | Default | Effect |
|---|---|---|
KickIdle |
true |
Kick players who have been completely still for IdleMinutes |
IdleMinutes |
5 |
Minutes of stillness before the kick |
IdleWarnMinutes |
2 |
Minutes of warning first, said once in the player's chat. 0 kicks without warning |
BepInEx writes every entry to disk on the first run, and the saved value beats a new default in
code. If a setting appears to do nothing after an update, check the .cfg before anything else.
Files Dyrr writes
All three live in BepInEx/config/, as plain text.
dyrr-home.txt (client). One line per character: playerId|worldUid|character|world. Only
the two numbers are read; the names are there so you can recognise the line. Delete a line to
unbind that character. The file is re-read whenever it changes, so you can edit it with the game
running. Editing it can only damage your own character, which is why it is not defended.
dyrr-mods.txt (server). The allowlist, one GUID per line, # for comments. Written with
its own explanation the first time Dyrr looks for it. It is re-read whenever it changes, so
letting one friend keep their map mod takes effect on the next connection rather than at the
next restart. AllowedMods in the .cfg still works and is added to this, but BepInEx never
reloads a .cfg on its own, so that entry costs a server restart and everybody online.
dyrr-inventories.txt (server). The last inventory snapshot per character. Safe to delete;
you lose the next comparison and nothing else.
Console commands
The console needs Valheim's -console launch argument on a client. A dedicated server has one
already. None of these are cheat commands and none are admin gated: the report is the server's
own state to whoever is already at its console, and the bindings are the local machine's own
file.
| Command | Does |
|---|---|
dyrr |
What the join check is doing here |
dyrr home |
Which world each character on this machine belongs to |
dyrr forget <id> |
Unbind a character, so the next world it plays in becomes its new home |
dyrr forget does not undo anywhere the character has already been. The game's record of that
is permanent and no mod can clear it.
On a server, dyrr prints something like:
Dyrr 1.4.0
World: 'midgard' (-4881...)
Enforce is OFF - failures are reported here and refused to nobody.
Checks: other worlds on, cheats on, cheat commands on, tampered on, unreported on
Mods: Allow (Refuse) - 12 permitted here
Refused so far this session: 0
Ragnar admitted
Sigrun would refuse: has played on 2 other world(s)
The same block also goes to BepInEx/LogOutput.log, because a console scrolls and a log file
does not. On a client it reports what that machine knows instead: whether a server has refused
it this session, and why, plus the local bindings.
Every plugin a joining client brings that the server does not run or allow is logged as it connects, admitted or refused. That line is what you build the allowlist from:
A client brought 2 plugin(s) this server does not run or allow: randyknapp.mods.equipmentandquickslots, ...
A refusal names who it was, by character name and by the platform id off the socket:
Refused a connection: Balder (76561198662440314) has played on 1 other world(s)
Multiplayer
Install Dyrr on the server and on every client. The server decides everything; the client only answers the question and protects itself.
Longhouse Core is optional and the join check works without it. Two things are lost when it is not installed:
- The version check. Core compares each Ezomic mod's version and build id when a client connects and makes the server reject mismatches. That matters more here than elsewhere, because the facts being judged are reported by the client, and an old build of Dyrr answering an unfamiliar question is exactly what the version check would have caught. A report in a format this build cannot read is treated as unreported rather than guessed at.
- The refusal screen. Valheim's kick screen carries no text of its own. Core is what puts
the reason on it. Without Core, a refused player gets a generic screen and the reason in their
own
BepInEx/LogOutput.log.
With Core installed, Dyrr registers at Requirement.Everyone, so Core requires the plugin on
both ends. Core also applies the host's config values on connected clients in memory, without
writing their config file.
What a refused player sees. With Core: This server refused this connection. It has played on 2 other world(s). on the kick screen, and the same line in their log. Without Core: the
stock kicked screen, and the line in their log. A refusal by the client's own protection is
different again: a local world gets a vanilla popup naming both worlds and the file to edit,
while a stopped server join gets a plain disconnect screen, with the reason on it only when Core
is installed.
Known limits
- Everything the client reports is self-reported, the plugin list included. A purpose-built client can lie about all of it. More records only raise what a liar has to keep straight.
- A cheat that never touches the console leaves no mark. Confirmed here rather than
reasoned about: a god mode toggle that calls
Player.SetGodModedirectly writes none of the four records the character checks read, so those checks correctly find nothing.RefuseModsis what sees that client, by its plugin GUID. RefuseTamperedhas never fired in practice. Producing a profile whose own records disagree takes deliberate editing nobody here has done. It cannot be tripped by playing the game wrong, so the risk is that it never fires rather than that it fires wrongly.- The inventory watch can raise a false alarm after a crash. See that section.
- If Dyrr cannot read
PlayerProfile.m_worldDataat all, it says so once at error level and judges nobody on travel, rather than reading an unreadable list as an empty one. Both travel rules go quiet together and the server logs that it is judging without them.
Troubleshooting
There is no config file. The plugin has to load once before BepInEx writes it. Start the game or the server, quit, look again.
A setting has no effect. BepInEx saved the old value on first run and it beats the new
default. Edit the .cfg, not just the docs.
Nobody is being refused. Enforce is off by default. Run dyrr to see what the checks are
set to and what the verdict on each connected player is.
Your own client is refused for mods. A server does not run the tools you develop with. Put
their GUIDs in dyrr-mods.txt.
The character select line is missing. It follows ProtectCharacter. With that off there is
nothing being enforced for it to describe.
A restored backup carries a stale home. Backups do not touch dyrr-home.txt, which lives
beside the config rather than with the character. Use dyrr forget <id> or delete that
character's line.
Upgrading from Threshold. Dyrr was called Threshold until 2026-08-18. On the first run it
copies ezomic.valheim.threshold.cfg to its new name and adopts threshold-home.txt (and
boon-home.txt before that) if dyrr-home.txt does not exist. Both originals are left in
place. Confirmed on a real run, config and bindings intact.
Status
Run against a real dedicated server in both directions: it refuses a character that has been
elsewhere, with the reason on the client's own screen and in its own log, and it admits a clean
character on an enforcing server. Both cheat checks have fired on a deliberately flagged
character. Backup recovery, the menu guard's binding and the migration from the Threshold-era
files are all confirmed. RefuseTampered is the one check that has never seen the thing it
looks for.
1.4.0 is the Valheim 1.0 rebuild. The achievements system turned PlayerProfile's single stat
record into an array of ten and moved the known-worlds and known-commands lists inside it, and
the cheat counter is now resolved by name rather than by the ordinal the compiler baked in.
Full history in CHANGELOG.md.
Bug reports
The Discord is the fastest route, and the right one if you are not sure whether what you are seeing is a bug. Issues on the repo work too and suit anything long.
Bring BepInEx\LogOutput.log from whichever end saw the problem, and say whether you were on a
server or in single player. If a refusal is wrong, the dyrr output and the character's line
from dyrr-home.txt are the two things that settle it. If a vanilla mechanic broke, check
AppData\LocalLow\IronGate\Valheim\Player.log as well, because gameplay exceptions land there
rather than in the BepInEx log.
Discord
discord.gg/hJzAVaZ5wb is where mod information, updates, support, bug reports and compatibility questions go. There is also a small EU server running the pack if you want somewhere to play. Details are in the Discord.
Licence
MIT. See LICENSE.
Robbin Thijssen / Thijssen Software.
Part of Longhouse
Dyrr is one of the mods in the Longhouse pack, which pins exact versions of its members. You do not need the pack to use this, and it behaves the same on its own.
Changelog
1.4.1 in Longhouse
Changed
Rewritten README. Same mod, clearer documentation: what it does and how to install it come first, then configuration, multiplayer behaviour, compatibility and troubleshooting. Every config table was checked against the plugin's own Config.Bind calls, so the settings, sections and defaults listed are the ones actually bound. No code changed in this release.
discuss
1.4.0
Rebuilt for Valheim 1.0. This version does not run on pre-1.0 Valheim, and the previous one does not run on 1.0.
Fixed
Reads the character's record again on Valheim 1.0. The achievements system turned
discussPlayerProfile's single stat record into an array of ten and moved the known-worlds and known-commands lists inside it. The indices are not interchangeable - one is every increment, one counts only what was eligible for an achievement, the rest are per difficulty - and this mod wants the lifetime totals, which is the question it has always asked.The cheat counter is found by name rather than by the number the compiler baked in. That enum nearly doubled in 1.0. Had the entry moved, this would have read some other counter - and since a mismatch between the cheat flag and its counter is treated as an altered record, a counter that is really a death count would not have degraded the door, it would have shut it on everyone.
discuss
1.3.0
Changed
discussRefuseModsis Off / Notice / Refuse instead of on and off. Watching the rule without acting on it used to mean turningEnforceoff, andEnforceis one switch over the whole verdict - so trialling the mod list also stopped refusing cheats, cheat commands and altered builds. Notice logs what it would have done and lets the player in.The allowlist moves into its own file, so a long list is edited as a list.
discuss
discussIdleMinutesnow defaults to 5, matching what the door has been set to in practice.
1.2.0
Added
Idle kick, on dedicated servers. A player who is genuinely still - no movement, no camera - for 5 minutes (configurable) is kicked, after a one-chat-line warning two minutes ahead. An AFK body holds a slot, keeps its zones simulated and blocks the night from being skipped; the door works in both directions. The kicked player is told why on their disconnect screen through the same channel a join refusal uses, and the log line is worded for Crier to post the departure to Discord.
discuss
1.1.1
A refusal says who was turned away. The line was "Refused a connection: has played on 1 other world(s)", which is fine in a server log you are reading beside the connection you just watched fail, and useless the moment it is forwarded to Discord - where it arrives as a rule nobody can attach to a person.
It now reads:
Refused a connection: Balder (76561198662440314) has played on 1 other world(s)
Both halves, because each covers the other's gap. The character name is what the other players know and the only part worth reading in a channel, but it is self-reported and a character can be renamed. The platform id comes off the socket, cannot be chosen, and survives a report that failed to parse.
The name is not added to the reason sent back to the client. That string is shown to the person being refused, and telling somebody their own name back is noise - they know who they are, they want to know which rule they broke.
Why it was missing
Judge is a prefix on ZNet.RPC_PeerInfo, so it runs before vanilla has parsed the
package the player's name arrives in. All it holds at that moment is the socket. The name
therefore had to come from Dyrr's own report, which the client sends first - so the report
format moves to 3 and the name is the first field after the version, deliberately:
everything below it can throw on a read and leave the report unreadable, and a report that
failed halfway is exactly the one worth naming.
A format mismatch was already handled and still is, with the same clear line rather than a garbled read.
1.1.0
The preventive half now covers servers, which is what it was always supposed to do.
Built, not yet run in game. Everything below compiles and nothing here has been tested against a real server.
Added
The client refuses a join into the wrong world, whatever the server does. Until now the only thing standing between a character and a server that would ruin it was that server choosing to enforce, because a server's world identity is not known in the menu and there was nothing to check. There is one moment on the join path where it is known and nothing has been written yet - the client reads the world name, seed and uid inside
ZNet.RPC_PeerInfo, and the permanent record this mod exists to prevent is only ever written byPlayerProfile.GetWorldData, which needs a spawned player. So the check goes there, and the connection is dropped the way vanilla drops a kicked one.This closes the hole the previous README described and left open: a non-enforcing server ruining a character that a different server would then refuse forever. It works whether or not the server runs this mod at all. New setting
discussProtectOnServers, on by default, underProtectCharacter.The character-select screen says which world a character belongs to. One line above the name, cloned from
m_csFileSource- vanilla's own "Cloud save" label - so the font, size, colour and alignment are the game's and not a guess.It reads the binding file first, and the character second, which is where it earns its keep. A binding is only written when a character spawns while Dyrr is running, so every character that last played before the mod arrived had none and said so. But
PlayerProfile.m_worldDatais one entry per world a character has spawned in, andSaveSystem.GetAllPlayerProfilesparses it in full for the menu - so the answer was already in memory. Exactly one world is that character's home by definition, and it is bound on the spot rather than merely shown; leaving it unbound would mean the menu guard protecting nothing until the character happened to play again.More than one world is left unbound on purpose and named rather than counted: "Has played in BaldoTest and longhouse_20260818". There is no single home to defend, and a count tells somebody they have a problem without telling them what it is. Names come from the local world list where the world is on this disk, topped up from
m_knownWorldswhere it is not - the game's own record of where a character has played, keyed by name wherem_worldDatais keyed by uid. That pairing is what names a server's world that nothing local could.No bare uid ever appears on that line. Nineteen digits standing where the answer goes was the first version of it; a world that still cannot be named now says it is not on this PC, and a name worked out late is written back into the binding so it is only worked out once.
This is the same fact
discussdyrr homereports, moved to where the decision is made. The console was the cheapest surface to write and the wrong one: off until somebody enables it, a developer's tool, and it has to be asked. The information matters at exactly one moment, and that moment already has a screen. Nothing here prevents anything - it is the sentence that stops the refusal at the next screen being a surprise.A
dyrrconsole command.dyrrprints what the door is doing here: the world, whether Enforce is on, which checks are live, how many connections have been refused this session, and the standing verdict for every player currently connected. That last part is the point of Enforce being off - it is meant to be sat in while deciding, and until now it answered "who would stop being able to play?" one line at a time into a log, at the moment each player connected. On a server the report is written to the BepInEx log as well, because a console scrolls and a log file does not.
discussdyrr homelists this machine's character bindings, anddyrr forget <id>unbinds one without going and finding the file. Neither is a cheat command and neither is admin-gated: the report is the server's own state to whoever is already at its console, and the bindings are this machine's own file.
discussdyrr-home.txtnow carries the character and world names beside the two ids, and the refusal popup names the world rather than quoting a bare 19-digit number. Nothing is ever matched on the names - two fields still load, four still load on an older build - but the one action this mod asks of a player is "delete the line starting with your character's id", and that is much harder when every line looks the same.Four more ways to see a cheat, because one flag is one thing to clear.
RefuseCheatsreadm_usedCheats, a bool set inTerminal.ConsoleCommand.RunAction. A mod that switches devcommands on will trip it; a mod that also clears it will not. So the client now reports the records the game keeps beside it, written at different moments by different code:PlayerStatType.Cheats, a counter incremented on the line after the flag. Flag clear and counter above zero is not a suspicion, it is a record that has been edited.m_knownCommands, the name of every console command the character has ever run, written a few lines further down and outside the branch that sets the flag. New settingRefuseCheatCommands, and the refusal names the command - "has run 'spawn'" is a fact somebody can answer where "used cheats" is only an accusation.m_knownWorlds, worlds by name at save time, againstm_worldData's uids at spawn time. More names than uids means the travel record was scrubbed. The inequality only runs one way, so the game itself cannot trip it.
The last two are
RefuseTampered, on by default. It is the only check here that does not need the client to be honest, only consistent - and consistency across four records written by four pieces of code is a different job from clearing one bool.Which commands count as cheats is decided on the server, from the server's own command table, so a cheat command added by some other mod counts for free. The table is only built when a console exists, which a dedicated server has no guarantee of, so there is a fallback list of all 73 vanilla
discussisCheat: truecommands - ripped out ofTerminal.InitTerminalrather than typed from memory.The client's mod list, judged by the server.
RefuseMods, on by default, withModPolicyatAllow: the plugins the server itself runs are always fine, anything else has to be named inAllowedMods.Denyinverts it for a server that only wants to name what it will not have.This is the check that actually reaches cheating on a dedicated server, and the reason is one line of vanilla:
Console.IsCheatsEnabledreturnsZNet.instance.IsServer(). A client's owndevcommandsis inert on somebody else's server - it flips a bool the gate then ignores - so anybody cheating there is necessarily running a mod that patched around it. What a character did in the past is a weaker question than what the client is running now.Both lists ship empty, and
DeniedModscould not honestly ship otherwise: a list of cheat mod GUIDs written in advance is stale the week after and reads as complete when it is not. Every plugin a client brings that the server does not run is written to the log as it connects, admitted or not, which is what those lists get built from.Self-reported, like everything else here. A purpose-built client can lie about all of it.
discussThe refusal message claimed which check had fired, and was usually wrong.
RefusedMessagedefaulted to "This server only accepts characters that have never played anywhere else" and the specific reason was appended in brackets after it. That was true when travel was the only thing that could refuse anybody. With six checks it became a lie on screen: a client turned away for running a mod was told its character had played somewhere else, with the real reason sitting in a parenthesis reading like an aside on the sentence that contradicted it. Seen exactly that way on a dev server.The default is now "This server refused this connection." and the reason follows as its own sentence starting "It", which reads correctly for every check - It is running 'x', It has played on 2 other world(s), It has run cheat command 'god'. A fixed sentence must not claim which of six checks fired.
discuss
Fixed
A hand edit to
discussdyrr-home.txtwas silently undone. The file was read once per process and the whole set written back on every bind, so deleting a line - the documented fix for a wrong binding, and what the refusal popup tells you to do - was reverted by the next character that bound. It is now re-read whenever it changes on disk, so the file the popup points at is the file that is actually read, and the edit can be made without restarting.
discussHome.Forgetexisted and nothing called it. It isdyrr forgetnow.
1.0.0
First published release.
The number means published and nothing else. It sat at 0.9.0 while the repo was public and Thunderstore had nothing on it, because a 1.0 with no package behind it claims a release that does not exist, and because a Thunderstore version can never be reissued once uploaded - so the number had to be spent on the build that actually ships rather than on one that drifted away from it on disk.
Everything below this line was already true at 0.9.0. Nothing about the mod's behaviour changed to get here.
Renamed from Threshold, 2026-08-18
The config file. BepInEx names it after the plugin GUID, so a rename hands everyone a file of defaults. That is not merely lost preferences:
discussEnforcedefaults to off, so a server that had the door on would quietly have it off with nothing said. The plugin now copiesezomic.valheim.threshold.cfgto its new name on first run, before binding anything, and leaves the old file alone.The character bindings.
discussthreshold-home.txtis adopted the same wayboon-home.txtalready was, and the fallback is now an ordered chain rather than a single name, so a machine that skipped a release cannot fall between the two. Losing these would hand every character one free trip to another world, silently and exactly once, and that trip has no undo.
Fixed since the split
The dedicated server no longer binds a character it does not have. A server has a
discussPlayerProfileobject with nobody behind it, so the binding ran there too and minted a fresh phantom id on every startup, one junk line per restart, forever. Binding now requires a local player, which is also the more correct moment: a character has not played anywhere until it spawns.Bindings are adopted from Rist's
discussrist-home.txton first run ifdyrr-home.txtdoes not exist. Without that, moving the feature between mods would have silently unbound every character on the machine and handed everyone one free trip to another world.
Verified against a real dedicated server
Both branches, which matters more than it sounds. It refuses a character that has been elsewhere, with the reason on the client's own screen and in its own log, and it admits a clean character on an enforcing server. Until the second one happened, "works" and "refuses everybody" were indistinguishable, because every test until then used a character that had genuinely travelled.
Also confirmed: the menu guard's binding, the adoption of bindings from Rist's old
rist-home.txt, and restoring a character backup as the documented recovery. A refused
character came back from backup and was admitted.
Every server must enforce
Found by running two servers, one enforcing and one not. The menu guard covers local worlds
only; the door covers servers only where Enforce is on. A non-enforcing server is therefore a
hole a bound character walks into, and all the mod can do afterwards is log "too late to stop
it - that world is now written into the character". The lenient server is the one that ruins
the character.
Enforce on every server, and keep a separate character for each.
Known limits
discussRefuseCheatsis untested; it needs a character deliberately flagged bydevcommands.
0.1.0
Split out of Rist, where it never belonged.
The line this sits on
Who comes through the door is server policy. It is not a levelling mod's decision.
Rist awards character levels for skill gains, so it wanted to know whether a character's skills were earned here, and answered by refusing the connection. That put an XP system in charge of who is allowed to play, and when it fired the player got Valheim's generic kick screen with the reason written only to the server's log, which on someone else's server they can never read. The first time it happened, the person affected blamed an unrelated mod.
Added
The door. Connections are refused for characters that have spawned in another world (
discussRefuseOtherWorlds), that the game has flagged for cheats (RefuseCheats), or that answered nothing at all (RefuseUnreported).The menu guard, which is the half that actually protects you. Refusing at the door is the lesser half: by then the harm is done, because loading a character into any world writes that world into its profile permanently. So the client also refuses to start a local world with a character belonging to a different one, at
discussFejdStartup.OnWorldStart, the last moment anything can be done about it.The refusal is shown on screen, on both machines, under the stock kicked line, not written to a log the person who needs it cannot read.
discussA home file per character,
discussBepInEx/config/dyrr-home.txt, binding each character to the first world that accepted it. Plain text on purpose: it is protection rather than enforcement, and editing it can only damage your own character.
Enforce is off by default, and that is deliberate
This is the one setting in the family that can lock people out of a server, including you.
The game never removes entries from a character's world list, so one visit anywhere else is
permanent for that character file, and restoring a backup from before the trip is the only way
back in. The cheat flag behaves the same way: set by devcommands, never cleared.
Turning it on should be an admin deciding, having read that paragraph, not something that happens because a mod got installed.
Verified
Refused a real connection on a dedicated server, logged the reason on both machines, and showed
it on the refusal panel. Not yet verified: admitting a genuinely clean character, and
RefuseCheats.
Every setting · 21
All of it lives in one file, written on first run with each description beside its default. The file is the source of truth; edit it there.
BepInEx/config/ezomic.valheim.dyrr.cfg
server On a server, the host's value applies to everyone while they are connected; your own file is left alone and counts again when you leave. yours Keys and personal settings stay as you set them, on any server.
Door 8
| Setting | What it does | Default | Whose |
|---|---|---|---|
| Enabled | Off leaves the plugin loaded and checking nothing. Only affects the server side; character protection has its own switch below. | true | server |
| Enforce | On refuses the connection. Off only logs what would have been refused. Off by default, and deliberately so. This is the one setting in the family that can lock people out of a server, including you, so it should be a thing somebody turns on having read what it does - not something that happens because a mod was installed. | false | server |
| RefuseOtherWorlds | Refuse a character that has spawned in any world but this one. Read this before enabling Enforce: the game never removes entries from a character's world list, so a single visit anywhere else is permanent for that character file. Restoring a backup taken before the trip is the only way back in. That is the intended severity - it is what makes skill levels on this server mean something - but it has no undo. | true | server |
| RefuseCheats | Refuse a character the game has flagged as having used cheats. Also permanent, and set by devcommands rather than by anything subtle. | true | server |
| RefuseCheatCommands | Refuse a character that has run a console command the game marks as a cheat. Separate from RefuseCheats because it reads a different record. The game keeps the name of every command a character has ever run, in m_knownCommands, written a few lines after the cheats flag and outside the branch that sets it - so a mod that clears the flag leaves this behind. Which command was run is named in the log and in the refusal. The classification is done here, from the server's own command table, so a cheat command added by another mod on this server counts too. | true | server |
| RefuseTampered | Refuse a character whose own records disagree with each other. The game writes the same facts in more than one place, at different moments, from different code: the cheats flag has a counter beside it, and the list of worlds a character has spawned in has a second list of world names written at save time. Clearing one is easy; clearing all of them so they still agree is different work, and a mod written to switch devcommands on has not done it. This is the only check here that does not depend on the client being honest, only on it being consistent. A false positive would need a character whose profile the game itself wrote inconsistently, which has not been seen. The world half only runs when the client could actually read its own list of worlds. A game update that moves or renames that field leaves the list empty rather than wrong, which used to read as every character having wiped it - so the whole server was refused, and told so. When that happens now, the server logs that it is judging without the travel rules and lets people in. | true | server |
| RefuseUnreported | Refuse a connection that answers nothing, or whose profile could not be read. A door that opens when the question goes unanswered is not a door - but Core's version gate should already have turned away a client without this plugin, so in practice this is a backstop. | true | server |
| RefusedMessage | Sent to the refused client so it lands in their own log, and on their screen when they have Core. Valheim's refusal screen carries no text of its own, and a player on somebody else's server can never read that server's log - so without this, being turned away is indistinguishable from a crash. The specific reason is appended to whatever this says, as a sentence starting "It". So keep this one general. It used to read "only accepts characters that have never played anywhere else", which was true when travel was the only thing that could refuse anybody and became a lie the moment it was not: a client turned away for running a mod was told, on screen, that its character had played somewhere else. A fixed sentence must not claim which of six checks fired. | This server refused this connection. | server |
Idle 3
| Setting | What it does | Default | Whose |
|---|---|---|---|
| KickIdle | Kick players who have been genuinely still - no movement, no camera - for IdleMinutes, after a warning. Dedicated servers only: an AFK body holds a slot, keeps its zones simulated and blocks the night from being skipped, and the door works in both directions. | true | server |
| IdleMinutes | Minutes of complete stillness before the kick. Moving, fighting, turning the camera or sorting a chest all reset it; only a hands-off body does not. | 5 | server |
| IdleWarnMinutes | Minutes of warning before the kick, said once in the player's chat. 0 kicks without warning. | 2 | server |
Inventory 4
| Setting | What it does | Default | Whose |
|---|---|---|---|
| WatchInventories | Say so when a character comes back carrying something it did not leave with. The client reports a tally of its own inventory every InventoryInterval seconds; the server keeps the last one and compares the first report of the next session against it, so what is reported is a change made WHILE AWAY rather than ordinary play. Self-reported, and honest about it: a player's inventory is never sent to a server by the game - it lives in their own .fch - so a client built to lie can lie. This catches the ordinary case, which is somebody closing the game, editing the file and coming back richer. A crash can also trip it: the last snapshot may be a minute stale, and the game may itself roll a character back. The line says what moved so forty iron can be told from one arrow. | true | server |
| InventoryInterval | Seconds between a client's inventory reports. Lower means a crash loses less and so raises fewer false alarms; it is a small package either way. Floored at 5. | 60 | server |
| InventoryDetail | How many changed items to name before the rest are counted. A line naming sixty items is a line nobody reads. | 6 | server |
| InventoryNamesIds | Put the character's player id beside the name. Off by default because the line goes to Discord and a name is enough to ask somebody about it. | false | server |
Mods 4
| Setting | What it does | Default | Whose |
|---|---|---|---|
| RefuseMods | Judge what the joining client has loaded, by BepInEx plugin GUID. Off - do not look. Notice - look, write what was found to the log, and let everybody in anyway. Refuse - turn them away, which still needs Enforce on. The old true and false still work and mean Refuse and Off. Notice exists because turning Enforce off is not a way to trial this rule: Enforce is one switch over every rule at the door, so trialling the mod list that way also stops refusing cheats and altered records for as long as the trial runs. Use Notice for a week, read the log, then set Refuse. This is the check that actually reaches cheating on a dedicated server. Console.IsCheatsEnabled returns ZNet.instance.IsServer(), so a client's own devcommands does nothing on somebody else's server - which means anyone cheating there is running a mod that patched around it. What the character did in the past is a weaker question than what the client is running now. Self-reported like everything else here, so a purpose-built client can lie. What it catches is a cheat mod installed from Thunderstore by somebody who did not think about it. | Mods.Refuse | server |
| ModPolicy | Allow: only the mods this server runs, plus AllowedMods, may come in. Deny: anything may come in except what is named in DeniedMods. Allow is the default because a list of things to permit is knowable in advance and a list of every cheat mod that will ever exist is not. Deny suits a server that does not mind what people run as long as it is not that. Whichever is set, nothing is refused while Enforce is off - it is reported, and 'dyrr' prints the standing answer for everyone connected. Turn Enforce on after reading that, not before. | Mods.Allow | server |
| AllowedMods | Extra plugin GUIDs a client may run, separated by commas. Case is ignored. The plugins this server itself runs are always allowed and do not need listing - otherwise adding a mod to the server would refuse everybody, including you. This is for the client-only ones: a map mod, an equipment bar, and whatever you personally develop with. Only read when ModPolicy is Allow, and added to whatever is in BepInEx/config/dyrr-mods.txt. Prefer that file: this entry only takes effect when the server restarts, and letting one friend keep their map mod is not worth dropping everybody who is online. | (empty) | server |
| DeniedMods | Plugin GUIDs no client may run, separated by commas. Case is ignored. Shipped empty on purpose. A list of cheat mod GUIDs written by me would be out of date the week after it shipped and would read as complete when it was not. Build it from what actually turns up: every plugin a client brings that this server does not run is written to the log when it connects. Only read when ModPolicy is Deny. | (empty) | server |
Protect 2
| Setting | What it does | Default | Whose |
|---|---|---|---|
| ProtectCharacter | Refuse to start a local world with a character that belongs to a different one, and remember which world each character belongs to. This is the only half of this mod that can prevent anything. By the time the door refuses a character the damage is already permanent - the game recorded the world it visited and never removes that record. The binding lives in BepInEx/config/dyrr-home.txt, which you can edit; it only protects your own characters, so there is nothing there worth defending against you. It refuses rather than asking, because a confirm dialog on an irreversible action is just a button for doing the unfixable thing. | true | server |
| ProtectOnServers | Extend the check above to servers: leave the connection before spawning if the server's world is not the one this character belongs to. This works whatever the server does, and whether or not the server runs this mod at all. It is the same rule the menu applies to local worlds, moved to the one moment on a join where the world is known and nothing has been written yet. Turn it off if you deliberately take one character between several worlds. Note that doing so is exactly what the door refuses people for. | true | server |
Discussion
Nothing said yet.
Sign in or make an account to join in.