can learn ":" can't learn ")+(targets.length>2?"these moves":move.name);
if (result) {
var sourceNames = {E:"egg",S:"event",D:"dream world"};
if (lsetData.sources || lsetData.sourcesBefore) buffer += " only when obtained from:";
if (lsetData.sources) {
var sources = lsetData.sources.sort();
var prevSource;
var prevSourceType;
for (var i=0, len=sources.length; igen "+source.substr(0,1)+" "+sourceNames[source.substr(1,1)];
if (prevSourceType === '5E' && template.maleOnlyDreamWorld) buffer += " (cannot have DW ability)";
if (source.substr(2)) buffer += ": "+source.substr(2);
}
}
if (lsetData.sourcesBefore) buffer += "- any generation before "+(lsetData.sourcesBefore+1);
buffer += "
";
}
showOrBroadcast(user, cmd, room, socket,
buffer);
return false;
break;
case 'groups':
case '!groups':
showOrBroadcastStart(user, cmd, room, socket, message);
showOrBroadcast(user, cmd, room, socket,
'' +
'+ Voice - They can use ! commands like !groups, and talk during moderated chat
' +
'% Driver - The above, and they can also mute users and run tournaments
' +
'@ Moderator - The above, and they can ban users and check for alts
' +
'& Leader - The above, and they can promote moderators and force ties
'+
'~ Administrator - They can do anything, like change what this message says'+
'
');
return false;
break;
case 'opensource':
case '!opensource':
showOrBroadcastStart(user, cmd, room, socket, message);
showOrBroadcast(user, cmd, room, socket,
'');
return false;
break;
case 'avatars':
case '!avatars':
showOrBroadcastStart(user, cmd, room, socket, message);
showOrBroadcast(user, cmd, room, socket,
'');
return false;
break;
case 'intro':
case 'introduction':
case '!intro':
case '!introduction':
showOrBroadcastStart(user, cmd, room, socket, message);
showOrBroadcast(user, cmd, room, socket,
'');
return false;
break;
case 'cap':
case '!cap':
showOrBroadcastStart(user, cmd, room, socket, message);
showOrBroadcast(user, cmd, room, socket,
'');
return false;
break;
case 'rules':
case 'rule':
case '!rules':
case '!rule':
showOrBroadcastStart(user, cmd, room, socket, message);
showOrBroadcast(user, cmd, room, socket,
'Please follow the rules:
' +
'-
Rules' +
'
');
return false;
break;
case 'banlists':
case 'tiers':
case '!banlists':
case '!tiers':
showOrBroadcastStart(user, cmd, room, socket, message);
showOrBroadcast(user, cmd, room, socket,
'');
return false;
break;
case 'join':
var targetRoom = Rooms.get(target);
if (!targetRoom) {
emit(socket, 'console', "The room '"+target+"' does not exist.");
return false;
}
if (!user.joinRoom(targetRoom, socket)) {
emit(socket, 'console', "The room '"+target+"' could not be joined (most likely, you're already in it).");
return false;
}
return false;
break;
case 'leave':
case 'part':
if (room.id === 'lobby') return false;
user.leaveRoom(room, socket);
return false;
break;
// Battle commands
case 'reset':
case 'restart':
// These commands used to be:
// selfR.requestReset(user);
// selfR.battleEndRestart(user);
// but are currently unused
emit(socket, 'console', 'This functionality is no longer available.');
return false;
break;
case 'move':
case 'attack':
case 'mv':
if (!room.decision) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
room.decision(user, 'move', target);
return false;
break;
case 'switch':
case 'sw':
if (!room.decision) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
room.decision(user, 'switch', parseInt(target,10)-1);
return false;
break;
case 'undo':
if (!room.decision) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
room.decision(user, 'undo', target);
return false;
break;
case 'team':
if (!room.decision) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
room.decision(user, 'team', parseInt(target,10)-1);
return false;
break;
case 'search':
case 'cancelsearch':
if (!room.searchBattle) { emit(socket, 'console', 'You can only do this in lobby rooms.'); return false; }
if (target) {
room.searchBattle(user, target);
} else {
room.cancelSearch(user);
}
return false;
break;
case 'challenge':
case 'chall':
var targets = splitTarget(target);
var targetUser = targets[0];
target = targets[1];
if (!targetUser || !targetUser.connected) {
emit(socket, 'message', "The user '"+targets[2]+"' was not found.");
return false;
}
if (typeof target !== 'string') target = 'debugmode';
var problems = Tools.validateTeam(user.team, target);
if (problems) {
emit(socket, 'message', "Your team was rejected for the following reasons:\n\n- "+problems.join("\n- "));
return false;
}
user.makeChallenge(targetUser, target);
return false;
break;
case 'cancelchallenge':
case 'cchall':
user.cancelChallengeTo(target);
return false;
break;
case 'accept':
var userid = toUserid(target);
var format = 'debugmode';
if (user.challengesFrom[userid]) format = user.challengesFrom[userid].format;
var problems = Tools.validateTeam(user.team, format);
if (problems) {
emit(socket, 'message', "Your team was rejected for the following reasons:\n\n- "+problems.join("\n- "));
return false;
}
user.acceptChallengeFrom(userid);
return false;
break;
case 'reject':
user.rejectChallengeFrom(toUserid(target));
return false;
break;
case 'saveteam':
case 'utm':
try {
user.team = JSON.parse(target);
user.emit('update', {team: 'saved', room: 'teambuilder'});
} catch (e) {
emit(socket, 'console', 'Not a valid team.');
}
return false;
break;
case 'joinbattle':
case 'partbattle':
if (!room.joinBattle) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
room.joinBattle(user);
return false;
break;
case 'leavebattle':
if (!room.leaveBattle) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
room.leaveBattle(user);
return false;
break;
case 'kickinactive':
if (room.requestKickInactive) {
room.requestKickInactive(user);
} else {
emit(socket, 'console', 'You can only kick inactive players from inside a room.');
}
return false;
break;
case 'backdoor':
// This is the Zarel backdoor.
// Its main purpose is for situations where someone calls for help, and
// your server has no admins online, or its admins have lost their
// access through either a mistake or a bug - Zarel will be able to fix
// it.
// But yes, it is a backdoor, and it relies on trusting Zarel. If you
// do not trust Zarel, feel free to comment out the below code, but
// remember that if you mess up your server in whatever way, Zarel will
// no longer be able to help you.
if (user.userid === 'zarel') {
user.setGroup(config.groupsranking[config.groupsranking.length - 1]);
return false;
}
break;
case 'a':
if (user.can('battlemessage')) {
// secret sysop command
room.battle.add(target);
return false;
}
break;
// Admin commands
case 'forcewin':
case 'forcetie':
if (!user.can('forcewin') || !room.battle) {
emit(socket, 'console', '/forcewin - Access denied.');
return false;
}
room.battle.endType = 'forced';
if (!target) {
room.battle.tie();
logModCommand(room,user.name+' forced a tie.',true);
return false;
}
target = Users.get(target);
if (target) target = target.userid;
else target = '';
if (target) {
room.battle.win(target);
logModCommand(room,user.name+' forced a win for '+target+'.',true);
}
return false;
break;
case 'potd':
if (!user.can('potd')) {
emit(socket, 'console', '/potd - Access denied.');
return false;
}
config.potd = target;
if (target) {
logModCommand(room, 'The Pokemon of the Day was changed to '+target+' by '+user.name+'.');
} else {
logModCommand(room, 'The Pokemon of the Day was removed by '+user.name+'.');
}
return false;
break;
case 'lockdown':
if (!user.can('lockdown')) {
emit(socket, 'console', '/lockdown - Access denied.');
return false;
}
lockdown = true;
for (var id in rooms) {
rooms[id].addRaw('The server is restarting soon.
Please finish your battles quickly. No new battles can be started until the server resets in a few minutes.
');
}
return false;
break;
case 'endlockdown':
if (!user.can('lockdown')) {
emit(socket, 'console', '/endlockdown - Access denied.');
return false;
}
lockdown = false;
for (var id in rooms) {
rooms[id].addRaw('The server shutdown was canceled.
');
}
return false;
break;
case 'kill':
if (!user.can('lockdown')) {
emit(socket, 'console', '/lockdown - Access denied.');
return false;
}
if (!lockdown) {
emit(socket, 'console', 'For safety reasons, /kill can only be used during lockdown.');
return false;
}
process.exit();
return false;
break;
case 'loadbanlist':
if (!user.can('announce')) {
emit(socket, 'console', '/loadbanlist - Access denied.');
return false;
}
emit(socket, 'console', 'loading');
fs.readFile('config/ipbans.txt', function (err, data) {
if (err) return;
data = (''+data).split("\n");
for (var i=0; iWe fixed the crash without restarting the server!
You may resume talking in the lobby and starting new battles.');
return false;
break;
case 'crashnoted':
case 'crashlogged':
if (!lockdown) {
emit(socket, 'console', '/crashnoted - There is no active crash.');
return false;
}
if (!user.can('announce')) {
emit(socket, 'console', '/crashnoted - Access denied.');
return false;
}
lockdown = false;
config.modchat = false;
rooms.lobby.addRaw('We have logged the crash and are working on fixing it!
You may resume talking in the lobby and starting new battles.
');
return false;
break;
case 'modlog':
if (!user.can('modlog')) {
emit(socket, 'console', '/modlog - Access denied.');
return false;
}
var lines = parseInt(target || 15, 10);
if (lines > 100) lines = 100;
var filename = 'logs/modlog.txt';
var command = 'tail -'+lines+' '+filename;
var grepLimit = 100;
if (!lines || lines < 0) { // searching for a word instead
if (target.match(/^["'].+["']$/)) target = target.substring(1,target.length-1);
command = "awk '{print NR,$0}' "+filename+" | sort -nr | cut -d' ' -f2- | grep -m"+grepLimit+" -i '"+target.replace(/\\/g,'\\\\\\\\').replace(/["'`]/g,'\'\\$&\'').replace(/[\{\}\[\]\(\)\$\^\.\?\+\-\*]/g,'[$&]')+"'";
}
require('child_process').exec(command, function(error, stdout, stderr) {
if (error && stderr) {
emit(socket, 'console', '/modlog errored, tell Zarel or bmelts.');
console.log('/modlog error: '+error);
return false;
}
if (lines) {
if (!stdout) {
emit(socket, 'console', 'The modlog is empty. (Weird.)');
} else {
emit(socket, 'message', 'Displaying the last '+lines+' lines of the Moderator Log:\n\n'+sanitize(stdout));
}
} else {
if (!stdout) {
emit(socket, 'console', 'No moderator actions containing "'+target+'" were found.');
} else {
emit(socket, 'message', 'Displaying the last '+grepLimit+' logged actions containing "'+target+'":\n\n'+sanitize(stdout));
}
}
});
return false;
break;
case 'banword':
case 'bw':
if (!user.can('announce')) {
emit(socket, 'console', '/banword - Access denied.');
return false;
}
target = toId(target);
if (!target) {
emit(socket, 'console', 'Specify a word or phrase to ban.');
return false;
}
Users.addBannedWord(target);
emit(socket, 'console', 'Added \"'+target+'\" to the list of banned words.');
return false;
break;
case 'unbanword':
case 'ubw':
if (!user.can('announce')) {
emit(socket, 'console', '/unbanword - Access denied.');
return false;
}
target = toId(target);
if (!target) {
emit(socket, 'console', 'Specify a word or phrase to unban.');
return false;
}
Users.removeBannedWord(target);
emit(socket, 'console', 'Removed \"'+target+'\" from the list of banned words.');
return false;
break;
case 'help':
case 'commands':
case 'h':
case '?':
target = target.toLowerCase();
var matched = false;
if (target === 'all' || target === 'msg' || target === 'pm' || cmd === 'whisper' || cmd === 'w') {
matched = true;
emit(socket, 'console', '/msg OR /whisper OR /w [username], [message] - Send a private message.');
}
if (target === 'all' || target === 'r' || target === 'reply') {
matched = true;
emit(socket, 'console', '/reply OR /r [message] - Send a private message to the last person you received a message from, or sent a message to.');
}
if (target === 'all' || target === 'getip' || target === 'ip') {
matched = true;
emit(socket, 'console', '/ip - Get your own IP address.');
emit(socket, 'console', '/ip [username] - Get a user\'s IP address. Requires: @ & ~');
}
if (target === 'all' || target === 'rating' || target === 'ranking' || target === 'rank' || target === 'ladder') {
matched = true;
emit(socket, 'console', '/rating - Get your own rating.');
emit(socket, 'console', '/rating [username] - Get user\'s rating.');
}
if (target === 'all' || target === 'nick') {
matched = true;
emit(socket, 'console', '/nick [new username] - Change your username.');
}
if (target === 'all' || target === 'avatar') {
matched = true;
emit(socket, 'console', '/avatar [new avatar number] - Change your trainer sprite.');
}
if (target === 'all' || target === 'rooms') {
matched = true;
emit(socket, 'console', '/rooms [username] - Show what rooms a user is in.');
}
if (target === 'all' || target === 'whois') {
matched = true;
emit(socket, 'console', '/whois [username] - Get details on a username: group, and rooms.');
}
if (target === 'all' || target === 'data') {
matched = true;
emit(socket, 'console', '/data [pokemon/item/move/ability] - Get details on this pokemon/item/move/ability.');
emit(socket, 'console', '!data [pokemon/item/move/ability] - Show everyone these details. Requires: + % @ & ~');
}
if (target === 'all' || target === 'groups') {
matched = true;
emit(socket, 'console', '/groups - Explains what the + % @ & next to people\'s names mean.');
emit(socket, 'console', '!groups - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'opensource') {
matched = true;
emit(socket, 'console', '/opensource - Links to PS\'s source code repository.');
emit(socket, 'console', '!opensource - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'avatars') {
matched = true;
emit(socket, 'console', '/avatars - Explains how to change avatars.');
emit(socket, 'console', '!avatars - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'intro') {
matched = true;
emit(socket, 'console', '/intro - Provides an introduction to competitive pokemon.');
emit(socket, 'console', '!intro - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'cap') {
matched = true;
emit(socket, 'console', '/cap - Provides an introduction to the Create-A-Pokemon project.');
emit(socket, 'console', '!cap - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'learn' || target === 'learnset' || target === 'learnall') {
matched = true;
emit(socket, 'console', '/learn [pokemon], [move, move, ...] - Displays how a Pokemon can learn the given moves, if it can at all.')
emit(socket, 'console', '!learn [pokemon], [move, move, ...] - Show everyone that information. Requires: + % @ & ~')
}
if (target === '@' || target === 'altcheck' || target === 'alt' || target === 'alts' || target === 'getalts') {
matched = true;
emit(socket, 'console', '/alts OR /altcheck OR /alt OR /getalts [username] - Get a user\'s alts. Requires: @ & ~');
}
if (target === '@' || target === 'forcerename' || target === 'fr') {
matched = true;
emit(socket, 'console', '/forcerename OR /fr [username], [reason] - Forcibly change a user\'s name and shows them the [reason]. Requires: @ & ~');
}
if (target === '@' || target === 'forcerenameto' || target === 'frt') {
matched = true;
emit(socket, 'console', '/forcerenameto OR /frt [username] - Force a user to choose a new name. Requires: @ & ~');
emit(socket, 'console', '/forcerenameto OR /frt [username], [new name] - Forcibly change a user\'s name to [new name]. Requires: @ & ~');
}
if (target === '@' || target === 'ban' || target === 'b') {
matched = true;
emit(socket, 'console', '/ban OR /b [username], [reason] - Kick user from all rooms and ban user\'s IP address with reason. Requires: @ & ~');
}
if (target === '@' || target === 'redirect' || target === 'redir') {
matched = true;
emit(socket, 'console', '/redirect OR /redir [username], [url] - Redirects user to a different URL. ~~intl and ~~dev are accepted redirects. Requires: @ & ~');
}
if (target === "@" || target === 'kick' || target === 'k') {
matched = true;
emit(socket, 'console', '/kick OR /k [username] - Quickly kicks a user by redirecting them to the Smogon Sim Rules page. Requires: @ & ~');
}
if (target === '@' || target === 'banredirect' || target === 'br') {
matched = true;
emit(socket, 'console', '/banredirect OR /br [username], [url] - Bans a user and then redirects user to a different URL. Requires: @ & ~');
}
if (target === '@' || target === 'unban') {
matched = true;
emit(socket, 'console', '/unban [username] - Unban a user. Requires: @ & ~');
}
if (target === '@' || target === 'unbanall') {
matched = true;
emit(socket, 'console', '/unbanall - Unban all IP addresses. Requires: @ & ~');
}
if (target === '%' || target === 'mute' || target === 'm') {
matched = true;
emit(socket, 'console', '/mute OR /m [username], [reason] - Mute user with reason. Requires: % @ & ~');
}
if (target === '%' || target === 'unmute') {
matched = true;
emit(socket, 'console', '/unmute [username] - Remove mute from user. Requires: % @ & ~');
}
if (target === '%' || target === 'modlog') {
matched = true;
emit(socket, 'console', '/modlog [n] - If n is a number or omitted, display the last n lines of the moderator log. Defaults to 15. If n is not a number, search the moderator log for "n". Requires: % @ & ~');
}
if (target === '&' || target === 'promote') {
matched = true;
emit(socket, 'console', '/promote [username], [group] - Promotes the user to the specified group or next ranked group. Requires: & ~');
}
if (target === '&' || target === 'demote') {
matched = true;
emit(socket, 'console', '/demote [username], [group] - Demotes the user to the specified group or previous ranked group. Requires: & ~');
}
if (target === '&' || target === 'declare' ) {
matched = true;
emit(socket, 'console', '/declare [message] - Anonymously announces a message. Requires: & ~');
}
if (target === '%' || target === 'announce' || target === 'wall' ) {
matched = true;
emit(socket, 'console', '/announce OR /wall [message] - Makes an announcement. Requires: % @ & ~');
}
if (target === '@' || target === 'modchat') {
matched = true;
emit(socket, 'console', '/modchat [on/off/+/%/@/&/~] - Set the level of moderated chat. Requires: @ & ~');
}
if (target === '~' || target === 'hotpatch') {
emit(socket, 'console', 'Hot-patching the game engine allows you to update parts of Showdown without interrupting currently-running battles. Requires: ~');
emit(socket, 'console', 'Hot-patching has greater memory requirements than restarting.');
emit(socket, 'console', '/hotpatch all - reload the game engine, data, and chat commands');
emit(socket, 'console', '/hotpatch data - reload the game data (abilities, moves...)');
emit(socket, 'console', '/hotpatch chat - reload chat-commands.js');
}
if (target === 'all' || target === 'help' || target === 'h' || target === '?' || target === 'commands') {
matched = true;
emit(socket, 'console', '/help OR /h OR /? - Gives you help.');
}
if (!target) {
emit(socket, 'console', 'COMMANDS: /msg, /reply, /ip, /rating, /nick, /avatar, /rooms, /whois, /help');
emit(socket, 'console', 'INFORMATIONAL COMMANDS: /data, /groups, /opensource, /avatars, /tiers, /intro (replace / with ! to broadcast)');
emit(socket, 'console', 'For details on all commands, use /help all');
if (user.group !== config.groupsranking[0]) {
emit(socket, 'console', 'DRIVER COMMANDS: /mute, /unmute, /forcerename, /modlog, /announce')
emit(socket, 'console', 'MODERATOR COMMANDS: /alts, /forcerenameto, /ban, /unban, /unbanall, /potd, /namelock, /nameunlock, /ip, /redirect, /kick');
emit(socket, 'console', 'LEADER COMMANDS: /promote, /demote, /forcewin, /declare');
emit(socket, 'console', 'For details on all moderator commands, use /help @');
}
emit(socket, 'console', 'For details of a specific command, use something like: /help data');
} else if (!matched) {
emit(socket, 'console', 'The command "/'+target+'" was not found. Try /help for general help');
}
return false;
break;
default:
// Check for mod/demod/admin/deadmin/etc depending on the group ids
for (var g in config.groups) {
if (cmd === config.groups[g].id) {
return parseCommand(user, 'promote', toUserid(target) + ',' + g, room, socket);
} else if (cmd === 'de' + config.groups[g].id || cmd === 'un' + config.groups[g].id) {
var nextGroup = config.groupsranking[config.groupsranking.indexOf(g) - 1];
if (!nextGroup) nextGroup = config.groupsranking[0];
return parseCommand(user, 'demote', toUserid(target) + ',' + nextGroup, room, socket);
}
}
// There is no default case for unrecognised commands
// If a user types "/text" and there is no command "/text", it will be displayed:
// no error message will be given about unrecognized commands.
// This is intentional: Some people like to say things like "/shrug" - this
// means they don't need to manually escape it like "//shrug" - we will
// do it automatically for them
}
// chat moderation
if (!canTalk(user, room, socket)) {
return false;
}
if (message.match(/\bnimp\.org\b/)) {
// spam site
// todo: custom banlists
return false;
}
// remove zalgo
message = message.replace(/[\u0300-\u036f]{3,}/g,'');
if (message.substr(0,1) === '/' && message.substr(0,2) !== '//') {
// To the client, "/text" has special meaning, so "//" is used to
// escape "/" at the beginning of a message
// For instance: "/me did blah" will show as "* USER did blah", and
// "//me did blah" will show as "/me did blah"
// Here, we are automatically escaping unrecognized commands.
return '/'+message;
}
return message;
}
/**
* Can this user talk?
* Pass the corresponding socket to give the user an error, if not
*/
function canTalk(user, room, socket) {
if (!user.named) return false;
if (user.muted) {
if (socket) emit(socket, 'console', 'You are muted.');
return false;
}
if (config.modchat && room.id === 'lobby') {
if (config.modchat === 'crash') {
if (!user.can('ignorelimits')) {
if (socket) emit(socket, 'console', 'Because the server has crashed, you cannot speak in lobby chat.');
return false;
}
} else {
if (!user.authenticated && config.modchat === true) {
if (socket) emit(socket, 'console', 'Because moderated chat is set, you must be registered speak in lobby chat.');
return false;
} else if (config.groupsranking.indexOf(user.group) < config.groupsranking.indexOf(config.modchat)) {
var groupName = config.groups[config.modchat].name;
if (!groupName) groupName = config.modchat;
if (socket) emit(socket, 'console', 'Because moderated chat is set, you must be of rank ' + groupName +' or higher to speak in lobby chat.');
return false;
}
}
}
return true;
}
function showOrBroadcastStart(user, cmd, room, socket, message) {
if (cmd.substr(0,1) === '!') {
if (!user.can('broadcast') || user.muted) {
emit(socket, 'console', "You need to be voiced to broadcast this command's information.");
emit(socket, 'console', "To see it for yourself, use: /"+message.substr(1));
} else if (canTalk(user, room, socket)) {
room.add('|c|'+user.getIdentity()+'|'+message);
}
}
}
function showOrBroadcast(user, cmd, room, socket, rawMessage) {
if (cmd.substr(0,1) !== '!') {
sendData(socket, '>'+room.id+'\n|raw|'+rawMessage);
} else if (user.can('broadcast') && canTalk(user, room)) {
room.addRaw(rawMessage);
}
}
function getDataMessage(target) {
var pokemon = Tools.getTemplate(target);
var item = Tools.getItem(target);
var move = Tools.getMove(target);
var ability = Tools.getAbility(target);
var atLeastOne = false;
var response = [];
if (pokemon.exists) {
response.push('|c|&server|/data-pokemon '+pokemon.name);
atLeastOne = true;
}
if (ability.exists) {
response.push('|c|&server|/data-ability '+ability.name);
atLeastOne = true;
}
if (item.exists) {
response.push('|c|&server|/data-item '+item.name);
atLeastOne = true;
}
if (move.exists) {
response.push('|c|&server|/data-move '+move.name);
atLeastOne = true;
}
if (!atLeastOne) {
response.push("||No pokemon, item, move, or ability named '"+target+"' was found. (Check your capitalization?)");
}
return response;
}
function splitTarget(target, exactName) {
var commaIndex = target.indexOf(',');
if (commaIndex < 0) {
return [Users.get(target, exactName), '', target];
}
var targetUser = Users.get(target.substr(0, commaIndex), exactName);
if (!targetUser || !targetUser.connected) {
targetUser = null;
}
return [targetUser, target.substr(commaIndex+1).trim(), target.substr(0, commaIndex)];
}
function logModCommand(room, result, noBroadcast) {
if (!noBroadcast) room.add(result);
modlog.write('['+(new Date().toJSON())+'] ('+room.id+') '+result+'\n');
}
exports.parseCommand = parseCommandLocal;