1221 lines
41 KiB
JavaScript
1221 lines
41 KiB
JavaScript
"use strict";
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
var room_battle_exports = {};
|
|
__export(room_battle_exports, {
|
|
PM: () => PM,
|
|
RoomBattle: () => RoomBattle,
|
|
RoomBattlePlayer: () => RoomBattlePlayer,
|
|
RoomBattleStream: () => RoomBattleStream,
|
|
RoomBattleTimer: () => RoomBattleTimer
|
|
});
|
|
module.exports = __toCommonJS(room_battle_exports);
|
|
var import_lib = require("../lib");
|
|
var import_child_process = require("child_process");
|
|
var import_battle_stream = require("../sim/battle-stream");
|
|
var RoomGames = __toESM(require("./room-game"));
|
|
/**
|
|
* Room Battle
|
|
* Pokemon Showdown - http://pokemonshowdown.com/
|
|
*
|
|
* This file wraps the simulator in an implementation of the RoomGame
|
|
* interface. It also abstracts away the multi-process nature of the
|
|
* simulator.
|
|
*
|
|
* For the actual battle simulation, see sim/
|
|
*
|
|
* @license MIT
|
|
*/
|
|
const TICK_TIME = 5;
|
|
const SECONDS = 1e3;
|
|
const STARTING_TIME = 150;
|
|
const MAX_TURN_TIME = 150;
|
|
const STARTING_TIME_CHALLENGE = 300;
|
|
const STARTING_GRACE_TIME = 60;
|
|
const MAX_TURN_TIME_CHALLENGE = 300;
|
|
const DISCONNECTION_TIME = 60;
|
|
const DISCONNECTION_BANK_TIME = 300;
|
|
const TIMER_COOLDOWN = 20 * SECONDS;
|
|
const LOCKDOWN_PERIOD = 30 * 60 * 1e3;
|
|
class RoomBattlePlayer extends RoomGames.RoomGamePlayer {
|
|
constructor(user, game, num) {
|
|
super(user, game, num);
|
|
if (typeof user === "string")
|
|
user = null;
|
|
this.slot = `p${num}`;
|
|
this.channelIndex = game.gameType === "multi" && num > 2 ? num - 2 : num;
|
|
this.request = { rqid: 0, request: "", isWait: "cantUndo", choice: "" };
|
|
this.wantsTie = false;
|
|
this.wantsOpenTeamSheets = null;
|
|
this.active = true;
|
|
this.eliminated = false;
|
|
this.secondsLeft = 1;
|
|
this.turnSecondsLeft = 1;
|
|
this.dcSecondsLeft = 1;
|
|
this.connected = true;
|
|
this.invite = "";
|
|
this.hasTeam = false;
|
|
if (user) {
|
|
user.games.add(this.game.roomid);
|
|
user.updateSearch();
|
|
for (const connection of user.connections) {
|
|
if (connection.inRooms.has(game.roomid)) {
|
|
Sockets.channelMove(connection.worker, this.game.roomid, this.channelIndex, connection.socketid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
getUser() {
|
|
return this.id && Users.get(this.id) || null;
|
|
}
|
|
unlinkUser() {
|
|
const user = this.getUser();
|
|
if (user) {
|
|
for (const connection of user.connections) {
|
|
Sockets.channelMove(connection.worker, this.game.roomid, 0, connection.socketid);
|
|
}
|
|
user.games.delete(this.game.roomid);
|
|
user.updateSearch();
|
|
}
|
|
this.id = "";
|
|
this.connected = false;
|
|
this.active = false;
|
|
}
|
|
updateChannel(user) {
|
|
if (user instanceof Users.Connection) {
|
|
Sockets.channelMove(user.worker, this.game.roomid, this.channelIndex, user.socketid);
|
|
return;
|
|
}
|
|
for (const connection of user.connections) {
|
|
Sockets.channelMove(connection.worker, this.game.roomid, this.channelIndex, connection.socketid);
|
|
}
|
|
}
|
|
toString() {
|
|
return this.id;
|
|
}
|
|
send(data) {
|
|
const user = this.getUser();
|
|
if (user)
|
|
user.send(data);
|
|
}
|
|
sendRoom(data) {
|
|
const user = this.getUser();
|
|
if (user)
|
|
user.sendTo(this.game.roomid, data);
|
|
}
|
|
}
|
|
class RoomBattleTimer {
|
|
constructor(battle) {
|
|
this.battle = battle;
|
|
this.timer = null;
|
|
this.timerRequesters = /* @__PURE__ */ new Set();
|
|
this.isFirstTurn = true;
|
|
this.lastTick = 0;
|
|
this.debug = false;
|
|
this.lastDisabledTime = 0;
|
|
this.lastDisabledByUser = null;
|
|
const hasLongTurns = Dex.formats.get(battle.format, true).gameType !== "singles";
|
|
const isChallenge = battle.challengeType === "challenge";
|
|
const timerEntry = Dex.formats.getRuleTable(Dex.formats.get(battle.format, true)).timer;
|
|
const timerSettings = timerEntry?.[0];
|
|
for (const k in timerSettings) {
|
|
if (timerSettings[k] === void 0)
|
|
delete timerSettings[k];
|
|
}
|
|
this.settings = {
|
|
dcTimer: !isChallenge,
|
|
dcTimerBank: isChallenge,
|
|
starting: isChallenge ? STARTING_TIME_CHALLENGE : STARTING_TIME,
|
|
grace: STARTING_GRACE_TIME,
|
|
addPerTurn: hasLongTurns ? 25 : 10,
|
|
maxPerTurn: isChallenge ? MAX_TURN_TIME_CHALLENGE : MAX_TURN_TIME,
|
|
maxFirstTurn: isChallenge ? MAX_TURN_TIME_CHALLENGE : MAX_TURN_TIME,
|
|
timeoutAutoChoose: false,
|
|
accelerate: !timerSettings && !isChallenge,
|
|
...timerSettings
|
|
};
|
|
if (this.settings.maxPerTurn <= 0)
|
|
this.settings.maxPerTurn = Infinity;
|
|
for (const player of this.battle.players) {
|
|
player.secondsLeft = this.settings.starting + this.settings.grace;
|
|
player.turnSecondsLeft = -1;
|
|
player.dcSecondsLeft = this.settings.dcTimerBank ? DISCONNECTION_BANK_TIME : DISCONNECTION_TIME;
|
|
}
|
|
}
|
|
start(requester) {
|
|
const userid = requester ? requester.id : "staff";
|
|
if (this.timerRequesters.has(userid))
|
|
return false;
|
|
if (this.battle.ended) {
|
|
requester?.sendTo(this.battle.roomid, `|inactiveoff|The timer can't be enabled after a battle has ended.`);
|
|
return false;
|
|
}
|
|
if (this.timer) {
|
|
this.battle.room.add(`|inactive|${requester ? requester.name : userid} also wants the timer to be on.`).update();
|
|
this.timerRequesters.add(userid);
|
|
return false;
|
|
}
|
|
if (requester && this.battle.playerTable[requester.id] && this.lastDisabledByUser === requester.id) {
|
|
const remainingCooldownMs = (this.lastDisabledTime || 0) + TIMER_COOLDOWN - Date.now();
|
|
if (remainingCooldownMs > 0) {
|
|
this.battle.playerTable[requester.id].sendRoom(
|
|
`|inactiveoff|The timer can't be re-enabled so soon after disabling it (${Math.ceil(remainingCooldownMs / SECONDS)} seconds remaining).`
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
this.timerRequesters.add(userid);
|
|
const requestedBy = requester ? ` (requested by ${requester.name})` : ``;
|
|
this.battle.room.add(`|inactive|Battle timer is ON: inactive players will automatically lose when time's up.${requestedBy}`).update();
|
|
this.nextRequest();
|
|
return true;
|
|
}
|
|
stop(requester) {
|
|
if (requester) {
|
|
if (!this.timerRequesters.has(requester.id))
|
|
return false;
|
|
this.timerRequesters.delete(requester.id);
|
|
this.lastDisabledByUser = requester.id;
|
|
this.lastDisabledTime = Date.now();
|
|
} else {
|
|
this.timerRequesters.clear();
|
|
}
|
|
if (this.timerRequesters.size) {
|
|
this.battle.room.add(`|inactive|${requester.name} no longer wants the timer on, but the timer is staying on because ${[...this.timerRequesters].join(", ")} still does.`).update();
|
|
return false;
|
|
}
|
|
if (this.end()) {
|
|
this.battle.room.add(`|inactiveoff|Battle timer is now OFF.`).update();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
end() {
|
|
this.timerRequesters.clear();
|
|
if (!this.timer)
|
|
return false;
|
|
clearTimeout(this.timer);
|
|
this.timer = null;
|
|
return true;
|
|
}
|
|
nextRequest() {
|
|
if (this.timer) {
|
|
clearTimeout(this.timer);
|
|
this.timer = null;
|
|
}
|
|
if (!this.timerRequesters.size)
|
|
return;
|
|
const players = this.battle.players;
|
|
if (players.some((player) => player.secondsLeft <= 0))
|
|
return;
|
|
let isFull = true;
|
|
let isEmpty = true;
|
|
for (const player of players) {
|
|
if (player.request.isWait)
|
|
isFull = false;
|
|
if (player.request.isWait !== "cantUndo")
|
|
isEmpty = false;
|
|
}
|
|
if (isEmpty) {
|
|
return;
|
|
}
|
|
const isFirst = this.isFirstTurn;
|
|
this.isFirstTurn = false;
|
|
const maxTurnTime = (isFirst ? this.settings.maxFirstTurn : 0) || this.settings.maxPerTurn;
|
|
let addPerTurn = isFirst ? 0 : this.settings.addPerTurn;
|
|
if (this.settings.accelerate && addPerTurn) {
|
|
if (this.battle.requestCount > 200 && addPerTurn > TICK_TIME) {
|
|
addPerTurn -= TICK_TIME;
|
|
}
|
|
if (this.battle.requestCount > 400 && Math.floor(this.battle.requestCount / 2) % 2) {
|
|
addPerTurn = 0;
|
|
}
|
|
}
|
|
if (!isFull && addPerTurn > TICK_TIME) {
|
|
addPerTurn = TICK_TIME;
|
|
}
|
|
const room = this.battle.room;
|
|
for (const player of players) {
|
|
if (!isFirst) {
|
|
player.secondsLeft = Math.min(player.secondsLeft + addPerTurn, this.settings.starting);
|
|
}
|
|
player.turnSecondsLeft = Math.min(player.secondsLeft, maxTurnTime);
|
|
const secondsLeft = player.turnSecondsLeft;
|
|
let grace = player.secondsLeft - this.settings.starting;
|
|
if (grace < 0)
|
|
grace = 0;
|
|
if (player) {
|
|
player.sendRoom(`|inactive|Time left: ${secondsLeft} sec this turn | ${player.secondsLeft - grace} sec total` + (grace ? ` | ${grace} sec grace` : ``));
|
|
}
|
|
if (secondsLeft <= 30 && secondsLeft < this.settings.starting) {
|
|
room.add(`|inactive|${player.name} has ${secondsLeft} seconds left this turn.`);
|
|
}
|
|
if (this.debug) {
|
|
room.add(`||${player.name} | Time left: ${secondsLeft} sec this turn | ${player.secondsLeft} sec total | +${addPerTurn} seconds`);
|
|
}
|
|
}
|
|
room.update();
|
|
this.lastTick = Date.now();
|
|
this.timer = setTimeout(() => this.nextTick(), TICK_TIME * SECONDS);
|
|
}
|
|
nextTick() {
|
|
if (this.timer)
|
|
clearTimeout(this.timer);
|
|
if (this.battle.ended)
|
|
return;
|
|
const room = this.battle.room;
|
|
for (const player of this.battle.players) {
|
|
if (player.request.isWait)
|
|
continue;
|
|
if (player.connected) {
|
|
player.secondsLeft -= TICK_TIME;
|
|
player.turnSecondsLeft -= TICK_TIME;
|
|
} else {
|
|
player.dcSecondsLeft -= TICK_TIME;
|
|
if (!this.settings.dcTimerBank) {
|
|
player.secondsLeft -= TICK_TIME;
|
|
player.turnSecondsLeft -= TICK_TIME;
|
|
}
|
|
}
|
|
const dcSecondsLeft = player.dcSecondsLeft;
|
|
if (dcSecondsLeft <= 0)
|
|
player.turnSecondsLeft = 0;
|
|
const secondsLeft = player.turnSecondsLeft;
|
|
if (!secondsLeft)
|
|
continue;
|
|
if (!player.connected && (dcSecondsLeft <= secondsLeft || this.settings.dcTimerBank)) {
|
|
if (dcSecondsLeft % 30 === 0 || dcSecondsLeft <= 20) {
|
|
room.add(`|inactive|${player.name} has ${dcSecondsLeft} seconds to reconnect!`);
|
|
}
|
|
} else {
|
|
if (secondsLeft % 30 === 0 || secondsLeft <= 20) {
|
|
room.add(`|inactive|${player.name} has ${secondsLeft} seconds left.`);
|
|
}
|
|
}
|
|
if (this.debug) {
|
|
room.add(`||[${player.name} has ${player.turnSecondsLeft}s this turn / ${player.secondsLeft}s total]`);
|
|
}
|
|
}
|
|
room.update();
|
|
if (!this.checkTimeout()) {
|
|
this.timer = setTimeout(() => this.nextTick(), TICK_TIME * 1e3);
|
|
}
|
|
}
|
|
checkActivity() {
|
|
if (this.battle.ended)
|
|
return;
|
|
for (const player of this.battle.players) {
|
|
const isConnected = !!player?.active;
|
|
if (isConnected === player.connected)
|
|
continue;
|
|
if (!isConnected) {
|
|
player.connected = false;
|
|
if (!this.settings.dcTimerBank) {
|
|
if (this.settings.dcTimer) {
|
|
player.dcSecondsLeft = DISCONNECTION_TIME;
|
|
} else {
|
|
player.dcSecondsLeft = DISCONNECTION_TIME * 10;
|
|
}
|
|
}
|
|
if (this.timerRequesters.size) {
|
|
let msg = `!`;
|
|
if (this.settings.dcTimer) {
|
|
msg = ` and has a minute to reconnect!`;
|
|
}
|
|
if (this.settings.dcTimerBank) {
|
|
if (player.dcSecondsLeft > 0) {
|
|
msg = ` and has ${player.dcSecondsLeft} seconds to reconnect!`;
|
|
} else {
|
|
msg = ` and has no disconnection time left!`;
|
|
}
|
|
}
|
|
this.battle.room.add(`|inactive|${player.name} disconnected${msg}`).update();
|
|
}
|
|
} else {
|
|
player.connected = true;
|
|
if (this.timerRequesters.size) {
|
|
let timeLeft = ``;
|
|
if (!player.request.isWait) {
|
|
timeLeft = ` and has ${player.turnSecondsLeft} seconds left`;
|
|
}
|
|
this.battle.room.add(`|inactive|${player.name} reconnected${timeLeft}.`).update();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
checkTimeout() {
|
|
const players = this.battle.players;
|
|
if (players.every((player) => player.turnSecondsLeft <= 0)) {
|
|
if (!this.settings.timeoutAutoChoose || players.every((player) => player.secondsLeft <= 0)) {
|
|
this.battle.room.add(`|-message|All players are inactive.`).update();
|
|
this.battle.tie();
|
|
return true;
|
|
}
|
|
}
|
|
let didSomething = false;
|
|
for (const player of players) {
|
|
if (player.turnSecondsLeft > 0)
|
|
continue;
|
|
if (this.settings.timeoutAutoChoose && player.secondsLeft > 0 && player.connected) {
|
|
void this.battle.stream.write(`>${player.slot} default`);
|
|
didSomething = true;
|
|
} else {
|
|
this.battle.forfeitPlayer(player, " lost due to inactivity.");
|
|
return true;
|
|
}
|
|
}
|
|
return didSomething;
|
|
}
|
|
}
|
|
class RoomBattle extends RoomGames.RoomGame {
|
|
constructor(room, options) {
|
|
super(room);
|
|
this.forcedSettings = {};
|
|
const format = Dex.formats.get(options.format, true);
|
|
this.gameid = "battle";
|
|
this.room = room;
|
|
this.title = format.name;
|
|
this.options = options;
|
|
if (!this.title.endsWith(" Battle"))
|
|
this.title += " Battle";
|
|
this.allowRenames = options.allowRenames !== void 0 ? !!options.allowRenames : !options.rated && !options.tour;
|
|
this.format = options.format;
|
|
this.gameType = format.gameType;
|
|
this.challengeType = options.challengeType || "challenge";
|
|
this.rated = options.rated === true ? 1 : options.rated || 0;
|
|
this.ladder = typeof format.rated === "string" ? toID(format.rated) : options.format;
|
|
this.missingBattleStartMessage = !!options.inputLog || options.delayedStart || false;
|
|
this.started = false;
|
|
this.ended = false;
|
|
this.active = false;
|
|
this.replaySaved = false;
|
|
this.playerCap = this.gameType === "multi" || this.gameType === "freeforall" ? 4 : 2;
|
|
this.p1 = null;
|
|
this.p2 = null;
|
|
this.p3 = null;
|
|
this.p4 = null;
|
|
this.inviteOnlySetter = null;
|
|
this.needsRejoin = options.restored ? new Set(options.players) : null;
|
|
this.allowExtraction = {};
|
|
this.logData = null;
|
|
this.endType = "normal";
|
|
this.score = null;
|
|
this.inputLog = null;
|
|
this.turn = 0;
|
|
this.rqid = 1;
|
|
this.requestCount = 0;
|
|
this.stream = PM.createStream();
|
|
let ratedMessage = "";
|
|
if (options.ratedMessage) {
|
|
ratedMessage = options.ratedMessage;
|
|
}
|
|
if (this.rated) {
|
|
ratedMessage = "Rated battle";
|
|
} else if (this.room.tour) {
|
|
ratedMessage = "Tournament battle";
|
|
}
|
|
this.room.battle = this;
|
|
const battleOptions = {
|
|
formatid: this.format,
|
|
roomid: this.roomid,
|
|
rated: ratedMessage,
|
|
seed: options.seed
|
|
};
|
|
if (options.inputLog) {
|
|
void this.stream.write(options.inputLog);
|
|
} else {
|
|
void this.stream.write(`>start ` + JSON.stringify(battleOptions));
|
|
}
|
|
void this.listen();
|
|
this.addPlayer(options.p1?.user || null, options.p1);
|
|
this.addPlayer(options.p2?.user || null, options.p2);
|
|
if (this.playerCap > 2) {
|
|
this.addPlayer(options.p3?.user || null, options.p3);
|
|
this.addPlayer(options.p4?.user || null, options.p4);
|
|
}
|
|
this.timer = new RoomBattleTimer(this);
|
|
if (Config.forcetimer || this.format.includes("blitz"))
|
|
this.timer.start();
|
|
this.start();
|
|
}
|
|
checkActive() {
|
|
let active = true;
|
|
if (this.ended || !this.started) {
|
|
active = false;
|
|
} else if (!this.p1?.active) {
|
|
active = false;
|
|
} else if (!this.p2?.active) {
|
|
active = false;
|
|
} else if (this.playerCap > 2) {
|
|
if (!this.p3?.active) {
|
|
active = false;
|
|
} else if (!this.p4?.active) {
|
|
active = false;
|
|
}
|
|
}
|
|
Rooms.global.battleCount += (active ? 1 : 0) - (this.active ? 1 : 0);
|
|
this.room.active = active;
|
|
this.active = active;
|
|
if (Rooms.global.battleCount === 0)
|
|
Rooms.global.automaticKillRequest();
|
|
}
|
|
choose(user, data) {
|
|
if (this.frozen) {
|
|
user.popup(`Your battle is currently paused, so you cannot move right now.`);
|
|
return;
|
|
}
|
|
const player = this.playerTable[user.id];
|
|
const [choice, rqid] = data.split("|", 2);
|
|
if (!player)
|
|
return;
|
|
const request = player.request;
|
|
if (request.isWait !== false && request.isWait !== true) {
|
|
player.sendRoom(`|error|[Invalid choice] There's nothing to choose`);
|
|
return;
|
|
}
|
|
const allPlayersWait = this.players.every((p) => !!p.request.isWait);
|
|
if (allPlayersWait || rqid && rqid !== "" + request.rqid) {
|
|
player.sendRoom(`|error|[Invalid choice] Sorry, too late to make a different move; the next turn has already started`);
|
|
return;
|
|
}
|
|
request.isWait = true;
|
|
request.choice = choice;
|
|
void this.stream.write(`>${player.slot} ${choice}`);
|
|
}
|
|
undo(user, data) {
|
|
const player = this.playerTable[user.id];
|
|
const [, rqid] = data.split("|", 2);
|
|
if (!player)
|
|
return;
|
|
const request = player.request;
|
|
if (request.isWait !== true) {
|
|
player.sendRoom(`|error|[Invalid choice] There's nothing to cancel`);
|
|
return;
|
|
}
|
|
const allPlayersWait = this.players.every((p) => !!p.request.isWait);
|
|
if (allPlayersWait || rqid && rqid !== "" + request.rqid) {
|
|
player.sendRoom(`|error|[Invalid choice] Sorry, too late to cancel; the next turn has already started`);
|
|
return;
|
|
}
|
|
request.isWait = false;
|
|
void this.stream.write(`>${player.slot} undo`);
|
|
}
|
|
joinGame(user, slot, playerOpts) {
|
|
if (this.needsRejoin?.size && !this.needsRejoin.has(user.id)) {
|
|
user.popup(`All the original players in this battle must join first.`);
|
|
return false;
|
|
}
|
|
if (user.id in this.playerTable) {
|
|
user.popup(`You have already joined this battle.`);
|
|
return false;
|
|
}
|
|
const validSlots = [];
|
|
for (const player of this.players) {
|
|
if (!player.id)
|
|
validSlots.push(player.slot);
|
|
}
|
|
if (slot && !validSlots.includes(slot)) {
|
|
user.popup(`This battle already has a user in slot ${slot}.`);
|
|
return false;
|
|
}
|
|
if (!validSlots.length) {
|
|
user.popup(`This battle already has two players.`);
|
|
return false;
|
|
}
|
|
if (!slot && validSlots.length > 1) {
|
|
user.popup(`Which slot would you like to join into? Use something like \`/joingame ${validSlots[0]}\``);
|
|
return false;
|
|
}
|
|
if (!slot)
|
|
slot = validSlots[0];
|
|
if (this[slot].invite === user.id) {
|
|
this.room.auth.set(user.id, Users.PLAYER_SYMBOL);
|
|
} else if (!user.can("joinbattle", null, this.room)) {
|
|
user.popup(`You must be set as a player to join a battle you didn't start. Ask a player to use /addplayer on you to join this battle.`);
|
|
return false;
|
|
}
|
|
this.updatePlayer(this[slot], user, playerOpts);
|
|
this.needsRejoin?.delete(user.id);
|
|
if (validSlots.length - 1 < 1 && this.missingBattleStartMessage) {
|
|
const users = this.players.map((player) => {
|
|
const u = player.getUser();
|
|
if (!u)
|
|
throw new Error(`User ${player.name} not found on ${this.roomid} battle creation`);
|
|
return u;
|
|
});
|
|
Rooms.global.onCreateBattleRoom(users, this.room, { rated: this.rated });
|
|
this.missingBattleStartMessage = false;
|
|
this.started = true;
|
|
this.room.add(`|uhtmlchange|invites|`);
|
|
} else if (!this.started && this.invitesFull()) {
|
|
this.sendInviteForm(true);
|
|
}
|
|
if (user.inRooms.has(this.roomid))
|
|
this.onConnect(user);
|
|
this.room.update();
|
|
return true;
|
|
}
|
|
leaveGame(user) {
|
|
if (!user)
|
|
return false;
|
|
if (this.room.rated || this.room.tour) {
|
|
user.popup(`Players can't be swapped out in a ${this.room.tour ? "tournament" : "rated"} battle.`);
|
|
return false;
|
|
}
|
|
const player = this.playerTable[user.id];
|
|
if (!player) {
|
|
user.popup(`Failed to leave battle - you're not a player.`);
|
|
return false;
|
|
}
|
|
Chat.runHandlers("onBattleLeave", user, this.room);
|
|
this.updatePlayer(player, null);
|
|
this.room.auth.set(user.id, "+");
|
|
this.room.update();
|
|
return true;
|
|
}
|
|
async listen() {
|
|
let disconnected = false;
|
|
try {
|
|
for await (const next of this.stream) {
|
|
if (!this.room)
|
|
return;
|
|
this.receive(next.split("\n"));
|
|
}
|
|
} catch (err) {
|
|
if (err.message.includes("Process disconnected")) {
|
|
disconnected = true;
|
|
} else {
|
|
Monitor.crashlog(err, "A sim stream");
|
|
}
|
|
}
|
|
if (!this.ended) {
|
|
this.room.add(`|bigerror|The simulator process crashed. We've been notified and will fix this ASAP.`);
|
|
if (!disconnected)
|
|
Monitor.crashlog(new Error(`Sim stream interrupted`), `A sim stream`);
|
|
this.started = true;
|
|
this.ended = true;
|
|
this.checkActive();
|
|
}
|
|
}
|
|
receive(lines) {
|
|
for (const player of this.players)
|
|
player.wantsTie = false;
|
|
switch (lines[0]) {
|
|
case "requesteddata":
|
|
lines = lines.slice(1);
|
|
const [resolver] = this.dataResolvers.shift();
|
|
resolver(lines);
|
|
break;
|
|
case "update":
|
|
for (const line of lines.slice(1)) {
|
|
if (line.startsWith("|turn|")) {
|
|
this.turn = parseInt(line.slice(6));
|
|
}
|
|
this.room.add(line);
|
|
if (line.startsWith(`|bigerror|You will auto-tie if `) && Config.allowrequestingties && !this.room.tour) {
|
|
this.room.add(`|-hint|If you want to tie earlier, consider using \`/offertie\`.`);
|
|
}
|
|
}
|
|
this.room.update();
|
|
if (!this.ended)
|
|
this.timer.nextRequest();
|
|
this.checkActive();
|
|
break;
|
|
case "sideupdate": {
|
|
const slot = lines[1];
|
|
const player = this[slot];
|
|
if (lines[2].startsWith(`|error|[Invalid choice] Can't do anything`)) {
|
|
} else if (lines[2].startsWith(`|error|[Invalid choice]`)) {
|
|
const undoFailed = lines[2].includes(`Can't undo`);
|
|
const request = this[slot].request;
|
|
request.isWait = undoFailed ? "cantUndo" : false;
|
|
request.choice = "";
|
|
} else if (lines[2].startsWith(`|request|`)) {
|
|
this.rqid++;
|
|
const request = JSON.parse(lines[2].slice(9));
|
|
request.rqid = this.rqid;
|
|
const requestJSON = JSON.stringify(request);
|
|
this[slot].request = {
|
|
rqid: this.rqid,
|
|
request: requestJSON,
|
|
isWait: request.wait ? "cantUndo" : false,
|
|
choice: ""
|
|
};
|
|
this.requestCount++;
|
|
if (player)
|
|
player.sendRoom(`|request|${requestJSON}`);
|
|
break;
|
|
}
|
|
if (player)
|
|
player.sendRoom(lines[2]);
|
|
break;
|
|
}
|
|
case "error": {
|
|
if (process.uptime() * 1e3 < LOCKDOWN_PERIOD) {
|
|
const error = new Error();
|
|
error.stack = lines.slice(1).join("\n");
|
|
Rooms.global.startLockdown(error);
|
|
}
|
|
break;
|
|
}
|
|
case "end":
|
|
this.logData = JSON.parse(lines[1]);
|
|
this.score = this.logData.score;
|
|
this.inputLog = this.logData.inputLog;
|
|
this.started = true;
|
|
if (!this.ended) {
|
|
this.ended = true;
|
|
void this.onEnd(this.logData.winner);
|
|
this.clearPlayers();
|
|
}
|
|
this.checkActive();
|
|
break;
|
|
}
|
|
}
|
|
async onEnd(winner) {
|
|
this.timer.end();
|
|
let p1score = 0.5;
|
|
const winnerid = toID(winner);
|
|
const p1name = this.p1.name;
|
|
const p2name = this.p2.name;
|
|
const p1id = toID(p1name);
|
|
const p2id = toID(p2name);
|
|
Chat.runHandlers("onBattleEnd", this, winnerid, [p1id, p2id, this.p3?.id, this.p4?.id].filter(Boolean));
|
|
if (this.room.rated) {
|
|
this.room.rated = 0;
|
|
if (winnerid === p1id) {
|
|
p1score = 1;
|
|
} else if (winnerid === p2id) {
|
|
p1score = 0;
|
|
}
|
|
winner = Users.get(winnerid);
|
|
if (winner && !winner.registered) {
|
|
this.room.sendUser(winner, "|askreg|" + winner.id);
|
|
}
|
|
const [score, p1rating, p2rating] = await Ladders(this.ladder).updateRating(p1name, p2name, p1score, this.room);
|
|
void this.logBattle(score, p1rating, p2rating);
|
|
Chat.runHandlers("onBattleRanked", this, winnerid, [p1rating, p2rating], [p1id, p2id]);
|
|
} else if (Config.logchallenges) {
|
|
if (winnerid === p1id) {
|
|
p1score = 1;
|
|
} else if (winnerid === p2id) {
|
|
p1score = 0;
|
|
}
|
|
void this.logBattle(p1score);
|
|
} else {
|
|
this.logData = null;
|
|
}
|
|
if (this.replaySaved || Config.autosavereplays) {
|
|
const uploader = Users.get(winnerid || p1id);
|
|
if (uploader?.connections[0]) {
|
|
Chat.parse("/savereplay silent", this.room, uploader, uploader.connections[0]);
|
|
}
|
|
}
|
|
const parentGame = this.room.parent && this.room.parent.game;
|
|
if (parentGame?.onBattleWin) {
|
|
parentGame.onBattleWin(this.room, winnerid);
|
|
}
|
|
if (this.room.hideReplay) {
|
|
this.room.settings.modjoin = "%";
|
|
this.room.setPrivate("hidden");
|
|
}
|
|
this.room.update();
|
|
}
|
|
async logBattle(p1score, p1rating = null, p2rating = null, p3rating = null, p4rating = null) {
|
|
if (Dex.formats.get(this.format, true).noLog)
|
|
return;
|
|
const logData = this.logData;
|
|
if (!logData)
|
|
return;
|
|
this.logData = null;
|
|
logData.log = this.room.getLog(-1).split("\n");
|
|
for (const rating of [p1rating, p2rating, p3rating, p4rating]) {
|
|
if (rating) {
|
|
delete rating.formatid;
|
|
delete rating.username;
|
|
delete rating.rpsigma;
|
|
delete rating.sigma;
|
|
}
|
|
}
|
|
logData.p1rating = p1rating;
|
|
logData.p2rating = p2rating;
|
|
if (this.playerCap > 2) {
|
|
logData.p3rating = p3rating;
|
|
logData.p4rating = p4rating;
|
|
}
|
|
logData.endType = this.endType;
|
|
if (!p1rating)
|
|
logData.ladderError = true;
|
|
const date = new Date();
|
|
logData.timestamp = "" + date;
|
|
logData.roomid = this.room.roomid;
|
|
logData.format = this.room.format;
|
|
const logsubfolder = Chat.toTimestamp(date).split(" ")[0];
|
|
const logfolder = logsubfolder.split("-", 2).join("-");
|
|
const tier = Dex.formats.get(this.room.format).id;
|
|
const logpath = `logs/${logfolder}/${tier}/${logsubfolder}/`;
|
|
await (0, import_lib.FS)(logpath).mkdirp();
|
|
await (0, import_lib.FS)(`${logpath}${this.room.getReplayData().id}.log.json`).write(JSON.stringify(logData));
|
|
}
|
|
onConnect(user, connection = null) {
|
|
const player = this.playerTable[user.id];
|
|
if (!player)
|
|
return;
|
|
player.updateChannel(connection || user);
|
|
const request = player.request;
|
|
if (request) {
|
|
let data = `|request|${request.request}`;
|
|
if (request.choice)
|
|
data += `
|
|
|sentchoice|${request.choice}`;
|
|
(connection || user).sendTo(this.roomid, data);
|
|
}
|
|
if (!this.started) {
|
|
this.sendInviteForm(connection || user);
|
|
}
|
|
if (!player.active)
|
|
this.onJoin(user);
|
|
}
|
|
onUpdateConnection(user, connection = null) {
|
|
this.onConnect(user, connection);
|
|
}
|
|
onRename(user, oldUserid, isJoining, isForceRenamed) {
|
|
if (user.id === oldUserid)
|
|
return;
|
|
if (!this.playerTable) {
|
|
user.games.delete(this.roomid);
|
|
return;
|
|
}
|
|
if (!(oldUserid in this.playerTable)) {
|
|
if (user.id in this.playerTable) {
|
|
this.onConnect(user);
|
|
}
|
|
return;
|
|
}
|
|
if (!this.allowRenames) {
|
|
const player2 = this.playerTable[oldUserid];
|
|
if (player2) {
|
|
const message = isForceRenamed ? " lost by having an inappropriate name." : " forfeited by changing their name.";
|
|
this.forfeitPlayer(player2, message);
|
|
}
|
|
if (!(user.id in this.playerTable)) {
|
|
user.games.delete(this.roomid);
|
|
}
|
|
return;
|
|
}
|
|
if (!user.named) {
|
|
this.onLeave(user, oldUserid);
|
|
return;
|
|
}
|
|
if (user.id in this.playerTable)
|
|
return;
|
|
const player = this.playerTable[oldUserid];
|
|
if (player) {
|
|
this.updatePlayer(player, user);
|
|
}
|
|
const options = {
|
|
name: user.name,
|
|
avatar: user.avatar
|
|
};
|
|
void this.stream.write(`>player ${player.slot} ` + JSON.stringify(options));
|
|
}
|
|
onJoin(user) {
|
|
const player = this.playerTable[user.id];
|
|
if (player && !player.active) {
|
|
player.active = true;
|
|
this.timer.checkActivity();
|
|
this.room.add(`|player|${player.slot}|${user.name}|${user.avatar}`);
|
|
}
|
|
}
|
|
onLeave(user, oldUserid) {
|
|
const player = this.playerTable[oldUserid || user.id];
|
|
if (player?.active) {
|
|
player.sendRoom(`|request|null`);
|
|
player.active = false;
|
|
this.timer.checkActivity();
|
|
this.room.add(`|player|${player.slot}|`);
|
|
}
|
|
}
|
|
win(user) {
|
|
if (!user) {
|
|
this.tie();
|
|
return true;
|
|
}
|
|
const player = this.playerTable[user.id];
|
|
if (!player)
|
|
return false;
|
|
void this.stream.write(`>forcewin ${player.slot}`);
|
|
}
|
|
tie() {
|
|
void this.stream.write(`>forcetie`);
|
|
}
|
|
tiebreak() {
|
|
void this.stream.write(`>tiebreak`);
|
|
}
|
|
forfeit(user, message = "") {
|
|
if (typeof user !== "string")
|
|
user = user.id;
|
|
else
|
|
user = toID(user);
|
|
if (!(user in this.playerTable))
|
|
return false;
|
|
return this.forfeitPlayer(this.playerTable[user], message);
|
|
}
|
|
forfeitPlayer(player, message = "") {
|
|
if (this.ended || !this.started)
|
|
return false;
|
|
if (!message)
|
|
message = " forfeited.";
|
|
this.room.add(`|-message|${player.name}${message}`);
|
|
this.endType = "forfeit";
|
|
if (this.playerCap > 2) {
|
|
player.sendRoom(`|request|null`);
|
|
this.removePlayer(player);
|
|
}
|
|
void this.stream.write(`>forcelose ${player.slot}`);
|
|
return true;
|
|
}
|
|
/**
|
|
* playerOpts should be empty only if importing an inputlog
|
|
* (so the player isn't recreated)
|
|
*/
|
|
addPlayer(user, playerOpts) {
|
|
const player = super.addPlayer(user);
|
|
if (!player)
|
|
return null;
|
|
const slot = player.slot;
|
|
this[slot] = player;
|
|
if (playerOpts) {
|
|
const options = {
|
|
name: player.name,
|
|
avatar: user ? "" + user.avatar : "",
|
|
team: playerOpts.team || void 0,
|
|
rating: Math.round(playerOpts.rating || 0)
|
|
};
|
|
void this.stream.write(`>player ${slot} ${JSON.stringify(options)}`);
|
|
player.hasTeam = true;
|
|
}
|
|
if (user) {
|
|
this.room.auth.set(player.id, Users.PLAYER_SYMBOL);
|
|
}
|
|
if (user?.inRooms.has(this.roomid))
|
|
this.onConnect(user);
|
|
return player;
|
|
}
|
|
checkPrivacySettings(options) {
|
|
let inviteOnly = false;
|
|
const privacySetter = /* @__PURE__ */ new Set([]);
|
|
for (const p of ["p1", "p2", "p3", "p4"]) {
|
|
const playerOptions = options[p];
|
|
if (playerOptions) {
|
|
if (playerOptions.inviteOnly) {
|
|
inviteOnly = true;
|
|
privacySetter.add(playerOptions.user.id);
|
|
} else if (playerOptions.hidden) {
|
|
privacySetter.add(playerOptions.user.id);
|
|
}
|
|
if (playerOptions.user)
|
|
this.checkForcedUserSettings(playerOptions.user);
|
|
}
|
|
}
|
|
if (privacySetter.size) {
|
|
const room = this.room;
|
|
if (this.forcedSettings.privacy) {
|
|
room.setPrivate(false);
|
|
room.settings.modjoin = null;
|
|
room.add(`|raw|<div class="broadcast-blue"><strong>This battle is required to be public due to a player having a name starting with '${this.forcedSettings.privacy}'.</div>`);
|
|
} else if (!options.tour || room.tour?.allowModjoin) {
|
|
room.setPrivate("hidden");
|
|
if (inviteOnly)
|
|
room.settings.modjoin = "%";
|
|
room.privacySetter = privacySetter;
|
|
if (inviteOnly) {
|
|
room.settings.modjoin = "%";
|
|
room.add(`|raw|<div class="broadcast-red"><strong>This battle is invite-only!</strong><br />Users must be invited with <code>/invite</code> (or be staff) to join</div>`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
checkForcedUserSettings(user) {
|
|
this.forcedSettings = {
|
|
modchat: this.forcedSettings.modchat || RoomBattle.battleForcedSetting(user, "modchat"),
|
|
privacy: this.forcedSettings.privacy || RoomBattle.battleForcedSetting(user, "privacy")
|
|
};
|
|
if (this.players.some((p) => p.getUser()?.battleSettings.special) || this.rated && this.forcedSettings.modchat) {
|
|
this.room.settings.modchat = "\u2606";
|
|
}
|
|
}
|
|
static battleForcedSetting(user, key) {
|
|
if (Config.forcedpublicprefixes) {
|
|
for (const prefix of Config.forcedpublicprefixes) {
|
|
Chat.plugins["username-prefixes"]?.prefixManager.addPrefix(prefix, "privacy");
|
|
}
|
|
delete Config.forcedpublicprefixes;
|
|
}
|
|
if (!Config.forcedprefixes)
|
|
return null;
|
|
for (const { type, prefix } of Config.forcedprefixes) {
|
|
if (user.id.startsWith(toID(prefix)) && type === key)
|
|
return prefix;
|
|
}
|
|
return null;
|
|
}
|
|
makePlayer(user) {
|
|
const num = this.players.length + 1;
|
|
return new RoomBattlePlayer(user, this, num);
|
|
}
|
|
updatePlayer(player, user, playerOpts) {
|
|
super.updatePlayer(player, user);
|
|
player.invite = "";
|
|
const slot = player.slot;
|
|
if (user) {
|
|
const options = {
|
|
name: player.name,
|
|
avatar: user.avatar,
|
|
team: playerOpts?.team
|
|
};
|
|
void this.stream.write(`>player ${slot} ` + JSON.stringify(options));
|
|
if (playerOpts)
|
|
player.hasTeam = true;
|
|
this.room.add(`|player|${slot}|${player.name}|${user.avatar}`);
|
|
} else {
|
|
const options = {
|
|
name: ""
|
|
};
|
|
void this.stream.write(`>player ${slot} ` + JSON.stringify(options));
|
|
this.room.add(`|player|${slot}|`);
|
|
}
|
|
}
|
|
start() {
|
|
const users = this.players.map((player) => {
|
|
const user = player.getUser();
|
|
if (!user && !this.missingBattleStartMessage) {
|
|
throw new Error(`User ${player.name} not found on ${this.roomid} battle creation`);
|
|
}
|
|
return user;
|
|
});
|
|
if (!this.missingBattleStartMessage) {
|
|
Rooms.global.onCreateBattleRoom(users, this.room, { rated: this.rated });
|
|
this.started = true;
|
|
}
|
|
if (this.gameType === "multi") {
|
|
this.room.title = `Team ${this.p1.name} vs. Team ${this.p2.name}`;
|
|
} else if (this.gameType === "freeforall") {
|
|
this.room.title = `${this.p1.name} and friends`;
|
|
} else {
|
|
this.room.title = `${this.p1.name} vs. ${this.p2.name}`;
|
|
}
|
|
this.room.send(`|title|${this.room.title}`);
|
|
const suspectTest = Chat.plugins["suspect-tests"]?.suspectTests[this.format];
|
|
if (suspectTest) {
|
|
const format = Dex.formats.get(this.format);
|
|
this.room.add(
|
|
`|html|<div class="broadcast-blue"><strong>${format.name} is currently suspecting ${suspectTest.suspect}! For information on how to participate check out the <a href="${suspectTest.url}">suspect thread</a>.</strong></div>`
|
|
).update();
|
|
}
|
|
if (this.missingBattleStartMessage === "multi") {
|
|
this.room.add(`|uhtml|invites|<div class="broadcast broadcast-blue"><strong>This is a 4-player challenge battle</strong><br />The players will need to add more players before the battle can start.</div>`);
|
|
}
|
|
}
|
|
invitesFull() {
|
|
return this.players.every((player) => player.id || player.invite);
|
|
}
|
|
/** true = send to every player; falsy = send to no one */
|
|
sendInviteForm(connection) {
|
|
if (connection === true) {
|
|
for (const player of this.players)
|
|
this.sendInviteForm(player.getUser());
|
|
return;
|
|
}
|
|
if (!connection)
|
|
return;
|
|
const playerForms = this.players.map((player) => player.id ? `<form><label>Player ${player.num}: <strong>${player.name}</strong></label></form>` : player.invite ? `<form data-submitsend="/msgroom ${this.roomid},/uninvitebattle ${player.invite}"><label>Player ${player.num}: <strong>${player.invite}</strong> (invited) <button>Uninvite</button></label></form>` : `<form data-submitsend="/msgroom ${this.roomid},/invitebattle {username}, p${player.num}"><label>Player ${player.num}: <input name="username" class="textbox" placeholder="Username" /></label> <button class="button">Add Player</button></form>`);
|
|
if (this.gameType === "multi") {
|
|
[playerForms[1], playerForms[2]] = [playerForms[2], playerForms[1]];
|
|
playerForms.splice(2, 0, "— vs —");
|
|
}
|
|
connection.sendTo(
|
|
this.room,
|
|
`|uhtmlchange|invites|<div class="broadcast broadcast-blue"><strong>This battle needs more players to start</strong><br /><br />${playerForms.join(``)}</div>`
|
|
);
|
|
}
|
|
clearPlayers() {
|
|
for (const player of this.players) {
|
|
player.unlinkUser();
|
|
}
|
|
}
|
|
destroy() {
|
|
for (const player of this.players) {
|
|
player.destroy();
|
|
}
|
|
this.playerTable = {};
|
|
this.players = [];
|
|
this.p1 = null;
|
|
this.p2 = null;
|
|
this.p3 = null;
|
|
this.p4 = null;
|
|
this.ended = true;
|
|
void this.stream.destroy();
|
|
if (this.active) {
|
|
Rooms.global.battleCount += -1;
|
|
this.active = false;
|
|
}
|
|
this.room = null;
|
|
if (this.dataResolvers) {
|
|
for (const [, reject] of this.dataResolvers) {
|
|
reject(new Error("Battle was destroyed."));
|
|
}
|
|
}
|
|
}
|
|
async getTeam(user) {
|
|
const id = toID(user);
|
|
const player = this.playerTable[id];
|
|
if (!player)
|
|
return;
|
|
void this.stream.write(`>requestteam ${player.slot}`);
|
|
const teamDataPromise = new Promise((resolve, reject) => {
|
|
if (!this.dataResolvers)
|
|
this.dataResolvers = [];
|
|
this.dataResolvers.push([resolve, reject]);
|
|
});
|
|
const resultStrings = await teamDataPromise;
|
|
if (!resultStrings)
|
|
return;
|
|
const result = Teams.unpack(resultStrings[0]);
|
|
return result;
|
|
}
|
|
onChatMessage(message, user) {
|
|
const parts = message.split("\n");
|
|
for (const line of parts) {
|
|
void this.stream.write(`>chat-inputlogonly ${user.getIdentity(this.room)}|${line}`);
|
|
}
|
|
}
|
|
async getLog() {
|
|
if (!this.logData)
|
|
this.logData = {};
|
|
void this.stream.write(">requestlog");
|
|
const logPromise = new Promise((resolve, reject) => {
|
|
if (!this.dataResolvers)
|
|
this.dataResolvers = [];
|
|
this.dataResolvers.push([resolve, reject]);
|
|
});
|
|
const result = await logPromise;
|
|
return result;
|
|
}
|
|
}
|
|
class RoomBattleStream extends import_battle_stream.BattleStream {
|
|
constructor() {
|
|
super({ keepAlive: true });
|
|
this.battle = null;
|
|
}
|
|
_write(chunk) {
|
|
const startTime = Date.now();
|
|
if (this.battle && Config.debugsimprocesses && process.send) {
|
|
process.send("DEBUG\n" + this.battle.inputLog.join("\n") + "\n" + chunk);
|
|
}
|
|
try {
|
|
this._writeLines(chunk);
|
|
} catch (err) {
|
|
const battle = this.battle;
|
|
Monitor.crashlog(err, "A battle", {
|
|
chunk,
|
|
inputLog: battle ? "\n" + battle.inputLog.join("\n") : "",
|
|
log: battle ? "\n" + battle.getDebugLog() : ""
|
|
});
|
|
this.push(`update
|
|
|html|<div class="broadcast-red"><b>The battle crashed</b><br />Don't worry, we're working on fixing it.</div>`);
|
|
if (battle) {
|
|
for (const side of battle.sides) {
|
|
if (side?.requestState) {
|
|
this.push(`sideupdate
|
|
${side.id}
|
|
|error|[Invalid choice] The battle crashed`);
|
|
}
|
|
}
|
|
}
|
|
this.push(`error
|
|
${err.stack}`);
|
|
}
|
|
if (this.battle)
|
|
this.battle.sendUpdates();
|
|
const deltaTime = Date.now() - startTime;
|
|
if (deltaTime > 1e3) {
|
|
Monitor.slow(`[slow battle] ${deltaTime}ms - ${chunk.replace(/\n/ig, " | ")}`);
|
|
}
|
|
}
|
|
}
|
|
const PM = new import_lib.ProcessManager.StreamProcessManager(module, () => new RoomBattleStream(), (message) => {
|
|
if (message.startsWith(`SLOW
|
|
`)) {
|
|
Monitor.slow(message.slice(5));
|
|
}
|
|
});
|
|
if (!PM.isParentProcess) {
|
|
global.Config = require("./config-loader").Config;
|
|
global.Dex = require("../sim/dex").Dex;
|
|
global.Monitor = {
|
|
crashlog(error, source = "A simulator process", details = null) {
|
|
const repr = JSON.stringify([error.name, error.message, source, details]);
|
|
process.send(`THROW
|
|
@!!@${repr}
|
|
${error.stack}`);
|
|
},
|
|
slow(text) {
|
|
process.send(`CALLBACK
|
|
SLOW
|
|
${text}`);
|
|
}
|
|
};
|
|
global.__version = { head: "" };
|
|
try {
|
|
const head = (0, import_child_process.execSync)("git rev-parse HEAD", {
|
|
stdio: ["ignore", "pipe", "ignore"]
|
|
});
|
|
const merge = (0, import_child_process.execSync)("git merge-base origin/master HEAD", {
|
|
stdio: ["ignore", "pipe", "ignore"]
|
|
});
|
|
global.__version.head = ("" + head).trim();
|
|
const origin = ("" + merge).trim();
|
|
if (origin !== global.__version.head)
|
|
global.__version.origin = origin;
|
|
} catch {
|
|
}
|
|
if (Config.crashguard) {
|
|
process.on("uncaughtException", (err) => {
|
|
Monitor.crashlog(err, "A simulator process");
|
|
});
|
|
process.on("unhandledRejection", (err) => {
|
|
Monitor.crashlog(err || {}, "A simulator process Promise");
|
|
});
|
|
}
|
|
import_lib.Repl.start(`sim-${process.pid}`, (cmd) => eval(cmd));
|
|
} else {
|
|
PM.spawn(global.Config ? Config.simulatorprocesses : 1);
|
|
}
|
|
//# sourceMappingURL=room-battle.js.map
|