- Phase 1: POI database (11,309 locations, SQLite with spatial queries) - Phase 2: Tile decoder + Leaflet.js web viewer (3 map layers, search, categories) - Phase 3: .db/.fch parser WIP for fog-of-war overlay - Full README with roadmap, architecture, and usage docs |
||
|---|---|---|
| data | ||
| notes | ||
| output | ||
| src | ||
| .gitignore | ||
| README.md | ||
Nerdhalla Valheim World Map
Seed:
yzZ5fr2tGa| World: 24000 × 24000 | Biomes: 9 | POIs: 11,309
An interactive web map of the Nerdhalla Valheim world, built from a valheim-map.world export. Includes biome/height map tiles, a searchable POI database, and a roadmap for fog-of-war integration.
Quick Start
# Serve the map viewer
cd ~/projects/valheim-map/output && python3 -m http.server 8765
# Open: http://72.60.69.120:8765/index.html
Live: http://72.60.69.120:8765/index.html (port 8765, ufw allowed)
Project Structure
~/projects/valheim-map/
├── src/ # Source scripts
│ ├── valheim_db.py # Phase 1: Build POI SQLite database from locations.json
│ ├── decode_valheim_tiles.py # Phase 2: Decode .bin.gz tiles → PNG images
│ ├── build_viewer.py # Phase 2: Generate Leaflet.js web viewer
│ ├── parse_valheim_db.py # Phase 3: Attempted .db parser (WIP)
│ └── parse_valheim_zpackage.py # Phase 3: ZPackage format exploration (WIP)
├── data/ # Data files (small)
│ ├── valheim_poi.db # SQLite POI database (55 MB)
│ └── Nerdhalla.fwl # World metadata file (499 bytes)
├── output/ # Generated map assets
│ ├── index.html # Leaflet.js web viewer (5.4 MB)
│ ├── world_composite.png # Full world map, biome+height (3.7 MB)
│ ├── world_biome.png # Full world biome classification (374 KB)
│ ├── world_height.png # Full world heightmap (2.4 MB)
│ ├── XX-YY_*.png # Individual tile images (16 tiles × 3 layers)
│ └── explored.png # (future) Explored/unexplored overlay
├── notes/ # Reference notes
│ └── tile-format-readme.txt # Original valheim-map.world tile format spec
└── README.md # This file
Large files not in repo (stored in /tmp/):
/tmp/valheim_locations.json— 52 MB raw location export/tmp/Nerdhalla.db— 85 MB world save from Nerdcade AMP container/tmp/Nerdhalla.json— 1.5 GB JSON conversion (valheim-save-tools)/tmp/valheim_tiles/tiles/*.bin.gz— 16 tile files, ~100 MB total
Roadmap
✅ Phase 1 — POI Database (Complete)
Script: src/valheim_db.py
Parsed locations.json (52 MB) into a queryable SQLite database with spatial support.
Tables:
locations— 11,309 entries (prefab name, category, coordinates, dungeon/loot flags)important_contents— 24,407 entries (veggisirs, surtling cores, treasure, spawners)random_contents— 457,617 entries (dungeon interior loot)dungeon_components— 49,338 entries (room layouts)
Spatial queries work:
- "Highest density of tar pits?" → Zone (1500, 3500) has 11 in 1000u radius
- "Most Surtling Cores?" → Crypt3 @ (2039, -1162) has 20 cores
- "Best base location for tar farming?" → (1210, -4159) — 5 tar pits in 200m, Plains biome
✅ Phase 2 — Web Map Viewer (Complete)
Scripts: src/decode_valheim_tiles.py, src/build_viewer.py
Decoded 16 tile files (4×4 grid, 1024×1024 samples each) into PNG images and built a Leaflet.js viewer.
Features:
- 3 map layers: Composite (biome+height shading), Biome (9-color classification), Height (grayscale elevation)
- 11,309 POI markers — color-coded by category, clickable with loot popups
- Category legend — toggle groups on/off (boss, dungeon, vegvisir, vendor, resource, etc.)
- Search — by prefab name or category
- Coordinate display — Valheim world coords in popups
Layer toggle buttons (bottom-left): Composite | Biome | Height
⏳ Phase 3 — Fog-of-War Overlay (Blocked)
Goal: Show explored vs unexplored areas on the map.
Blocked by: The explored map data is stored per-player in .fch character files on each player's local machine, NOT in the world .db file. The .db contains 1.8M ZDOs (world objects, terrain, structures) but no explored bitmap.
What we have:
Nerdhalla.db(85 MB) — pulled from Nerdcade AMP container, parsed via valheim-save-toolsNerdhalla.fwl(499 bytes) — world settings (seed, presets, difficulty)- ZPackage parser WIP in
src/parse_valheim_zpackage.py
What's needed: Player .fch files from:
- Windows:
%USERPROFILE%\AppData\LocalLow\IronGate\Valheim\characters\ - Linux:
~/.config/unity3d/IronGate/Valheim/characters/
🔮 Phase 4 — Cartography Table Integration (Planned)
Goal: A shared map of truth for the server.
Approach options (in order of preference):
-
ServerSideMap mod (BepInEx) — Gold standard. Strips map data from client files, hosts on server in real-time. As you explore, fog clears for everyone. Pin sharing with dedup. Requires BepInEx on AMP Valheim instance + all players install the mod.
-
Periodic merge script — Collect
.fchfiles from players, merge explored bitmaps (OR operation), deduplicate pins (same name + within 15m), write back clean files. Run on cron. -
Cartography table automation — Lightweight BepInEx plugin that auto-triggers "Record Discoveries" near the table. Still manual but removes forgetfulness.
Pin cleaning strategy:
- Dedup by name + proximity (15m radius)
- "Last writer wins" for conflicting pins
- Ghost pin prevention: track pin origin, only remove if no active player has it
Technical Details
Tile Format
From the valheim-map.world export Readme:
struct MapSample {
uint16_t biome; // 2 bytes
float height; // 4 bytes
float forestFactor; // 4 bytes
} // 10 bytes per sample
- 1024 × 1024 samples per tile = 10,485,760 bytes uncompressed
- 4 × 4 tiles = 16 tiles, 24000 × 24000 Valheim units
- Gzip compressed individually
Biome enum: None=0, Meadows=1, Swamp=2, Mountain=4, BlackForest=8, Plains=16, AshLands=32, DeepNorth=64, Ocean=256, Mistlands=512
Coordinate Mapping
Valheim coords: (-12000, -12000) to (+12000, +12000)
Image coords: (0, 0) to (4096, 4096)
pixel_x = (valheim_x + 12000) / 24000 * 4096
pixel_y = (valheim_z + 12000) / 24000 * 4096
Zone System
From the Valheim Wiki:
- Zones are 64m × 64m
- Generated: 9×9 around each player (288m radius)
- Loaded: 5×5 (160m radius)
- Active: 3×3 (96m radius)
- Current: the zone(s) containing the player
Database Schema
-- Core locations table
CREATE TABLE locations (
id INTEGER PRIMARY KEY,
prefab_name TEXT NOT NULL,
category TEXT NOT NULL,
pos_x REAL, pos_y REAL, pos_z REAL,
has_dungeon INTEGER DEFAULT 0,
has_important INTEGER DEFAULT 0,
has_random INTEGER DEFAULT 0
);
-- Important contents (veggisirs, cores, treasure)
CREATE TABLE important_contents (
id INTEGER PRIMARY KEY,
location_id INTEGER NOT NULL,
friendly_name TEXT,
count INTEGER,
icon TEXT,
FOREIGN KEY (location_id) REFERENCES locations(id)
);
-- Random contents (dungeon interior loot)
CREATE TABLE random_contents (
id INTEGER PRIMARY KEY,
location_id INTEGER NOT NULL,
type TEXT, parent TEXT, name TEXT, interior INTEGER,
FOREIGN KEY (location_id) REFERENCES locations(id)
);
-- Dungeon components (room layouts)
CREATE TABLE dungeon_components (
id INTEGER PRIMARY KEY,
location_id INTEGER NOT NULL,
delta_x REAL, delta_y REAL, rotation_y REAL,
parent_type TEXT, parent_name TEXT,
FOREIGN KEY (location_id) REFERENCES locations(id)
);
.db File Format (ZPackage)
The world .db file uses Unity's ZPackage binary serialization format:
int32: world_version
double: net_time
int64: my_id
uint32: next_uid
int32: num_zdos
ZDO[]: zone data objects (1,800,945 in Nerdhalla)
Zones: zone management data
RandomEvent: random event state
SHA512: 64-byte hash
Each ZDO contains: uid, prefab hash, owner, data type, revision, key-value pairs (floats, vectors, strings, byte arrays), position, rotation.
Current world version: 37 (valheim-save-tools max: 34 — parses with warning)
Usage Examples
Query the POI database
sqlite3 ~/projects/valheim-map/data/valheim_poi.db
# Find all tar pits
SELECT pos_x, pos_z FROM locations WHERE prefab_name LIKE '%TarPit%';
# Find boss altars
SELECT prefab_name, pos_x, pos_z FROM locations WHERE category='boss';
# Find dungeons with most surtling cores
SELECT l.prefab_name, l.pos_x, l.pos_z, SUM(ic.count) as total
FROM locations l JOIN important_contents ic ON l.id = ic.location_id
WHERE ic.friendly_name LIKE '%SurtlingCore%'
GROUP BY l.id ORDER BY total DESC LIMIT 10;
# Find vegvisirs
SELECT ic.friendly_name, l.pos_x, l.pos_z
FROM locations l JOIN important_contents ic ON l.id = ic.location_id
WHERE ic.friendly_name LIKE '%Vegvisir%';
Rebuild from scratch
# 1. Build POI database
python3 src/valheim_db.py
# 2. Decode tiles to PNG
python3 src/decode_valheim_tiles.py
# 3. Build web viewer
python3 src/build_viewer.py
# 4. Serve
cd output && python3 -m http.server 8765
Data Sources
| Source | File | Size | Origin |
|---|---|---|---|
| valheim-map.world export | locations.json |
52 MB | ROG (E:\Downloads\MapData_yzZ5fr2tGa) |
| valheim-map.world export | tiles/*.bin.gz |
~100 MB | ROG |
| AMP Valheim container | Nerdhalla.db |
85 MB | Nerdcade (docker cp AMP_Nerdhalla01:/AMP/Valheim/896660/Saves/worlds_local/) |
| AMP Valheim container | Nerdhalla.fwl |
499 B | Nerdcade |
Future Work
- Fog-of-war overlay — get
.fchfiles from players, extract explored bitmap - ServerSideMap mod — install BepInEx on AMP Valheim, deploy shared map
- Pin cleaning/syncing — dedup, merge, ghost pin prevention
- Auto-update cron — periodically pull
.dbfrom Nerdcade, regenerate map - Forgejo repo — host this project in a self-hosted git instance
- FoundryVTT export — convert POI data for tabletop use
- Mobile-friendly — responsive layout for phone viewing