woekshop phase 1

This commit is contained in:
Frank Harris 2026-06-09 06:13:44 -05:00
parent 17a31b7f5f
commit 5a03946bdf
15 changed files with 409 additions and 357 deletions

View file

@ -184,7 +184,7 @@ function exec_ogp_module() {
$content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : '';
$requires_stop = !empty($addon_info['requires_stop']) ? 1 : 0;
$user_override_keys = ($install_method === 'steam_workshop')
? array('workshop_item_id', 'workshop_app_id', 'target_path_template', 'optional_folder_name')
? array('workshop_item_id')
: array();
$install_payload = scm_collect_install_payload($addon_info, $_REQUEST, $user_override_keys);
$post_script = '';
@ -276,18 +276,23 @@ function exec_ogp_module() {
$cache_mode
);
$_SESSION['scm_history_id_' . $home_id . '_' . $addon_id] = $history_id;
scm_log_content_install_action(array(
'addon_id' => (int)$addon_id,
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'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',
));
$log_target_path = (string)$install_payload['path'];
if ($install_method === 'steam_workshop') {
$log_target_path = scm_extract_workshop_install_path($server_xml);
if ($log_target_path === '') {
$log_target_path = scm_get_default_workshop_target_template(scm_detect_workshop_install_strategy($home_info, $server_xml));
}
}
scm_log_content_install_action(array(
'addon_id' => (int)$addon_id,
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_id' => isset($install_payload['workshop_item_id']) ? (string)$install_payload['workshop_item_id'] : '',
'target_path' => $log_target_path,
'action' => 'started',
));
if ($install_method === 'steam_workshop') {
scm_ensure_workshop_schema($db);
$workshop_runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $install_payload, $validation_message);
@ -305,15 +310,14 @@ function exec_ogp_module() {
'addon_id' => (int)$addon_id,
'target_path_template' => $target_path_template,
'target_path_resolved' => $target_path_resolved,
'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']),
'launch_param_additions' => isset($server_xml->workshop_support->startup_param_format) ? trim((string)$server_xml->workshop_support->startup_param_format) : '',
'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),
'post_install_script' => scm_workshop_post_install_action($server_xml),
);
$workshop_error = '';
$workshop_ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest);
@ -581,14 +585,11 @@ function exec_ogp_module() {
$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']) : '';
$default_workshop_app_id = scm_extract_workshop_app_id($server_xml);
$default_target_template = scm_extract_workshop_install_path($server_xml);
if ($default_target_template === '') {
$default_target_template = scm_get_default_workshop_target_template(scm_detect_workshop_install_strategy($home_info, $server_xml));
}
?>
<h2><?php echo htmlentities($home_info['home_name'])."&nbsp;".$addon_type_lang ;?></h2>
<table class='center'>
@ -612,9 +613,6 @@ function exec_ogp_module() {
value="<?php echo (int)$addon['addon_id']; ?>"
data-install-method="<?php echo htmlspecialchars(scm_get_install_method_default(isset($addon['install_method']) ? $addon['install_method'] : ''), ENT_QUOTES, 'UTF-8'); ?>"
data-workshop-item-id="<?php echo htmlspecialchars(isset($addon['workshop_item_id']) ? $addon['workshop_item_id'] : '', ENT_QUOTES, 'UTF-8'); ?>"
data-workshop-app-id="<?php echo htmlspecialchars(isset($addon['workshop_app_id']) ? $addon['workshop_app_id'] : '', ENT_QUOTES, 'UTF-8'); ?>"
data-target-path-template="<?php echo htmlspecialchars(isset($addon['target_path_template']) ? $addon['target_path_template'] : '', ENT_QUOTES, 'UTF-8'); ?>"
data-optional-folder-name="<?php echo htmlspecialchars(isset($addon['optional_folder_name']) ? $addon['optional_folder_name'] : '', ENT_QUOTES, 'UTF-8'); ?>"
><?php echo htmlspecialchars($addon['name']); ?></option>
<?php } ?>
</select>
@ -623,22 +621,15 @@ function exec_ogp_module() {
<input type="text" id="scm-user-workshop-id" name="workshop_item_id" size="50" value="<?php echo htmlspecialchars(isset($selected_addon['workshop_item_id']) ? $selected_addon['workshop_item_id'] : '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Example Arma 3 Workshop ID: 450814997" />
<div class="info" style="margin-top:4px;">Install a Steam Workshop mod using Workshop ID. URL is not required.</div>
</td></tr>
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong>Workshop App ID Override</strong></td><td align='left'>
<input type="text" id="scm-user-workshop-app-id" name="workshop_app_id" size="50" value="<?php echo htmlspecialchars($default_workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>" data-default-app-id="<?php echo htmlspecialchars($default_workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>" placeholder="Optional App ID override" />
</td></tr>
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong><?php print_lang('optional_folder_name'); ?></strong></td><td align='left'>
<input type="text" id="scm-user-optional-folder-name" name="optional_folder_name" size="50" value="<?php echo htmlspecialchars($default_optional_folder_name, ENT_QUOTES, 'UTF-8'); ?>" placeholder="@myWorkshopMod (optional)" />
</td></tr>
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong><?php print_lang('target_path_template'); ?></strong></td><td align='left'>
<input type="text" id="scm-user-target-path-template" name="target_path_template" size="85" value="<?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?>" data-default-template="<?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?>" placeholder="{SERVER_ROOT}/{MOD_FOLDER}" />
<div class="info" style="margin-top:4px;">Supported placeholders: {SERVER_ROOT}, {GAME_ROOT}, {WORKSHOP_ID}, {WORKSHOP_APP_ID}, {STEAM_APP_ID}, {FOLDER_NAME}, {MOD_FOLDER}</div>
</td></tr>
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong>Target Path Preview</strong></td><td align='left'>
<code id="scm-user-target-path-preview"
data-server-root="<?php echo htmlspecialchars(rtrim((string)$home_info['home_path'], '/'), ENT_QUOTES, 'UTF-8'); ?>"
data-game-root="<?php echo htmlspecialchars(rtrim((string)$home_info['home_path'], '/'), ENT_QUOTES, 'UTF-8'); ?>"
data-steam-app-id="<?php echo htmlspecialchars((is_array($workshop_profile) && !empty($workshop_profile['steam_app_id'])) ? (string)$workshop_profile['steam_app_id'] : '', ENT_QUOTES, 'UTF-8'); ?>"
data-steam-app-id="<?php echo htmlspecialchars(scm_extract_workshop_steam_app_id($server_xml), ENT_QUOTES, 'UTF-8'); ?>"
data-workshop-app-id="<?php echo htmlspecialchars($default_workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>"
data-target-template="<?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?>"
><?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?></code>
<div class="info" style="margin-top:4px;">Workshop App ID, install path, and launch parameter format are defined in the game XML.</div>
</td></tr>
<tr><td colspan='2' class='info'>&nbsp;</td></tr>
<td align='left'>

View file

@ -14,7 +14,7 @@
* 3. A script-driven installer (post_script only, no download required).
* 4. A Minecraft server jar / version switcher (future: install_method=minecraft_jar).
* 5. A DayZ/Epoch/Arma profile copy (future: install_method=profile_copy).
* 6. A Steam Workshop content bundle (future: install_method=steam_workshop).
* 6. A Steam Workshop entry point (install behavior comes from game XML).
* 7. A config preset (type=config).
* 8. A full server profile built from multiple actions (type=profile).
*
@ -26,6 +26,7 @@
// Central category map — defines all valid addon_type values and their labels.
require_once(dirname(__FILE__) . '/server_content_categories.php');
require_once(dirname(__FILE__) . '/server_content_helpers.php');
require_once("modules/config_games/server_config_parser.php");
function exec_ogp_module() {
@ -59,13 +60,19 @@ function exec_ogp_module() {
$fields['restart_after_install'] = !empty($_POST['restart_after_install']) ? 1 : 0;
$fields['is_cacheable'] = !empty($_POST['is_cacheable']) ? 1 : 0;
$fields['description'] = isset($_POST['description']) ? $_POST['description'] : '';
$fields['workshop_item_id'] = isset($_POST['workshop_item_id']) ? trim((string)$_POST['workshop_item_id']) : '';
$fields['workshop_app_id'] = isset($_POST['workshop_app_id']) ? trim((string)$_POST['workshop_app_id']) : '';
$fields['target_path_template']= isset($_POST['target_path_template']) ? trim((string)$_POST['target_path_template']) : '';
$fields['optional_folder_name']= isset($_POST['optional_folder_name']) ? trim((string)$_POST['optional_folder_name']) : '';
$fields['workshop_item_id'] = '';
$fields['workshop_app_id'] = '';
$fields['target_path_template']= '';
$fields['optional_folder_name']= '';
$fields['config_edit_rule'] = isset($_POST['config_edit_rule']) ? trim((string)$_POST['config_edit_rule']) : '';
$fields['launch_param_additions'] = isset($_POST['launch_param_additions']) ? trim((string)$_POST['launch_param_additions']) : '';
$fields['launch_param_additions'] = '';
$fields['addon_type'] = scm_get_addon_type_from_install_method($fields['install_method']);
if ($fields['install_method'] === 'steam_workshop') {
$fields['url'] = '';
$fields['path'] = '';
$fields['post_script'] = '';
$fields['config_edit_rule'] = '';
}
if ($fields['name'] === '')
{
@ -75,6 +82,10 @@ function exec_ogp_module() {
{
print_failure(get_lang("select_a_game_type"));
}
elseif ($fields['install_method'] === 'steam_workshop' && (!($game_cfg = $db->getGameCfg($fields['home_cfg_id'])) || !($server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $game_cfg['home_cfg_file'])) || !scm_workshop_is_supported($server_xml)))
{
print_failure('Steam Workshop support must be configured in the selected game XML before creating a Workshop content entry.');
}
else
{
$validation_payload = array(
@ -115,12 +126,7 @@ function exec_ogp_module() {
$restart_after_install = isset($_POST['restart_after_install']) ? (int)$_POST['restart_after_install'] : 0;
$is_cacheable = isset($_POST['is_cacheable']) ? (int)$_POST['is_cacheable'] : 0;
$description = isset($_POST['description']) ? $_POST['description'] : "";
$workshop_item_id = isset($_POST['workshop_item_id']) ? $_POST['workshop_item_id'] : "";
$workshop_app_id = isset($_POST['workshop_app_id']) ? $_POST['workshop_app_id'] : "";
$target_path_template = isset($_POST['target_path_template']) ? $_POST['target_path_template'] : "";
$optional_folder_name = isset($_POST['optional_folder_name']) ? $_POST['optional_folder_name'] : "";
$config_edit_rule = isset($_POST['config_edit_rule']) ? $_POST['config_edit_rule'] : "";
$launch_param_additions = isset($_POST['launch_param_additions']) ? $_POST['launch_param_additions'] : "";
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit']))
{
@ -143,12 +149,7 @@ function exec_ogp_module() {
$restart_after_install = isset($addon_info['restart_after_install']) ? (int)$addon_info['restart_after_install'] : 0;
$is_cacheable = isset($addon_info['is_cacheable']) ? (int)$addon_info['is_cacheable'] : 0;
$description = isset($addon_info['description']) ? $addon_info['description'] : "";
$workshop_item_id = isset($addon_info['workshop_item_id']) ? $addon_info['workshop_item_id'] : "";
$workshop_app_id = isset($addon_info['workshop_app_id']) ? $addon_info['workshop_app_id'] : "";
$target_path_template = isset($addon_info['target_path_template']) ? $addon_info['target_path_template'] : "";
$optional_folder_name = isset($addon_info['optional_folder_name']) ? $addon_info['optional_folder_name'] : "";
$config_edit_rule = isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : "";
$launch_param_additions = isset($addon_info['launch_param_additions']) ? $addon_info['launch_param_additions'] : "";
}
?>
<form action="" method="post">
@ -197,38 +198,12 @@ function exec_ogp_module() {
<input type="text" value="<?php echo $path; ?>" name="path" size="85" title="<?php print_lang('path_info'); ?>" />
</td>
</tr>
<tr id="scm-row-workshop-id">
<tr id="scm-row-workshop-xml-info">
<td align="right">
<b>Default Workshop IDs (Optional)</b>
<b>Steam Workshop</b>
</td>
<td align="left">
<input type="text" value="<?php echo htmlspecialchars($workshop_item_id, ENT_QUOTES, 'UTF-8'); ?>" name="workshop_item_id" size="85" placeholder="Leave blank users enter Workshop IDs on their server page" />
<small style="color:#666;">Optional. Users enter the actual Workshop IDs they want installed from their own server page. This field is not required.</small>
</td>
</tr>
<tr id="scm-row-workshop-app-id">
<td align="right">
<b>Game Compatibility (Workshop App ID)</b>
</td>
<td align="left">
<input type="text" value="<?php echo htmlspecialchars($workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>" name="workshop_app_id" size="85" placeholder="Optional App ID override, e.g. 221100" />
</td>
</tr>
<tr id="scm-row-target-path-template">
<td align="right">
<b><?php print_lang('target_path_template'); ?></b>
</td>
<td align="left">
<input type="text" value="<?php echo htmlspecialchars($target_path_template, ENT_QUOTES, 'UTF-8'); ?>" name="target_path_template" size="85" placeholder="{SERVER_ROOT}/{MOD_FOLDER}" />
<small style="color:#666;">Supported placeholders: {HOME_ID}, {SERVER_ROOT}, {GAME_ROOT}, {WORKSHOP_ID}, {WORKSHOP_APP_ID}, {STEAM_APP_ID}, {FOLDER_NAME}, {MOD_FOLDER}</small>
</td>
</tr>
<tr id="scm-row-optional-folder-name">
<td align="right">
<b><?php print_lang('optional_folder_name'); ?></b>
</td>
<td align="left">
<input type="text" value="<?php echo htmlspecialchars($optional_folder_name, ENT_QUOTES, 'UTF-8'); ?>" name="optional_folder_name" size="85" placeholder="@MyWorkshopMod" />
<div class="info">Steam Workshop behavior is configured by the selected game's XML <code>workshop_support</code> block. Users install Workshop items from their server management page.</div>
</td>
</tr>
<tr id="scm-row-post-script">
@ -256,14 +231,6 @@ function exec_ogp_module() {
<textarea name="config_edit_rule" style="width:99%;height:90px;" placeholder="Text/rules to append or apply to the target config."><?php echo htmlspecialchars($config_edit_rule, ENT_QUOTES, 'UTF-8'); ?></textarea>
</td>
</tr>
<tr id="scm-row-launch-param-additions">
<td align="right">
<b><?php print_lang('launch_param_additions'); ?></b>
</td>
<td align="left">
<input type="text" value="<?php echo htmlspecialchars($launch_param_additions, ENT_QUOTES, 'UTF-8'); ?>" name="launch_param_additions" size="85" placeholder="-mod=@myMod;@anotherMod" />
</td>
</tr>
<tr>
<td align="right">
<b><?php print_lang('select_game_type'); ?></b>

View file

@ -153,9 +153,6 @@ function server_content_resolve_script_path(array $home_info, $script_key, array
if ($server_xml === false) {
return array(false, false);
}
if ($script_path === '' && $script_key === 'workshop') {
$script_path = scm_get_workshop_script_path($home_info, $server_xml);
}
if ($script_path === '' && $script_key !== '' && isset($server_xml->$script_key)) {
$script_path = trim((string)$server_xml->$script_key);
}
@ -172,10 +169,6 @@ function server_content_execute_manifest($home_id, $manifest_path, $script_key,
if ($server_xml === false) {
return server_content_result('failed', 'Unable to load server XML configuration.', array('home_id' => (int)$home_id));
}
$script_path = trim((string)$script_path);
if ($script_path === '' || !preg_match('/^[^\r\n\0]+$/', $script_path)) {
return server_content_result('failed', 'Configured server content script path is invalid.', array('script_key' => (string)$script_key));
}
$remote = server_content_create_remote($home_info);
if ($remote->status_chk() !== 1) {
return server_content_result('failed', 'Agent is offline.', array('remote_server_id' => (int)$home_info['remote_server_id']));
@ -188,8 +181,14 @@ function server_content_execute_manifest($home_id, $manifest_path, $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));
else {
$script_path = trim((string)$script_path);
if ($script_path === '' || !preg_match('/^[^\r\n\0]+$/', $script_path)) {
return server_content_result('failed', 'Configured server content script path is invalid.', array('script_key' => (string)$script_key));
}
if ((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:$?";
$output = $remote->exec($command);
@ -262,7 +261,7 @@ function server_content_install_updates($home_id, $options = array())
}
$manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $workshop_ids, array());
if (empty($manifest_context['workshop_app_id'])) {
return server_content_result('failed', 'Workshop App ID is missing. Configure it in game XML or the Server Content template.');
return server_content_result('failed', 'Workshop App ID is missing from the game XML workshop_support block.');
}
if (empty($options['check_only'])) {
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, 'installing', null, false, false);

View file

@ -105,12 +105,30 @@ function scm_ensure_workshop_schema($db)
`tags` TEXT NULL,
`game_key` VARCHAR(128) NULL,
`local_cache_path` VARCHAR(512) NULL,
`author` VARCHAR(255) NULL,
`thumbnail_url` VARCHAR(512) NULL,
UNIQUE KEY `uniq_workshop_app` (`workshop_id`, `app_id`),
KEY `idx_app_id` (`app_id`),
KEY `idx_install_count` (`install_count`),
KEY `idx_last_installed` (`last_installed`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
$catalog_table = $db->realEscapeSingle(OGP_DB_PREFIX . 'server_content_workshop_catalog');
foreach (array(
'author' => "VARCHAR(255) NULL AFTER `title`",
'thumbnail_url' => "VARCHAR(512) NULL AFTER `author`",
) as $col => $definition) {
$escaped_col = $db->realEscapeSingle($col);
$col_check = $db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{$catalog_table}'
AND COLUMN_NAME = '{$escaped_col}'"
);
if (empty($col_check)) {
$db->query("ALTER TABLE `".OGP_DB_PREFIX."server_content_workshop_catalog` ADD COLUMN `{$col}` {$definition}");
}
}
return $ok;
}
@ -225,20 +243,37 @@ function scm_workshop_catalog_sort_sql($sort)
'published_date' => 'published_date DESC, title ASC, workshop_id ASC',
'last_updated' => 'last_updated DESC, title ASC, workshop_id ASC',
'last_installed' => 'last_installed DESC, title ASC, workshop_id ASC',
'workshop_id' => 'CAST(workshop_id AS UNSIGNED) ASC, workshop_id ASC',
);
return isset($allowed[$sort]) ? $allowed[$sort] : $allowed['last_installed'];
}
function scm_get_workshop_catalog_rows($db, $app_id = '', $sort = 'last_installed', $limit = 50)
function scm_get_workshop_catalog_rows($db, $app_id = '', $sort = 'last_installed', $limit = 50, $query = '', $tag = '')
{
if (!scm_ensure_workshop_schema($db)) {
return array();
}
$where = '';
$where_parts = array();
$app_id = trim((string)$app_id);
if ($app_id !== '' && preg_match('/^[0-9]+$/', $app_id)) {
$where = "WHERE app_id='" . $db->realEscapeSingle($app_id) . "'";
$where_parts[] = "app_id='" . $db->realEscapeSingle($app_id) . "'";
}
$query = trim((string)$query);
if ($query !== '') {
$query_id = scm_extract_workshop_item_id($query);
if ($query_id !== '') {
$where_parts[] = "workshop_id='" . $db->realEscapeSingle($query_id) . "'";
} else {
$like = "%" . $db->realEscapeSingle($query) . "%";
$where_parts[] = "(title LIKE '{$like}' OR author LIKE '{$like}' OR tags LIKE '{$like}' OR game_key LIKE '{$like}')";
}
}
$tag = trim((string)$tag);
if ($tag !== '') {
$like = "%" . $db->realEscapeSingle($tag) . "%";
$where_parts[] = "tags LIKE '{$like}'";
}
$where = empty($where_parts) ? '' : ('WHERE ' . implode(' AND ', $where_parts));
$limit = (int)$limit;
if ($limit <= 0 || $limit > 200) {
$limit = 50;
@ -266,14 +301,18 @@ function scm_workshop_record_catalog_items($db, $workshop_app_id, array $item_id
}
$detail = isset($item_details[$item_id]) && is_array($item_details[$item_id]) ? $item_details[$item_id] : array();
$title = isset($detail['title']) ? (string)$detail['title'] : '';
$author = isset($detail['author']) ? (string)$detail['author'] : '';
$thumbnail = isset($detail['thumbnail_url']) ? (string)$detail['thumbnail_url'] : '';
$install_path = isset($detail['target_path_resolved']) ? (string)$detail['target_path_resolved'] : '';
$db->query(
"INSERT INTO `".OGP_DB_PREFIX."server_content_workshop_catalog`
(workshop_id, app_id, title, install_count, first_seen, last_installed, last_updated, game_key, local_cache_path)
(workshop_id, app_id, title, author, thumbnail_url, install_count, first_seen, last_installed, last_updated, game_key, local_cache_path)
VALUES (
'".$db->realEscapeSingle($item_id)."',
'".$db->realEscapeSingle($workshop_app_id)."',
".($title === '' ? "NULL" : "'".$db->realEscapeSingle($title)."'").",
".($author === '' ? "NULL" : "'".$db->realEscapeSingle($author)."'").",
".($thumbnail === '' ? "NULL" : "'".$db->realEscapeSingle($thumbnail)."'").",
1,
NOW(),
NOW(),
@ -284,6 +323,8 @@ function scm_workshop_record_catalog_items($db, $workshop_app_id, array $item_id
ON DUPLICATE KEY UPDATE
install_count=install_count+1,
title=IF(VALUES(title) IS NULL OR VALUES(title)='', title, VALUES(title)),
author=IF(VALUES(author) IS NULL OR VALUES(author)='', author, VALUES(author)),
thumbnail_url=IF(VALUES(thumbnail_url) IS NULL OR VALUES(thumbnail_url)='', thumbnail_url, VALUES(thumbnail_url)),
last_installed=NOW(),
last_updated=".($mark_update ? "NOW()" : "last_updated").",
game_key=IF(VALUES(game_key) IS NULL OR VALUES(game_key)='', game_key, VALUES(game_key)),
@ -390,23 +431,20 @@ function scm_extract_workshop_app_id($server_xml)
return $value;
}
}
$candidates = array(
'workshop_app_id',
'workshop_appid',
'steam_workshop_app_id',
'steam_workshop_appid',
);
foreach ((array)$candidates as $candidate) {
if (isset($server_xml->$candidate)) {
$value = trim((string)$server_xml->$candidate);
if ($value !== '' && preg_match('/^[0-9]+$/', $value)) {
return $value;
}
}
}
return "";
}
function scm_workshop_is_supported($server_xml)
{
if (!isset($server_xml->workshop_support)) {
return false;
}
if (isset($server_xml->workshop_support->enabled) && !scm_workshop_xml_bool((string)$server_xml->workshop_support->enabled, true)) {
return false;
}
return scm_extract_workshop_app_id($server_xml) !== '';
}
function scm_extract_workshop_steam_app_id($server_xml)
{
if (isset($server_xml->workshop_support->steam_app_id)) {
@ -444,42 +482,15 @@ function scm_workshop_xml_bool($value, $default = false)
return (bool)$default;
}
function scm_get_workshop_script_path(array $home_info, $server_xml)
function scm_workshop_mod_prefix($server_xml)
{
$key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux';
$nested_key = scm_is_windows_home($home_info) ? 'script_windows' : 'script_linux';
if (isset($server_xml->workshop_support->$nested_key)) {
$xml_path = trim((string)$server_xml->workshop_support->$nested_key);
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
return $xml_path;
if (isset($server_xml->workshop_support->mod_prefix)) {
$prefix = trim((string)$server_xml->workshop_support->mod_prefix);
if ($prefix !== '' && strpos($prefix, '/') === false && strpos($prefix, '\\') === false && strpos($prefix, "\0") === false) {
return $prefix;
}
}
if (isset($server_xml->$key)) {
$xml_path = trim((string)$server_xml->$key);
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
return $xml_path;
}
}
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';
$nested_key = scm_is_windows_home($home_info) ? 'script_windows' : 'script_linux';
if (isset($server_xml->workshop_support->$nested_key)) {
$xml_path = trim((string)$server_xml->workshop_support->$nested_key);
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
return $xml_path;
}
}
if (isset($server_xml->$key)) {
$xml_path = trim((string)$server_xml->$key);
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
return $xml_path;
}
}
return '';
return '@';
}
function scm_get_bundled_workshop_script_source(array $home_info)
@ -488,22 +499,6 @@ function scm_get_bundled_workshop_script_source(array $home_info)
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);
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']), '/');
@ -518,16 +513,6 @@ 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 = 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)) {
scm_log_content_install_action(array(
'type' => 'workshop_script_deprecated',
'home_id' => isset($home_info['home_id']) ? (int)$home_info['home_id'] : 0,
'configured_path' => $configured_path,
'message' => 'Configured static Workshop script ignored; Server Content generates a per-job script and runs it through the generic agent exec path.',
));
}
$source_path = scm_get_bundled_workshop_script_source($home_info);
if (!is_file($source_path)) {
$error = 'Panel Workshop job template is missing: ' . $source_path;
@ -649,7 +634,7 @@ function scm_get_install_method_help_text()
{
return array(
'download_zip' => 'Download and extract a ZIP, RAR, or archive file.',
'steam_workshop' => 'Configure how users may install Steam Workshop items for this game. Users enter the actual Workshop IDs from their server page.',
'steam_workshop' => 'Users install Steam Workshop items from their server page. App IDs, install paths, key-copy rules, and launch parameter format come from the game XML.',
'config_edit' => 'Install configuration files, profiles, or templates.',
'post_script' => 'Run a custom scripted installation process.',
);
@ -669,7 +654,7 @@ function scm_get_install_method_validation_errors()
{
return array(
'download_zip' => 'Please enter a download URL.',
'steam_workshop' => 'Please configure Workshop App ID or ensure the game XML provides one.',
'steam_workshop' => 'Workshop behavior is configured in the game XML.',
'config_edit' => 'Please enter the config target and edit action.',
'post_script' => 'Please enter the installer script/action.',
);
@ -749,16 +734,6 @@ function scm_validate_download_content(array $payload, &$message = '')
function scm_validate_workshop_content(array $payload, &$message = '')
{
// workshop_item_id is NOT required for admin content templates.
// Users supply Workshop IDs on their server page (workshop_content.php).
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;
}
@ -828,64 +803,21 @@ function scm_validate_install_method_payload($install_method, array $payload, &$
function scm_build_workshop_runtime_context($db, array $home_info, $server_xml, array $payload, &$message = '')
{
// workshop_item_id is now optional in admin templates; validate only the
// numeric format constraints (workshop_app_id, optional_folder_name).
if (!scm_validate_workshop_content($payload, $message)) {
return false;
}
$workshop_item_id = trim((string)(isset($payload['workshop_item_id']) ? $payload['workshop_item_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(
'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'] : scm_extract_workshop_steam_app_id($server_xml);
$folder_name = ($optional_folder_name !== '') ? $optional_folder_name : '@' . $workshop_item_id;
$effective_template = $target_path_template;
if ($effective_template === '') {
if (is_array($fallback_profile) && !empty($fallback_profile['install_path_template'])) {
$effective_template = (string)$fallback_profile['install_path_template'];
} else {
$xml_install_path = scm_extract_workshop_install_path($server_xml);
$effective_template = $xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy);
}
}
$workshop_app_id = scm_extract_workshop_app_id($server_xml);
$steam_app_id = scm_extract_workshop_steam_app_id($server_xml);
$folder_prefix = scm_workshop_mod_prefix($server_xml);
$folder_name = $folder_prefix . $workshop_item_id;
$xml_install_path = scm_extract_workshop_install_path($server_xml);
$effective_template = $xml_install_path !== '' ? $xml_install_path : 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,
'WORKSHOP_APP_ID' => $fallback_workshop_app_id,
'WORKSHOP_APP_ID' => $workshop_app_id,
'STEAM_APP_ID' => $steam_app_id,
'FOLDER_NAME' => $folder_name,
'MOD_FOLDER' => $folder_name,
@ -893,39 +825,25 @@ function scm_build_workshop_runtime_context($db, array $home_info, $server_xml,
$message = '';
return array(
'workshop_item_id' => $workshop_item_id,
'workshop_app_id' => $fallback_workshop_app_id,
'workshop_app_id' => $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'] : '',
'steamcmd_path' => '',
'workshop_download_dir' => '',
);
}
function scm_detect_workshop_install_strategy(array $home_info, $server_xml, array $template = array())
{
if (!empty($template['install_strategy'])) {
$strategy = trim((string)$template['install_strategy']);
if (preg_match('/^[a-z0-9_\-]+$/i', $strategy)) {
return strtolower($strategy);
}
}
if (isset($server_xml->workshop_support->install_strategy)) {
$strategy = trim((string)$server_xml->workshop_support->install_strategy);
if ($strategy !== '' && preg_match('/^[a-z0-9_\-]+$/i', $strategy)) {
return strtolower($strategy);
}
}
foreach (array('workshop_install_strategy', 'install_strategy') as $tag) {
if (isset($server_xml->$tag)) {
$strategy = trim((string)$server_xml->$tag);
if ($strategy !== '' && preg_match('/^[a-z0-9_\-]+$/i', $strategy)) {
return strtolower($strategy);
}
}
}
$game_key = strtolower((string)(isset($home_info['game_key']) ? $home_info['game_key'] : ''));
$cfg_file = strtolower((string)(isset($home_info['home_cfg_file']) ? $home_info['home_cfg_file'] : ''));
$name = strtolower((string)(isset($home_info['game_name']) ? $home_info['game_name'] : ''));
@ -947,11 +865,6 @@ function scm_workshop_should_copy_keys($server_xml, $install_strategy)
return scm_workshop_xml_bool((string)$attrs['enabled'], false);
}
}
foreach (array('workshop_copy_keys', 'copy_workshop_keys') as $tag) {
if (isset($server_xml->$tag)) {
return scm_workshop_xml_bool((string)$server_xml->$tag, false);
}
}
return in_array((string)$install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true);
}
@ -968,6 +881,18 @@ function scm_workshop_keys_target_path($server_xml, array $home_info)
return scm_apply_placeholders($template, $map);
}
function scm_workshop_post_install_action($server_xml)
{
if (!isset($server_xml->workshop_support->post_install_action)) {
return '';
}
$action = trim((string)$server_xml->workshop_support->post_install_action);
if ($action === '' || strpos($action, "\0") !== false || strpos($action, "\r") !== false || strpos($action, "\n") !== false) {
return '';
}
return $action;
}
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);
@ -1006,6 +931,44 @@ function scm_apply_placeholders($template, array $placeholder_map)
return str_replace(array_keys($placeholder_map), array_values($placeholder_map), $template);
}
function scm_get_workshop_enabled_games($query = '', $tag = '')
{
$games = array();
$config_dir = defined('SERVER_CONFIG_LOCATION') ? SERVER_CONFIG_LOCATION : 'modules/config_games/server_configs/';
$schema = defined('XML_SCHEMA') ? XML_SCHEMA : 'modules/config_games/schema_server_config.xml';
$query = strtolower(trim((string)$query));
$tag = strtolower(trim((string)$tag));
foreach (glob(rtrim($config_dir, '/') . '/*.xml') ?: array() as $file) {
$xml = @simplexml_load_file($file);
if ($xml === false || !scm_workshop_is_supported($xml)) {
continue;
}
$game_key = isset($xml->game_key) ? (string)$xml->game_key : '';
$game_name = isset($xml->game_name) ? (string)$xml->game_name : basename($file, '.xml');
$app_id = scm_extract_workshop_app_id($xml);
$haystack = strtolower($game_key . ' ' . $game_name . ' ' . basename($file) . ' ' . $app_id);
if ($query !== '' && strpos($haystack, $query) === false) {
continue;
}
if ($tag !== '' && strpos($haystack, $tag) === false) {
continue;
}
$games[] = array(
'game_key' => $game_key,
'game_name' => $game_name,
'config_file' => basename($file),
'workshop_app_id' => $app_id,
'steam_app_id' => scm_extract_workshop_steam_app_id($xml),
'install_strategy' => isset($xml->workshop_support->install_strategy) ? (string)$xml->workshop_support->install_strategy : '',
'schema' => $schema,
);
}
usort($games, function ($a, $b) {
return strcasecmp($a['game_name'] . $a['config_file'], $b['game_name'] . $b['config_file']);
});
return $games;
}
function scm_content_logs_dir()
{
return dirname(__FILE__) . '/logs';

View file

@ -88,16 +88,11 @@ scm_workshop_test_assert(isset($windowsRemote->files[$script]), 'writes Windows/
scm_workshop_test_assert(strpos($windowsRemote->files[$script], '+runscript') !== false, 'Windows/Cygwin per-job script runs SteamCMD through runscript');
scm_workshop_test_assert($error === '', 'default Windows/Cygwin job staging does not report missing agent script');
$configuredXml = simplexml_load_string('<game_config><workshop_support><script_linux>/agent/custom/workshop.sh</script_linux></workshop_support></game_config>');
$configuredXml = simplexml_load_string('<game_config><workshop_support><enabled>1</enabled><workshop_app_id>107410</workshop_app_id></workshop_support></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(strpos($script, '/srv/games/arma3/gsp_server_content/jobs/workshop/workshop_job_') === 0, 'ignores configured static script and uses generated per-job script');
$missingCustomRemote = new ScmWorkshopFakeRemote();
$script = scm_prepare_workshop_script_for_agent($missingCustomRemote, $linuxHome, $configuredXml, $error);
scm_workshop_test_assert(strpos($script, '/srv/games/arma3/gsp_server_content/jobs/workshop/workshop_job_') === 0, 'missing custom static script still uses generated per-job script');
scm_workshop_test_assert($error === '', 'missing custom script does not expose script-not-found error');
scm_workshop_test_assert(strpos($script, '/srv/games/arma3/gsp_server_content/jobs/workshop/workshop_job_') === 0, 'XML Workshop support still stages generated per-job script');
scm_workshop_test_assert($error === '', 'generated per-job script does not expose script-not-found error');
@unlink(dirname(__DIR__) . '/logs/content_install.log');
@rmdir(dirname(__DIR__) . '/logs');
@ -119,6 +114,7 @@ scm_workshop_test_assert($runtime['target_path_resolved'] === '/srv/games/arma3/
$appXml = simplexml_load_string('<game_config><workshop_support><steam_app_id>107410</steam_app_id><workshop_app_id>107410</workshop_app_id><install_strategy>arma_mod_folder</install_strategy><install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path><copy_keys enabled="1"><target_path>{SERVER_ROOT}/keys</target_path></copy_keys></workshop_support></game_config>');
scm_workshop_test_assert(scm_extract_workshop_app_id($appXml) === '107410', 'extracts canonical Workshop app ID from game XML');
scm_workshop_test_assert(scm_extract_workshop_steam_app_id($appXml) === '107410', 'extracts canonical Steam app ID from game XML');
scm_workshop_test_assert(scm_workshop_is_supported($appXml) === true, 'canonical Workshop XML enables Workshop support');
scm_workshop_test_assert(scm_detect_workshop_install_strategy($linuxHome, $appXml) === 'arma_mod_folder', 'extracts canonical Workshop install strategy from game XML');
$runtime = scm_build_workshop_runtime_context(new stdClass(), $linuxHome, $appXml, array('workshop_item_id' => '450814997'), $message);
scm_workshop_test_assert($runtime['target_path_template'] === '{SERVER_ROOT}/{MOD_FOLDER}', 'canonical Workshop install_path controls target template');

View file

@ -18,6 +18,7 @@
// Central category map — load so we can iterate all types dynamically.
require_once(dirname(__FILE__) . '/server_content_categories.php');
require_once(dirname(__FILE__) . '/server_content_helpers.php');
require_once("modules/config_games/server_config_parser.php");
function exec_ogp_module() {
global $db;
@ -47,6 +48,8 @@ function exec_ogp_module() {
{
scm_ensure_workshop_schema($db);
$home_cfg_id = $home_info['home_cfg_id'];
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
$workshop_supported = ($server_xml !== false && scm_workshop_is_supported($server_xml));
echo "<h2>Server Content: ".htmlentities($home_info['home_name'])."</h2>\n".
"<table class='center' >\n".
"<tr>\n";
@ -68,6 +71,10 @@ function exec_ogp_module() {
"AND home_cfg_id=" . (int)$home_cfg_id . $query_groups
);
$items_qty = is_array($items) ? count((array)$items) : 0;
if ($type_key === 'workshop_item' && $workshop_supported && $items_qty < 1) {
$items = array(array('addon_id' => 0, 'name' => 'Steam Workshop', 'game_name' => isset($home_info['game_name']) ? $home_info['game_name'] : ''));
$items_qty = 1;
}
if ($items && $items_qty >= 1)
{
if ($printed_any_cell)
@ -85,7 +92,7 @@ function exec_ogp_module() {
($first_addon_id > 0 ? "&amp;addon_id=" . $first_addon_id : '') .
"&amp;ip=" . htmlspecialchars($ip) .
"&amp;port=" . htmlspecialchars($port) . "'>" .
htmlspecialchars($type_label) . " (" . $items_qty . ")" .
htmlspecialchars($type_label) . ($workshop_supported ? "" : " (" . $items_qty . ")") .
"</a>\n";
} else {
echo "<a href='?m=addonsmanager&amp;p=addons" .

View file

@ -75,8 +75,7 @@ function scm_workshop_get_content_template($db, $addon_id)
}
scm_ensure_phase2_schema($db);
$rows = $db->resultQuery(
"SELECT addon_id, name, workshop_app_id, target_path_template, optional_folder_name,
post_script, launch_param_additions, content_version, description
"SELECT addon_id, name, content_version, description
FROM `" . OGP_DB_PREFIX . "addons`
WHERE addon_id=" . $addon_id . " AND install_method='steam_workshop'
LIMIT 1"
@ -93,22 +92,12 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
$item_details = array();
$resolved_app_id = '';
$steam_app_id = '';
$template_payload = array(
'workshop_app_id' => isset($template['workshop_app_id']) ? (string)$template['workshop_app_id'] : '',
'target_path_template' => isset($template['target_path_template']) ? (string)$template['target_path_template'] : '',
'optional_folder_name' => isset($template['optional_folder_name']) ? (string)$template['optional_folder_name'] : '',
);
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.
if (count($item_ids) !== 1) {
$payload['optional_folder_name'] = '';
}
$payload = array(
'workshop_item_id' => (string)$item_id,
'install_strategy' => $install_strategy,
);
$message = '';
$runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $payload, $message);
if ($runtime === false) {
@ -146,10 +135,10 @@ 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']) : ($xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy)),
'target_path_template' => $xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy),
'keys_target_path' => $keys_target_path,
'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']) : '',
'post_install_script' => scm_workshop_post_install_action($server_xml),
'launch_param_additions' => isset($server_xml->workshop_support->startup_param_format) ? trim((string)$server_xml->workshop_support->startup_param_format) : '',
'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0,
'content_template_name' => isset($template['name']) ? (string)$template['name'] : '',
'item_details' => $item_details,
@ -261,6 +250,10 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
$message = 'Unable to read server configuration for workshop action.';
return false;
}
if (!scm_workshop_is_supported($server_xml)) {
$message = 'This game XML does not enable Steam Workshop support. Add a valid workshop_support block before installing Workshop items.';
return false;
}
$template = scm_workshop_get_content_template($db, $addon_id);
@ -279,7 +272,7 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
$manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $item_ids, $template);
$resolved_app_id = isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '';
if ($resolved_app_id === '') {
$message = 'Workshop App ID is missing. Configure it on the Server Content template or game XML before installing Workshop items.';
$message = 'Workshop App ID is missing from the game XML workshop_support block.';
return false;
}

View file

@ -4,7 +4,7 @@
* GSP - Server Content Workshop page
*
* Users enter Steam Workshop IDs or URLs to install on their server.
* The admin defines the content template (game, app ID, install path).
* Game-specific Workshop behavior comes from the game XML workshop_support block.
*
*/
@ -39,12 +39,18 @@ function exec_ogp_module() {
return;
}
scm_ensure_phase2_schema($db);
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
if ($server_xml === false || !scm_workshop_is_supported($server_xml)) {
print_failure('Steam Workshop is not enabled for this game XML. Add a valid workshop_support block before using Workshop management.');
echo create_back_button("addonsmanager","user_addons");
return;
}
// Load the admin content template if an addon_id was provided.
// Template records are optional and used only as labels/history anchors.
$addon_template = null;
if ($addon_id > 0) {
$template_rows = $db->resultQuery(
"SELECT addon_id, name, workshop_app_id, target_path_template, optional_folder_name, description
"SELECT addon_id, name, description
FROM `" . OGP_DB_PREFIX . "addons`
WHERE addon_id=" . $addon_id . " AND install_method='steam_workshop'"
);
@ -57,6 +63,8 @@ function exec_ogp_module() {
$is_error = false;
$entered_ids = '';
$catalog_sort = isset($_REQUEST['catalog_sort']) ? (string)$_REQUEST['catalog_sort'] : 'last_installed';
$catalog_query = isset($_REQUEST['workshop_search']) ? trim((string)$_REQUEST['workshop_search']) : '';
$catalog_tag = isset($_REQUEST['workshop_tag']) ? trim((string)$_REQUEST['workshop_tag']) : '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$posted_home_id = isset($_POST['home_id']) ? (int)$_POST['home_id'] : 0;
@ -82,11 +90,17 @@ function exec_ogp_module() {
}
}
$rows = scm_get_workshop_rows($db, $home_id);
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
$catalog_app_id = ($server_xml !== false) ? scm_extract_workshop_app_id($server_xml) : '';
$catalog_rows = scm_get_workshop_catalog_rows($db, $catalog_app_id, $catalog_sort, 50);
$steam_app_id = ($server_xml !== false) ? scm_extract_workshop_steam_app_id($server_xml) : '';
$install_strategy = ($server_xml !== false) ? scm_detect_workshop_install_strategy($home_info, $server_xml) : '';
$rows = scm_get_workshop_rows($db, $home_id);
$catalog_rows = scm_get_workshop_catalog_rows($db, $catalog_app_id, $catalog_sort, 50, $catalog_query, $catalog_tag);
$game_search_rows = scm_get_workshop_enabled_games($catalog_query, $catalog_tag);
$csrf_token = scm_get_csrf_token();
$base_query = 'm=addonsmanager&p=workshop_content&home_id=' . (int)$home_id .
'&mod_id=' . (int)$mod_id . '&ip=' . urlencode($ip) . '&port=' . urlencode($port) .
'&addon_id=' . (int)$addon_id .
'&workshop_search=' . urlencode($catalog_query) . '&workshop_tag=' . urlencode($catalog_tag);
echo "<h2>Workshop Mods: " . scm_h($home_info['home_name']) . "</h2>";
if ($addon_template !== null) {
@ -96,7 +110,7 @@ function exec_ogp_module() {
}
echo "</p>";
}
echo "<p class='info'>Enter a Steam Workshop URL or numeric item ID. GSP stores only the numeric Workshop ID and uses the Server Content template for game-specific install behavior.</p>";
echo "<p class='info'>Enter a Steam Workshop URL or numeric item ID. GSP stores only the numeric Workshop ID. App IDs, install paths, mod folder strategy, key-copy behavior, and launch parameter format come from this game's XML.</p>";
if ($message !== '') {
if ($is_error) {
@ -109,8 +123,58 @@ function exec_ogp_module() {
<table class='center'>
<tr><td align='right'><strong>Server Name:</strong></td><td align='left'><?php echo scm_h($home_info['home_name']); ?></td></tr>
<tr><td align='right'><strong>Game Name:</strong></td><td align='left'><?php echo scm_h($home_info['game_name']); ?></td></tr>
<tr><td align='right'><strong>Workshop App ID:</strong></td><td align='left'><?php echo scm_h($catalog_app_id); ?></td></tr>
<tr><td align='right'><strong>Steam App ID:</strong></td><td align='left'><?php echo scm_h($steam_app_id); ?></td></tr>
<tr><td align='right'><strong>Install Strategy:</strong></td><td align='left'><?php echo scm_h($install_strategy); ?></td></tr>
</table>
<h3>Search Workshop</h3>
<form method='get' action=''>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='workshop_content' />
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
<input type='hidden' name='addon_id' value='<?php echo (int)$addon_id; ?>' />
<table class='center'>
<tr>
<td align='right'><strong>Keyword / ID / URL</strong></td>
<td align='left'><input type='text' name='workshop_search' size='42' value='<?php echo scm_h($catalog_query); ?>' placeholder='ACE, CBA, Zombies, Maps, or Workshop ID' /></td>
<td align='right'><strong>Tag</strong></td>
<td align='left'><input type='text' name='workshop_tag' size='24' value='<?php echo scm_h($catalog_tag); ?>' placeholder='Weapons, Missions, Maps' /></td>
<td><button type='submit'>Search</button></td>
</tr>
<?php if ($catalog_app_id !== '' && ($catalog_query !== '' || $catalog_tag !== '')): ?>
<tr>
<td></td>
<td colspan='4' class='info'>
<a target='_blank' rel='noopener' href='https://steamcommunity.com/workshop/browse/?appid=<?php echo scm_h($catalog_app_id); ?>&amp;searchtext=<?php echo scm_h(urlencode(trim($catalog_query . ' ' . $catalog_tag))); ?>'>Open matching Steam Workshop search</a>
</td>
</tr>
<?php endif; ?>
</table>
</form>
<?php if ($catalog_query !== '' || $catalog_tag !== ''): ?>
<h3>Workshop-Enabled Games</h3>
<table class='center'>
<tr><th>Game</th><th>Config</th><th>Workshop App ID</th><th>Strategy</th></tr>
<?php if (empty($game_search_rows)): ?>
<tr><td colspan='4' class='info'>No Workshop-enabled game XML files matched this search.</td></tr>
<?php else: ?>
<?php foreach ((array)$game_search_rows as $game_row): ?>
<tr>
<td><?php echo scm_h($game_row['game_name']); ?></td>
<td><?php echo scm_h($game_row['config_file']); ?></td>
<td><?php echo scm_h($game_row['workshop_app_id']); ?></td>
<td><?php echo scm_h($game_row['install_strategy']); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</table>
<?php endif; ?>
<form method='post' action=''>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='workshop_content' />
@ -191,27 +255,52 @@ function exec_ogp_module() {
</form>
<h3>Known Workshop Items</h3>
<p class='info'>These are Workshop items previously installed through Server Content Manager. Metadata is optional; direct ID or URL install remains available even when Steam metadata has not been fetched yet.</p>
<p class='info'>These are Workshop items previously installed through Server Content Manager. The catalog grows automatically from real installs. Metadata is optional; direct ID or URL install remains available even when Steam metadata has not been fetched yet.</p>
<table class='center'>
<tr>
<th>Workshop ID</th>
<th><a href='?m=addonsmanager&amp;p=workshop_content&amp;home_id=<?php echo (int)$home_id; ?>&amp;mod_id=<?php echo (int)$mod_id; ?>&amp;ip=<?php echo scm_h($ip); ?>&amp;port=<?php echo scm_h($port); ?>&amp;addon_id=<?php echo (int)$addon_id; ?>&amp;catalog_sort=name'>Name</a></th>
<th><a href='?m=addonsmanager&amp;p=workshop_content&amp;home_id=<?php echo (int)$home_id; ?>&amp;mod_id=<?php echo (int)$mod_id; ?>&amp;ip=<?php echo scm_h($ip); ?>&amp;port=<?php echo scm_h($port); ?>&amp;addon_id=<?php echo (int)$addon_id; ?>&amp;catalog_sort=install_count'>Install Count</a></th>
<th><a href='?m=addonsmanager&amp;p=workshop_content&amp;home_id=<?php echo (int)$home_id; ?>&amp;mod_id=<?php echo (int)$mod_id; ?>&amp;ip=<?php echo scm_h($ip); ?>&amp;port=<?php echo scm_h($port); ?>&amp;addon_id=<?php echo (int)$addon_id; ?>&amp;catalog_sort=published_date'>Published</a></th>
<th><a href='?m=addonsmanager&amp;p=workshop_content&amp;home_id=<?php echo (int)$home_id; ?>&amp;mod_id=<?php echo (int)$mod_id; ?>&amp;ip=<?php echo scm_h($ip); ?>&amp;port=<?php echo scm_h($port); ?>&amp;addon_id=<?php echo (int)$addon_id; ?>&amp;catalog_sort=last_updated'>Last Updated</a></th>
<th><a href='?m=addonsmanager&amp;p=workshop_content&amp;home_id=<?php echo (int)$home_id; ?>&amp;mod_id=<?php echo (int)$mod_id; ?>&amp;ip=<?php echo scm_h($ip); ?>&amp;port=<?php echo scm_h($port); ?>&amp;addon_id=<?php echo (int)$addon_id; ?>&amp;catalog_sort=last_installed'>Last Installed</a></th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=name'>Name</a></th>
<th>Author</th>
<th>Thumbnail</th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=install_count'>Install Count</a></th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=published_date'>Published</a></th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=last_updated'>Last Updated</a></th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=last_installed'>Last Installed</a></th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=workshop_id'>Sort ID</a></th>
<th>Action</th>
</tr>
<?php if (empty($catalog_rows)): ?>
<tr><td colspan='6' class='info'>No known Workshop items have been installed for this app yet.</td></tr>
<tr><td colspan='10' class='info'>No known Workshop items have been installed for this app yet.</td></tr>
<?php else: ?>
<?php foreach ((array)$catalog_rows as $catalog_row): ?>
<tr>
<td><?php echo scm_h($catalog_row['workshop_id']); ?></td>
<td><?php echo scm_h($catalog_row['title']); ?></td>
<td><?php echo scm_h(isset($catalog_row['author']) ? $catalog_row['author'] : ''); ?></td>
<td>
<?php if (!empty($catalog_row['thumbnail_url'])): ?>
<img src='<?php echo scm_h($catalog_row['thumbnail_url']); ?>' alt='' style='max-width:72px;max-height:48px;' />
<?php endif; ?>
</td>
<td><?php echo scm_h($catalog_row['install_count']); ?></td>
<td><?php echo scm_h($catalog_row['published_date']); ?></td>
<td><?php echo scm_h($catalog_row['last_updated']); ?></td>
<td><?php echo scm_h($catalog_row['last_installed']); ?></td>
<td><?php echo scm_h($catalog_row['workshop_id']); ?></td>
<td>
<form method='post' action='' style='margin:0;'>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='workshop_content' />
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
<input type='hidden' name='addon_id' value='<?php echo (int)$addon_id; ?>' />
<input type='hidden' name='workshop_csrf' value='<?php echo scm_h($csrf_token); ?>' />
<input type='hidden' name='workshop_ids' value='<?php echo scm_h($catalog_row['workshop_id']); ?>' />
<button type='submit' name='workshop_action' value='install_new'>Install</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>

View file

@ -335,8 +335,7 @@
<xs:element name="mod_prefix" type="xs:string" minOccurs="0" />
<xs:element name="mod_folder_format" type="xs:string" minOccurs="0" />
<xs:element name="copy_keys" type="workshop_copy_keys_type" minOccurs="0" />
<xs:element name="script_linux" type="xs:string" minOccurs="0" />
<xs:element name="script_windows" type="xs:string" minOccurs="0" />
<xs:element name="post_install_action" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:complexType>