mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-04-26 02:39:38 -05:00
373 lines
9.0 KiB
JavaScript
373 lines
9.0 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';
|
|
|
|
let battles = Object.create(null);
|
|
|
|
let SimulatorProcess = (function () {
|
|
function SimulatorProcess() {
|
|
this.process = require('child_process').fork('battle-engine.js', {cwd: __dirname});
|
|
this.process.on('message', function (message) {
|
|
let lines = message.split('\n');
|
|
let battle = battles[lines[0]];
|
|
if (battle) {
|
|
battle.receive(lines);
|
|
}
|
|
});
|
|
this.send = this.process.send.bind(this.process);
|
|
}
|
|
SimulatorProcess.prototype.load = 0;
|
|
SimulatorProcess.prototype.active = true;
|
|
SimulatorProcess.processes = [];
|
|
SimulatorProcess.spawn = function (num) {
|
|
if (!num) num = Config.simulatorprocesses || 1;
|
|
for (let i = this.processes.length; i < num; ++i) {
|
|
this.processes.push(new SimulatorProcess());
|
|
}
|
|
};
|
|
SimulatorProcess.respawn = function () {
|
|
this.processes.splice(0).forEach(function (process) {
|
|
process.active = false;
|
|
if (!process.load) process.process.disconnect();
|
|
});
|
|
this.spawn();
|
|
};
|
|
SimulatorProcess.acquire = function () {
|
|
let process = this.processes[0];
|
|
for (let i = 1; i < this.processes.length; ++i) {
|
|
if (this.processes[i].load < process.load) {
|
|
process = this.processes[i];
|
|
}
|
|
}
|
|
process.load++;
|
|
return process;
|
|
};
|
|
SimulatorProcess.release = function (process) {
|
|
process.load--;
|
|
if (!process.load && !process.active) {
|
|
process.process.disconnect();
|
|
}
|
|
};
|
|
SimulatorProcess.eval = function (code) {
|
|
this.processes.forEach(function (process) {
|
|
process.send('|eval|' + code);
|
|
});
|
|
};
|
|
return SimulatorProcess;
|
|
})();
|
|
|
|
// 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;
|
|
|
|
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.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];
|
|
for (let j = 0; j < user.connections.length; j++) {
|
|
let connection = user.connections[j];
|
|
Sockets.subchannelMove(connection.worker, this.id, '0', connection.socketid);
|
|
}
|
|
}
|
|
this.game[this.slot] = null;
|
|
}
|
|
updateSubchannel(user) {
|
|
if (!user.connections) {
|
|
// "user" is actually a connection
|
|
Sockets.subchannelMove(user.worker, this.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.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) {
|
|
if (battles[room.id]) {
|
|
throw new Error("Battle with ID " + room.id + " already exists.");
|
|
}
|
|
|
|
this.id = room.id;
|
|
this.room = room;
|
|
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();
|
|
this.send('init', this.format, rated ? '1' : '');
|
|
|
|
battles[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() {
|
|
if (this.ended || !this.started) return false;
|
|
if (!this.p1 || !this.p1.active) return false;
|
|
if (!this.p2 || !this.p2.active) return false;
|
|
return true;
|
|
}
|
|
|
|
receive(lines) {
|
|
Monitor.activeIp = this.activeIp;
|
|
switch (lines[1]) {
|
|
case 'update':
|
|
this.active = 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.active = false;
|
|
this.inactiveSide = -1;
|
|
if (!this.ended) {
|
|
this.ended = true;
|
|
this.room.win(lines[2]);
|
|
}
|
|
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], 10);
|
|
break;
|
|
|
|
case 'score':
|
|
this.score = [parseInt(lines[2], 10), parseInt(lines[3], 10)];
|
|
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, oldid) {
|
|
if (user.userid === oldid) return;
|
|
let player = this.players[oldid];
|
|
if (player) {
|
|
if (!this.allowRenames && user.userid !== oldid) {
|
|
this.room.forfeit(user, " forfeited by changing their name.");
|
|
return;
|
|
}
|
|
if (!this.players[user]) {
|
|
this.players[user] = player;
|
|
player.userid = user.userid;
|
|
player.name = user.name;
|
|
delete this.players[oldid];
|
|
player.simSend('rename', user.name, user.avatar);
|
|
}
|
|
}
|
|
if (!player && user in this.players) {
|
|
// this handles a user renaming themselves into a user in the
|
|
// battle (e.g. by using /nick)
|
|
this.onConnect(user);
|
|
}
|
|
}
|
|
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');
|
|
}
|
|
|
|
addPlayer(user) {
|
|
if (user.userid in this.players) return false;
|
|
if (this.playerCount >= this.playerCap) return false;
|
|
let player = this.makePlayer.apply(this, arguments);
|
|
if (!player) return false;
|
|
this.players[user.userid] = player;
|
|
this.playerCount++;
|
|
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;
|
|
}
|
|
|
|
destroy() {
|
|
this.send('dealloc');
|
|
|
|
for (let i in this.players) {
|
|
this.players[i].destroy();
|
|
}
|
|
this.players = null;
|
|
this.room = null;
|
|
SimulatorProcess.release(this.process);
|
|
this.process = null;
|
|
delete battles[this.id];
|
|
}
|
|
}
|
|
|
|
exports.BattlePlayer = BattlePlayer;
|
|
exports.Battle = Battle;
|
|
exports.battles = battles;
|
|
exports.SimulatorProcess = SimulatorProcess;
|
|
|
|
exports.create = function (id, format, rated, room) {
|
|
if (battles[id]) return battles[id];
|
|
return new Battle(room, format, rated);
|
|
};
|