From e921a49d5baf19ace8821d6cc11a694094f5118f Mon Sep 17 00:00:00 2001 From: Frank Harris Date: Sat, 6 Jun 2026 12:50:14 -0500 Subject: [PATCH] steam workshotp --- .../addonsmanager/SERVER_CONTENT_ROADMAP.md | 5 +- .../SERVER_CONTENT_WORKSHOP_PHASE1.md | 29 ++++- .../workshop/generic_steam_workshop_linux.sh | 2 +- .../generic_steam_workshop_windows_cygwin.sh | 2 +- .../addonsmanager/server_content_helpers.php | 55 +++++++-- .../tests/workshop_helpers_test.php | 116 ++++++++++++++++++ .../modules/addonsmanager/workshop_action.php | 7 +- .../server_configs/arma3_linux64.xml | 1 + .../server_configs/arma3_win64.xml | 1 + .../steam_workshop/includes/functions.php | 23 +++- docs/decisions/0004-workshop-system.md | 3 + .../WORKSHOP_PHASE1_IMPLEMENTATION.md | 22 +++- docs/features/WORKSHOP_SYSTEM.md | 28 ++++- docs/modules/SERVER_CONTENT_MANAGER.md | 17 +++ 14 files changed, 285 insertions(+), 26 deletions(-) create mode 100644 Panel/modules/addonsmanager/tests/workshop_helpers_test.php diff --git a/Panel/modules/addonsmanager/SERVER_CONTENT_ROADMAP.md b/Panel/modules/addonsmanager/SERVER_CONTENT_ROADMAP.md index d6a616f1..afc575f2 100644 --- a/Panel/modules/addonsmanager/SERVER_CONTENT_ROADMAP.md +++ b/Panel/modules/addonsmanager/SERVER_CONTENT_ROADMAP.md @@ -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`, diff --git a/Panel/modules/addonsmanager/SERVER_CONTENT_WORKSHOP_PHASE1.md b/Panel/modules/addonsmanager/SERVER_CONTENT_WORKSHOP_PHASE1.md index 3068e84d..c302a7c0 100644 --- a/Panel/modules/addonsmanager/SERVER_CONTENT_WORKSHOP_PHASE1.md +++ b/Panel/modules/addonsmanager/SERVER_CONTENT_WORKSHOP_PHASE1.md @@ -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/@`. +- Arma/DayZ-style strategies install under `{SERVER_ROOT}/@` 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 diff --git a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh index 2f29207d..dc366922 100755 --- a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh +++ b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh @@ -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) diff --git a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh index c8700564..09d594d2 100755 --- a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh +++ b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh @@ -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) diff --git a/Panel/modules/addonsmanager/server_content_helpers.php b/Panel/modules/addonsmanager/server_content_helpers.php index 9a3f8757..3c6f1162 100644 --- a/Panel/modules/addonsmanager/server_content_helpers.php +++ b/Panel/modules/addonsmanager/server_content_helpers.php @@ -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, diff --git a/Panel/modules/addonsmanager/tests/workshop_helpers_test.php b/Panel/modules/addonsmanager/tests/workshop_helpers_test.php new file mode 100644 index 00000000..1ee45db1 --- /dev/null +++ b/Panel/modules/addonsmanager/tests/workshop_helpers_test.php @@ -0,0 +1,116 @@ +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(''); +$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('/agent/custom/workshop.sh'); +$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('107410'); +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"; diff --git a/Panel/modules/addonsmanager/workshop_action.php b/Panel/modules/addonsmanager/workshop_action.php index 5bb5c6cf..7a922943 100644 --- a/Panel/modules/addonsmanager/workshop_action.php +++ b/Panel/modules/addonsmanager/workshop_action.php @@ -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 @ 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)) { diff --git a/Panel/modules/config_games/server_configs/arma3_linux64.xml b/Panel/modules/config_games/server_configs/arma3_linux64.xml index 892e5728..c0e85c6e 100644 --- a/Panel/modules/config_games/server_configs/arma3_linux64.xml +++ b/Panel/modules/config_games/server_configs/arma3_linux64.xml @@ -2,6 +2,7 @@ arma3_linux64 steamcmd Arma 3 + 107410 arma3server_x64 %CONFIG% %CFG% %PROFILES% %NAME% %IP% %PORT% %PLAYERS% %MODLIST% %SERVERMODLIST% %AUTOINIT% diff --git a/Panel/modules/config_games/server_configs/arma3_win64.xml b/Panel/modules/config_games/server_configs/arma3_win64.xml index caa59806..52fd77aa 100644 --- a/Panel/modules/config_games/server_configs/arma3_win64.xml +++ b/Panel/modules/config_games/server_configs/arma3_win64.xml @@ -2,6 +2,7 @@ arma3_win64 steamcmd Arma 3 + 107410 arma3server.exe -profiles=profile -name=player -config=profile\server.cfg -cfg=profile\basic.cfg %PORT% %PLAYERS% %RANKING% %AUTOINIT% %DEBUG% %MODS% %SERVERMODS% diff --git a/Panel/modules/steam_workshop/includes/functions.php b/Panel/modules/steam_workshop/includes/functions.php index 9cbb8d97..d32ade36 100644 --- a/Panel/modules/steam_workshop/includes/functions.php +++ b/Panel/modules/steam_workshop/includes/functions.php @@ -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( diff --git a/docs/decisions/0004-workshop-system.md b/docs/decisions/0004-workshop-system.md index 3e4dfbc9..7a9f58b4 100644 --- a/docs/decisions/0004-workshop-system.md +++ b/docs/decisions/0004-workshop-system.md @@ -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 `@` folders and copy `.bikey` files into `keys` when present. - Startup parameter generation remains a later phase. diff --git a/docs/features/WORKSHOP_PHASE1_IMPLEMENTATION.md b/docs/features/WORKSHOP_PHASE1_IMPLEMENTATION.md index 3ad66b3c..591e8f62 100644 --- a/docs/features/WORKSHOP_PHASE1_IMPLEMENTATION.md +++ b/docs/features/WORKSHOP_PHASE1_IMPLEMENTATION.md @@ -59,6 +59,13 @@ The admin template defines capability and policy. The customer supplies only Wor - `bash