Hirsla not released yet
First version. Chests within 20 metres serve the crafting panel - crafting and upgrading, at a
workbench, a forge, a cauldron, an artisan station or any other crafting station - and they
serve only materials from a biome whose boss is down.
Narrowed to the crafting panel, before it ever shipped
It was wider than that until 2026-09-06, and the narrowing is worth the space because nothing
about it was a bug fix. Two things were removed on the user's instruction:
Station feeding. Pressing Use on a smelter, kiln, cooking station, fire, fermenter or
ballista drew what it was about to consume out of a nearby chest. Seven Harmony prefixes,
their conversion-table walks, the HaveDoneItem reflection binding that told "collect the
cooked food" from "put raw food on", and the Stations config section with it.
Building. Placing a piece could pay for it out of a chest. That took the prefix on
Player.TryPlacePiece that refused a click while the material came across the network, the
UpdatePlacementGhost binding behind it, the scope bracket on
Player.HaveRequirements(Piece, RequirementMode), and the half of the requirement line that
drew a chest total in the build HUD.
Neither of them was broken. What they cost was the sentence that describes the mod. "Chests
serve you" has no edge to it: every key a player presses anywhere in the game becomes a thing
to wonder about, and somebody who has seen a fire take a log out of a chest has no way to guess
whether a ballista will. "The crafting panel reaches into nearby chests" is a claim a player
can hold whole, and a convenience nobody can predict is worse than none - which is the same
argument the mod already makes about its own gate, applied to its own surface.
They also carried the only two paths that could not fit a network round trip inside the action
they belonged to. Player.UpdatePlacement asks whether you can afford a piece, places it and
charges you in one frame on a click; a station press is the same shape. Both were paid for by
refusing the first one, fetching a run of them and letting the second through - one stutter per
material, once, and honest, but a cost the crafting panel never has to pay because
InventoryGui.OnCraftPressed starts a timer of at least 0.8 seconds before DoCrafting
spends anything. So the buffer that stutter needed went too: FetchAhead and FetchMessage
are gone from the config, and a craft asks for exactly what the recipe costs.
Three smaller consequences, each of which would have been a quiet wrongness if it had been
missed:
- The requirement lines moved their scope bracket from
InventoryGui.SetupRequirement to
the private InventoryGui.SetupRequirementList. That method draws both the crafting panel
and the build HUD, so bracketing it put chest stock into the hammer's red flash as well -
harmless while the hammer drew on chests and a lie the moment it stopped.
SetupRequirementList is the crafting panel's own and is called from one place. Reading
SetupRequirement's craft argument instead does not work: Hud.UpdateBuild passes
piece.FreeBuildKey() == GlobalKeys.NoCraftCost, which is true for any piece carrying an
ItemDrop or a Feast, so the build HUD really does ask for craft: true on item stands
and feasts.
ConsumeResources keeps its bracket and now spans a building call it can do nothing
with. In ordinary play that costs nothing: UpdatePlacement gates on
HaveRequirements(piece, CanBuild), which is vanilla again, so a placement that reaches the
spend is one the pack could already pay for and the chest shortfall computes as zero. This
entry first said "safe by construction rather than by a check", and that was one word too
strong - see the review fix below.
Withdraw.Awaiting went with them. It answered "is a request for this material still
live", and it existed to choose between Fetching from the chests and The chests could not
supply it on a refused click. Building and station feeding were its only two callers. The
owner-routed withdrawal itself stays and is still load-bearing - crafting still draws from
chests this client does not own - as does Chests.MoveFromOwned, which the station feeding
shared with the one-ingredient craft path and which that path still needs.
Reviewed after the narrowing
Four things, one of them a real path and three of them documents that had stopped describing
the code.
- The
noplacementcost cheat could still pay for a wall out of a chest. The claim above
rested on UpdatePlacement asking HaveRequirements before it spends, and the vanilla line
is actually if (m_noPlacementCost || HaveRequirements(...)) (Player.cs:1160, spend at
1167). The cheat short-circuits the gate and keeps the spend, so a piece placed with an
empty pack arrived at ConsumeResources with a real shortfall and the chests covered it -
silently, and against the one promise the README now makes without qualification.
TakeFromChests refuses on NoCostCheat and nothing else changed; the craft path cannot
notice, because DoCrafting only reaches ConsumeResources when that flag is false.
- The
RequireOnClients comment still described the old withdrawal. It told the reader
that two players with Hirsla are the case that duplicates a chest, which stopped being true
when the withdrawal became a request answered by the chest's owner. The plugin's copy of
that argument had been corrected and this one, the copy a server operator actually reads in
their .cfg, had not - so the file argued against the setting on a hazard that no longer
exists, and said nothing about the concrete thing it buys.
- The README claimed a host's settings never touch your config file. Core applies them
through
ConfigEntryBase.BoxedValue, which is BepInEx's setter, and BepInEx saves the file
on every set. The values are still restored on disconnect; the file is not untouched while
you are connected, and saying otherwise leaves a player with no explanation for the host's
numbers in their own settings. Core's own log line makes the same claim and is that repo's
to fix.
Chests.HasAllowed read as live code. Its only caller is the deliberately dormant
HaveItem patch, whose reason for existing is written out in full one file away. A helper
nothing reachable calls, with a one-line docstring, is a helper somebody deletes on a tidy-up
- and it takes the guard with it. The docstring now says what it is and where the argument
lives.
Everything below this line was written before the narrowing and several entries describe
machinery it has since removed - the station loops, the placement stutter, the build menu's
hidden pieces. They are left standing rather than tidied away: they are a true record of what
was found and fixed on the way here, and a changelog that quietly deletes the work it decided
against is a changelog that cannot be used to reason about anything.
This is the constrained answer to the craft-from-container mods, and the constraint is the
feature rather than a limitation of it. Reaching into every chest in sight from the first
minute deletes the early game's one real lesson - that a trip has to be planned - so the reach
arrives one biome at a time and is complete only when the game is. Before Eikthyr the mod does
nothing at all.
It replaces Tether, which tied one chest to one station by hand. That rule turned out to be
bookkeeping in exchange for a precision nobody wanted, and a radius has no state to get out of
step with the world. Tether's crafting core is where most of the hard-won detail here came
from and it is not deleted; the two simply cannot run together, and Hirsla disables itself and
says so if it finds Tether loaded.
Three things Tether got wrong that this does not:
- Inventory.HaveItem was never patched. The build menu's hide-unavailable filter runs
HaveRequirements in CanAlmostBuild mode, and that mode routes to HaveItem rather than
CountItems. So a piece stayed hidden in the menu while its own requirement numbers said it
was available - two parts of the interface disagreeing about one piece. The smelter, the
cooking station and the fire gate on HaveItem too.
CountItems gained a third parameter. matchWorldLevel decides whether an item from a
lower world level counts at all, and a postfix that ignores it counts Ashlands items one way
in the chest and another way in the pack. That is a count and a consume disagreeing, which
is a free craft.
- Recipes marked
m_requireOnlyOneIngredient bypass ConsumeResources entirely.
InventoryGui.DoCrafting spends those by calling RemoveItem on the player's inventory
directly, with the scope bracket long since closed - so nothing comes out of the chest and
RemoveItem(string, ...) returns void and stops quietly. Worse, it does not get that far:
Recipe.GetAmount dereferences a null with no guard when the count was inflated but the
player carries none, and that exception lands in Player.log rather than in the BepInEx log
anyone would think to read. Hirsla physically moves the material into the pack before
vanilla looks, so every check, message and removal afterwards runs on real items.
Four things found in review before any of this ever ran, all fixed in the same version and all
worth naming because each one was invisible in the shape it shipped in:
- The chest search truncated itself silently.
Physics.OverlapSphereNonAlloc counts
colliders, not objects, fills its buffer to capacity and gives no sign that it stopped -
and a 20 metre sphere in a longhouse is thousands of walls, beams and floors before it is
one chest. A base big enough to want this mod was the base where it quietly found nothing,
and the broadphase order shifts as things move, so it would have come and gone between
frames. The query is now masked to the layers containers are actually on, worked out from
the game's own prefabs rather than guessed, and it grows and asks again rather than
accepting a full buffer as an answer.
- The station loops drew material the station was never going to use. Pressing Use
empty-handed on a smelter makes it pick the first ore in its conversion list that you are
carrying. Hirsla walked the same list but could not tell "nothing to fetch" from "you
already have some", so carrying copper and having tin in a chest drew the tin out - and
vanilla then smelted the copper, because copper is first. Another was drawn on every press
after that.
- A cancelled craft left material in your pack. The one-ingredient recipes had their
material moved out of the chest when the craft button was pressed, and cancelling only
stops a timer. The press now works the number out without moving anything; the move happens
where the spend does.
- Carts. A container in a cart shares the cart's network object, so claiming it to take
one log claimed the cart - and a cart changes hands by detaching from whoever is towing it.
Vanilla refuses every request on a cart in use for exactly this reason; Hirsla now does too,
and leaves any vehicle another player owns alone.
The chests are no longer written from the wrong machine
The first draft of this mod took ownership of a chest and then wrote it locally, and the
changelog above described the resulting race as documented rather than fixed - something a
client could not do better. That was wrong on both counts. It could be done better, the way
the game itself does it, and what was shipped was worse than "a small window": it could
duplicate a chest's entire contents and leave the duplication in place for the rest of the
session.
Hirsla now never writes a chest it does not own. A withdrawal from somebody else's chest
is a request sent to the machine that owns it. That machine removes what is actually in the
chest at that instant - clamped to the stock in hand, not to a count taken a moment ago - and
answers with what it really took; the asking side credits nothing but the answer. Ask for
eight iron when six remain and six arrive. It is the same shape as the game's own chest RPCs,
which all work by asking the owner, and it is the only shape that cannot invent or lose items.
Three separate things were wrong with the old design and each of them read as careful code:
- Claiming ownership is not a lock.
ZNetView.ClaimOwnership is one line - if you are not
the owner, become the owner - with no handshake, no acknowledgement and no way to refuse.
Two clients can both succeed and each then believes it holds the chest.
- The write does not merge. A container saves its entire inventory as one blob, so a
client working from a copy a second old does not lose its own change on top of somebody
else's; it restores everything the other person removed.
- And the disagreement does not heal. The game drops an incoming update whose revision is
not strictly newer than the copy it already has, refuses to offer it again, and has no
"send me the truth" call at all. So the fork lasts the session and it resolves in favour of
whichever client was stale. Only a relog clears it.
Crafting hides the round trip; building cannot. The request is fired when the craft button
is pressed and lands during the craft timer, so the material is genuinely in your pack before
the game charges you and nothing about the craft changes. Building checks, places and charges
inside a single frame on a click, with no timer to hide anything in - so the first click on a
piece whose material is in someone else's chest is refused, says Fetching from the chests,
and the next one builds. Feeding a station is the same shape and costs one dead press. One
stutter per material, once, and only on a dedicated server. The two alternatives were building
the wall without paying for it and moving things out of a chest for aiming at the ground; the
README argues it in full.
Both paths verify at the moment of payment rather than trusting the fetch. A craft that was
started on chest stock which has since gone declines with The chests could not supply it
instead of happening for free. The requirement numbers on screen stay optimistic and that is
deliberate: the crafting panel asks about every recipe in the game every frame and cannot go
over the network, so it reports what this client's copy of each chest says. Show what you
know; pay for what is there.
Two more things went with it:
- The forced re-read is gone, and its claim was false. Hirsla used to make each container
re-read its inventory before counting, and both this file and the README said that narrowed
the multiplayer window "to network latency". It did nothing of the sort - that read goes to
the local replica and there is no network in it. What it did do was break the container's
own once-a-second check, which skips its work whenever the re-read reports nothing new: a
chest set to destroy itself when empty stopped doing so, and an open/closed lid stopped
updating. That bug fired in singleplayer and had nothing to do with any race.
- A removal's return value is now read.
Inventory.RemoveItem reports whether it removed
anything and the old code ignored it, counting what it had asked for rather than what it
got. That is not pedantry: the two-argument form hands off to the one-argument form whenever
the amount is the whole stack, and MultiUserChest - the mod this design is modelled on -
patches exactly that method to refuse a removal from an inventory the caller does not own.
With both mods installed the old code would have counted items it had not removed and handed
out free crafts.
Read again afterwards, and it was not finished
The authoritative withdrawal was then reviewed a second time - once for the protocol, once for
how it feels to play, once for whether anything could destroy or duplicate an item - and it
came back with work in all three. None of it refuted the design. All of it was the difference
between a mechanism that is correct and a mod somebody would want installed.
Two ways items could still be created out of nothing, and both were in code written to
prevent exactly that.
- The undo that was supposed to take back a duplicate could not run. Moving one item from a
chest to your pack added it first and removed it second, so that a failed removal could be
compensated - but adding one of something you are already carrying does not add the object
you handed over, it increments the stack you have. The compensating removal was then handed
an object the pack had never heard of, logged that it was not there, and returned false to
nobody. The item stayed in the pack and stayed in the chest. It is remove-then-add now, which
is safe at an amount of one and only at an amount of one, and a refused add puts the unit
straight back in the chest it left a line earlier.
- A withdrawal reply was credited whoever sent it. The reply carries a request number, and
nothing checked that this client had ever issued it, that the reply came from the machine it
was sent to, or that it was even about the same chest. Any peer could have written items
straight into somebody's character file, which is saved outside the world and cannot be
reconciled against anything. All three are checked now. A slow answer is still honoured -
refusing it would destroy stock that has already left the chest - so the record outlives its
own timeout by half a minute rather than being deleted by it.
Three ways items could quietly vanish. A request that failed part way through answered
"nothing" while the stacks it had already taken out of the chest went out of scope on the
owner's machine; they are put back now. A station press asked every chest in reach for the
whole outstanding amount rather than for its share, so four chests could each answer in full
and the surplus ended up on the floor. And a reply carrying an item this game has no prefab for
was discarded in silence by the game's own loader; it now says so, naming the count, because a
short delivery reads as a short chest.
The stutter was a rhythm. Building asked for exactly one placement's worth, which the very
next placement spent - so it was refused, built, refused, built for the whole of a wall, with a
message on every other click, and both the code and the README claimed it happened once. It now
asks for FetchAhead placements' worth and refills the buffer on a click that goes through, so
it really is one refused click per material. Station feeding had the identical bug with
FeedAmount at its default of one, and the dead press was worse than silent: the game ran
afterwards and announced that you did not have any coal, at the moment the coal was on its way.
That press now says what is actually happening.
And it fetched for clicks that were never going to build anything. The hold ran before the
game had decided whether the piece could stand, so aiming at a no-build zone, at somebody's
ward, or at ground that would not take the piece pulled material out of a stranger's chest and
replaced the real reason with Fetching from the chests. That is the very outcome the
fetch-while-aiming option was rejected for. The placement is checked first now, and an invalid
one is handed straight back to the game to refuse in its own words.
Two smaller things. The refusal message no longer borrows vanilla's you are missing some
requirements, which was literally true and read as a bug two seconds after the panel drew the
chest total in blue; $msg_missingrequirement is still one line in the config away. And a
container is nobody's for up to two seconds after a world load or a teleport - the game hands
them out on a timer - so in that window a chest is counted and cannot be spent from, in
singleplayer as much as anywhere. Hirsla now says it could not supply the material rather than
promising a fetch that no request went out for. Widening the ownership test to cover it was
tried on paper and is wrong: a write to a container nobody owns is never saved, so the material
would come back after being spent.
Entries here are prose, not bullet lists of commits. Say what changed from the player's side,
then why, then what it used to do wrong - including the part that made the old behaviour look
correct. A changelog nobody can act on is a version number with decoration.
Merki not released yet
First version. Built and deployed; never run in a game.
Everyone connected to the server is on everyone's map, and cannot take themselves off it.
The checkbox on the map is greyed out rather than left to tick itself back on a frame after
you click it, because a checkbox that undoes your click reads as a bug and a greyed one
reads as a rule.
Two optional limits on top of that, both off: a range in metres, and same-biome-only. With
either set, the server builds a separate player list per recipient instead of broadcasting
one - which is the reason they are off by default rather than merely a taste.
Deaths are pinned on the other players' maps, not just the dead player's own, labelled with
their name and expiring after thirty minutes. The server passes each death on under the same
rule as the map, so a private position sends no mark and Range and SameBiome decide who gets
one.
Each player's own answer to the map checkbox is never saved over. The server applies its
rule on top, so turning the rule off, or removing the mod, gives everyone back the choice
they had made. A client with Merki on a server without it forces nothing and greys out
nothing.
Renamed from Beacon to Merki before it ever ran, with the GUID following to
ezomic.valheim.merki. Nothing needs carrying over: no config file with the old name was
ever written anywhere.
Builds against Valheim 1.0: the per-recipient player list writes the server-assigned display
name and the PlayFab id in the order 1.0 reads them.
Death marks are remembered by the server, one per player, so somebody who joins or reconnects
after a death still gets the gravestone, for the time that is left rather than a fresh half
hour. A second death moves a player's mark instead of adding another, you are never sent your
own, and the dead player's name sits under the gravestone on the large map. DeathMarkMinutes
now runs from 1 to 30, and the option to keep a mark for the whole session is gone. A server
restart forgets every mark.
A second death now also reaches the players who only had the first: if the new spot is out of
their Range or biome, the old gravestone is taken off their map instead of staying for up to
half an hour. A brand-new character's scope is measured where the valkyrie sets it down, not
where the ride starts. Reports and marks travel over each player's own connection to the
server, which takes the name from that player's character, so no client can report a death for
somebody else or send a gravestone that did not come from the server. Switching DeathMarks or
Enabled off now drops the marks the server was holding and the pins already drawn.
Skra not released yet
First version. Built, not yet run in game, and the locks are unproven in multiplayer.
Chests and portals a player built now say who built it when you hover them, and the builder can make one private so only they, the characters on its list, and server admins can use it. A private portal refuses travel both ways and refuses retagging by anyone off its list. Everything starts public, so installing it changes nothing until a builder decides otherwise, and vanilla's personal chest stays private and gains a list.
The server keeps a name book per world, built from beds, wards, tombstones and logins, so an owner line has a name to show even for a builder who is offline. It also decides who counts as an admin and tells every player, because vanilla's own admin check disagrees between the server and the players for a bare Steam id.
The settings panel opens and closes and every action behind it is wired, but its layout is not drawn yet. It waits for a pick between three mockups.
Not in this version, on purpose: damage protection, carts, ships, tombstones and dungeon chests, and blocking hammer removal of a private chest in a base without a ward.
Vitni not released yet
Skill levels go into the ledger. A player reached Rist level 63 in three days, and Rist's
log could date only the level-ups it refused; the ones it accepted left no trace, so
"when did this skill get to 100" had no answer.
Rist already reports every level the game awards to the server, and asks for the whole
skill list at every join and respawn. Vitni now reads both off the wire: a skill row per
level-up (skill, level, and prev once this session has seen that skill) and a
skills row holding the full list at each hello. Both are the client's claims, attributed
to its connection like every other row. On by default under [Events] Skills.
Levels are truncated to two decimals, never rounded, so a row cannot say 100 while the game
still shows 99. A level set without playing for it (the console's raiseskill, a save
editor) sends no level-up, so it shows only in the next snapshot. Snapshots need the
server's Rist to keep [Gate] CheckSkillBaseline on. A client asking other players for
their skill lists, which Rist never does, gets a skills_ask row, and the answers it
provokes are not logged against the players answering.
At logoff, a skills row marked source: reconstructed gathers the last snapshot and
every level-up since. An inventory row records what a player carries at join, after
every spawn, and at logoff, read off the tally Dyrr's client already sends every minute
([Events] Inventories). A logoff row carries age_s, how old that tally was. Both go
into the daily ledger, so RetentionDays keeps the history.
Players named in vitni-blocked.txt are refused at the door with their line's message on
screen, applied within seconds of saving the file. A pid:<id> line blocks one
character instead of the account; it is disconnected with the message as soon as its
character reaches the server. An until: time after the id makes either kind of line
lift by itself.
Read here rather than logged by Rist because Rist is on Core's version gate: a log line
there is a new build, and every player would have to update before they could join.