Move SCUM lifecycle ownership to plugin

This commit is contained in:
npc0-hue
2026-08-01 09:36:12 +08:00
parent bca4f935ba
commit d1249fca85
49 changed files with 795 additions and 566 deletions
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env sh
set -eu
printf 'dev server started\n'
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env sh
set -eu
printf 'dev install complete\n'
@@ -56,6 +56,15 @@
"restart": "actions/restart.json",
"status": "actions/status.json"
},
"assetFiles": [
{ "path": "actions/install.json", "mode": 384 },
{ "path": "actions/start.json", "mode": 384 },
{ "path": "actions/stop.json", "mode": 384 },
{ "path": "actions/restart.json", "mode": 384 },
{ "path": "actions/status.json", "mode": 384 },
{ "path": "bin/install-server", "mode": 448 },
{ "path": "bin/game-server", "mode": 448 }
],
"productionLifecycle": {
"operations": ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"],
"dependencyPolicy": "optional",
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env sh
set -eu
printf 'minecraft server started\n'
@@ -0,0 +1,3 @@
#!/usr/bin/env sh
set -eu
printf 'minecraft install complete\n'
@@ -98,6 +98,15 @@
"restart": "actions/restart.json",
"status": "actions/status.json"
},
"assetFiles": [
{ "path": "actions/install.json", "mode": 384 },
{ "path": "actions/start.json", "mode": 384 },
{ "path": "actions/stop.json", "mode": 384 },
{ "path": "actions/restart.json", "mode": 384 },
{ "path": "actions/status.json", "mode": 384 },
{ "path": "bin/install-server", "mode": 448 },
{ "path": "bin/game-server", "mode": 448 }
],
"productionLifecycle": {
"operations": ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"],
"dependencyPolicy": "required",
@@ -2,11 +2,15 @@
"version": 1,
"action": "install",
"mode": "oneshot",
"executableKey": "bin/install-server",
"executableKey": "bin/scum-install-update.cmd",
"arguments": [],
"environment": {
"GAME_ID": "scum",
"SERVER_TEMPLATE": "scum-server"
"SERVER_TEMPLATE": "scum-server",
"SERVER_STEAM_APP_ID": "3792580",
"SERVER_STEAMCMD_INSTALL_DIR_ARG": "+force_install_dir",
"SERVER_STEAMCMD_UPDATE_ARGS": "+login anonymous +app_update 3792580 validate +quit",
"SERVER_EXECUTABLE_REF": "SCUM/Binaries/Win64/SCUMServer.exe"
},
"timeoutMs": 30000
"timeoutMs": 300000
}
@@ -2,11 +2,15 @@
"version": 1,
"action": "start",
"mode": "supervised",
"executableKey": "bin/game-server",
"arguments": ["--foreground"],
"executableKey": "bin/scum-start.cmd",
"arguments": [],
"environment": {
"GAME_ID": "scum",
"SERVER_ACTION": "start"
"SERVER_ACTION": "start",
"SERVER_EXECUTABLE_REF": "SCUM/Binaries/Win64/SCUMServer.exe",
"SERVER_PORT_FIELD": "gamePort",
"SERVER_MAX_PLAYERS_FIELD": "maxPlayers",
"SERVER_LOG_FLAG": "-log"
},
"timeoutMs": 30000
}
@@ -0,0 +1,31 @@
@echo off
setlocal EnableExtensions
if "%SERVER_ROOT%"=="" set "SERVER_ROOT=C:\scumserver"
if "%SERVER_STEAM_APP_ID%"=="" set "SERVER_STEAM_APP_ID=3792580"
if "%SERVER_STEAMCMD_INSTALL_DIR_ARG%"=="" set "SERVER_STEAMCMD_INSTALL_DIR_ARG=+force_install_dir"
if "%SERVER_STEAMCMD_UPDATE_ARGS%"=="" set "SERVER_STEAMCMD_UPDATE_ARGS=+login anonymous +app_update 3792580 validate +quit"
if "%SERVER_EXECUTABLE_REF%"=="" set "SERVER_EXECUTABLE_REF=SCUM\Binaries\Win64\SCUMServer.exe"
set "STEAMCMD_DIR=%SERVER_ROOT%\steamcmd"
set "STEAMCMD_EXE=%STEAMCMD_DIR%\steamcmd.exe"
set "SCUM_EXE=%SERVER_ROOT%\%SERVER_EXECUTABLE_REF:/=\%"
if not exist "%SERVER_ROOT%" mkdir "%SERVER_ROOT%"
if not exist "%STEAMCMD_DIR%" mkdir "%STEAMCMD_DIR%"
if not exist "%STEAMCMD_EXE%" (
powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $zip=Join-Path $env:STEAMCMD_DIR 'steamcmd.zip'; Invoke-WebRequest -Uri 'https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip' -OutFile $zip; Expand-Archive -Path $zip -DestinationPath $env:STEAMCMD_DIR -Force; Remove-Item $zip -Force"
if errorlevel 1 exit /b 1
)
taskkill /IM SCUMServer.exe /F >nul 2>nul
pushd "%STEAMCMD_DIR%"
"%STEAMCMD_EXE%" %SERVER_STEAMCMD_INSTALL_DIR_ARG% "%SERVER_ROOT%" %SERVER_STEAMCMD_UPDATE_ARGS%
set "SCUM_STEAMCMD_RESULT=%ERRORLEVEL%"
popd
if not "%SCUM_STEAMCMD_RESULT%"=="0" exit /b %SCUM_STEAMCMD_RESULT%
if not exist "%SCUM_EXE%" exit /b 2
exit /b 0
+17
View File
@@ -0,0 +1,17 @@
@echo off
setlocal EnableExtensions
if "%SERVER_ROOT%"=="" set "SERVER_ROOT=C:\scumserver"
if "%SERVER_EXECUTABLE_REF%"=="" set "SERVER_EXECUTABLE_REF=SCUM\Binaries\Win64\SCUMServer.exe"
if "%SERVER_CREATE_GAMEPORT%"=="" set "SERVER_CREATE_GAMEPORT=7779"
if "%SERVER_CREATE_MAXPLAYERS%"=="" set "SERVER_CREATE_MAXPLAYERS=128"
if "%SERVER_LOG_FLAG%"=="" set "SERVER_LOG_FLAG=-log"
set "SCUM_EXE=%SERVER_ROOT%\%SERVER_EXECUTABLE_REF:/=\%"
if not exist "%SCUM_EXE%" exit /b 2
pushd "%SERVER_ROOT%"
"%SCUM_EXE%" -port=%SERVER_CREATE_GAMEPORT% -MaxPlayers=%SERVER_CREATE_MAXPLAYERS% %SERVER_LOG_FLAG%
set "SCUM_START_RESULT=%ERRORLEVEL%"
popd
exit /b %SCUM_START_RESULT%
@@ -4,9 +4,9 @@ import type { SCUMConfigField, SCUMConfigPatch, SCUMFeatureAvailability, SCUMSta
// per server, but a game version never enables or disables a feature.
export const configurationCatalog: readonly SCUMConfigField[] = [
{ key: "server-name", configKey: "ServerName", label: "服务器名称", description: "显示在服务器浏览器与玩家连接界面。", control: "text", defaultValue: "SCUM Server", restartImpact: "restart-required" },
{ key: "game-port", configKey: "GamePort", label: "游戏端口", description: "玩家连接所使用的游戏端口。", control: "port", minimum: 1, maximum: 65535, defaultValue: "7777", restartImpact: "restart-required" },
{ key: "game-port", configKey: "GamePort", label: "游戏端口", description: "玩家连接所使用的游戏端口。", control: "port", minimum: 1, maximum: 65535, defaultValue: "7779", restartImpact: "restart-required" },
{ key: "query-port", configKey: "QueryPort", label: "查询端口", description: "服务器查询和状态发现所使用的端口。", control: "port", minimum: 1, maximum: 65535, defaultValue: "27015", restartImpact: "restart-required" },
{ key: "max-players", configKey: "MaxPlayers", label: "最大玩家数", description: "允许同时进入服务器的玩家上限。", control: "number", minimum: 1, maximum: 128, defaultValue: "64", restartImpact: "restart-required" },
{ key: "max-players", configKey: "MaxPlayers", label: "最大玩家数", description: "允许同时进入服务器的玩家上限。", control: "number", minimum: 1, maximum: 128, defaultValue: "128", restartImpact: "restart-required" },
{ key: "welcome-message", configKey: "WelcomeMessage", label: "欢迎消息", description: "登录成功后由已声明的服务器扩展显示给玩家。", control: "text", defaultValue: "", restartImpact: "none" }
];
export const vehicleSpawnCatalog: readonly SCUMVehicleSpawnOption[] = [{ code: "BPC_Laika_C", label: "Laika" }, { code: "BPC_WolfsWagen_C", label: "WolfsWagen" }];
@@ -22,9 +22,9 @@
"createFormSchema": "schemas/create-form.schema.json",
"createFields": [
{ "key": "serverName", "label": "SCUM 服务器名称", "type": "text", "required": true, "configKey": "serverName" },
{ "key": "gamePort", "label": "游戏端口", "type": "port", "required": true, "defaultValue": "7777", "configKey": "gamePort" },
{ "key": "gamePort", "label": "游戏端口", "type": "port", "required": true, "defaultValue": "7779", "configKey": "gamePort" },
{ "key": "queryPort", "label": "查询端口", "type": "port", "required": true, "defaultValue": "27015", "configKey": "queryPort" },
{ "key": "maxPlayers", "label": "最大玩家数", "type": "number", "required": true, "defaultValue": "64", "configKey": "maxPlayers" }
{ "key": "maxPlayers", "label": "最大玩家数", "type": "number", "required": true, "defaultValue": "128", "configKey": "maxPlayers" }
]
},
"capabilities": [
@@ -33,7 +33,6 @@
"process.stop",
"process.restart",
"process.status",
"deployment.scum.v1",
"files.list",
"files.read",
"files.patch",
@@ -360,6 +359,15 @@
"restart": "actions/restart.json",
"status": "actions/status.json"
},
"assetFiles": [
{ "path": "actions/install.json", "mode": 384 },
{ "path": "actions/start.json", "mode": 384 },
{ "path": "actions/stop.json", "mode": 384 },
{ "path": "actions/restart.json", "mode": 384 },
{ "path": "actions/status.json", "mode": 384 },
{ "path": "bin/scum-install-update.cmd", "mode": 448 },
{ "path": "bin/scum-start.cmd", "mode": 448 }
],
"productionLifecycle": {
"operations": ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"],
"dependencyPolicy": "required",
@@ -370,7 +378,7 @@
"defaultDirectoryKey": "scum-config",
"directories": [{ "key": "scum-config", "label": "服务器配置", "scope": "config" }, { "key": "scum-logs", "label": "日志文件", "scope": "logs" }],
"files": [{ "key": "scum-server-settings", "directoryKey": "scum-config", "label": "ServerSettings.ini", "kind": "config", "editable": true }, { "key": "scum-server-log", "directoryKey": "scum-logs", "label": "SCUM Server.log", "kind": "log", "streamKey": "scum.server" }, { "key": "scum-chat-log", "directoryKey": "scum-logs", "label": "SCUM Chat.log", "kind": "log", "streamKey": "scum.chat" }],
"configFields": [{ "key": "server-name", "fileKey": "scum-server-settings", "configKey": "ServerName", "label": "服务器名称", "description": "显示在服务器浏览器与玩家连接界面。", "control": "text", "defaultValue": "SCUM Server", "restartImpact": "restart-required" }, { "key": "game-port", "fileKey": "scum-server-settings", "configKey": "GamePort", "label": "游戏端口", "description": "玩家连接所使用的游戏端口。", "control": "port", "minimum": 1, "maximum": 65535, "defaultValue": "7777", "restartImpact": "restart-required" }, { "key": "query-port", "fileKey": "scum-server-settings", "configKey": "QueryPort", "label": "查询端口", "description": "服务器查询和状态发现所使用的端口。", "control": "port", "minimum": 1, "maximum": 65535, "defaultValue": "27015", "restartImpact": "restart-required" }, { "key": "max-players", "fileKey": "scum-server-settings", "configKey": "MaxPlayers", "label": "最大玩家数", "description": "允许同时进入服务器的玩家上限。", "control": "number", "minimum": 1, "maximum": 128, "defaultValue": "64", "restartImpact": "restart-required" }, { "key": "welcome-message", "fileKey": "scum-server-settings", "configKey": "WelcomeMessage", "label": "欢迎消息", "description": "登录成功后由已声明的服务器扩展显示给玩家。", "control": "text", "defaultValue": "", "restartImpact": "none" }]
"configFields": [{ "key": "server-name", "fileKey": "scum-server-settings", "configKey": "ServerName", "label": "服务器名称", "description": "显示在服务器浏览器与玩家连接界面。", "control": "text", "defaultValue": "SCUM Server", "restartImpact": "restart-required" }, { "key": "game-port", "fileKey": "scum-server-settings", "configKey": "GamePort", "label": "游戏端口", "description": "玩家连接所使用的游戏端口。", "control": "port", "minimum": 1, "maximum": 65535, "defaultValue": "7779", "restartImpact": "restart-required" }, { "key": "query-port", "fileKey": "scum-server-settings", "configKey": "QueryPort", "label": "查询端口", "description": "服务器查询和状态发现所使用的端口。", "control": "port", "minimum": 1, "maximum": 65535, "defaultValue": "27015", "restartImpact": "restart-required" }, { "key": "max-players", "fileKey": "scum-server-settings", "configKey": "MaxPlayers", "label": "最大玩家数", "description": "允许同时进入服务器的玩家上限。", "control": "number", "minimum": 1, "maximum": 128, "defaultValue": "128", "restartImpact": "restart-required" }, { "key": "welcome-message", "fileKey": "scum-server-settings", "configKey": "WelcomeMessage", "label": "欢迎消息", "description": "登录成功后由已声明的服务器扩展显示给玩家。", "control": "text", "defaultValue": "", "restartImpact": "none" }]
},
"ai": {
"purposes": [
@@ -494,49 +502,6 @@
]
}
],
"serverDeployments": [
{
"key": "scum-steamcmd-windows",
"version": "1.0.0",
"supportedTargets": [
{ "os": "windows", "arch": "amd64" }
],
"steamAppId": "3792580",
"executableKey": "scum/server-executable",
"installRootKey": "server/install-root",
"configKey": "scum/server-settings",
"configFormat": "ini",
"prerequisites": [
{ "key": "steamcmd", "kind": "steamcmd" },
{ "key": "vcredist-2012-x86", "kind": "windows-vcredist" },
{ "key": "vcredist-2012-x64", "kind": "windows-vcredist" },
{ "key": "vcredist-2013-x86", "kind": "windows-vcredist" },
{ "key": "vcredist-2013-x64", "kind": "windows-vcredist" },
{ "key": "vcredist-2015-2022-x86", "kind": "windows-vcredist" },
{ "key": "vcredist-2015-2022-x64", "kind": "windows-vcredist" },
{ "key": "directx-jun2010", "kind": "windows-directx" }
],
"configMappings": [
{ "fieldKey": "serverName", "configKey": "server-settings.server-name", "valueType": "text", "required": true },
{ "fieldKey": "gamePort", "configKey": "server-settings.game-port", "valueType": "port", "required": true },
{ "fieldKey": "queryPort", "configKey": "server-settings.query-port", "valueType": "port", "required": true },
{ "fieldKey": "maxPlayers", "configKey": "server-settings.max-players", "valueType": "integer", "required": true }
],
"discoveryMarkers": [
{ "key": "scum-executable", "kind": "file.exists", "targetKey": "scum/server-executable", "required": true },
{ "key": "scum-steam-app", "kind": "steam.app", "targetKey": "server/install-root", "expected": "3792580", "required": true },
{ "key": "scum-config", "kind": "file.exists", "targetKey": "scum/server-settings", "expected": "ServerSettings.ini", "required": true },
{ "key": "scum-game-port", "kind": "port.open", "targetKey": "game-port", "required": true },
{ "key": "scum-query-port", "kind": "port.open", "targetKey": "query-port", "required": true }
],
"verificationChecks": [
{ "key": "executable", "kind": "executable.present", "targetKey": "scum/server-executable", "required": true },
{ "key": "game-port", "kind": "port.bound", "targetKey": "game-port", "required": true },
{ "key": "config", "kind": "config.readable", "targetKey": "scum/server-settings", "required": true },
{ "key": "process", "kind": "process.healthy", "targetKey": "scum/server-executable", "required": true }
]
}
],
"logSources": [
{
"key": "scum-console-stdout",
@@ -11,7 +11,7 @@
"label": "游戏端口",
"type": "port",
"required": true,
"default": 7777
"default": 7779
},
{
"key": "queryPort",
@@ -25,7 +25,7 @@
"label": "最大玩家数",
"type": "number",
"required": true,
"default": 64
"default": 128
}
]
}
@@ -157,6 +157,12 @@
"status": { "$ref": "#/$defs/relativeJsonRef" }
}
},
"assetFiles": {
"type": "array",
"items": { "$ref": "#/$defs/pluginAssetFile" },
"uniqueItems": true,
"maxItems": 64
},
"productionLifecycle": {
"type": "object",
"required": ["operations", "dependencyPolicy", "approvalRequired"],
@@ -222,6 +228,15 @@
"pluginLogicalDirectory": { "type": "object", "required": ["key", "label", "scope"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "label": { "type": "string", "minLength": 1, "maxLength": 60 }, "scope": { "enum": ["config", "logs"] } } },
"pluginLogicalFile": { "type": "object", "required": ["key", "directoryKey", "label", "kind"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "directoryKey": { "$ref": "#/$defs/logicalKey" }, "label": { "type": "string", "minLength": 1, "maxLength": 80 }, "kind": { "enum": ["config", "log"] }, "streamKey": { "$ref": "#/$defs/logicalKey" }, "editable": { "type": "boolean" } } },
"pluginConfigField": { "type": "object", "required": ["key", "fileKey", "configKey", "label", "description", "control", "restartImpact"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "fileKey": { "$ref": "#/$defs/logicalKey" }, "configKey": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_.-]*$", "maxLength": 120 }, "label": { "type": "string", "minLength": 1, "maxLength": 80 }, "description": { "type": "string", "minLength": 1, "maxLength": 240 }, "control": { "enum": ["text", "number", "boolean", "port"] }, "minimum": { "type": "integer", "minimum": 0, "maximum": 65535 }, "maximum": { "type": "integer", "minimum": 0, "maximum": 65535 }, "defaultValue": { "type": "string", "maxLength": 120 }, "restartImpact": { "enum": ["none", "restart-required"] } } },
"pluginAssetFile": {
"type": "object",
"required": ["path"],
"additionalProperties": false,
"properties": {
"path": { "$ref": "#/$defs/relativePathRef" },
"mode": { "type": "integer", "enum": [384, 448] }
}
},
"pluginCreateField": {
"type": "object",
"required": ["key", "label", "type"],
@@ -378,7 +393,6 @@
"process.restart",
"process.status",
"deployment.plan.v1",
"deployment.scum.v1",
"config.write",
"files.list",
"files.read",
+65
View File
@@ -134,6 +134,10 @@ function isSafeRelativeJsonRef(value: string): boolean {
return /^(?!\/)(?![A-Za-z]:)(?!.*:\/\/)(?!.*\.\.)[a-zA-Z0-9_./-]+\.json$/.test(value);
}
function isSafeRelativePathRef(value: string): boolean {
return /^(?!\/)(?![A-Za-z]:)(?!.*:\/\/)(?!.*\.\.)[a-zA-Z0-9_./-]+$/.test(value);
}
function identifierTokens(value: string): string[] {
return value
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
@@ -353,6 +357,57 @@ function referencedLifecycleActions(manifest: unknown): Array<{ action: string;
return [...refs.values()];
}
type PluginAssetFileDeclaration = { path?: unknown; mode?: unknown };
function declaredAssetFiles(manifest: unknown): PluginAssetFileDeclaration[] {
if (typeof manifest !== "object" || manifest === null) {
return [];
}
const assetFiles = (manifest as { assetFiles?: unknown }).assetFiles;
return Array.isArray(assetFiles) ? (assetFiles as PluginAssetFileDeclaration[]) : [];
}
function validateManifestAssetFiles(manifest: unknown, manifestDir: string): { errors: string[]; declared: Set<string> } {
const errors: string[] = [];
const declared = new Set<string>();
const root = path.resolve(manifestDir);
for (const [index, file] of declaredAssetFiles(manifest).entries()) {
const location = `manifest.assetFiles[${index}]`;
if (typeof file.path !== "string" || !isSafeRelativePathRef(file.path)) {
errors.push(`${location}.path: unsafe file reference`);
continue;
}
if (declared.has(file.path)) {
errors.push(`${location}.path: duplicate asset file`);
continue;
}
declared.add(file.path);
if (file.mode !== undefined && file.mode !== 0o600 && file.mode !== 0o700) {
errors.push(`${location}.mode: unsafe file mode`);
}
const target = path.resolve(manifestDir, file.path);
const relative = path.relative(root, target);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
errors.push(`${location}.path: file escapes plugin directory`);
continue;
}
if (!fs.existsSync(target)) {
errors.push(`${location}.path: missing file ${file.path}`);
continue;
}
const stat = fs.statSync(target);
if (!stat.isFile() || stat.size > 64 * 1024) {
errors.push(`${location}.path: asset file must be a regular file under 64KiB`);
continue;
}
const body = fs.readFileSync(target);
if (body.includes(0)) {
errors.push(`${location}.path: asset file contains NUL bytes`);
}
}
return { errors, declared };
}
function validateDependencyPlans(manifest: unknown): string[] {
if (typeof manifest !== "object" || manifest === null) {
return [];
@@ -1187,18 +1242,28 @@ export function validateManifestFile(manifestPath: string): string[] {
errors.push(...validateGameClientBridgeCompanionConfig(manifest, manifestDir));
errors.push(...validateRuntimeLogEventCatalog(manifest));
errors.push(...validateRuntimeLogEventSchemaFiles(manifest, manifestDir));
const assetValidation = validateManifestAssetFiles(manifest, manifestDir);
errors.push(...assetValidation.errors);
for (const declaration of referencedLifecycleActions(manifest)) {
if (!isSafeRelativeJsonRef(declaration.ref)) {
errors.push(`lifecycleAction.${declaration.action}: unsafe file reference`);
continue;
}
if (!assetValidation.declared.has(declaration.ref)) {
errors.push(`lifecycleAction.${declaration.action}: action file must be declared in manifest.assetFiles`);
}
const actionPath = path.resolve(manifestDir, declaration.ref);
if (!fs.existsSync(actionPath)) {
errors.push(`lifecycleAction.${declaration.action}: missing file ${declaration.ref}`);
continue;
}
errors.push(...validateLifecycleActionFile(path.relative(rootDir, actionPath), declaration.action));
const action = readJson(actionPath);
const executableKey = typeof action === "object" && action !== null ? (action as { executableKey?: unknown }).executableKey : undefined;
if (typeof executableKey === "string" && !assetValidation.declared.has(executableKey)) {
errors.push(`lifecycleAction.${declaration.action}.executableKey: ${executableKey} must be declared in manifest.assetFiles`);
}
}
if (typeof manifest === "object" && manifest !== null && "server" in manifest) {
+16
View File
@@ -22,11 +22,18 @@ export type RunCapability =
| "process.stop"
| "process.restart"
| "process.status"
| "deployment.plan.v1"
| "config.write"
| "files.list"
| "files.read"
| "files.write"
| "files.patch"
| "logs.read"
| "run.self-update"
| "distribution.build"
| "dependencies.check"
| "dependencies.install"
| "logs.backfill"
| "remote.ftp.read"
| "remote.ftp.write"
| "remote.rsync.read"
@@ -39,6 +46,9 @@ export type RunCapability =
| "remote.run.db.sqlite.query"
| "remote.run.logs.transfer"
| "remote.run.rcon.command"
| "remote.run.protected.sql"
| "remote.run.protected.rcon"
| "remote.run.program.command"
| "client-manager.deploy"
| "client-manager.control"
| "client-manager.update"
@@ -646,6 +656,11 @@ export interface GamePluginBridge {
actions: PluginBridgeAction[];
}
export interface PluginAssetFile {
path: string;
mode?: 384 | 448;
}
export interface GamePluginManifest {
id: `game.${string}`;
name: string;
@@ -666,6 +681,7 @@ export interface GamePluginManifest {
runtimeProfiles?: GamePluginRuntimeProfiles;
gameClientBridge?: GameClientBridgeManifest;
actions?: GamePluginActions;
assetFiles?: PluginAssetFile[];
productionLifecycle: {
operations: ProductionPluginLifecycleOperation[];
dependencyPolicy: "required" | "optional";
+18 -9
View File
@@ -189,19 +189,28 @@ describe("plugin manifest validation", () => {
}
});
it("declares a frozen SCUM install/adopt template with explicit mapping and verification checks", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as any;
const template = manifest.runtimeProfiles.serverDeployments[0];
expect(template).toMatchObject({ key: "scum-steamcmd-windows", version: "1.0.0", steamAppId: "3792580", configFormat: "ini" });
expect(template.configMappings.map((mapping: any) => mapping.fieldKey)).toEqual(["serverName", "gamePort", "queryPort", "maxPlayers"]);
expect(template.verificationChecks.filter((check: any) => check.required)).toHaveLength(4);
it("declares SCUM install/update and start lifecycle through plugin assets", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as any;
const assetPaths = manifest.assetFiles.map((file: { path: string }) => file.path);
const installAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.install), "utf8"));
const startAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.start), "utf8"));
const installScript = fs.readFileSync(path.join(pluginDir, installAction.executableKey), "utf8");
const startScript = fs.readFileSync(path.join(pluginDir, startAction.executableKey), "utf8");
expect(manifest.runtimeProfiles.serverDeployments).toBeUndefined();
expect(assetPaths).toEqual(expect.arrayContaining(["actions/install.json", "actions/start.json", "bin/scum-install-update.cmd", "bin/scum-start.cmd"]));
expect(installAction).toMatchObject({ executableKey: "bin/scum-install-update.cmd", environment: { SERVER_STEAM_APP_ID: "3792580", SERVER_STEAMCMD_UPDATE_ARGS: "+login anonymous +app_update 3792580 validate +quit" } });
expect(startAction).toMatchObject({ executableKey: "bin/scum-start.cmd", environment: { SERVER_LOG_FLAG: "-log" } });
expect(installScript).toContain("taskkill /IM SCUMServer.exe /F");
expect(installScript).toContain("%SERVER_STEAMCMD_INSTALL_DIR_ARG% \"%SERVER_ROOT%\" %SERVER_STEAMCMD_UPDATE_ARGS%");
expect(startScript).toContain("-port=%SERVER_CREATE_GAMEPORT% -MaxPlayers=%SERVER_CREATE_MAXPLAYERS% %SERVER_LOG_FLAG%");
});
it("rejects an SCUM template mapping an undeclared field", () => {
it("rejects lifecycle executable assets missing from the manifest seed declaration", () => {
const errors = validateTemporaryScumCompanionManifest((manifest) => {
manifest.runtimeProfiles.serverDeployments[0].configMappings[0].fieldKey = "hostCommand";
manifest.assetFiles = manifest.assetFiles.filter((file: { path: string }) => file.path !== "bin/scum-start.cmd");
});
expect(errors.some((error) => error.includes("configMappings") && error.includes("fieldKey"))).toBe(true);
expect(errors.some((error) => error.includes("lifecycleAction.start.executableKey") && error.includes("manifest.assetFiles"))).toBe(true);
});
it("rejects unsupported or unsafe inline create-field declarations", () => {