Skip to content

Commit

Permalink
feat: serveme.tf integration (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
garrappachc authored Dec 22, 2024
1 parent 0c017f0 commit 7873a78
Show file tree
Hide file tree
Showing 29 changed files with 555 additions and 74 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
"@kitajs/html": "4.2.6",
"@kitajs/ts-html-plugin": "4.1.1",
"@tailwindcss/typography": "0.5.15",
"@tf2pickup-org/serveme-tf-client": "0.1.2",
"async-mutex": "0.5.0",
"autoprefixer": "10.4.20",
"country-flag-icons": "1.5.13",
"croner": "9.0.0",
"cssnano": "7.0.6",
"date-fns": "4.1.0",
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './htmx.js'
import 'https://unpkg.com/htmx-ext-head-support@2.0.2/head-support.js'
import 'https://unpkg.com/htmx-ext-ws@2.0.1/ws.js'
import 'https://unpkg.com/hyperscript.org@0.9.13/dist/_hyperscript.min.js'
import 'https://unpkg.com/htmx.org@1.9.12/dist/ext/remove-me.js'

import './countdown.js'
import './fade-scroll.js'
Expand Down
56 changes: 47 additions & 9 deletions src/admin/game-servers/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
import { z } from 'zod'
import { standardAdminPage } from '../plugins/standard-admin-page'
import fp from 'fastify-plugin'
import { GameServersPage } from './views/html/game-servers.page'
import type { ZodTypeProvider } from 'fastify-type-provider-zod'
import { PlayerRole } from '../../database/models/player.model'
import { z } from 'zod'
import { configuration } from '../../configuration'
import { RegionList } from './views/html/serveme-tf-preferred-region'

export default standardAdminPage({
path: '/admin/game-servers',
page: async user => await GameServersPage({ user }),
bodySchema: z.object({}),
// eslint-disable-next-line @typescript-eslint/no-empty-function
save: async () => {},
})
export default fp(
// eslint-disable-next-line @typescript-eslint/require-await
async app => {
app
.withTypeProvider<ZodTypeProvider>()
.get(
'/admin/game-servers',
{
config: {
authorize: [PlayerRole.admin],
},
},
async (request, reply) => {
return reply.status(200).html(GameServersPage({ user: request.user! }))
},
)
.put(
'/admin/game-servers/serveme-tf/preferred-region',
{
config: {
authorize: [PlayerRole.admin],
},
schema: {
body: z.object({
servemeTfPreferredRegion: z.string().transform(val => (val === 'none' ? null : val)),
}),
},
},
async (request, reply) => {
await configuration.set(
'serveme_tf.preferred_region',
request.body.servemeTfPreferredRegion,
)
return reply.status(200).html(RegionList({ saveResult: { success: true } }))
},
)
},
{
name: `admin - game servers`,
},
)
12 changes: 12 additions & 0 deletions src/admin/game-servers/views/html/game-servers.page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { User } from '../../../../auth/types/user'
import { servemeTf } from '../../../../serveme-tf'
import { Admin } from '../../../views/html/admin'
import { ServemeTfPreferredRegion } from './serveme-tf-preferred-region'
import { ServemeTfStatus } from './serveme-tf-status'
import { StaticGameServerList } from './static-game-server-list'

export async function GameServersPage(props: { user: User }) {
Expand All @@ -9,6 +12,15 @@ export async function GameServersPage(props: { user: User }) {
<h4 class="pb-4">Static servers</h4>
<StaticGameServerList />
</div>

<div class="admin-panel-set mt-4">
<div class="flex flex-row gap-2">
<h4 class="pb-4">serveme.tf</h4>
<ServemeTfStatus />
</div>

{servemeTf.isEnabled && <ServemeTfPreferredRegion />}
</div>
</Admin>
)
}
67 changes: 67 additions & 0 deletions src/admin/game-servers/views/html/serveme-tf-preferred-region.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { memoize } from 'lodash-es'
import { servemeTf } from '../../../../serveme-tf'
import getUnicodeFlagIcon from 'country-flag-icons/unicode'
import { configuration } from '../../../../configuration'
import { IconLoader3 } from '../../../../html/components/icons'

export function ServemeTfPreferredRegion() {
return (
<p>
<dl>
<dt>
<label for="serveme-tf-preferred-region-select">
Preferred region for reserved servers
</label>
</dt>
<dd>
<div>
<RegionList />
</div>
<span class="text-sm text-abru-light-75">
If a game server from the preferred region is not available, another one will be picked
up instead.
</span>
</dd>
</dl>
</p>
)
}

const getRegions = memoize(async () => await servemeTf.listRegions())

type SaveResult = { success: true }

export async function RegionList(props?: { saveResult?: SaveResult }) {
const regions = await getRegions()
const selected = await configuration.get('serveme_tf.preferred_region')

return (
<div id="serveme-tf-preferred-region" class="flex flex-row items-center gap-2">
<select
name="servemeTfPreferredRegion"
id="serveme-tf-preferred-region-select"
hx-put="/admin/game-servers/serveme-tf/preferred-region"
hx-trigger="change"
hx-target="#serveme-tf-preferred-region"
hx-swap="outerHTML"
hx-disabled-elt="this"
class="peer"
>
<option value="none" selected={selected === null}>
none
</option>
{regions.map(region => (
<option value={region} selected={region === selected} safe>
{getUnicodeFlagIcon(region)} {region}
</option>
))}
</select>
<IconLoader3 class="hidden animate-spin peer-[.htmx-request]:block" />
{props?.saveResult?.success === true && (
<span remove-me="3s" class="text-green-600">
saved!
</span>
)}
</div>
)
}
16 changes: 16 additions & 0 deletions src/admin/game-servers/views/html/serveme-tf-status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IconCheck, IconX } from '../../../../html/components/icons'
import { servemeTf } from '../../../../serveme-tf'

