// T5/T6 zombies map catalogs. Server names encode `slug` so we can // derive game, mode, and map from the folder name (t5_zombie_cosmodrome_1 → // game 't5', mode 'zombie', slug 'cosmodrome', canonical 'zombie_cosmodrome'). // // Fields: // friendly — human name shown in the deploy UI dropdown // slug — segment used in server name (t5_zombie__) // canonical — actual mapname Pluto/RCON emits (used for script scoping) // // Add rows when new maps land; keep slugs short so paths stay readable. export interface ZombieMapEntry { friendly: string; slug: string; canonical: string; /** T6 selects a ruleset/location before loading the engine map. */ gameSettings?: string; } export type T5MapEntry = ZombieMapEntry; export type ZombieGame = 't5' | 't6'; export const T5_ZOMBIE_MAPS: readonly T5MapEntry[] = Object.freeze([ { friendly: 'Kino Der Toten', slug: 'theater', canonical: 'zombie_theater' }, { friendly: 'Ascension', slug: 'cosmodrome', canonical: 'zombie_cosmodrome' }, { friendly: 'Call of the Dead', slug: 'coast', canonical: 'zombie_coast' }, { friendly: 'Shangri-La', slug: 'temple', canonical: 'zombie_temple' }, { friendly: 'Moon', slug: 'moon', canonical: 'zombie_moon' }, { friendly: 'Five', slug: 'pentagon', canonical: 'zombie_pentagon' }, { friendly: 'Nacht Der Untoten', slug: 'prototype', canonical: 'zombie_cod5_prototype' }, { friendly: 'Verrückt', slug: 'asylum', canonical: 'zombie_cod5_asylum' }, { friendly: 'Shi No Numa', slug: 'sumpf', canonical: 'zombie_cod5_sumpf' }, { friendly: 'Der Riese', slug: 'factory', canonical: 'zombie_cod5_factory' } ]); // Rulesets from xerxes-at/T6ServerConfigs, also seeded by Plutainer. // Several selections share an engine map; slug preserves the selected variant. export const T6_ZOMBIE_MAPS: readonly ZombieMapEntry[] = Object.freeze([ { friendly: 'TranZit', slug: 'transit', canonical: 'zm_transit', gameSettings: 'zm_classic_transit.cfg' }, { friendly: 'Town Survival', slug: 'town', canonical: 'zm_transit', gameSettings: 'zm_standard_town.cfg' }, { friendly: 'Farm Survival', slug: 'farm', canonical: 'zm_transit', gameSettings: 'zm_standard_farm.cfg' }, { friendly: 'Bus Depot Survival', slug: 'busdepot', canonical: 'zm_transit', gameSettings: 'zm_standard_transit.cfg' }, { friendly: 'Nuketown', slug: 'nuked', canonical: 'zm_nuked', gameSettings: 'zm_standard_nuked.cfg' }, { friendly: 'Die Rise', slug: 'highrise', canonical: 'zm_highrise', gameSettings: 'zm_classic_rooftop.cfg' }, { friendly: 'Mob of the Dead', slug: 'prison', canonical: 'zm_prison', gameSettings: 'zm_classic_prison.cfg' }, { friendly: 'Buried', slug: 'buried', canonical: 'zm_buried', gameSettings: 'zm_classic_processing.cfg' }, { friendly: 'Origins', slug: 'tomb', canonical: 'zm_tomb', gameSettings: 'zm_classic_tomb.cfg' }, { friendly: 'Town Grief', slug: 'towngrief', canonical: 'zm_transit', gameSettings: 'zm_grief_town.cfg' }, { friendly: 'Farm Grief', slug: 'farmgrief', canonical: 'zm_transit', gameSettings: 'zm_grief_farm.cfg' }, { friendly: 'Bus Depot Grief', slug: 'busdepotgrief', canonical: 'zm_transit', gameSettings: 'zm_grief_transit.cfg' }, { friendly: 'Cell Block Grief', slug: 'cellblock', canonical: 'zm_prison', gameSettings: 'zm_grief_cellblock.cfg' }, { friendly: 'Buried Grief', slug: 'buriedgrief', canonical: 'zm_buried', gameSettings: 'zm_grief_street.cfg' }, { friendly: 'Diner Turned', slug: 'diner', canonical: 'zm_transit_dr', gameSettings: 'zm_cleansed_diner.cfg' }, { friendly: 'Buried Turned', slug: 'buriedturned', canonical: 'zm_buried', gameSettings: 'zm_cleansed_street.cfg' } ]); export function zombieMaps(game: string | null): readonly ZombieMapEntry[] { return game === 't5' ? T5_ZOMBIE_MAPS : game === 't6' ? T6_ZOMBIE_MAPS : []; } export function zombieMapBySlug(game: string, slug: string): ZombieMapEntry | undefined { return zombieMaps(game).find((entry) => entry.slug === slug.toLowerCase()); } export function selectedZombieMap(game: string | null, map: string | null, slug?: string | null): ZombieMapEntry | undefined { const maps = zombieMaps(game); // T5 maps are unambiguous, including legacy friendly slugs. T6 variants // require the stored selection; never fall back from an unknown variant. return maps.find((entry) => entry.canonical === map && ( game !== 't6' || !slug || entry.slug === slug || entry.friendly.toLowerCase().replace(/[^a-z0-9]/g, '') === slug )); } export function mapRotation(entry: ZombieMapEntry): string { return `${entry.gameSettings ? `execgts ${entry.gameSettings} ` : ''}map ${entry.canonical}`; } const BY_SLUG: ReadonlyMap = new Map( T5_ZOMBIE_MAPS.map((m) => [m.slug, m]) ); export function t5ZombieMapBySlug(slug: string): T5MapEntry | undefined { return BY_SLUG.get(slug.toLowerCase()); } export type GameMode = 'zombie' | 'mp' | 'sp'; export interface ParsedServerName { game: string; // 't5' gameMode: GameMode | null; // 'zombie' — null when the second segment isn't a known mode mapSlug: string; // 'cosmodrome' — joined without separators for lookup mapCanonical: string | null;// 'zombie_cosmodrome' — null when slug not in catalog lobby: number | null; } const KNOWN_MODES: ReadonlySet = new Set(['zombie', 'mp', 'sp']); /** * `t5_zombie_cosmodrome_1` → { game:'t5', gameMode:'zombie', mapSlug:'cosmodrome', mapCanonical:'zombie_cosmodrome', lobby:1 } * * Rules: * - First segment = game * - Second segment = mode when it matches a known mode (zombie|mp|sp); otherwise the whole * tail is treated as legacy `__` (mode null, slug from segments 1..-1) * - Trailing all-digits segment = lobby number * - Everything in between = map slug (joined without separators for the catalog lookup) * * Multi-word slugs still work because we strip underscores when looking up * (e.g. `t5_zombie_shi_no_numa_1` → slug 'shinonuma'). */ export function parseServerName(name: string): ParsedServerName | null { const parts = name.split('_').filter(Boolean); if (parts.length < 2) return null; const game = parts[0]; const lastIsLobby = /^\d+$/.test(parts[parts.length - 1]); const lobby = lastIsLobby ? Number.parseInt(parts[parts.length - 1], 10) : null; const tail = lastIsLobby ? parts.slice(1, -1) : parts.slice(1); if (tail.length === 0) return null; let gameMode: GameMode | null = null; let slugParts = tail; const maybeMode = tail[0].toLowerCase(); if (KNOWN_MODES.has(maybeMode as GameMode)) { gameMode = maybeMode as GameMode; slugParts = tail.slice(1); } if (slugParts.length === 0) return null; const mapSlug = slugParts.join('').toLowerCase(); const catalog = zombieMaps(game.toLowerCase()); const entry = gameMode === null || gameMode === 'zombie' ? catalog.find((m) => m.slug === mapSlug) ?? catalog.find((m) => m.friendly.toLowerCase().replace(/[^a-z0-9]/g, '') === mapSlug) : undefined; const mapCanonical = entry?.canonical ?? null; return { game, gameMode, mapSlug, mapCanonical, lobby }; }