164 lines
5.9 KiB
JavaScript
164 lines
5.9 KiB
JavaScript
'use strict';
|
|
|
|
const UINT32_MAX = 0xffffffff;
|
|
const MAX_CUSTOM_PLAYERS = 64;
|
|
|
|
const DEFAULT_LOBBY_SETTINGS = Object.freeze({
|
|
lobbyName: '',
|
|
passKey: '',
|
|
serverRegion: 3,
|
|
customGameId: '',
|
|
customGameMode: '',
|
|
customMapName: '',
|
|
customGameCrc: '',
|
|
customGameTimestamp: '',
|
|
customMinPlayers: 2,
|
|
customMaxPlayers: 10,
|
|
notifyAtPlayers: 8,
|
|
leaveAtPlayers: 9,
|
|
adminNames: [],
|
|
allowSpectating: true,
|
|
});
|
|
|
|
function asInteger(value, name, min, max) {
|
|
const parsed = typeof value === 'number' ? value : Number(String(value).trim());
|
|
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
throw new Error(`${name} must be an integer between ${min} and ${max}.`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function asOptionalIntegerString(value, name, max) {
|
|
const raw = String(value ?? '').trim();
|
|
if (!raw) return '';
|
|
if (!/^\d+$/.test(raw)) throw new Error(`${name} must contain digits only.`);
|
|
const parsed = BigInt(raw);
|
|
if (parsed < 0n || parsed > BigInt(max)) throw new Error(`${name} is outside its supported range.`);
|
|
return raw;
|
|
}
|
|
|
|
function asUint64String(value, name, { required = false } = {}) {
|
|
const raw = String(value ?? '').trim();
|
|
if (!raw) {
|
|
if (required) throw new Error(`${name} is required.`);
|
|
return '';
|
|
}
|
|
if (!/^\d+$/.test(raw)) throw new Error(`${name} must contain digits only.`);
|
|
const parsed = BigInt(raw);
|
|
if (parsed <= 0n || parsed > 0xffffffffffffffffn) {
|
|
throw new Error(`${name} must be a non-zero unsigned 64-bit integer.`);
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function parseAdminNames(value) {
|
|
const values = Array.isArray(value) ? value : String(value ?? '').split(',');
|
|
return [...new Set(values.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
}
|
|
|
|
function normalizeIdentity(value) {
|
|
return String(value ?? '').normalize('NFKC').trim().toLocaleLowerCase('en-US');
|
|
}
|
|
|
|
function validateLobbySettings(input = {}) {
|
|
const merged = { ...DEFAULT_LOBBY_SETTINGS, ...input };
|
|
const lobbyName = String(merged.lobbyName ?? '').trim();
|
|
const customGameMode = String(merged.customGameMode ?? '').trim();
|
|
const customMapName = String(merged.customMapName ?? '').trim();
|
|
const passKey = String(merged.passKey ?? '');
|
|
|
|
if (!lobbyName) throw new Error('Lobby name is required.');
|
|
if (lobbyName.length > 100) throw new Error('Lobby name must be at most 100 characters.');
|
|
if (passKey.length > 64) throw new Error('Lobby password must be at most 64 characters.');
|
|
if (!customGameMode) {
|
|
throw new Error('Internal addon name is required for a public custom-game lobby. Create the form from a discovered lobby template.');
|
|
}
|
|
if (!customMapName) throw new Error('Custom map name is required.');
|
|
|
|
const customMinPlayers = asInteger(merged.customMinPlayers, 'Minimum players', 1, MAX_CUSTOM_PLAYERS);
|
|
const customMaxPlayers = asInteger(merged.customMaxPlayers, 'Maximum players', 2, MAX_CUSTOM_PLAYERS);
|
|
const notifyAtPlayers = asInteger(merged.notifyAtPlayers, 'Notification threshold', 1, MAX_CUSTOM_PLAYERS);
|
|
const leaveAtPlayers = asInteger(merged.leaveAtPlayers, 'Auto-leave threshold', 1, MAX_CUSTOM_PLAYERS);
|
|
|
|
if (customMinPlayers > customMaxPlayers) {
|
|
throw new Error('Minimum players cannot exceed maximum players.');
|
|
}
|
|
if (notifyAtPlayers > leaveAtPlayers) {
|
|
throw new Error('Notification threshold must not exceed the auto-leave threshold.');
|
|
}
|
|
if (leaveAtPlayers > customMaxPlayers) {
|
|
throw new Error('Auto-leave threshold cannot exceed the custom-game maximum player count.');
|
|
}
|
|
if (leaveAtPlayers === customMaxPlayers) {
|
|
throw new Error('Auto-leave threshold must be below the custom-game maximum while the bot occupies one lobby slot.');
|
|
}
|
|
|
|
return {
|
|
lobbyName,
|
|
passKey,
|
|
serverRegion: asInteger(merged.serverRegion, 'Server region', 0, UINT32_MAX),
|
|
customGameId: asUint64String(merged.customGameId, 'Custom game workshop ID', { required: true }),
|
|
customGameMode,
|
|
customMapName,
|
|
customGameCrc: asUint64String(merged.customGameCrc, 'Custom game CRC'),
|
|
customGameTimestamp: asOptionalIntegerString(merged.customGameTimestamp, 'Custom game timestamp', UINT32_MAX),
|
|
customMinPlayers,
|
|
customMaxPlayers,
|
|
notifyAtPlayers,
|
|
leaveAtPlayers,
|
|
adminNames: parseAdminNames(merged.adminNames),
|
|
allowSpectating: merged.allowSpectating !== false,
|
|
};
|
|
}
|
|
|
|
function toDotaLobbyDetails(settings) {
|
|
const details = {
|
|
gameName: settings.lobbyName,
|
|
serverRegion: settings.serverRegion,
|
|
gameMode: 15,
|
|
allowCheats: false,
|
|
fillWithBots: false,
|
|
allowSpectating: settings.allowSpectating,
|
|
passKey: settings.passKey,
|
|
lan: false,
|
|
customMapName: settings.customMapName,
|
|
customDifficulty: 0,
|
|
customGameId: settings.customGameId,
|
|
customMinPlayers: settings.customMinPlayers,
|
|
customMaxPlayers: settings.customMaxPlayers,
|
|
visibility: 0,
|
|
customGamePenalties: true,
|
|
};
|
|
|
|
details.customGameMode = settings.customGameMode;
|
|
if (settings.customGameCrc) details.customGameCrc = settings.customGameCrc;
|
|
if (settings.customGameTimestamp) details.customGameTimestamp = Number(settings.customGameTimestamp);
|
|
return details;
|
|
}
|
|
|
|
function findAdminMember(settings, members) {
|
|
const wanted = new Set(settings.adminNames.map(normalizeIdentity));
|
|
if (!wanted.size) return null;
|
|
return members.find((member) => wanted.has(normalizeIdentity(member.name))
|
|
|| wanted.has(normalizeIdentity(member.steamId))) || null;
|
|
}
|
|
|
|
function decideLobbySafety(settings, members, lobbyState) {
|
|
if (lobbyState !== 0) return { action: 'unsafe-state', admin: null };
|
|
if (members.length >= settings.leaveAtPlayers) return { action: 'player-limit', admin: null };
|
|
const admin = findAdminMember(settings, members);
|
|
if (admin) return { action: 'admin-joined', admin };
|
|
return { action: 'monitor', admin: null };
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_LOBBY_SETTINGS,
|
|
MAX_CUSTOM_PLAYERS,
|
|
decideLobbySafety,
|
|
findAdminMember,
|
|
normalizeIdentity,
|
|
parseAdminNames,
|
|
toDotaLobbyDetails,
|
|
validateLobbySettings,
|
|
};
|