Auto-derive server.game and server.map from the server name
Server folders encode the game and map (e.g. t5_ascension_1 = game t5, map alias ascension, lobby 1). A T5_MAP_ALIASES table translates the friendly alias to the canonical mapname RCON reports (ascension → zombie_cosmodrome), covering every base + Rezurrection map. seedServerRegistry now populates game and map at insert time and backfills existing NULL rows on every boot; unknown aliases log a warning so we know when to add them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
// T5 (Black Ops 1) zombies maps: friendly aliases → canonical mapname the game
|
||||||
|
// actually reports via RCON. Server names in the fleet encode the friendly
|
||||||
|
// alias (t5_ascension_1 → 'ascension') so we can derive game + map from the
|
||||||
|
// folder name instead of asking the operator to fill it in.
|
||||||
|
//
|
||||||
|
// Add rows here when new servers land with unrecognized aliases; the parser
|
||||||
|
// logs unknowns so they're easy to spot in worker startup.
|
||||||
|
export const T5_MAP_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
||||||
|
kino: 'zombie_theater', // Kino Der Toten
|
||||||
|
five: 'zombie_pentagon', // Five
|
||||||
|
pentagon: 'zombie_pentagon',
|
||||||
|
doa: 'zombietron', // Dead Ops Arcade
|
||||||
|
deadops: 'zombietron',
|
||||||
|
ascension: 'zombie_cosmodrome',
|
||||||
|
cotd: 'zombie_coast', // Call Of The Dead
|
||||||
|
coast: 'zombie_coast',
|
||||||
|
shangri: 'zombie_temple', // Shangri-La
|
||||||
|
shangrila: 'zombie_temple',
|
||||||
|
temple: 'zombie_temple',
|
||||||
|
moon: 'zombie_moon',
|
||||||
|
nacht: 'zombie_cod5_prototype', // Nacht Der Untoten (WaW map, playable in T5 via Rezurrection)
|
||||||
|
verruckt: 'zombie_cod5_asylum',
|
||||||
|
asylum: 'zombie_cod5_asylum',
|
||||||
|
shi: 'zombie_cod5_sumpf', // Shi No Numa
|
||||||
|
shinonuma: 'zombie_cod5_sumpf',
|
||||||
|
sumpf: 'zombie_cod5_sumpf',
|
||||||
|
riese: 'zombie_cod5_factory', // Der Riese
|
||||||
|
derriese: 'zombie_cod5_factory',
|
||||||
|
factory: 'zombie_cod5_factory'
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ParsedServerName {
|
||||||
|
game: string;
|
||||||
|
mapAlias: string;
|
||||||
|
mapCanonical: string | null; // null when the alias isn't in T5_MAP_ALIASES
|
||||||
|
lobby: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `t5_ascension_1` → { game:'t5', mapAlias:'ascension', mapCanonical:'zombie_cosmodrome', lobby:1 }
|
||||||
|
*
|
||||||
|
* Rules:
|
||||||
|
* - First segment = game (e.g. 't5')
|
||||||
|
* - Trailing all-digits segment = lobby number
|
||||||
|
* - Everything in between = map alias (joined without separators)
|
||||||
|
*
|
||||||
|
* Multi-word aliases work because we strip underscores when looking up
|
||||||
|
* (e.g. `t5_shi_no_numa_1` → alias '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 mapParts = lastIsLobby ? parts.slice(1, -1) : parts.slice(1);
|
||||||
|
if (mapParts.length === 0) return null;
|
||||||
|
const mapAlias = mapParts.join('').toLowerCase();
|
||||||
|
const mapCanonical = T5_MAP_ALIASES[mapAlias] ?? null;
|
||||||
|
return { game, mapAlias, mapCanonical, lobby };
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { readdir, mkdir, stat } from 'node:fs/promises';
|
|||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { config, destOf } from '../config.js';
|
import { config, destOf } from '../config.js';
|
||||||
import { db } from '../db/index.js';
|
import { db } from '../db/index.js';
|
||||||
|
import { parseServerName } from './map-aliases.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All server folders under SERVERS_ROOT. Used by seedServerRegistry to backfill
|
* All server folders under SERVERS_ROOT. Used by seedServerRegistry to backfill
|
||||||
@@ -81,13 +82,28 @@ export interface ServerRow {
|
|||||||
export async function seedServerRegistry(): Promise<number> {
|
export async function seedServerRegistry(): Promise<number> {
|
||||||
const rows = await allServers();
|
const rows = await allServers();
|
||||||
const insert = db().prepare(
|
const insert = db().prepare(
|
||||||
'INSERT OR IGNORE INTO servers (name, komodo_type, komodo_name) VALUES (?, ?, ?)'
|
'INSERT OR IGNORE INTO servers (name, komodo_type, komodo_name, game, map) VALUES (?, ?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
// Also backfill existing rows where game/map were never set (upgrade path
|
||||||
|
// from before name-based auto-derivation existed).
|
||||||
|
const backfill = db().prepare(
|
||||||
|
'UPDATE servers SET game = COALESCE(game, ?), map = COALESCE(map, ?) WHERE name = ? AND (game IS NULL OR map IS NULL)'
|
||||||
);
|
);
|
||||||
let n = 0;
|
let n = 0;
|
||||||
const tx = db().transaction((names: string[]) => {
|
const tx = db().transaction((names: string[]) => {
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
const res = insert.run(name, 'deployment', name.replace(/_/g, '-'));
|
const parsed = parseServerName(name);
|
||||||
if (res.changes > 0) n++;
|
const game = parsed?.game ?? null;
|
||||||
|
const map = parsed?.mapCanonical ?? null;
|
||||||
|
const res = insert.run(name, 'deployment', name.replace(/_/g, '-'), game, map);
|
||||||
|
if (res.changes > 0) {
|
||||||
|
n++;
|
||||||
|
} else {
|
||||||
|
backfill.run(game, map, name);
|
||||||
|
}
|
||||||
|
if (parsed && !parsed.mapCanonical) {
|
||||||
|
console.warn(`[servers] unknown map alias '${parsed.mapAlias}' for ${name} — add to T5_MAP_ALIASES`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tx(rows);
|
tx(rows);
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { parseServerName, T5_MAP_ALIASES } from '../../src/lib/server/fleet/map-aliases.js';
|
||||||
|
|
||||||
|
describe('parseServerName', () => {
|
||||||
|
it('extracts game, map alias, canonical map, and lobby from a T5 server name', () => {
|
||||||
|
expect(parseServerName('t5_ascension_1')).toEqual({
|
||||||
|
game: 't5',
|
||||||
|
mapAlias: 'ascension',
|
||||||
|
mapCanonical: 'zombie_cosmodrome',
|
||||||
|
lobby: 1
|
||||||
|
});
|
||||||
|
expect(parseServerName('t5_kino_1')).toEqual({
|
||||||
|
game: 't5',
|
||||||
|
mapAlias: 'kino',
|
||||||
|
mapCanonical: 'zombie_theater',
|
||||||
|
lobby: 1
|
||||||
|
});
|
||||||
|
expect(parseServerName('t5_cotd_1')).toEqual({
|
||||||
|
game: 't5',
|
||||||
|
mapAlias: 'cotd',
|
||||||
|
mapCanonical: 'zombie_coast',
|
||||||
|
lobby: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('joins multi-segment map aliases and matches the alias table', () => {
|
||||||
|
expect(parseServerName('t5_shi_no_numa_1')).toEqual({
|
||||||
|
game: 't5',
|
||||||
|
mapAlias: 'shinonuma',
|
||||||
|
mapCanonical: 'zombie_cod5_sumpf',
|
||||||
|
lobby: 1
|
||||||
|
});
|
||||||
|
expect(parseServerName('t5_der_riese_2')).toEqual({
|
||||||
|
game: 't5',
|
||||||
|
mapAlias: 'derriese',
|
||||||
|
mapCanonical: 'zombie_cod5_factory',
|
||||||
|
lobby: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats a name with no trailing lobby number as an alias-only tail', () => {
|
||||||
|
expect(parseServerName('t5_moon')).toEqual({
|
||||||
|
game: 't5',
|
||||||
|
mapAlias: 'moon',
|
||||||
|
mapCanonical: 'zombie_moon',
|
||||||
|
lobby: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns mapCanonical=null for unknown aliases so operators can spot the gap', () => {
|
||||||
|
expect(parseServerName('t5_newmap_1')).toEqual({
|
||||||
|
game: 't5',
|
||||||
|
mapAlias: 'newmap',
|
||||||
|
mapCanonical: null,
|
||||||
|
lobby: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects names with fewer than two segments', () => {
|
||||||
|
expect(parseServerName('t5')).toBeNull();
|
||||||
|
expect(parseServerName('')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('every alias in T5_MAP_ALIASES resolves to a zombie_* canonical mapname', () => {
|
||||||
|
for (const canonical of Object.values(T5_MAP_ALIASES)) {
|
||||||
|
expect(canonical.startsWith('zombie')).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user