- Travel planner shows the compass bearing (N/NE/.../NW) of each hop, computed from world-map coordinates (+x East, +y North, calibrated against Bridgewatch=south, Lymhurst=east). Zone positions parsed from world.bin into new zones.pos_x/pos_y columns. - Agent: retry ingest POSTs with backoff + longer timeout, graceful failure (no more uncaught crash on a transient connect timeout). - Opportunities: state the city prices apply to in the breakdown, and show tier+enchant as one "T4.3" chip with a tooltip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
79 lines
3.4 KiB
TypeScript
79 lines
3.4 KiB
TypeScript
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<string | null> {
|
|
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<WorldImportStats> {
|
|
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<string>();
|
|
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<WorldImportStats> {
|
|
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}`);
|
|
}
|