feat: patch cs2 gameinfo after metamod install
This commit is contained in:
parent
2a3ad5e78f
commit
c7d1627e18
|
|
@ -9,6 +9,7 @@ import type {
|
|||
ServerAutomationAction,
|
||||
ServerAutomationGitHubReleaseExtractAction,
|
||||
ServerAutomationHttpDirectoryExtractAction,
|
||||
ServerAutomationInsertBeforeLineAction,
|
||||
} from '@source/shared';
|
||||
import {
|
||||
daemonReadFile,
|
||||
|
|
@ -20,6 +21,21 @@ import {
|
|||
const DEFAULT_RELEASE_MAX_BYTES = 256 * 1024 * 1024;
|
||||
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 120_000;
|
||||
const AUTOMATION_MARKER_ROOT = '/.gamepanel/automation';
|
||||
const CS2_GAMEINFO_PATH = '/game/csgo/gameinfo.gi';
|
||||
const CS2_GAMEINFO_METAMOD_LINE = '\t\t\tGame csgo/addons/metamod';
|
||||
const CS2_GAMEINFO_INSERT_BEFORE_PATTERN = '^\\s*Game\\s+csgo\\s*$';
|
||||
const CS2_GAMEINFO_EXISTS_PATTERN = '^\\s*Game\\s+csgo/addons/metamod\\s*$';
|
||||
const CS2_GAMEINFO_INSERT_ACTION_ID = 'ensure-cs2-metamod-gameinfo-entry';
|
||||
|
||||
const DEFAULT_CS2_GAMEINFO_INSERT_ACTION: ServerAutomationInsertBeforeLineAction = {
|
||||
id: CS2_GAMEINFO_INSERT_ACTION_ID,
|
||||
type: 'insert_before_line',
|
||||
path: CS2_GAMEINFO_PATH,
|
||||
line: CS2_GAMEINFO_METAMOD_LINE,
|
||||
beforePattern: CS2_GAMEINFO_INSERT_BEFORE_PATTERN,
|
||||
existsPattern: CS2_GAMEINFO_EXISTS_PATTERN,
|
||||
skipIfExists: true,
|
||||
};
|
||||
|
||||
const DEFAULT_GAME_AUTOMATION_RULES: Record<string, GameAutomationRule[]> = {
|
||||
cs2: [
|
||||
|
|
@ -39,6 +55,7 @@ const DEFAULT_GAME_AUTOMATION_RULES: Record<string, GameAutomationRule[]> = {
|
|||
stripComponents: 0,
|
||||
maxBytes: DEFAULT_RELEASE_MAX_BYTES,
|
||||
},
|
||||
{ ...DEFAULT_CS2_GAMEINFO_INSERT_ACTION },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -129,8 +146,8 @@ function normalizeWorkflow(
|
|||
workflow: GameAutomationRule,
|
||||
): GameAutomationRule {
|
||||
if (gameSlug.toLowerCase() !== 'cs2') return workflow;
|
||||
if (workflow.id !== 'cs2-install-latest-counterstrikesharp-runtime') return workflow;
|
||||
|
||||
if (workflow.id === 'cs2-install-latest-counterstrikesharp-runtime') {
|
||||
const normalizedActions = workflow.actions.map((action) => {
|
||||
if (action.type !== 'github_release_extract') return action;
|
||||
if (action.id !== 'install-cs2-runtime') return action;
|
||||
|
|
@ -148,6 +165,24 @@ function normalizeWorkflow(
|
|||
...workflow,
|
||||
actions: normalizedActions,
|
||||
};
|
||||
}
|
||||
|
||||
if (workflow.id === 'cs2-install-latest-metamod') {
|
||||
const hasGameInfoAction = workflow.actions.some(
|
||||
(action) =>
|
||||
action.type === 'insert_before_line' &&
|
||||
(action.id === CS2_GAMEINFO_INSERT_ACTION_ID || action.path === CS2_GAMEINFO_PATH),
|
||||
);
|
||||
|
||||
if (hasGameInfoAction) return workflow;
|
||||
|
||||
return {
|
||||
...workflow,
|
||||
actions: [...workflow.actions, { ...DEFAULT_CS2_GAMEINFO_INSERT_ACTION }],
|
||||
};
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
function asAutomationRules(raw: unknown, gameSlug: string): GameAutomationRule[] {
|
||||
|
|
@ -609,6 +644,76 @@ async function executeHttpDirectoryExtract(
|
|||
);
|
||||
}
|
||||
|
||||
async function executeInsertBeforeLine(
|
||||
app: FastifyInstance,
|
||||
context: ServerAutomationContext,
|
||||
action: ServerAutomationInsertBeforeLineAction,
|
||||
): Promise<void> {
|
||||
const file = await daemonReadFile(context.node, context.serverUuid, action.path);
|
||||
const content = file.data.toString('utf8');
|
||||
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
||||
const hasTrailingEol = content.endsWith('\n');
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
if (hasTrailingEol && lines[lines.length - 1] === '') {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
const skipIfExists = action.skipIfExists !== false;
|
||||
if (skipIfExists) {
|
||||
const existsRegex = action.existsPattern
|
||||
? new RegExp(action.existsPattern, 'i')
|
||||
: null;
|
||||
|
||||
const alreadyExists = lines.some((line) =>
|
||||
existsRegex ? existsRegex.test(line) : line === action.line,
|
||||
);
|
||||
|
||||
if (alreadyExists) {
|
||||
app.log.info(
|
||||
{
|
||||
serverId: context.serverId,
|
||||
serverUuid: context.serverUuid,
|
||||
event: context.event,
|
||||
actionId: action.id,
|
||||
path: action.path,
|
||||
},
|
||||
'Automation action skipped: line already present',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let beforeRegex: RegExp;
|
||||
try {
|
||||
beforeRegex = new RegExp(action.beforePattern);
|
||||
} catch {
|
||||
throw new Error(`Invalid beforePattern regex for action ${action.id}`);
|
||||
}
|
||||
|
||||
const insertIndex = lines.findIndex((line) => beforeRegex.test(line));
|
||||
if (insertIndex < 0) {
|
||||
throw new Error(
|
||||
`Could not find insertion point in ${action.path} with pattern: ${action.beforePattern}`,
|
||||
);
|
||||
}
|
||||
|
||||
const updated = [...lines.slice(0, insertIndex), action.line, ...lines.slice(insertIndex)];
|
||||
const output = `${updated.join(eol)}${hasTrailingEol ? eol : ''}`;
|
||||
await daemonWriteFile(context.node, context.serverUuid, action.path, output);
|
||||
|
||||
app.log.info(
|
||||
{
|
||||
serverId: context.serverId,
|
||||
serverUuid: context.serverUuid,
|
||||
event: context.event,
|
||||
actionId: action.id,
|
||||
path: action.path,
|
||||
},
|
||||
'Automation action completed: insert_before_line',
|
||||
);
|
||||
}
|
||||
|
||||
async function executeAction(
|
||||
app: FastifyInstance,
|
||||
context: ServerAutomationContext,
|
||||
|
|
@ -625,6 +730,11 @@ async function executeAction(
|
|||
return;
|
||||
}
|
||||
|
||||
case 'insert_before_line': {
|
||||
await executeInsertBeforeLine(app, context, action);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'write_file': {
|
||||
const payload =
|
||||
action.encoding === 'base64'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { FastifyInstance } from 'fastify';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { nodes, servers } from '@source/database';
|
||||
import { games, nodes, servers } from '@source/database';
|
||||
import { AppError } from '../../lib/errors.js';
|
||||
import { requirePermission } from '../../lib/permissions.js';
|
||||
import {
|
||||
|
|
@ -19,6 +19,17 @@ const FileParamSchema = {
|
|||
}),
|
||||
};
|
||||
|
||||
function shouldHideFileForGame(gameSlug: string, fileName: string, isDirectory: boolean): boolean {
|
||||
if (gameSlug !== 'cs2') return false;
|
||||
if (isDirectory) return false;
|
||||
|
||||
const normalizedName = fileName.trim().toLowerCase();
|
||||
if (normalizedName.endsWith('.vpk')) return true;
|
||||
if (/^backup_round.*\.txt$/.test(normalizedName)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function decodeBase64Payload(data: string): Buffer {
|
||||
const normalized = data.trim();
|
||||
if (!normalized) return Buffer.alloc(0);
|
||||
|
|
@ -56,7 +67,11 @@ export default async function fileRoutes(app: FastifyInstance) {
|
|||
path?.trim() || '/',
|
||||
);
|
||||
|
||||
return { files };
|
||||
const filteredFiles = files.filter(
|
||||
(file) => !shouldHideFileForGame(serverContext.gameSlug, file.name, file.isDirectory),
|
||||
);
|
||||
|
||||
return { files: filteredFiles };
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -151,17 +166,20 @@ export default async function fileRoutes(app: FastifyInstance) {
|
|||
|
||||
async function getServerContext(app: FastifyInstance, orgId: string, serverId: string): Promise<{
|
||||
serverUuid: string;
|
||||
gameSlug: string;
|
||||
node: DaemonNodeConnection;
|
||||
}> {
|
||||
const [server] = await app.db
|
||||
.select({
|
||||
uuid: servers.uuid,
|
||||
gameSlug: games.slug,
|
||||
nodeFqdn: nodes.fqdn,
|
||||
nodeGrpcPort: nodes.grpcPort,
|
||||
nodeDaemonToken: nodes.daemonToken,
|
||||
})
|
||||
.from(servers)
|
||||
.innerJoin(nodes, eq(servers.nodeId, nodes.id))
|
||||
.innerJoin(games, eq(servers.gameId, games.id))
|
||||
.where(and(eq(servers.id, serverId), eq(servers.organizationId, orgId)));
|
||||
|
||||
if (!server) {
|
||||
|
|
@ -170,6 +188,7 @@ async function getServerContext(app: FastifyInstance, orgId: string, serverId: s
|
|||
|
||||
return {
|
||||
serverUuid: server.uuid,
|
||||
gameSlug: server.gameSlug,
|
||||
node: {
|
||||
fqdn: server.nodeFqdn,
|
||||
grpcPort: server.nodeGrpcPort,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,15 @@ WITH metamod_rule AS (
|
|||
"destination": "/game/csgo",
|
||||
"stripComponents": 0,
|
||||
"maxBytes": 268435456
|
||||
},
|
||||
{
|
||||
"id": "ensure-cs2-metamod-gameinfo-entry",
|
||||
"type": "insert_before_line",
|
||||
"path": "/game/csgo/gameinfo.gi",
|
||||
"line": "\\t\\t\\tGame csgo/addons/metamod",
|
||||
"beforePattern": "^\\\\s*Game\\\\s+csgo\\\\s*$",
|
||||
"existsPattern": "^\\\\s*Game\\\\s+csgo/addons/metamod\\\\s*$",
|
||||
"skipIfExists": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,15 @@ async function seed() {
|
|||
stripComponents: 0,
|
||||
maxBytes: 256 * 1024 * 1024,
|
||||
},
|
||||
{
|
||||
id: 'ensure-cs2-metamod-gameinfo-entry',
|
||||
type: 'insert_before_line',
|
||||
path: '/game/csgo/gameinfo.gi',
|
||||
line: '\t\t\tGame csgo/addons/metamod',
|
||||
beforePattern: '^\\s*Game\\s+csgo\\s*$',
|
||||
existsPattern: '^\\s*Game\\s+csgo/addons/metamod\\s*$',
|
||||
skipIfExists: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type ServerAutomationEvent =
|
|||
export type ServerAutomationActionType =
|
||||
| 'github_release_extract'
|
||||
| 'http_directory_extract'
|
||||
| 'insert_before_line'
|
||||
| 'write_file'
|
||||
| 'send_command';
|
||||
|
||||
|
|
@ -51,6 +52,16 @@ export interface ServerAutomationHttpDirectoryExtractAction {
|
|||
maxBytes?: number;
|
||||
}
|
||||
|
||||
export interface ServerAutomationInsertBeforeLineAction {
|
||||
id: string;
|
||||
type: 'insert_before_line';
|
||||
path: string;
|
||||
line: string;
|
||||
beforePattern: string;
|
||||
existsPattern?: string;
|
||||
skipIfExists?: boolean;
|
||||
}
|
||||
|
||||
export interface ServerAutomationSendCommandAction {
|
||||
id: string;
|
||||
type: 'send_command';
|
||||
|
|
@ -60,6 +71,7 @@ export interface ServerAutomationSendCommandAction {
|
|||
export type ServerAutomationAction =
|
||||
| ServerAutomationGitHubReleaseExtractAction
|
||||
| ServerAutomationHttpDirectoryExtractAction
|
||||
| ServerAutomationInsertBeforeLineAction
|
||||
| ServerAutomationWriteFileAction
|
||||
| ServerAutomationSendCommandAction;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue