codex steam workshop integrarion

This commit is contained in:
Frank Harris 2026-06-05 13:31:15 -05:00
parent 3cefad183d
commit c687165132
12 changed files with 629 additions and 84 deletions

View file

@ -81,6 +81,7 @@ function exec_ogp_module() {
'url' => $fields['url'],
'path' => $fields['path'],
'workshop_item_id' => $fields['workshop_item_id'],
'workshop_app_id' => $fields['workshop_app_id'],
'target_path_template' => $fields['target_path_template'],
'post_script' => $fields['post_script'],
'config_edit_rule' => $fields['config_edit_rule'],

View file

@ -25,13 +25,16 @@
* (allow_user_workshop_ids, max_workshop_ids, required_workshop_ids,
* blocked_workshop_ids); add content_id column to
* server_content_workshop so user installs link to their template
* 7 add Phase 1 Workshop runtime tracking columns to
* server_content_workshop (install_path, install_strategy, enabled,
* load_order)
*
*/
// Module general information
$module_title = "Server Content Manager";
$module_version = "2.4";
$db_version = 6;
$module_version = "2.5";
$db_version = 7;
$module_required = TRUE;
$module_menus = array(
array( 'subpage' => 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' )
@ -260,4 +263,31 @@ $install_queries[5] = array(
return true;
},
);
// ── db_version 7 : Workshop Phase 1 runtime tracking columns ────────────────
$install_queries[6] = array(
function ($db) {
$prefix = OGP_DB_PREFIX;
$table = $db->realEscapeSingle($prefix . 'server_content_workshop');
$columns = array(
'install_path' => "VARCHAR(512) NULL AFTER `title`",
'install_strategy' => "VARCHAR(64) NULL AFTER `install_path`",
'enabled' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `install_strategy`",
'load_order' => "INT NOT NULL DEFAULT 0 AFTER `enabled`",
);
foreach ($columns as $col => $definition) {
$check = $db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{$table}'
AND COLUMN_NAME = '" . $db->realEscapeSingle($col) . "'"
);
if (empty($check)) {
if (!$db->query("ALTER TABLE `{$prefix}server_content_workshop` ADD COLUMN `{$col}` {$definition}")) {
return false;
}
}
}
return true;
},
);
?>

View file

@ -69,6 +69,19 @@ def ensure_under_home(path_value):
return target
def safe_folder_name(value, fallback):
text = str(value or '').strip()
if not text or '..' in text or '/' in text or '\\' in text or '\x00' in text:
return fallback
return text
def truthy(value):
if isinstance(value, bool):
return value
return str(value).strip().lower() in ('1', 'yes', 'true', 'on')
def resolve_steamcmd(explicit_path=''):
candidates = []
explicit_path = str(explicit_path or '').strip()
@ -104,6 +117,26 @@ def sync_copy(src, dst):
shutil.copy2(source_entry, target_entry)
def copy_bikeys(mod_path, keys_target, workshop_id):
if not os.path.isdir(mod_path):
return 0
keys_target = ensure_under_home(keys_target)
os.makedirs(keys_target, exist_ok=True)
copied = 0
for root, dirs, files in os.walk(mod_path):
for filename in files:
if not filename.lower().endswith('.bikey'):
continue
source_file = os.path.join(root, filename)
target_file = os.path.join(keys_target, filename)
shutil.copy2(source_file, target_file)
copied += 1
log(f"workshop_id={workshop_id} key={target_file}", 'Copying Key')
if copied == 0:
log(f"workshop_id={workshop_id} no .bikey files found; continuing", 'Copying Key')
return copied
try:
with open(manifest_path, 'r', encoding='utf-8') as handle:
manifest = json.load(handle)
@ -122,13 +155,17 @@ try:
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()
item_details = manifest.get('item_details') or extra.get('item_details') or {}
default_install_strategy = str(manifest.get('install_strategy') or extra.get('install_strategy') or 'copy_to_mod_folder').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)
detail = item_details.get(workshop_id) or item_details.get(str(workshop_id)) or {}
folder_name = safe_folder_name(detail.get('folder_name') or extra.get('optional_folder_name') or '', '@' + workshop_id)
install_strategy = str(detail.get('install_strategy') or default_install_strategy).strip() or 'copy_to_mod_folder'
template_values = {
'HOME_ID': manifest.get('home_id', ''),
'SERVER_ROOT': server_root,
@ -139,11 +176,13 @@ try:
'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()
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
if len(items) != 1 or not target_path:
target_path = render_template(target_template, template_values)
target_path = ensure_under_home(target_path)
keys_target_path = str(detail.get('keys_target_path') or extra.get('keys_target_path') or os.path.join(server_root, 'keys'))
should_copy_keys = truthy(detail.get('copy_keys', extra.get('copy_keys', install_strategy in ('dayz_mod_folder', 'arma_mod_folder'))))
download_dir = ensure_under_home(render_template(default_download_dir, template_values))
source_path = os.path.join(download_dir, workshop_id)
@ -169,9 +208,11 @@ try:
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')
log(f"workshop_id={workshop_id} strategy={install_strategy} 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 should_copy_keys:
copy_bikeys(target_path, keys_target_path, workshop_id)
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)

View file

@ -69,6 +69,19 @@ def ensure_under_home(path_value):
return target
def safe_folder_name(value, fallback):
text = str(value or '').strip()
if not text or '..' in text or '/' in text or '\\' in text or '\x00' in text:
return fallback
return text
def truthy(value):
if isinstance(value, bool):
return value
return str(value).strip().lower() in ('1', 'yes', 'true', 'on')
def resolve_steamcmd(explicit_path=''):
candidates = []
explicit_path = str(explicit_path or '').strip()
@ -105,6 +118,26 @@ def sync_copy(src, dst):
shutil.copy2(source_entry, target_entry)
def copy_bikeys(mod_path, keys_target, workshop_id):
if not os.path.isdir(mod_path):
return 0
keys_target = ensure_under_home(keys_target)
os.makedirs(keys_target, exist_ok=True)
copied = 0
for root, dirs, files in os.walk(mod_path):
for filename in files:
if not filename.lower().endswith('.bikey'):
continue
source_file = os.path.join(root, filename)
target_file = os.path.join(keys_target, filename)
shutil.copy2(source_file, target_file)
copied += 1
log(f"workshop_id={workshop_id} key={target_file}", 'Copying Key')
if copied == 0:
log(f"workshop_id={workshop_id} no .bikey files found; continuing", 'Copying Key')
return copied
try:
with open(manifest_path, 'r', encoding='utf-8') as handle:
manifest = json.load(handle)
@ -123,13 +156,17 @@ try:
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()
item_details = manifest.get('item_details') or extra.get('item_details') or {}
default_install_strategy = str(manifest.get('install_strategy') or extra.get('install_strategy') or 'copy_to_mod_folder').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)
detail = item_details.get(workshop_id) or item_details.get(str(workshop_id)) or {}
folder_name = safe_folder_name(detail.get('folder_name') or extra.get('optional_folder_name') or '', '@' + workshop_id)
install_strategy = str(detail.get('install_strategy') or default_install_strategy).strip() or 'copy_to_mod_folder'
template_values = {
'HOME_ID': manifest.get('home_id', ''),
'SERVER_ROOT': server_root,
@ -140,11 +177,13 @@ try:
'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()
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
if len(items) != 1 or not target_path:
target_path = render_template(target_template, template_values)
target_path = ensure_under_home(target_path)
keys_target_path = str(detail.get('keys_target_path') or extra.get('keys_target_path') or os.path.join(server_root, 'keys'))
should_copy_keys = truthy(detail.get('copy_keys', extra.get('copy_keys', install_strategy in ('dayz_mod_folder', 'arma_mod_folder'))))
download_dir = ensure_under_home(render_template(default_download_dir, template_values))
source_path = os.path.join(download_dir, workshop_id)
@ -170,9 +209,11 @@ try:
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')
log(f"workshop_id={workshop_id} strategy={install_strategy} 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 should_copy_keys:
copy_bikeys(target_path, keys_target_path, workshop_id)
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)

View file

@ -30,6 +30,10 @@ function scm_ensure_workshop_schema($db)
`workshop_app_id` VARCHAR(32) NULL,
`workshop_item_id` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NULL,
`install_path` VARCHAR(512) NULL,
`install_strategy` VARCHAR(64) NULL,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`load_order` INT NOT NULL DEFAULT 0,
`install_state` VARCHAR(32) NOT NULL DEFAULT 'selected',
`last_installed_at` DATETIME NULL,
`last_updated_at` DATETIME NULL,
@ -60,6 +64,25 @@ function scm_ensure_workshop_schema($db)
);
}
$workshop_columns = array(
'install_path' => "VARCHAR(512) NULL AFTER `title`",
'install_strategy' => "VARCHAR(64) NULL AFTER `install_path`",
'enabled' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `install_strategy`",
'load_order' => "INT NOT NULL DEFAULT 0 AFTER `enabled`",
);
foreach ($workshop_columns 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 = '{$wk_table}'
AND COLUMN_NAME = '{$escaped_col}'"
);
if (empty($col_check)) {
$db->query("ALTER TABLE `".OGP_DB_PREFIX."server_content_workshop` ADD COLUMN `{$col}` {$definition}");
}
}
return $ok;
}
@ -98,28 +121,49 @@ function scm_get_workshop_rows($db, $home_id)
return array();
}
$rows = $db->resultQuery(
"SELECT * FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." ORDER BY created_at DESC, workshop_item_id ASC"
"SELECT * FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." ORDER BY load_order ASC, created_at DESC, workshop_item_id ASC"
);
return is_array($rows) ? $rows : array();
}
function scm_extract_workshop_item_id($value)
{
$value = trim((string)$value);
if ($value === '') {
return '';
}
if (preg_match('/^[0-9]{1,20}$/', $value)) {
return ltrim($value, '0') === '' ? '' : $value;
}
if (preg_match('/[?&]id=([0-9]{1,20})(?:[^0-9]|$)/i', $value, $matches)) {
return ltrim($matches[1], '0') === '' ? '' : $matches[1];
}
if (preg_match('/steamcommunity\.com\/(?:sharedfiles|workshop)\/filedetails\/\?[^ \t\r\n]*id=([0-9]{1,20})/i', $value, $matches)) {
return ltrim($matches[1], '0') === '' ? '' : $matches[1];
}
return '';
}
function scm_parse_workshop_ids($raw, &$invalid = array())
{
$invalid = array();
$ids = array();
// Accept IDs separated by commas, newlines, or a mix of both.
$normalized = str_replace(array("\r\n", "\r", "\n"), ',', (string)$raw);
$parts = explode(',', $normalized);
// Accept IDs or full Steam Workshop URLs separated by commas, whitespace,
// or newlines. Customer input is reduced to numeric IDs before it reaches
// manifests, shell commands, or install paths.
$normalized = preg_replace('/[\r\n\t ]+/', ',', (string)$raw);
$parts = explode(',', (string)$normalized);
foreach ((array)$parts as $part) {
$value = trim((string)$part);
if ($value === '') {
continue;
}
if (!preg_match('/^[0-9]+$/', $value)) {
$item_id = scm_extract_workshop_item_id($value);
if ($item_id === '') {
$invalid[] = $value;
continue;
}
$ids[$value] = $value;
$ids[$item_id] = $item_id;
}
return array_values($ids);
}
@ -472,11 +516,11 @@ function scm_validate_workshop_user_ids($raw_ids, &$message = '')
$invalid = array();
$ids = scm_parse_workshop_ids($raw_ids, $invalid);
if (!empty($invalid)) {
$message = 'Invalid Workshop IDs: ' . implode(', ', $invalid);
$message = 'Invalid Workshop item entries. Use a numeric Workshop ID or a Steam Workshop URL: ' . implode(', ', $invalid);
return false;
}
if (empty($ids)) {
$message = 'Enter at least one numeric Workshop ID.';
$message = 'Enter at least one Steam Workshop ID or Workshop URL.';
return false;
}
$message = '';
@ -604,6 +648,46 @@ function scm_build_workshop_runtime_context($db, array $home_info, $server_xml,
);
}
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);
}
}
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'] : ''));
$haystack = $game_key . ' ' . $cfg_file . ' ' . $name;
if (strpos($haystack, 'dayz') !== false) {
return 'dayz_mod_folder';
}
if (strpos($haystack, 'arma') !== false) {
return 'arma_mod_folder';
}
return 'copy_to_mod_folder';
}
function scm_workshop_should_copy_keys($server_xml, $install_strategy)
{
foreach (array('workshop_copy_keys', 'copy_workshop_keys') as $tag) {
if (isset($server_xml->$tag)) {
$value = strtolower(trim((string)$server_xml->$tag));
return in_array($value, array('1', 'yes', 'true', 'on'), true);
}
}
return in_array((string)$install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true);
}
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);

