From d0486fa4f7bcfb67654809a74346d88f63483a1a Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sat, 3 Apr 2021 14:45:39 -0500 Subject: [PATCH 1/9] Support searching multi formats --- config/formats.ts | 1 - server/chat-commands/core.ts | 58 ++++++++++++++++++++++++++++++++++++ server/ladders-challenges.ts | 8 +++++ server/ladders.ts | 27 +++++++++++++++-- server/rooms.ts | 3 ++ server/users.ts | 2 ++ 6 files changed, 96 insertions(+), 3 deletions(-) diff --git a/config/formats.ts b/config/formats.ts index c0a847f39c84..ae7472bbb8e9 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -68,7 +68,6 @@ export const Formats: FormatList = [ mod: 'gen9', team: 'random', gameType: 'multi', - searchShow: false, tournamentShow: false, rated: false, ruleset: [ diff --git a/server/chat-commands/core.ts b/server/chat-commands/core.ts index ee6a9b46cbe0..b1f7aceb3ed8 100644 --- a/server/chat-commands/core.ts +++ b/server/chat-commands/core.ts @@ -1420,6 +1420,64 @@ export const commands: Chat.ChatCommands = { `If no format is given, cancels searches for all formats.`, ], + requestpartner(target, room, user) { + target = this.splitTarget(target); + const targetUser = this.targetUser; + if (!targetUser) return this.popupReply(`User not found.`); + if (targetUser.locked && !user.locked) { + return this.popupReply(`That user is locked and cannot be invited to battles.`); + } + if (user.locked && !targetUser.locked) { + return this.errorReply(`You are locked and cannot invite others to battles.`); + } + const format = Dex.getFormat(target); + if (!format.exists) return this.popupReply(`Invalid format: ${target}`); + if (format.gameType !== 'multi') { + return this.popupReply(`You cannot invite people to non-multibattle formats. Challenge them instead.`); + } + + let requestMap = Ladders.requests.get(targetUser.id); + if (!requestMap) { + requestMap = new Map(); + Ladders.requests.set(targetUser.id, requestMap); + } + requestMap.set(user.id, format.id); + targetUser.send(`|requestmulti|${user.name}|${format.id}`); + this.connection.send( + `|pm|${user.getIdentity()}|${targetUser.getIdentity()}|/text` + + `You invited ${targetUser.name} to join you in ${format.name}.` + ); + }, + async acceptpartner(target, room, user, connection) { + target = this.splitTarget(target); + const targetUser = this.targetUser; + if (!targetUser) return this.popupReply(`User not found.`); + const reqs = Ladders.requests.get(user.id); + if (!reqs) return this.popupReply(`You have no battle requests pending.`); + const formatid = reqs.get(targetUser.id); + if (!formatid) return this.popupReply(`You have no request pending from that user.`); + const format = Dex.getFormat(formatid); + const search = await Ladders(formatid).prepBattle(connection, 'rated', user.battleSettings.team, !!format.rated, true); + if (search === null) return null; + targetUser.battleSettings.teammate = search; + targetUser.chat(`/search ${formatid}`, null, targetUser.connections[0]); + targetUser.popup(`Your teammate has accepted, and a battle search has been started.`); + }, + denypartner(target, room, user) { + target = this.splitTarget(target); + const targetUser = this.targetUser; + if (!targetUser) return this.popupReply(`User not found.`); + const reqs = Ladders.requests.get(user.id); + if (!reqs) return this.popupReply(`You have no pending teammate requests.`); + if (!reqs.has(targetUser.id)) { + return this.popupReply(`That user has not sent you a teammate request.`); + } + reqs.delete(targetUser.id); + if (!reqs.size) Ladders.requests.delete(user.id); + this.popupReply(`Request denied.`); + targetUser.popup(`${user.id} denied your teammate request.`); + }, + chall: 'challenge', challenge(target, room, user, connection) { const {targetUser, targetUsername, rest: formatName} = this.splitUser(target); diff --git a/server/ladders-challenges.ts b/server/ladders-challenges.ts index b075a76524a2..f1c96a05f67c 100644 --- a/server/ladders-challenges.ts +++ b/server/ladders-challenges.ts @@ -30,6 +30,14 @@ export class BattleReady { this.challengeType = challengeType; this.time = Date.now(); } + static averageRatings(readies: BattleReady[]) { + const combined = readies.map(r => r.rating).reduce((cur, prev) => cur + prev); + const newRating = combined / readies.length; + for (const ready of readies) { + (ready as any).rating = newRating; + } + return newRating; + } } export abstract class AbstractChallenge { diff --git a/server/ladders.ts b/server/ladders.ts index 2c7cb33207d5..9346ca28da5d 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -26,6 +26,10 @@ const searches = new Map, }>(); +/** + * Map> + */ +const requests = new Map>(); /** * This keeps track of searches for battles, creating a new battle for a newly @@ -37,7 +41,13 @@ class Ladder extends LadderStore { super(formatid); } - async prepBattle(connection: Connection, challengeType: ChallengeType, team: string | null = null, isRated = false) { + async prepBattle( + connection: Connection, + challengeType: ChallengeType, + team: string | null = null, + isRated = false, + noPartner = false + ) { // all validation for a battle goes through here const user = connection.user; const userid = user.id; @@ -137,6 +147,13 @@ class Ladder extends LadderStore { return null; } + if (Dex.getFormat(this.formatid).gameType === 'multi' && !noPartner) { + if (!user.battleSettings.teammate) { + connection.popup(`You must have a teammate consent to play with you before playing this tier.`); + return null; + } + } + const settings = {...user.battleSettings, team: valResult.slice(1)}; user.battleSettings.inviteOnly = false; user.battleSettings.hidden = false; @@ -329,6 +346,9 @@ class Ladder extends LadderStore { if (oldUserid !== user.id) return; if (!search) return; + if (user.battleSettings.teammate) { + BattleReady.averageRatings([search, user.battleSettings.teammate]); + } this.addSearch(search, user); } @@ -422,7 +442,6 @@ class Ladder extends LadderStore { static periodicMatch() { // In order from longest waiting to shortest waiting for (const [formatid, formatTable] of Ladders.searches) { - if (formatTable.playerCount > 2) continue; // TODO: implement const matchmaker = Ladders(formatid); let longest: [BattleReady, User] | null = null; for (const search of formatTable.searches.values()) { @@ -459,6 +478,9 @@ class Ladder extends LadderStore { missingUser = ready.userid; break; } + if (user.battleSettings.teammate) { + readies.push(user.battleSettings.teammate); + } players.push({ user, team: ready.settings.team, @@ -513,6 +535,7 @@ export const Ladders = Object.assign(getLadder, { searches, challenges, + requests, periodicMatchInterval, // tells the client to ask the server for format information diff --git a/server/rooms.ts b/server/rooms.ts index 39f8429a339d..1a30c6ee62c1 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -1474,6 +1474,9 @@ export class GlobalRoomState { if (level === 50) displayCode |= 16; // 32 was previously used for Multi Battles if (format.bestOfDefault) displayCode |= 64; + if (format.gameType === 'multi') { + displayCode |= 32; + } this.formatList += ',' + displayCode.toString(16); } return this.formatList; diff --git a/server/users.ts b/server/users.ts index abc54fa164bf..401086b8dfb5 100644 --- a/server/users.ts +++ b/server/users.ts @@ -49,6 +49,7 @@ import {FS, Utils, ProcessManager} from '../lib'; import { Auth, GlobalAuth, SECTIONLEADER_SYMBOL, PLAYER_SYMBOL, HOST_SYMBOL, RoomPermission, GlobalPermission, } from './user-groups'; +import {BattleReady} from './ladders-challenges'; const MINUTES = 60 * 1000; const IDLE_TIMER = 60 * MINUTES; @@ -385,6 +386,7 @@ export class User extends Chat.MessageContext { hidden: boolean, inviteOnly: boolean, special?: string, + teammate?: BattleReady, }; isSysop: boolean; From e18e40e26f5662c8978f3d5d734c899a99af6dd3 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Mon, 2 Aug 2021 17:51:13 -0500 Subject: [PATCH 2/9] Add support for latest challenge protocol --- server/chat-commands/core.ts | 59 ++++++++++++++---------------------- server/ladders-challenges.ts | 6 ++-- server/ladders.ts | 7 +---- 3 files changed, 26 insertions(+), 46 deletions(-) diff --git a/server/chat-commands/core.ts b/server/chat-commands/core.ts index b1f7aceb3ed8..2892a634f533 100644 --- a/server/chat-commands/core.ts +++ b/server/chat-commands/core.ts @@ -1421,59 +1421,46 @@ export const commands: Chat.ChatCommands = { ], requestpartner(target, room, user) { - target = this.splitTarget(target); - const targetUser = this.targetUser; - if (!targetUser) return this.popupReply(`User not found.`); + const {targetUser, rest} = this.requireUser(target); if (targetUser.locked && !user.locked) { return this.popupReply(`That user is locked and cannot be invited to battles.`); } if (user.locked && !targetUser.locked) { return this.errorReply(`You are locked and cannot invite others to battles.`); } - const format = Dex.getFormat(target); - if (!format.exists) return this.popupReply(`Invalid format: ${target}`); + const format = Dex.formats.get(rest); + if (!format.exists) return this.popupReply(`Invalid format: ${rest}`); if (format.gameType !== 'multi') { return this.popupReply(`You cannot invite people to non-multibattle formats. Challenge them instead.`); } - - let requestMap = Ladders.requests.get(targetUser.id); - if (!requestMap) { - requestMap = new Map(); - Ladders.requests.set(targetUser.id, requestMap); - } - requestMap.set(user.id, format.id); - targetUser.send(`|requestmulti|${user.name}|${format.id}`); - this.connection.send( - `|pm|${user.getIdentity()}|${targetUser.getIdentity()}|/text` + - `You invited ${targetUser.name} to join you in ${format.name}.` - ); + Ladders.challenges.add(new Ladders.GameChallenge(user.id, targetUser.id, format.id, { + acceptCommand: `/acceptpartner ${user.id}`, + rejectCommand: `/denypartner ${user.id}`, + message: `${user.name} wants you to play ${format.name} with them!`, + })); }, async acceptpartner(target, room, user, connection) { - target = this.splitTarget(target); - const targetUser = this.targetUser; - if (!targetUser) return this.popupReply(`User not found.`); - const reqs = Ladders.requests.get(user.id); - if (!reqs) return this.popupReply(`You have no battle requests pending.`); - const formatid = reqs.get(targetUser.id); - if (!formatid) return this.popupReply(`You have no request pending from that user.`); - const format = Dex.getFormat(formatid); - const search = await Ladders(formatid).prepBattle(connection, 'rated', user.battleSettings.team, !!format.rated, true); + const challenge = Ladders.challenges.resolveAcceptCommand(this); + Ladders.challenges.remove(challenge, true); + const format = Dex.formats.get(challenge.format); + const targetUser = Users.get(challenge.from); + if (!targetUser) return this.popupReply(`${challenge.from} is not available right now.`); + const search = await Ladders(format.id).prepBattle(connection, 'rated', user.battleSettings.team, !!format.rated, true); if (search === null) return null; targetUser.battleSettings.teammate = search; - targetUser.chat(`/search ${formatid}`, null, targetUser.connections[0]); + const latestConn = Utils.sortBy(targetUser.connections.slice(), b => -b.lastActiveTime)[0]; + targetUser.chat(`/search ${format.id}`, null, latestConn); targetUser.popup(`Your teammate has accepted, and a battle search has been started.`); + this.pmTarget = targetUser; + this.sendReply(`You accepted ${targetUser.name}'s partnership request!`); + user.send(`|updatesearch|${JSON.stringify({searching: [format.id], games: null})}`); }, denypartner(target, room, user) { - target = this.splitTarget(target); - const targetUser = this.targetUser; + const {targetUser} = this.splitUser(target); if (!targetUser) return this.popupReply(`User not found.`); - const reqs = Ladders.requests.get(user.id); - if (!reqs) return this.popupReply(`You have no pending teammate requests.`); - if (!reqs.has(targetUser.id)) { - return this.popupReply(`That user has not sent you a teammate request.`); - } - reqs.delete(targetUser.id); - if (!reqs.size) Ladders.requests.delete(user.id); + const chall = Ladders.challenges.search(user.id, targetUser.id); + if (!chall) return this.popupReply(`Challenge not found between you and ${targetUser.name}`); + Ladders.challenges.remove(chall, false); this.popupReply(`Request denied.`); targetUser.popup(`${user.id} denied your teammate request.`); }, diff --git a/server/ladders-challenges.ts b/server/ladders-challenges.ts index f1c96a05f67c..9aaea89e96bd 100644 --- a/server/ladders-challenges.ts +++ b/server/ladders-challenges.ts @@ -31,12 +31,10 @@ export class BattleReady { this.time = Date.now(); } static averageRatings(readies: BattleReady[]) { - const combined = readies.map(r => r.rating).reduce((cur, prev) => cur + prev); - const newRating = combined / readies.length; + const average = Math.round(readies.map(r => r.rating).reduce((a, b) => a + b) / readies.length); for (const ready of readies) { - (ready as any).rating = newRating; + (ready as any).rating = average; } - return newRating; } } diff --git a/server/ladders.ts b/server/ladders.ts index 9346ca28da5d..c9188e56c5c5 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -26,10 +26,6 @@ const searches = new Map, }>(); -/** - * Map> - */ -const requests = new Map>(); /** * This keeps track of searches for battles, creating a new battle for a newly @@ -147,7 +143,7 @@ class Ladder extends LadderStore { return null; } - if (Dex.getFormat(this.formatid).gameType === 'multi' && !noPartner) { + if (Dex.formats.get(this.formatid).gameType === 'multi' && !noPartner) { if (!user.battleSettings.teammate) { connection.popup(`You must have a teammate consent to play with you before playing this tier.`); return null; @@ -535,7 +531,6 @@ export const Ladders = Object.assign(getLadder, { searches, challenges, - requests, periodicMatchInterval, // tells the client to ask the server for format information From 7f45513a3b98348fdb3810d631b2a7fd9f7571d3 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Wed, 1 May 2024 11:42:51 -0500 Subject: [PATCH 3/9] Fixes --- server/ladders.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/ladders.ts b/server/ladders.ts index c9188e56c5c5..756413a6e531 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -145,7 +145,9 @@ class Ladder extends LadderStore { if (Dex.formats.get(this.formatid).gameType === 'multi' && !noPartner) { if (!user.battleSettings.teammate) { - connection.popup(`You must have a teammate consent to play with you before playing this tier.`); + connection.popup( + `You must have a teammate consent to play with you before playing this tier. Just fill out their name in the box and hit enter.` + ); return null; } } @@ -263,6 +265,9 @@ class Ladder extends LadderStore { formatTable.searches.delete(user.id); cancelCount++; } + if (user.battleSettings.teammate) { + delete user.battleSettings.teammate; + } Ladder.updateSearch(user); return cancelCount; @@ -476,6 +481,7 @@ class Ladder extends LadderStore { } if (user.battleSettings.teammate) { readies.push(user.battleSettings.teammate); + delete user.battleSettings.teammate; } players.push({ user, From 9ec2253c5659dddc3bd9a05d8302174d2741773c Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Wed, 1 May 2024 21:22:25 -0500 Subject: [PATCH 4/9] Clear partner search UI on cancel --- server/ladders.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/ladders.ts b/server/ladders.ts index 756413a6e531..74860e61bbb8 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -266,6 +266,8 @@ class Ladder extends LadderStore { cancelCount++; } if (user.battleSettings.teammate) { + const partner = Users.get(user.battleSettings.teammate.userid); + if (partner) Ladder.updateSearch(partner); delete user.battleSettings.teammate; } From 1b4722f2f7a217c7e1f0f3e6f02da5b4995e0ded Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Wed, 1 May 2024 21:30:20 -0500 Subject: [PATCH 5/9] Don't clear partner actually --- server/ladders.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/ladders.ts b/server/ladders.ts index 74860e61bbb8..38ac8620a94a 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -268,7 +268,6 @@ class Ladder extends LadderStore { if (user.battleSettings.teammate) { const partner = Users.get(user.battleSettings.teammate.userid); if (partner) Ladder.updateSearch(partner); - delete user.battleSettings.teammate; } Ladder.updateSearch(user); From 7b5f44b0021d6d64e5ebb1c6b080998167e81a4b Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Wed, 1 May 2024 21:34:14 -0500 Subject: [PATCH 6/9] Let's try this? --- server/ladders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ladders.ts b/server/ladders.ts index 38ac8620a94a..d8abfeb50a4d 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -267,7 +267,7 @@ class Ladder extends LadderStore { } if (user.battleSettings.teammate) { const partner = Users.get(user.battleSettings.teammate.userid); - if (partner) Ladder.updateSearch(partner); + if (partner) Ladder.cancelSearches(partner); } Ladder.updateSearch(user); From 21b45544f94565b9510a0764c61c3d7d1e6b52e9 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Wed, 1 May 2024 21:36:57 -0500 Subject: [PATCH 7/9] hack this --- server/ladders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ladders.ts b/server/ladders.ts index d8abfeb50a4d..b76bd0e4cc7f 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -267,7 +267,7 @@ class Ladder extends LadderStore { } if (user.battleSettings.teammate) { const partner = Users.get(user.battleSettings.teammate.userid); - if (partner) Ladder.cancelSearches(partner); + if (partner) partner.send(`|updatesearch|${JSON.stringify({searching: [], games: null})}`); } Ladder.updateSearch(user); From 74ce64ca9d06e3ec2c781cc30a359568a01a9992 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Wed, 1 May 2024 21:43:05 -0500 Subject: [PATCH 8/9] unhack actually --- server/ladders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ladders.ts b/server/ladders.ts index b76bd0e4cc7f..38ac8620a94a 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -267,7 +267,7 @@ class Ladder extends LadderStore { } if (user.battleSettings.teammate) { const partner = Users.get(user.battleSettings.teammate.userid); - if (partner) partner.send(`|updatesearch|${JSON.stringify({searching: [], games: null})}`); + if (partner) Ladder.updateSearch(partner); } Ladder.updateSearch(user); From 4c39e01bf3c969e19696b68748679355b3a295ce Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Wed, 1 May 2024 21:52:30 -0500 Subject: [PATCH 9/9] can't partner yourself --- server/chat-commands/core.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/chat-commands/core.ts b/server/chat-commands/core.ts index 2892a634f533..f1d927f4d7df 100644 --- a/server/chat-commands/core.ts +++ b/server/chat-commands/core.ts @@ -1425,6 +1425,9 @@ export const commands: Chat.ChatCommands = { if (targetUser.locked && !user.locked) { return this.popupReply(`That user is locked and cannot be invited to battles.`); } + if (targetUser.id === user.id) { + return this.popupReply(`You cannot be your own partner.`); + } if (user.locked && !targetUser.locked) { return this.errorReply(`You are locked and cannot invite others to battles.`); }