${this.tr`This user has not played any ladder games yet.`}
`;
} else {
buffer += `
${this.tr`Format`}
Elo
${this.tr`W`}
${this.tr`L`}
${this.tr`Total`}
`;
buffer += ratings;
}
buffer += `
`;
this.sendReply(`|raw|${buffer}`);
},
showrank: 'hiderank',
hiderank(target, room, user, connection, cmd) {
const userGroup = Users.Auth.getGroup(Users.globalAuth.get(user.id));
if (!userGroup['hiderank']) return this.errorReply(`/hiderank - Access denied.`);
const isShow = cmd === 'showrank';
const group = (isShow ? Users.globalAuth.get(user.id) : (target.trim() || Users.Auth.defaultSymbol()) as GroupSymbol);
if (user.tempGroup === group) {
return this.errorReply(this.tr`You already have the temporary symbol '${group}'.`);
}
if (!Users.Auth.isValidSymbol(group) || !(group in Config.groups)) {
return this.errorReply(this.tr`You must specify a valid group symbol.`);
}
if (!isShow && Config.groups[group].rank > Config.groups[user.tempGroup].rank) {
return this.errorReply(this.tr`You may only set a temporary symbol below your current rank.`);
}
user.tempGroup = group;
user.updateIdentity();
this.sendReply(`|c|~|${this.tr`Your temporary group symbol is now`} \`\`${user.tempGroup}\`\`.`);
},
showrankhelp: 'hiderankhelp',
hiderankhelp: [
`/hiderank [rank] - Displays your global rank as the given [rank].`,
`/showrank - Displays your true global rank instead of the rank you're hidden as.`,
],
language(target, room, user) {
if (!target) {
const language = Chat.languages.get(user.language || 'english' as ID);
return this.sendReply(this.tr`Currently, you're viewing Pokémon Showdown in ${language}.`);
}
const languageID = toID(target);
if (!Chat.languages.has(languageID)) {
const languages = [...Chat.languages.values()].join(', ');
return this.errorReply(this.tr`Valid languages are: ${languages}`);
}
user.language = languageID;
user.update();
const language = Chat.languages.get(languageID);
return this.sendReply(this.tr`Pokémon Showdown will now be displayed in ${language} (except in language rooms).`);
},
languagehelp: [
`/language - View your current language setting.`,
`/language [language] - Changes the language Pokémon Showdown will be displayed to you in.`,
`Note that rooms can set their own language, which will override this setting.`,
],
updatesettings(target, room, user) {
const settings: Partial = {};
try {
const raw = JSON.parse(target);
if (typeof raw !== 'object' || Array.isArray(raw) || !raw) {
this.errorReply(this.tr("/updatesettings expects JSON encoded object."));
}
if (typeof raw.language === 'string') this.parse(`/noreply /language ${raw.language}`);
for (const setting in user.settings) {
if (setting in raw) {
if (setting === 'blockPMs' &&
Users.Auth.isAuthLevel(raw[setting])) {
settings[setting] = raw[setting];
} else {
settings[setting as keyof UserSettings] = !!raw[setting];
}
}
}
Object.assign(user.settings, settings);
user.update();
} catch {
this.errorReply(this.tr("Unable to parse settings in /updatesettings!"));
}
},
/*********************************************************
* Battle management commands
*********************************************************/
allowexportinputlog(target, room, user) {
room = this.requireRoom();
const battle = room.battle;
if (!battle) {
return this.errorReply(this.tr`Must be in a battle.`);
}
const targetUser = Users.getExact(target);
if (!targetUser) {
return this.errorReply(this.tr`User ${target} not found.`);
}
if (!battle.playerTable[user.id]) {
return this.errorReply(this.tr("Must be a player in this battle."));
}
if (!battle.allowExtraction[targetUser.id]) {
return this.errorReply(this.tr`${targetUser.name} has not requested extraction.`);
}
if (battle.allowExtraction[targetUser.id].has(user.id)) {
return this.errorReply(this.tr`You have already consented to extraction with ${targetUser.name}.`);
}
battle.allowExtraction[targetUser.id].add(user.id);
this.addModAction(room.tr`${user.name} consents to sharing battle team and choices with ${targetUser.name}.`);
if (!battle.inputLog) return this.errorReply(this.tr('No input log found.'));
if (Object.keys(battle.playerTable).length === battle.allowExtraction[targetUser.id].size) {
this.addModAction(room.tr`${targetUser.name} has extracted the battle input log.`);
const inputLog = battle.inputLog.map(Utils.escapeHTML).join(` `);
targetUser.sendTo(
room,
`|html|
${inputLog}
`,
);
}
},
requestinputlog: 'exportinputlog',
exportinputlog(target, room, user) {
room = this.requireRoom();
const battle = room.battle;
if (!battle) {
return this.errorReply(this.tr`This command only works in battle rooms.`);
}
if (!battle.inputLog) {
this.errorReply(this.tr`This command only works when the battle has ended - if the battle has stalled, use /offertie.`);
if (user.can('forcewin')) this.errorReply(this.tr`Alternatively, you can end the battle with /forcetie.`);
return;
}
this.checkCan('exportinputlog', null, room);
if (user.can('forcewin')) {
if (!battle.inputLog) return this.errorReply(this.tr('No input log found.'));
this.addModAction(room.tr`${user.name} has extracted the battle input log.`);
const inputLog = battle.inputLog.map(Utils.escapeHTML).join(` `);
user.sendTo(
room,
`|html|
${inputLog}
`,
);
} else if (!battle.allowExtraction[user.id]) {
battle.allowExtraction[user.id] = new Set();
for (const player of battle.players) {
const playerUser = player.getUser();
if (!playerUser) continue;
if (playerUser.id === user.id) {
battle.allowExtraction[user.id].add(user.id);
} else {
playerUser.sendTo(
room,
Utils.html`|html|${user.name} wants to extract the battle input log. `
);
}
}
this.addModAction(room.tr`${user.name} wants to extract the battle input log.`);
} else {
// Re-request to make the buttons appear again for users who have not allowed extraction
let logExported = true;
for (const player of battle.players) {
const playerUser = player.getUser();
if (!playerUser || battle.allowExtraction[user.id].has(playerUser.id)) continue;
logExported = false;
playerUser.sendTo(
room,
Utils.html`|html|${user.name} wants to extract the battle input log. `
);
}
if (logExported) return this.errorReply(this.tr`You already extracted the battle input log.`);
this.sendReply(this.tr`Battle input log re-requested.`);
}
},
exportinputloghelp: [`/exportinputlog - Asks players in a battle for permission to export an inputlog. Requires: &`],
importinputlog(target, room, user, connection) {
this.checkCan('importinputlog');
const formatIndex = target.indexOf(`"formatid":"`);
const nextQuoteIndex = target.indexOf(`"`, formatIndex + 12);
if (formatIndex < 0 || nextQuoteIndex < 0) return this.errorReply(this.tr`Invalid input log.`);
target = target.replace(/\r/g, '');
if ((`\n` + target).includes(`\n>eval `) && !user.hasConsoleAccess(connection)) {
return this.errorReply(this.tr`Your input log contains untrusted code - you must have console access to use it.`);
}
const formatid = target.slice(formatIndex + 12, nextQuoteIndex);
const battleRoom = Rooms.createBattle(formatid, {inputLog: target});
if (!battleRoom) return; // createBattle will inform the user if creating the battle failed
const nameIndex1 = target.indexOf(`"name":"`);
const nameNextQuoteIndex1 = target.indexOf(`"`, nameIndex1 + 8);
const nameIndex2 = target.indexOf(`"name":"`, nameNextQuoteIndex1 + 1);
const nameNextQuoteIndex2 = target.indexOf(`"`, nameIndex2 + 8);
if (nameIndex1 >= 0 && nameNextQuoteIndex1 >= 0 && nameIndex2 >= 0 && nameNextQuoteIndex2 >= 0) {
const battle = battleRoom.battle!;
battle.p1.name = target.slice(nameIndex1 + 8, nameNextQuoteIndex1);
battle.p2.name = target.slice(nameIndex2 + 8, nameNextQuoteIndex2);
}
battleRoom.auth.set(user.id, Users.HOST_SYMBOL);
this.parse(`/join ${battleRoom.roomid}`);
setTimeout(() => {
// timer to make sure this goes under the battle
battleRoom.add(`|html|
This is an imported replay Players will need to be manually added with /addplayer or /restoreplayers
`);
}, 500);
},
importinputloghelp: [`/importinputlog [inputlog] - Starts a battle with a given inputlog. Requires: + % @ &`],
showteam: 'showset',
async showset(target, room, user, connection, cmd) {
this.checkChat();
const showAll = cmd === 'showteam';
const hideStats = toID(target) === 'hidestats';
room = this.requireRoom();
const battle = room.battle;
if (!showAll && !target) return this.parse(`/help showset`);
if (!battle) return this.errorReply(this.tr("This command can only be used in a battle."));
let teamStrings = await battle.getTeam(user);
if (!teamStrings) return this.errorReply(this.tr("Only players can extract their team."));
if (!showAll) {
const parsed = parseInt(target);
if (parsed > 6) return this.errorReply(this.tr`Use a number between 1-6 to view a specific set.`);
if (isNaN(parsed)) {
const matchedSet = teamStrings.filter(set => {
const id = toID(target);
return toID(set.name) === id || toID(set.species) === id;
})[0];
if (!matchedSet) return this.errorReply(this.tr`The Pokemon "${target}" is not in your team.`);
teamStrings = [matchedSet];
} else {
const setIndex = parsed - 1;
const indexedSet = teamStrings[setIndex];
if (!indexedSet) return this.errorReply(this.tr`That Pokemon is not in your team.`);
teamStrings = [indexedSet];
}
}
const nicknames = teamStrings.map(set => {
const species = Dex.getSpecies(set.species).baseSpecies;
return species !== set.name ? set.name : species;
});
let resultString = Dex.stringifyTeam(teamStrings, nicknames, hideStats);
if (showAll) {
resultString = `${this.tr`View team`}${resultString}`;
}
this.runBroadcast(true);
return this.sendReplyBox(resultString);
},
showsethelp: [
`!showteam - show the team you're using in the current battle (must be used in a battle you're a player in).`,
`!showteam hidestats - show the team you're using in the current battle, without displaying any stat-related information.`,
`!showset [number] - shows the set of the pokemon corresponding to that number (in original Team Preview order, not necessarily current order)`,
],
acceptdraw: 'offertie',
accepttie: 'offertie',
offerdraw: 'offertie',
requesttie: 'offertie',
offertie(target, room, user, connection, cmd) {
room = this.requireRoom();
const battle = room.battle;
if (!battle) return this.errorReply(this.tr("Must be in a battle room."));
if (!Config.allowrequestingties) {
return this.errorReply(this.tr("This server does not allow offering ties."));
}
if (room.tour) {
return this.errorReply(this.tr("You can't offer ties in tournaments."));
}
if (battle.turn < 100) {
return this.errorReply(this.tr("It's too early to tie, please play until turn 100."));
}
this.checkCan('roomvoice', null, room);
if (cmd === 'accepttie' && !battle.players.some(player => player.wantsTie)) {
return this.errorReply(this.tr("No other player is requesting a tie right now. It was probably canceled."));
}
const player = battle.playerTable[user.id];
if (!battle.players.some(curPlayer => curPlayer.wantsTie)) {
this.add(this.tr`${user.name} is offering a tie.`);
room.update();
for (const otherPlayer of battle.players) {
if (otherPlayer !== player) {
otherPlayer.sendRoom(
Utils.html`|uhtml|offertie| `
);
} else {
player.wantsTie = true;
}
}
} else {
if (!player) {
return this.errorReply(this.tr("Must be a player to accept ties."));
}
if (!player.wantsTie) {
player.wantsTie = true;
} else {
return this.errorReply(this.tr("You have already agreed to a tie."));
}
player.sendRoom(Utils.html`|uhtmlchange|offertie|`);
this.add(this.tr`${user.name} accepted the tie.`);
if (battle.players.every(curPlayer => curPlayer.wantsTie)) {
if (battle.players.length > 2) {
this.add(this.tr`All players have accepted the tie.`);
}
battle.tie();
}
}
},
offertiehelp: [`/offertie - Offers a tie to all players in a battle; if all accept, it ties. Requires: \u2606 @ # &`],
rejectdraw: 'rejecttie',
rejecttie(target, room, user) {
room = this.requireRoom();
const battle = room.battle;
if (!battle) return this.errorReply(this.tr("Must be in a battle room."));
const player = battle.playerTable[user.id];
if (!player) {
return this.errorReply(this.tr("Must be a player to reject ties."));
}
if (!battle.players.some(curPlayer => curPlayer.wantsTie)) {
return this.errorReply(this.tr("No other player is requesting a tie right now. It was probably canceled."));
}
if (player.wantsTie) player.wantsTie = false;
for (const otherPlayer of battle.players) {
otherPlayer.sendRoom(Utils.html`|uhtmlchange|offertie|`);
}
return this.add(this.tr`${user.name} rejected the tie.`);
},
rejecttiehelp: [`/rejecttie - Rejects a tie offered by another player in a battle.`],
inputlog() {
this.parse(`/help exportinputlog`);
this.parse(`/help importinputlog`);
},
/*********************************************************
* Battle commands
*********************************************************/
forfeit(target, room, user) {
room = this.requireRoom();
if (!room.game) return this.errorReply(this.tr("This room doesn't have an active game."));
if (!room.game.forfeit) {
return this.errorReply(this.tr("This kind of game can't be forfeited."));
}
room.game.forfeit(user);
},
guess: 'choose',
choose(target, room, user) {
room = this.requireRoom();
if (!room.game) return this.errorReply(this.tr("This room doesn't have an active game."));
if (!room.game.choose) return this.errorReply(this.tr("This game doesn't support /choose"));
if (room.game.checkChat) this.checkChat();
room.game.choose(user, target);
},
choosehelp: [
`/choose [text] - Make a choice for the currently active game.`,
],
mv: 'move',
attack: 'move',
move(target, room, user) {
this.parse(`/choose move ${target}`);
},
sw: 'switch',
switch(target, room, user) {
this.parse(`/choose switch ${target}`);
},
team(target, room, user) {
this.parse(`/choose team ${target}`);
},
undo(target, room, user) {
room = this.requireRoom();
if (!room.game) return this.errorReply(this.tr("This room doesn't have an active game."));
if (!room.game.undo) return this.errorReply(this.tr("This game doesn't support /undo"));
room.game.undo(user, target);
},
uploadreplay: 'savereplay',
async savereplay(target, room, user, connection) {
if (!room || !room.battle) {
return this.errorReply(this.tr`You can only save replays for battles.`);
}
const options = (target === 'forpunishment' || target === 'silent') ? target : undefined;
await room.uploadReplay(user, connection, options);
},
hidereplay(target, room, user, connection) {
if (!room || !room.battle) return this.errorReply(`Must be used in a battle.`);
this.checkCan('joinbattle', null, room);
if (room.tour?.forcePublic) {
return this.errorReply(this.tr`This battle can't have hidden replays, because the tournament is set to be forced public.`);
}
if (room.hideReplay) return this.errorReply(this.tr`The replay for this battle is already set to hidden.`);
room.hideReplay = true;
// If a replay has already been saved, /savereplay again to update the uploaded replay's hidden status
if (room.battle.replaySaved) this.parse('/savereplay');
this.addModAction(room.tr`${user.name} hid the replay of this battle.`);
},
addplayer(target, room, user) {
room = this.requireRoom();
if (!target) return this.parse('/help addplayer');
if (!room.battle) return this.errorReply(this.tr("You can only do this in battle rooms."));
if (room.rated) return this.errorReply(this.tr("You can only add a Player to unrated battles."));
target = this.splitTarget(target, true).trim();
if (target !== 'p1' && target !== 'p2') {
this.errorReply(this.tr`Player must be set to "p1" or "p2", not "${target}".`);
return this.parse('/help addplayer');
}
const targetUser = this.targetUser;
const name = this.targetUsername;
if (!targetUser) return this.errorReply(this.tr`User ${name} not found.`);
if (!targetUser.inRooms.has(room.roomid)) {
return this.errorReply(this.tr`User ${name} must be in the battle room already.`);
}
this.checkCan('joinbattle', null, room);
if (room.battle[target].id) {
return this.errorReply(this.tr`This room already has a player in slot ${target}.`);
}
if (targetUser.id in room.battle.playerTable) {
return this.errorReply(this.tr`${targetUser.name} is already a player in this battle.`);
}
room.auth.set(targetUser.id, Users.PLAYER_SYMBOL);
const success = room.battle.joinGame(targetUser, target);
if (!success) {
room.auth.delete(targetUser.id);
return;
}
const playerNum = target.slice(1);
this.addModAction(room.tr`${name} was added to the battle as Player ${playerNum} by ${user.name}.`);
this.modlog('ROOMPLAYER', targetUser.getLastId());
},
addplayerhelp: [
`/addplayer [username], p1 - Allow the specified user to join the battle as Player 1.`,
`/addplayer [username], p2 - Allow the specified user to join the battle as Player 2.`,
],
restoreplayers(target, room, user) {
room = this.requireRoom();
if (!room.battle) return this.errorReply(this.tr("You can only do this in battle rooms."));
if (room.rated) return this.errorReply(this.tr("You can only add a Player to unrated battles."));
let didSomething = false;
if (!room.battle.p1.id && room.battle.p1.name !== 'Player 1') {
this.parse(`/addplayer ${room.battle.p1.name}, p1`);
didSomething = true;
}
if (!room.battle.p2.id && room.battle.p2.name !== this.tr('Player 2')) {
this.parse(`/addplayer ${room.battle.p2.name}, p2`);
didSomething = true;
}
if (!didSomething) {
return this.errorReply(this.tr`Players could not be restored (maybe this battle already has two players?).`);
}
},
restoreplayershelp: [
`/restoreplayers - Restore previous players in an imported input log.`,
],
joinbattle: 'joingame',
joingame(target, room, user) {
room = this.requireRoom();
if (!room.game) return this.errorReply(this.tr("This room doesn't have an active game."));
if (!room.game.joinGame) return this.errorReply(this.tr("This game doesn't support /joingame"));
room.game.joinGame(user, target);
},
leavebattle: 'leavegame',
partbattle: 'leavegame',
leavegame(target, room, user) {
room = this.requireRoom();
if (!room.game) return this.errorReply(this.tr("This room doesn't have an active game."));
if (!room.game.leaveGame) return this.errorReply(this.tr("This game doesn't support /leavegame"));
room.game.leaveGame(user);
},
kickbattle: 'kickgame',
kickgame(target, room, user) {
room = this.requireRoom();
if (!room.battle) return this.errorReply(this.tr("You can only do this in battle rooms."));
if (room.battle.challengeType === 'tour' || room.battle.rated) {
return this.errorReply(this.tr("You can only do this in unrated non-tour battles."));
}
target = this.splitTarget(target);
const targetUser = this.targetUser;
if (!targetUser || !targetUser.connected) {
const targetUsername = this.targetUsername;
return this.errorReply(this.tr`User ${targetUsername} not found.`);
}
this.checkCan('kick', targetUser, room);
if (room.battle.leaveGame(targetUser)) {
const displayTarget = target ? ` (${target})` : ``;
this.addModAction(room.tr`${targetUser.name} was kicked from a battle by ${user.name} ${displayTarget}`);
this.modlog('KICKBATTLE', targetUser, target, {noip: 1, noalts: 1});
} else {
this.errorReply("/kickbattle - User isn't in battle.");
}
},
kickbattlehelp: [`/kickbattle [username], [reason] - Kicks a user from a battle with reason. Requires: % @ &`],
kickinactive(target, room, user) {
this.parse(`/timer on`);
},
timer(target, room, user) {
target = toID(target);
room = this.requireRoom();
if (!room.game || !room.game.timer) {
return this.errorReply(this.tr`You can only set the timer from inside a battle room.`);
}
const timer = room.game.timer as any;
if (!timer.timerRequesters) {
return this.sendReply(this.tr`This game's timer is managed by a different command.`);
}
if (!target) {
if (!timer.timerRequesters.size) {
return this.sendReply(this.tr`The game timer is OFF.`);
}
const requester = [...timer.timerRequesters].join(', ');
return this.sendReply(this.tr`The game timer is ON (requested by ${requester})`);
}
const force = user.can('timer', null, room);
if (!force && !room.game.playerTable[user.id]) {
return this.errorReply(this.tr`Access denied.`);
}
if (this.meansNo(target) || target === 'stop') {
if (timer.timerRequesters.size) {
timer.stop(force ? undefined : user);
if (force) {
room.send(`|inactiveoff|${room.tr`Timer was turned off by staff. Please do not turn it back on until our staff say it's okay.`}`);
}
} else {
this.errorReply(this.tr`The timer is already off.`);
}
} else if (this.meansYes(target) || target === 'start') {
timer.start(user);
} else {
this.errorReply(this.tr`"${target}" is not a recognized timer state.`);
}
},
autotimer: 'forcetimer',
forcetimer(target, room, user) {
room = this.requireRoom();
target = toID(target);
this.checkCan('autotimer');
if (this.meansNo(target) || target === 'stop') {
Config.forcetimer = false;
this.addModAction(room.tr`Forcetimer is now OFF: The timer is now opt-in. (set by ${user.name})`);
} else if (this.meansYes(target) || target === 'start' || !target) {
Config.forcetimer = true;
this.addModAction(room.tr`Forcetimer is now ON: All battles will be timed. (set by ${user.name})`);
} else {
this.errorReply(this.tr`'${target}' is not a recognized forcetimer setting.`);
}
},
forcetie: 'forcewin',
forcewin(target, room, user) {
room = this.requireRoom();
this.checkCan('forcewin');
if (!room.battle) {
this.errorReply("/forcewin - This is not a battle room.");
return false;
}
room.battle.endType = 'forced';
if (!target) {
room.battle.tie();
this.modlog('FORCETIE');
return false;
}
const targetUser = Users.getExact(target);
if (!targetUser) return this.errorReply(this.tr`User '${target}' not found.`);
room.battle.win(targetUser);
this.modlog('FORCEWIN', targetUser.id);
},
forcewinhelp: [
`/forcetie - Forces the current match to end in a tie. Requires: &`,
`/forcewin [user] - Forces the current match to end in a win for a user. Requires: &`,
],
/*********************************************************
* Challenging and searching commands
*********************************************************/
async search(target, room, user, connection) {
if (target) {
if (Config.laddermodchat && !Users.globalAuth.atLeast(user, Config.laddermodchat)) {
const groupName = Config.groups[Config.laddermodchat].name || Config.laddermodchat;
this.popupReply(this.tr`This server requires you to be rank ${groupName} or higher to search for a battle.`);
return false;
}
const ladder = Ladders(target);
if (!user.registered && Config.forceregisterelo && await ladder.getRating(user.id) >= Config.forceregisterelo) {
user.send(
Utils.html`|popup||html|${this.tr`Since you have reached ${Config.forceregisterelo} ELO in ${target}, you must register your account to continue playing that format on ladder.`}`
);
return false;
}
return ladder.searchBattle(user, connection);
}
return Ladders.cancelSearches(user);
},
cancelsearch(target, room, user) {
if (target) {
Ladders(toID(target)).cancelSearch(user);
} else {
Ladders.cancelSearches(user);
}
},
chall: 'challenge',
challenge(target, room, user, connection) {
target = this.splitTarget(target);
const targetUser = this.targetUser;
if (!targetUser || !targetUser.connected) {
const targetUsername = this.targetUsername;
return this.popupReply(this.tr`The user '${targetUsername}' was not found.`);
}
if (user.locked && !targetUser.locked) {
return this.popupReply(this.tr`You are locked and cannot challenge unlocked users.`);
}
if (Punishments.isBattleBanned(user)) {
return this.popupReply(this.tr`You are banned from battling and cannot challenge users.`);
}
if (!user.named) {
return this.popupReply(this.tr`You must choose a username before you challenge someone.`);
}
if (Config.pmmodchat && !user.hasSysopAccess() && !Users.globalAuth.atLeast(user, Config.pmmodchat as GroupSymbol)) {
const groupName = Config.groups[Config.pmmodchat].name || Config.pmmodchat;
this.popupReply(this.tr`This server requires you to be rank ${groupName} or higher to challenge users.`);
return false;
}
return Ladders(target).makeChallenge(connection, targetUser);
},
bch: 'blockchallenges',
blockchall: 'blockchallenges',
blockchalls: 'blockchallenges',
blockchallenges(target, room, user) {
if (user.settings.blockChallenges) return this.errorReply(this.tr("You are already blocking challenges!"));
user.settings.blockChallenges = true;
user.update();
this.sendReply(this.tr("You are now blocking all incoming challenge requests."));
},
blockchallengeshelp: [
`/blockchallenges - Blocks challenges so no one can challenge you. Unblock them with /unblockchallenges.`,
],
unbch: 'allowchallenges',
unblockchall: 'allowchallenges',
unblockchalls: 'allowchallenges',
unblockchallenges: 'allowchallenges',
allowchallenges(target, room, user) {
if (!user.settings.blockChallenges) return this.errorReply(this.tr("You are already available for challenges!"));
user.settings.blockChallenges = false;
user.update();
this.sendReply(this.tr("You are available for challenges from now on."));
},
allowchallengeshelp: [
`/unblockchallenges - Unblocks challenges so you can be challenged again. Block them with /blockchallenges.`,
],
cchall: 'cancelChallenge',
cancelchallenge(target, room, user) {
Ladders.cancelChallenging(user);
},
accept(target, room, user, connection) {
target = this.splitTarget(target);
if (target) return this.popupReply(this.tr`This command does not support specifying multiple users`);
const targetUser = this.targetUser || this.pmTarget;
const targetUsername = this.targetUsername;
if (!targetUser) return this.popupReply(this.tr`User "${targetUsername}" not found.`);
return Ladders.acceptChallenge(connection, targetUser);
},
reject(target, room, user) {
target = toID(target);
if (!target && this.pmTarget) target = this.pmTarget.id;
Ladders.rejectChallenge(user, target);
},
saveteam: 'useteam',
utm: 'useteam',
useteam(target, room, user) {
user.battleSettings.team = target;
},
vtm(target, room, user, connection) {
if (Monitor.countPrepBattle(connection.ip, connection)) {
return;
}
if (!target) return this.errorReply(this.tr("Provide a valid format."));
const originalFormat = Dex.getFormat(target);
// Note: The default here of [Gen 8] Anything Goes isn't normally hit; since the web client will send a default format
const format = originalFormat.effectType === 'Format' ? originalFormat : Dex.getFormat(
'[Gen 8] Anything Goes'
);
if (format.effectType !== this.tr('Format')) return this.popupReply(this.tr("Please provide a valid format."));
return TeamValidatorAsync.get(format.id).validateTeam(user.battleSettings.team).then(result => {
const matchMessage = (originalFormat === format ? "" : this.tr`The format '${originalFormat.name}' was not found.`);
if (result.charAt(0) === '1') {
connection.popup(`${(matchMessage ? matchMessage + "\n\n" : "")}${this.tr`Your team is valid for ${format.name}.`}`);
} else {
connection.popup(`${(matchMessage ? matchMessage + "\n\n" : "")}${this.tr`Your team was rejected for the following reasons:`}\n\n- ${result.slice(1).replace(/\n/g, '\n- ')}`);
}
});
},
hbtc: 'hidebattlesfromtrainercard',
sbtc: 'hidebattlesfromtrainercard',
showbattlesinusercard: 'hidebattlesfromtrainercard',
hidebattlesfromusercard: 'hidebattlesfromtrainercard',
showbattlesintrainercard: 'hidebattlesfromtrainercard',
hidebattlesfromtrainercard(target, room, user, connection, cmd) {
const shouldHide = cmd.includes('hide') || cmd === 'hbtc';
user.settings.hideBattlesFromTrainerCard = shouldHide;
user.update();
if (shouldHide) {
this.sendReply(this.tr`Battles are now hidden (except to staff) in your trainer card.`);
} else {
this.sendReply(this.tr`Battles are now visible in your trainer card.`);
}
},
hidebattlesfromtrainercardhelp: [
`/hidebattlesfromtrainercard OR /hbtc - Hides your battles in your trainer card.`,
`/showbattlesintrainercard OR /sbtc - Displays your battles in your trainer card.`,
],
/*********************************************************
* Low-level
*********************************************************/
cmd: 'crq',
query: 'crq',
crq(target, room, user, connection) {
// In emergency mode, clamp down on data returned from crq's
const trustable = (!Config.emergency || (user.named && user.registered));
const spaceIndex = target.indexOf(' ');
let cmd = target;
if (spaceIndex > 0) {
cmd = target.substr(0, spaceIndex);
target = target.substr(spaceIndex + 1);
} else {
target = '';
}
if (cmd === 'userdetails') {
if (target.length > 18) {
connection.send('|queryresponse|userdetails|null');
return false;
}
const targetUser = Users.get(target);
if (!trustable || !targetUser) {
connection.send('|queryresponse|userdetails|' + JSON.stringify({
id: target,
userid: toID(target),
name: target,
rooms: false,
}));
return false;
}
interface RoomData {p1?: string; p2?: string; isPrivate?: boolean | 'hidden' | 'voice'}
let roomList: {[roomid: string]: RoomData} | false = {};
for (const roomid of targetUser.inRooms) {
const targetRoom = Rooms.get(roomid);
if (!targetRoom) continue; // shouldn't happen
const roomData: RoomData = {};
if (targetRoom.settings.isPrivate) {
if (!user.inRooms.has(roomid) && !user.games.has(roomid)) continue;
roomData.isPrivate = true;
}
if (targetRoom.battle) {
if (targetUser.settings.hideBattlesFromTrainerCard && user.id !== targetUser.id && !user.can('lock')) continue;
const battle = targetRoom.battle;
roomData.p1 = battle.p1 ? ' ' + battle.p1.name : '';
roomData.p2 = battle.p2 ? ' ' + battle.p2.name : '';
}
let roomidWithAuth: string = roomid;
if (targetRoom.auth.has(targetUser.id)) {
roomidWithAuth = targetRoom.auth.getDirect(targetUser.id) + roomid;
}
roomList[roomidWithAuth] = roomData;
}
if (!targetUser.connected) roomList = false;
let group = targetUser.tempGroup;
if (targetUser.locked) group = Config.punishgroups?.locked?.symbol ?? '\u203d';
if (targetUser.namelocked) group = Config.punishgroups?.namelocked?.symbol ?? '✖';
const userdetails: AnyObject = {
id: target,
userid: targetUser.id,
name: targetUser.name,
avatar: targetUser.avatar,
group: group,
autoconfirmed: !!targetUser.autoconfirmed,
status: targetUser.getStatus(),
rooms: roomList,
};
connection.send('|queryresponse|userdetails|' + JSON.stringify(userdetails));
} else if (cmd === 'roomlist') {
if (!trustable) return false;
connection.send('|queryresponse|roomlist|' + JSON.stringify({
rooms: Rooms.global.getBattles(target),
}));
} else if (cmd === 'rooms') {
if (!trustable) return false;
connection.send('|queryresponse|rooms|' + JSON.stringify(
Rooms.global.getRooms(user)
));
} else if (cmd === 'laddertop') {
if (!trustable) return false;
const [format, prefix] = target.split(',').map(x => x.trim());
return Ladders(toID(format)).getTop(prefix).then(result => {
connection.send('|queryresponse|laddertop|' + JSON.stringify(result));
});
} else if (cmd === 'roominfo') {
if (!trustable) return false;
if (target.length > 225) {
connection.send('|queryresponse|roominfo|null');
return false;
}
const targetRoom = Rooms.get(target);
if (!targetRoom || (
targetRoom.settings.isPrivate && !user.inRooms.has(targetRoom.roomid) && !user.games.has(targetRoom.roomid)
)) {
const roominfo = {id: target, error: 'not found or access denied'};
connection.send(`|queryresponse|roominfo|${JSON.stringify(roominfo)}`);
return false;
}
let visibility;
if (targetRoom.settings.isPrivate) {
visibility = (targetRoom.settings.isPrivate === 'hidden') ? 'hidden' : 'secret';
} else {
visibility = 'public';
}
const roominfo: AnyObject = {
id: target,
roomid: targetRoom.roomid,
title: targetRoom.title,
type: targetRoom.type,
visibility: visibility,
modchat: targetRoom.settings.modchat,
modjoin: targetRoom.settings.modjoin,
auth: {},
users: [],
};
for (const [id, rank] of targetRoom.auth) {
if (!roominfo.auth[rank]) roominfo.auth[rank] = [];
roominfo.auth[rank].push(id);
}
for (const userid in targetRoom.users) {
const curUser = targetRoom.users[userid];
if (!curUser.named) continue;
const userinfo = curUser.getIdentity(targetRoom.roomid);
roominfo.users.push(userinfo);
}
connection.send(`|queryresponse|roominfo|${JSON.stringify(roominfo)}`);
} else {
// default to sending null
connection.send(`|queryresponse|${cmd}|null`);
}
},
trn(target, room, user, connection) {
if (target === user.name) return false;
let commaIndex = target.indexOf(',');
let targetName = target;
let targetRegistered = false;
let targetToken = '';
if (commaIndex >= 0) {
targetName = target.substr(0, commaIndex);
target = target.substr(commaIndex + 1);
commaIndex = target.indexOf(',');
targetRegistered = !!target;
if (commaIndex >= 0) {
targetRegistered = !!parseInt(target.substr(0, commaIndex));
targetToken = target.substr(commaIndex + 1);
}
}
return user.rename(targetName, targetToken, targetRegistered, connection);
},
/*********************************************************
* Help commands
*********************************************************/
commands: 'help',
h: 'help',
'?': 'help',
man: 'help',
help(target, room, user) {
if (!this.runBroadcast()) return;
target = target.toLowerCase();
if (target.startsWith('/') || target.startsWith('!')) target = target.slice(1);
if (!target) {
const broadcastMsg = this.tr('(replace / with ! to broadcast. Broadcasting requires: + % @ # &)');
this.sendReply(`${this.tr('COMMANDS')}: /msg, /reply, /logout, /challenge, /search, /rating, /whois, /user, /report, /join, /leave, /makegroupchat, /userauth, /roomauth`);
this.sendReply(`${this.tr('BATTLE ROOM COMMANDS')}: /savereplay, /hideroom, /inviteonly, /invite, /timer, /forfeit`);
this.sendReply(`${this.tr('OPTION COMMANDS')}: /nick, /avatar, /ignore, /status, /away, /busy, /back, /timestamps, /highlight, /showjoins, /hidejoins, /blockchallenges, /blockpms`);
this.sendReply(`${this.tr('INFORMATIONAL/RESOURCE COMMANDS')}: /groups, /faq, /rules, /intro, /formatshelp, /othermetas, /analysis, /punishments, /calc, /git, /cap, /roomhelp, /roomfaq ${broadcastMsg}`);
this.sendReply(`${this.tr('DATA COMMANDS')}: /data, /dexsearch, /movesearch, /itemsearch, /learn, /statcalc, /effectiveness, /weakness, /coverage, /randommove, /randompokemon ${broadcastMsg}`);
if (user.tempGroup !== Users.Auth.defaultSymbol()) {
this.sendReply(`${this.tr('DRIVER COMMANDS')}: /warn, /mute, /hourmute, /unmute, /alts, /forcerename, /modlog, /modnote, /modchat, /lock, /weeklock, /unlock, /announce`);
this.sendReply(`${this.tr('MODERATOR COMMANDS')}: /globalban, /unglobalban, /ip, /markshared, /unlockip`);
this.sendReply(`${this.tr('ADMIN COMMANDS')}: /declare, /forcetie, /forcewin, /promote, /demote, /banip, /host, /unbanall, /ipsearch`);
}
this.sendReply(this.tr("For an overview of room commands, use /roomhelp"));
this.sendReply(this.tr("For details of a specific command, use something like: /help data"));
return;
}
const cmds = target.split(' ');
let namespace = Chat.commands;
let currentBestHelp: {help: string[] | Chat.AnnotatedChatHandler, for: string[]} | null = null;
for (const [i, cmd] of cmds.entries()) {
let nextNamespace = namespace[cmd];
if (typeof nextNamespace === 'string') {
const help = namespace[`${nextNamespace}help`];
if (Array.isArray(help) || typeof help === 'function') {
currentBestHelp = {
help, for: cmds.slice(0, i + 1),
};
}
nextNamespace = namespace[nextNamespace];
}
if (typeof nextNamespace === 'string') {
throw new Error(`Recursive alias in "${target}"`);
}
if (Array.isArray(nextNamespace)) {
const command = cmds.slice(0, i + 1).join(' ');
this.sendReply(this.tr`'/${command}' is a help command.`);
return this.parse(`/${target}`);
}
if (!nextNamespace) {
for (const g in Config.groups) {
const groupid = Config.groups[g].id;
if (new RegExp(`(global)?(un|de)?${groupid}`).test(target)) {
return this.parse(`/help promote`);
}
if (new RegExp(`room(un|de)?${groupid}`).test(target)) {
return this.parse(`/help roompromote`);
}
}
return this.errorReply(this.tr`The command '/${target}' does not exist.`);
}
const help = namespace[`${cmd}help`];
if (Array.isArray(help) || typeof help === 'function') {
currentBestHelp = {
help, for: cmds.slice(0, i + 1),
};
}
if (typeof nextNamespace === 'function') break;
namespace = nextNamespace as import('../chat').AnnotatedChatCommands;
}
if (!currentBestHelp) {
return this.errorReply(this.tr`Could not find help for '/${target}'. Try /help for general help.`);
}
const closestHelp = currentBestHelp.for.join(' ');
if (currentBestHelp.for.length < cmds.length) {
this.errorReply(this.tr`Could not find help for '/${target}' - displaying help for '/${closestHelp}' instead`);
}
const curHandler = this.parseCommand(`/${closestHelp}`)?.handler;
if (curHandler?.isPrivate && !user.can('lock')) {
return this.errorReply(this.tr`The command '/${target}' does not exist.`);
}
if (typeof currentBestHelp.help === 'function') {
// If the help command is a function, parse it instead
this.run(currentBestHelp.help);
} else if (Array.isArray(currentBestHelp.help)) {
this.sendReply(currentBestHelp.help.map(line => this.tr(line)).join('\n'));
}
},
helphelp: [
`/help OR /h OR /? - Gives you help.`,
`/help [command] - Gives you help for a specific command.`,
],
};
process.nextTick(() => {
// We might want to migrate most of this to a JSON schema of command attributes.
Chat.multiLinePattern.register(
'>>>? ', '/(?:room|staff)intro ', '/(?:staff)?topic ', '/(?:add|widen)datacenters ', '/bash ', '!code ', '/code ', '/modnote ', '/mn ',
'/eval', '!eval', '/evalbattle',
'/importinputlog '
);
});