import fs from 'node:fs'; import path from 'node:path'; import { decryptBin, listInterfaces, parseWorldXml, startCapture, WORLD_BIN_SUBPATH, type AlbionOrder, } from '@albion/shared'; import { ServerClient } from './api.js'; import { loadConfig, saveConfig, configPath, type AgentConfig } from './config.js'; /** * Albion companion agent — runs on the gaming PC. It reads game-local data that * a deployed server can't see and pushes it to the server: * - world map (one-shot, from the game's world.bin) * - live market orders (passive capture while you play) * Read-only. No memory reading, no injection, no automation. * * Usage: * npm run agent -- interfaces list capture interfaces * npm run agent -- world parse + upload the world map, then exit * npm run agent -- run upload world map, then stream market orders (default) * npm run agent -- init write a starter config file */ function resolveWorldBin(gamePath: string): string | null { const direct = path.join(gamePath, WORLD_BIN_SUBPATH); if (fs.existsSync(direct)) return direct; if (gamePath.endsWith('world.bin') && fs.existsSync(gamePath)) return gamePath; return null; } async function uploadWorld(client: ServerClient, cfg: AgentConfig): Promise { const binPath = resolveWorldBin(cfg.gamePath); if (!binPath) { console.error(`world.bin not found under "${cfg.gamePath}". Set gamePath in ${configPath()}.`); return; } const xml = decryptBin(fs.readFileSync(binPath)).toString('utf8'); const world = parseWorldXml(xml); const res = await client.sendWorld(world); console.log(`Uploaded world map: ${res.zones} zones, ${res.connections} connections.`); } function streamOrders(client: ServerClient, cfg: AgentConfig): void { let buffer: AlbionOrder[] = []; let packets = 0; let sent = 0; const capture = startCapture( { onPacket: () => { packets++; }, onOrders: (orders) => { buffer.push(...orders); }, onError: (message) => console.error('capture:', message), }, cfg.interfaces, ); if ('error' in capture) { console.error(capture.error); process.exit(1); } const flush = async () => { if (buffer.length === 0) return; const batch = buffer; buffer = []; try { const res = await client.sendOrders(batch); sent += res.pricesStored ?? 0; console.log(`[${new Date().toLocaleTimeString()}] sent ${batch.length} orders (${res.pricesStored} prices) · ${packets} packets seen · ${sent} prices total`); } catch (e) { buffer.unshift(...batch); // retry next tick console.error('upload failed (will retry):', e instanceof Error ? e.message : e); } }; const timer = setInterval(() => void flush(), Math.max(5, cfg.orderFlushSeconds) * 1000); timer.unref?.(); console.log('Capturing market orders — open a marketplace in game to record prices. Ctrl+C to stop.'); const shutdown = () => { clearInterval(timer); capture.stop(); void flush().finally(() => process.exit(0)); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); } async function main(): Promise { const command = process.argv[2] ?? 'run'; const cfg = loadConfig(); if (command === 'init') { saveConfig(cfg); console.log(`Wrote config to ${configPath()}. Edit serverUrl, apiKey and gamePath, then run: npm run agent`); return; } if (command === 'interfaces') { const ifaces = listInterfaces(); if (ifaces.length === 0) { console.error('No interfaces found. Is Wireshark/tshark installed?'); return; } for (const i of ifaces) console.log(`${i.id}\t${i.name}`); console.log('\nAdd the ids you want to "interfaces" in your config, or leave empty for all.'); return; } const client = new ServerClient(cfg); console.log(`Server: ${cfg.serverUrl}`); try { await client.ping(); console.log('Server reachable.'); } catch (e) { console.error(`Cannot reach server at ${cfg.serverUrl}:`, e instanceof Error ? e.message : e); process.exit(1); } if (command === 'world') { await uploadWorld(client, cfg); return; } if (command === 'run') { await uploadWorld(client, cfg).catch((e) => console.error('world upload failed:', e instanceof Error ? e.message : e)); streamOrders(client, cfg); return; } console.error(`Unknown command: ${command}. Use: init | interfaces | world | run`); process.exit(1); } void main();