From 1492c280d59d958c0dc2ceaeed83870ecd30eaa8 Mon Sep 17 00:00:00 2001
From: MrEgosar
{server.message}
+ {isOrg ? ( + + ) :Der Organisator kann die Messerrunde neu einrichten.
} + > + ) : !knife.ready ? (Der Server fährt hoch, die Verbindungsdaten erscheinen gleich hier. {server.message && ({server.message})} diff --git a/server/src/matchService.js b/server/src/matchService.js index 36e4a77..8d4a3a1 100644 --- a/server/src/matchService.js +++ b/server/src/matchService.js @@ -1070,7 +1070,11 @@ export class MatchService extends EventEmitter { }; // Messerrunde auf dem Match-Server: Spieler bekommen die Verbindungsdaten schon in der Lobby const knife = match.status === 'lobby' && row.knife_at && row.status === 'ready'; - if (match.status === 'lobby' && match.veto_start === 'knife') view.knife = { ready: !!knife, phase: knife ? row.knife_phase : null }; + if (match.status === 'lobby' && match.veto_start === 'knife') { + // failed: alle automatischen Versuche fehlgeschlagen, der Organisator kann es von Hand wiederholen + const failed = !knife && row.status === 'ready' && String(row.message ?? '').startsWith('Messerrunde konnte nicht'); + view.knife = { ready: !!knife, phase: knife ? row.knife_phase : null, failed }; + } if (viewer.isOrganizer) { // Zeitpunkt der letzten Anforderung (neuer Start oder "Server neu starten / wiederholen") view.requestedAt = row.started_at ?? null; diff --git a/server/src/servers/serverManager.js b/server/src/servers/serverManager.js index 469bfd0..e963e96 100644 --- a/server/src/servers/serverManager.js +++ b/server/src/servers/serverManager.js @@ -18,6 +18,9 @@ export const SERVER_STATUS = { const ACTIVE = ['starting', 'ready', 'configuring', 'configured', 'config_failed', 'failed']; const SLOW_POLL_MS = 30_000; +// Messerrunde: so oft wird das Einrichten versucht, bevor der Organisator selbst neu starten muss +const KNIFE_RETRIES = 3; +const KNIFE_RETRY_DELAY_MS = 10_000; /** * Steuert den Lebenszyklus der Match-Server. Kennt nur die Adapter-Schnittstelle @@ -29,6 +32,9 @@ export class ServerManager { this.busy = new Set(); this.simulator = new DemoMatchSimulator(); this.autoAdvance = new Map(); + // Fehlversuche beim Einrichten der Messerrunde je Match (für die automatische Wiederholung) + this.knifeAttempts = new Map(); + this.knifeRetryDelayMs = KNIFE_RETRY_DELAY_MS; this.lastPoll = new Map(); this.timers = []; } @@ -444,10 +450,22 @@ export class ServerManager { try { await this.adapter.configure(JSON.parse(row.handle), buildKnifeCommands({ map, pool, password: row.server_password, endlessWarmup: true })); } catch (err) { - this.update(matchId, { knife_at: null, knife_phase: null, message: `Messerrunde konnte nicht eingerichtet werden: ${err.message}` }); + // Das erste Laden einer Map dauert nach einer frischen Installation oft länger: von selbst noch einmal versuchen + const attempt = (this.knifeAttempts.get(matchId) ?? 0) + 1; + this.knifeAttempts.set(matchId, attempt); + const again = attempt < KNIFE_RETRIES; + this.update(matchId, { + knife_at: null, + knife_phase: null, + message: again + ? `Messerrunde wird erneut eingerichtet (Versuch ${attempt + 1} von ${KNIFE_RETRIES}): ${err.message}` + : `Messerrunde konnte nicht eingerichtet werden: ${err.message}`, + }); this.service.log(matchId, 'server', `Messerrunde nicht eingerichtet: ${err.message}`); + if (again) this.timers.push(setTimeout(() => this.maybeConfigure(matchId), this.knifeRetryDelayMs)); return; } + this.knifeAttempts.delete(matchId); this.update(matchId, { knife_at: Date.now(), knife_phase: 'warmup', message: 'Messerrunde: Aufwärmphase, bis der Organisator startet' }); this.service.log(matchId, 'server', 'Server für die Messerrunde bereit (Aufwärmphase)'); } finally { @@ -555,6 +573,7 @@ export class ServerManager { return; } if (this.wantsKnife(match, row)) { + this.knifeAttempts.delete(matchId); this.service.log(matchId, 'server', 'Organisator richtet die Messerrunde neu ein'); this.configureKnife(matchId).catch((err) => this.logger.error(err)); return; diff --git a/server/test/api.test.js b/server/test/api.test.js index 0ca8d5b..15ecd00 100644 --- a/server/test/api.test.js +++ b/server/test/api.test.js @@ -730,7 +730,7 @@ test('Messerrunde auf dem eigenen Match-Server: Verbindungsdaten in der Lobby, d await app.manager.poll(); await waitFor(() => app.manager.row(id).knife_phase === 'warmup', { label: 'Messerrunde eingerichtet' }); const player = (await call('GET', `/matches/${id}`, { token: tokens.b1 })).body; - assert.deepEqual(player.server.knife, { ready: true, phase: 'warmup' }); + assert.deepEqual(player.server.knife, { ready: true, phase: 'warmup', failed: false }); assert.equal(player.server.connect.password, app.manager.row(id).server_password, 'Spieler bekommen das Passwort des Matches'); const pub = (await call('GET', `/matches/${id}`)).body; assert.equal(pub.server?.connect, undefined, 'öffentlich keine Verbindungsdaten'); @@ -775,3 +775,33 @@ test('Messerrunde auf dem eigenen Match-Server: Verbindungsdaten in der Lobby, d await close(); } }); + +test('Messerrunde: Einrichten wird bei Fehlern von selbst wiederholt, danach kann der Organisator neu starten', async () => { + const { app, call, close } = await setup(); + try { + app.manager.knifeRetryDelayMs = 0; + const configure = app.adapter.configure.bind(app.adapter); + let failures = 2; + app.adapter.configure = async (h, plan) => { + if (failures > 0) { failures -= 1; throw new Error('Map de_mirage wurde nicht rechtzeitig geladen'); } + return configure(h, plan); + }; + const created = await call('POST', '/matches', { body: { teamA: 'Alpha', teamB: 'Bravo', mode: 'wingman', format: 'bo1', vetoStart: 'knife' } }); + const { id, organizerToken: org } = created.body; + await app.manager.poll(); + // Zwei Fehlschläge, der dritte Versuch klappt ohne Zutun + await waitFor(() => app.manager.row(id).knife_phase === 'warmup', { label: 'Messerrunde nach Wiederholung eingerichtet' }); + + // Scheitern alle Versuche, bekommt der Organisator den Knopf + failures = 99; + app.manager.update(id, { knife_at: null, knife_phase: null }); + app.manager.knifeAttempts.delete(id); + app.manager.maybeConfigure(id); + await waitFor(async () => (await call('GET', `/matches/${id}`, { token: org })).body.server.knife.failed, { label: 'als fehlgeschlagen gemeldet' }); + failures = 0; + assert.equal((await call('POST', `/matches/${id}/server/retry`, { token: org })).status, 200); + await waitFor(() => app.manager.row(id).knife_phase === 'warmup', { label: 'von Hand neu eingerichtet' }); + } finally { + await close(); + } +});