diff --git a/Panel/js/modules/addonsmanager.js b/Panel/js/modules/addonsmanager.js
index b2135530..b8ccc7ab 100644
--- a/Panel/js/modules/addonsmanager.js
+++ b/Panel/js/modules/addonsmanager.js
@@ -1,7 +1,15 @@
$(function() {
+ function replaceTemplate(template, values) {
+ var output = String(template || '');
+ $.each(values, function(key, value) {
+ output = output.split(key).join(value);
+ });
+ return output;
+ }
+
var methodToRows = {
download_zip: ['#scm-row-url', '#scm-row-path'],
- steam_workshop: ['#scm-row-workshop-id', '#scm-row-workshop-app-id', '#scm-row-target-path-template', '#scm-row-optional-folder-name'],
+ steam_workshop: ['#scm-row-workshop-id', '#scm-row-workshop-app-id', '#scm-row-target-path-template', '#scm-row-optional-folder-name', '#scm-row-post-script', '#scm-row-launch-param-additions'],
post_script: ['#scm-row-post-script'],
config_edit: ['#scm-row-path', '#scm-row-config-edit-rule']
};
@@ -37,4 +45,66 @@ $(function() {
$method.on('change', applyContentTypeUi);
applyContentTypeUi();
+
+ var $userSelect = $('#scm-user-addon-select');
+ var $userWorkshopRows = $('.scm-user-workshop-row');
+ var $userWorkshopId = $('#scm-user-workshop-id');
+ var $userWorkshopAppId = $('#scm-user-workshop-app-id');
+ var $userTargetTemplate = $('#scm-user-target-path-template');
+ var $userOptionalFolderName = $('#scm-user-optional-folder-name');
+ var $userPreview = $('#scm-user-target-path-preview');
+
+ function updateUserWorkshopUi() {
+ if ($userSelect.length === 0) return;
+ var $selected = $userSelect.find('option:selected');
+ var installMethod = String($selected.data('installMethod') || '');
+ var isWorkshop = installMethod === 'steam_workshop';
+ $userWorkshopRows.toggle(isWorkshop);
+ if (!isWorkshop) {
+ return;
+ }
+
+ if (!$userWorkshopId.val()) {
+ $userWorkshopId.val(String($selected.data('workshopItemId') || ''));
+ }
+ if (!$userWorkshopAppId.val()) {
+ $userWorkshopAppId.val(String($selected.data('workshopAppId') || $userWorkshopAppId.data('defaultAppId') || ''));
+ }
+ if (!$userTargetTemplate.val()) {
+ $userTargetTemplate.val(String($selected.data('targetPathTemplate') || $userTargetTemplate.data('defaultTemplate') || ''));
+ }
+ if (!$userOptionalFolderName.val()) {
+ $userOptionalFolderName.val(String($selected.data('optionalFolderName') || ''));
+ }
+
+ var workshopId = $.trim($userWorkshopId.val());
+ var workshopAppId = $.trim($userWorkshopAppId.val()) || String($selected.data('workshopAppId') || $userWorkshopAppId.data('defaultAppId') || '');
+ var folderName = $.trim($userOptionalFolderName.val()) || (workshopId ? '@' + workshopId : '@{WORKSHOP_ID}');
+ var targetTemplate = $.trim($userTargetTemplate.val()) || String($selected.data('targetPathTemplate') || $userTargetTemplate.data('defaultTemplate') || '');
+ var previewValues = {
+ '{SERVER_ROOT}': String($userPreview.data('serverRoot') || ''),
+ '{GAME_ROOT}': String($userPreview.data('gameRoot') || ''),
+ '{WORKSHOP_ID}': workshopId || '{WORKSHOP_ID}',
+ '{WORKSHOP_APP_ID}': workshopAppId || '{WORKSHOP_APP_ID}',
+ '{STEAM_APP_ID}': String($userPreview.data('steamAppId') || '{STEAM_APP_ID}'),
+ '{FOLDER_NAME}': folderName,
+ '{MOD_FOLDER}': folderName
+ };
+ $userPreview.text(replaceTemplate(targetTemplate, previewValues));
+ }
+
+ if ($userSelect.length) {
+ $userSelect.on('change', function() {
+ $userWorkshopId.val(String($(this).find('option:selected').data('workshopItemId') || ''));
+ $userWorkshopAppId.val(String($(this).find('option:selected').data('workshopAppId') || ''));
+ $userTargetTemplate.val(String($(this).find('option:selected').data('targetPathTemplate') || ''));
+ $userOptionalFolderName.val(String($(this).find('option:selected').data('optionalFolderName') || ''));
+ updateUserWorkshopUi();
+ });
+ $userWorkshopId.on('input', updateUserWorkshopUi);
+ $userWorkshopAppId.on('input', updateUserWorkshopUi);
+ $userTargetTemplate.on('input', updateUserWorkshopUi);
+ $userOptionalFolderName.on('input', updateUserWorkshopUi);
+ updateUserWorkshopUi();
+ }
});
diff --git a/Panel/lang/English/modules/addonsmanager.php b/Panel/lang/English/modules/addonsmanager.php
index 22e50cce..d41e110d 100644
--- a/Panel/lang/English/modules/addonsmanager.php
+++ b/Panel/lang/English/modules/addonsmanager.php
@@ -42,9 +42,9 @@ define('LANG_wait_while_decompressing', "Wait while the file %s is decompressed.
define('LANG_addon_name', "Content Item Name");
define('LANG_url', "URL");
define('LANG_select_game_type', "Select Game Type");
-define('LANG_plugin', "Plugins / Mods");
-define('LANG_mappack', "Map Packs");
-define('LANG_config', "Config Packs");
+define('LANG_plugin', "Downloadable Mod");
+define('LANG_mappack', "Steam Workshop Item");
+define('LANG_config', "Configuration Package");
if (!defined('LANG_version')) {
define('LANG_version', "Version");
}
@@ -93,8 +93,8 @@ define('LANG_target_path_template', "Target Path");
define('LANG_optional_folder_name', "Optional Folder Name");
define('LANG_config_edit_rule', "Config Edit Rule");
define('LANG_launch_param_additions', "Launch Parameter Additions");
-define('LANG_content_type_help_download_zip', "Downloads an archive/file from URL; extract path is optional.");
-define('LANG_content_type_help_steam_workshop', "Installs/updates a Workshop item with Workshop ID (no URL required).");
-define('LANG_content_type_help_post_script', "Runs a scripted installer action (no URL required).");
-define('LANG_content_type_help_config_edit', "Edits config at target path using provided action/rules (no URL required).");
+define('LANG_content_type_help_download_zip', "Download and extract a ZIP, RAR, or archive file.");
+define('LANG_content_type_help_steam_workshop', "Install a Steam Workshop mod using Workshop ID.");
+define('LANG_content_type_help_post_script', "Run a custom scripted installation process.");
+define('LANG_content_type_help_config_edit', "Install configuration files, profiles, or templates.");
?>
diff --git a/Panel/modules/addonsmanager/addons_installer.php b/Panel/modules/addonsmanager/addons_installer.php
index b602f5b7..f22fbe01 100644
--- a/Panel/modules/addonsmanager/addons_installer.php
+++ b/Panel/modules/addonsmanager/addons_installer.php
@@ -1,3 +1,4 @@
+
isset($addon_info['url']) ? $addon_info['url'] : '',
- 'path' => isset($addon_info['path']) ? $addon_info['path'] : '',
- 'workshop_item_id' => isset($addon_info['workshop_item_id']) ? $addon_info['workshop_item_id'] : '',
- 'target_path_template' => isset($addon_info['target_path_template']) ? $addon_info['target_path_template'] : '',
- 'post_script' => isset($addon_info['post_script']) ? $addon_info['post_script'] : '',
- 'config_edit_rule' => isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : '',
- );
$validation_message = '';
- if ($state == "start" && !scm_validate_install_method_payload($install_method, $validation_payload, $validation_message)) {
+ if ($state == "start" && !scm_validate_install_method_payload($install_method, $install_payload, $validation_message)) {
print_failure($validation_message);
return;
}
@@ -209,7 +206,7 @@ function exec_ogp_module() {
return;
}
}
- $url = $addon_info['url'];
+ $url = $install_payload['url'];
$filename = basename($url);
#### Replace template variables in the post-install script with
#### live server data before sending to the agent.
@@ -285,52 +282,38 @@ function exec_ogp_module() {
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
- 'workshop_id' => isset($addon_info['workshop_item_id']) ? (string)$addon_info['workshop_item_id'] : '',
- 'target_path' => isset($addon_info['target_path_template']) ? (string)$addon_info['target_path_template'] : (string)$addon_info['path'],
+ 'workshop_id' => isset($install_payload['workshop_item_id']) ? (string)$install_payload['workshop_item_id'] : '',
+ 'target_path' => ($install_method === 'steam_workshop')
+ ? (string)$install_payload['target_path_template']
+ : (string)$install_payload['path'],
'action' => 'started',
));
if ($install_method === 'steam_workshop') {
scm_ensure_workshop_schema($db);
- $workshop_item_id = trim((string)$addon_info['workshop_item_id']);
- $target_path_template = trim((string)$addon_info['target_path_template']);
- $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(
- 'optional_folder_name' => trim((string)$addon_info['optional_folder_name']),
- 'workshop_app_id' => trim((string)$addon_info['workshop_app_id']),
- ))
- : array('ok' => false);
- if (empty($resolved['ok'])) {
- $fallback_profile = function_exists('sw_get_profile_for_home') ? sw_get_profile_for_home($db, (int)$home_id) : false;
- $fallback_workshop_app_id = trim((string)$addon_info['workshop_app_id']);
- if ($fallback_workshop_app_id === '' && is_array($fallback_profile) && !empty($fallback_profile['workshop_app_id'])) {
- $fallback_workshop_app_id = (string)$fallback_profile['workshop_app_id'];
- }
- if ($fallback_workshop_app_id === '') {
- $fallback_workshop_app_id = scm_extract_workshop_app_id($server_xml);
- }
- $placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => (string)$server_xml->exe_location), array(
- 'WORKSHOP_ID' => $workshop_item_id,
- 'WORKSHOP_APP_ID' => $fallback_workshop_app_id,
- 'STEAM_APP_ID' => (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '',
- ));
- $resolved = array(
- 'workshop_app_id' => $fallback_workshop_app_id,
- 'steam_app_id' => (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '',
- 'target_path_resolved' => scm_apply_placeholders($target_path_template, $placeholder_map),
- );
+ $workshop_runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $install_payload, $validation_message);
+ if ($workshop_runtime === false) {
+ print_failure($validation_message);
+ echo "
".get_lang('back')."
";
+ return;
}
- $workshop_app_id = isset($resolved['workshop_app_id']) ? (string)$resolved['workshop_app_id'] : '';
- $steam_app_id = isset($resolved['steam_app_id']) ? (string)$resolved['steam_app_id'] : '';
- $target_path_resolved = isset($resolved['target_path_resolved']) ? (string)$resolved['target_path_resolved'] : '';
+ $workshop_item_id = (string)$workshop_runtime['workshop_item_id'];
+ $target_path_template = (string)$workshop_runtime['target_path_template'];
+ $workshop_app_id = (string)$workshop_runtime['workshop_app_id'];
+ $steam_app_id = (string)$workshop_runtime['steam_app_id'];
+ $target_path_resolved = (string)$workshop_runtime['target_path_resolved'];
$extra_manifest = array(
'addon_id' => (int)$addon_id,
'target_path_template' => $target_path_template,
'target_path_resolved' => $target_path_resolved,
- 'optional_folder_name' => trim((string)$addon_info['optional_folder_name']),
+ 'optional_folder_name' => trim((string)$install_payload['optional_folder_name']),
'config_edit_rule' => trim((string)$addon_info['config_edit_rule']),
'launch_param_additions' => trim((string)$addon_info['launch_param_additions']),
'workshop_app_id' => $workshop_app_id,
'steam_app_id' => $steam_app_id,
+ 'steamcmd_path' => isset($workshop_runtime['steamcmd_path']) ? (string)$workshop_runtime['steamcmd_path'] : '',
+ 'workshop_download_dir' => isset($workshop_runtime['workshop_download_dir']) ? (string)$workshop_runtime['workshop_download_dir'] : '',
+ 'server_root' => isset($workshop_runtime['server_root']) ? (string)$workshop_runtime['server_root'] : rtrim((string)$home_info['home_path'], '/'),
+ 'post_install_script' => trim((string)$post_script),
);
$workshop_error = '';
$workshop_ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest);
@@ -350,7 +333,10 @@ function exec_ogp_module() {
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_id' => $workshop_item_id,
+ 'steam_app_id' => $steam_app_id,
+ 'workshop_app_id' => $workshop_app_id,
'target_path' => $target_path_resolved,
+ 'final_folder_path' => $target_path_resolved,
'action' => 'succeeded',
));
print_success(get_lang('addon_installed_successfully'));
@@ -363,6 +349,8 @@ function exec_ogp_module() {
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_id' => $workshop_item_id,
+ 'steam_app_id' => $steam_app_id,
+ 'workshop_app_id' => $workshop_app_id,
'target_path' => $target_path_resolved,
'action' => 'failed',
'error' => $workshop_error,
@@ -558,10 +546,28 @@ function exec_ogp_module() {
}
?>
resultQuery(
+ "SELECT addon_id, name, install_method, workshop_item_id, workshop_app_id, target_path_template, optional_folder_name
+ FROM OGP_DB_PREFIXaddons
+ WHERE addon_type='".$addon_type."' AND home_cfg_id=" . $home_cfg_id . $query_groups . "
+ ORDER BY name ASC"
+ );
+ if (!is_array($addons)) {
+ $addons = [];
+ }
+ $selected_addon = isset($addons[0]) ? $addons[0] : array();
+ $default_install_method = isset($selected_addon['install_method']) ? scm_get_install_method_default($selected_addon['install_method']) : '';
+ $is_workshop_default = ($default_install_method === 'steam_workshop');
+ $workshop_profile = function_exists('sw_get_profile_for_home') ? sw_get_profile_for_home($db, (int)$home_id) : false;
+ $default_workshop_app_id = !empty($selected_addon['workshop_app_id'])
+ ? trim((string)$selected_addon['workshop_app_id'])
+ : ((is_array($workshop_profile) && !empty($workshop_profile['workshop_app_id'])) ? (string)$workshop_profile['workshop_app_id'] : scm_extract_workshop_app_id($server_xml));
+ $default_target_template = !empty($selected_addon['target_path_template'])
+ ? trim((string)$selected_addon['target_path_template'])
+ : ((is_array($workshop_profile) && !empty($workshop_profile['install_path_template'])) ? (string)$workshop_profile['install_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}');
+ $default_optional_folder_name = !empty($selected_addon['optional_folder_name']) ? trim((string)$selected_addon['optional_folder_name']) : '';
?>
@@ -579,21 +585,40 @@ function exec_ogp_module() {
|
| : |
- |
+ > |
+
+ Install a Steam Workshop mod using Workshop ID. URL is not required.
+ |
+ >| Workshop App ID Override |
+
+ |
+ > |
+
+ |
+ > |
+
+ Supported placeholders: {SERVER_ROOT}, {GAME_ROOT}, {WORKSHOP_ID}, {WORKSHOP_APP_ID}, {STEAM_APP_ID}, {FOLDER_NAME}, {MOD_FOLDER}
+ |
+ >| Target Path Preview |
+
+ |
| |
diff --git a/Panel/modules/addonsmanager/addons_manager.php b/Panel/modules/addonsmanager/addons_manager.php
index 9d402239..05d0e506 100644
--- a/Panel/modules/addonsmanager/addons_manager.php
+++ b/Panel/modules/addonsmanager/addons_manager.php
@@ -202,6 +202,7 @@ function exec_ogp_module() {
|
+ Example Arma 3 Workshop ID: 450814997
|
@@ -217,8 +218,8 @@ function exec_ogp_module() {
|
-
- Supported placeholders: {HOME_ID}, {SERVER_ROOT}, {GAME_ROOT}, {WORKSHOP_ID}, {WORKSHOP_APP_ID}, {STEAM_APP_ID}
+
+ Supported placeholders: {HOME_ID}, {SERVER_ROOT}, {GAME_ROOT}, {WORKSHOP_ID}, {WORKSHOP_APP_ID}, {STEAM_APP_ID}, {FOLDER_NAME}, {MOD_FOLDER}
|
diff --git a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh
old mode 100644
new mode 100755
index ab6b5271..e63912e5
--- a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh
+++ b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh
@@ -7,57 +7,190 @@ if [[ -z "$MANIFEST_PATH" ]]; then
exit 1
fi
-if [[ ! -f "$MANIFEST_PATH" ]]; then
- echo "Manifest not found: $MANIFEST_PATH"
- exit 1
-fi
+python3 - "$MANIFEST_PATH" <<'PY'
+import json
+import os
+import shutil
+import subprocess
+import sys
+from datetime import datetime
-MANIFEST_DIR="$(dirname "$MANIFEST_PATH")"
-WORKSHOP_DIR="${MANIFEST_DIR}/workshop"
-REMOVED_DIR="${WORKSHOP_DIR}/removed"
-LOG_FILE="${MANIFEST_DIR}/workshop_phase1.log"
+manifest_path = os.path.abspath(sys.argv[1])
+if not os.path.isfile(manifest_path):
+ print(f"Manifest not found: {manifest_path}")
+ sys.exit(1)
-mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR"
+manifest_dir = os.path.dirname(manifest_path)
+home_root = os.path.dirname(manifest_dir)
+log_file = os.path.join(manifest_dir, 'workshop_install.log')
+removed_dir = os.path.join(manifest_dir, 'workshop', 'removed')
+os.makedirs(removed_dir, exist_ok=True)
-ACTION="$(python3 - <<'PY' "$MANIFEST_PATH"
-import json,sys
-with open(sys.argv[1], "r", encoding="utf-8") as f:
- data=json.load(f)
-print(data.get("action",""))
+
+def log(message, status=None):
+ line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
+ if status:
+ line += f" [{status}]"
+ line += f" {message}"
+ print(line)
+ with open(log_file, 'a', encoding='utf-8') as handle:
+ handle.write(line + "\n")
+
+
+def fail(message):
+ log(message, 'Failed')
+ raise RuntimeError(message)
+
+
+def uniq_numeric_items(raw_items):
+ seen = []
+ for value in raw_items:
+ text = str(value).strip()
+ if text.isdigit() and text not in seen:
+ seen.append(text)
+ return seen
+
+
+def render_template(template, values):
+ rendered = str(template or '')
+ for key, value in values.items():
+ rendered = rendered.replace('{' + key + '}', str(value))
+ return rendered
+
+
+def ensure_under_home(path_value):
+ target = os.path.abspath(path_value)
+ try:
+ common = os.path.commonpath([home_root, target])
+ except ValueError:
+ common = ''
+ if common != os.path.abspath(home_root):
+ fail(f"Refusing to write outside server home: {target}")
+ return target
+
+
+def resolve_steamcmd(explicit_path=''):
+ candidates = []
+ explicit_path = str(explicit_path or '').strip()
+ if explicit_path:
+ candidates.append(explicit_path)
+ env_value = os.environ.get('STEAMCMD_PATH', '').strip()
+ if env_value:
+ candidates.append(env_value)
+ for path_value in (
+ '/home/gameserver/steamcmd/steamcmd.sh',
+ shutil.which('steamcmd'),
+ shutil.which('steamcmd.exe'),
+ ):
+ if path_value:
+ candidates.append(path_value)
+ for candidate in candidates:
+ if candidate and os.path.isfile(candidate):
+ return candidate
+ fail('SteamCMD is missing on the agent host.')
+
+
+def sync_copy(src, dst):
+ if not os.path.isdir(src):
+ fail(f"Workshop download source was not found: {src}")
+ os.makedirs(dst, exist_ok=True)
+ for entry in os.listdir(src):
+ source_entry = os.path.join(src, entry)
+ target_entry = os.path.join(dst, entry)
+ if os.path.isdir(source_entry):
+ sync_copy(source_entry, target_entry)
+ else:
+ os.makedirs(os.path.dirname(target_entry), exist_ok=True)
+ shutil.copy2(source_entry, target_entry)
+
+
+try:
+ with open(manifest_path, 'r', encoding='utf-8') as handle:
+ manifest = json.load(handle)
+
+ extra = manifest.get('extra') or {}
+ action = str(manifest.get('action', '')).strip()
+ raw_items = manifest.get('items', [])
+ if isinstance(raw_items, dict):
+ raw_items = raw_items.get('workshop_item_ids', [])
+ items = uniq_numeric_items(raw_items)
+ if not items:
+ fail('No Workshop IDs were found in the manifest.')
+
+ workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip()
+ steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip()
+ server_root = ensure_under_home(extra.get('server_root') or home_root)
+ steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '')
+ post_install_script = str(extra.get('post_install_script') or '').strip()
+ default_download_dir = extra.get('workshop_download_dir') or os.path.join(server_root, 'steamapps', 'workshop', 'content', workshop_app_id or steam_app_id)
+
+ action_label = 'Queued' if action in ('install', 'update', 'check_updates') else action
+ log(f"action={action} manifest={manifest_path} steam_app_id={steam_app_id or 'n/a'} workshop_app_id={workshop_app_id or 'n/a'}", action_label)
+
+ for workshop_id in items:
+ folder_name = str(extra.get('optional_folder_name') or '').strip() or ('@' + workshop_id)
+ template_values = {
+ 'HOME_ID': manifest.get('home_id', ''),
+ 'SERVER_ROOT': server_root,
+ 'GAME_ROOT': server_root,
+ 'WORKSHOP_ID': workshop_id,
+ 'WORKSHOP_APP_ID': workshop_app_id,
+ 'STEAM_APP_ID': steam_app_id,
+ 'FOLDER_NAME': folder_name,
+ 'MOD_FOLDER': folder_name,
+ }
+ target_template = str(extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
+ target_path = str(extra.get('target_path_resolved') or '').strip()
+ if len(items) != 1 or not target_path:
+ target_path = render_template(target_template, template_values)
+ target_path = ensure_under_home(target_path)
+
+ download_dir = ensure_under_home(render_template(default_download_dir, template_values))
+ source_path = os.path.join(download_dir, workshop_id)
+
+ if action in ('install', 'update', 'check_updates'):
+ if not workshop_app_id:
+ fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
+ command = [
+ steamcmd_path,
+ '+force_install_dir', server_root,
+ '+login', 'anonymous',
+ '+workshop_download_item', workshop_app_id, workshop_id, 'validate',
+ '+quit',
+ ]
+ log(f"workshop_id={workshop_id} steamcmd={' '.join(command)}", 'Downloading Workshop Item')
+ result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
+ if result.stdout:
+ for line in result.stdout.splitlines():
+ log(f"steamcmd[{workshop_id}] {line}")
+ if result.returncode != 0:
+ fail(f"SteamCMD failed for Workshop item {workshop_id} with exit code {result.returncode}.")
+ if not os.path.isdir(source_path):
+ fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}")
+
+ if action != 'check_updates':
+ log(f"workshop_id={workshop_id} install_path={target_path}", 'Extracting/Copying')
+ sync_copy(source_path, target_path)
+ log(f"workshop_id={workshop_id} final_folder_path={target_path}", 'Applying Folder Name')
+ if post_install_script:
+ log(f"workshop_id={workshop_id} cwd={server_root}", 'Running Post-install Script')
+ post_result = subprocess.run(['bash', '-lc', post_install_script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
+ if post_result.stdout:
+ for line in post_result.stdout.splitlines():
+ log(f"post_install[{workshop_id}] {line}")
+ if post_result.returncode != 0:
+ fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.")
+ log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed')
+ elif action == 'remove':
+ if os.path.exists(target_path):
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+ removed_path = os.path.join(removed_dir, f"{workshop_id}_{timestamp}")
+ shutil.move(target_path, removed_path)
+ log(f"workshop_id={workshop_id} removed_path={removed_path}", 'Completed')
+ else:
+ log(f"workshop_id={workshop_id} target_path_missing={target_path}", 'Completed')
+ else:
+ fail(f"Unknown workshop action: {action}")
+except RuntimeError:
+ sys.exit(1)
PY
-)"
-
-ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH"
-import json,sys
-with open(sys.argv[1], "r", encoding="utf-8") as f:
- data=json.load(f)
-items=data.get("items",[])
-print(",".join(str(x) for x in items if str(x).isdigit()))
-PY
-)"
-
-{
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 action=${ACTION} manifest=${MANIFEST_PATH}"
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 items=${ITEMS}"
-} >> "$LOG_FILE"
-
-case "$ACTION" in
- install|update)
- # TODO: Replace with game-specific SteamCMD workshop install/update logic.
- # Example flow:
- # 1) Use workshop_app_id + item IDs from the manifest.
- # 2) Download/refresh content into a controlled staging folder.
- # 3) Copy/sync approved files into the game server content path.
- ;;
- remove)
- # Phase 1 safety behavior: avoid destructive delete.
- # TODO: move/disable per-item content using game-specific mapping rules.
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE"
- ;;
- *)
- echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE"
- exit 1
- ;;
-esac
-
-exit 0
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
old mode 100644
new mode 100755
index c9b419ac..011a4f1e
--- a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh
+++ b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh
@@ -7,52 +7,191 @@ if [[ -z "$MANIFEST_PATH" ]]; then
exit 1
fi
-if [[ ! -f "$MANIFEST_PATH" ]]; then
- echo "Manifest not found: $MANIFEST_PATH"
- exit 1
-fi
+python3 - "$MANIFEST_PATH" <<'PY'
+import json
+import os
+import shutil
+import subprocess
+import sys
+from datetime import datetime
-MANIFEST_DIR="$(dirname "$MANIFEST_PATH")"
-WORKSHOP_DIR="${MANIFEST_DIR}/workshop"
-REMOVED_DIR="${WORKSHOP_DIR}/removed"
-LOG_FILE="${MANIFEST_DIR}/workshop_phase1_windows.log"
+manifest_path = os.path.abspath(sys.argv[1])
+if not os.path.isfile(manifest_path):
+ print(f"Manifest not found: {manifest_path}")
+ sys.exit(1)
-mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR"
+manifest_dir = os.path.dirname(manifest_path)
+home_root = os.path.dirname(manifest_dir)
+log_file = os.path.join(manifest_dir, 'workshop_install_windows.log')
+removed_dir = os.path.join(manifest_dir, 'workshop', 'removed')
+os.makedirs(removed_dir, exist_ok=True)
-ACTION="$(python3 - <<'PY' "$MANIFEST_PATH"
-import json,sys
-with open(sys.argv[1], "r", encoding="utf-8") as f:
- data=json.load(f)
-print(data.get("action",""))
+
+def log(message, status=None):
+ line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
+ if status:
+ line += f" [{status}]"
+ line += f" {message}"
+ print(line)
+ with open(log_file, 'a', encoding='utf-8') as handle:
+ handle.write(line + "\n")
+
+
+def fail(message):
+ log(message, 'Failed')
+ raise RuntimeError(message)
+
+
+def uniq_numeric_items(raw_items):
+ seen = []
+ for value in raw_items:
+ text = str(value).strip()
+ if text.isdigit() and text not in seen:
+ seen.append(text)
+ return seen
+
+
+def render_template(template, values):
+ rendered = str(template or '')
+ for key, value in values.items():
+ rendered = rendered.replace('{' + key + '}', str(value))
+ return rendered
+
+
+def ensure_under_home(path_value):
+ target = os.path.abspath(path_value)
+ try:
+ common = os.path.commonpath([home_root, target])
+ except ValueError:
+ common = ''
+ if common != os.path.abspath(home_root):
+ fail(f"Refusing to write outside server home: {target}")
+ return target
+
+
+def resolve_steamcmd(explicit_path=''):
+ candidates = []
+ explicit_path = str(explicit_path or '').strip()
+ if explicit_path:
+ candidates.append(explicit_path)
+ env_value = os.environ.get('STEAMCMD_PATH', '').strip()
+ if env_value:
+ candidates.append(env_value)
+ for path_value in (
+ '/home/gameserver/steamcmd/steamcmd.sh',
+ shutil.which('steamcmd'),
+ shutil.which('steamcmd.exe'),
+ shutil.which('steamcmd.sh'),
+ ):
+ if path_value:
+ candidates.append(path_value)
+ for candidate in candidates:
+ if candidate and os.path.isfile(candidate):
+ return candidate
+ fail('SteamCMD is missing on the agent host.')
+
+
+def sync_copy(src, dst):
+ if not os.path.isdir(src):
+ fail(f"Workshop download source was not found: {src}")
+ os.makedirs(dst, exist_ok=True)
+ for entry in os.listdir(src):
+ source_entry = os.path.join(src, entry)
+ target_entry = os.path.join(dst, entry)
+ if os.path.isdir(source_entry):
+ sync_copy(source_entry, target_entry)
+ else:
+ os.makedirs(os.path.dirname(target_entry), exist_ok=True)
+ shutil.copy2(source_entry, target_entry)
+
+
+try:
+ with open(manifest_path, 'r', encoding='utf-8') as handle:
+ manifest = json.load(handle)
+
+ extra = manifest.get('extra') or {}
+ action = str(manifest.get('action', '')).strip()
+ raw_items = manifest.get('items', [])
+ if isinstance(raw_items, dict):
+ raw_items = raw_items.get('workshop_item_ids', [])
+ items = uniq_numeric_items(raw_items)
+ if not items:
+ fail('No Workshop IDs were found in the manifest.')
+
+ workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip()
+ steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip()
+ server_root = ensure_under_home(extra.get('server_root') or home_root)
+ steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '')
+ post_install_script = str(extra.get('post_install_script') or '').strip()
+ default_download_dir = extra.get('workshop_download_dir') or os.path.join(server_root, 'steamapps', 'workshop', 'content', workshop_app_id or steam_app_id)
+
+ action_label = 'Queued' if action in ('install', 'update', 'check_updates') else action
+ log(f"action={action} manifest={manifest_path} steam_app_id={steam_app_id or 'n/a'} workshop_app_id={workshop_app_id or 'n/a'}", action_label)
+
+ for workshop_id in items:
+ folder_name = str(extra.get('optional_folder_name') or '').strip() or ('@' + workshop_id)
+ template_values = {
+ 'HOME_ID': manifest.get('home_id', ''),
+ 'SERVER_ROOT': server_root,
+ 'GAME_ROOT': server_root,
+ 'WORKSHOP_ID': workshop_id,
+ 'WORKSHOP_APP_ID': workshop_app_id,
+ 'STEAM_APP_ID': steam_app_id,
+ 'FOLDER_NAME': folder_name,
+ 'MOD_FOLDER': folder_name,
+ }
+ target_template = str(extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
+ target_path = str(extra.get('target_path_resolved') or '').strip()
+ if len(items) != 1 or not target_path:
+ target_path = render_template(target_template, template_values)
+ target_path = ensure_under_home(target_path)
+
+ download_dir = ensure_under_home(render_template(default_download_dir, template_values))
+ source_path = os.path.join(download_dir, workshop_id)
+
+ if action in ('install', 'update', 'check_updates'):
+ if not workshop_app_id:
+ fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
+ command = [
+ steamcmd_path,
+ '+force_install_dir', server_root,
+ '+login', 'anonymous',
+ '+workshop_download_item', workshop_app_id, workshop_id, 'validate',
+ '+quit',
+ ]
+ log(f"workshop_id={workshop_id} steamcmd={' '.join(command)}", 'Downloading Workshop Item')
+ result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
+ if result.stdout:
+ for line in result.stdout.splitlines():
+ log(f"steamcmd[{workshop_id}] {line}")
+ if result.returncode != 0:
+ fail(f"SteamCMD failed for Workshop item {workshop_id} with exit code {result.returncode}.")
+ if not os.path.isdir(source_path):
+ fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}")
+
+ if action != 'check_updates':
+ log(f"workshop_id={workshop_id} install_path={target_path}", 'Extracting/Copying')
+ sync_copy(source_path, target_path)
+ log(f"workshop_id={workshop_id} final_folder_path={target_path}", 'Applying Folder Name')
+ if post_install_script:
+ log(f"workshop_id={workshop_id} cwd={server_root}", 'Running Post-install Script')
+ post_result = subprocess.run(['bash', '-lc', post_install_script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
+ if post_result.stdout:
+ for line in post_result.stdout.splitlines():
+ log(f"post_install[{workshop_id}] {line}")
+ if post_result.returncode != 0:
+ fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.")
+ log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed')
+ elif action == 'remove':
+ if os.path.exists(target_path):
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+ removed_path = os.path.join(removed_dir, f"{workshop_id}_{timestamp}")
+ shutil.move(target_path, removed_path)
+ log(f"workshop_id={workshop_id} removed_path={removed_path}", 'Completed')
+ else:
+ log(f"workshop_id={workshop_id} target_path_missing={target_path}", 'Completed')
+ else:
+ fail(f"Unknown workshop action: {action}")
+except RuntimeError:
+ sys.exit(1)
PY
-)"
-
-ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH"
-import json,sys
-with open(sys.argv[1], "r", encoding="utf-8") as f:
- data=json.load(f)
-items=data.get("items",[])
-print(",".join(str(x) for x in items if str(x).isdigit()))
-PY
-)"
-
-{
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows action=${ACTION} manifest=${MANIFEST_PATH}"
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows items=${ITEMS}"
-} >> "$LOG_FILE"
-
-case "$ACTION" in
- install|update)
- # TODO: Replace with game-specific SteamCMD workshop install/update logic for Cygwin environments.
- ;;
- remove)
- # Phase 1 safety behavior: avoid destructive delete.
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE"
- ;;
- *)
- echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE"
- exit 1
- ;;
-esac
-
-exit 0
diff --git a/Panel/modules/addonsmanager/server_content_actions.php b/Panel/modules/addonsmanager/server_content_actions.php
index 6e131f28..b78308cf 100644
--- a/Panel/modules/addonsmanager/server_content_actions.php
+++ b/Panel/modules/addonsmanager/server_content_actions.php
@@ -176,7 +176,15 @@ function server_content_execute_manifest($home_id, $manifest_path, $script_key,
if ($remote->status_chk() !== 1) {
return server_content_result('failed', 'Agent is offline.', array('remote_server_id' => (int)$home_info['remote_server_id']));
}
- if ((int)$remote->rfile_exists($script_path) !== 1) {
+ if ($script_key === 'workshop') {
+ $prepare_error = '';
+ $prepared_path = scm_prepare_workshop_script_for_agent($remote, $home_info, $server_xml, $prepare_error);
+ if ($prepared_path === false) {
+ return server_content_result('failed', $prepare_error, array('script_key' => (string)$script_key));
+ }
+ $script_path = $prepared_path;
+ }
+ elseif ((int)$remote->rfile_exists($script_path) !== 1) {
return server_content_result('failed', 'Server content script was not found on agent host.', array('script_path' => $script_path));
}
$command = "bash " . escapeshellarg($script_path) . " " . escapeshellarg((string)$manifest_path) . " ; echo __GSP_SERVER_CONTENT_EXIT:$?";
@@ -469,4 +477,3 @@ function server_content_run_scheduled_action($home_id, $action, $options = array
server_content_log_action($home_id, $action, $result['status'], $result['message'], $result['details']);
return $result;
}
-
diff --git a/Panel/modules/addonsmanager/server_content_categories.php b/Panel/modules/addonsmanager/server_content_categories.php
index 57b0af55..aee6dff8 100644
--- a/Panel/modules/addonsmanager/server_content_categories.php
+++ b/Panel/modules/addonsmanager/server_content_categories.php
@@ -40,9 +40,9 @@
function get_server_content_categories()
{
return array(
- 'file_download' => 'File Download / Archive',
+ 'file_download' => 'Downloadable Mod',
'workshop_item' => 'Steam Workshop Item',
- 'config_edit' => 'Config Edit',
+ 'config_edit' => 'Configuration Package',
'scripted_installer' => 'Scripted Installer',
);
}
diff --git a/Panel/modules/addonsmanager/server_content_helpers.php b/Panel/modules/addonsmanager/server_content_helpers.php
index 935f9c97..9d4fc234 100644
--- a/Panel/modules/addonsmanager/server_content_helpers.php
+++ b/Panel/modules/addonsmanager/server_content_helpers.php
@@ -6,10 +6,10 @@
*/
if (!defined('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT')) {
- define('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT', '/var/www/html/GSP/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh');
+ define('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT', 'generic_steam_workshop_linux.sh');
}
if (!defined('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT')) {
- define('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT', '/var/www/html/GSP/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh');
+ define('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT', 'generic_steam_workshop_windows_cygwin.sh');
}
function scm_ensure_workshop_schema($db)
@@ -182,6 +182,73 @@ 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_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_legacy_panel_workshop_script_path($script_path)
+{
+ $script_path = trim((string)$script_path);
+ if ($script_path === '') {
+ return false;
+ }
+ return (strpos($script_path, '/var/www/html/') === 0 && strpos($script_path, '/Panel/modules/addonsmanager/scripts/workshop/') !== false)
+ || (strpos($script_path, '/OGP_User_Files/modules/addonsmanager/scripts/') !== false);
+}
+
+function scm_get_agent_managed_workshop_script_path(array $home_info)
+{
+ $home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
+ $filename = scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
+ $remote_path = clean_path($home_path . '/gsp_server_content/scripts/workshop/' . $filename);
+ if (!scm_path_is_under_home($home_path, $remote_path)) {
+ return false;
+ }
+ return $remote_path;
+}
+
+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)) {
+ if ((int)$remote->rfile_exists($configured_path) === 1) {
+ return $configured_path;
+ }
+ $error = 'Configured workshop script not found on agent host: ' . $configured_path;
+ return false;
+ }
+
+ $source_path = scm_get_bundled_workshop_script_source($home_info);
+ if (!is_file($source_path)) {
+ $error = 'Bundled workshop script is missing from the panel: ' . $source_path;
+ return false;
+ }
+
+ $remote_path = scm_get_agent_managed_workshop_script_path($home_info);
+ if ($remote_path === false) {
+ $error = 'Unable to resolve an agent-managed workshop script path for this server.';
+ return false;
+ }
+
+ $script_body = @file_get_contents($source_path);
+ if ($script_body === false || $script_body === '') {
+ $error = 'Failed to read bundled workshop script: ' . $source_path;
+ return false;
+ }
+
+ $remote_dir = dirname($remote_path);
+ $remote->exec("mkdir -p " . escapeshellarg($remote_dir));
+ if ((int)$remote->remote_writefile($remote_path, $script_body) !== 1) {
+ $error = 'Failed to sync workshop script to agent host.';
+ return false;
+ }
+ $remote->exec("chmod 755 " . escapeshellarg($remote_path) . " >/dev/null 2>&1 || true");
+ return $remote_path;
+}
+
function scm_get_csrf_token()
{
if (empty($_SESSION['addonsmanager_workshop_csrf'])) {
@@ -255,9 +322,9 @@ function scm_get_cache_mode($db)
function scm_get_install_methods()
{
return array(
- 'download_zip' => 'File Download / Archive',
+ 'download_zip' => 'Downloadable Mod',
'steam_workshop' => 'Steam Workshop Item',
- 'config_edit' => 'Config Edit',
+ 'config_edit' => 'Configuration Package',
'post_script' => 'Scripted Installer',
);
}
@@ -265,10 +332,10 @@ function scm_get_install_methods()
function scm_get_install_method_help_text()
{
return array(
- 'download_zip' => 'Downloads an archive or file from URL; extract path is optional.',
- 'steam_workshop' => 'Installs a Steam Workshop item by Workshop ID without requiring URL.',
- 'config_edit' => 'Applies config edits to the target file/path without requiring URL.',
- 'post_script' => 'Runs an installer script/action body without requiring URL.',
+ 'download_zip' => 'Download and extract a ZIP, RAR, or archive file.',
+ 'steam_workshop' => 'Install a Steam Workshop mod using Workshop ID.',
+ 'config_edit' => 'Install configuration files, profiles, or templates.',
+ 'post_script' => 'Run a custom scripted installation process.',
);
}
@@ -305,52 +372,201 @@ function scm_get_install_method_default($value = '')
return isset($methods[$value]) ? $value : 'download_zip';
}
-function scm_validate_install_method_payload($install_method, array $payload, &$message = '')
+function scm_get_install_payload_keys()
{
- $install_method = scm_get_install_method_default($install_method);
- $required = scm_get_install_method_required_fields();
- $errors = scm_get_install_method_validation_errors();
- if (!isset($required[$install_method])) {
- $message = 'Invalid install/content type selected.';
+ return array(
+ 'url',
+ 'path',
+ 'workshop_item_id',
+ 'workshop_app_id',
+ 'target_path_template',
+ 'optional_folder_name',
+ 'post_script',
+ 'config_edit_rule',
+ 'launch_param_additions',
+ );
+}
+
+function scm_collect_install_payload(array $defaults = array(), array $request = array(), array $override_keys = array())
+{
+ $payload = array();
+ foreach (scm_get_install_payload_keys() as $key) {
+ $payload[$key] = isset($defaults[$key]) ? trim((string)$defaults[$key]) : '';
+ }
+ foreach ($override_keys as $key) {
+ if (array_key_exists($key, $request)) {
+ $payload[$key] = trim((string)$request[$key]);
+ }
+ }
+ return $payload;
+}
+
+function scm_validate_numeric_content_value($value, $error_message, &$message, $allow_blank = false)
+{
+ $value = trim((string)$value);
+ if ($value === '') {
+ if ($allow_blank) {
+ $message = '';
+ return true;
+ }
+ $message = $error_message;
return false;
}
- if ($install_method === 'config_edit') {
- $path = isset($payload['path']) ? trim((string)$payload['path']) : '';
- $rule = isset($payload['config_edit_rule']) ? trim((string)$payload['config_edit_rule']) : '';
- if ($path === '' || $rule === '') {
- $message = $errors['config_edit'];
- return false;
- }
- $message = '';
- return true;
- }
- if ($install_method === 'post_script') {
- $script = isset($payload['post_script']) ? trim((string)$payload['post_script']) : '';
- if ($script === '') {
- $message = $errors['post_script'];
- return false;
- }
- $message = '';
- return true;
- }
- foreach ($required[$install_method] as $field) {
- $value = isset($payload[$field]) ? trim((string)$payload[$field]) : '';
- if ($value === '') {
- $message = isset($errors[$install_method]) ? $errors[$install_method] : 'Missing required field.';
- return false;
- }
- }
- if ($install_method === 'steam_workshop') {
- $wid = isset($payload['workshop_item_id']) ? trim((string)$payload['workshop_item_id']) : '';
- if ($wid === '' || !preg_match('/^[0-9]+$/', $wid)) {
- $message = 'Please enter a Workshop ID.';
- return false;
- }
+ if (!preg_match('/^[0-9]+$/', $value)) {
+ $message = $error_message;
+ return false;
}
$message = '';
return true;
}
+function scm_validate_download_content(array $payload, &$message = '')
+{
+ $url = isset($payload['url']) ? trim((string)$payload['url']) : '';
+ if ($url === '') {
+ $message = 'Please enter a download URL.';
+ return false;
+ }
+ $message = '';
+ return true;
+}
+
+function scm_validate_workshop_content(array $payload, &$message = '')
+{
+ if (!scm_validate_numeric_content_value(isset($payload['workshop_item_id']) ? $payload['workshop_item_id'] : '', 'Please enter a Workshop ID.', $message, false)) {
+ return false;
+ }
+ if (!scm_validate_numeric_content_value(isset($payload['workshop_app_id']) ? $payload['workshop_app_id'] : '', 'Workshop App ID must be numeric.', $message, true)) {
+ return false;
+ }
+ $folder_name = isset($payload['optional_folder_name']) ? trim((string)$payload['optional_folder_name']) : '';
+ if ($folder_name !== '' && (strpos($folder_name, '..') !== false || preg_match('/[\\\\\\/]/', $folder_name))) {
+ $message = 'Optional folder name must be a single folder name.';
+ return false;
+ }
+ $message = '';
+ return true;
+}
+
+function scm_validate_scripted_installer(array $payload, &$message = '')
+{
+ $script = isset($payload['post_script']) ? trim((string)$payload['post_script']) : '';
+ if ($script === '') {
+ $message = 'Please enter the installer script/action.';
+ return false;
+ }
+ $message = '';
+ return true;
+}
+
+function scm_validate_configuration_package(array $payload, &$message = '')
+{
+ $path = isset($payload['path']) ? trim((string)$payload['path']) : '';
+ $rule = isset($payload['config_edit_rule']) ? trim((string)$payload['config_edit_rule']) : '';
+ if ($path === '' || $rule === '') {
+ $message = 'Please enter the config target and edit action.';
+ return false;
+ }
+ $message = '';
+ return true;
+}
+
+function scm_validate_install_method_payload($install_method, array $payload, &$message = '')
+{
+ $install_method = scm_get_install_method_default($install_method);
+ if (!isset(scm_get_install_method_required_fields()[$install_method])) {
+ $message = 'Invalid install/content type selected.';
+ return false;
+ }
+
+ if ($install_method === 'download_zip') {
+ return scm_validate_download_content($payload, $message);
+ }
+ if ($install_method === 'steam_workshop') {
+ return scm_validate_workshop_content($payload, $message);
+ }
+ if ($install_method === 'post_script') {
+ return scm_validate_scripted_installer($payload, $message);
+ }
+ if ($install_method === 'config_edit') {
+ return scm_validate_configuration_package($payload, $message);
+ }
+ $message = '';
+ return true;
+}
+
+function scm_build_workshop_runtime_context($db, array $home_info, $server_xml, array $payload, &$message = '')
+{
+ if (!scm_validate_workshop_content($payload, $message)) {
+ return false;
+ }
+
+ $workshop_item_id = trim((string)$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']);
+ $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(
+ 'optional_folder_name' => $optional_folder_name,
+ 'workshop_app_id' => $workshop_app_id_override,
+ ))
+ : array('ok' => false);
+
+ if (!empty($resolved['ok'])) {
+ $profile = (isset($resolved['profile']) && is_array($resolved['profile'])) ? $resolved['profile'] : array();
+ $message = '';
+ return array(
+ 'workshop_item_id' => $workshop_item_id,
+ 'workshop_app_id' => isset($resolved['workshop_app_id']) ? (string)$resolved['workshop_app_id'] : '',
+ 'steam_app_id' => isset($resolved['steam_app_id']) ? (string)$resolved['steam_app_id'] : '',
+ 'folder_name' => isset($resolved['folder_name']) ? (string)$resolved['folder_name'] : ($optional_folder_name !== '' ? $optional_folder_name : '@' . $workshop_item_id),
+ 'target_path_template' => isset($resolved['target_path_template']) ? (string)$resolved['target_path_template'] : $target_path_template,
+ 'target_path_resolved' => isset($resolved['target_path_resolved']) ? (string)$resolved['target_path_resolved'] : '',
+ 'server_root' => rtrim((string)$home_info['home_path'], '/'),
+ 'steamcmd_path' => isset($profile['steamcmd_path']) ? trim((string)$profile['steamcmd_path']) : '',
+ 'workshop_download_dir' => (isset($profile['workshop_download_dir_template']) && trim((string)$profile['workshop_download_dir_template']) !== '')
+ ? sw_apply_template((string)$profile['workshop_download_dir_template'], (array)$resolved['vars'])
+ : '',
+ );
+ }
+
+ $fallback_workshop_app_id = $workshop_app_id_override;
+ if ($fallback_workshop_app_id === '' && is_array($fallback_profile) && !empty($fallback_profile['workshop_app_id'])) {
+ $fallback_workshop_app_id = (string)$fallback_profile['workshop_app_id'];
+ }
+ if ($fallback_workshop_app_id === '') {
+ $fallback_workshop_app_id = scm_extract_workshop_app_id($server_xml);
+ }
+ $steam_app_id = (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '';
+ $folder_name = ($optional_folder_name !== '') ? $optional_folder_name : '@' . $workshop_item_id;
+ $effective_template = $target_path_template;
+ 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}';
+ }
+ $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,
+ 'WORKSHOP_APP_ID' => $fallback_workshop_app_id,
+ 'STEAM_APP_ID' => $steam_app_id,
+ 'FOLDER_NAME' => $folder_name,
+ 'MOD_FOLDER' => $folder_name,
+ ));
+ $message = '';
+ return array(
+ 'workshop_item_id' => $workshop_item_id,
+ 'workshop_app_id' => $fallback_workshop_app_id,
+ 'steam_app_id' => $steam_app_id,
+ 'folder_name' => $folder_name,
+ 'target_path_template' => $effective_template,
+ 'target_path_resolved' => scm_apply_placeholders($effective_template, $placeholder_map),
+ 'server_root' => rtrim((string)$home_info['home_path'], '/'),
+ 'steamcmd_path' => (is_array($fallback_profile) && !empty($fallback_profile['steamcmd_path'])) ? (string)$fallback_profile['steamcmd_path'] : '',
+ 'workshop_download_dir' => '',
+ );
+}
+
function scm_build_placeholder_map(array $home_info, array $server_context = array(), array $overrides = array())
{
$home_id = (int)(isset($home_info['home_id']) ? $home_info['home_id'] : 0);
@@ -370,6 +586,8 @@ function scm_build_placeholder_map(array $home_info, array $server_context = arr
'{WORKSHOP_ID}' => '',
'{WORKSHOP_APP_ID}' => '',
'{STEAM_APP_ID}' => '',
+ '{FOLDER_NAME}' => '',
+ '{MOD_FOLDER}' => '',
);
foreach ($overrides as $key => $value) {
$token = '{' . strtoupper(trim((string)$key, '{}')) . '}';
diff --git a/Panel/modules/addonsmanager/workshop_action.php b/Panel/modules/addonsmanager/workshop_action.php
index 1e4c4c1c..c0b94b7e 100644
--- a/Panel/modules/addonsmanager/workshop_action.php
+++ b/Panel/modules/addonsmanager/workshop_action.php
@@ -81,13 +81,6 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
return false;
}
- $script_path = scm_get_workshop_script_path($home_info, $server_xml);
- $script_path = trim((string)$script_path);
- if ($script_path === '' || !preg_match('/^[^\\r\\n\\0]+$/', $script_path)) {
- $error = 'Workshop script path is invalid.';
- return false;
- }
-
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
if (!scm_path_is_under_home($home_path, $manifest_path)) {
$error = 'Manifest path is outside of the server home.';
@@ -99,8 +92,10 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
'action' => (string)$action,
'home_id' => (int)$home_info['home_id'],
'home_cfg_id' => (int)$home_info['home_cfg_id'],
- 'workshop_app_id' => scm_extract_workshop_app_id($server_xml),
+ 'workshop_app_id' => (!empty($extra_manifest['workshop_app_id']) ? (string)$extra_manifest['workshop_app_id'] : scm_extract_workshop_app_id($server_xml)),
+ 'steam_app_id' => !empty($extra_manifest['steam_app_id']) ? (string)$extra_manifest['steam_app_id'] : '',
'items' => array_values($item_ids),
+ 'generated_at' => date('Y-m-d H:i:s'),
);
if (!empty($extra_manifest)) {
$manifest['extra'] = $extra_manifest;
@@ -123,8 +118,8 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
$error = 'Failed to write workshop manifest to remote server.';
return false;
}
- if ((int)$remote->rfile_exists($script_path) !== 1) {
- $error = 'Configured workshop script not found on agent host: ' . $script_path;
+ $script_path = scm_prepare_workshop_script_for_agent($remote, $home_info, $server_xml, $error);
+ if ($script_path === false) {
return false;
}
diff --git a/Panel/ogp_api.php b/Panel/ogp_api.php
index 6d27d5cb..eaa2a278 100644
--- a/Panel/ogp_api.php
+++ b/Panel/ogp_api.php
@@ -1432,14 +1432,7 @@ function api_addonsmanager()
$addon_info = $addons_rows[0];
$install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip');
- $validation_payload = array(
- 'url' => isset($addon_info['url']) ? $addon_info['url'] : '',
- 'path' => isset($addon_info['path']) ? $addon_info['path'] : '',
- 'workshop_item_id' => isset($addon_info['workshop_item_id']) ? $addon_info['workshop_item_id'] : '',
- 'target_path_template' => isset($addon_info['target_path_template']) ? $addon_info['target_path_template'] : '',
- 'post_script' => isset($addon_info['post_script']) ? $addon_info['post_script'] : '',
- 'config_edit_rule' => isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : '',
- );
+ $validation_payload = scm_collect_install_payload($addon_info);
$validation_message = '';
if (!scm_validate_install_method_payload($install_method, $validation_payload, $validation_message))
return array("status" => '422', "message" => $validation_message);
@@ -1480,14 +1473,24 @@ function api_addonsmanager()
if ($install_method === 'steam_workshop') {
scm_ensure_workshop_schema($db);
- $workshop_item_id = trim((string)$addon_info['workshop_item_id']);
+ $workshop_runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $validation_payload, $validation_message);
+ if ($workshop_runtime === false)
+ return array("status" => '422', "message" => $validation_message);
+ $workshop_item_id = (string)$workshop_runtime['workshop_item_id'];
$workshop_error = '';
$extra_manifest = array(
'addon_id' => (int)$addon_id,
- 'target_path_template' => isset($addon_info['target_path_template']) ? (string)$addon_info['target_path_template'] : '',
- 'optional_folder_name' => isset($addon_info['optional_folder_name']) ? (string)$addon_info['optional_folder_name'] : '',
+ 'target_path_template' => (string)$workshop_runtime['target_path_template'],
+ 'target_path_resolved' => (string)$workshop_runtime['target_path_resolved'],
+ 'optional_folder_name' => isset($validation_payload['optional_folder_name']) ? (string)$validation_payload['optional_folder_name'] : '',
'config_edit_rule' => isset($addon_info['config_edit_rule']) ? (string)$addon_info['config_edit_rule'] : '',
'launch_param_additions' => isset($addon_info['launch_param_additions']) ? (string)$addon_info['launch_param_additions'] : '',
+ 'workshop_app_id' => (string)$workshop_runtime['workshop_app_id'],
+ 'steam_app_id' => (string)$workshop_runtime['steam_app_id'],
+ 'steamcmd_path' => isset($workshop_runtime['steamcmd_path']) ? (string)$workshop_runtime['steamcmd_path'] : '',
+ 'workshop_download_dir' => isset($workshop_runtime['workshop_download_dir']) ? (string)$workshop_runtime['workshop_download_dir'] : '',
+ 'server_root' => isset($workshop_runtime['server_root']) ? (string)$workshop_runtime['server_root'] : rtrim((string)$home_info['home_path'], '/'),
+ 'post_install_script' => trim((string)$post_script),
);
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest);
if ($ok)