View file

@ -67,9 +67,92 @@ function scm_workshop_filter_existing_ids($db, $home_id, array $item_ids)
return array_values($allowed);
}
function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml, $action, array $item_ids, &$error = '', array $extra_manifest = array())
function scm_workshop_get_content_template($db, $addon_id)
{
$addon_id = (int)$addon_id;
if ($addon_id <= 0) {
return array();
}
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
FROM `" . OGP_DB_PREFIX . "addons`
WHERE addon_id=" . $addon_id . " AND install_method='steam_workshop'
LIMIT 1"
);
return (is_array($rows) && !empty($rows)) ? $rows[0] : array();
}
function scm_workshop_build_manifest_context($db, array $home_info, $server_xml, array $item_ids, array $template = array())
{
$install_strategy = scm_detect_workshop_install_strategy($home_info, $server_xml, $template);
$copy_keys = scm_workshop_should_copy_keys($server_xml, $install_strategy);
$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;
// 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'] = '';
}
$message = '';
$runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $payload, $message);
if ($runtime === false) {
$runtime = array();
}
$item_app_id = isset($runtime['workshop_app_id']) ? (string)$runtime['workshop_app_id'] : '';
if ($resolved_app_id === '' && $item_app_id !== '') {
$resolved_app_id = $item_app_id;
}
if ($steam_app_id === '' && !empty($runtime['steam_app_id'])) {
$steam_app_id = (string)$runtime['steam_app_id'];
}
$item_details[(string)$item_id] = array(
'workshop_item_id' => (string)$item_id,
'title' => '',
'folder_name' => isset($runtime['folder_name']) && $runtime['folder_name'] !== '' ? (string)$runtime['folder_name'] : '@' . $item_id,
'target_path_template' => isset($runtime['target_path_template']) ? (string)$runtime['target_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}',
'target_path_resolved' => isset($runtime['target_path_resolved']) ? (string)$runtime['target_path_resolved'] : '',
'install_strategy' => $install_strategy,
'copy_keys' => $copy_keys ? 1 : 0,
'keys_target_path' => rtrim((string)$home_info['home_path'], '/') . '/keys',
);
}
if ($resolved_app_id === '') {
$resolved_app_id = scm_extract_workshop_app_id($server_xml);
}
return array(
'workshop_app_id' => $resolved_app_id,
'steam_app_id' => $steam_app_id,
'server_root' => rtrim((string)$home_info['home_path'], '/'),
'install_strategy' => $install_strategy,
'copy_keys' => $copy_keys ? 1 : 0,
'target_path_template' => isset($template['target_path_template']) && trim((string)$template['target_path_template']) !== '' ? trim((string)$template['target_path_template']) : '{SERVER_ROOT}/{MOD_FOLDER}',
'post_install_script' => isset($template['post_script']) ? trim((string)$template['post_script']) : '',
'launch_param_additions' => isset($template['launch_param_additions']) ? trim((string)$template['launch_param_additions']) : '',
'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0,
'content_template_name' => isset($template['name']) ? (string)$template['name'] : '',
'item_details' => $item_details,
);
}
function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml, $action, array $item_ids, &$error = '', array $extra_manifest = array(), &$result_details = array())
{
$error = '';
$result_details = array();
if (empty($item_ids)) {
$error = 'No Workshop IDs were selected for this action.';
return false;
@ -89,12 +172,18 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
$manifest_dir = dirname($manifest_path);
$manifest = array(
'manifest_version' => 1,
'action' => (string)$action,
'home_id' => (int)$home_info['home_id'],
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'game_path' => $home_path,
'server_path' => $home_path,
'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),
'item_details' => !empty($extra_manifest['item_details']) && is_array($extra_manifest['item_details']) ? $extra_manifest['item_details'] : array(),
'install_strategy' => !empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : '',
'target_path' => !empty($extra_manifest['target_path_template']) ? (string)$extra_manifest['target_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}',
'generated_at' => date('Y-m-d H:i:s'),
);
if (!empty($extra_manifest)) {
@ -133,6 +222,12 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
$error = 'Workshop script exit marker not found in output.';
return false;
}
$result_details = array(
'manifest_path' => $manifest_path,
'script_path' => $script_path,
'log_path' => clean_path($manifest_dir . (scm_is_windows_home($home_info) ? '/workshop_install_windows.log' : '/workshop_install.log')),
'output' => trim(preg_replace('/__GSP_WORKSHOP_EXIT:\d+/', '', $output)),
);
$exit_code = (int)$matches[1];
if ($exit_code !== 0) {
$error = 'Workshop script failed (exit '.$exit_code.'): '.trim($output);
@ -149,6 +244,7 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
$message = 'Workshop schema migration failed.';
return false;
}
scm_ensure_phase2_schema($db);
$home_id = (int)$home_info['home_id'];
$user_id = (int)$user_id;
@ -159,39 +255,26 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
return false;
}
// Resolve the workshop_app_id: prefer the admin content template, fall
// back to the game XML.
$template_workshop_app_id = '';
if ($addon_id > 0) {
$tpl = $db->resultQuery(
"SELECT workshop_app_id FROM `" . OGP_DB_PREFIX . "addons`
WHERE addon_id=" . $addon_id . " AND install_method='steam_workshop'"
);
if (is_array($tpl) && !empty($tpl[0]['workshop_app_id'])) {
$template_workshop_app_id = trim((string)$tpl[0]['workshop_app_id']);
}
}
$extra_manifest = array();
if ($template_workshop_app_id !== '') {
$extra_manifest['workshop_app_id'] = $template_workshop_app_id;
}
$template = scm_workshop_get_content_template($db, $addon_id);
if ($action === 'install_new') {
$invalid = array();
$item_ids = scm_parse_workshop_ids($raw_ids, $invalid);
if (!empty($invalid)) {
$message = 'Invalid Workshop IDs: ' . implode(', ', $invalid);
$message = 'Invalid Workshop item entries. Use a numeric Workshop ID or Steam Workshop URL: ' . implode(', ', $invalid);
return false;
}
if (empty($item_ids)) {
$message = 'Enter at least one numeric Workshop ID.';
$message = 'Enter at least one Steam Workshop ID or Workshop URL.';
return false;
}
// Determine the resolved workshop_app_id for storage (template first, then XML).
$resolved_app_id = $template_workshop_app_id !== ''
? $template_workshop_app_id
: scm_extract_workshop_app_id($server_xml);
$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.';
return false;
}
// Check whether the content_id column exists (added in db_version 6).
$has_content_id_col = (bool)$db->resultQuery(
@ -201,19 +284,32 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
AND COLUMN_NAME = 'content_id'"
);
$next_order_rows = $db->resultQuery(
"SELECT COALESCE(MAX(load_order), 0) AS max_order FROM `".OGP_DB_PREFIX."server_content_workshop`
WHERE home_id=".$home_id
);
$next_order = (is_array($next_order_rows) && isset($next_order_rows[0]['max_order'])) ? (int)$next_order_rows[0]['max_order'] : 0;
foreach ($item_ids as $item_id) {
$item_detail = isset($manifest_context['item_details'][(string)$item_id]) ? $manifest_context['item_details'][(string)$item_id] : array();
$install_path = isset($item_detail['target_path_resolved']) ? (string)$item_detail['target_path_resolved'] : '';
$install_strategy = isset($item_detail['install_strategy']) ? (string)$item_detail['install_strategy'] : (string)$manifest_context['install_strategy'];
$next_order++;
$content_id_col = $has_content_id_col && $addon_id > 0 ? ", content_id" : '';
$content_id_val = $has_content_id_col && $addon_id > 0 ? ", " . $addon_id : '';
$content_id_upd = $has_content_id_col && $addon_id > 0 ? ", content_id=VALUES(content_id)" : '';
$query = "INSERT INTO `".OGP_DB_PREFIX."server_content_workshop`
(home_id, home_cfg_id, remote_server_id, workshop_app_id, workshop_item_id, install_state, created_by, created_at, updated_at" . $content_id_col . ")
(home_id, home_cfg_id, remote_server_id, workshop_app_id, workshop_item_id, install_path, install_strategy, enabled, load_order, install_state, created_by, created_at, updated_at" . $content_id_col . ")
VALUES (
".$home_id.",
".(int)$home_info['home_cfg_id'].",
".(int)$home_info['remote_server_id'].",
'".$db->realEscapeSingle($resolved_app_id)."',
'".$db->realEscapeSingle($item_id)."',
'selected',
'".$db->realEscapeSingle($install_path)."',
'".$db->realEscapeSingle($install_strategy)."',
1,
".$next_order.",
'queued',
".$user_id.",
NOW(),
NOW()
@ -223,7 +319,10 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
home_cfg_id=VALUES(home_cfg_id),
remote_server_id=VALUES(remote_server_id),
workshop_app_id=VALUES(workshop_app_id),
install_state='selected',
install_path=VALUES(install_path),
install_strategy=VALUES(install_strategy),
enabled=1,
install_state='queued',
last_error=NULL,
updated_at=NOW()" . $content_id_upd;
$db->query($query);
@ -231,12 +330,13 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', $item_ids, $error, $extra_manifest);
$details = array();
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', $item_ids, $error, $manifest_context, $details);
if ($ok) {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, true, true);
scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=success");
$is_error = false;
$message = 'Workshop IDs installed successfully.';
$message = 'Workshop item(s) installed successfully. Manifest: '.scm_h(isset($details['manifest_path']) ? $details['manifest_path'] : '').' Log: '.scm_h(isset($details['log_path']) ? $details['log_path'] : '');
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
@ -252,9 +352,11 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
return false;
}
$target_action = ($action === 'remove_selected') ? 'remove' : 'update';
$manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $item_ids, $template);
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, $target_action, $item_ids, $error, $extra_manifest);
$details = array();
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, $target_action, $item_ids, $error, $manifest_context, $details);
if ($ok) {
if ($target_action === 'remove') {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'removed', null, false, true);
@ -263,7 +365,7 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
}
scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=success");
$is_error = false;
$message = ($target_action === 'remove') ? 'Selected Workshop IDs marked removed.' : 'Selected Workshop IDs updated successfully.';
$message = (($target_action === 'remove') ? 'Selected Workshop item(s) removed.' : 'Selected Workshop item(s) updated successfully.') . ' Log: ' . scm_h(isset($details['log_path']) ? $details['log_path'] : '');
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
@ -288,14 +390,16 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
$message = 'No Workshop IDs are currently saved for this server.';
return false;
}
$manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $item_ids, $template);
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'update', $item_ids, $error, $extra_manifest);
$details = array();
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'update', $item_ids, $error, $manifest_context, $details);
if ($ok) {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true);
scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=success");
$is_error = false;
$message = 'All saved Workshop IDs updated successfully.';
$message = 'All saved Workshop item(s) updated successfully. Log: ' . scm_h(isset($details['log_path']) ? $details['log_path'] : '');
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);