export function ServemeTfStatus() {
return servemeTf.isEnabled ? (
<span class="flex flex-row text-green-600">
<IconCheck />
enabled
</span>
) : (
<span class="flex flex-row text-red-600">
<IconX />
disabled
</span>
)
}
4 changes: 2 additions & 2 deletions src/admin/plugins/standard-admin-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function standardAdminPage<Input, Output>({
},
},
async (request, reply) => {
reply.status(200).html(await page(request.user!))
return reply.status(200).html(page(request.user!))
},
)
.post(
Expand All @@ -47,7 +47,7 @@ export function standardAdminPage<Input, Output>({
async (request, reply) => {
await save(request.body as Input)
requestContext.set('messages', { success: ['Configuration saved'] })
reply.status(200).html(await page(request.user!))
return reply.status(200).html(page(request.user!))
},
)
},
Expand Down
2 changes: 1 addition & 1 deletion src/database/models/configuration-entry.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export const configurationSchema = z.discriminatedUnion('key', [
value: z.string().nullable().default(null),
}),
z.object({
key: z.literal('serveme.tf.ban_gameservers'),
key: z.literal('serveme_tf.ban_gameservers'),
value: z.array(z.string()).default([]),
}),
z.object({
Expand Down
9 changes: 9 additions & 0 deletions src/database/models/game.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ export interface GameServer {
name: string
address: string
port: string

// if logSecret is undefined, a random one will be assigned automatically
logSecret?: string

rcon: {
address: string
port: string
password: string
}
}

export interface GameModel {
Expand Down
4 changes: 4 additions & 0 deletions src/environment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import dotenv from 'dotenv'
import { KnownEndpoint } from '@tf2pickup-org/serveme-tf-client'

dotenv.config()

Expand All @@ -19,6 +20,9 @@ const environmentSchema = z.object({
GAME_SERVER_SECRET: z.string(),
THUMBNAIL_SERVICE_URL: z.string().url(),

SERVEME_TF_API_ENDPOINT: z.string().default(KnownEndpoint.europe),
SERVEME_TF_API_KEY: z.string().optional(),

TWITCH_CLIENT_ID: z.string().optional(),
TWITCH_CLIENT_SECRET: z.string().optional(),
})
Expand Down
29 changes: 15 additions & 14 deletions src/game-servers/assign.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
import { GameServerProvider, type GameModel } from '../database/models/game.model'
import { type GameModel, type GameServer } from '../database/models/game.model'
import { games } from '../games'
import { staticGameServers } from '../static-game-servers'
import { events } from '../events'
import { GameEventType } from '../database/models/game-event.model'
import { logger } from '../logger'
import { Mutex } from 'async-mutex'
import { servemeTf } from '../serveme-tf'

const mutex = new Mutex()

export async function assign(game: GameModel) {
await mutex.runExclusive(async () => {
const staticGameServer = await staticGameServers.assign(game)
if (!staticGameServer) {
throw new Error(`no free servers available for game ${game.number}`)
}
const gameServer = await assignFirstFree(game)

game = await games.update(game.number, {
$set: {
gameServer: {
id: staticGameServer.id,
name: staticGameServer.name,
address: staticGameServer.address,
port: staticGameServer.port,
provider: GameServerProvider.static,
},
gameServer,
},
$push: {
events: {
event: GameEventType.gameServerAssigned,
at: new Date(),
gameServerName: staticGameServer.name,
gameServerName: gameServer.name,
},
},
})
logger.info({ game }, `game ${game.number} assigned to game server ${staticGameServer.name}`)
logger.info({ game }, `game ${game.number} assigned to game server ${gameServer.name}`)
events.emit('game:gameServerAssigned', { game })
})
}

function assignFirstFree(game: GameModel): Promise<GameServer> {
return staticGameServers
.assign(game)
.catch(() => servemeTf.assign(game))
.catch(() => {
throw new Error(`no free servers available for game ${game.number}`)
})
}
2 changes: 1 addition & 1 deletion src/games/plugins/sync-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export default fp(async app => {
events.on(
'game:playerConnectionStatusUpdated',
safe(async ({ game, player, playerConnectionStatus }) => {
app.gateway.broadcast(
app.gateway.to({ url: `/games/${game.number}` }).send(
async () =>
await PlayerConnectionStatusIndicator({
steamId: player,
Expand Down
Loading

0 comments on commit 7873a78

Please sign in to comment.