Longhouse

Longhouse 1.0.4

19 August 2026. After 1.0.3.

Two pins move: Utangard to 1.2.0 and Dyrr to 1.1.0. No member joins or leaves, and the other four are the versions 1.0.3 already named.

Both moves are forced rather than chosen. Core's version gate compares the compiler's build id, so a client on Utangard 1.1.0 and a server on 1.2.0 do not merely play by different rules - the connection is refused. Leaving either pin behind while a server runs the new build locks out everybody who installed the pack, which is the failure this pack exists to prevent.

What is inside the two, in one line each; the reasoning is in their own changelogs.

  • Utangard 1.2.0 widens the gate to a five metre band and stops health regeneration inside it, and gives the rules a compendium page so the person losing their food can read why. Both new rules are configurable and host-synced.
  • Dyrr 1.1.0 makes the client refuse a join into the wrong world on its own, whatever the server does, and names a character's home world on the select screen.

The published pack is still 1.0.1

1.0.2 and 1.0.3 were assembled here and never uploaded, so a publish of this version carries all three changes at once: the rewritten page, the Rist repin, and these two. Nothing is lost by skipping the numbers - Thunderstore only requires that a version go up - and the entries below stay where they are because they are the pack's history, not its release log.

What moved · 2

Dyrr 1.0.0 → 1.1.0

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 by PlayerProfile.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 ProtectOnServers, on by default, under ProtectCharacter.

    discuss
  • 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_worldData is one entry per world a character has spawned in, and SaveSystem.GetAllPlayerProfiles parses 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_knownWorlds where it is not - the game's own record of where a character has played, keyed by name where m_worldData is 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 dyrr home reports, 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.

    discuss
  • A dyrr console command. dyrr prints 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.

    dyrr home lists this machine's character bindings, and dyrr 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.

    discuss
  • dyrr-home.txt now 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.

    discuss
  • Four more ways to see a cheat, because one flag is one thing to clear. RefuseCheats read m_usedCheats, a bool set in Terminal.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 setting RefuseCheatCommands, 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, against m_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 isCheat: true commands - ripped out of Terminal.InitTerminal rather than typed from memory.

    discuss
  • The client's mod list, judged by the server. RefuseMods, on by default, with ModPolicy at Allow: the plugins the server itself runs are always fine, anything else has to be named in AllowedMods. Deny inverts 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.IsCheatsEnabled returns ZNet.instance.IsServer(). A client's own devcommands is 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 DeniedMods could 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.

    discuss
  • The refusal message claimed which check had fired, and was usually wrong. RefusedMessage defaulted 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 dyrr-home.txt was 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.

    discuss
  • Home.Forget existed and nothing called it. It is dyrr forget now.

    discuss

Utangard 1.1.0 → 1.2.0

1.2.0

The border is a band, and wounds do not close

Two rules, both configurable, both on by default.

Gate.BorderMargin, 5 metres. The gate now reaches five metres past the edge of a gated biome. On a line, every penalty in the mod is escapable by taking three steps out of the Swamp, eating, and stepping back in - the drain, the refusal and the grudge all end at a boundary you can see and stand behind. That makes it a rule about where you may chew rather than where you may live, and it is worst exactly where it matters most, at the edge of a fight you are already in. A band has to be genuinely cleared. Set it to 0 to put the gate back on the border.

It samples eight compass points at the margin, so it costs eight biome lookups. Those are cached against the player's position and re-taken every quarter of a metre walked; what is cached is which biomes are within reach and never the verdict on them, so a biome that opens while somebody stands at its border opens for them where they stand.

Food.HealthRegenMultiplier, 0. Health regeneration in a gated biome, as a fraction of normal. It sits in the Food section because food is the only passive healing Valheim has - Player.UpdateFood adds up every meal's m_foodRegen every ten seconds and heals you by it - so this multiplies exactly the healing the food you are not allowed to eat would have given. The land that will not feed you does not mend you either.

It rides StatusEffect.ModifyHealthRegen on the marker effect rather than a patch, because that is the seam vanilla already offers and it composes with every other multiplier instead of overriding them. Which meant the marker had to stop being skipped when ShowStatusEffects was off: it was pure signage then and is carrying a rule now, and turning off the HUD would otherwise have quietly turned off the healing block.

Both are host-synced with Core, like every other setting that decides a rule.

Also: the deadline in the entry message is now read from the biome that is actually withering you rather than the one underfoot. With a margin those part company, and a countdown for the wrong boss is worse than no countdown.

You can see the gate, and you are told when it opens

A Utangard page in the compendium, beside Logs and Active Effects: every biome, whether it is open, who still owes it, and how long until the deadline opens it anyway. Until now that report existed only as log lines on spawn, which is the wrong medium for the person who most needs it - somebody mid-raid wondering why their food vanished is not going to read LogOutput.log.

It is a postfix on TextsDialog.UpdateTextsList, so it is vanilla's list with vanilla's skin, font, scrolling, gamepad handling and close behaviour, none of which this mod then owns. The alternative was an IMGUI window: four patches (both TakeInput overloads, PlayerController.InInventoryEtc, GameCamera.UpdateMouseCapture) and a keybind, to arrive at something that looks like a different game.

The log and the page are one function now. They were about to be two copies of "is this biome open, and if not who owes it", and the interesting part is not the wording but the three-way distinction between open-because-latched, open-because-everyone-has-it and shut-with-an-empty- roster. Two copies of that stay right for about a week.

Presentation.AnnounceOpenings. A message when a biome opens, wherever you are. The mod's whole argument is that fetching the friend who is behind is worth doing, and the payoff for doing it used to land silently - you found out by walking to the Mountain and not being refused. It watches the answer rather than the kill, so a catch-up deadline expiring and a roster member ageing out announce themselves too, and it needs no network code at all: global keys are already broadcast to every client.

The gate keys are checked against the game, not assumed

defeated_queen and defeated_fader are set from prefab data rather than named in the GlobalKeys enum, so they were the two shipped defaults that could not be verified from the game's code - and a wrong key fails closed, which looks exactly like a working gate.

Character.m_defeatSetGlobalKey is a public string on every creature prefab and OnDeath hands it straight to SetGlobalKey, so walking ZNetScene's prefab list gives the complete list of keys any death in this world can set, another mod's creatures included. On spawn Utangard now warns about any gate row naming a key nothing here sets, and prints the ones that exist - which is the answer to the question the warning provokes. Diagnostics.LogDefeatKeys prints the whole map.

It checks and never corrects. A row pointed at another mod's key, or at a key a location sets, is a supported thing to want.

It has now been run, and both names are right. The scan on a live world reported defeated_eikthyr, defeated_gdking, defeated_bonemass, defeated_dragon, defeated_goblinking, defeated_queen, defeated_fader, and also defeated_hive and defeated_serpent for the two creatures that set a key without gating anything here.

Played

All of it, on a live world: the border margin refusing a player standing three metres outside a gated biome, the healing block, the compendium page, the announcement firing on the transition, and the healing block again with ShowStatusEffects = false - where the gate still refused food and held healing at zero with both icons hidden, and regeneration returned on leaving. A presentation toggle does not switch off a rule.

Discussion

Nothing said yet.

Sign in or make an account to join in.

← 1.0.5 1.0.3 →