#!/usr/bin/env python3 """Valheim World POI Database Builder Parses locations.json into a queryable SQLite database with spatial support. """ import json, sqlite3, re, sys from collections import Counter DB_PATH = "/tmp/valheim_poi.db" JSON_PATH = "/tmp/valheim_locations.json" # ── Parse position strings like "(1.8, 81.4, 0.0)" ── def parse_pos(s): """Returns (x, y_height, z) — x/z are map coords, y is altitude.""" m = re.match(r'\(?(-?[\d.]+),\s*(-?[\d.]+),\s*(-?[\d.]+)\)?', str(s)) if m: return float(m.group(1)), float(m.group(2)), float(m.group(3)) return None, None, None def classify_prefab(name): """Classify a prefab into a category for querying.""" categories = { 'boss': ['Eikthyrnir', 'Bonemass', 'GDKing', 'GoblinKing', 'Moder', 'Yagluth', 'SeekerQueen'], 'dungeon': ['Crypt', 'SunkenCrypt', 'Cave', 'DvergrTown', 'Mistlands_Dungeon'], 'vegvisir': ['Vegvisir'], 'altar': ['OfferingPlace', 'Runestone', 'StoneHenge'], 'vendor': ['Vendor_BlackForest', 'Haldor', 'Hildir'], 'structure': ['Castle', 'Tower', 'Ruin', 'Fortress', 'Camp'], 'resource': ['TarPit', 'Obsidian', 'Silver', 'Copper', 'Tin', 'DragonEgg', 'Mistlands_Guards'], 'spawner': ['Spawner', 'SpawnArea'], 'point_of_interest': ['StartTemple', 'FulingVillage', 'VikingBroad', 'StoneBridge'], } for cat, patterns in categories.items(): for p in patterns: if p.lower() in name.lower(): return cat return 'other' def create_db(conn): c = conn.cursor() c.executescript(""" DROP TABLE IF EXISTS locations; DROP TABLE IF EXISTS important_contents; DROP TABLE IF EXISTS random_contents; DROP TABLE IF EXISTS dungeon_components; DROP TABLE IF EXISTS prefab_categories; CREATE TABLE locations ( id INTEGER PRIMARY KEY, prefab_name TEXT NOT NULL, category TEXT NOT NULL, pos_x REAL, pos_y REAL, -- altitude/height pos_z REAL, -- map depth has_dungeon INTEGER DEFAULT 0, has_important INTEGER DEFAULT 0, has_random INTEGER DEFAULT 0 ); 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) ); 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) ); 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) ); CREATE INDEX idx_locations_prefab ON locations(prefab_name); CREATE INDEX idx_locations_category ON locations(category); CREATE INDEX idx_locations_pos ON locations(pos_x, pos_z); CREATE INDEX idx_important_name ON important_contents(friendly_name); CREATE INDEX idx_random_type ON random_contents(type, parent); """) conn.commit() def parse_and_insert(conn): c = conn.cursor() loc_id = 0 imp_id = 0 rnd_id = 0 cmp_id = 0 prefab_counter = Counter() category_counter = Counter() imp_counter = Counter() print("Parsing locations.json...") with open(JSON_PATH, 'r', encoding='utf-8') as f: data = json.load(f) total = len(data) print(f"Total locations: {total}") for i, loc in enumerate(data): prefab = loc.get('PrefabName', 'Unknown') pos_str = loc.get('Position', '') x, y, z = parse_pos(pos_str) category = classify_prefab(prefab) prefab_counter[prefab] += 1 category_counter[category] += 1 has_dungeon = 1 if 'DungeonComponents' in loc and loc['DungeonComponents'] else 0 has_important = 1 if 'ImportantContents' in loc and loc['ImportantContents'] else 0 has_random = 1 if 'RandomContents' in loc and loc['RandomContents'] else 0 loc_id += 1 c.execute(""" INSERT INTO locations (id, prefab_name, category, pos_x, pos_y, pos_z, has_dungeon, has_important, has_random) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, (loc_id, prefab, category, x, y, z, has_dungeon, has_important, has_random)) # Important contents for ic in loc.get('ImportantContents', []): imp_id += 1 friendly = ic.get('FriendlyName', '') count = ic.get('Count', 1) icon = ic.get('Icon', '') imp_counter[friendly] += count c.execute(""" INSERT INTO important_contents (id, location_id, friendly_name, count, icon) VALUES (?, ?, ?, ?, ?) """, (imp_id, loc_id, friendly, count, icon)) # Random contents for rc in loc.get('RandomContents', []): rnd_id += 1 c.execute(""" INSERT INTO random_contents (id, location_id, type, parent, name, interior) VALUES (?, ?, ?, ?, ?, ?) """, (rnd_id, loc_id, rc.get('Type',''), rc.get('Parent',''), rc.get('Name',''), rc.get('Interior',0))) # Dungeon components for dc in loc.get('DungeonComponents', []): cmp_id += 1 ik = dc.get('ImageKey', {}) c.execute(""" INSERT INTO dungeon_components (id, location_id, delta_x, delta_y, rotation_y, parent_type, parent_name) VALUES (?, ?, ?, ?, ?, ?, ?) """, (cmp_id, loc_id, dc.get('DeltaX',0), dc.get('DeltaY',0), dc.get('RotationY',0), ik.get('Type',''), ik.get('Parent',''))) if (i + 1) % 5000 == 0: print(f" Processed {i+1}/{total}...") conn.commit() conn.commit() print("\n=== Summary ===") print(f"Locations: {loc_id}") print(f"Important contents: {imp_id}") print(f"Random contents: {rnd_id}") print(f"Dungeon components: {cmp_id}") print("\n--- By Category ---") for cat, cnt in sorted(category_counter.items(), key=lambda x: -x[1]): print(f" {cat}: {cnt}") print("\n--- Top 20 Prefabs ---") for prefab, cnt in prefab_counter.most_common(20): print(f" {prefab}: {cnt}") print("\n--- Top 20 Important Contents ---") for name, cnt in imp_counter.most_common(20): print(f" {name}: {cnt}") return conn def demo_queries(conn): c = conn.cursor() print("\n" + "="*60) print("DEMO QUERIES") print("="*60) # 1. Boss locations print("\n--- Boss Altars ---") for row in c.execute(""" SELECT prefab_name, pos_x, pos_z FROM locations WHERE category='boss' ORDER BY prefab_name """): print(f" {row[0]}: ({row[1]:.0f}, {row[2]:.0f})") # 2. Tar pits print("\n--- Tar Pits ---") for row in c.execute(""" SELECT pos_x, pos_z FROM locations WHERE prefab_name LIKE '%TarPit%' """): print(f" Tar Pit at ({row[0]:.0f}, {row[1]:.0f})") # 3. Surtling cores in dungeons print("\n--- Surtling Core Locations ---") for row in c.execute(""" 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 """): print(f" {row[0]} @ ({row[1]:.0f},{row[2]:.0f}): {row[3]} cores") # 4. Vegvisirs print("\n--- Vegvisirs (Boss Hint Stones) ---") for row in c.execute(""" 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%' """): print(f" {row[0]} at ({row[1]:.0f}, {row[2]:.0f})") # 5. Tar pit density - find clusters print("\n--- Tar Pit Cluster Analysis ---") rows = c.execute(""" SELECT pos_x, pos_z FROM locations WHERE prefab_name LIKE '%TarPit%' """).fetchall() if rows: # Find highest density zone (grid-based) grid = {} grid_size = 1000 # 1000 unit cells for x, z in rows: gx, gz = int(x // grid_size), int(z // grid_size) grid[(gx, gz)] = grid.get((gx, gz), 0) + 1 top = sorted(grid.items(), key=lambda x: -x[1])[:5] for (gx, gz), cnt in top: center_x = gx * grid_size + grid_size // 2 center_z = gz * grid_size + grid_size // 2 print(f" Zone ({center_x}, {center_z}): {cnt} tar pits in {grid_size}u radius") if __name__ == '__main__': import os if os.path.exists(DB_PATH): os.remove(DB_PATH) conn = sqlite3.connect(DB_PATH) conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA synchronous=NORMAL") create_db(conn) parse_and_insert(conn) demo_queries(conn) conn.close() print(f"\nDatabase saved to {DB_PATH}") print(f"Size: {os.path.getsize(DB_PATH) / 1024 / 1024:.1f} MB")