Move SCUM lifecycle ownership to plugin
This commit is contained in:
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
printf 'dev server started\n'
|
||||
+3
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user