Initial commit: POE2 automated trade bot
Monitors pathofexile.com/trade2 for new listings, travels to seller hideouts, buys items from public stash tabs, and stores them. Includes persistent C# OCR daemon for fast screen capture + Windows native OCR, web dashboard for managing trade links and settings, and full game automation via Win32 SendInput. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
41d174195e
28 changed files with 6449 additions and 0 deletions
130
src/log/ClientLogWatcher.ts
Normal file
130
src/log/ClientLogWatcher.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import { watch } from 'chokidar';
|
||||
import { createReadStream, statSync } from 'fs';
|
||||
import { createInterface } from 'readline';
|
||||
import { logger } from '../util/logger.js';
|
||||
|
||||
export interface LogEvents {
|
||||
'area-entered': (area: string) => void;
|
||||
'whisper-received': (data: { player: string; message: string }) => void;
|
||||
'whisper-sent': (data: { player: string; message: string }) => void;
|
||||
'trade-accepted': () => void;
|
||||
'party-joined': (player: string) => void;
|
||||
'party-left': (player: string) => void;
|
||||
line: (line: string) => void;
|
||||
}
|
||||
|
||||
export class ClientLogWatcher extends EventEmitter {
|
||||
private watcher: ReturnType<typeof watch> | null = null;
|
||||
private fileOffset: number = 0;
|
||||
private logPath: string;
|
||||
|
||||
constructor(logPath: string) {
|
||||
super();
|
||||
this.logPath = logPath;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
// Start reading from end of file (only new lines)
|
||||
try {
|
||||
const stats = statSync(this.logPath);
|
||||
this.fileOffset = stats.size;
|
||||
} catch {
|
||||
logger.warn({ path: this.logPath }, 'Log file not found yet, will watch for creation');
|
||||
this.fileOffset = 0;
|
||||
}
|
||||
|
||||
this.watcher = watch(this.logPath, {
|
||||
persistent: true,
|
||||
usePolling: true,
|
||||
interval: 200,
|
||||
});
|
||||
|
||||
this.watcher.on('change', () => {
|
||||
this.readNewLines();
|
||||
});
|
||||
|
||||
logger.info({ path: this.logPath }, 'Watching Client.txt for game events');
|
||||
}
|
||||
|
||||
private readNewLines(): void {
|
||||
const stream = createReadStream(this.logPath, {
|
||||
start: this.fileOffset,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
const rl = createInterface({ input: stream });
|
||||
let bytesRead = 0;
|
||||
|
||||
rl.on('line', (line) => {
|
||||
bytesRead += Buffer.byteLength(line, 'utf-8') + 2; // +2 for \r\n on Windows
|
||||
if (line.trim()) {
|
||||
this.parseLine(line.trim());
|
||||
}
|
||||
});
|
||||
|
||||
rl.on('close', () => {
|
||||
this.fileOffset += bytesRead;
|
||||
});
|
||||
}
|
||||
|
||||
private parseLine(line: string): void {
|
||||
this.emit('line', line);
|
||||
|
||||
// Area transition: "You have entered Hideout"
|
||||
const areaMatch = line.match(/You have entered (.+?)\.?$/);
|
||||
if (areaMatch) {
|
||||
const area = areaMatch[1];
|
||||
logger.info({ area }, 'Area entered');
|
||||
this.emit('area-entered', area);
|
||||
return;
|
||||
}
|
||||
|
||||
// Incoming whisper: "@From PlayerName: message"
|
||||
const whisperFromMatch = line.match(/@From\s+(.+?):\s+(.+)$/);
|
||||
if (whisperFromMatch) {
|
||||
const data = { player: whisperFromMatch[1], message: whisperFromMatch[2] };
|
||||
logger.info(data, 'Whisper received');
|
||||
this.emit('whisper-received', data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Outgoing whisper: "@To PlayerName: message"
|
||||
const whisperToMatch = line.match(/@To\s+(.+?):\s+(.+)$/);
|
||||
if (whisperToMatch) {
|
||||
const data = { player: whisperToMatch[1], message: whisperToMatch[2] };
|
||||
this.emit('whisper-sent', data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Party join: "PlayerName has joined the party"
|
||||
const partyJoinMatch = line.match(/(.+?) has joined the party/);
|
||||
if (partyJoinMatch) {
|
||||
logger.info({ player: partyJoinMatch[1] }, 'Player joined party');
|
||||
this.emit('party-joined', partyJoinMatch[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Party leave: "PlayerName has left the party"
|
||||
const partyLeaveMatch = line.match(/(.+?) has left the party/);
|
||||
if (partyLeaveMatch) {
|
||||
this.emit('party-left', partyLeaveMatch[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trade accepted
|
||||
if (line.includes('Trade accepted') || line.includes('Trade completed')) {
|
||||
logger.info('Trade accepted/completed');
|
||||
this.emit('trade-accepted');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.watcher) {
|
||||
await this.watcher.close();
|
||||
this.watcher = null;
|
||||
}
|
||||
logger.info('Client log watcher stopped');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue