steam workshotp
This commit is contained in:
parent
d8c66c4c49
commit
e921a49d5b
14 changed files with 285 additions and 26 deletions
|
|
@ -215,8 +215,9 @@ Apply this as `$install_queries[2]` (db_version 3) in `module.php` when ready.
|
|||
- [x] `addons_installer.php` and `user_addons.php` use category map for validation.
|
||||
- [x] Full TODO/comment blocks added to installer for next phase work.
|
||||
- [x] Module folder, table names, URL routes, function names unchanged.
|
||||
- [x] Workshop Content Phase 1: manual Workshop ID entry, per-server manifest,
|
||||
agent script runner, install/update/remove actions (`db_version 3`).
|
||||
- [x] Workshop Content Phase 1: manual Workshop ID/URL entry, per-server
|
||||
manifest, bundled Linux and Windows/Cygwin SteamCMD handlers, generic
|
||||
handler fallback, install/update/remove actions (`db_version 3`).
|
||||
|
||||
### Phase 2 — Schema & install_method support
|
||||
- [x] Apply Phase 2 schema: `install_method`, `content_version`, `requires_stop`,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` modu
|
|||
|
||||
- No Steam Workshop browser/search UI yet.
|
||||
- No Steam scraping.
|
||||
- User enters comma-separated numeric Workshop IDs.
|
||||
- User enters Steam Workshop numeric IDs or full Workshop URLs.
|
||||
- Input may be comma-separated, newline-separated, or whitespace-separated.
|
||||
- Panel validates IDs, removes duplicates, and stores them per server home.
|
||||
- Panel lists saved IDs and supports:
|
||||
- Install New
|
||||
|
|
@ -15,7 +16,14 @@ Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` modu
|
|||
- Update All
|
||||
- Panel generates a per-server manifest at:
|
||||
- `%home_path%/gsp_server_content/workshop_manifest.json`
|
||||
- Panel runs an approved script path (safe default or game-specific config), never user-supplied command/path.
|
||||
- Panel runs an approved handler only, never a user-supplied command/path.
|
||||
- If a game does not define a custom Workshop script, the panel stages the
|
||||
bundled generic handler for the agent OS:
|
||||
- Linux: `generic_steam_workshop_linux.sh`
|
||||
- Windows/Cygwin: `generic_steam_workshop_windows_cygwin.sh`
|
||||
- If a custom configured script is missing on the agent, the panel falls back
|
||||
to the bundled generic handler and logs a warning instead of failing with
|
||||
"script not found."
|
||||
|
||||
## Security model
|
||||
|
||||
|
|
@ -24,6 +32,9 @@ Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` modu
|
|||
- IDs must be numeric only.
|
||||
- Script path is not user-editable.
|
||||
- Manifest path is validated to remain under server home.
|
||||
- Default generic installs stay under `{SERVER_ROOT}/workshop/@<workshop_id>`.
|
||||
- Arma/DayZ-style strategies install under `{SERVER_ROOT}/@<workshop_id>` so
|
||||
startup parameters can later use normal `-mod=@...` values.
|
||||
- Remove is non-destructive in the generic scripts (preserve/move behavior for Phase 1).
|
||||
- All actions are logged through panel logging.
|
||||
|
||||
|
|
@ -40,11 +51,21 @@ and keeps `OGP_DB_PREFIXaddons.addon_type` at `VARCHAR(32)` so `workshop` is val
|
|||
Each game should define and document:
|
||||
|
||||
- `workshop_app_id`
|
||||
- Linux workshop script path
|
||||
- Windows/Cygwin workshop script path
|
||||
- optional Linux workshop script path only when the bundled generic handler is
|
||||
not sufficient
|
||||
- optional Windows/Cygwin workshop script path only when the bundled generic
|
||||
handler is not sufficient
|
||||
- target install location
|
||||
- restart/update behavior
|
||||
|
||||
If `workshop_app_id` is not defined in the content profile, server content
|
||||
template, or game XML, Phase 1 returns a clear error rather than guessing.
|
||||
|
||||
Known Phase 1 app IDs:
|
||||
|
||||
- Arma 3 Workshop app ID: `107410`
|
||||
- Arma 3 dedicated server installer app ID remains `233780`
|
||||
|
||||
## Phase 2 (not included here)
|
||||
|
||||
- Workshop browsing/search/select UI
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ try:
|
|||
'FOLDER_NAME': folder_name,
|
||||
'MOD_FOLDER': folder_name,
|
||||
}
|
||||
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
|
||||
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}')
|
||||
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
|
||||
if len(items) != 1 or not target_path:
|
||||
target_path = render_template(target_template, template_values)
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ try:
|
|||
'FOLDER_NAME': folder_name,
|
||||
'MOD_FOLDER': folder_name,
|
||||
}
|
||||
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
|
||||
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}')
|
||||
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
|
||||
if len(items) != 1 or not target_path:
|
||||
target_path = render_template(target_template, template_values)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ if (!defined('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT')) {
|
|||
if (!defined('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT')) {
|
||||
define('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT', 'generic_steam_workshop_windows_cygwin.sh');
|
||||
}
|
||||
if (!defined('SCM_WORKSHOP_TARGET_GENERIC_DEFAULT')) {
|
||||
define('SCM_WORKSHOP_TARGET_GENERIC_DEFAULT', '{SERVER_ROOT}/workshop/{MOD_FOLDER}');
|
||||
}
|
||||
if (!defined('SCM_WORKSHOP_TARGET_MOD_ROOT_DEFAULT')) {
|
||||
define('SCM_WORKSHOP_TARGET_MOD_ROOT_DEFAULT', '{SERVER_ROOT}/{MOD_FOLDER}');
|
||||
}
|
||||
|
||||
function scm_ensure_workshop_schema($db)
|
||||
{
|
||||
|
|
@ -246,12 +252,31 @@ function scm_get_workshop_script_path(array $home_info, $server_xml)
|
|||
return scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
|
||||
}
|
||||
|
||||
function scm_get_configured_workshop_script_path(array $home_info, $server_xml)
|
||||
{
|
||||
$key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux';
|
||||
if (!isset($server_xml->$key)) {
|
||||
return '';
|
||||
}
|
||||
$xml_path = trim((string)$server_xml->$key);
|
||||
if ($xml_path === '' || !preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return '';
|
||||
}
|
||||
return $xml_path;
|
||||
}
|
||||
|
||||
function scm_get_bundled_workshop_script_source(array $home_info)
|
||||
{
|
||||
$filename = scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
|
||||
return dirname(__FILE__) . '/scripts/workshop/' . $filename;
|
||||
}
|
||||
|
||||
function scm_is_default_workshop_script_name($script_path)
|
||||
{
|
||||
$base = basename(trim((string)$script_path));
|
||||
return in_array($base, array(SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT, SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT), true);
|
||||
}
|
||||
|
||||
function scm_is_legacy_panel_workshop_script_path($script_path)
|
||||
{
|
||||
$script_path = trim((string)$script_path);
|
||||
|
|
@ -276,13 +301,17 @@ function scm_get_agent_managed_workshop_script_path(array $home_info)
|
|||
function scm_prepare_workshop_script_for_agent($remote, array $home_info, $server_xml, &$error = '')
|
||||
{
|
||||
$error = '';
|
||||
$configured_path = trim((string)scm_get_workshop_script_path($home_info, $server_xml));
|
||||
if ($configured_path !== '' && !scm_is_legacy_panel_workshop_script_path($configured_path) && preg_match('/^[^\\r\\n\\0]+$/', $configured_path)) {
|
||||
$configured_path = scm_get_configured_workshop_script_path($home_info, $server_xml);
|
||||
if ($configured_path !== '' && !scm_is_default_workshop_script_name($configured_path) && !scm_is_legacy_panel_workshop_script_path($configured_path)) {
|
||||
if ((int)$remote->rfile_exists($configured_path) === 1) {
|
||||
return $configured_path;
|
||||
}
|
||||
$error = 'Configured workshop script not found on agent host: ' . $configured_path;
|
||||
return false;
|
||||
scm_log_content_install_action(array(
|
||||
'type' => 'workshop_script_fallback',
|
||||
'home_id' => isset($home_info['home_id']) ? (int)$home_info['home_id'] : 0,
|
||||
'configured_path' => $configured_path,
|
||||
'message' => 'Configured workshop script was not found on the agent; falling back to bundled generic handler.',
|
||||
));
|
||||
}
|
||||
|
||||
$source_path = scm_get_bundled_workshop_script_source($home_info);
|
||||
|
|
@ -313,6 +342,15 @@ function scm_prepare_workshop_script_for_agent($remote, array $home_info, $serve
|
|||
return $remote_path;
|
||||
}
|
||||
|
||||
function scm_get_default_workshop_target_template($install_strategy = '')
|
||||
{
|
||||
$install_strategy = strtolower(trim((string)$install_strategy));
|
||||
if (in_array($install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true)) {
|
||||
return SCM_WORKSHOP_TARGET_MOD_ROOT_DEFAULT;
|
||||
}
|
||||
return SCM_WORKSHOP_TARGET_GENERIC_DEFAULT;
|
||||
}
|
||||
|
||||
function scm_get_csrf_token()
|
||||
{
|
||||
if (empty($_SESSION['addonsmanager_workshop_csrf'])) {
|
||||
|
|
@ -583,9 +621,10 @@ function scm_build_workshop_runtime_context($db, array $home_info, $server_xml,
|
|||
}
|
||||
|
||||
$workshop_item_id = trim((string)(isset($payload['workshop_item_id']) ? $payload['workshop_item_id'] : ''));
|
||||
$target_path_template = trim((string)$payload['target_path_template']);
|
||||
$optional_folder_name = trim((string)$payload['optional_folder_name']);
|
||||
$workshop_app_id_override = trim((string)$payload['workshop_app_id']);
|
||||
$target_path_template = trim((string)(isset($payload['target_path_template']) ? $payload['target_path_template'] : ''));
|
||||
$optional_folder_name = trim((string)(isset($payload['optional_folder_name']) ? $payload['optional_folder_name'] : ''));
|
||||
$workshop_app_id_override = trim((string)(isset($payload['workshop_app_id']) ? $payload['workshop_app_id'] : ''));
|
||||
$install_strategy = isset($payload['install_strategy']) ? trim((string)$payload['install_strategy']) : '';
|
||||
$fallback_profile = function_exists('sw_get_profile_for_home') ? sw_get_profile_for_home($db, (int)$home_info['home_id']) : false;
|
||||
$resolved = function_exists('steam_workshop_install_item_to_home')
|
||||
? steam_workshop_install_item_to_home($db, $home_info, $workshop_item_id, $target_path_template, array(
|
||||
|
|
@ -625,7 +664,7 @@ function scm_build_workshop_runtime_context($db, array $home_info, $server_xml,
|
|||
if ($effective_template === '') {
|
||||
$effective_template = (is_array($fallback_profile) && !empty($fallback_profile['install_path_template']))
|
||||
? (string)$fallback_profile['install_path_template']
|
||||
: '{SERVER_ROOT}/{MOD_FOLDER}';
|
||||
: scm_get_default_workshop_target_template($install_strategy);
|
||||
}
|
||||
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => isset($server_xml->exe_location) ? (string)$server_xml->exe_location : ''), array(
|
||||
'WORKSHOP_ID' => $workshop_item_id,
|
||||
|
|
|
|||
116
Panel/modules/addonsmanager/tests/workshop_helpers_test.php
Normal file
116
Panel/modules/addonsmanager/tests/workshop_helpers_test.php
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
if (!function_exists('clean_path')) {
|
||||
function clean_path($path)
|
||||
{
|
||||
$path = str_replace('\\', '/', (string)$path);
|
||||
$path = preg_replace('#/+#', '/', $path);
|
||||
return rtrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
require_once(dirname(__DIR__) . '/server_content_helpers.php');
|
||||
|
||||
class ScmWorkshopFakeRemote
|
||||
{
|
||||
public $files = array();
|
||||
public $commands = array();
|
||||
public $existing = array();
|
||||
|
||||
public function rfile_exists($path)
|
||||
{
|
||||
return in_array($path, $this->existing, true) || isset($this->files[$path]) ? 1 : 0;
|
||||
}
|
||||
|
||||
public function remote_writefile($path, $contents)
|
||||
{
|
||||
$this->files[$path] = $contents;
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function exec($command)
|
||||
{
|
||||
$this->commands[] = $command;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function scm_workshop_test_assert($condition, $message)
|
||||
{
|
||||
if (!$condition) {
|
||||
fwrite(STDERR, "FAIL: {$message}\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "PASS: {$message}\n";
|
||||
}
|
||||
|
||||
$invalid = array();
|
||||
$ids = scm_parse_workshop_ids("https://steamcommunity.com/sharedfiles/filedetails/?id=450814997\n450814997, 463939057 463939057", $invalid);
|
||||
scm_workshop_test_assert($ids === array('450814997', '463939057'), 'parses URLs, whitespace, commas, and duplicates');
|
||||
scm_workshop_test_assert(empty($invalid), 'valid mixed Workshop input has no invalid entries');
|
||||
|
||||
$ids = scm_parse_workshop_ids("abc, https://steamcommunity.com/sharedfiles/filedetails/?id=0, 123", $invalid);
|
||||
scm_workshop_test_assert($ids === array('123'), 'keeps valid IDs when invalid values are present');
|
||||
scm_workshop_test_assert(count($invalid) === 2, 'reports invalid Workshop entries');
|
||||
|
||||
$linuxHome = array(
|
||||
'home_id' => 10,
|
||||
'home_path' => '/srv/games/arma3',
|
||||
'home_cfg_file' => 'arma3_linux64.xml',
|
||||
'game_key' => 'arma3_linux64',
|
||||
'game_name' => 'Arma 3',
|
||||
);
|
||||
$windowsHome = array(
|
||||
'home_id' => 11,
|
||||
'home_path' => '/cygdrive/c/OGP_User_Files/11',
|
||||
'home_cfg_file' => 'arma3_win64.xml',
|
||||
'game_key' => 'arma3_win64',
|
||||
'game_name' => 'Arma 3',
|
||||
);
|
||||
|
||||
$emptyXml = simplexml_load_string('<game_config></game_config>');
|
||||
$linuxRemote = new ScmWorkshopFakeRemote();
|
||||
$error = '';
|
||||
$script = scm_prepare_workshop_script_for_agent($linuxRemote, $linuxHome, $emptyXml, $error);
|
||||
scm_workshop_test_assert($script === '/srv/games/arma3/gsp_server_content/scripts/workshop/generic_steam_workshop_linux.sh', 'stages default Linux script under server home');
|
||||
scm_workshop_test_assert(isset($linuxRemote->files[$script]), 'writes Linux bundled script to fake agent');
|
||||
scm_workshop_test_assert($error === '', 'default Linux script staging does not report missing script');
|
||||
|
||||
$windowsRemote = new ScmWorkshopFakeRemote();
|
||||
$script = scm_prepare_workshop_script_for_agent($windowsRemote, $windowsHome, $emptyXml, $error);
|
||||
scm_workshop_test_assert($script === '/cygdrive/c/OGP_User_Files/11/gsp_server_content/scripts/workshop/generic_steam_workshop_windows_cygwin.sh', 'stages default Windows/Cygwin script under server home');
|
||||
scm_workshop_test_assert(isset($windowsRemote->files[$script]), 'writes Windows/Cygwin bundled script to fake agent');
|
||||
scm_workshop_test_assert($error === '', 'default Windows/Cygwin script staging does not report missing script');
|
||||
|
||||
$configuredXml = simplexml_load_string('<game_config><workshop_script_linux>/agent/custom/workshop.sh</workshop_script_linux></game_config>');
|
||||
$customRemote = new ScmWorkshopFakeRemote();
|
||||
$customRemote->existing[] = '/agent/custom/workshop.sh';
|
||||
$script = scm_prepare_workshop_script_for_agent($customRemote, $linuxHome, $configuredXml, $error);
|
||||
scm_workshop_test_assert($script === '/agent/custom/workshop.sh', 'uses existing configured custom script');
|
||||
|
||||
$missingCustomRemote = new ScmWorkshopFakeRemote();
|
||||
$script = scm_prepare_workshop_script_for_agent($missingCustomRemote, $linuxHome, $configuredXml, $error);
|
||||
scm_workshop_test_assert($script === '/srv/games/arma3/gsp_server_content/scripts/workshop/generic_steam_workshop_linux.sh', 'falls back to bundled script when configured custom script is missing');
|
||||
scm_workshop_test_assert($error === '', 'missing custom script fallback does not expose script-not-found error');
|
||||
@unlink(dirname(__DIR__) . '/logs/content_install.log');
|
||||
@rmdir(dirname(__DIR__) . '/logs');
|
||||
|
||||
$genericHome = array(
|
||||
'home_id' => 12,
|
||||
'home_path' => '/srv/games/generic',
|
||||
'home_cfg_file' => 'generic_linux.xml',
|
||||
'game_key' => 'generic_linux',
|
||||
'game_name' => 'Generic Game',
|
||||
);
|
||||
$runtime = scm_build_workshop_runtime_context(new stdClass(), $genericHome, $emptyXml, array('workshop_item_id' => '12345'), $message);
|
||||
scm_workshop_test_assert($runtime['target_path_template'] === '{SERVER_ROOT}/workshop/{MOD_FOLDER}', 'generic fallback target path uses server_root/workshop');
|
||||
scm_workshop_test_assert($runtime['target_path_resolved'] === '/srv/games/generic/workshop/@12345', 'generic fallback target path resolves under workshop folder');
|
||||
|
||||
$runtime = scm_build_workshop_runtime_context(new stdClass(), $linuxHome, $emptyXml, array('workshop_item_id' => '450814997', 'install_strategy' => 'arma_mod_folder'), $message);
|
||||
scm_workshop_test_assert($runtime['target_path_template'] === '{SERVER_ROOT}/{MOD_FOLDER}', 'Arma strategy keeps @mod folder at server root');
|
||||
scm_workshop_test_assert($runtime['target_path_resolved'] === '/srv/games/arma3/@450814997', 'Arma target path resolves to root @WorkshopID folder');
|
||||
|
||||
$appXml = simplexml_load_string('<game_config><workshop_app_id>107410</workshop_app_id></game_config>');
|
||||
scm_workshop_test_assert(scm_extract_workshop_app_id($appXml) === '107410', 'extracts explicit Workshop app ID from game XML');
|
||||
|
||||
echo "All Workshop helper smoke tests passed.\n";
|
||||
|
|
@ -100,6 +100,7 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
|||
foreach ($item_ids as $item_id) {
|
||||
$payload = $template_payload;
|
||||
$payload['workshop_item_id'] = (string)$item_id;
|
||||
$payload['install_strategy'] = $install_strategy;
|
||||
// A fixed optional folder name is safe only when installing one item. For
|
||||
// multi-item installs, use @<workshop_id> so items cannot overwrite each
|
||||
// other by sharing the same target folder.
|
||||
|
|
@ -122,7 +123,7 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
|||
'workshop_item_id' => (string)$item_id,
|
||||
'title' => '',
|
||||
'folder_name' => isset($runtime['folder_name']) && $runtime['folder_name'] !== '' ? (string)$runtime['folder_name'] : '@' . $item_id,
|
||||
'target_path_template' => isset($runtime['target_path_template']) ? (string)$runtime['target_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}',
|
||||
'target_path_template' => isset($runtime['target_path_template']) ? (string)$runtime['target_path_template'] : scm_get_default_workshop_target_template($install_strategy),
|
||||
'target_path_resolved' => isset($runtime['target_path_resolved']) ? (string)$runtime['target_path_resolved'] : '',
|
||||
'install_strategy' => $install_strategy,
|
||||
'copy_keys' => $copy_keys ? 1 : 0,
|
||||
|
|
@ -140,7 +141,7 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
|||
'server_root' => rtrim((string)$home_info['home_path'], '/'),
|
||||
'install_strategy' => $install_strategy,
|
||||
'copy_keys' => $copy_keys ? 1 : 0,
|
||||
'target_path_template' => isset($template['target_path_template']) && trim((string)$template['target_path_template']) !== '' ? trim((string)$template['target_path_template']) : '{SERVER_ROOT}/{MOD_FOLDER}',
|
||||
'target_path_template' => isset($template['target_path_template']) && trim((string)$template['target_path_template']) !== '' ? trim((string)$template['target_path_template']) : scm_get_default_workshop_target_template($install_strategy),
|
||||
'post_install_script' => isset($template['post_script']) ? trim((string)$template['post_script']) : '',
|
||||
'launch_param_additions' => isset($template['launch_param_additions']) ? trim((string)$template['launch_param_additions']) : '',
|
||||
'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0,
|
||||
|
|
@ -183,7 +184,7 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
|
|||
'items' => array_values($item_ids),
|
||||
'item_details' => !empty($extra_manifest['item_details']) && is_array($extra_manifest['item_details']) ? $extra_manifest['item_details'] : array(),
|
||||
'install_strategy' => !empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : '',
|
||||
'target_path' => !empty($extra_manifest['target_path_template']) ? (string)$extra_manifest['target_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}',
|
||||
'target_path' => !empty($extra_manifest['target_path_template']) ? (string)$extra_manifest['target_path_template'] : scm_get_default_workshop_target_template(!empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : ''),
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
);
|
||||
if (!empty($extra_manifest)) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<game_key>arma3_linux64</game_key>
|
||||
<installer>steamcmd</installer>
|
||||
<game_name>Arma 3</game_name>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<server_exec_name>arma3server_x64</server_exec_name>
|
||||
<cli_template>%CONFIG% %CFG% %PROFILES% %NAME% %IP% %PORT% %PLAYERS% %MODLIST% %SERVERMODLIST% %AUTOINIT%</cli_template>
|
||||
<cli_params>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<game_key>arma3_win64</game_key>
|
||||
<installer>steamcmd</installer>
|
||||
<game_name>Arma 3</game_name>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<server_exec_name>arma3server.exe</server_exec_name>
|
||||
<cli_template>-profiles=profile -name=player -config=profile\server.cfg -cfg=profile\basic.cfg %PORT% %PLAYERS% %RANKING% %AUTOINIT% %DEBUG% %MODS% %SERVERMODS%</cli_template>
|
||||
<cli_params>
|
||||
|
|
|
|||
|
|
@ -518,22 +518,37 @@ function sw_detect_profile_defaults_from_xml($configName)
|
|||
}
|
||||
}
|
||||
|
||||
$xmlBlob = $matched->asXML();
|
||||
$workshopAppId = '';
|
||||
if ($xmlBlob !== false && preg_match('/steamapps\/workshop\/content\/(\d+)/i', $xmlBlob, $m)) {
|
||||
foreach (array('workshop_app_id', 'workshop_appid', 'steam_workshop_app_id', 'steam_workshop_appid') as $tag) {
|
||||
if (isset($matched->$tag)) {
|
||||
$candidate = trim((string)$matched->$tag);
|
||||
if ($candidate !== '' && preg_match('/^\d+$/', $candidate)) {
|
||||
$workshopAppId = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$xmlBlob = $matched->asXML();
|
||||
if ($workshopAppId === '' && $xmlBlob !== false && preg_match('/steamapps\/workshop\/content\/(\d+)/i', $xmlBlob, $m)) {
|
||||
$workshopAppId = $m[1];
|
||||
}
|
||||
if ($workshopAppId === '') {
|
||||
$workshopAppId = $steamAppId;
|
||||
}
|
||||
|
||||
$gameKey = strtolower(trim((string)$matched->game_key));
|
||||
$gameName = strtolower(trim((string)$matched->game_name));
|
||||
$installPathTemplate = (strpos($gameKey . ' ' . $gameName, 'arma') !== false || strpos($gameKey . ' ' . $gameName, 'dayz') !== false)
|
||||
? '{SERVER_ROOT}/{MOD_FOLDER}'
|
||||
: '{SERVER_ROOT}/workshop/{MOD_FOLDER}';
|
||||
|
||||
return array(
|
||||
'steam_app_id' => $steamAppId,
|
||||
'workshop_app_id' => $workshopAppId,
|
||||
'steamcmd_path' => '/home/gameserver/steamcmd/steamcmd.sh',
|
||||
'server_root_template' => '{SERVER_ROOT}',
|
||||
'workshop_download_dir_template' => '{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}',
|
||||
'install_path_template' => '{SERVER_ROOT}/{MOD_FOLDER}',
|
||||
'install_path_template' => $installPathTemplate,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -608,7 +623,7 @@ function steam_workshop_resolve_paths($db, array $home_info, $workshop_id, $targ
|
|||
);
|
||||
$target_template = trim((string)$target_path_template);
|
||||
if ($target_template === '') {
|
||||
$target_template = !empty($profile['install_path_template']) ? (string)$profile['install_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}';
|
||||
$target_template = !empty($profile['install_path_template']) ? (string)$profile['install_path_template'] : '{SERVER_ROOT}/workshop/{MOD_FOLDER}';
|
||||
}
|
||||
$resolved_target = sw_apply_template($target_template, $vars);
|
||||
return array(
|
||||
|
|
|
|||
|
|
@ -31,5 +31,8 @@ Phase 1 implements this decision by routing the user-facing Workshop install flo
|
|||
- Workshop input accepts numeric IDs or Steam URLs, then stores numeric IDs only.
|
||||
- Manifests are written under the server home in `gsp_server_content`.
|
||||
- Bundled Linux/Cygwin scripts are copied from the Panel module to an agent-managed folder under the server home before execution.
|
||||
- Default script names are treated as bundled handlers, not as existing agent paths.
|
||||
- Missing custom scripts fall back to bundled handlers and log the fallback.
|
||||
- Generic content installs under `{SERVER_ROOT}/workshop/{MOD_FOLDER}` by default.
|
||||
- DayZ/Arma-style installs default to `@<workshop_id>` folders and copy `.bikey` files into `keys` when present.
|
||||
- Startup parameter generation remains a later phase.
|
||||
|
|
|
|||
|
|
@ -59,6 +59,13 @@ The admin template defines capability and policy. The customer supplies only Wor
|
|||
- `bash <script> <manifest>`
|
||||
7. The script runs SteamCMD, copies downloaded content into the target mod folder, logs output, and copies `.bikey` files for DayZ/Arma-style strategies.
|
||||
|
||||
Current repair:
|
||||
|
||||
- The default script filename is no longer treated as an agent-host path.
|
||||
- Missing custom scripts fall back to the bundled generic handler and log a warning.
|
||||
- Generic installs default to `{SERVER_ROOT}/workshop/{MOD_FOLDER}`.
|
||||
- DayZ/Arma installs keep `{SERVER_ROOT}/{MOD_FOLDER}` for `@<workshop_id>` compatibility.
|
||||
|
||||
## Manifest Fields
|
||||
|
||||
Phase 1 manifests include:
|
||||
|
|
@ -95,6 +102,11 @@ Default install folder:
|
|||
|
||||
- `@<workshop_id>`
|
||||
|
||||
Arma 3 game XML now declares:
|
||||
|
||||
- Workshop app ID: `107410`
|
||||
- Dedicated server app ID: `233780` through the existing `installer_name`
|
||||
|
||||
Key-copy behavior:
|
||||
|
||||
- `.bikey` files found anywhere in the installed mod folder are copied to `{SERVER_HOME}/keys`.
|
||||
|
|
@ -149,10 +161,16 @@ Phase 2 should generate structured mod lists from enabled `server_content_worksh
|
|||
- full Steam URL accepted
|
||||
- invalid text rejected
|
||||
- Temporary manifest test with fake SteamCMD confirmed:
|
||||
- Linux script installs into `@<workshop_id>`
|
||||
- Windows/Cygwin script installs into `@<workshop_id>`
|
||||
- Linux script installs generic content into `workshop/@<workshop_id>`
|
||||
- Windows/Cygwin script installs generic content into `workshop/@<workshop_id>`
|
||||
- Arma/DayZ strategies resolve to root `@<workshop_id>` folders
|
||||
- `.bikey` files are copied to `keys`
|
||||
- useful logs are written under `gsp_server_content`
|
||||
- Current smoke tests also confirm:
|
||||
- default Linux/Windows script staging no longer reports `script not found`
|
||||
- full URLs, numeric IDs, whitespace, comma, newline, duplicates, and invalid values parse correctly
|
||||
- generic no-template installs use `server_root/workshop`
|
||||
- missing SteamCMD returns a clear failure
|
||||
|
||||
## Phase 2 Work
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,13 @@ The Panel syncs the bundled install script to:
|
|||
|
||||
The agent executes the synced script with the manifest path. Customers do not need to place scripts manually on the agent.
|
||||
|
||||
Script selection rules:
|
||||
|
||||
1. If game XML defines a custom `workshop_script_linux` or `workshop_script_windows` and that script exists on the agent, use it.
|
||||
2. If the custom script is missing, log a fallback warning and stage the bundled generic handler.
|
||||
3. If no custom script is defined, stage the bundled generic handler for the server OS.
|
||||
4. The default script filename must never be treated as a pre-existing agent path. The Panel must copy the bundled script first.
|
||||
|
||||
The manifest includes:
|
||||
|
||||
- `home_id`
|
||||
|
|
@ -52,7 +59,17 @@ The manifest includes:
|
|||
- key-copy settings
|
||||
- content template metadata
|
||||
|
||||
DayZ/Arma-style installs default to `dayz_mod_folder` or `arma_mod_folder` based on the game key/name/config file. Those strategies install to `@<workshop_id>` by default and copy `.bikey` files into the server `keys` folder when found. Missing key files are logged but do not fail the install.
|
||||
Default install paths:
|
||||
|
||||
- Generic Workshop installs default to `{SERVER_ROOT}/workshop/{MOD_FOLDER}`.
|
||||
- DayZ/Arma-style installs default to `dayz_mod_folder` or `arma_mod_folder` based on the game key/name/config file. Those strategies install to `{SERVER_ROOT}/{MOD_FOLDER}` so `@<workshop_id>` folders remain compatible with existing `-mod=` workflows.
|
||||
- DayZ/Arma key-copy behavior copies `.bikey` files into the server `keys` folder when found. Missing key files are logged but do not fail the install.
|
||||
|
||||
App ID rules:
|
||||
|
||||
- `workshop_app_id` must come from a Server Content template, Steam Workshop profile, or game XML.
|
||||
- Do not silently use the dedicated server Steam app ID as the Workshop app ID unless a legacy profile explicitly does so.
|
||||
- Arma 3 XML declares Workshop app ID `107410`; its dedicated server Steam app ID remains `233780`.
|
||||
|
||||
## Database State
|
||||
|
||||
|
|
@ -101,6 +118,15 @@ The current direction already supports:
|
|||
- caching and cleanup policy need product-level design, not just ad hoc scripts.
|
||||
- `-mod=` / `-serverMod=` generation still needs a safe structured implementation.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Meaning | Fix |
|
||||
|---|---|---|
|
||||
| `Configured workshop script not found on agent host: generic_steam_workshop_windows_cygwin.sh` | Old Panel logic treated the default script filename as an agent path. | Update the Panel. Current logic stages the bundled handler under `gsp_server_content/scripts/workshop/`. |
|
||||
| `SteamCMD is missing on the agent host.` | The handler could not find SteamCMD at the configured path, `STEAMCMD_PATH`, or common locations. | Install SteamCMD on the agent and/or set the SteamCMD path in the Workshop profile/template. |
|
||||
| `Workshop App ID is missing` | No template/profile/XML provided an app ID. | Add `workshop_app_id` to the Server Content template or game XML. |
|
||||
| Download succeeds but mod does not load | Startup parameters are not yet regenerated from installed Workshop rows. | Manually add the installed `@...` folders to the game startup params until Phase 2 startup integration is complete. |
|
||||
|
||||
## Recommended Mental Model
|
||||
|
||||
Use `addonsmanager` as the main future home for:
|
||||
|
|
|
|||
|
|
@ -50,6 +50,23 @@ For Workshop items, the current flow lets users enter Workshop IDs or full Steam
|
|||
9. Agent executes the script with the manifest path.
|
||||
10. Script runs SteamCMD, copies Workshop content into the configured target path, copies DayZ/Arma `.bikey` files when applicable, and writes a log under `gsp_server_content`.
|
||||
|
||||
Current script fallback behavior:
|
||||
|
||||
- Admin-defined custom scripts are supported when they exist on the agent.
|
||||
- Missing custom scripts fall back to the bundled generic handler and are logged.
|
||||
- Default script names such as `generic_steam_workshop_windows_cygwin.sh` are copied from the Panel module source and must not be checked as bare files on the agent.
|
||||
|
||||
Current default install paths:
|
||||
|
||||
- Generic Steam Workshop content: `{SERVER_ROOT}/workshop/{MOD_FOLDER}`
|
||||
- DayZ / Arma strategy content: `{SERVER_ROOT}/{MOD_FOLDER}` for root `@<workshop_id>` folder compatibility
|
||||
|
||||
SteamCMD requirements:
|
||||
|
||||
- Linux agents need SteamCMD available at the configured profile/template path, `STEAMCMD_PATH`, `/home/gameserver/steamcmd/steamcmd.sh`, or in `PATH`.
|
||||
- Windows agents currently use the existing Cygwin agent model and run the bundled Cygwin-compatible shell handler. SteamCMD may be provided as `steamcmd.exe`, `steamcmd.sh`, an explicit configured path, or via `STEAMCMD_PATH`.
|
||||
- Missing SteamCMD should return a clear error, not a generic script failure.
|
||||
|
||||
The legacy `steam_workshop` monitor button is intentionally suppressed so users are not sent to the deprecated standalone module.
|
||||
|
||||
## Current Limitations
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue