mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-04-23 02:57:48 -05:00
Some checks failed
Node.js CI / build (18.x) (push) Has been cancelled
This minimizes side effects of import/require across the codebase, and lets the caller be responsible of initializing child processeses, as well as other async logic, such as restoring saved battles.
513 lines
19 KiB
TypeScript
513 lines
19 KiB
TypeScript
/**
|
|
* Battle search - handles searching battle logs.
|
|
*/
|
|
import { FS, Utils, ProcessManager } from '../../lib';
|
|
|
|
import * as ConfigLoader from '../config-loader';
|
|
|
|
import * as path from 'path';
|
|
import * as child_process from 'child_process';
|
|
|
|
const BATTLESEARCH_PROCESS_TIMEOUT = 3 * 60 * 60 * 1000; // 3 hours
|
|
|
|
interface BattleOutcome {
|
|
lost: string;
|
|
won: string;
|
|
turns: string;
|
|
}
|
|
|
|
interface BattleSearchResults {
|
|
totalBattles: number;
|
|
/** Total battle outcomes. Null when only searching for one userid. */
|
|
totalOutcomes: BattleOutcome[] | null;
|
|
totalWins: { [k: string]: number };
|
|
totalLosses: { [k: string]: number };
|
|
totalTies: number;
|
|
timesBattled: { [k: string]: number };
|
|
}
|
|
|
|
export async function runBattleSearch(userids: ID[], month: string, tierid: ID, turnLimit?: number) {
|
|
const useRipgrep = await ConfigLoader.checkRipgrepAvailability();
|
|
const pathString = `${month}/${tierid}/`;
|
|
const results: { [k: string]: BattleSearchResults } = {};
|
|
let files = [];
|
|
try {
|
|
files = await Monitor.logPath(pathString).readdir();
|
|
} catch (err: any) {
|
|
if (err.code === 'ENOENT') {
|
|
return results;
|
|
}
|
|
throw err;
|
|
}
|
|
const [userid] = userids;
|
|
files = files.filter(item => item.startsWith(month)).map(item => Monitor.logPath(`${month}/${tierid}/${item}`).path);
|
|
|
|
if (useRipgrep) {
|
|
// Matches non-word (including _ which counts as a word) characters between letters/numbers
|
|
// in a user's name so the userid can case-insensitively be matched to the name.
|
|
// We want to be order-insensitive for the player IDs. This union is much cheaper than using PCRE.
|
|
const userUnion = userids.map(id => `${[...id].join('[^a-zA-Z0-9]*')}[^a-zA-Z0-9]*`).join('|');
|
|
const regexString = userids.map(id => `(.*?("p(1|2)":"(${userUnion})"))`).join('');
|
|
let output;
|
|
try {
|
|
output = await ProcessManager.exec(['rg', '-i', regexString, '--no-line-number', '-tjson', ...files]);
|
|
} catch {
|
|
return results;
|
|
}
|
|
for (const line of output.stdout.split('\n').reverse()) {
|
|
const [file, raw] = Utils.splitFirst(line, ':');
|
|
if (!raw || !line) continue;
|
|
const data = JSON.parse(raw);
|
|
const day = file.split('/')[3];
|
|
if (!results[day]) {
|
|
results[day] = {
|
|
totalBattles: 0,
|
|
totalWins: {},
|
|
totalOutcomes: userids.length > 1 ? [] : null,
|
|
totalLosses: {},
|
|
totalTies: 0,
|
|
timesBattled: {},
|
|
};
|
|
}
|
|
const p1id = toID(data.p1);
|
|
const p2id = toID(data.p2);
|
|
|
|
if (userids.length > 1) {
|
|
// looking for specific userids, only register ones where those users are players
|
|
if (userids.filter(item => [p1id, p2id].includes(item)).length < userids.length) continue;
|
|
} else {
|
|
if (!(p1id === userid || p2id === userid)) continue;
|
|
}
|
|
|
|
if (turnLimit && data.turns > turnLimit) continue;
|
|
if (!results[day]) {
|
|
results[day] = {
|
|
totalBattles: 0,
|
|
totalWins: {},
|
|
totalOutcomes: userids.length > 1 ? [] : null,
|
|
totalLosses: {},
|
|
totalTies: 0,
|
|
timesBattled: {},
|
|
};
|
|
}
|
|
results[day].totalBattles++;
|
|
const winnerid = toID(data.winner);
|
|
const loser = winnerid === p1id ? p2id : p1id;
|
|
if (userids.includes(winnerid)) {
|
|
if (!results[day].totalWins[winnerid]) results[day].totalWins[winnerid] = 0;
|
|
results[day].totalWins[winnerid]++;
|
|
} else if (data.winner) {
|
|
if (!results[day].totalLosses[loser]) results[day].totalLosses[loser] = 0;
|
|
results[day].totalLosses[loser]++;
|
|
} else {
|
|
results[day].totalTies++;
|
|
}
|
|
// explicitly state 0 of stats if none
|
|
for (const id of userids) {
|
|
if (!results[day].totalLosses[id]) results[day].totalLosses[id] = 0;
|
|
if (!results[day].totalWins[id]) results[day].totalWins[id] = 0;
|
|
}
|
|
|
|
const outcomes = results[day].totalOutcomes;
|
|
if (outcomes) {
|
|
outcomes.push({ won: winnerid, lost: loser, turns: data.turns });
|
|
}
|
|
// we only want foe data for single-userid searches
|
|
const foe = userids.length > 1 ? null : userid === toID(data.p1) ? toID(data.p2) : toID(data.p1);
|
|
if (foe) {
|
|
if (!results[day].timesBattled[foe]) results[day].timesBattled[foe] = 0;
|
|
results[day].timesBattled[foe]++;
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
for (const file of files) {
|
|
const subFiles = FS(`${file}`).readdirSync();
|
|
const day = file.split('/')[3];
|
|
for (const dayFile of subFiles) {
|
|
const json = FS(`${file}/${dayFile}`).readIfExistsSync();
|
|
const data = JSON.parse(json);
|
|
const p1id = toID(data.p1);
|
|
const p2id = toID(data.p2);
|
|
if (userids.length > 1) {
|
|
// looking for specific userids, only register ones where those users are players
|
|
if (userids.filter(item => item === p1id || item === p2id).length < userids.length) continue;
|
|
} else {
|
|
if (!(p1id === userid || p2id === userid)) continue;
|
|
}
|
|
if (turnLimit && data.turns > turnLimit) continue;
|
|
if (!results[day]) {
|
|
results[day] = {
|
|
totalBattles: 0,
|
|
totalWins: {},
|
|
totalOutcomes: [],
|
|
totalLosses: {},
|
|
totalTies: 0,
|
|
timesBattled: {},
|
|
};
|
|
}
|
|
results[day].totalBattles++;
|
|
const winnerid = toID(data.winner);
|
|
const loser = winnerid === p1id ? p2id : p1id;
|
|
if (userids.includes(winnerid)) {
|
|
if (!results[day].totalWins[winnerid]) results[day].totalWins[winnerid] = 0;
|
|
results[day].totalWins[winnerid]++;
|
|
} else if (data.winner) {
|
|
if (!results[day].totalLosses[loser]) results[day].totalLosses[loser] = 0;
|
|
results[day].totalLosses[loser]++;
|
|
} else {
|
|
results[day].totalTies++;
|
|
}
|
|
// explicitly state 0 of stats if none
|
|
for (const id of userids) {
|
|
if (!results[day].totalLosses[id]) results[day].totalLosses[id] = 0;
|
|
if (!results[day].totalWins[id]) results[day].totalWins[id] = 0;
|
|
}
|
|
|
|
const outcomes = results[day].totalOutcomes;
|
|
if (outcomes) {
|
|
outcomes.push({ won: winnerid, lost: loser, turns: data.turns });
|
|
}
|
|
|
|
// we don't want foe data if we're searching for 2 userids
|
|
const foe = userids.length > 1 ? null : userid === p1id ? p2id : p1id;
|
|
if (foe) {
|
|
if (!results[day].timesBattled[foe]) results[day].timesBattled[foe] = 0;
|
|
results[day].timesBattled[foe]++;
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function buildResults(
|
|
data: { [k: string]: BattleSearchResults }, userids: ID[],
|
|
month: string, tierid: ID, turnLimit?: number
|
|
) {
|
|
let buf = `>view-battlesearch-${userids.join('-')}--${turnLimit}--${month}--${tierid}--confirm\n|init|html\n|title|[Battle Search][${userids.join('-')}][${tierid}][${month}]\n`;
|
|
buf += `|pagehtml|<div class="pad ladder"><p>`;
|
|
buf += `${tierid} battles on ${month} where `;
|
|
buf += userids.length > 1 ? `the users ${userids.join(', ')} were players` : `the user ${userids[0]} was a player`;
|
|
buf += turnLimit ? ` and the battle lasted less than ${turnLimit} turn${Chat.plural(turnLimit)}` : '';
|
|
buf += `:</p><li style="display: inline; list-style: none"><a href="/view-battlesearch-${userids.join('-')}--${turnLimit}--${month}--${tierid}" target="replace">`;
|
|
buf += `<button class="button">Back</button></a></li><br />`;
|
|
if (userids.length > 1) {
|
|
const outcomes: BattleOutcome[] = [];
|
|
for (const day in data) {
|
|
const curOutcomes = data[day].totalOutcomes;
|
|
if (curOutcomes) outcomes.push(...curOutcomes);
|
|
}
|
|
buf += `<table><tbody><tr><h3 style="margin: 5px auto">Full summary</h3></tr>`;
|
|
buf += `<tr><th>Won</th><th>Lost</th><th>Turns</th></tr>`;
|
|
for (const battle of outcomes) {
|
|
const { won, lost, turns } = battle;
|
|
buf += `<tr><td>${won}</td><td>${lost}</td><td>${turns}</td></tr>`;
|
|
}
|
|
}
|
|
buf += `</tbody></table><br />`;
|
|
for (const day in data) {
|
|
const dayStats = data[day];
|
|
buf += `<p style="text-align:left">`;
|
|
const { totalWins, totalLosses } = dayStats;
|
|
buf += `<table style=""><tbody><tr><th colspan="2"><h3 style="margin: 5px auto">${day}</h3>`;
|
|
buf += `</th></tr><tr><th>Category</th><th>Number</th></tr>`;
|
|
buf += `<tr><td>Total Battles</td><td>${dayStats.totalBattles}</td></tr>`;
|
|
for (const id in totalWins) {
|
|
// hide userids if we're only searching for 1
|
|
buf += `<tr><td>Total Wins${userids.length > 1 ? ` (${id}) ` : ''}</td><td>${totalWins[id]}</td></tr>`;
|
|
}
|
|
for (const id in totalLosses) {
|
|
buf += `<tr><td>Total Losses${userids.length > 1 ? ` (${id}) ` : ''}</td><td>${totalLosses[id]}</td></tr>`;
|
|
}
|
|
if (userids.length < 2) {
|
|
buf += `<tr><th>Opponent</th><th>Times Battled</th></tr>`;
|
|
const [userid] = userids;
|
|
for (const foe in dayStats.timesBattled) {
|
|
buf += `<tr><td>`;
|
|
buf += `<a href="/view-battlesearch-${userid}-${foe}--${turnLimit}--${month}--${tierid}" target="replace">${foe}</a>`;
|
|
buf += `</td><td>${dayStats.timesBattled[foe]}</td></tr>`;
|
|
}
|
|
}
|
|
buf += `</p><br />`;
|
|
}
|
|
buf += `</tbody></table></div>`;
|
|
return buf;
|
|
}
|
|
|
|
async function rustBattleSearch(
|
|
context: Chat.PageContext, targetUser: string, daysString: string, format: ID
|
|
) {
|
|
const days = parseInt(daysString);
|
|
if (days < 1 || days > 15) {
|
|
throw new Chat.ErrorMessage(`Days must be between 1 and 15. To search longer ranges, use psbattletools manually on sim3.`);
|
|
}
|
|
if (!targetUser) {
|
|
throw new Chat.ErrorMessage(`No user specified.`);
|
|
}
|
|
const { connection, user } = context;
|
|
const currentDayOfMonth = (new Date()).getDate();
|
|
if (days < 1 || days > 15) {
|
|
throw new Chat.ErrorMessage(`Days must be between 1 and 15. To search longer ranges, use psbattletools manually on sim3.`);
|
|
}
|
|
|
|
try {
|
|
await ProcessManager.exec(`psbattletools --version`, {
|
|
env: { PATH: `${process.env.PATH}:${process.env.HOME}/.cargo/bin` },
|
|
});
|
|
} catch {
|
|
throw new Chat.ErrorMessage(`You must install <a href="https://crates.io/crates/psbattletools">psbattletools</a> to use the alternate battlesearch.`);
|
|
}
|
|
if (user.lastCommand !== '/battlesearch' && [30, 31, 1].includes(currentDayOfMonth)) {
|
|
const buf = [`Warning: Usage stats may be running currently.`];
|
|
buf.push(`Battlesearch can interfere with usage stats processing due to high computational load.`);
|
|
buf.push(`Please exercise caution.`);
|
|
buf.push(`Type the command again to confirm.`);
|
|
user.lastCommand = '/battlesearch';
|
|
throw new Chat.ErrorMessage(buf);
|
|
}
|
|
user.lastCommand = '';
|
|
|
|
const directories = [];
|
|
for (let daysAgo = 0; daysAgo < days; daysAgo++) {
|
|
const date = new Date(Date.now() - 24 * 60 * 60 * 1000 * daysAgo);
|
|
const year = date.getFullYear();
|
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
const day = date.getDate().toString().padStart(2, '0');
|
|
|
|
directories.push(
|
|
Monitor.logPath(path.join(`${year}-${month}`, format, `${year}-${month}-${day}`)).path
|
|
);
|
|
}
|
|
|
|
// TODO: implement flag?
|
|
let buf = `>view-battlesearch-${toID(targetUser)}\n|init|html\n|title|[Battlesearch] ${targetUser} in ${format}\n`;
|
|
buf += `|pagehtml|<div class="pad"><h2>Battlesearch for ${targetUser} in ${format} in the last ${days} days</h2>`;
|
|
buf += `<div style="white-space:pre-wrap;display:block;"></div>`;
|
|
buf += `<p>Searching...</p>`;
|
|
buf += `</div>`;
|
|
connection.send(buf);
|
|
|
|
const search = child_process.spawn(
|
|
'psbattletools',
|
|
['--threads', '3', 'search', targetUser, ...directories],
|
|
{ env: { PATH: `${process.env.PATH}:${process.env.HOME}/.cargo/bin` } }
|
|
);
|
|
search.stdout.on('data', data => {
|
|
buf = buf.replace('</div>', `${Chat.formatText(data.toString()).replace(/\n/g, '<br />')}</div>`);
|
|
connection.send(buf);
|
|
});
|
|
search.on('close', () => {
|
|
buf = buf.replace('Searching...', 'Done!');
|
|
connection.send(buf);
|
|
});
|
|
}
|
|
|
|
async function fsBattleSearch(
|
|
connection: Connection, userids: string[], month: string,
|
|
tierid: ID, turnLimit?: number
|
|
) {
|
|
userids = userids.map(toID);
|
|
const user = connection.user;
|
|
if (!user.can('forcewin')) return connection.popup(`/battlesearch - Access Denied`);
|
|
|
|
const response = await PM.query({ userids, turnLimit, month, tierid });
|
|
connection.send(buildResults(response, userids as ID[], month, tierid, turnLimit));
|
|
}
|
|
|
|
export const pages: Chat.PageTable = {
|
|
async battlesearch(args, user, connection) {
|
|
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
|
|
this.checkCan('forcewin');
|
|
if (Config.nobattlesearch === true) {
|
|
throw new Chat.ErrorMessage(`Battlesearch has been temporarily disabled due to load issues.`);
|
|
}
|
|
if (Config.nobattlesearch === 'psbattletools') {
|
|
return rustBattleSearch(this, args[0], args[1], toID(args[2]));
|
|
}
|
|
const [ids, rawLimit, month, formatid, confirmation] = Utils.splitFirst(this.pageid.slice(18), '--', 5);
|
|
let turnLimit: number | undefined = parseInt(rawLimit);
|
|
if (isNaN(turnLimit)) turnLimit = undefined;
|
|
const userids = ids.split('-');
|
|
if (!ids || turnLimit && turnLimit < 1) {
|
|
return user.popup(`Some arguments are missing or invalid for battlesearch. Use /battlesearch to start over.`);
|
|
}
|
|
this.title = `[Battle Search][${userids.join(', ')}]`;
|
|
let buf = `<div class="pad ladder"><h2>Battle Search</h2><p>Userid${Chat.plural(userids)}: ${userids.join(', ')}</p><p>`;
|
|
if (turnLimit) {
|
|
buf += `Maximum Turns: ${turnLimit}`;
|
|
}
|
|
buf += `</p>`;
|
|
|
|
const months = Utils.sortBy(
|
|
(await Monitor.logPath('/').readdir()).filter(f => f.length === 7 && f.includes('-')),
|
|
name => ({ reverse: name })
|
|
);
|
|
if (!month) {
|
|
buf += `<p>Please select a month:</p><ul style="list-style: none; display: block; padding: 0">`;
|
|
for (const i of months) {
|
|
buf += `<li style="display: inline; list-style: none"><a href="/view-battlesearch-${userids.join('-')}--${turnLimit}--${i}" target="replace"><button class="button">${i}</button></li>`;
|
|
}
|
|
return `${buf}</ul></div>`;
|
|
} else {
|
|
if (!months.includes(month)) {
|
|
return `${buf}Invalid month selected. <a href="/view-battlesearch-${userids.join('-')}--${turnLimit}" target="replace"><button class="button">Back to month selection</button></a></div>`;
|
|
}
|
|
buf += `<p><a href="/view-battlesearch-${userids.join('-')}--${turnLimit}" target="replace"><button class="button">Back</button></a> <button class="button disabled">${month}</button></p>`;
|
|
}
|
|
|
|
const tierid = toID(formatid);
|
|
const tiers = Utils.sortBy(await Monitor.logPath(`${month}/`).readdir(), tier => [
|
|
// First sort by gen with the latest being first
|
|
tier.startsWith('gen') ? -parseInt(tier.charAt(3)) : -6,
|
|
// Then sort alphabetically
|
|
tier,
|
|
]).map(tier => {
|
|
// Use the official tier name
|
|
const format = Dex.formats.get(tier);
|
|
if (format?.exists) tier = format.name;
|
|
// Otherwise format as best as possible
|
|
if (tier.startsWith('gen')) {
|
|
return `[Gen ${tier.substring(3, 4)}] ${tier.substring(4)}`;
|
|
}
|
|
return tier;
|
|
});
|
|
if (!tierid) {
|
|
buf += `<p>Please select the tier to search:</p><ul style="list-style: none; display: block; padding: 0">`;
|
|
for (const tier of tiers) {
|
|
buf += `<li style="display: inline; list-style: none">`;
|
|
buf += `<a href="/view-battlesearch-${userids.join('-')}--${turnLimit}--${month}--${toID(tier)}" target="replace">`;
|
|
buf += `<button class="button">${tier}</button></a></li><br />`;
|
|
}
|
|
return `${buf}</ul></div>`;
|
|
} else {
|
|
if (!tiers.map(toID).includes(tierid)) {
|
|
return `${buf}Invalid tier selected. <a href="/view-battlesearch-${userids.join('-')}--${turnLimit}--${month}" target="replace"><button class="button">Back to tier selection</button></a></div>`;
|
|
}
|
|
this.title += `[${tierid}]`;
|
|
buf += `<p><a href="/view-battlesearch-${userids.join('-')}--${turnLimit}--${month}" target="replace"><button class="button">Back</button></a> <button class="button disabled">${tierid}</button></p>`;
|
|
}
|
|
|
|
const [userid] = userids;
|
|
if (toID(confirmation) !== 'confirm') {
|
|
buf += `<p>Are you sure you want to run a battle search for for ${tierid} battles on ${month} `;
|
|
buf += `where the ${userids.length > 1 ? `user(s) ${userids.join(', ')} were players` : `the user ${userid} was a player`}`;
|
|
if (turnLimit) buf += ` and the battle lasted less than ${turnLimit} turn${Chat.plural(turnLimit)}`;
|
|
buf += `?</p><p><a href="/view-battlesearch-${userids.join('-')}--${turnLimit}--${month}--${tierid}--confirm" target="replace"><button class="button notifying">Yes, run the battle search</button></a> <a href="/view-battlesearch-${userids.join('-')}--${turnLimit}--${month}--${tierid}" target="replace"><button class="button">No, go back</button></a></p>`;
|
|
return `${buf}</div>`;
|
|
}
|
|
|
|
// Run search
|
|
void fsBattleSearch(connection, userids, month, tierid, turnLimit);
|
|
return (
|
|
`<div class="pad ladder"><h2>Battle Search</h2><p>` +
|
|
`Searching for ${tierid} battles on ${month} where the ` +
|
|
`${userids.length > 1 ? `user(s) ${userids.join(', ')} were players` : `the user ${userid} was a player`} ` +
|
|
(turnLimit ? `and the battle lasted less than ${turnLimit} turn${Chat.plural(turnLimit)}.` : '') +
|
|
`</p><p>Loading... (this will take a while)</p></div>`
|
|
);
|
|
},
|
|
};
|
|
|
|
export const commands: Chat.ChatCommands = {
|
|
battlesearch(target, room, user, connection) {
|
|
if (!target.trim()) return this.parse('/help battlesearch');
|
|
this.checkCan('forcewin');
|
|
|
|
const parts = target.split(',');
|
|
let turnLimit;
|
|
const ids = [];
|
|
for (const part of parts) {
|
|
if (part.startsWith('limit=')) {
|
|
const n = parseInt(part.slice('limit='.length).trim());
|
|
if (isNaN(n)) {
|
|
throw new Chat.ErrorMessage(`Invalid limit: ${part.slice('limit='.length)}`);
|
|
}
|
|
turnLimit = n;
|
|
continue;
|
|
}
|
|
ids.push(part);
|
|
}
|
|
// Selection on month, tier, and date will be handled in the HTML room
|
|
return this.parse(`/join view-battlesearch-${ids.map(toID).join('-')}--${turnLimit || ""}`);
|
|
},
|
|
battlesearchhelp() {
|
|
if (Config.nobattlesearch === 'psbattletools') {
|
|
// use the psbattletools battlesearch command instead
|
|
return this.parse('/help rustbattlesearch');
|
|
}
|
|
|
|
this.runBroadcast();
|
|
return this.sendReply(
|
|
'/battlesearch [args] - Searches rated battle history for the provided [args] and returns information on battles between the userids given.\n' +
|
|
`If a number is provided in the [args], it is assumed to be a turn limit, else they're assumed to be userids. Requires ~`
|
|
);
|
|
},
|
|
rustbattlesearchhelp: [
|
|
`/battlesearch <user>, <format>, <days> - Searches for battles played by <user> in the past <days> days. Requires: ~`,
|
|
],
|
|
};
|
|
|
|
/*********************************************************
|
|
* Process manager
|
|
*********************************************************/
|
|
|
|
export const PM = new ProcessManager.QueryProcessManager<AnyObject, AnyObject>(
|
|
'battlesearch', module,
|
|
async data => {
|
|
const { userids, turnLimit, month, tierid } = data;
|
|
const startTime = Date.now();
|
|
try {
|
|
const result = await runBattleSearch(userids, month, tierid, turnLimit);
|
|
const elapsedTime = Date.now() - startTime;
|
|
if (elapsedTime > 10 * 60 * 1000) {
|
|
Monitor.slow(`[Slow battlesearch query] ${elapsedTime}ms: ${JSON.stringify(data)}`);
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
Monitor.crashlog(err, 'A battle search query', {
|
|
userids,
|
|
turnLimit,
|
|
month,
|
|
tierid,
|
|
});
|
|
}
|
|
return null;
|
|
},
|
|
BATTLESEARCH_PROCESS_TIMEOUT,
|
|
message => {
|
|
if (message.startsWith('SLOW\n')) {
|
|
Monitor.slow(message.slice(5));
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!PM.isParentProcess) {
|
|
ConfigLoader.ensureLoaded();
|
|
global.Monitor = {
|
|
crashlog(error: Error, source = 'A battle search process', details: AnyObject | null = null) {
|
|
const repr = JSON.stringify([error.name, error.message, source, details]);
|
|
process.send!(`THROW\n@!!@${repr}\n${error.stack}`);
|
|
},
|
|
slow(text: string) {
|
|
process.send!(`CALLBACK\nSLOW\n${text}`);
|
|
},
|
|
};
|
|
process.on('uncaughtException', err => {
|
|
if (Config.crashguard) {
|
|
Monitor.crashlog(err, 'A battle search child process');
|
|
}
|
|
});
|
|
global.Dex = require('../../sim/dex').Dex;
|
|
global.toID = Dex.toID;
|
|
// eslint-disable-next-line no-eval
|
|
PM.startRepl(cmd => eval(cmd));
|
|
}
|
|
|
|
export function start(processCount: ConfigLoader.SubProcessesConfig) {
|
|
PM.spawn(processCount['battlesearch'] ?? 1);
|
|
}
|
|
|
|
export function destroy() {
|
|
void PM.destroy();
|
|
}
|