switched to new way :)
This commit is contained in:
parent
b03a2a25f1
commit
f22d182c8f
30 changed files with 0 additions and 0 deletions
|
|
@ -1,182 +0,0 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import { watch } from 'chokidar';
|
||||
import { createReadStream, statSync, openSync, readSync, closeSync } 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;
|
||||
|
||||
/** Last area we transitioned into (from [SCENE] Set Source or "You have entered"). */
|
||||
currentArea: 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;
|
||||
// Read tail of log to determine current area before we start watching
|
||||
this.detectCurrentArea(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, currentArea: this.currentArea || '(unknown)' }, 'Watching Client.txt for game events');
|
||||
}
|
||||
|
||||
/** Read the last chunk of the log file to determine the current area. */
|
||||
private detectCurrentArea(fileSize: number): void {
|
||||
const TAIL_BYTES = 8192;
|
||||
const start = Math.max(0, fileSize - TAIL_BYTES);
|
||||
const buf = Buffer.alloc(Math.min(TAIL_BYTES, fileSize));
|
||||
const fd = openSync(this.logPath, 'r');
|
||||
try {
|
||||
readSync(fd, buf, 0, buf.length, start);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
const tail = buf.toString('utf-8');
|
||||
const lines = tail.split(/\r?\n/);
|
||||
|
||||
// Walk backwards to find the most recent area transition
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i];
|
||||
const sceneMatch = line.match(/\[SCENE\] Set Source \[(.+?)\]/);
|
||||
if (sceneMatch && sceneMatch[1] !== '(null)') {
|
||||
this.currentArea = sceneMatch[1];
|
||||
logger.info({ area: this.currentArea }, 'Detected current area from log tail');
|
||||
return;
|
||||
}
|
||||
const areaMatch = line.match(/You have entered (.+?)\.?$/);
|
||||
if (areaMatch) {
|
||||
this.currentArea = areaMatch[1];
|
||||
logger.info({ area: this.currentArea }, 'Detected current area from log tail');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: "[SCENE] Set Source [Shoreline Hideout]"
|
||||
// POE2 uses this format instead of "You have entered ..."
|
||||
const sceneMatch = line.match(/\[SCENE\] Set Source \[(.+?)\]/);
|
||||
if (sceneMatch) {
|
||||
const area = sceneMatch[1];
|
||||
// Skip the "(null)" transition — it's an intermediate state before the real area loads
|
||||
if (area !== '(null)') {
|
||||
this.currentArea = area;
|
||||
logger.info({ area }, 'Area entered');
|
||||
this.emit('area-entered', area);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy fallback: "You have entered Hideout"
|
||||
const areaMatch = line.match(/You have entered (.+?)\.?$/);
|
||||
if (areaMatch) {
|
||||
const area = areaMatch[1];
|
||||
this.currentArea = area;
|
||||
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