Water Mod
Tarn
Water at any height — lakes above sea level with real water in them, drawn on the map and held in step by the server.
About Tarn
Valheim has one water level, and it is the sea’s. A hollow in the hills sixty metres above the shore is a dry hollow however deep it is, so every lake above sea level is empty. Tarn fills them: it reads a list of lakes — a name, a deepest point, a surface height — and builds vanilla water in each basin at its own height. You can swim in it, and it shows on the map.
Tarn ships with no lakes of its own. They come from a text file a map maker writes for a world, or from a console command you run while standing in a hollow. It was made for Raven Iron’s Ravenrest map.
It registers no prefabs and ships no assets. It is a prototype — it works and is used on test worlds, but the details may still change.
What It Does
💧 A Lake Fills Its Basin
A lake entry gives a deepest point, a surface height and a box the water may fill. Tarn samples the world generator’s ground every 4 metres across that box (plus 12 metres of margin) and floods outward from the deepest point: every cell below the surface height that joins it is water. Ground below that height which the flood cannot reach — behind a sill, beyond a rim — stays dry, which is why a lake takes the shape of its basin instead of being a rectangle. The surface runs one cell past the wet ground, so it meets the shore where the terrain cuts it.
The shape comes from the generator’s heights, never from the terrain as it is currently loaded, so the server and every player work out the same lake. Ground a player has since dug away or built up is not part of that picture.
If the water reaches the edge of its box, the basin is not closed there: the log says so, and the shore is cut off at the box.
🌊 The Water Is Vanilla’s
The surface is the game’s own sea tile, at its own 64-metre size on the same grid as the zones, one for each square the lake touches, with its mesh clipped to the basin. The water you swim in is a set of trigger boxes on the game’s water layer that cover the basin — the same kind of volume the sea uses, so swimming, the Wet status, floating objects and the underwater view find a lake the way they find the sea. Swimming and Wet have been confirmed in game with a lake sixty metres above the sea.
Colour, foam and wave size follow depth, as they do at sea, using the ground’s depth under each tile’s four corners. DepthScale sets how deep the shader is told a lake is, as a share of its real depth (full colour and waves arrive at 10 metres): lower is calmer and greener, higher is bluer and rougher.
🏞️ Built Near You, Dropped Behind You
Lakes within BuildRange of you (400 metres by default) are built, one at a time, spending at most BuildBudgetMs per frame (2 milliseconds by default). A lake is dropped again once you are 150 metres beyond that range. The check runs twice a second. A dedicated server builds no water of its own — it holds the list and sends it.
🗺️ On The Map
Vanilla draws water on the minimap and large map wherever its height texture is under the sea, so a lake sixty metres up is land to it. Once the map exists, Tarn repaints each lake’s pixels with the height of a sea as deep as the lake is, and draws no forest there. It changes the map picture only — nothing in the world reads those textures. A lake too small to cover a map pixel is skipped.
🔊 The Sound
Within about 80 metres of a lake the game’s own ocean loop plays, quieter, and louder in wind. LakeVolume sets it (0.35 by default; 0 turns it off). If the sound code ever throws, it switches itself off for the rest of the session.
🌲 Lake Beds Stay Clear
No trees, rocks, bushes or pickable debris are placed on a lake’s bed, from the shore down — by default, and including lakes made with tarn here. Tarn adds clear areas to vanilla’s own zone vegetation pass, the same kind of area the game uses to keep trees out of locations. The game tests each spot against its clear areas after rolling for it, so only what would have stood on a cleared spot goes; everything else is placed exactly as vanilla places it.
The cleared ground is the same flood that shapes the water, widened by LakeBedShore (2 metres by default, 0 to 10). A lake is sampled every 4 metres, so at the default the cleared edge lands 0 to 4 metres past the real waterline, about 2 on average. Lower it for less bare shore; at 0 a bush can still stand in the shallows here and there. Vanilla’s clear area is a square, so Tarn merges the wet cells into rectangles and covers each with squares rather than one area per cell: vanilla checks every candidate spot against every area in turn, so fewer, larger areas keep that cheap.
Big lakes are shaped ahead of time, a few milliseconds a frame, for any lake within 640 metres of a player, so zone generation finds them ready. If a zone needs a lake that is not ready, it is shaped on the spot, and the log says how long that took when it was slow (over 20 milliseconds).
It only affects zones generated after a lake exists. tarn here on ground you have already explored leaves the trees standing there — that is what tarn clearbeds is for. Grass is not vegetation to the game: it follows the ground paint, so a bed needs dirt paint or grass stands up through the water.
🧹 Clearing What Already Stands There
tarn clearbeds <lake|all> counts the trees, rocks, bushes and pickables already standing on lake beds in explored ground, naming each kind and how many, with a tally per lake when there is more than one. Add confirm and it removes them, which cannot be undone.
Only what vanilla places as vegetation goes, and only on the bed as Tarn clears it. Anything with a creator — a player’s piece — is skipped, and so are stumps, logs, drops and creatures. A tree grown from a planted sapling is the same prefab as a wild one, so it goes too if it stands on a bed. It runs as a job a few milliseconds a frame, shaping any lake that is not shaped yet first, and refuses a second request while one is running.
🛣️ A Clearing Mask For Roads
A map maker can also mark ground that must stay free of vegetation — a road, say — in an optional per-world .clear file, and Tarn clears it the same way. Better Continents’ vegetation map cannot do this: it is read once per 64-metre zone, at the zone’s centre, so it clears a whole zone or nothing. Like lake beds, the mask only affects zones generated after it exists, so use a new world or reset those zones.
📡 The Server Holds Lakes, Mask And Settings
The server — or the host, or the game itself in single player — owns the lake list, the clearing mask and the settings. Vegetation is placed by whichever machine generates a zone, which in multiplayer is usually a player’s game (a dedicated server generates zones around each player too), so every one of them needs the same lakes and the same mask.
- Lakes. A joining player’s game asks for them, repeating every four seconds until the server answers, and gives up after about a minute with a log line asking whether Tarn is installed on the server. While connected it never reads its own lake file, so a stale local copy cannot disagree with the server’s. After any change —
tarn here,tarn remove,tarn reload— the new list goes to everyone, and a lake whose line has not changed keeps its shape on the receiving side. - The mask. Sent to every joining player, deflated and in 48 KB chunks: the Ravenrest map’s 2 MB mask goes out as 68 KB. A connected player never reads their own
.clearfile. - Settings. All six of Tarn’s settings follow the server. It sends them to each player who joins, and to everyone again whenever one changes; while connected a player’s game uses the server’s values in memory only, and their own config file is never written to. A setting changes on the server when an admin runs
tarn depth, when a config manager changes it there, or when the server’s.cfgis edited — it notices the file within a few seconds, andtarn reloadre-reads it at once. While connected to a server that sends them, a player’s own edit to their file changes nothing.
A change after the first sync shows the player a short notice at the top left of the screen. Nothing is sent continuously: it goes out when someone joins and when something changes.
Technical Specifications
| Parameter | Detail |
|---|---|
| Runs On | Client & Server — one DLL; the server (or host) owns the lakes, each player’s game builds the water, and a dedicated server draws none |
| Target Runtime | BepInEx 5.4 — the package depends on BepInExPack 5.4.2351 |
| Engine Target | Valheim (Unity Engine) |
| Current Version | 0.4.1 |
| Source | github.com/RavenIron-Games/Tarn |
| Prefabs Added | None. The water is built at run time from copies of the game’s own zone water |
| Lakes Come From | A per-world text file, or tarn here / tarn at |
| Persistence | The lake list is a plain-text file per world on the server; lakes write nothing into the world save |
| Frame Cost | Time-budgeted — BuildBudgetMs, 2 ms a frame by default |
| Network Cost | None per tick. Lakes, settings and the mask go out when someone joins and when something changes |
Files
Everything is per world, in BepInEx/config/Tarn/, and only the server (or host) reads it.
| File | What it holds |
|---|---|
<world name>.txt |
The lake list, one lake per line: name;x;z;level;minX;minZ;maxX;maxZ;depth. x and z are the deepest point, level is the absolute world height of the surface (the sea is 30), and the box is the area the lake may fill. Lines starting with # are ignored. Tarn rewrites the file whenever a lake is added or removed |
<world name>.clear |
The clearing mask, optional. The ASCII header TARNCLR1, an int32 size, a float32 half-extent in metres, then size × size bits: row 0 is the north edge, high bit first (numpy packbits order) |
Configuration
Everything is in BepInEx/config/com.raveniron.tarn.cfg. On a server every setting is the server’s; players get them on joining.
| Setting | What it controls |
|---|---|
Look.DepthScale |
How deep lakes look — colour, foam and waves (0.5, range 0 to 3). Live with tarn depth |
Sound.LakeVolume |
The water sound near a lake (0.35, range 0 to 1; 0 is off) |
Performance.BuildRange |
Lakes within this many metres are built (400, range 150 to 2000) |
Performance.BuildBudgetMs |
Milliseconds a frame spent building lakes, and shaping nearby lake beds ahead of zone generation (2, range 0.5 to 20) |
Clearing.LakeBeds |
Keep vegetation off every lake’s bed (on) |
Clearing.LakeBedShore |
How far past the water’s edge that clearing reaches, in metres (2, range 0 to 10) |
Console Commands
| Command | Effect |
|---|---|
tarn here [radius] [margin] |
Fill the hollow you are standing in, up to just under its spill point. radius (120 m by default) is how far out Tarn looks for the hollow’s rim, margin (0.3 m) how far under the spill point the surface sits. It names the lake lake1, lake2 and so on, saves it, and reports the level, the spill height, the deepest point and the area. A hollow that drains to the sea, or ground that is not in a hollow, is refused with a reason |
tarn at <x> <z> [radius] [margin] |
The same at a point — it is what a player’s tarn here becomes when it goes to the server |
tarn list [n] |
The nearest lakes (10 by default): where they are, their level, how far away, and whether they are built |
tarn depth [scale] |
Show or set how deep lakes look. Applies live |
tarn reload |
Re-read this world’s lake file and Tarn’s config on the server; nearby lakes rebuild |
tarn remove <name|all> |
Remove one lake, or all of them |
tarn clearbeds <lake|all> [confirm] |
Count, then with confirm remove, the vegetation standing on lake beds in explored ground |
tarn info |
Describe the zone water Tarn copies — a diagnostic |
From a player’s game on a server, everything except list, info and a bare depth is sent to the server and needs the player to be on adminlist.txt. tarn clearbeds has its own request, and the server’s answer comes back to that player’s console.
Installation
Drop Tarn.dll into BepInEx/plugins/ on the server and every client, or install it through a mod manager. The server reads the world’s lake file and mask and sends them; each player’s game builds the water and clears the vegetation of the zones it generates. A server without Tarn never answers, and the player’s log says so after about a minute. Water is built on each player’s machine, so a player without Tarn gets no lakes.
To make a first lake: stand in a hollow in a world above sea level and run tarn here. On a server, an admin runs it.
Keep the server and every player on the same version. The lake and settings messages work while their protocol matches, and a client whose version differs from the server’s logs a warning naming both. Mixed versions degrade rather than break:
- A server older than 0.4.1 sends no settings, so players keep their own.
- A server older than 0.3.0 never sends the clearing mask.
- A 0.3.0 player still gets the lakes but clears no beds, and a 0.4.0 or newer player on a 0.3.0 server clears none either.
Plays Well With
Better Continents — Tarn was made and tested beside it. The Ravenrest map pipeline writes both Better Continents’ maps and Tarn’s lake and clearing files, and Tarn’s test worlds ran on a Better Continents profile. Tarn neither depends on it nor patches it. Where they meet, Tarn picks up what Better Continents’ vegetation map cannot do: clear a narrow road or a lake bed.
Tarn’s patches are narrow. The zone vegetation pass gets a low-priority prefix that never skips the original, and a finalizer that takes back exactly the clear areas it added. The water’s depth setup gets a postfix that only touches Tarn’s own lake volumes, and its material setup a low-priority prefix that skips the original only for Tarn’s own swim boxes — every other volume is left alone. The two map-building calls get postfixes that only mark the map for a redraw, and the console gets the tarn command.
Known Limitations
- A prototype. The README says so: it works and is used on test worlds, but the details may still change.
- Lakes start empty. There is no bundled lake list. A world gets lakes from a map maker’s file or from
tarn here. - Existing ground keeps what stands on it. Lake-bed clearing and the mask only affect zones generated after they exist.
tarn clearbedshandles lake beds on explored ground; the mask needs a new world or reset zones. - Grass is not cleared. A lake bed needs dirt paint on its ground, or grass stands up through the water. The Ravenrest map paints its beds.
- A race at join. Zones a player generates in the seconds before the server’s lakes and mask arrive keep vanilla’s vegetation. Tarn logs the first such zone; it does not prevent it.
- A one-off pause beside a large lake. On a dedicated server, one large lake was shaped on the spot in 245 ms (423 × 578 samples) when a joining player’s zones reached it before the ahead-of-time shaping had. The notes flag the cost of testing every spot against the extra clear areas in shore zones as something to watch; no measurement of it is recorded.
- The shape ignores terrain edits. It follows the ground as the world generator made it, as described above.
- What has been confirmed in game.
- Swimming and the Wet status in a lake are confirmed. Sailing on one and floating objects use the same water volumes, and the README says you can sail on a lake, but no test of either is recorded.
- Lake sync and the mask sync are confirmed on dedicated servers, including one carrying 172 lakes. The mask keeping roads clear has been confirmed by eye on a test world, but roads generated by a joined player’s game have not been checked by eye.
- On a dedicated server the log line showing lake-bed clearing ran was seen for new ground. No one is recorded as walking a new lake’s shore to check that nothing stands in the water.
tarn clearbedsis confirmed in single player: it counted, removed, and a recount found nothing standing, including after the world was loaded again. The server path — the admin check and the answer coming back to the admin’s console — is not yet confirmed.- Settings following the server is confirmed with a joined player, including an edit to the server’s file and
tarn depthfrom a player. A player’s own values coming back in single player afterwards has not been seen.
Part of the RavenIron Suite
Every RavenIron project is engineered server-authoritative in C# and .NET, and shares the same forge.

