Files
doka2-lobby-list/lib/lobby-settings.js
T
Aleksandr Bogomiakov c239669932
Docker image / build-and-push (push) Successful in 57s
Added lobby auto creation
2026-08-06 20:33:27 +01:00

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,
};