From 824014ca7bcdbb4c39f05ab2aa247976e9a7b33b Mon Sep 17 00:00:00 2001 From: Karthik <32044378+Karthik99999@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:15:44 -0700 Subject: [PATCH] Allow BestOfGames to be private (#10208) --- server/chat-commands/room-settings.ts | 13 ++--- server/room-battle-bestof.ts | 76 ++++++++++++++++++++++++--- server/rooms.ts | 27 +++++++--- 3 files changed, 98 insertions(+), 18 deletions(-) diff --git a/server/chat-commands/room-settings.ts b/server/chat-commands/room-settings.ts index 983e396683..40bf085b8c 100644 --- a/server/chat-commands/room-settings.ts +++ b/server/chat-commands/room-settings.ts @@ -1088,10 +1088,11 @@ export const commands: Chat.ChatCommands = { unlistroom: 'privateroom', privateroom(target, room, user, connection, cmd) { room = this.requireRoom(); - if (room.battle) { + const battle = room.battle || room.bestOf; + if (battle) { this.checkCan('editprivacy', null, room); - if (room.battle.forcedSettings.privacy) { - return this.errorReply(`This battle is required to be public because a player has a name prefixed by '${room.battle.forcedSettings.privacy}'.`); + if (battle.forcedSettings.privacy) { + return this.errorReply(`This battle is required to be public because a player has a name prefixed by '${battle.forcedSettings.privacy}'.`); } if (room.tour?.forcePublic) { return this.errorReply(`This battle can't be hidden, because the tournament is set to be forced public.`); @@ -1136,7 +1137,7 @@ export const commands: Chat.ChatCommands = { if (room.parent && room.parent.settings.isPrivate) { return this.errorReply(`This room's parent ${room.parent.title} must be public for this room to be public.`); } - if (room.settings.isPersonal && !room.battle) { + if (room.settings.isPersonal && !battle) { return this.errorReply(`This room can't be made public.`); } if (room.privacySetter && user.can('nooverride', null, room) && !user.can('makeroom')) { @@ -1156,7 +1157,7 @@ export const commands: Chat.ChatCommands = { room.setPrivate(false); } else { const settingName = (setting === true ? 'secret' : setting); - if (room.subRooms) { + if (room.subRooms && !room.bestOf) { if (settingName === 'secret') return this.errorReply("Secret rooms cannot have subrooms."); for (const subRoom of room.subRooms.values()) { if (!subRoom.settings.isPrivate) { @@ -1173,7 +1174,7 @@ export const commands: Chat.ChatCommands = { } this.addModAction(`${user.name} made this room ${settingName}.`); this.modlog(`${settingName.toUpperCase()}ROOM`); - if (!room.settings.isPersonal && !room.battle) room.setSection(); + if (!room.settings.isPersonal && !battle) room.setSection(); room.setPrivate(setting); room.privacySetter = new Set([user.id]); } diff --git a/server/room-battle-bestof.ts b/server/room-battle-bestof.ts index c48fcc41cc..ad9e8343f6 100644 --- a/server/room-battle-bestof.ts +++ b/server/room-battle-bestof.ts @@ -1,6 +1,7 @@ import {Utils} from '../lib'; import {RoomGamePlayer, RoomGame} from "./room-game"; import type {RoomBattlePlayerOptions, RoomBattleOptions} from './room-battle'; +import type {PrivacySetting, RoomSettings} from './rooms'; const BEST_OF_IN_BETWEEN_TIME = 40; @@ -67,7 +68,8 @@ export class BestOfGame extends RoomGame { bestOf: number; format: Format; winThreshold: number; - options: Omit & {players: null}; + options: Omit & {parent: Room, players: null}; + forcedSettings: {modchat?: string | null, privacy?: string | null} = {}; ties = 0; games: {room: GameRoom, winner: BestOfPlayer | null | undefined, rated: number}[] = []; playerNum = 0; @@ -89,9 +91,11 @@ export class BestOfGame extends RoomGame { if (!toID(this.title).includes('bestof')) { this.title += ` (Best-of-${this.bestOf})`; } + this.room.bestOf = this; this.options = { ...options, isBestOfSubBattle: true, + parent: this.room, allowRenames: false, players: null, }; @@ -114,6 +118,65 @@ export class BestOfGame extends RoomGame { this.room.auth.set(user.id, Users.PLAYER_SYMBOL); return player; } + checkPrivacySettings(options: RoomBattleOptions & Partial) { + let inviteOnly = false; + const privacySetter = new Set([]); + for (const p of options.players) { + if (p.user) { + if (p.inviteOnly) { + inviteOnly = true; + privacySetter.add(p.user.id); + } else if (p.hidden) { + privacySetter.add(p.user.id); + } + this.checkForcedUserSettings(p.user); + } + } + + if (privacySetter.size) { + const room = this.room; + if (this.forcedSettings.privacy) { + room.setPrivate(false); + room.settings.modjoin = null; + room.add(`|raw|
This best-of set is required to be public due to a player having a name starting with '${this.forcedSettings.privacy}'.
`); + } else if (!options.tour || (room.tour?.allowModjoin)) { + room.setPrivate('hidden'); + if (inviteOnly) room.settings.modjoin = '%'; + room.privacySetter = privacySetter; + if (inviteOnly) { + room.settings.modjoin = '%'; + room.add(`|raw|
This best-of set is invite-only!
Users must be invited with /invite (or be staff) to join
`); + } + } + } + } + checkForcedUserSettings(user: User) { + this.forcedSettings = { + modchat: this.forcedSettings.modchat || Rooms.RoomBattle.battleForcedSetting(user, 'modchat'), + privacy: this.forcedSettings.privacy || Rooms.RoomBattle.battleForcedSetting(user, 'privacy'), + }; + if ( + this.players.some(p => p.getUser()?.battleSettings.special) || + (this.options.rated && this.forcedSettings.modchat) + ) { + this.room.settings.modchat = '\u2606'; + } + } + setPrivacyOfGames(privacy: PrivacySetting) { + for (let i = 0; i < this.games.length; i++) { + const room = this.games[i].room; + const prevRoom = this.games[i - 1]?.room; + const gameNum = i + 1; + + room.setPrivate(privacy); + this.room.add(`|uhtmlchange|game${gameNum}|${room.title}`); + room.add(`|uhtmlchange|bestof|

Game ${gameNum} of a best-of-${this.bestOf}

`).update(); + if (prevRoom) { + prevRoom.add(`|uhtmlchange|next|Next: Game ${gameNum} of ${this.bestOf}`).update(); + } + } + this.updateDisplay(); + } clearWaiting() { this.waitingBattle = null; for (const player of this.players) { @@ -159,7 +222,6 @@ export class BestOfGame extends RoomGame { const battleRoom = Rooms.createBattle(options); // shouldn't happen even in lockdown if (!battleRoom) throw new Error("Failed to create battle for " + this.title); - battleRoom.setParent(this.room); this.games.push({ room: battleRoom, winner: undefined, @@ -175,16 +237,18 @@ export class BestOfGame extends RoomGame { const p2 = this.players[1]; battleRoom.add( Utils.html`|html|` + - `
${p1.name}${p2.name}
${this.renderWins(p1)}${this.renderWins(p2)}
` + - `

Game ${gameNum} of a best-of-${this.bestOf}

` + `${this.renderWins(p1)}${this.renderWins(p2)}` + ); + battleRoom.add( + `|uhtml|bestof|

Game ${gameNum} of a best-of-${this.bestOf}

` ).update(); this.room.add(`|html|

Game ${gameNum}

`); - this.room.add(Utils.html`|html|${battleRoom.title}`); + this.room.add(Utils.html`|uhtml|game${gameNum}|${battleRoom.title}`); this.updateDisplay(); prevBattleRoom?.add( - `|html|Next: Game ${gameNum} of ${this.bestOf}` + `|uhtml|next|Next: Game ${gameNum} of ${this.bestOf}` ).update(); } renderWins(player: BestOfPlayer) { diff --git a/server/rooms.ts b/server/rooms.ts index 4484ba4e4d..392310103b 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -178,6 +178,12 @@ export abstract class BasicRoom { * In all other rooms, `this.battle` is `null`. */ battle: RoomBattle | null; + /** + * The room's current best-of set. Best-of sets are a type of RoomGame, so in best-of set + * rooms (which can only be `GameRoom`s), `this.bestof === this.game`. + * In all other rooms, `this.bestof` is `null`. + */ + bestOf: BestOfGame | null; /** * The game room's current tournament. If the room is a battle room whose * battle is part of a tournament, `this.tour === this.parent.game`. @@ -234,6 +240,7 @@ export abstract class BasicRoom { this.muteQueue = []; this.battle = null; + this.bestOf = null; this.game = null; this.subGame = null; this.tour = null; @@ -824,7 +831,7 @@ export abstract class BasicRoom { } } - if (this.battle) { + if (this.battle || this.bestOf) { if (privacy) { if (this.roomid.endsWith('pw')) return true; @@ -843,6 +850,7 @@ export abstract class BasicRoom { this.rename(this.title, this.roomid.slice(0, lastDashIndex) as RoomID); } } + this.bestOf?.setPrivacyOfGames(privacy); } validateSection(section: string) { const target = toID(section); @@ -1889,6 +1897,7 @@ export class GameRoom extends BasicRoom { */ rated: number; declare battle: RoomBattle | null; + declare bestOf: BestOfGame | null; declare game: RoomGame; modchatUser: string; constructor(roomid: RoomID, title: string, options: Partial) { @@ -1915,6 +1924,7 @@ export class GameRoom extends BasicRoom { this.rated = options.rated === true ? 1 : options.rated || 0; this.battle = null; + this.bestOf = null; this.game = null!; this.modchatUser = ''; @@ -2113,7 +2123,7 @@ export class GameRoom extends BasicRoom { function getRoom(roomid?: string | BasicRoom) { if (typeof roomid === 'string') { // Accounts for private battles that were made public - if (roomid.startsWith('battle-') && roomid.endsWith('pw')) { + if ((roomid.startsWith('battle-') || roomid.startsWith('game-bestof')) && roomid.endsWith('pw')) { const room = Rooms.rooms.get(roomid.slice(0, roomid.lastIndexOf('-')) as RoomID); if (room) return room; } @@ -2229,12 +2239,17 @@ export const Rooms = { roomid ||= Rooms.global.prepBattleRoom(options.format); options.isPersonal = true; const room = Rooms.createGameRoom(roomid, roomTitle, options); + let game: RoomBattle | BestOfGame; if (options.isBestOfSubBattle || !isBestOf) { - const battle = new Rooms.RoomBattle(room, options); - room.game = battle; - battle.checkPrivacySettings(options); + game = new RoomBattle(room, options); } else { - room.game = new BestOfGame(room, options); + game = new BestOfGame(room, options); + } + room.game = game; + if (options.isBestOfSubBattle && room.parent) { + room.setPrivate(room.parent.settings.isPrivate || false); + } else { + game.checkPrivacySettings(options); } for (const p of players) {