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
+ }
+ }
}
}