Allow BestOfGames to be private (#10208)

This commit is contained in:
Karthik
2024-03-21 19:15:44 -07:00
committed by GitHub
parent 559968c9d2
commit 824014ca7b
3 changed files with 98 additions and 18 deletions

View File

@@ -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]);
}

View File

@@ -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<BestOfPlayer> {
bestOf: number;
format: Format;
winThreshold: number;
options: Omit<RoomBattleOptions, 'players'> & {players: null};
options: Omit<RoomBattleOptions, 'players'> & {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<BestOfPlayer> {
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<BestOfPlayer> {
this.room.auth.set(user.id, Users.PLAYER_SYMBOL);
return player;
}
checkPrivacySettings(options: RoomBattleOptions & Partial<RoomSettings>) {
let inviteOnly = false;
const privacySetter = new Set<ID>([]);
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|<div class="broadcast-blue"><strong>This best-of set is required to be public due to a player having a name starting with '${this.forcedSettings.privacy}'.</div>`);
} 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|<div class="broadcast-red"><strong>This best-of set is invite-only!</strong><br />Users must be invited with <code>/invite</code> (or be staff) to join</div>`);
}
}
}
}
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}|<a href="/${room.roomid}">${room.title}</a>`);
room.add(`|uhtmlchange|bestof|<h2><strong>Game ${gameNum}</strong> of <a href="/${this.roomid}">a best-of-${this.bestOf}</a></h2>`).update();
if (prevRoom) {
prevRoom.add(`|uhtmlchange|next|Next: <a href="/${room.roomid}"><strong>Game ${gameNum} of ${this.bestOf}</strong></a>`).update();
}
}
this.updateDisplay();
}
clearWaiting() {
this.waitingBattle = null;
for (const player of this.players) {
@@ -159,7 +222,6 @@ export class BestOfGame extends RoomGame<BestOfPlayer> {
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<BestOfPlayer> {
const p2 = this.players[1];
battleRoom.add(
Utils.html`|html|<table width="100%"><tr><td align="left">${p1.name}</td><td align="right">${p2.name}</tr>` +
`<tr><td align="left">${this.renderWins(p1)}</td><td align="right">${this.renderWins(p2)}</tr></table>` +
`<h2><strong>Game ${gameNum}</strong> of <a href="/${this.roomid}">a best-of-${this.bestOf}</a></h2>`
`<tr><td align="left">${this.renderWins(p1)}</td><td align="right">${this.renderWins(p2)}</tr></table>`
);
battleRoom.add(
`|uhtml|bestof|<h2><strong>Game ${gameNum}</strong> of <a href="/${this.roomid}">a best-of-${this.bestOf}</a></h2>`
).update();
this.room.add(`|html|<h2>Game ${gameNum}</h2>`);
this.room.add(Utils.html`|html|<a href="/${battleRoom.roomid}">${battleRoom.title}</a>`);
this.room.add(Utils.html`|uhtml|game${gameNum}|<a href="/${battleRoom.roomid}">${battleRoom.title}</a>`);
this.updateDisplay();
prevBattleRoom?.add(
`|html|Next: <a href="/${battleRoom.roomid}"><strong>Game ${gameNum} of ${this.bestOf}</strong></a>`
`|uhtml|next|Next: <a href="/${battleRoom.roomid}"><strong>Game ${gameNum} of ${this.bestOf}</strong></a>`
).update();
}
renderWins(player: BestOfPlayer) {

View File

@@ -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<RoomSettings & RoomBattleOptions>) {
@@ -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) {