import { findTshark, listInterfaces, startCapture, type CaptureHandle } from '@albion/shared'; import { storeOrders, type CapturedOrder } from './store.js'; /** * LOCAL sniffer: used only when the server runs on the same PC as the game. * When the server is deployed, the Windows agent captures instead and pushes * results to /api/ingest/*. Capture + Photon decoding live in @albion/shared. */ export interface SnifferStatus { tsharkPath: string | null; running: boolean; packets: number; photonEvents: number; marketOrdersSeen: number; pricesStored: number; startedAt: string | null; lastError: string | null; eventCodeCounts: Record; recentOrders: CapturedOrder[]; } let handle: CaptureHandle | null = null; const state = { packets: 0, photonEvents: 0, marketOrdersSeen: 0, pricesStored: 0, startedAt: null as string | null, lastError: null as string | null, eventCodeCounts: new Map(), recentOrders: [] as CapturedOrder[], }; export { listInterfaces }; export function startSniffer(interfaceIds?: string[]): { started: boolean; error?: string } { if (handle) return { started: false, error: 'sniffer already running' }; Object.assign(state, { packets: 0, photonEvents: 0, marketOrdersSeen: 0, pricesStored: 0, startedAt: new Date().toISOString(), lastError: null, recentOrders: [], }); state.eventCodeCounts.clear(); const result = startCapture( { onPacket: () => { state.packets++; }, onEventCode: (code) => { state.photonEvents++; state.eventCodeCounts.set(code, (state.eventCodeCounts.get(code) ?? 0) + 1); }, onOrders: (orders) => { state.marketOrdersSeen += orders.length; void storeOrders(orders) .then(({ pricesStored, recent }) => { state.pricesStored += pricesStored; state.recentOrders = [...recent, ...state.recentOrders].slice(0, 50); }) .catch((e) => { state.lastError = `storing sniffed orders failed: ${e instanceof Error ? e.message : e}`; }); }, onError: (message) => { state.lastError = message; }, }, interfaceIds, ); if ('error' in result) { state.startedAt = null; return { started: false, error: result.error }; } handle = result; return { started: true }; } export function stopSniffer(): void { handle?.stop(); handle = null; } export function snifferStatus(): SnifferStatus { return { tsharkPath: findTshark(), running: handle != null, packets: state.packets, photonEvents: state.photonEvents, marketOrdersSeen: state.marketOrdersSeen, pricesStored: state.pricesStored, startedAt: state.startedAt, lastError: state.lastError, eventCodeCounts: Object.fromEntries([...state.eventCodeCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 40)), recentOrders: state.recentOrders, }; }