import fs from 'node:fs'; import path from 'node:path'; import { decryptBin, parseWorldXml, WORLD_BIN_SUBPATH, type ParsedWorld } from '@albion/shared'; import { getMeta, setMeta, transaction } from '../db/index.js'; /** * World map (zones + portal connections) storage. Two entry points: * - importWorldLocal(): read the game's world.bin from THIS machine's disk * (used when the server runs on the gaming PC). * - storeWorld(): persist a ParsedWorld sent by the Windows agent (used when * the server is deployed and the game runs elsewhere). * Parsing/decryption lives in @albion/shared so the agent reuses it verbatim. */ const COMMON_GAME_DIRS = [ 'E:\\albion', 'C:\\Program Files (x86)\\Albion Online', 'C:\\Program Files (x86)\\AlbionOnline', 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Albion Online', ]; export async function findWorldBin(): Promise { const configured = await getMeta('game_path'); const candidates = configured ? [configured, ...COMMON_GAME_DIRS] : COMMON_GAME_DIRS; for (const dir of candidates) { const direct = path.join(dir, WORLD_BIN_SUBPATH); if (fs.existsSync(direct)) return direct; if (dir.endsWith('world.bin') && fs.existsSync(dir)) return dir; } return null; } export interface WorldImportStats { zones: number; connections: number; source: string; } export async function storeWorld(world: ParsedWorld, source: string): Promise { await transaction(async (tx) => { await tx.exec('DELETE FROM zone_connections; DELETE FROM zones;'); // de-dup edges (both directions collapse to one sorted pair) const edgeSet = new Set(); for (const [a, b] of world.connections) edgeSet.add([a, b].sort().join('|')); // chunked bulk inserts const zoneRows = world.zones.map((z) => [z.albionId, z.name, z.type, z.color, z.x, z.y]); for (let i = 0; i < zoneRows.length; i += 5000) { const chunk = zoneRows.slice(i, i + 5000); const params: unknown[] = []; const groups = chunk.map((r) => `(${r.map((v) => `$${params.push(v)}`).join(', ')})`); await tx.query(`INSERT INTO zones (albion_id, name, type, color, pos_x, pos_y) VALUES ${groups.join(', ')} ON CONFLICT (albion_id) DO NOTHING`, params); } const edges = [...edgeSet].map((e) => e.split('|')); for (let i = 0; i < edges.length; i += 10000) { const chunk = edges.slice(i, i + 10000); const params: unknown[] = []; const groups = chunk.map((r) => `(${r.map((v) => `$${params.push(v)}`).join(', ')})`); await tx.query(`INSERT INTO zone_connections (zone_a, zone_b) VALUES ${groups.join(', ')} ON CONFLICT (zone_a, zone_b) DO NOTHING`, params); } }); await setMeta('world_imported_at', new Date().toISOString()); await setMeta('world_source', source); return { zones: world.zones.length, connections: world.connections.length, source }; } /** Read + parse world.bin from this machine's game install. */ export async function importWorldLocal(): Promise { const binPath = await findWorldBin(); if (!binPath) { throw new Error( 'world.bin not found on this machine. Set your Albion Online game folder in Settings, ' + 'or push it from the Windows agent if the game runs on another PC.', ); } const xml = decryptBin(fs.readFileSync(binPath)).toString('utf8'); const world = parseWorldXml(xml); return storeWorld(world, `local:${binPath}`); }