View file

@ -3,7 +3,7 @@
*
* GSP - Server Content Workshop page
*
* Users enter Steam Workshop IDs to install on their server.
* Users enter Steam Workshop IDs or URLs to install on their server.
* The admin defines the content template (game, app ID, install path).
*
*/
@ -38,6 +38,7 @@ function exec_ogp_module() {
print_failure('Failed to initialize Workshop Content storage.');
return;
}
scm_ensure_phase2_schema($db);
// Load the admin content template if an addon_id was provided.
$addon_template = null;
@ -88,6 +89,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>";
if ($message !== '') {
if ($is_error) {
@ -114,10 +116,10 @@ function exec_ogp_module() {
<table class='center'>
<tr>
<td align='right'><strong>Workshop Item IDs</strong></td>
<td align='right'><strong>Workshop URLs / IDs</strong></td>
<td align='left'>
<textarea name='workshop_ids' rows='4' cols='72' placeholder='450814997&#10;463939057&#10;...'><?php echo scm_h($entered_ids); ?></textarea>
<br><small style="color:#666;">Enter one or more Steam Workshop IDs, one per line or comma-separated.<br>Example for Arma 3 CBA_A3: <code>450814997</code></small>
<textarea name='workshop_ids' rows='4' cols='72' placeholder='https://steamcommunity.com/sharedfiles/filedetails/?id=450814997&#10;463939057'><?php echo scm_h($entered_ids); ?></textarea>
<br><small style="color:#666;">Enter one or more Steam Workshop URLs or numeric IDs, one per line, comma-separated, or space-separated.<br>Example for Arma 3 CBA_A3: <code>https://steamcommunity.com/sharedfiles/filedetails/?id=450814997</code></small>
</td>
<td align='left' style='vertical-align:top;padding-top:4px;'>
<button type='submit' name='workshop_action' value='install_new'>Install / Queue</button>
@ -131,20 +133,26 @@ function exec_ogp_module() {
<th></th>
<th>Workshop ID</th>
<th>Title</th>
<th>Enabled</th>
<th>Order</th>
<th>State</th>
<th>Install Path</th>
<th>Last Installed</th>
<th>Last Updated</th>
<th>Last Error</th>
</tr>
<?php if (empty($rows)): ?>
<tr><td colspan='7' class='info'>No Workshop IDs saved for this server yet.</td></tr>
<tr><td colspan='10' class='info'>No Workshop items saved for this server yet.</td></tr>
<?php else: ?>
<?php foreach ((array)$rows as $row): ?>
<tr>
<td><input type='checkbox' name='selected_ids[]' value='<?php echo scm_h($row['workshop_item_id']); ?>'></td>
<td><?php echo scm_h($row['workshop_item_id']); ?></td>
<td><?php echo scm_h($row['title']); ?></td>
<td><?php echo !empty($row['enabled']) ? 'Yes' : 'No'; ?></td>
<td><?php echo scm_h(isset($row['load_order']) ? $row['load_order'] : ''); ?></td>
<td><?php echo scm_h($row['install_state']); ?></td>
<td><small><?php echo scm_h(isset($row['install_path']) ? $row['install_path'] : ''); ?></small></td>
<td><?php echo scm_h($row['last_installed_at']); ?></td>
<td><?php echo scm_h($row['last_updated_at']); ?></td>
<td><?php echo scm_h($row['last_error']); ?></td>
@ -173,4 +181,3 @@ function exec_ogp_module() {
</form>
<?php
}

View file

@ -38,23 +38,7 @@ if (!function_exists('sw_monitor_table')) {
}
}
// Only show the button when a Workshop profile is enabled for this game config.
$_sw_profile = $db->resultQuery(
"SELECT p.`id`
FROM " . sw_monitor_table('steam_workshop_game_profiles') . " p
JOIN " . sw_monitor_table('config_homes') . " c ON c.`game_key` = p.`config_name`
JOIN " . sw_monitor_table('server_homes') . " s ON s.`home_cfg_id` = c.`home_cfg_id`
WHERE s.home_id = " . (int)$server_home['home_id'] . "
AND p.enabled = 1
LIMIT 1"
);
if (!empty($_sw_profile)) {
$module_buttons[] = "<a class='monitorbutton' href='home.php?m=steam_workshop&amp;p=user&amp;home_id=" . (int)$server_home['home_id'] . "'>
<img src='" . htmlspecialchars(check_theme_image("images/steam_workshop.png"), ENT_QUOTES, 'UTF-8') . "' title='Steam Workshop'>
<span>Steam Workshop</span>
</a>";
}
unset($_sw_profile);
// Deprecated: the standalone steam_workshop workflow is no longer the primary
// user path. Server Content Manager (addonsmanager) now owns Workshop installs
// and exposes its own monitor button when content templates exist.
?>