pokemon-showdown/simulator.js
Guangcong Luo ed256ef68b Work around crash in simulator.js
Yet another crash caused by inconsistent state between the
user -> battle and battle -> user tables.

One of these days I guess I'll pin it down...
2016-06-09 03:19:33 -04:00

428 lines
10 KiB
JavaScript

/**
* Simulator abstraction layer
* Pokemon Showdown - http://pokemonshowdown.com/
*
* This file abstracts away Pokemon Showdown's multi-process simulator
* model. You can basically include this file, use its API, and pretend
* Pokemon Showdown is just one big happy process.
*
* For the actual simulation, see battle-engine.js
*
* @license MIT license
*/
'use strict';
const ProcessManager = require('./process-manager');
const SimulatorProcess = new ProcessManager({
maxProcesses: Config.simulatorprocesses,
execFile: 'battle-engine.js',
onMessageUpstream: function (message) {
let lines = message.split('\n');
let battle = this.pendingTasks.get(lines[0]);
if (battle) battle.receive(lines);
},
eval: function (code) {
for (let process of this.processes) {
process.send('|eval|' + code);
}
},
});
// Create the initial set of simulator processes.
SimulatorProcess.spawn();
let slice = Array.prototype.slice;
class BattlePlayer {
constructor(user, game, slot) {
this.userid = user.userid;
this.name = user.name;
this.game = game;
user.games[this.game.id] = this.game;
user.updateSearch();
this.slot = slot;
this.slotNum = Number(slot.charAt(1)) - 1;
this.active = true;
for (let i = 0; i < user.connections.length; i++) {
let connection = user.connections[i];
Sockets.subchannelMove(connection.worker, this.game.id, this.slotNum + 1, connection.socketid);
}
}
destroy() {
if (this.active) this.simSend('leave');
let user = Users(this.userid);
if (user) {
delete user.games[this.game.id];
user.updateSearch();
for (let j = 0; j < user.connections.length; j++) {
let connection = user.connections[j];
Sockets.subchannelMove(connection.worker, this.game.id, '0', connection.socketid);
}
}
this.game[this.slot] = null;
}
updateSubchannel(user) {
if (!user.connections) {
// "user" is actually a connection
Sockets.subchannelMove(user.worker, this.game.id, this.slotNum + 1, user.socketid);
return;
}
for (let i = 0; i < user.connections.length; i++) {
let connection = user.connections[i];
Sockets.subchannelMove(connection.worker, this.game.id, this.slotNum + 1, connection.socketid);
}
}
toString() {
return this.userid;
}
send(data) {
let user = Users(this.userid);
if (user) user.send(data);
}
sendRoom(data) {
let user = Users(this.userid);
if (user) user.sendTo(this.game.id, data);
}
simSend(action) {
this.game.send.apply(this.game, [action, this.slot].concat(slice.call(arguments, 1)));
}
}
class Battle {
constructor(room, format, rated) {
this.id = room.id;
this.room = room;
this.title = Tools.getFormat(format).name;
if (!this.title.endsWith(" Battle")) this.title += " Battle";
this.allowRenames = !rated;
this.format = toId(format);
this.rated = rated;
this.started = false;
this.ended = false;
this.active = false;
this.players = Object.create(null);
this.playerCount = 0;
this.playerCap = 2;
this.p1 = null;
this.p2 = null;
this.playerNames = [room.p1.name, room.p2.name];
this.requests = {};
// log information
this.logData = null;
this.endType = 'normal';
this.rqid = '';
this.inactiveQueued = false;
this.process = SimulatorProcess.acquire();
if (this.process.pendingTasks.has(room.id)) {
throw new Error("Battle with ID " + room.id + " already exists.");
}
this.send('init', this.format, rated ? '1' : '');
this.process.pendingTasks.set(room.id, this);
}
send() {
this.activeIp = Monitor.activeIp;
this.process.send('' + this.id + '|' + slice.call(arguments).join('|'));
}
sendFor(user, action) {
let player = this.players[user];
if (!player) return;
this.send.apply(this, [action, player.slot].concat(slice.call(arguments, 2)));
}
checkActive() {
let active = true;
if (this.ended || !this.started) {
active = false;
} else if (!this.p1 || !this.p1.active) {
active = false;
} else if (!this.p2 || !this.p2.active) {
active = false;
}
Rooms.global.battleCount += (active ? 1 : 0) - (this.active ? 1 : 0);
this.room.active = active;
this.active = active;
}
choose(user, data) {
this.sendFor(user, 'choose', data);
}
undo(user, data) {
this.sendFor(user, 'undo', data);
}
joinGame(user, team) {
if (this.playerCount >= 2) {
user.popup("This battle already has two players.");
return false;
}
if (!user.can('joinbattle', null, this.room)) {
user.popup("You must be a 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;
}
if (!this.addPlayer(user, team)) {
user.popup("Failed to join battle.");
return false;
}
this.room.update();
this.room.kickInactiveUpdate();
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;
}
if (!this.removePlayer(user)) {
user.popup("Failed to leave battle.");
return false;
}
this.room.auth[user.userid] = '+';
this.room.update();
this.room.kickInactiveUpdate();
return true;
}
receive(lines) {
Monitor.activeIp = this.activeIp;
switch (lines[1]) {
case 'update':
this.checkActive();
this.room.push(lines.slice(2));
this.room.update();
if (this.inactiveQueued) {
this.room.nextInactive();
this.inactiveQueued = false;
}
break;
case 'winupdate':
this.room.push(lines.slice(3));
this.started = true;
this.inactiveSide = -1;
if (!this.ended) {
this.ended = true;
this.room.win(lines[2]);
this.removeAllPlayers();
}
this.checkActive();
break;
case 'sideupdate': {
let player = this[lines[2]];
if (player) {
player.sendRoom(lines[3]);
}
break;
}
case 'request': {
let player = this[lines[2]];
let rqid = lines[3];
if (player) {
this.requests[player.slot] = lines[4];
player.sendRoom('|request|' + lines[4]);
}
if (rqid !== this.rqid) {
this.rqid = rqid;
this.inactiveQueued = true;
}
break;
}
case 'log':
this.logData = JSON.parse(lines[2]);
break;
case 'inactiveside':
this.inactiveSide = parseInt(lines[2]);
break;
case 'score':
this.score = [parseInt(lines[2]), parseInt(lines[3])];
break;
}
Monitor.activeIp = null;
}
onConnect(user, connection) {
// this handles joining a battle in which a user is a participant,
// where the user has already identified before attempting to join
// the battle
let player = this.players[user];
if (!player) return;
player.updateSubchannel(connection || user);
let request = this.requests[player.slot];
if (request) {
(connection || user).sendTo(this.id, '|request|' + request);
}
}
onUpdateConnection(user, connection) {
this.onConnect(user, connection);
}
onRename(user, oldUserid) {
if (user.userid === oldUserid) return;
if (!this.players) {
// !! should never happen but somehow still does
delete user.games[this.id];
return;
}
if (!(oldUserid in this.players)) {
if (user.userid in this.players) {
// this handles a user renaming themselves into a user in the
// battle (e.g. by using /nick)
this.onConnect(user);
}
return;
}
if (!this.allowRenames) {
this.forfeit(user, " forfeited by changing their name.");
return;
}
if (user.userid in this.players) return;
let player = this.players[oldUserid];
this.players[user.userid] = player;
player.userid = user.userid;
player.name = user.name;
delete this.players[oldUserid];
player.simSend('rename', user.name, user.avatar);
}
onJoin(user) {
let player = this.players[user];
if (player && !player.active) {
player.active = true;
player.simSend('join', user.name, user.avatar);
}
}
onLeave(user) {
let player = this.players[user];
if (player && player.active) {
player.active = false;
player.simSend('leave');
}
}
win(user) {
if (!user) {
this.tie();
return true;
}
let player = this.players[user];
if (!player) return false;
player.simSend('win');
}
tie() {
this.send('tie');
}
forfeit(user, message, side) {
if (this.ended || !this.started) return false;
if (!message) message = ' forfeited.';
if (side === undefined) {
if (user in this.players) side = this.players[user].slotNum;
}
if (side === undefined) return false;
let ids = ['p1', 'p2'];
let otherids = ['p2', 'p1'];
let name = 'Player ' + (side + 1);
if (this[ids[side]]) {
name = this[ids[side]].name;
}
this.room.add('|-message|' + name + message);
this.endType = 'forfeit';
this.send('win', otherids[side]);
return true;
}
addPlayer(user, team) {
if (user.userid in this.players) return false;
if (this.playerCount >= this.playerCap) return false;
let player = this.makePlayer(user, team);
if (!player) return false;
this.players[user.userid] = player;
this.playerCount++;
this.room.auth[user.userid] = '\u2605';
if (this.playerCount >= 2) {
this.room.title = "" + this.p1.name + " vs. " + this.p2.name;
this.room.send('|title|' + this.room.title);
}
return true;
}
makePlayer(user, team) {
let slotNum = 0;
while (this['p' + (slotNum + 1)]) slotNum++;
let slot = 'p' + (slotNum + 1);
// console.log('joining: ' + user.name + ' ' + slot);
let player = new BattlePlayer(user, this, slot);
this[slot] = player;
let message = '' + user.avatar;
if (!this.started) {
message += "\n" + team;
}
player.simSend('join', user.name, message);
if (this.p1 && this.p2) this.started = true;
return player;
}
removePlayer(user) {
if (!this.allowRenames) return false;
if (!(user.userid in this.players)) return false;
this.players[user.userid].destroy();
delete this.players[user.userid];
this.playerCount--;
return true;
}
removeAllPlayers() {
for (let i in this.players) {
this.players[i].destroy();
delete this.players[i];
this.playerCount--;
}
}
destroy() {
this.send('dealloc');
if (this.active) {
Rooms.global.battleCount += -1;
this.active = false;
}
for (let i in this.players) {
this.players[i].destroy();
}
this.players = null;
this.room = null;
this.process.pendingTasks.delete(this.id);
this.process.release();
this.process = null;
}
}
exports.BattlePlayer = BattlePlayer;
exports.Battle = Battle;
exports.SimulatorProcess = SimulatorProcess;
exports.create = function (id, format, rated, room) {
return new Battle(room, format, rated);
};