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