diff --git a/src/main.ts b/src/main.ts index 7f8accce..20d7110a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -51,5 +51,6 @@ await app.register((await import('./statistics')).default) await app.register((await import('./twitch-tv')).default) await app.register((await import('./admin')).default) await app.register((await import('./hall-of-game')).default) +await app.register((await import('./pre-ready')).default) await app.listen({ host: environment.APP_HOST, port: environment.APP_PORT }) diff --git a/src/players/plugins/timeout-pre-ready.ts b/src/players/plugins/timeout-pre-ready.ts deleted file mode 100644 index bc3b7f2b..00000000 --- a/src/players/plugins/timeout-pre-ready.ts +++ /dev/null @@ -1,22 +0,0 @@ -import fp from 'fastify-plugin' -import { collections } from '../../database/collections' -import { update } from '../update' -import { secondsToMilliseconds } from 'date-fns' -import { events } from '../../events' - -async function process() { - const toRemove = await collections.players.find({ preReadyUntil: { $lte: new Date() } }).toArray() - for (const p of toRemove) { - await update(p.steamId, { $unset: { preReadyUntil: 1 } }) - } -} - -export default fp(async () => { - setInterval(process, secondsToMilliseconds(1)) - - events.on('game:created', async ({ game }) => { - for (const slot of game.slots) { - await update(slot.player, { $unset: { preReadyUntil: 1 } }) - } - }) -}) diff --git a/src/queue/pre-ready/cancel.ts b/src/pre-ready/cancel.ts similarity index 53% rename from src/queue/pre-ready/cancel.ts rename to src/pre-ready/cancel.ts index 59d97c46..d8afa745 100644 --- a/src/queue/pre-ready/cancel.ts +++ b/src/pre-ready/cancel.ts @@ -1,5 +1,5 @@ -import { players } from '../../players' -import type { SteamId64 } from '../../shared/types/steam-id-64' +import { players } from '../players' +import type { SteamId64 } from '../shared/types/steam-id-64' export async function cancel(player: SteamId64) { await players.update(player, { $unset: { preReadyUntil: 1 } }) diff --git a/src/pre-ready/index.ts b/src/pre-ready/index.ts new file mode 100644 index 00000000..9d232352 --- /dev/null +++ b/src/pre-ready/index.ts @@ -0,0 +1,22 @@ +import fp from 'fastify-plugin' +import { cancel } from './cancel' +import { isPreReadied } from './is-pre-readied' +import { start } from './start' +import { toggle } from './toggle' +import { resolve } from 'node:path' + +export const preReady = { + cancel, + isPreReadied, + start, + toggle, +} as const + +export default fp( + async app => { + await app.register((await import('@fastify/autoload')).default, { + dir: resolve(import.meta.dirname, 'plugins'), + }) + }, + { name: 'pre-ready up' }, +) diff --git a/src/queue/pre-ready/is-pre-readied.ts b/src/pre-ready/is-pre-readied.ts similarity index 62% rename from src/queue/pre-ready/is-pre-readied.ts rename to src/pre-ready/is-pre-readied.ts index 97a093ad..773c0898 100644 --- a/src/queue/pre-ready/is-pre-readied.ts +++ b/src/pre-ready/is-pre-readied.ts @@ -1,5 +1,5 @@ -import { players } from '../../players' -import type { SteamId64 } from '../../shared/types/steam-id-64' +import { players } from '../players' +import type { SteamId64 } from '../shared/types/steam-id-64' export async function isPreReadied(player: SteamId64) { const p = await players.bySteamId(player) diff --git a/src/pre-ready/plugins/auto-cancel.ts b/src/pre-ready/plugins/auto-cancel.ts new file mode 100644 index 00000000..76533896 --- /dev/null +++ b/src/pre-ready/plugins/auto-cancel.ts @@ -0,0 +1,33 @@ +import fp from 'fastify-plugin' +import { collections } from '../../database/collections' +import { update } from '../../players/update' +import { secondsToMilliseconds } from 'date-fns' +import { events } from '../../events' +import { safe } from '../../utils/safe' + +async function process() { + const toRemove = await collections.players.find({ preReadyUntil: { $lte: new Date() } }).toArray() + for (const p of toRemove) { + await update(p.steamId, { $unset: { preReadyUntil: 1 } }) + } +} + +export default fp( + async () => { + setInterval(process, secondsToMilliseconds(1)) + + events.on( + 'game:created', + safe(async ({ game }) => { + await Promise.all( + game.slots.map( + async ({ player }) => await update(player, { $unset: { preReadyUntil: 1 } }), + ), + ) + }), + ) + }, + { + name: 'auto cancel pre-ready up', + }, +) diff --git a/src/queue/pre-ready/start.ts b/src/pre-ready/start.ts similarity index 63% rename from src/queue/pre-ready/start.ts rename to src/pre-ready/start.ts index e4c9fead..57ff8a09 100644 --- a/src/queue/pre-ready/start.ts +++ b/src/pre-ready/start.ts @@ -1,6 +1,6 @@ -import { configuration } from '../../configuration' -import { players } from '../../players' -import type { SteamId64 } from '../../shared/types/steam-id-64' +import { configuration } from '../configuration' +import { players } from '../players' +import type { SteamId64 } from '../shared/types/steam-id-64' export async function start(player: SteamId64) { const timeout = await configuration.get('queue.pre_ready_up_timeout') diff --git a/src/queue/pre-ready/toggle.ts b/src/pre-ready/toggle.ts similarity index 80% rename from src/queue/pre-ready/toggle.ts rename to src/pre-ready/toggle.ts index acce94fe..46a4d2fb 100644 --- a/src/queue/pre-ready/toggle.ts +++ b/src/pre-ready/toggle.ts @@ -1,4 +1,4 @@ -import type { SteamId64 } from '../../shared/types/steam-id-64' +import type { SteamId64 } from '../shared/types/steam-id-64' import { cancel } from './cancel' import { isPreReadied } from './is-pre-readied' import { start } from './start' diff --git a/src/queue/views/html/pre-ready-up-button.tsx b/src/pre-ready/views/html/pre-ready-up-button.tsx similarity index 93% rename from src/queue/views/html/pre-ready-up-button.tsx rename to src/pre-ready/views/html/pre-ready-up-button.tsx index 74c3105a..4213a203 100644 --- a/src/queue/views/html/pre-ready-up-button.tsx +++ b/src/pre-ready/views/html/pre-ready-up-button.tsx @@ -23,6 +23,8 @@ export async function PreReadyUpButton(props: { actor?: SteamId64 | undefined }) class="button button--lighter min-w-[200px]" id="pre-ready-up-button" name="prereadytoggle" + ws-send + hx-trigger="click" disabled={!isInQueue} aria-selected={timeLeft > 0} > @@ -49,7 +51,7 @@ PreReadyUpButton.enable = () => {
) @@ -61,7 +63,7 @@ PreReadyUpButton.disable = () => {
) diff --git a/src/queue/join.ts b/src/queue/join.ts index c6c063b9..002a5612 100644 --- a/src/queue/join.ts +++ b/src/queue/join.ts @@ -3,6 +3,7 @@ import type { QueueSlotModel } from '../database/models/queue-slot.model' import { QueueState } from '../database/models/queue-state.model' import { events } from '../events' import { logger } from '../logger' +import { preReady } from '../pre-ready' import type { SteamId64 } from '../shared/types/steam-id-64' import { getState } from './get-state' import { mutex } from './mutex' @@ -69,6 +70,11 @@ export async function join(slotId: number, steamId: SteamId64): Promise { return await mutex.runExclusive(async () => { diff --git a/src/queue/leave.ts b/src/queue/leave.ts index e507e674..92c5ea65 100644 --- a/src/queue/leave.ts +++ b/src/queue/leave.ts @@ -7,7 +7,7 @@ import type { SteamId64 } from '../shared/types/steam-id-64' import { getMapVoteResults } from './get-map-vote-results' import { getState } from './get-state' import { mutex } from './mutex' -import { preReady } from './pre-ready' +import { preReady } from '../pre-ready' export async function leave(steamId: SteamId64): Promise { return await mutex.runExclusive(async () => { diff --git a/src/queue/plugins/gateway-listeners.ts b/src/queue/plugins/gateway-listeners.ts index f097d38a..925c09fc 100644 --- a/src/queue/plugins/gateway-listeners.ts +++ b/src/queue/plugins/gateway-listeners.ts @@ -12,8 +12,8 @@ import type { SteamId64 } from '../../shared/types/steam-id-64' import { markAsFriend } from '../mark-as-friend' import { getState } from '../get-state' import { QueueState } from '../../database/models/queue-state.model' -import { PreReadyUpButton } from '../views/html/pre-ready-up-button' -import { preReady } from '../pre-ready' +import { PreReadyUpButton } from '../../pre-ready/views/html/pre-ready-up-button' +import { preReady } from '../../pre-ready' export default fp( // eslint-disable-next-line @typescript-eslint/require-await diff --git a/src/queue/plugins/sync-clients.ts b/src/queue/plugins/sync-clients.ts index 01a3143a..2a534ce1 100644 --- a/src/queue/plugins/sync-clients.ts +++ b/src/queue/plugins/sync-clients.ts @@ -16,7 +16,7 @@ import { BanAlerts } from '../views/html/ban-alerts' import type { ObjectId } from 'mongodb' import { whenGameEnds } from '../../games/when-game-ends' import { CurrentPlayerCount } from '../views/html/current-player-count' -import { PreReadyUpButton } from '../views/html/pre-ready-up-button' +import { PreReadyUpButton } from '../../pre-ready/views/html/pre-ready-up-button' export default fp( // eslint-disable-next-line @typescript-eslint/require-await diff --git a/src/queue/pre-ready/index.ts b/src/queue/pre-ready/index.ts deleted file mode 100644 index 6c183c0a..00000000 --- a/src/queue/pre-ready/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { cancel } from './cancel' -import { isPreReadied } from './is-pre-readied' -import { start } from './start' -import { toggle } from './toggle' - -export const preReady = { - cancel, - isPreReadied, - start, - toggle, -} as const diff --git a/src/queue/ready-up.ts b/src/queue/ready-up.ts index d19b63e2..10c08518 100644 --- a/src/queue/ready-up.ts +++ b/src/queue/ready-up.ts @@ -6,6 +6,7 @@ import { logger } from '../logger' import type { SteamId64 } from '../shared/types/steam-id-64' import { getState } from './get-state' import { mutex } from './mutex' +import { preReady } from '../pre-ready' export async function readyUp(steamId: SteamId64): Promise { return await mutex.runExclusive(async () => { @@ -25,6 +26,7 @@ export async function readyUp(steamId: SteamId64): Promise { } events.emit('queue/slots:updated', { slots: [slot] }) + await preReady.start(steamId) return slot }) } diff --git a/src/queue/views/html/queue.page.tsx b/src/queue/views/html/queue.page.tsx index 85856a6a..5144c38e 100644 --- a/src/queue/views/html/queue.page.tsx +++ b/src/queue/views/html/queue.page.tsx @@ -21,7 +21,7 @@ import { StreamList } from './stream-list' import { BanAlerts } from './ban-alerts' import { AcceptRulesDialog } from './accept-rules-dialog' import { CurrentPlayerCount } from './current-player-count' -import { PreReadyUpButton } from './pre-ready-up-button' +import { PreReadyUpButton } from '../../../pre-ready/views/html/pre-ready-up-button' export async function QueuePage(props: { user?: User | undefined }) { const slots = await collections.queueSlots.find().toArray() diff --git a/tests/10-queue/07-pre-ready-up.spec.ts b/tests/10-queue/07-pre-ready-up.spec.ts index 3210d522..01dda39f 100644 --- a/tests/10-queue/07-pre-ready-up.spec.ts +++ b/tests/10-queue/07-pre-ready-up.spec.ts @@ -1,6 +1,6 @@ import { mergeTests } from '@playwright/test' import { accessMongoDb } from '../fixtures/access-mongo-db' -import { secondsToMilliseconds } from 'date-fns' +import { minutesToMilliseconds, secondsToMilliseconds } from 'date-fns' import { launchGame, expect } from '../fixtures/launch-game' const test = mergeTests(accessMongoDb, launchGame) @@ -91,3 +91,49 @@ test('pre-ready up readies up when the queue is ready', async ({ } } }) + +test('pre-ready up enables automatically after readying up', async ({ + users, + players, + desiredSlots, +}) => { + const polemic = users.byName('Polemic') + const shadowhunter = users.byName('Shadowhunter') + + await Promise.all( + players + .filter(p => p.playerName !== 'Polemic') + .map(async user => { + const page = await user.queuePage() + await page.goto() + const slot = desiredSlots.get(user.playerName)! + await page.slot(slot).join() + }), + ) + + const page = await polemic.queuePage() + await page.goto() + await page.slot(desiredSlots.get('Polemic')!).join() + await expect(page.preReadyUpButton()).toHaveAttribute('aria-selected') + + { + const page = await shadowhunter.queuePage() + await page.readyUpDialog().readyUp() + await expect(page.preReadyUpButton()).toHaveAttribute('aria-selected') + } + + await Promise.all( + players.map(async user => { + const page = await user.queuePage() + const slot = desiredSlots.get(user.playerName)! + + if (!(await page.slot(slot).isReady())) { + await page.readyUpDialog().notReady() + } + + if (await page.slot(slot).isTaken()) { + await page.leaveQueue(minutesToMilliseconds(1)) + } + }), + ) +}) diff --git a/tests/pages/queue.page.ts b/tests/pages/queue.page.ts index f0207371..5a807536 100644 --- a/tests/pages/queue.page.ts +++ b/tests/pages/queue.page.ts @@ -11,6 +11,14 @@ class QueueSlot { this.locator = this.page.getByLabel(`Queue slot ${this.slotNumber}`, { exact: true }) } + async isTaken() { + return this.locator.locator('player-info').isVisible() + } + + async isReady() { + return (await this.locator.locator('.player-info').getAttribute('data-player-ready')) === 'true' + } + joinButton() { return this.locator.getByRole('button', { name: `Join queue on slot ${this.slotNumber}`, @@ -46,8 +54,19 @@ class ReadyUpDialog { } } + notReadyButton() { + return this.page.getByRole('button', { name: `No, I can't play now` }) + } + async notReady() { - await this.page.getByRole('button', { name: `No, I can't play now` }).click() + const button = this.notReadyButton() + try { + await button.click({ timeout: secondsToMilliseconds(5) }) + } catch (error) { + if (error instanceof errors.TimeoutError) { + return + } + } } }