Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
WebMap
Dedicated-server web map for Valheim 1.0. Live players, shared fog-of-war exploration, player-built structures, forest and logging, and in-game server announcements. Clients need no mods.
| Date uploaded | a week ago |
| Version | 2.11.0 |
| Download link | Mubs-WebMap-2.11.0.zip |
| Downloads | 269 |
| Dependency string | Mubs-WebMap-2.11.0 |
This mod requires the following mods to function
denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2350README
Valheim WebMap
A server-side mod that publishes a live web map of your world. Share
http://your_ip:port and anyone can watch — clients need no mods. Dedicated
server only.
A fork of h0tw1r3/valheim-webmap rebuilt for Valheim 1.0 (Deep North).
Features
- Explorable map in the browser — wheel zoom, pinch on mobile.
- Shared fog of war: only what players have actually explored.
- Live player list and positions, auto-follow, and in-game pings.
- Structures — placed pieces drawn in the colour of their material, so bases read as bases. Keyed off the piece's creator, so terrain and world-generated ruins never appear. The sweep behind it runs only while someone is looking at the map.
- Forest and logging — standing trees shade the terrain and felled ground stops being shaded, so clearings show through. Stumps are counted as the record of felling.
- Boats and carts, in explored territory only.
- Portals, graves and every placed piece as JSON, for a front-end of your own. The bundled page does not draw them yet.
- World render at the resolution you choose —
render_size4096 halves the metres per pixel, and rendering no longer stalls the server. - Server announcements on every player's screen, for restart warnings and the like.
- Chat, deaths and joins in the message log; optional Discord notifications.
Install
- With BepInEx working, put the
WebMapdirectory inValheim dedicated server/BepInEx/plugins/WebMap. - Start the server once to write a default config.
- Stop the server before editing that config — BepInEx rewrites it on shutdown, so edits made while it runs are discarded.
- Open the configured port (default
8080) and visithttp://your_ip:port.
After updating, hard-reload the page (shift+reload) to clear cached layers.
Using the map
The viewer at http://your_ip:port is four pages — the live map, a portal atlas drawn on
a biome chart, a planning board for drawing and sharing routes, and per-player tallies —
with a Layers list on the map to switch each layer off. Its source is WebMap/web; our own hosted copy at
xn-valheim deploys the same files with a site-config.js that names our server.
Players only appear once they set visible to other players on the in-game map (m).
Chat commands
!pin— a dot where you stand.!pin some textlabels it.!pin <type> <text>— types aredot,fire,mine,house,cave.!undoPin— remove your most recent pin.!deletePin <text>— remove the pin matching that text. With no text, your most recent unnamed pin.
Not case sensitive. Past the configured limit a player's oldest pin is dropped.
Server announcements
POST /announce, message as the body, X-Announce-Token header. The secret lives in
announce.token beside the DLL — not in the BepInEx config, which is rewritten on
shutdown and would discard it. No token file means the route is closed.
Configuration
Standard BepInEx config, plus:
render_size— pixels across the world render, default 2048. Same area astexture_size, only sharper: 4096 halves the metres per pixel for a one-time render of about a minute and a larger download. The overlays stay attexture_size, where extra resolution buys nothing. Amap.pngof the wrong size is rebuilt on start.show_vehicles— report boats and carts at/vehicles. They are only ever reported in explored territory; off stops them being reported at all.
HTTP endpoints
| Path | Returns |
|---|---|
/map, /map.jpg |
the world render; the JPEG is about a seventh the size |
/fog |
explored mask (PNG) |
/chart |
the world as a chart: each pixel its biome's flat colour, water one blue, no relief (PNG, once per world) |
/structures, /structures/stats |
structures overlay; counts by prefab and the last sweep's cost |
/forest, /forest/stats |
forest overlay, tree and stump counts with density percentiles |
/pieces |
every placed piece as [prefab, x, z, yaw] against a table of prefab footprint and colour (JSON, about 60 KB for a world) |
/portals |
portals with their tag and the portal each is linked to, as the game has connected them (JSON) |
/graves |
tombstones still holding gear: owner, position, seconds since the death (JSON) |
/vehicles |
boats and carts, position and type (JSON) |
/stats/players |
per-player tallies: joins, deaths, chat, distance, portal hops, pins, standing pieces/portals/ships, graves (JSON) |
/players, /pins, /messages |
live state (JSON) |
/state |
all of the small JSON blocks in one document, plus a content revision per layer (rev.fog, rev.forest, rev.structures, rev.pieces) so a viewer fetches a layer only when its picture changed; pass the revision as ?v= |
/announce |
POST, see above |
The structure sweep walks every ZDO on the game thread, a few thousand per frame;
everything after the walk runs on a pool thread. It runs only while someone is reading the map: a request to any layer or to the sweep-fed JSON
arms it for two minutes, sweeps start at least a minute apart, and an idle server does
none at all. /config, /players, /map, /pins and /messages do not arm it, so a
monitor probing those keeps the game idle. /structures/stats reports the last sweep —
ZDOs walked, game-thread milliseconds, frames, wall time, gen-2 collections — so the cost
can be read rather than guessed.
Notes for developers
Chat. Valheim 1.0 addresses chat to each recipient rather than broadcasting it, so a
hook on HandleRoutedRPC sees only pings. It still passes through the server, though:
RPC_RoutedRPC calls RouteRPC to forward it, and that is where this mod observes chat.
A shout arrives once per recipient and is de-duplicated on sender, method and payload.
Upstream instead registered a fake server-side player so clients would address the server. On 1.0 that stops anyone joining at all — the server stays healthy and registered but logs zero connection attempts — so it is gone here. Don't put it back.
The sweep yields only between sector lists. Yielding inside a List<ZDO> lets the
game's removals shift the index under the walk and skip ZDOs; a whole sector between
yields is the smallest safe step.
Announcements use MessageHud's ShowMessage rather than chat: Chat gates every
message on a RelationsManager permission check against the sender's platform user id,
which a server does not have, so chat sent from a server is dropped in silence.
Local test server
./testserver.sh runs the same image the hosts do
(indifferentbroccoli/valheim-server-docker) under podman.
./testserver.sh up # first run downloads the game, ~10 min
./testserver.sh deploy # build, copy in, restart
./testserver.sh status # container + endpoints
./testserver.sh logs
Join with Join by IP → 127.0.0.1:2456, password testpass123.
Two things worth knowing: steamcmd fails app_update with
Missing configuration, having downloaded nothing, in roughly one run in four — it is
transient, so just run it again. And on an SELinux host the bind mounts need :z or the
container silently sees nothing.
Licence and credit
MIT where applicable.
- 1.0 update, structures, forest, vehicles, portals, graves and pieces by Hunter Boyd
- Maintained upstream by Jeff Clark
- Original work by Kyle Paulsen
- Background by webtreats, CC BY 2.0
CHANGELOG
Changelog
2.12.0
- A Layers card on the map, as on Google Maps: a thumbnail of the other ground in the
corner, and behind it the choice of ground -- the world render, or the flat biome
atlas the portals page draws on, which carries no forest shading -- and presets of
the legend (Default, Travel, Builds, Wilds, Bare).
Lcycles the ground. Graves are off by default, like portals. - One sidebar shell for the map, the portals and the plan: the ☰ in the bar folds it away, and the choice holds across pages. The portals page now draws its diagram full-height with the hub and pair lists beside it instead of a boxed chart above them.
- Phones: the sidebar is a drawer, shut on arrival, opened by the ☰ or by the online pill on the map; the layers tray opens upward; the bar fades at its edge where it swipes.
- Torches and fires: a Details row in the Layers card, off by default, draws torches, fire pits, hearths and braziers as warm points over the map -- one glow where a base is kept -- and a burnt-out one as a grey ring. The sweep now reads each fire's fuel and sends it as a fifth field on those pieces; a viewer on an older server treats every fire as lit.
- Deaths and trails, two more details in the Layers card. Every death is kept with
where it happened (
deathsin/state, the last 500) and drawn as a red glow that fades over a month. Trails count, per map pixel, the times a player walked into it, kept intrails.binbeside the stats and rendered each sweep as/trails, a faint blue over the ground;rev.trailsin/statesays when it moved. - The bundled viewer's file cache keys on each file's timestamp, so a viewer file replaced on disk is served at once instead of after the next restart.
- Panning no longer lags. The map and the plan rendered every raster and every one of the thousands of build footprints again on every pointer event; now the scene renders once per zoom into a window wider than the screen, a pan slides it, and a zoom stretches it until a fresh render lands -- at once where that is quick, after the zoom settles where it is not. The footprints themselves draw as one path per colour instead of a fill each, and the deaths re-render only when the list changes.
- One style system in
site.css: the tokens (surfaces, ink, accents, radii, shadows), the base, and the pieces every page shares -- cards, stats, buttons, chips, fields, the legend, the map's button column, section rules. The pages' own styles shrank to what each alone draws. The bar underlines the page you are on, controls ease between states, focus is visible, and scrollbars are thin everywhere. - Hildir is her camp and nothing else. The trader scan matched any location whose name
contained "hildir", which caught her three quest sites -- the mountain cave, the swamp
crypt and the plains fortress -- and drew her standing in each of them. The three
camps are now matched by name (
Vendor_BlackForest,Hildir_camp,BogWitch_Camp), and the scan logs which it found. - The Log reads as chat again. A player asked for the web map's chat back: it had
never gone, but the panel had become a join-and-death ticker with a line of chat
every few hours lost in it -- and every disconnect was written twice, once by the
announcement and once by the notifier. The server now marks its own lines (
evon each message), keeps chat and events to their own depth so a busy evening cannot push the chat out, and writes a leave once. The panel has a chat / all switch, chat by default, remembered per browser; a viewer on an older server falls back to treating a line with no speaker as an event. - Four bugs found by reading the paths the last few features added to:
- A chat message was dropped whole if the server could not resolve the sender's character -- the position was looked up before anything else, though only the pin commands use it. Chat is now read first and the lookup happens only for a pin.
- A pin command from a sender with no id would have matched every pin in the
list (
StartsWith("")), so the per-player trim could delete other players' pins and!undoPincould remove someone else's. Pins now need a known owner. - An RPC addressed to everybody runs both branches of
RPC_RoutedRPC, so the two observation points saw it twice; only one of them de-duplicated. Both do. - The Discord webhook posted from the join and disconnect handlers on the game thread: a slow Discord stalled a player's join for as long as it took to fail, and a bad URL threw inside the handshake. The post goes to a pool thread, and an unset webhook no longer logs a line per join.
2.11.0
- The bundled viewer is now the same site that fronts our own server: the live map with
builds drawn as shaded roofs and floor plans, the portal atlas on a biome chart, the
planning board, and the players page — served by the mod on its own port, so it needs
no proxy and no hosting. The old viewer, its webpack build and the websocket pings it
showed are gone; the pages poll
/stateinstead. - Pages are served with
no-cacheso a new build shows on the next visit; assets for five minutes. They were cached for a week.
2.10.0
render_size: the world render at its own resolution. 4096 halves the metres per pixel over the same area; the overlays stay attexture_size. The render is a coroutine that yields every row, so it no longer freezes the server, and amap.pngof the wrong width is rebuilt rather than served.- Structure sweeps run only while someone is reading the map. A request to any layer or
sweep-fed JSON arms them for two minutes and they start at least a minute apart; an idle
server does no sweep work. The fixed two-minute timer and the immediate re-sweep on
/structures/refreshare gone. Every sweep measures itself — ZDOs, game-thread milliseconds for walk and finish, frames yielded, wall time, gen-2 collections — as asweepfield on/structures/statsand one log line. /pieces: every placed piece as[prefab, x, z, yaw]against a small table of prefab footprint and colour, for viewers that draw builds as vectors. Recorded in the same sweep and the same ZDO visit; about 60 KB for a world./portals: portals with their tag and the portal each is linked to, taken from the game's own connection rather than from matching names./graves: tombstones still holding gear, with owner and seconds since the death.- The chat observer compares the method hash before doing anything else. It used to hash,
key and de-duplicate every routed RPC the server forwards, on the game thread, thousands
a second with a few players on. Also removed: a postfix on
GetStableHashCodethat wrote two dictionaries on every string the game hashes, and theZSyncAnimationhooks beside it. /fogencodes its PNG once per change instead of on every request, and request paths drop their query string before routing, so a cache-busting parameter no longer 404s.- Player state is read by ZDOVars hash and
/configis built on the game thread when the world loads, so HTTP threads no longer touchZNet. - Everything a sweep does after the walk — rendering the overlays, the forest blur, the
JSON, the PNG encodes — runs on a pool thread. The game thread pays for the walk and
nothing else; the ~140 ms finish it used to absorb is gone. Overlays are encoded with
EncodeArrayToPNG, which Unity marks thread-safe;EncodeToPNGon aTexture2D, which was being called from HTTP threads, is not. - The fog is kept as bytes and encoded the same thread-safe way; it was a
Texture2Dencoded on HTTP threads while the game thread painted it./structures/refreshis gone: reading any layer arms a sweep, which is all it did. /state: every small JSON block in one document per tick, with a content revision per layer. A viewer polls one URL and fetches a layer only when its revision moved;?v=on a layer request lets a cache keep it as long as it likes. The bundled viewer polls it every 30 s instead of re-pulling both overlays on timers, and loads the world render as the JPEG: browsers gave up on the 16 MB PNG a 4096 render produces part way through, after which the viewer never got to its overlays, pins or messages./chart: the world as a flat chart, each pixel its biome's colour and water one blue, sampled from the world generator once per world and kept besidemap.png.- Traders (Haldor, Hildir, the Bog Witch) in
/state, in explored ground only: the world generated them, and a player's own map pins them once they have been near. /stats/players: per-player tallies — joins, deaths, chat lines, distance covered, portal hops, pins, and what is standing in the world with their name on it (pieces, portals, ships, graves). No time played, by design. Persisted beside the world's map data.
2.9.1
- Removed a Harmony prefix on
ZRoutedRpc.InvokeRoutedRPCthat ran for every outgoing routed RPC and did nothing unlessdebugortestwas set. Chat is observed atRouteRPCinstead, so it was redundant as well as costly. - Removed the
testsetting it existed for, which reroutedDiscoverLocationResponsto everybody, anddiscord_invite_url, which was read by nothing at all. - Removed
MapDataServer.BroadcastMessage, which had no callers.
2.9.0
- Forest and logging overlay (
/forest,/forest/stats): trees shade the terrain, cleared ground shows through. The shading saturates smoothly rather than clamping, so thinning a wood is visible and not just clear-felling it;/forest/statsreports the density percentiles behind the picture. - Boats and carts (
/vehicles): reported with position and type rather than painted into the structures layer as anonymous pieces. Detected byShip/Vagoncomponent, so later additions are picked up without a name list. - Server announcements (
POST /announce) viaMessageHud.ShowMessage, so they reach unmodded clients; chat cannot be used for this, see the README. - World render also served as JPEG (
/map.jpg), roughly a seventh of the PNG. - Chat is observed at
ZRoutedRpc.RouteRPC, where 1.0 actually routes it, instead ofHandleRoutedRPC, which never sees a message addressed to another player. - Removed the fake server-client patches: they block joins on 1.0 and are unnecessary now.
Fixes that matter now that chat is actually observed:
- The chat lists are no longer edited from three threads at once. They are written from
the game thread, drained by a timer on a pool thread and read by HTTP on a third;
/messagesnow serves a snapshot instead of walking a list another thread is editing. - Chat observation decides whether a routed RPC is chat before looking up the sender. It now runs for every RPC the server forwards, and used to throw and catch a NullReferenceException on each one whose sender was not a live peer.
- No more one console line per chat message and per ping; both are behind
debug. - The static file cache is concurrent: a browser opens several connections on first load and a plain Dictionary can corrupt under that.
- Static files resolve through
Path.GetFileName, so a backslash in the URL cannot walk out of the web root on Windows. /messagessendsapplication/jsonrather thanapplicaion/json.- Player state is built once on the game thread and read from there. The broadcast timer runs on a pool thread and HTTP on others again, and both used to read ZDOs and walk ZNet's live peer list themselves.
/vehiclesonly reports what is in explored territory, andshow_vehiclesturns it off entirely.- The bundled map viewer draws the structures and forest layers. The server has produced both since 2.8.0, but the viewer never asked for them, so the structures heatmap looked missing even with the sweep plainly running in the log. The menu has a toggle for each.
package.shbuilds the web bundle.web/main.jsis a build product and gitignored, so packaging from a fresh clone shipped a viewer with no script at all.
2.8.0
- Build for Valheim 1.0 (Deep North).
- Add a player-built structures overlay (
/structures,/structures/stats,/structures/refresh), coloured by material albedo with piece density driving opacity. - Add death notices to the message feed.
- Route image encode/decode through reflection so the mod builds against the 1.0 assemblies.
- Disable the fake server-side chat client: on 1.0 it prevents players from joining. See "Known issues" in the README.
2.7.1 and earlier
See upstream.