222 lines
5.9 KiB
TypeScript
222 lines
5.9 KiB
TypeScript
import { EventEmitter } from 'events';
|
|
import { logger } from '../util/logger.js';
|
|
import type { LinkMode, PostAction } from '../types.js';
|
|
import type { ConfigStore, SavedLink } from './ConfigStore.js';
|
|
|
|
export interface TradeLink {
|
|
id: string;
|
|
url: string;
|
|
name: string;
|
|
label: string;
|
|
active: boolean;
|
|
mode: LinkMode;
|
|
postAction: PostAction;
|
|
addedAt: string;
|
|
}
|
|
|
|
export interface BotStatus {
|
|
paused: boolean;
|
|
state: string;
|
|
links: TradeLink[];
|
|
tradesCompleted: number;
|
|
tradesFailed: number;
|
|
uptime: number;
|
|
settings: {
|
|
poe2LogPath: string;
|
|
poe2WindowTitle: string;
|
|
travelTimeoutMs: number;
|
|
waitForMoreItemsMs: number;
|
|
betweenTradesDelayMs: number;
|
|
};
|
|
inventory?: {
|
|
grid: boolean[][];
|
|
items: { row: number; col: number; w: number; h: number }[];
|
|
free: number;
|
|
};
|
|
}
|
|
|
|
export class BotController extends EventEmitter {
|
|
private paused = false;
|
|
private links: Map<string, TradeLink> = new Map();
|
|
private _state = 'IDLE';
|
|
private tradesCompleted = 0;
|
|
private tradesFailed = 0;
|
|
private startTime = Date.now();
|
|
private store: ConfigStore;
|
|
private _inventory: BotStatus['inventory'] = undefined;
|
|
|
|
constructor(store: ConfigStore) {
|
|
super();
|
|
this.store = store;
|
|
this.paused = store.settings.paused;
|
|
}
|
|
|
|
get isPaused(): boolean {
|
|
return this.paused;
|
|
}
|
|
|
|
get state(): string {
|
|
return this._state;
|
|
}
|
|
|
|
set state(s: string) {
|
|
this._state = s;
|
|
this.emit('state-change', s);
|
|
}
|
|
|
|
pause(): void {
|
|
this.paused = true;
|
|
this.store.setPaused(true);
|
|
logger.info('Bot paused');
|
|
this.emit('paused');
|
|
}
|
|
|
|
resume(): void {
|
|
this.paused = false;
|
|
this.store.setPaused(false);
|
|
logger.info('Bot resumed');
|
|
this.emit('resumed');
|
|
}
|
|
|
|
addLink(url: string, name: string = '', mode?: LinkMode, postAction?: PostAction): TradeLink {
|
|
url = this.stripLive(url);
|
|
const id = this.extractId(url);
|
|
const label = this.extractLabel(url);
|
|
// Check if we have saved state for this link
|
|
const savedLink = this.store.links.find((l) => l.url === url);
|
|
const resolvedMode = mode || savedLink?.mode || 'live';
|
|
const link: TradeLink = {
|
|
id,
|
|
url,
|
|
name: name || savedLink?.name || '',
|
|
label,
|
|
active: savedLink?.active !== undefined ? savedLink.active : true,
|
|
mode: resolvedMode,
|
|
postAction: postAction || savedLink?.postAction || (resolvedMode === 'scrap' ? 'salvage' : 'stash'),
|
|
addedAt: new Date().toISOString(),
|
|
};
|
|
this.links.set(id, link);
|
|
this.store.addLink(url, link.name, link.mode, link.postAction);
|
|
logger.info({ id, url, name: link.name, active: link.active, mode: link.mode, postAction: link.postAction }, 'Trade link added');
|
|
this.emit('link-added', link);
|
|
return link;
|
|
}
|
|
|
|
removeLink(id: string): void {
|
|
const link = this.links.get(id);
|
|
this.links.delete(id);
|
|
if (link) {
|
|
this.store.removeLink(link.url);
|
|
} else {
|
|
this.store.removeLinkById(id);
|
|
}
|
|
logger.info({ id }, 'Trade link removed');
|
|
this.emit('link-removed', id);
|
|
}
|
|
|
|
toggleLink(id: string, active: boolean): void {
|
|
const link = this.links.get(id);
|
|
if (!link) return;
|
|
link.active = active;
|
|
this.store.updateLinkById(id, { active });
|
|
logger.info({ id, active }, `Trade link ${active ? 'activated' : 'deactivated'}`);
|
|
this.emit('link-toggled', { id, active, link });
|
|
}
|
|
|
|
updateLinkName(id: string, name: string): void {
|
|
const link = this.links.get(id);
|
|
if (!link) return;
|
|
link.name = name;
|
|
this.store.updateLinkById(id, { name });
|
|
}
|
|
|
|
updateLinkMode(id: string, mode: LinkMode): void {
|
|
const link = this.links.get(id);
|
|
if (!link) return;
|
|
link.mode = mode;
|
|
this.store.updateLinkById(id, { mode });
|
|
logger.info({ id, mode }, 'Trade link mode updated');
|
|
this.emit('link-mode-changed', { id, mode, link });
|
|
}
|
|
|
|
updateLinkPostAction(id: string, postAction: PostAction): void {
|
|
const link = this.links.get(id);
|
|
if (!link) return;
|
|
link.postAction = postAction;
|
|
this.store.updateLinkById(id, { postAction });
|
|
logger.info({ id, postAction }, 'Trade link postAction updated');
|
|
this.emit('link-postaction-changed', { id, postAction, link });
|
|
}
|
|
|
|
isLinkActive(searchId: string): boolean {
|
|
const link = this.links.get(searchId);
|
|
return link ? link.active : false;
|
|
}
|
|
|
|
getLinks(): TradeLink[] {
|
|
return Array.from(this.links.values());
|
|
}
|
|
|
|
recordTradeSuccess(): void {
|
|
this.tradesCompleted++;
|
|
this.emit('trade-completed');
|
|
}
|
|
|
|
recordTradeFailure(): void {
|
|
this.tradesFailed++;
|
|
this.emit('trade-failed');
|
|
}
|
|
|
|
getStatus(): BotStatus {
|
|
const s = this.store.settings;
|
|
return {
|
|
paused: this.paused,
|
|
state: this._state,
|
|
links: this.getLinks(),
|
|
tradesCompleted: this.tradesCompleted,
|
|
tradesFailed: this.tradesFailed,
|
|
uptime: Date.now() - this.startTime,
|
|
settings: {
|
|
poe2LogPath: s.poe2LogPath,
|
|
poe2WindowTitle: s.poe2WindowTitle,
|
|
travelTimeoutMs: s.travelTimeoutMs,
|
|
waitForMoreItemsMs: s.waitForMoreItemsMs,
|
|
betweenTradesDelayMs: s.betweenTradesDelayMs,
|
|
},
|
|
inventory: this._inventory,
|
|
};
|
|
}
|
|
|
|
setInventory(inv: BotStatus['inventory']): void {
|
|
this._inventory = inv;
|
|
}
|
|
|
|
getStore(): ConfigStore {
|
|
return this.store;
|
|
}
|
|
|
|
private stripLive(url: string): string {
|
|
return url.replace(/\/live\/?$/, '');
|
|
}
|
|
|
|
private extractId(url: string): string {
|
|
const parts = url.split('/');
|
|
return parts[parts.length - 1] || url;
|
|
}
|
|
|
|
private extractLabel(url: string): string {
|
|
try {
|
|
const urlObj = new URL(url);
|
|
const parts = urlObj.pathname.split('/').filter(Boolean);
|
|
const poe2Idx = parts.indexOf('poe2');
|
|
if (poe2Idx >= 0 && parts.length > poe2Idx + 2) {
|
|
const league = decodeURIComponent(parts[poe2Idx + 1]);
|
|
const searchId = parts[poe2Idx + 2];
|
|
return `${league} / ${searchId}`;
|
|
}
|
|
} catch {
|
|
// fallback
|
|
}
|
|
return url.substring(0, 60);
|
|
}
|
|
}
|