feat: overhaul server automation, files editor, and CS2 setup workflows

This commit is contained in:
2026-02-26 21:01:00 +00:00
parent 44c439e2f9
commit 2a3ad5e78f
40 changed files with 4675 additions and 468 deletions
@@ -0,0 +1,36 @@
ALTER TABLE "games"
ADD COLUMN IF NOT EXISTS "automation_rules" jsonb DEFAULT '[]'::jsonb NOT NULL;
UPDATE "games"
SET
"automation_rules" = '[
{
"id": "cs2-install-latest-counterstrikesharp-runtime",
"event": "server.install.completed",
"enabled": true,
"runOncePerServer": true,
"continueOnError": false,
"actions": [
{
"id": "install-cs2-runtime",
"type": "github_release_extract",
"owner": "roflmuffin",
"repo": "CounterStrikeSharp",
"assetNamePatterns": [
"^counterstrikesharp-with-runtime-.*linux.*\\\\.zip$",
"^counterstrikesharp-with-runtime.*\\\\.zip$"
],
"destination": "/game/csgo",
"stripComponents": 0,
"maxBytes": 268435456
}
]
}
]'::jsonb,
"updated_at" = now()
WHERE
"slug" = 'cs2'
AND (
"automation_rules" IS NULL
OR "automation_rules" = '[]'::jsonb
);
@@ -0,0 +1,35 @@
WITH metamod_rule AS (
SELECT '[
{
"id": "cs2-install-latest-metamod",
"event": "server.install.completed",
"enabled": true,
"runOncePerServer": true,
"continueOnError": false,
"actions": [
{
"id": "install-cs2-metamod",
"type": "http_directory_extract",
"indexUrl": "https://mms.alliedmods.net/mmsdrop/2.0/",
"assetNamePattern": "^mmsource-2\\.0\\.0-git\\d+-linux\\.tar\\.gz$",
"destination": "/game/csgo",
"stripComponents": 0,
"maxBytes": 268435456
}
]
}
]'::jsonb AS rule
)
UPDATE "games" g
SET
"automation_rules" = CASE
WHEN g."automation_rules" IS NULL OR jsonb_typeof(g."automation_rules") <> 'array'
THEN (SELECT rule FROM metamod_rule)
ELSE g."automation_rules" || (SELECT rule FROM metamod_rule)
END,
"updated_at" = now()
WHERE
g."slug" = 'cs2'
AND NOT (
COALESCE(g."automation_rules", '[]'::jsonb) @> '[{"id":"cs2-install-latest-metamod"}]'::jsonb
);
@@ -0,0 +1,27 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1771748754705,
"tag": "0000_red_sunset_bain",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1772200000000,
"tag": "0001_game_automation_rules",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1772300000000,
"tag": "0002_cs2_add_metamod_workflow",
"breakpoints": true
}
]
}
+1
View File
@@ -7,6 +7,7 @@ export const games = pgTable('games', {
dockerImage: text('docker_image').notNull(),
defaultPort: integer('default_port').notNull(),
configFiles: jsonb('config_files').default([]).notNull(),
automationRules: jsonb('automation_rules').default([]).notNull(),
startupCommand: text('startup_command').notNull(),
stopCommand: text('stop_command'),
environmentVars: jsonb('environment_vars').default([]).notNull(),
+56 -12
View File
@@ -91,14 +91,13 @@ async function seed() {
{
slug: 'cs2',
name: 'Counter-Strike 2',
dockerImage: 'cm2network/csgo:latest',
dockerImage: 'cm2network/cs2:latest',
defaultPort: 27015,
startupCommand:
'./srcds_run -game csgo -console -usercon +game_type 0 +game_mode 0 +mapgroup mg_active +map de_dust2',
startupCommand: '',
stopCommand: 'quit',
configFiles: [
{
path: 'csgo/cfg/server.cfg',
path: 'game/csgo/cfg/server.cfg',
parser: 'keyvalue',
editableKeys: [
'hostname',
@@ -109,21 +108,66 @@ async function seed() {
'mp_limitteams',
],
},
{ path: 'csgo/cfg/autoexec.cfg', parser: 'keyvalue' },
{ path: 'game/csgo/cfg/autoexec.cfg', parser: 'keyvalue' },
],
automationRules: [
{
id: 'cs2-install-latest-metamod',
event: 'server.install.completed',
enabled: true,
runOncePerServer: true,
continueOnError: false,
actions: [
{
id: 'install-cs2-metamod',
type: 'http_directory_extract',
indexUrl: 'https://mms.alliedmods.net/mmsdrop/2.0/',
assetNamePattern: '^mmsource-2\\.0\\.0-git\\d+-linux\\.tar\\.gz$',
destination: '/game/csgo',
stripComponents: 0,
maxBytes: 256 * 1024 * 1024,
},
],
},
{
id: 'cs2-install-latest-counterstrikesharp-runtime',
event: 'server.install.completed',
enabled: true,
runOncePerServer: true,
continueOnError: false,
actions: [
{
id: 'install-cs2-runtime',
type: 'github_release_extract',
owner: 'roflmuffin',
repo: 'CounterStrikeSharp',
assetNamePatterns: [
'^counterstrikesharp-with-runtime-.*linux.*\\.zip$',
'^counterstrikesharp-with-runtime.*\\.zip$',
],
destination: '/game/csgo',
stripComponents: 0,
maxBytes: 256 * 1024 * 1024,
},
],
},
],
environmentVars: [
{
key: 'SRCDS_TOKEN',
default: '',
description: 'Steam Game Server Login Token',
required: true,
description: 'Steam Game Server Login Token (optional for local testing)',
required: false,
},
{ key: 'SRCDS_RCONPW', default: '', description: 'RCON password', required: false },
{ key: 'SRCDS_PW', default: '', description: 'Server password', required: false },
{ key: 'CS2_SERVERNAME', default: 'GamePanel CS2 Server', description: 'Server name', required: false },
{ key: 'CS2_PORT', default: '27015', description: 'Game port', required: false },
{ key: 'CS2_STARTMAP', default: 'de_dust2', description: 'Initial map', required: false },
{ key: 'CS2_MAXPLAYERS', default: '16', description: 'Max players', required: false },
{ key: 'CS2_RCONPW', default: '', description: 'RCON password', required: false },
{
key: 'SRCDS_MAXPLAYERS',
default: '16',
description: 'Max players',
key: 'CS2_IP',
default: '0.0.0.0',
description: 'Bind address',
required: false,
},
],
+7 -1
View File
@@ -1,4 +1,10 @@
// Proto generated types will be exported here after running `pnpm generate`
// For now, this is a placeholder
export const PROTO_PATH = new URL('../daemon.proto', import.meta.url).pathname;
const moduleUrl = (import.meta as ImportMeta & { url: string }).url;
export const PROTO_PATH = decodeURIComponent(
moduleUrl
.replace(/^file:\/\//, '')
.replace(/\/src\/index\.(ts|js)$/, '/daemon.proto'),
);
+64
View File
@@ -10,6 +10,68 @@ export type PluginSource = 'spiget' | 'manual';
export type ConfigParser = 'properties' | 'json' | 'yaml' | 'keyvalue';
export type ServerAutomationEvent =
| 'server.created'
| 'server.install.completed'
| 'server.power.started'
| 'server.power.stopped';
export type ServerAutomationActionType =
| 'github_release_extract'
| 'http_directory_extract'
| 'write_file'
| 'send_command';
export interface ServerAutomationGitHubReleaseExtractAction {
id: string;
type: 'github_release_extract';
owner: string;
repo: string;
assetNamePatterns: string[];
destination?: string;
stripComponents?: number;
maxBytes?: number;
}
export interface ServerAutomationWriteFileAction {
id: string;
type: 'write_file';
path: string;
data: string;
encoding?: 'utf8' | 'base64';
}
export interface ServerAutomationHttpDirectoryExtractAction {
id: string;
type: 'http_directory_extract';
indexUrl: string;
assetNamePattern: string;
destination?: string;
stripComponents?: number;
maxBytes?: number;
}
export interface ServerAutomationSendCommandAction {
id: string;
type: 'send_command';
command: string;
}
export type ServerAutomationAction =
| ServerAutomationGitHubReleaseExtractAction
| ServerAutomationHttpDirectoryExtractAction
| ServerAutomationWriteFileAction
| ServerAutomationSendCommandAction;
export interface GameAutomationWorkflow {
id: string;
event: ServerAutomationEvent;
enabled?: boolean;
runOncePerServer?: boolean;
continueOnError?: boolean;
actions: ServerAutomationAction[];
}
export interface GameConfigFile {
path: string;
parser: ConfigParser;
@@ -23,6 +85,8 @@ export interface GameEnvVar {
required: boolean;
}
export type GameAutomationRule = GameAutomationWorkflow;
export interface ConfigEntry {
key: string;
value: string;