diff --git a/README.md b/README.md index 375ef88..2e25d96 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ the sweep marks pending rows `manual` so nothing hangs forever. Use **Server keys** to add, replace, or remove a Plutonium key for an exact server ID such as `t5_zombie_theater_2`. IDs use a supported map slug and a lobby number from 1 to 20; the server does not need to be deployed first. -During deployment, choose the map and lobby. A saved key is reused automatically, +During deployment, choose the map. The lowest available lobby is assigned +automatically, with the exact server ID shown before deployment. Its saved key is reused automatically, and any newly entered key is saved for that ID, including if deployment fails. Destroying a server preserves its saved key. Updating or removing a saved key does not change a running server; changes apply to future deployments. diff --git a/src/lib/server/fleet/create.ts b/src/lib/server/fleet/create.ts index ce10bed..729cabe 100644 --- a/src/lib/server/fleet/create.ts +++ b/src/lib/server/fleet/create.ts @@ -48,7 +48,7 @@ export interface CreateServerInput { gameMode: GameMode; // 'zombie' — only value supported today mapSlug: string; // 'cosmodrome' plutoKey?: string; // saved for this server ID when supplied - lobby?: number; // explicit stable identity; otherwise first available + expectedName?: string; // optional preview guard; never selects a lobby createdBy?: string; // 'ui' by default } @@ -160,15 +160,14 @@ async function createServerUnlocked(input: CreateServerInput): Promise 20)) { - return { ok: false, kind: 'bad-request', message: 'Lobby must be an integer from 1 to 20' }; - } - const lobby = input.lobby ?? nextLobbyNumber(baseName); + const lobby = nextLobbyNumber(baseName); if (lobby === null) { return { ok: false, kind: 'no-slot', message: `all lobby slots (1..20) for ${baseName} are taken` }; } const name = `${baseName}_${lobby}`; - if (getServer(name)) return { ok: false, kind: 'no-slot', message: `${name} is already deployed` }; + if (input.expectedName !== undefined && input.expectedName !== name) { + return { ok: false, kind: 'no-slot', message: 'Available lobby changed. Review the updated server ID and key, then deploy again.' }; + } const plutoKey = input.plutoKey?.trim() || getServerKey(name); if (!plutoKey) return { ok: false, kind: 'bad-request', message: `No saved key for ${name}. Enter a Plutonium server key.` }; diff --git a/src/lib/ui/components/DeployServerDialog.svelte b/src/lib/ui/components/DeployServerDialog.svelte index 4957c3d..453827f 100644 --- a/src/lib/ui/components/DeployServerDialog.svelte +++ b/src/lib/ui/components/DeployServerDialog.svelte @@ -22,7 +22,6 @@ let gameMode = $state<'zombie'>('zombie'); let mapSlug = $state(''); let plutoKey = $state(''); - let lobby = $state('1'); let savedIds = $state([]); let deployedIds = $state([]); let keysLoaded = $state(false); @@ -77,21 +76,12 @@ if (e.target === dialog) close(); } + let lobby = $derived(Array.from({ length: 20 }, (_, i) => i + 1) + .find(n => !deployedIds.includes(`t5_${gameMode}_${mapSlug}_${n}`)) ?? null); let selectedMap = $derived(maps.find((m) => m.slug === mapSlug)); - let previewName = $derived(selectedMap ? `t5_${gameMode}_${selectedMap.slug}_${lobby}` : ''); + let previewName = $derived(selectedMap && keysLoaded && lobby !== null ? `t5_${gameMode}_${selectedMap.slug}_${lobby}` : ''); let hasSavedKey = $derived(savedIds.includes(previewName)); - let alreadyDeployed = $derived(deployedIds.includes(previewName)); - let lobbyOptions = $derived(Array.from({ length: 20 }, (_, i) => { - const value = String(i + 1); - const id = `t5_${gameMode}_${mapSlug}_${value}`; - return { value, label: `${value}${deployedIds.includes(id) ? ' (deployed)' : savedIds.includes(id) ? ' (saved key)' : ''}` }; - })); - $effect(() => { - const prefix = `t5_${gameMode}_${mapSlug}_`; - lobby = String(Array.from({ length: 20 }, (_, i) => i + 1) - .find(n => !deployedIds.includes(`${prefix}${n}`)) ?? 1); - }); $effect(() => { mapSlug; lobby; plutoKey = ''; }); let modeOptions = [{ value: 'zombie', label: 'Zombies' }]; @@ -99,7 +89,7 @@ async function submit(e: SubmitEvent): Promise { e.preventDefault(); - if (deploying) return; + if (deploying || !keysLoaded || lobby === null) return; if (!selectedMap) { flash = 'Pick a map'; return; } if (!hasSavedKey && plutoKey.trim().length === 0) { flash = 'Pluto server key is required'; return; } deploying = true; @@ -108,11 +98,12 @@ const res = await fetch('/api/servers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ gameMode, mapSlug: selectedMap.slug, lobby: Number(lobby), plutoKey: plutoKey.trim() || undefined }) + body: JSON.stringify({ gameMode, mapSlug: selectedMap.slug, expectedName: previewName, plutoKey: plutoKey.trim() || undefined }) }); const body = (await res.json()) as { ok: boolean; name?: string; port?: number; error?: string; kind?: string }; if (!body.ok || !body.name) { flash = body.error ?? 'Deploy failed'; + if (body.kind === 'no-slot') await loadKeys(); deploying = false; return; } @@ -150,10 +141,6 @@ - - Will create {previewName || '—'} - {alreadyDeployed ? "Already deployed; choose another lobby" : "Port auto-assigned"} + {!keysLoaded ? "Loading available lobby..." : lobby === null ? "All lobby slots for this map are in use" : "Lowest available lobby; port auto-assigned"} Manage saved keys @@ -177,7 +164,7 @@