Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/discord/plugins/bump-join-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { queue } from '../../queue-auto'
import { client } from '../client'
import { safe } from '../../utils/safe'
import { queuePromptMutex } from '../queue-prompt-mutex'
import { defaultGamemode } from '../../shared/default-gamemode'

// eslint-disable-next-line @typescript-eslint/require-await
export default fp(async app => {
Expand Down Expand Up @@ -39,7 +40,7 @@ async function ensurePromptIsVisible() {
}

const thresholdRatio = config.bumpPlayerThresholdRatio
const slots = await queue.getSlots()
const slots = await queue.getSlots(defaultGamemode)
const playerCount = slots.filter(slot => !!slot.player).length
const requiredPlayerCount = slots.length

Expand Down
5 changes: 3 additions & 2 deletions src/discord/plugins/send-join-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { Tf2ClassName } from '../../shared/types/tf2-class-name'
import type { QueueSlotModel } from '../../database/models/queue-slot.model'
import { collections } from '../../database/collections'
import { forEachEnabledChannel } from '../for-each-enabled-channel'
import { defaultGamemode } from '../../shared/default-gamemode'
import { getMessage } from '../get-message'
import { safe } from '../../utils/safe'
import { queuePromptMutex } from '../queue-prompt-mutex'
Expand All @@ -31,10 +32,10 @@ const iconUrl = `${environment.WEBSITE_URL}/favicon.png`

async function refreshPrompt() {
await queuePromptMutex.runExclusive(async () => {
const slots = await queue.getSlots()
const slots = await queue.getSlots(defaultGamemode)
const playerCount = slots.filter(slot => !!slot.player).length
const requiredPlayerCount = slots.length
const mapVoteResults = await queue.getMapVoteResults()
const mapVoteResults = await queue.getMapVoteResults(defaultGamemode)
await forEachEnabledChannel('queuePrompts', async channel => {
const embed = queuePreview({
playerCount,
Expand Down
8 changes: 8 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { Configuration } from './database/models/configuration-entry.model'
import type { MumbleClientStatus } from './mumble/status'
import type { ChatMessageModel } from './database/models/chat-message.model'
import type { GameSlotId } from './shared/types/game-slot-id'
import type { Gamemode } from './shared/types/gamemode'
import type { WithId } from 'mongodb'

export interface Events {
Expand Down Expand Up @@ -202,29 +203,36 @@ export interface Events {
maps: MapPoolEntry[]
}
'queue/slots:updated': {
gamemode: Gamemode
slots: QueueSlotModel[]
}
'queue/state:updated': {
gamemode: Gamemode
state: QueueState
}
'queue/mapOptions:reset': {
gamemode: Gamemode
mapOptions: string[]
}
'queue/mapVoteResults:updated': {
gamemode: Gamemode
results: Record<string, number>
}
'queue/friendship:created': {
gamemode: Gamemode
source: SteamId64
target: SteamId64
}
'queue/friendship:updated': {
gamemode: Gamemode
source: SteamId64
target: {
before: SteamId64
after: SteamId64
}
}
'queue/friendship:removed': {
gamemode: Gamemode
source: SteamId64
target: SteamId64
}
Expand Down
3 changes: 2 additions & 1 deletion src/games/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export async function create(
) {
const playerSlots: PlayerSlot[] = await Promise.all(queueSlots.map(queueSlotToPlayerSlot))
const slots = pickTeams(playerSlots, { friends })
const gamemode = queueSlots[0]?.gamemode ?? defaultGamemode

const { insertedId } = await collections.games.insertOne({
number: await getNextGameNumber(),
gamemode: defaultGamemode,
gamemode,
map,
state: GameState.created,
slots: slots.map(slot => ({
Expand Down
5 changes: 3 additions & 2 deletions src/games/launch-game.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { launchGame } from './launch-game'
import { create } from './create'
import { assignGameServer } from './assign-game-server'
import { queue } from '../queue-auto'
import { Gamemode } from '../shared/types/gamemode'

describe('launchGame()', () => {
beforeEach(() => {
Expand All @@ -39,7 +40,7 @@ describe('launchGame()', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(create).mockResolvedValue({ number: 42 } as any)

await launchGame()
await launchGame(Gamemode.sixes)

expect(assignGameServer).toHaveBeenCalledWith(42, { retries: 3 })
expect(queue.unreadyQueue).not.toHaveBeenCalled()
Expand All @@ -48,7 +49,7 @@ describe('launchGame()', () => {
it('reverts the queue when game creation fails', async () => {
vi.mocked(create).mockRejectedValue(new Error('queue slot medic-1 is empty'))

await launchGame()
await launchGame(Gamemode.sixes)

expect(queue.unreadyQueue).toHaveBeenCalled()
expect(assignGameServer).not.toHaveBeenCalled()
Expand Down
15 changes: 8 additions & 7 deletions src/games/launch-game.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import type { GameModel } from '../database/models/game.model'
import { logger } from '../logger'
import { queue } from '../queue-auto'
import type { Gamemode } from '../shared/types/gamemode'
import { assignGameServer } from './assign-game-server'
import { create } from './create'
import { configure } from './rcon/configure'

export async function launchGame() {
logger.info('launching game')
export async function launchGame(gamemode: Gamemode) {
logger.info({ gamemode }, 'launching game')

let game: GameModel
try {
const slots = await queue.getSlots()
const map = await queue.getMapWinner()
const friends = await queue.getFriends()
logger.trace({ slots, map, friends }, 'launchGame()')
const slots = await queue.getSlots(gamemode)
const map = await queue.getMapWinner(gamemode)
const friends = await queue.getFriends(gamemode)
logger.trace({ gamemode, slots, map, friends }, 'launchGame()')
game = await create(slots, map, friends)
} catch (error) {
logger.error({ error }, 'failed to launch game; reverting queue')
await queue.unreadyQueue()
await queue.unreadyQueue(gamemode)
return
}

Expand Down
15 changes: 9 additions & 6 deletions src/games/plugins/launch-new-game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,32 @@ import { events } from '../../events'
import { QueueState } from '../../database/models/queue-state.model'
import { logger } from '../../logger'
import { queue } from '../../queue-auto'
import { debounce } from 'es-toolkit'
import { safe } from '../../utils/safe'
import { debounceLazy } from '../../utils/debounce-lazy'
import { launchGame } from '../launch-game'
import { assignGameServer } from '../assign-game-server'
import { configure } from '../rcon/configure'
import { getOrphanedGames } from '../get-orphaned-games'
import { collections } from '../../database/collections'
import { GameState } from '../../database/models/game.model'
import { enabledGamemodes } from '../../shared/enabled-gamemodes'

export default fp(
// eslint-disable-next-line @typescript-eslint/require-await
async app => {
const launchGameDebounced = debounce(safe(launchGame), 100)
const launchGameDebounced = debounceLazy(safe(launchGame), 100)

events.on('queue/state:updated', ({ state }) => {
events.on('queue/state:updated', ({ gamemode, state }) => {
if (state === QueueState.launching) {
launchGameDebounced()
launchGameDebounced(gamemode)
}
})

app.addHook('onListen', async () => {
if ((await queue.getState()) === QueueState.launching) {
launchGameDebounced()
for (const gamemode of enabledGamemodes) {
if ((await queue.getState(gamemode)) === QueueState.launching) {
launchGameDebounced(gamemode)
}
}

const orphanedGames = await getOrphanedGames()
Expand Down
14 changes: 6 additions & 8 deletions src/maps/reset-options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { collections } from '../database/collections'
import type { MapPoolEntry } from '../database/models/map-pool-entry.model'
import { events } from '../events'
import { defaultGamemode } from '../shared/default-gamemode'
import type { Gamemode } from '../shared/types/gamemode'
import { mapPool } from './pool'

export async function resetMapOptions() {
export async function resetMapOptions(gamemode: Gamemode) {
if ((await collections.maps.countDocuments()) === 0) {
await mapPool.reset()
}
Expand All @@ -30,10 +30,8 @@ export async function resetMapOptions() {
{ $sample: { size: 3 } },
])
.toArray()
await collections.queueMapOptions.deleteMany({})
await collections.queueMapOptions.insertMany(
choices.map(({ name }) => ({ name, gamemode: defaultGamemode })),
)
await collections.queueMapVotes.deleteMany({})
events.emit('queue/mapOptions:reset', { mapOptions: choices.map(({ name }) => name) })
await collections.queueMapOptions.deleteMany({ gamemode })
await collections.queueMapOptions.insertMany(choices.map(({ name }) => ({ name, gamemode })))
await collections.queueMapVotes.deleteMany({ gamemode })
events.emit('queue/mapOptions:reset', { gamemode, mapOptions: choices.map(({ name }) => name) })
}
12 changes: 8 additions & 4 deletions src/queue-auto/cleanup-friendships.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { collections } from '../database/collections'
import { events } from '../events'
import type { Gamemode } from '../shared/types/gamemode'

export async function cleanupFriendships() {
export async function cleanupFriendships(gamemode: Gamemode) {
const medics = (
await collections.queueSlots
.find({ 'canMakeFriendsWith.0': { $exists: true }, player: { $ne: null } })
.find({ gamemode, 'canMakeFriendsWith.0': { $exists: true }, player: { $ne: null } })
.toArray()
).map(({ player }) => player!.steamId)
const friendships = await collections.queueFriends.find({ source: { $nin: medics } }).toArray()
const friendships = await collections.queueFriends
.find({ gamemode, source: { $nin: medics } })
.toArray()
if (friendships.length === 0) return
await collections.queueFriends.deleteMany({
gamemode,
source: { $in: friendships.map(({ source }) => source) },
})
for (const { source, target } of friendships) {
events.emit('queue/friendship:removed', { source, target })
events.emit('queue/friendship:removed', { gamemode, source, target })
}
}
7 changes: 4 additions & 3 deletions src/queue-auto/get-friends.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { collections } from '../database/collections'
import type { Gamemode } from '../shared/types/gamemode'
import type { SteamId64 } from '../shared/types/steam-id-64'

export async function getFriends(): Promise<SteamId64[][]> {
const friendships = await collections.queueFriends.find().toArray()
const slots = await collections.queueSlots.find().toArray()
export async function getFriends(gamemode: Gamemode): Promise<SteamId64[][]> {
const friendships = await collections.queueFriends.find({ gamemode }).toArray()
const slots = await collections.queueSlots.find({ gamemode }).toArray()

return friendships
.filter(({ source, target }) =>
Expand Down
18 changes: 15 additions & 3 deletions src/queue-auto/get-map-vote-results.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { collections } from '../database/collections'
import type { Gamemode } from '../shared/types/gamemode'

export async function getMapVoteResults(): Promise<Record<string, number>> {
export async function getMapVoteResults(gamemode: Gamemode): Promise<Record<string, number>> {
const results = await collections.queueMapOptions
.aggregate([
{
$match: { gamemode },
},
{
$lookup: {
from: collections.queueMapVotes.collectionName,
localField: 'name',
foreignField: 'map',
let: { map: '$name' },
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ['$gamemode', gamemode] }, { $eq: ['$map', '$$map'] }],
},
},
},
],
as: 'votes',
},
},
Expand Down
10 changes: 7 additions & 3 deletions src/queue-auto/get-map-winner.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { maxBy, sample } from 'es-toolkit'
import { collections } from '../database/collections'
import { logger } from '../logger'
import type { Gamemode } from '../shared/types/gamemode'

export async function getMapWinner(): Promise<string> {
export async function getMapWinner(gamemode: Gamemode): Promise<string> {
const mapsWithVotes = await collections.queueMapOptions
.aggregate<{ name: string; votes: number }>([
{
$match: { gamemode },
},
{
$lookup: {
from: 'queue.mapvotes',
Expand All @@ -15,7 +19,7 @@ export async function getMapWinner(): Promise<string> {
{
$match: {
$expr: {
$eq: ['$map', '$$map'],
$and: [{ $eq: ['$gamemode', gamemode] }, { $eq: ['$map', '$$map'] }],
},
},
},
Expand All @@ -34,7 +38,7 @@ export async function getMapWinner(): Promise<string> {
},
])
.toArray()
logger.trace({ mapsWithVotes }, 'queue.getMapWinner()')
logger.trace({ gamemode, mapsWithVotes }, 'queue.getMapWinner()')
const maxVotes = maxBy(mapsWithVotes, r => r.votes)?.votes ?? 0
const mapsWithMaxVotes = mapsWithVotes.filter(m => m.votes === maxVotes)
return sample(mapsWithMaxVotes).name
Expand Down
5 changes: 3 additions & 2 deletions src/queue-auto/get-slots.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { collections } from '../database/collections'
import type { QueueSlotModel } from '../database/models/queue-slot.model'
import type { Gamemode } from '../shared/types/gamemode'

export async function getSlots(): Promise<QueueSlotModel[]> {
return await collections.queueSlots.find().toArray()
export async function getSlots(gamemode: Gamemode): Promise<QueueSlotModel[]> {
return await collections.queueSlots.find({ gamemode }).toArray()
}
11 changes: 7 additions & 4 deletions src/queue-auto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import { getFriends } from './get-friends'
import { getMapVoteResults } from './get-map-vote-results'
import { unreadyQueue } from './unready-queue'
import { kick } from './kick'
import { enabledGamemodes } from '../shared/enabled-gamemodes'

const slotCount = await collections.queueSlots.countDocuments()
if (slotCount === 0) {
logger.info(`no queue initialized, initializing one now...`)
await reset()
for (const gamemode of enabledGamemodes) {
const slotCount = await collections.queueSlots.countDocuments({ gamemode })
if (slotCount === 0) {
logger.info(`no queue initialized for ${gamemode}, initializing one now...`)
await reset(gamemode)
}
}

export const queue = {
Expand Down
Loading
Loading