From 1492c280d59d958c0dc2ceaeed83870ecd30eaa8 Mon Sep 17 00:00:00 2001 From: MrEgosar Date: Sun, 1 Nov 2026 19:23:52 +0100 Subject: [PATCH] Messerrunde: Einrichten bei Fehlern automatisch wiederholen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nach einer frischen Installation dauert das erste Laden einer Map oft länger als die Wartezeit. Die App versucht das Einrichten jetzt bis zu dreimal (alle 10 s). Scheitern alle Versuche, zeigt die Lobby den Fehler und dem Organisator den Knopf "Messerrunde neu einrichten". --- client/src/pages/MatchPage.jsx | 11 +++++++++- server/src/matchService.js | 6 +++++- server/src/servers/serverManager.js | 21 ++++++++++++++++++- server/test/api.test.js | 32 ++++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/client/src/pages/MatchPage.jsx b/client/src/pages/MatchPage.jsx index 494bb51..083c835 100644 --- a/client/src/pages/MatchPage.jsx +++ b/client/src/pages/MatchPage.jsx @@ -235,7 +235,16 @@ function OwnKnifeServer({ state, isOrg, act, busy }) { Für euer Match läuft ein eigener Server. Beide Teams verbinden sich, spielen dort eine Runde nur mit Messer, danach legt der Organisator fest, welches Team das Veto beginnt. Nach dem Veto geht es auf demselben Server weiter.

- {!knife.ready ? ( + {knife.failed ? ( + <> +

{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(); + } +});