diff --git a/docs/guides/autoscaling.md b/docs/guides/autoscaling.md index ead83a0..107b101 100644 --- a/docs/guides/autoscaling.md +++ b/docs/guides/autoscaling.md @@ -4,6 +4,8 @@ Open **Fleet → Autoscaling** to keep empty lobbies available for new groups. P Autoscaling is opt-in. Installing the feature creates no policies and changes no lobby counts. Use a two-week dry run before enabling live actions. +T5 game-wide policies cover the ten standard Zombies maps. Dead Ops Arcade is not supported. + ## Configure a policy Choose **Add policy**, select the game and scope, and enter an explicit maximum. The initial editor selects Dry run, with a minimum of one running lobby, one empty spare, a 20-minute empty interval, and a five-minute action cooldown. Counts must be between 1 and 20; the minimum and spare target cannot exceed the maximum. diff --git a/src/lib/map-aliases.ts b/src/lib/map-aliases.ts index 469fa4e..db5b86f 100644 --- a/src/lib/map-aliases.ts +++ b/src/lib/map-aliases.ts @@ -27,7 +27,6 @@ export const T5_ZOMBIE_MAPS: readonly T5MapEntry[] = Object.freeze([ { friendly: 'Shangri-La', slug: 'temple', canonical: 'zombie_temple' }, { friendly: 'Moon', slug: 'moon', canonical: 'zombie_moon' }, { friendly: 'Five', slug: 'pentagon', canonical: 'zombie_pentagon' }, - { friendly: 'Dead Ops Arcade', slug: 'zombietron', canonical: 'zombietron' }, { 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' }, diff --git a/tests/unit/autoscaling-game.test.ts b/tests/unit/autoscaling-game.test.ts index 6d6e5c8..03d57b4 100644 --- a/tests/unit/autoscaling-game.test.ts +++ b/tests/unit/autoscaling-game.test.ts @@ -10,6 +10,18 @@ beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => { await fixture.cleanup(); }); const settings = { game: 't6', mapSlug: '*', maximum: 2, mode: 'create-only' }; const maps = zombieMaps('t6'); + +it('satisfies a T5 game-wide policy with the ten standard Zombies maps', () => { + const now = 2_000_000; + const policy: ScalingPolicy = { ...scalingSettings({ ...settings, game: 't5' }), + id: 'policy', revision: 1, nextActionAt: 0, pausedReason: null, updatedAt: '' }; + const members: ScalingMember[] = zombieMaps('t5').map(map => ({ name: `t5_zombie_${map.slug}_1`, mapSlug: map.slug, + identity: map.slug, state: 'empty', players: 0, observedAt: now, source: 'rcon', emptySince: 1, blocker: null, operationId: null })); + const snapshot = { members, reservedNames: members.map(m => m.name), keyPool: { total: 20, available: 10 }, blockers: [] }; + expect(planScaling(policy, snapshot, now)).toMatchObject({ action: 'wait', server: null, desired: 10, deficit: 0, empty: 10 }); + expect(() => scalingSettings({ ...settings, game: 't5', mapSlug: 'zombietron' })).toThrow('supported game and map'); +}); + async function setup() { await prepareNativeHost(); const { db } = await import('../../src/lib/server/db/index.js'); diff --git a/tests/unit/collection-match-performance.test.ts b/tests/unit/collection-match-performance.test.ts index b266f92..6d17e0e 100644 --- a/tests/unit/collection-match-performance.test.ts +++ b/tests/unit/collection-match-performance.test.ts @@ -89,7 +89,7 @@ describe('matching a native collection', () => { const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js'); const { previewCollection, queueCollection, getCollection } = await import('../../src/lib/server/fleet/collections.js'); const { T5_ZOMBIE_MAPS } = await import('../../src/lib/map-aliases.js'); - const blueprint = saveBlueprint({ name: 'Replace nine with ten', entries: T5_ZOMBIE_MAPS.filter(map => map.slug !== 'zombietron') + const blueprint = saveBlueprint({ name: 'Replace nine with ten', entries: T5_ZOMBIE_MAPS .map(map => ({ game: 't5', mapSlug: map.slug, count: 1 })) }); const preview = previewCollection(blueprint.id, 'exact'); const queued = queueCollection({ previewId: preview.id, @@ -137,7 +137,7 @@ describe('matching a native collection', () => { const { GET: fleetResponse } = await import('../../src/routes/api/servers/+server.js'); const { POST: agentHeartbeat } = await import('../../src/routes/api/agent/heartbeat/+server.js'); const { T5_ZOMBIE_MAPS } = await import('../../src/lib/map-aliases.js'); - const blueprint = saveBlueprint({ name: 'Match ten T5 lobbies', entries: T5_ZOMBIE_MAPS.filter(map => map.slug !== 'zombietron') + const blueprint = saveBlueprint({ name: 'Match ten T5 lobbies', entries: T5_ZOMBIE_MAPS .map(map => ({ game: 't5', mapSlug: map.slug, count: 1 })) }); const preview = previewCollection(blueprint.id, 'exact'); expect(preview).toMatchObject({ ready: true, mode: 'exact' }); diff --git a/tests/unit/config-compose.test.ts b/tests/unit/config-compose.test.ts index 5f4c00d..ca0c742 100644 --- a/tests/unit/config-compose.test.ts +++ b/tests/unit/config-compose.test.ts @@ -106,8 +106,8 @@ describe('automatic map selection', () => { }); it('appends the rotation when absent and preserves CRLF', () => { - expect(composeServerCfg('set sv_voice "1"\r\n', [], 't5', 'zombietron')) - .toBe('set sv_voice "1"\r\n// Map selected by Electric Cherry\r\nset sv_maprotation "map zombietron"\r\n'); + expect(composeServerCfg('set sv_voice "1"\r\n', [], 't5', 'zombie_moon')) + .toBe('set sv_voice "1"\r\n// Map selected by Electric Cherry\r\nset sv_maprotation "map zombie_moon"\r\n'); }); it('removes conflicting duplicate assignments regardless of case or command form', () => { diff --git a/tests/unit/map-aliases.test.ts b/tests/unit/map-aliases.test.ts index 9069075..1e6ee01 100644 --- a/tests/unit/map-aliases.test.ts +++ b/tests/unit/map-aliases.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { parseServerName, + selectedZombieMap, t5ZombieMapBySlug, T5_ZOMBIE_MAPS, T6_ZOMBIE_MAPS @@ -98,6 +99,12 @@ describe('parseServerName (new naming: t5___)', () => { }); describe('t5ZombieMapBySlug', () => { + it('excludes Dead Ops Arcade from supported map selection', () => { + expect(t5ZombieMapBySlug('zombietron')).toBeUndefined(); + expect(selectedZombieMap('t5', 'zombietron')).toBeUndefined(); + expect(parseServerName('t5_zombie_zombietron_1')?.mapCanonical).toBeNull(); + }); + it('resolves each catalog slug to its canonical + friendly', () => { for (const entry of T5_ZOMBIE_MAPS) { const found = t5ZombieMapBySlug(entry.slug);