albion/server/src/travel/import.ts
Заид Омар Медхат 742b7c2085 Travel directions, agent resilience, opportunities clarity
- 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>
2026-08-23 04:53:28 +05:00

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}`);
}