Spaces:
Paused
Paused
| ; | |
| var __defProp = Object.defineProperty; | |
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | |
| var __getOwnPropNames = Object.getOwnPropertyNames; | |
| 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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | |
| var ladders_exports = {}; | |
| __export(ladders_exports, { | |
| Ladders: () => Ladders | |
| }); | |
| module.exports = __toCommonJS(ladders_exports); | |
| var import_ladders_challenges = require("./ladders-challenges"); | |
| /** | |
| * Matchmaker | |
| * Pokemon Showdown - http://pokemonshowdown.com/ | |
| * | |
| * This keeps track of challenges to battle made between users, setting up | |
| * matches between users looking for a battle, and starting new battles. | |
| * | |
| * @license MIT | |
| */ | |
| const LadderStore = (typeof Config === "object" && Config.remoteladder ? require("./ladders-remote") : require("./ladders-local")).LadderStore; | |
| const SECONDS = 1e3; | |
| const PERIODIC_MATCH_INTERVAL = 60 * SECONDS; | |
| const searches = /* @__PURE__ */ new Map(); | |
| class Ladder extends LadderStore { | |
| async prepBattle(connection, challengeType, team = null, isRated = false) { | |
| const user = connection.user; | |
| const userid = user.id; | |
| if (team === null) | |
| team = user.battleSettings.team; | |
| if (Rooms.global.lockdown && Rooms.global.lockdown !== "pre") { | |
| let message = `The server is restarting. Battles will be available again in a few minutes.`; | |
| if (Rooms.global.lockdown === "ddos") { | |
| message = `The server is under attack. Battles cannot be started at this time.`; | |
| } | |
| connection.popup(message); | |
| return null; | |
| } | |
| if (Punishments.isBattleBanned(user)) { | |
| connection.popup(`You are barred from starting any new games until your battle ban expires.`); | |
| return null; | |
| } | |
| const gameCount = user.games.size; | |
| if (Monitor.countConcurrentBattle(gameCount, connection)) { | |
| return null; | |
| } | |
| if (Monitor.countPrepBattle(connection.ip, connection)) { | |
| return null; | |
| } | |
| try { | |
| this.formatid = Dex.formats.validate(this.formatid); | |
| } catch (e) { | |
| connection.popup(`Your selected format is invalid: | |
| - ${e.message}`); | |
| return null; | |
| } | |
| let rating = 0; | |
| let valResult; | |
| let removeNicknames = !!(user.locked || user.namelocked); | |
| const regex = /(?:^|])([^|]*)\|([^|]*)\|/g; | |
| let match = regex.exec(team); | |
| let unownWord = ""; | |
| while (match) { | |
| const nickname = match[1]; | |
| const speciesid = toID(match[2] || match[1]); | |
| if (speciesid.length <= 6 && speciesid.startsWith("unown")) { | |
| unownWord += speciesid.charAt(5) || "a"; | |
| } | |
| if (nickname) { | |
| const filtered = Chat.nicknamefilter(nickname, user); | |
| if (typeof filtered === "string" && (!filtered || filtered !== match[1])) { | |
| connection.popup( | |
| `Your team was rejected for the following reason: | |
| - Your Pok\xE9mon has a banned nickname: ${match[1]}` | |
| ); | |
| return null; | |
| } else if (filtered === false) { | |
| removeNicknames = true; | |
| } | |
| } | |
| match = regex.exec(team); | |
| } | |
| if (unownWord) { | |
| const filtered = Chat.nicknamefilter(unownWord, user); | |
| if (!filtered || filtered !== unownWord) { | |
| connection.popup( | |
| `Your team was rejected for the following reason: | |
| - Your Unowns spell out a banned word: ${unownWord.toUpperCase()}` | |
| ); | |
| return null; | |
| } | |
| } | |
| if (isRated && !Ladders.disabled) { | |
| const uid = user.id; | |
| [valResult, rating] = await Promise.all([ | |
| TeamValidatorAsync.get(this.formatid).validateTeam(team, { removeNicknames, user: uid }), | |
| this.getRating(uid) | |
| ]); | |
| if (uid !== user.id) { | |
| return null; | |
| } | |
| if (!rating) | |
| rating = 1; | |
| } else { | |
| if (Ladders.disabled) { | |
| connection.popup(`The ladder is temporarily disabled due to technical difficulties - you will not receive ladder rating for this game.`); | |
| rating = 1; | |
| } | |
| const validator = TeamValidatorAsync.get(this.formatid); | |
| valResult = await validator.validateTeam(team, { removeNicknames, user: user.id }); | |
| } | |
| if (!valResult.startsWith("1")) { | |
| connection.popup( | |
| `Your team was rejected for the following reasons: | |
| - ` + valResult.slice(1).replace(/\n/g, ` | |
| - `) | |
| ); | |
| return null; | |
| } | |
| const settings = { ...user.battleSettings, team: valResult.slice(1) }; | |
| user.battleSettings.inviteOnly = false; | |
| user.battleSettings.hidden = false; | |
| return new import_ladders_challenges.BattleReady(userid, this.formatid, settings, rating, challengeType); | |
| } | |
| static getChallenging(userid) { | |
| const userChalls = Ladders.challenges.get(userid); | |
| if (userChalls) { | |
| for (const chall of userChalls) { | |
| if (chall.from === userid) | |
| return chall; | |
| } | |
| } | |
| return null; | |
| } | |
| async makeChallenge(connection, targetUser) { | |
| const user = connection.user; | |
| if (targetUser === user) { | |
| connection.popup(`You can't battle yourself. The best you can do is open PS in Private Browsing (or another browser) and log into a different username, and battle that username.`); | |
| return false; | |
| } | |
| if (Ladder.getChallenging(user.id)) { | |
| connection.popup(`You are already challenging someone. Cancel that challenge before challenging someone else.`); | |
| return false; | |
| } | |
| let blockChallenge; | |
| if (typeof targetUser.settings.blockChallenges === "boolean") { | |
| blockChallenge = targetUser.settings.blockChallenges; | |
| } else if (targetUser.settings.blockChallenges === "friends") { | |
| blockChallenge = !targetUser.friends?.has(user.id); | |
| } else { | |
| blockChallenge = !Users.globalAuth.atLeast(user, targetUser.settings.blockChallenges); | |
| } | |
| if (blockChallenge && !user.can("bypassblocks", targetUser)) { | |
| connection.popup(`The user '${targetUser.name}' is not accepting challenges right now.`); | |
| Chat.maybeNotifyBlocked("challenge", targetUser, user); | |
| return false; | |
| } | |
| if (Date.now() < user.lastChallenge + 10 * SECONDS && !Config.nothrottle) { | |
| connection.popup(`You challenged less than 10 seconds after your last challenge! It's cancelled in case it's a misclick.`); | |
| return false; | |
| } | |
| const currentChallenges = Ladders.challenges.get(targetUser.id); | |
| if (currentChallenges && currentChallenges.length >= 3 && !user.autoconfirmed) { | |
| connection.popup( | |
| `This user already has 3 pending challenges. | |
| You must be autoconfirmed to challenge them.` | |
| ); | |
| return false; | |
| } | |
| const ready = await this.prepBattle(connection, "challenge"); | |
| if (!ready) | |
| return false; | |
| const existingChall = Ladders.challenges.search(user.id, targetUser.id); | |
| if (existingChall) { | |
| if (existingChall.from === targetUser.id && existingChall.to === user.id && existingChall.format === this.formatid && existingChall.ready) { | |
| if (Ladders.challenges.remove(existingChall)) { | |
| Ladders.match([existingChall.ready, ready]); | |
| return true; | |
| } | |
| } else { | |
| connection.popup(`There's already a challenge (${existingChall.format}) between you and ${targetUser.name}!`); | |
| Ladders.challenges.update(user.id, targetUser.id); | |
| return false; | |
| } | |
| } | |
| Ladders.challenges.add(new import_ladders_challenges.BattleChallenge(user.id, targetUser.id, ready)); | |
| Ladders.challenges.send(user.id, targetUser.id, `/log ${user.name} wants to battle!`); | |
| user.lastChallenge = Date.now(); | |
| Chat.runHandlers("onChallenge", user, targetUser, ready.formatid); | |
| return true; | |
| } | |
| static async acceptChallenge(connection, chall) { | |
| const ladder = Ladders(chall.format); | |
| const ready = await ladder.prepBattle(connection, "challenge"); | |
| if (!ready) | |
| return; | |
| if (Ladders.challenges.remove(chall)) { | |
| return Ladders.match([chall.ready, ready]); | |
| } | |
| } | |
| cancelSearch(user) { | |
| const formatid = toID(this.formatid); | |
| const formatTable = Ladders.searches.get(formatid); | |
| if (!formatTable) | |
| return false; | |
| if (!formatTable.searches.has(user.id)) | |
| return false; | |
| formatTable.searches.delete(user.id); | |
| Ladder.updateSearch(user); | |
| return true; | |
| } | |
| static cancelSearches(user) { | |
| let cancelCount = 0; | |
| for (const formatTable of Ladders.searches.values()) { | |
| const search = formatTable.searches.get(user.id); | |
| if (!search) | |
| continue; | |
| formatTable.searches.delete(user.id); | |
| cancelCount++; | |
| } | |
| Ladder.updateSearch(user); | |
| return cancelCount; | |
| } | |
| getSearcher(search) { | |
| const formatid = toID(this.formatid); | |
| const user = Users.get(search.userid); | |
| if (!user?.connected || user.id !== search.userid) { | |
| const formatTable = Ladders.searches.get(formatid); | |
| if (formatTable) | |
| formatTable.searches.delete(search.userid); | |
| if (user?.connected) { | |
| user.popup(`You changed your name and are no longer looking for a battle in ${formatid}`); | |
| Ladder.updateSearch(user); | |
| } | |
| return null; | |
| } | |
| return user; | |
| } | |
| static getSearches(user) { | |
| const userSearches = []; | |
| for (const [formatid, formatTable] of Ladders.searches) { | |
| if (formatTable.searches.has(user.id)) | |
| userSearches.push(formatid); | |
| } | |
| return userSearches; | |
| } | |
| static updateSearch(user, connection = null) { | |
| let games = {}; | |
| let atLeastOne = false; | |
| for (const roomid of user.games) { | |
| const room = Rooms.get(roomid); | |
| if (!room) { | |
| Monitor.warn(`while searching, room ${roomid} expired for user ${user.id} in rooms ${[...user.inRooms]} and games ${[...user.games]}`); | |
| user.games.delete(roomid); | |
| continue; | |
| } | |
| const game = room.game; | |
| if (!game) { | |
| Monitor.warn(`while searching, room ${roomid} has no game for user ${user.id} in rooms ${[...user.inRooms]} and games ${[...user.games]}`); | |
| user.games.delete(roomid); | |
| continue; | |
| } | |
| games[roomid] = game.title + (game.allowRenames ? "" : "*"); | |
| atLeastOne = true; | |
| } | |
| if (!atLeastOne) | |
| games = null; | |
| const searching = Ladders.getSearches(user); | |
| (connection || user).send(`|updatesearch|` + JSON.stringify({ | |
| searching, | |
| games | |
| })); | |
| } | |
| hasSearch(user) { | |
| const formatid = toID(this.formatid); | |
| const formatTable = Ladders.searches.get(formatid); | |
| if (!formatTable) | |
| return false; | |
| return formatTable.searches.has(user.id); | |
| } | |
| /** | |
| * Validates a user's team and fetches their rating for a given format | |
| * before creating a search for a battle. | |
| */ | |
| async searchBattle(user, connection) { | |
| if (!user.connected) | |
| return; | |
| const format = Dex.formats.get(this.formatid); | |
| if (!format.searchShow) { | |
| connection.popup(`Error: Your format ${format.id} is not ladderable.`); | |
| return; | |
| } | |
| const oldUserid = user.id; | |
| const search = await this.prepBattle(connection, format.rated ? "rated" : "unrated", null, format.rated !== false); | |
| if (oldUserid !== user.id) | |
| return; | |
| if (!search) | |
| return; | |
| this.addSearch(search, user); | |
| } | |
| /** | |
| * Verifies whether or not a match made between two users is valid. Returns | |
| */ | |
| matchmakingOK(matches) { | |
| const formatid = toID(this.formatid); | |
| const users = matches.map(([ready, user]) => user); | |
| const userids = users.map((user) => user.id); | |
| if (new Set(users).size !== users.length) | |
| return false; | |
| if (Config.noipchecks) { | |
| users[0].lastMatch = users[1].id; | |
| users[1].lastMatch = users[0].id; | |
| return true; | |
| } | |
| if (new Set(users.map((user) => user.latestIp)).size !== users.length) | |
| return false; | |
| for (const user of users) { | |
| if (userids.includes(user.lastMatch)) | |
| return false; | |
| } | |
| let searchRange = 100; | |
| const times = matches.map(([search]) => search.time); | |
| const elapsed = Date.now() - Math.min(...times); | |
| if (formatid === `gen${Dex.gen}ou` || formatid === `gen${Dex.gen}randombattle`) { | |
| searchRange = 50; | |
| } | |
| searchRange += elapsed / 300; | |
| if (searchRange > 300) | |
| searchRange = 300 + (searchRange - 300) / 10; | |
| if (searchRange > 600) | |
| searchRange = 600; | |
| const ratings = matches.map(([search]) => search.rating); | |
| if (Math.max(...ratings) - Math.min(...ratings) > searchRange) | |
| return false; | |
| matches[0][1].lastMatch = matches[1][1].id; | |
| matches[1][1].lastMatch = matches[0][1].id; | |
| return true; | |
| } | |
| /** | |
| * Starts a search for a battle for a user under the given format. | |
| */ | |
| addSearch(newSearch, user) { | |
| const formatid = newSearch.formatid; | |
| let formatTable = Ladders.searches.get(formatid); | |
| if (!formatTable) { | |
| formatTable = { | |
| playerCount: Dex.formats.get(formatid).playerCount, | |
| searches: /* @__PURE__ */ new Map() | |
| }; | |
| Ladders.searches.set(formatid, formatTable); | |
| } | |
| if (formatTable.searches.has(user.id)) { | |
| user.popup(`Couldn't search: You are already searching for a ${formatid} battle.`); | |
| return; | |
| } | |
| const matches = [newSearch]; | |
| for (const search of formatTable.searches.values()) { | |
| const searcher = this.getSearcher(search); | |
| if (!searcher) | |
| continue; | |
| const matched = this.matchmakingOK([[search, searcher], [newSearch, user]]); | |
| if (matched) { | |
| matches.push(search); | |
| } | |
| if (matches.length >= formatTable.playerCount) { | |
| for (const matchedSearch of matches) | |
| formatTable.searches.delete(matchedSearch.userid); | |
| Ladder.match(matches); | |
| return; | |
| } | |
| } | |
| formatTable.searches.set(newSearch.userid, newSearch); | |
| Ladder.updateSearch(user); | |
| } | |
| /** | |
| * Creates a match for a new battle for each format in this.searches if a | |
| * valid match can be made. This is run periodically depending on | |
| * PERIODIC_MATCH_INTERVAL. | |
| */ | |
| static periodicMatch() { | |
| for (const [formatid, formatTable] of Ladders.searches) { | |
| if (formatTable.playerCount > 2) | |
| continue; | |
| const matchmaker = Ladders(formatid); | |
| let longest = null; | |
| for (const search of formatTable.searches.values()) { | |
| if (!longest) { | |
| const longestSearcher2 = matchmaker.getSearcher(search); | |
| if (!longestSearcher2) | |
| continue; | |
| longest = [search, longestSearcher2]; | |
| continue; | |
| } | |
| const searcher = matchmaker.getSearcher(search); | |
| if (!searcher) | |
| continue; | |
| const [longestSearch, longestSearcher] = longest; | |
| const matched = matchmaker.matchmakingOK([[search, searcher], [longestSearch, longestSearcher]]); | |
| if (matched) { | |
| formatTable.searches.delete(search.userid); | |
| formatTable.searches.delete(longestSearch.userid); | |
| Ladder.match([longestSearch, search]); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| static match(readies) { | |
| const formatid = readies[0].formatid; | |
| if (readies.some((ready) => ready.formatid !== formatid)) | |
| throw new Error(`Format IDs don't match`); | |
| const players = []; | |
| let missingUser = null; | |
| let minRating = Infinity; | |
| for (const ready of readies) { | |
| const user = Users.get(ready.userid); | |
| if (!user) { | |
| missingUser = ready.userid; | |
| break; | |
| } | |
| players.push({ | |
| user, | |
| team: ready.settings.team, | |
| rating: ready.rating, | |
| hidden: ready.settings.hidden, | |
| inviteOnly: ready.settings.inviteOnly | |
| }); | |
| if (ready.rating < minRating) | |
| minRating = ready.rating; | |
| } | |
| if (missingUser) { | |
| for (const ready of readies) { | |
| Users.get(ready.userid)?.popup(`Sorry, your opponent ${missingUser} went offline before your battle could start.`); | |
| } | |
| return void 0; | |
| } | |
| const format = Dex.formats.get(formatid); | |
| const delayedStart = format.playerCount > players.length ? "multi" : false; | |
| return Rooms.createBattle({ | |
| format: formatid, | |
| players, | |
| rated: minRating, | |
| challengeType: readies[0].challengeType, | |
| delayedStart | |
| }); | |
| } | |
| } | |
| function getLadder(formatid) { | |
| return new Ladder(formatid); | |
| } | |
| const periodicMatchInterval = setInterval( | |
| () => Ladder.periodicMatch(), | |
| PERIODIC_MATCH_INTERVAL | |
| ); | |
| const Ladders = Object.assign(getLadder, { | |
| BattleReady: import_ladders_challenges.BattleReady, | |
| LadderStore, | |
| Ladder, | |
| BattleChallenge: import_ladders_challenges.BattleChallenge, | |
| GameChallenge: import_ladders_challenges.GameChallenge, | |
| BattleInvite: import_ladders_challenges.BattleInvite, | |
| cancelSearches: Ladder.cancelSearches, | |
| updateSearch: Ladder.updateSearch, | |
| acceptChallenge: Ladder.acceptChallenge, | |
| visualizeAll: Ladder.visualizeAll, | |
| getSearches: Ladder.getSearches, | |
| match: Ladder.match, | |
| searches, | |
| challenges: import_ladders_challenges.challenges, | |
| periodicMatchInterval, | |
| // tells the client to ask the server for format information | |
| formatsListPrefix: LadderStore.formatsListPrefix, | |
| disabled: false | |
| }); | |
| //# sourceMappingURL=ladders.js.map | |