From 96d7bca9af493b0de89a1295d4bf1fedd48979d2 Mon Sep 17 00:00:00 2001 From: InfinityLoop Date: Mon, 11 May 2026 16:04:26 +0000 Subject: [PATCH 1/4] anti bot --- package-lock.json | 12 ++--- .../multiplayer/ServerMultiplayerRoomMixin.js | 38 +++++++++----- server/multiplayer/ServerPlayer.js | 10 ++++ server/multiplayer/ServerTossupBonusRoom.js | 26 +++++++++- .../multiplayer/configure-permanent-room.js | 52 +++++++++++++++++++ server/multiplayer/handle-wss-connection.js | 9 +++- 6 files changed, 123 insertions(+), 24 deletions(-) create mode 100644 server/multiplayer/configure-permanent-room.js diff --git a/package-lock.json b/package-lock.json index f7ab7e35f..f51481ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3332,9 +3332,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -9529,9 +9529,9 @@ "dev": true }, "fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true }, "fastest-levenshtein": { diff --git a/server/multiplayer/ServerMultiplayerRoomMixin.js b/server/multiplayer/ServerMultiplayerRoomMixin.js index e73596134..cc1cf5785 100644 --- a/server/multiplayer/ServerMultiplayerRoomMixin.js +++ b/server/multiplayer/ServerMultiplayerRoomMixin.js @@ -273,46 +273,51 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { } setCategories ({ userId, username }, { categories, subcategories, alternateSubcategories, percentView, categoryPercents }) { - if (this.isPermanent || !this.allowed(userId)) { return; } + if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } super.setCategories({ userId, username }, { categories, subcategories, alternateSubcategories, percentView, categoryPercents }); } + setDifficulties ({ userId, username }, { difficulties }) { + if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } + super.setDifficulties({ userId, username }, { difficulties }); + } + setMode ({ userId, username }, { mode }) { - if (this.isPermanent || !this.allowed(userId)) { return; } + if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } if (this.mode !== MODE_ENUM.SET_NAME && this.mode !== MODE_ENUM.RANDOM) { return; } super.setMode({ userId, username }, { mode }); this.adjustQuery(['setName'], [this.query.setName]); } setPacketNumbers ({ userId, username }, { packetNumbers }) { - if (this.isPermanent || !this.allowed(userId)) { return; } + if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } super.setPacketNumbers({ userId, username }, { doNotFetch: false, packetNumbers }); } setReadingSpeed ({ userId, username }, { readingSpeed }) { - if (this.isPermanent || !this.allowed(userId)) { return false; } + if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return false; } super.setReadingSpeed({ userId, username }, { readingSpeed }); } async setSetName ({ userId, username }, { setName }) { - if (!this.allowed(userId)) { return; } + if (this.settings.controlled || !this.allowed(userId)) { return; } if (!this.packetList) { return; } if (!this.packetList.includes(setName)) { return; } super.setSetName({ userId, username }, { doNotFetch: false, setName }); } setStrictness ({ userId, username }, { strictness }) { - if (this.isPermanent || !this.allowed(userId)) { return; } + if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } super.setStrictness({ userId, username }, { strictness }); } setMinYear ({ userId, username }, { minYear }) { - if (this.isPermanent || !this.allowed(userId)) { return; } + if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } super.setMinYear({ userId, username }, { minYear }); } setMaxYear ({ userId, username }, { maxYear }) { - if (this.isPermanent || !this.allowed(userId)) { return; } + if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } super.setMaxYear({ userId, username }, { maxYear }); } @@ -341,7 +346,7 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { } toggleEnableBonuses ({ userId, username }, { enableBonuses }) { - if (this.isPermanent || !this.allowed(userId)) { return; } + if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } super.toggleEnableBonuses({ userId, username }, { enableBonuses }); } @@ -363,20 +368,25 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { } togglePowermarkOnly ({ userId, username }, { powermarkOnly }) { - if (!this.allowed(userId)) { return; } + if (this.settings.controlled || !this.allowed(userId)) { return; } super.togglePowermarkOnly({ userId, username }, { powermarkOnly }); } toggleSkip ({ userId, username }, { skip }) { - if (!this.allowed(userId)) { return; } + if (this.settings.controlled || !this.allowed(userId)) { return; } super.toggleSkip({ userId, username }, { skip }); } toggleStandardOnly ({ userId, username }, { standardOnly }) { - if (!this.allowed(userId)) { return; } + if (this.settings.controlled || !this.allowed(userId)) { return; } super.toggleStandardOnly({ userId, username }, { doNotFetch: false, standardOnly }); } + toggleStopOnPower ({ userId, username }, { stopOnPower }) { + if (this.settings.controlled || !this.allowed(userId)) { return; } + super.toggleStopOnPower({ userId, username }, { stopOnPower }); + } + togglePublic ({ userId, username }, { public: isPublic }) { if (this.isPermanent || this.settings.controlled) { return; } this.settings.public = isPublic; @@ -389,12 +399,12 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { } toggleRebuzz ({ userId, username }, { rebuzz }) { - if (!this.allowed(userId)) { return false; } + if (this.settings.controlled || !this.allowed(userId)) { return false; } super.toggleRebuzz({ userId, username }, { rebuzz }); } toggleTimer ({ userId, username }, { timer }) { - if (this.settings.public || !this.allowed(userId)) { return; } + if (this.settings.public || this.settings.controlled || !this.allowed(userId)) { return; } super.toggleTimer({ userId, username }, { timer }); } diff --git a/server/multiplayer/ServerPlayer.js b/server/multiplayer/ServerPlayer.js index 54ab67d05..0e0590fb0 100644 --- a/server/multiplayer/ServerPlayer.js +++ b/server/multiplayer/ServerPlayer.js @@ -5,5 +5,15 @@ export default class ServerPlayer extends Player { constructor (userId) { super(userId, USERNAME_MAX_LENGTH); this.online = true; + this.consecutiveEarlyCorrect = 0; + } + + resetBotDetectionCounter () { + this.consecutiveEarlyCorrect = 0; + } + + recordEarlyCorrect () { + this.consecutiveEarlyCorrect++; + return this.consecutiveEarlyCorrect >= 3; } } diff --git a/server/multiplayer/ServerTossupBonusRoom.js b/server/multiplayer/ServerTossupBonusRoom.js index 9610b60c7..6d6f91285 100644 --- a/server/multiplayer/ServerTossupBonusRoom.js +++ b/server/multiplayer/ServerTossupBonusRoom.js @@ -5,10 +5,10 @@ import { QUESTION_TYPE_ENUM, TOSSUP_PROGRESS_ENUM } from '../../quizbowl/constan export default class ServerTossupBonusRoom extends ServerMultiplayerRoomMixin(TossupBonusRoom) { constructor (name, ownerId, isPermanent, categoryManager, isVerified = false) { super(name, ownerId, isPermanent, categoryManager, ['tossups', 'bonuses'], isVerified); + this.EARLY_CORRECT_CELERITY_THRESHOLD = 0.85; } giveAnswerLiveUpdate ({ userId, username }, { givenAnswer }) { - // Allow live updates during bonuses (when buzzedIn is null) or from the user who buzzed switch (this.currentQuestionType) { case QUESTION_TYPE_ENUM.TOSSUP: if (userId !== this.buzzedIn) { return false; } @@ -20,8 +20,30 @@ export default class ServerTossupBonusRoom extends ServerMultiplayerRoomMixin(To super.giveAnswerLiveUpdate({ userId, username }, { givenAnswer }); } + giveTossupAnswer ({ userId, username }, { givenAnswer }) { + if (typeof givenAnswer !== 'string') { return false; } + if (this.buzzedIn !== userId) { return false; } + + if (Object.keys(this.tossup || {}).length === 0) { return; } + + const { celerity, directive } = this.scoreTossup({ givenAnswer }); + + if (directive === 'accept' && celerity >= this.EARLY_CORRECT_CELERITY_THRESHOLD) { + const shouldKick = this.players[userId].recordEarlyCorrect(); + if (shouldKick) { + console.log(`Bot detected: User ${userId} (${username}) got 3 correct with abnormally high celerity. Kicking.`); + this.emitMessage({ type: 'bot-kicked', userId, username }); + setTimeout(() => this.closeConnection({ userId, username }), 100); + return; + } + } else { + this.players[userId].resetBotDetectionCounter(); + } + + super.giveTossupAnswer({ userId, username }, { givenAnswer }); + } + next ({ userId, username }) { - // prevents spam-skipping trolls if ( this.currentQuestionType === QUESTION_TYPE_ENUM.TOSSUP && this.tossupProgress === TOSSUP_PROGRESS_ENUM.READING && diff --git a/server/multiplayer/configure-permanent-room.js b/server/multiplayer/configure-permanent-room.js new file mode 100644 index 000000000..ef61c1db9 --- /dev/null +++ b/server/multiplayer/configure-permanent-room.js @@ -0,0 +1,52 @@ +export function configurePermanentRoomSettings (room, roomName) { + room.settings.public = true; + room.settings.controlled = true; + + room.settings.skip = false; + room.settings.rebuzz = false; + + room.settings.timer = true; + + const difficultyConfig = getPermanentRoomDifficulty(roomName); + if (difficultyConfig) { + room.query.difficulties = difficultyConfig.difficulties; + if (room.adjustQuery) { + room.adjustQuery(['difficulties'], [difficultyConfig.difficulties]); + } + } +} + +function getPermanentRoomDifficulty (roomName) { + switch (roomName) { + case 'msquizbowl': + return { difficulties: [1] }; + case 'hsquizbowl': + return { difficulties: [2, 3, 4, 5] }; + case 'collegequizbowl': + return { difficulties: [6, 7, 8, 9] }; + case 'literature': + case 'history': + case 'science': + case 'fine-arts': + case 'rmpss': + case 'geography': + case 'pop-culture': + return { difficulties: [2, 3, 4, 5] }; + case 'verified-msquizbowl': + return { difficulties: [1] }; + case 'verified-hsquizbowl': + return { difficulties: [2, 3, 4, 5] }; + case 'verified-collegequizbowl': + return { difficulties: [6, 7, 8, 9] }; + case 'verified-literature': + case 'verified-history': + case 'verified-science': + case 'verified-fine-arts': + case 'verified-rmpss': + case 'verified-geography': + case 'verified-pop-culture': + return { difficulties: [2, 3, 4, 5] }; + default: + return null; + } +} diff --git a/server/multiplayer/handle-wss-connection.js b/server/multiplayer/handle-wss-connection.js index 9d82b31d6..b2b59e82e 100644 --- a/server/multiplayer/handle-wss-connection.js +++ b/server/multiplayer/handle-wss-connection.js @@ -1,5 +1,6 @@ import { MAX_ONLINE_PLAYERS, MAX_CONNECTIONS_PER_IP, PERMANENT_ROOMS, VERIFIED_ROOMS, ROOM_NAME_MAX_LENGTH } from './constants.js'; import ServerTossupBonusRoom from './ServerTossupBonusRoom.js'; +import { configurePermanentRoomSettings } from './configure-permanent-room.js'; import { checkToken } from '../authentication.js'; import CategoryManager from '../../quizbowl/category-manager.js'; import getRandomName from '../../quizbowl/get-random-name.js'; @@ -22,15 +23,19 @@ export const tossupBonusRooms = {}; const connectionsByIp = new Map(); for (const room of PERMANENT_ROOMS) { const { name, categories, subcategories } = room; - tossupBonusRooms[name] = new ServerTossupBonusRoom( + const permanentRoom = new ServerTossupBonusRoom( name, Symbol('unique permanent room owner'), true, new CategoryManager(categories, subcategories), false ); + configurePermanentRoomSettings(permanentRoom, name); + tossupBonusRooms[name] = permanentRoom; } for (const room of VERIFIED_ROOMS) { const { name, categories, subcategories } = room; - tossupBonusRooms[name] = new ServerTossupBonusRoom( + const verifiedRoom = new ServerTossupBonusRoom( name, Symbol('unique verified room owner'), true, new CategoryManager(categories, subcategories), true ); + configurePermanentRoomSettings(verifiedRoom, name); + tossupBonusRooms[name] = verifiedRoom; } /** From ca8edfbd18ef761eef8543f044353787318cc0b6 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Sun, 17 May 2026 17:29:04 -0400 Subject: [PATCH 2/4] better EARLY_CORRECT_CELERITY_THRESHOLD --- server/multiplayer/ServerTossupBonusRoom.js | 4 ++-- server/multiplayer/constants.js | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/multiplayer/ServerTossupBonusRoom.js b/server/multiplayer/ServerTossupBonusRoom.js index 6d6f91285..09d8d9994 100644 --- a/server/multiplayer/ServerTossupBonusRoom.js +++ b/server/multiplayer/ServerTossupBonusRoom.js @@ -1,3 +1,4 @@ +import { EARLY_CORRECT_CELERITY_THRESHOLD } from './constants.js'; import ServerMultiplayerRoomMixin from './ServerMultiplayerRoomMixin.js'; import TossupBonusRoom from '../../quizbowl/TossupBonusRoom.js'; import { QUESTION_TYPE_ENUM, TOSSUP_PROGRESS_ENUM } from '../../quizbowl/constants.js'; @@ -5,7 +6,6 @@ import { QUESTION_TYPE_ENUM, TOSSUP_PROGRESS_ENUM } from '../../quizbowl/constan export default class ServerTossupBonusRoom extends ServerMultiplayerRoomMixin(TossupBonusRoom) { constructor (name, ownerId, isPermanent, categoryManager, isVerified = false) { super(name, ownerId, isPermanent, categoryManager, ['tossups', 'bonuses'], isVerified); - this.EARLY_CORRECT_CELERITY_THRESHOLD = 0.85; } giveAnswerLiveUpdate ({ userId, username }, { givenAnswer }) { @@ -28,7 +28,7 @@ export default class ServerTossupBonusRoom extends ServerMultiplayerRoomMixin(To const { celerity, directive } = this.scoreTossup({ givenAnswer }); - if (directive === 'accept' && celerity >= this.EARLY_CORRECT_CELERITY_THRESHOLD) { + if (directive === 'accept' && celerity >= EARLY_CORRECT_CELERITY_THRESHOLD) { const shouldKick = this.players[userId].recordEarlyCorrect(); if (shouldKick) { console.log(`Bot detected: User ${userId} (${username}) got 3 correct with abnormally high celerity. Kicking.`); diff --git a/server/multiplayer/constants.js b/server/multiplayer/constants.js index 4b052ea33..957a82628 100644 --- a/server/multiplayer/constants.js +++ b/server/multiplayer/constants.js @@ -6,6 +6,8 @@ export const USERNAME_MAX_LENGTH = 32; export const MAX_ONLINE_PLAYERS = 500; export const MAX_CONNECTIONS_PER_IP = 50; +export const EARLY_CORRECT_CELERITY_THRESHOLD = 0.95; + /** * List of multiplayer permanent room names. */ From 3dee9b10377e4803e3326c2815960544ab0bdb9a Mon Sep 17 00:00:00 2001 From: InfinityLoop Date: Mon, 18 May 2026 14:54:29 +0000 Subject: [PATCH 3/4] fix --- .../multiplayer/ServerMultiplayerRoomMixin.js | 34 +++++++++---------- server/multiplayer/ServerTossupBonusRoom.js | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/server/multiplayer/ServerMultiplayerRoomMixin.js b/server/multiplayer/ServerMultiplayerRoomMixin.js index cc1cf5785..6097e70a2 100644 --- a/server/multiplayer/ServerMultiplayerRoomMixin.js +++ b/server/multiplayer/ServerMultiplayerRoomMixin.js @@ -273,51 +273,51 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { } setCategories ({ userId, username }, { categories, subcategories, alternateSubcategories, percentView, categoryPercents }) { - if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } + if (this.isPermanent || !this.allowed(userId)) { return; } super.setCategories({ userId, username }, { categories, subcategories, alternateSubcategories, percentView, categoryPercents }); } setDifficulties ({ userId, username }, { difficulties }) { - if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } + if (this.isPermanent || !this.allowed(userId)) { return; } super.setDifficulties({ userId, username }, { difficulties }); } setMode ({ userId, username }, { mode }) { - if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } + if (this.isPermanent || !this.allowed(userId)) { return; } if (this.mode !== MODE_ENUM.SET_NAME && this.mode !== MODE_ENUM.RANDOM) { return; } super.setMode({ userId, username }, { mode }); this.adjustQuery(['setName'], [this.query.setName]); } setPacketNumbers ({ userId, username }, { packetNumbers }) { - if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } + if (this.isPermanent || !this.allowed(userId)) { return; } super.setPacketNumbers({ userId, username }, { doNotFetch: false, packetNumbers }); } setReadingSpeed ({ userId, username }, { readingSpeed }) { - if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return false; } + if (this.isPermanent || !this.allowed(userId)) { return false; } super.setReadingSpeed({ userId, username }, { readingSpeed }); } async setSetName ({ userId, username }, { setName }) { - if (this.settings.controlled || !this.allowed(userId)) { return; } + if (!this.allowed(userId)) { return; } if (!this.packetList) { return; } if (!this.packetList.includes(setName)) { return; } super.setSetName({ userId, username }, { doNotFetch: false, setName }); } setStrictness ({ userId, username }, { strictness }) { - if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } + if (this.isPermanent || !this.allowed(userId)) { return; } super.setStrictness({ userId, username }, { strictness }); } setMinYear ({ userId, username }, { minYear }) { - if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } + if (this.isPermanent || !this.allowed(userId)) { return; } super.setMinYear({ userId, username }, { minYear }); } setMaxYear ({ userId, username }, { maxYear }) { - if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } + if (this.isPermanent || !this.allowed(userId)) { return; } super.setMaxYear({ userId, username }, { maxYear }); } @@ -346,7 +346,7 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { } toggleEnableBonuses ({ userId, username }, { enableBonuses }) { - if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; } + if (this.isPermanent || !this.allowed(userId)) { return; } super.toggleEnableBonuses({ userId, username }, { enableBonuses }); } @@ -368,27 +368,27 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { } togglePowermarkOnly ({ userId, username }, { powermarkOnly }) { - if (this.settings.controlled || !this.allowed(userId)) { return; } + if (!this.allowed(userId)) { return; } super.togglePowermarkOnly({ userId, username }, { powermarkOnly }); } toggleSkip ({ userId, username }, { skip }) { - if (this.settings.controlled || !this.allowed(userId)) { return; } + if (!this.allowed(userId)) { return; } super.toggleSkip({ userId, username }, { skip }); } toggleStandardOnly ({ userId, username }, { standardOnly }) { - if (this.settings.controlled || !this.allowed(userId)) { return; } + if (!this.allowed(userId)) { return; } super.toggleStandardOnly({ userId, username }, { doNotFetch: false, standardOnly }); } toggleStopOnPower ({ userId, username }, { stopOnPower }) { - if (this.settings.controlled || !this.allowed(userId)) { return; } + if (!this.allowed(userId)) { return; } super.toggleStopOnPower({ userId, username }, { stopOnPower }); } togglePublic ({ userId, username }, { public: isPublic }) { - if (this.isPermanent || this.settings.controlled) { return; } + if (this.isPermanent) { return; } this.settings.public = isPublic; if (isPublic) { this.settings.lock = false; @@ -399,12 +399,12 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { } toggleRebuzz ({ userId, username }, { rebuzz }) { - if (this.settings.controlled || !this.allowed(userId)) { return false; } + if (!this.allowed(userId)) { return false; } super.toggleRebuzz({ userId, username }, { rebuzz }); } toggleTimer ({ userId, username }, { timer }) { - if (this.settings.public || this.settings.controlled || !this.allowed(userId)) { return; } + if (this.settings.public || !this.allowed(userId)) { return; } super.toggleTimer({ userId, username }, { timer }); } diff --git a/server/multiplayer/ServerTossupBonusRoom.js b/server/multiplayer/ServerTossupBonusRoom.js index 09d8d9994..6945a6c80 100644 --- a/server/multiplayer/ServerTossupBonusRoom.js +++ b/server/multiplayer/ServerTossupBonusRoom.js @@ -32,7 +32,7 @@ export default class ServerTossupBonusRoom extends ServerMultiplayerRoomMixin(To const shouldKick = this.players[userId].recordEarlyCorrect(); if (shouldKick) { console.log(`Bot detected: User ${userId} (${username}) got 3 correct with abnormally high celerity. Kicking.`); - this.emitMessage({ type: 'bot-kicked', userId, username }); + this.sendToSocket(userId, { type: 'alert', message: 'You were removed for suspected bot behavior.' }); setTimeout(() => this.closeConnection({ userId, username }), 100); return; } From b1b4ad394535d8c383f6d5ce50e8177cd863a023 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Thu, 21 May 2026 21:12:09 -0400 Subject: [PATCH 4/4] undo changes to togglePublic --- server/multiplayer/ServerMultiplayerRoomMixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/multiplayer/ServerMultiplayerRoomMixin.js b/server/multiplayer/ServerMultiplayerRoomMixin.js index 6097e70a2..d1f26d28b 100644 --- a/server/multiplayer/ServerMultiplayerRoomMixin.js +++ b/server/multiplayer/ServerMultiplayerRoomMixin.js @@ -388,7 +388,7 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { } togglePublic ({ userId, username }, { public: isPublic }) { - if (this.isPermanent) { return; } + if (this.isPermanent || this.settings.controlled) { return; } this.settings.public = isPublic; if (isPublic) { this.settings.lock = false;