diff --git a/docs/guides/collections.md b/docs/guides/collections.md
index a1e5a7e..b5c6d5f 100644
--- a/docs/guides/collections.md
+++ b/docs/guides/collections.md
@@ -91,7 +91,7 @@ and cleanup recovery where applicable.
### Collection limits and editing
-Collections support up to 40 distinct game/map selections, 1–20 lobbies per map,
+Collections support up to 40 distinct game/map selections, 1–15 lobbies per map,
and 100 servers total. The host's configured port range and existing fleet may
allow fewer. A collection can be saved even when there is insufficient capacity
to launch it. Saving a collection does not capture or pin managed file versions.
diff --git a/src/lib/server/fleet/launch-recipes.ts b/src/lib/server/fleet/launch-recipes.ts
index 14442e2..303d660 100644
--- a/src/lib/server/fleet/launch-recipes.ts
+++ b/src/lib/server/fleet/launch-recipes.ts
@@ -61,7 +61,7 @@ function buildEntries(input: unknown): BlueprintSelection[] {
const entries = input.map(entry => {
if (!entry || (entry.game !== 't5' && entry.game !== 't6') || typeof entry.mapSlug !== 'string'
|| !zombieMapBySlug(entry.game, entry.mapSlug)) throw new Error('Each selection needs a supported game and map.');
- if (!Number.isSafeInteger(entry.count) || entry.count < 1 || entry.count > 20) throw new Error('Choose 1–20 lobbies for each map.');
+ if (!Number.isSafeInteger(entry.count) || entry.count < 1 || entry.count > 15) throw new Error('Choose 1–15 lobbies for each map.');
const key = `${entry.game}:${entry.mapSlug}`;
if (seen.has(key)) throw new Error('Each game/map can appear once. Increase its lobby count instead.');
seen.add(key);
diff --git a/src/lib/ui/components/BlueprintBuilder.svelte b/src/lib/ui/components/BlueprintBuilder.svelte
index 71e3b89..c3cab6b 100644
--- a/src/lib/ui/components/BlueprintBuilder.svelte
+++ b/src/lib/ui/components/BlueprintBuilder.svelte
@@ -16,8 +16,8 @@
let error = $state('');
let total = $derived(rows.reduce((sum, row) => sum + (Number(row.count) || 0), 0));
const duplicate = (row: typeof rows[number]) => rows.some(other => other.key !== row.key && other.game === row.game && other.mapSlug === row.mapSlug);
- let valid = $derived(name.trim().length > 0 && rows.length > 0 && total <= 100 && rows.every(row => row.mapSlug && Number.isInteger(row.count) && row.count >= 1 && row.count <= 20 && !duplicate(row)));
- let reason = $derived(!name.trim() ? 'Enter a collection name to save.' : total > 100 ? `Reduce the total by ${total - 100} to stay within 100 target lobbies.` : rows.some(duplicate) ? 'Choose each game and map once; increase its lobby count instead.' : !valid ? 'Choose a map and 1–20 target lobbies for every row.' : '');
+ let valid = $derived(name.trim().length > 0 && rows.length > 0 && total <= 100 && rows.every(row => row.mapSlug && Number.isInteger(row.count) && row.count >= 1 && row.count <= 15 && !duplicate(row)));
+ let reason = $derived(!name.trim() ? 'Enter a collection name to save.' : total > 100 ? `Reduce the total by ${total - 100} to stay within 100 target lobbies.` : rows.some(duplicate) ? 'Choose each game and map once; increase its lobby count instead.' : !valid ? 'Choose a map and 1–15 target lobbies for every row.' : '');
let canAdd = $derived((['t5', 't6'] as const).some(game => catalogs[game].some(map => !rows.some(row => row.game === game && row.mapSlug === map.slug))));
let wasOpen = false;
let session = 0;
@@ -83,7 +83,7 @@
{String(index + 1).padStart(2, '0')}
-
+
{/each}
diff --git a/tests/unit/collection-launch-performance.test.ts b/tests/unit/collection-launch-performance.test.ts
index b3edfb2..e7d6384 100644
--- a/tests/unit/collection-launch-performance.test.ts
+++ b/tests/unit/collection-launch-performance.test.ts
@@ -62,7 +62,8 @@ describe('large native collection admission', () => {
const { previewCollection, queueCollection, getCollection } = await import('../../src/lib/server/fleet/collections.js');
const { db } = await import('../../src/lib/server/db/index.js');
const blueprint = saveBlueprint({ name: 'One hundred lobbies', entries:
- ['theater', 'cosmodrome', 'coast', 'temple', 'moon'].map(mapSlug => ({ game: 't5', mapSlug, count: 20 })) });
+ ['theater', 'cosmodrome', 'coast', 'temple', 'moon', 'pentagon', 'prototype']
+ .map((mapSlug, index) => ({ game: 't5', mapSlug, count: index === 6 ? 10 : 15 })) });
const parse = JSON.parse;
let manifestParses = 0;
diff --git a/tests/unit/deployment-collections.test.ts b/tests/unit/deployment-collections.test.ts
index df97b6f..e8995e8 100644
--- a/tests/unit/deployment-collections.test.ts
+++ b/tests/unit/deployment-collections.test.ts
@@ -72,15 +72,16 @@ describe('multi-server blueprints', () => {
expect(old.entries).toEqual([{ count: 1, game: 't5', mapSlug: 'theater', mapName: 'Kino Der Toten' }]);
expect(old).not.toHaveProperty('recipe');
expect(previewCreation({ blueprintId: 'legacy' }).recipe.files).toContainEqual(expect.objectContaining({ path: 'telemetry.gsc', versionId: current.id }));
- const edited = updateBlueprint('legacy', { name: 'Whole lineup', entries });
- expect(edited.totalServers).toBe(5);
- expect(edited.entries[0]).toEqual({ ...old.entries[0], count: 2 });
+ const updatedEntries = [{ ...entries[0], count: 15 }, ...entries.slice(1)];
+ const edited = updateBlueprint('legacy', { name: 'Whole lineup', entries: updatedEntries });
+ expect(edited.totalServers).toBe(18);
+ expect(edited.entries[0]).toEqual({ ...old.entries[0], count: 15 });
const stored = db().prepare('SELECT recipe FROM server_blueprints WHERE id = ?').get('legacy') as { recipe: string };
- expect(JSON.parse(stored.recipe)).toEqual({ format: 3, entries });
+ expect(JSON.parse(stored.recipe)).toEqual({ format: 3, entries: updatedEntries });
expect(listBlueprints()).toHaveLength(1);
});
- it.each([[], [{ game: 't5', mapSlug: 'theater', count: 0 }], [{ game: 't5', mapSlug: 'theater', count: 21 }],
+ it.each([[], [{ game: 't5', mapSlug: 'theater', count: 0 }], [{ game: 't5', mapSlug: 'theater', count: 16 }],
[{ game: 't5', mapSlug: 'theater', count: 1.5 }], [{ game: 't5', mapSlug: 'theater', count: '2' }],
[{ game: 't5', mapSlug: 'town', count: 1 }], [entries[0], entries[0]]].map(input => ({ input })))('rejects an invalid lineup: $input', async ({ input }) => {
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');