redo of steam workshop
This commit is contained in:
parent
e662415f36
commit
e6541370b9
19 changed files with 610 additions and 38 deletions
|
|
@ -56,6 +56,10 @@ define('LANG_server_content_notify_updates_only', 'Notify Updates Only');
|
|||
define('LANG_server_content_update_all', 'Update All Server Content');
|
||||
define('LANG_server_content_validate_files', 'Validate Server Content Files');
|
||||
define('LANG_server_content_backup_before_update', 'Backup Before Server Content Update');
|
||||
define('LANG_workshop_update', 'Update Workshop Items');
|
||||
define('LANG_workshop_update_and_restart', 'Update Workshop Items And Restart');
|
||||
define('LANG_workshop_download_only', 'Download Workshop Items Only');
|
||||
define('LANG_workshop_install_pending_on_restart', 'Install Pending Workshop Items On Restart');
|
||||
define('LANG_safe_restart', 'Safe Restart');
|
||||
define('LANG_workshop_content', 'Workshop Content');
|
||||
define('LANG_server_content', 'Server Content');
|
||||
|
|
|
|||
|
|
@ -153,13 +153,15 @@ try:
|
|||
workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip()
|
||||
steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip()
|
||||
server_root = ensure_under_home(extra.get('server_root') or home_root)
|
||||
steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '')
|
||||
steamcmd_path = ''
|
||||
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
|
||||
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
|
||||
action_label = 'Queued' if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files') 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:
|
||||
|
|
@ -187,7 +189,7 @@ try:
|
|||
download_dir = ensure_under_home(render_template(default_download_dir, template_values))
|
||||
source_path = os.path.join(download_dir, workshop_id)
|
||||
|
||||
if action in ('install', 'update', 'check_updates'):
|
||||
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
|
||||
if not workshop_app_id:
|
||||
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
|
||||
command = [
|
||||
|
|
@ -207,7 +209,7 @@ try:
|
|||
if not os.path.isdir(source_path):
|
||||
fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}")
|
||||
|
||||
if action != 'check_updates':
|
||||
if action not in ('check_updates', 'download_only', 'validate_files'):
|
||||
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')
|
||||
|
|
@ -221,6 +223,10 @@ try:
|
|||
log(f"post_install[{workshop_id}] {line}")
|
||||
if post_result.returncode != 0:
|
||||
fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.")
|
||||
elif action == 'download_only':
|
||||
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Downloaded Only')
|
||||
elif action == 'validate_files':
|
||||
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Validated')
|
||||
log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed')
|
||||
elif action == 'remove':
|
||||
if os.path.exists(target_path):
|
||||
|
|
|
|||
|
|
@ -154,13 +154,15 @@ try:
|
|||
workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip()
|
||||
steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip()
|
||||
server_root = ensure_under_home(extra.get('server_root') or home_root)
|
||||
steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '')
|
||||
steamcmd_path = ''
|
||||
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
|
||||
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
|
||||
action_label = 'Queued' if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files') 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:
|
||||
|
|
@ -188,7 +190,7 @@ try:
|
|||
download_dir = ensure_under_home(render_template(default_download_dir, template_values))
|
||||
source_path = os.path.join(download_dir, workshop_id)
|
||||
|
||||
if action in ('install', 'update', 'check_updates'):
|
||||
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
|
||||
if not workshop_app_id:
|
||||
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
|
||||
command = [
|
||||
|
|
@ -208,7 +210,7 @@ try:
|
|||
if not os.path.isdir(source_path):
|
||||
fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}")
|
||||
|
||||
if action != 'check_updates':
|
||||
if action not in ('check_updates', 'download_only', 'validate_files'):
|
||||
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')
|
||||
|
|
@ -222,6 +224,10 @@ try:
|
|||
log(f"post_install[{workshop_id}] {line}")
|
||||
if post_result.returncode != 0:
|
||||
fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.")
|
||||
elif action == 'download_only':
|
||||
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Downloaded Only')
|
||||
elif action == 'validate_files':
|
||||
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Validated')
|
||||
log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed')
|
||||
elif action == 'remove':
|
||||
if os.path.exists(target_path):
|
||||
|
|
|
|||
|
|
@ -20,11 +20,15 @@ function server_content_get_home_info($home_id)
|
|||
return $db->getGameHome($home_id);
|
||||
}
|
||||
|
||||
function server_content_collect_workshop_ids($db, $home_id)
|
||||
function server_content_collect_workshop_ids($db, $home_id, $pending_only = false)
|
||||
{
|
||||
$where = "home_id=".(int)$home_id." AND install_state<>'removed'";
|
||||
if ($pending_only) {
|
||||
$where .= " AND pending_action='install_on_restart'";
|
||||
}
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
|
||||
WHERE home_id=".(int)$home_id." AND install_state<>'removed'"
|
||||
WHERE {$where}"
|
||||
);
|
||||
$item_ids = array();
|
||||
if (is_array($rows)) {
|
||||
|
|
@ -237,7 +241,7 @@ function server_content_install_updates($home_id, $options = array())
|
|||
if ($workshop_action === '') {
|
||||
$workshop_action = !empty($options['check_only']) ? 'check_updates' : 'update';
|
||||
}
|
||||
$workshop_ids = server_content_collect_workshop_ids($db, (int)$home_info['home_id']);
|
||||
$workshop_ids = server_content_collect_workshop_ids($db, (int)$home_info['home_id'], !empty($options['pending_only']));
|
||||
$manifest_rows = server_content_collect_manifest_addon_rows($db, (int)$home_info['home_id']);
|
||||
if (empty($workshop_ids) && empty($manifest_rows)) {
|
||||
$result = server_content_result('no_updates', 'No installed server content records were found for this home.', array(
|
||||
|
|
@ -251,12 +255,65 @@ function server_content_install_updates($home_id, $options = array())
|
|||
return $result;
|
||||
}
|
||||
|
||||
if (!empty($workshop_ids) && empty($options['check_only'])) {
|
||||
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, 'installing', null, false, false);
|
||||
if (!empty($workshop_ids)) {
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
|
||||
if ($server_xml === false) {
|
||||
return server_content_result('failed', 'Unable to load server XML configuration for Workshop update.');
|
||||
}
|
||||
$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.');
|
||||
}
|
||||
if (empty($options['check_only'])) {
|
||||
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, 'installing', null, false, false);
|
||||
}
|
||||
$error = '';
|
||||
$details = array();
|
||||
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, $workshop_action, $workshop_ids, $error, $manifest_context, $details);
|
||||
if (!$ok) {
|
||||
if (empty($options['check_only'])) {
|
||||
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, 'failed', $error, false, false);
|
||||
}
|
||||
return server_content_result('failed', $error, $details);
|
||||
}
|
||||
if (empty($options['check_only'])) {
|
||||
$final_state = ($workshop_action === 'download_only') ? 'downloaded' : 'installed';
|
||||
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, $final_state, null, false, true);
|
||||
if (!empty($options['pending_only'])) {
|
||||
scm_workshop_set_update_policy($db, (int)$home_info['home_id'], $workshop_ids, 'manual');
|
||||
}
|
||||
if ($workshop_action === 'download_only') {
|
||||
scm_workshop_set_update_policy($db, (int)$home_info['home_id'], $workshop_ids, 'install_on_restart');
|
||||
}
|
||||
scm_workshop_record_catalog_items($db, (string)$manifest_context['workshop_app_id'], $workshop_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true);
|
||||
}
|
||||
server_content_record_installed_content_state($home_info, array(
|
||||
'home_id' => (int)$home_info['home_id'],
|
||||
'last_action' => (string)$workshop_action,
|
||||
'last_result' => 'success',
|
||||
'last_manifest' => isset($details['manifest_path']) ? $details['manifest_path'] : '',
|
||||
'last_updated_at' => date('Y-m-d H:i:s'),
|
||||
'installed_workshop_ids' => $workshop_ids,
|
||||
));
|
||||
if (empty($manifest_rows) || !empty($options['workshop_only'])) {
|
||||
if (!empty($options['check_only'])) {
|
||||
return server_content_result('success', 'Workshop update check completed.', array(
|
||||
'workshop_items' => count($workshop_ids),
|
||||
'manifest_path' => isset($details['manifest_path']) ? $details['manifest_path'] : '',
|
||||
'log_path' => isset($details['log_path']) ? $details['log_path'] : '',
|
||||
));
|
||||
}
|
||||
return server_content_result('success', 'Workshop action completed.', array(
|
||||
'workshop_action' => $workshop_action,
|
||||
'workshop_items' => count($workshop_ids),
|
||||
'manifest_path' => isset($details['manifest_path']) ? $details['manifest_path'] : '',
|
||||
'log_path' => isset($details['log_path']) ? $details['log_path'] : '',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$manifest_items = array(
|
||||
'workshop_item_ids' => $workshop_ids,
|
||||
'workshop_item_ids' => array(),
|
||||
'manifest_addons' => $manifest_rows,
|
||||
);
|
||||
$manifest_path = server_content_build_manifest($home_info['home_id'], 'server_content', $workshop_action, $manifest_items, $options);
|
||||
|
|
@ -276,9 +333,6 @@ function server_content_install_updates($home_id, $options = array())
|
|||
return $execute;
|
||||
}
|
||||
|
||||
if (!empty($workshop_ids) && empty($options['check_only'])) {
|
||||
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, 'installed', null, false, true);
|
||||
}
|
||||
server_content_record_installed_content_state($home_info, array(
|
||||
'home_id' => (int)$home_info['home_id'],
|
||||
'last_action' => (string)$workshop_action,
|
||||
|
|
@ -447,6 +501,10 @@ function server_content_run_scheduled_action($home_id, $action, $options = array
|
|||
'server_content_notify_updates_only' => 'server_content_check_updates',
|
||||
'server_content_validate_files' => 'server_content_update_workshop',
|
||||
'server_content_backup_before_update' => 'server_content_install_updates',
|
||||
'workshop_update' => 'server_content_update_workshop',
|
||||
'workshop_update_and_restart' => 'server_content_install_updates_and_restart',
|
||||
'workshop_download_only' => 'server_content_update_workshop',
|
||||
'workshop_install_pending_on_restart' => 'server_content_install_updates',
|
||||
);
|
||||
if (!isset($handlers[$action]) || !function_exists($handlers[$action])) {
|
||||
$result = server_content_result('failed', 'Unsupported scheduled server content action.', array(
|
||||
|
|
@ -463,9 +521,22 @@ function server_content_run_scheduled_action($home_id, $action, $options = array
|
|||
if ($action === 'server_content_backup_before_update') {
|
||||
$options['backup_before_update'] = true;
|
||||
}
|
||||
if ($action === 'server_content_install_updates_and_restart' && !isset($options['safe_restart'])) {
|
||||
if ($action === 'server_content_install_updates_and_restart' || $action === 'workshop_update_and_restart') {
|
||||
$options['workshop_action'] = 'update';
|
||||
}
|
||||
if (in_array($action, array('workshop_update', 'workshop_update_and_restart', 'workshop_download_only', 'workshop_install_pending_on_restart'), true)) {
|
||||
$options['workshop_only'] = true;
|
||||
}
|
||||
if (($action === 'server_content_install_updates_and_restart' || $action === 'workshop_update_and_restart') && !isset($options['safe_restart'])) {
|
||||
$options['safe_restart'] = true;
|
||||
}
|
||||
if ($action === 'workshop_download_only') {
|
||||
$options['workshop_action'] = 'download_only';
|
||||
}
|
||||
if ($action === 'workshop_install_pending_on_restart') {
|
||||
$options['workshop_action'] = 'update';
|
||||
$options['pending_only'] = true;
|
||||
}
|
||||
if ($action === 'server_content_notify_updates_only') {
|
||||
$options['notify_only'] = true;
|
||||
$options['check_only'] = true;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ function scm_ensure_workshop_schema($db)
|
|||
'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`",
|
||||
'update_policy' => "VARCHAR(64) NOT NULL DEFAULT 'manual' AFTER `load_order`",
|
||||
'pending_action' => "VARCHAR(64) NULL AFTER `update_policy`",
|
||||
);
|
||||
foreach ($workshop_columns as $col => $definition) {
|
||||
$escaped_col = $db->realEscapeSingle($col);
|
||||
|
|
@ -89,6 +91,27 @@ function scm_ensure_workshop_schema($db)
|
|||
}
|
||||
}
|
||||
|
||||
$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop_catalog` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`workshop_id` VARCHAR(64) NOT NULL,
|
||||
`app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`title` VARCHAR(255) NULL,
|
||||
`install_count` INT NOT NULL DEFAULT 0,
|
||||
`first_seen` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_installed` DATETIME NULL,
|
||||
`last_updated` DATETIME NULL,
|
||||
`published_date` DATETIME NULL,
|
||||
`tags` TEXT NULL,
|
||||
`game_key` VARCHAR(128) NULL,
|
||||
`local_cache_path` 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"
|
||||
);
|
||||
|
||||
return $ok;
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +155,144 @@ function scm_get_workshop_rows($db, $home_id)
|
|||
return is_array($rows) ? $rows : array();
|
||||
}
|
||||
|
||||
function scm_get_workshop_update_policies()
|
||||
{
|
||||
return array(
|
||||
'manual' => 'Manual update only',
|
||||
'scheduled' => 'Update on scheduled task',
|
||||
'update_now' => 'Update immediately',
|
||||
'update_and_restart' => 'Update immediately and restart',
|
||||
'download_only' => 'Download now, install later',
|
||||
'install_on_restart' => 'Install during next restart',
|
||||
);
|
||||
}
|
||||
|
||||
function scm_normalize_workshop_update_policy($policy)
|
||||
{
|
||||
$policy = trim((string)$policy);
|
||||
$policies = scm_get_workshop_update_policies();
|
||||
return isset($policies[$policy]) ? $policy : 'manual';
|
||||
}
|
||||
|
||||
function scm_workshop_set_enabled($db, $home_id, array $item_ids, $enabled)
|
||||
{
|
||||
if (empty($item_ids)) {
|
||||
return false;
|
||||
}
|
||||
$escaped_ids = array();
|
||||
foreach ($item_ids as $item_id) {
|
||||
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
|
||||
}
|
||||
return (bool)$db->query(
|
||||
"UPDATE `".OGP_DB_PREFIX."server_content_workshop`
|
||||
SET enabled=" . ((int)$enabled ? 1 : 0) . ", updated_at=NOW()
|
||||
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")"
|
||||
);
|
||||
}
|
||||
|
||||
function scm_workshop_set_update_policy($db, $home_id, array $item_ids, $policy)
|
||||
{
|
||||
if (empty($item_ids)) {
|
||||
return false;
|
||||
}
|
||||
$policy = scm_normalize_workshop_update_policy($policy);
|
||||
$pending = '';
|
||||
if ($policy === 'download_only') {
|
||||
$pending = 'install_on_restart';
|
||||
}
|
||||
elseif ($policy === 'install_on_restart') {
|
||||
$pending = 'install_on_restart';
|
||||
}
|
||||
$escaped_ids = array();
|
||||
foreach ($item_ids as $item_id) {
|
||||
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
|
||||
}
|
||||
return (bool)$db->query(
|
||||
"UPDATE `".OGP_DB_PREFIX."server_content_workshop`
|
||||
SET update_policy='" . $db->realEscapeSingle($policy) . "',
|
||||
pending_action=" . ($pending === '' ? "NULL" : "'" . $db->realEscapeSingle($pending) . "'") . ",
|
||||
updated_at=NOW()
|
||||
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")"
|
||||
);
|
||||
}
|
||||
|
||||
function scm_workshop_catalog_sort_sql($sort)
|
||||
{
|
||||
$sort = trim((string)$sort);
|
||||
$allowed = array(
|
||||
'name' => 'title ASC, workshop_id ASC',
|
||||
'install_count' => 'install_count DESC, last_installed DESC, workshop_id ASC',
|
||||
'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',
|
||||
);
|
||||
return isset($allowed[$sort]) ? $allowed[$sort] : $allowed['last_installed'];
|
||||
}
|
||||
|
||||
function scm_get_workshop_catalog_rows($db, $app_id = '', $sort = 'last_installed', $limit = 50)
|
||||
{
|
||||
if (!scm_ensure_workshop_schema($db)) {
|
||||
return array();
|
||||
}
|
||||
$where = '';
|
||||
$app_id = trim((string)$app_id);
|
||||
if ($app_id !== '' && preg_match('/^[0-9]+$/', $app_id)) {
|
||||
$where = "WHERE app_id='" . $db->realEscapeSingle($app_id) . "'";
|
||||
}
|
||||
$limit = (int)$limit;
|
||||
if ($limit <= 0 || $limit > 200) {
|
||||
$limit = 50;
|
||||
}
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM `".OGP_DB_PREFIX."server_content_workshop_catalog`
|
||||
{$where}
|
||||
ORDER BY " . scm_workshop_catalog_sort_sql($sort) . "
|
||||
LIMIT {$limit}"
|
||||
);
|
||||
return is_array($rows) ? $rows : array();
|
||||
}
|
||||
|
||||
function scm_workshop_record_catalog_items($db, $workshop_app_id, array $item_ids, array $home_info = array(), array $item_details = array(), $mark_update = false)
|
||||
{
|
||||
if (empty($item_ids) || !scm_ensure_workshop_schema($db)) {
|
||||
return false;
|
||||
}
|
||||
$workshop_app_id = preg_match('/^[0-9]+$/', (string)$workshop_app_id) ? (string)$workshop_app_id : '';
|
||||
$game_key = isset($home_info['game_key']) ? (string)$home_info['game_key'] : '';
|
||||
foreach ($item_ids as $item_id) {
|
||||
$item_id = (string)$item_id;
|
||||
if (!preg_match('/^[0-9]+$/', $item_id)) {
|
||||
continue;
|
||||
}
|
||||
$detail = isset($item_details[$item_id]) && is_array($item_details[$item_id]) ? $item_details[$item_id] : array();
|
||||
$title = isset($detail['title']) ? (string)$detail['title'] : '';
|
||||
$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)
|
||||
VALUES (
|
||||
'".$db->realEscapeSingle($item_id)."',
|
||||
'".$db->realEscapeSingle($workshop_app_id)."',
|
||||
".($title === '' ? "NULL" : "'".$db->realEscapeSingle($title)."'").",
|
||||
1,
|
||||
NOW(),
|
||||
NOW(),
|
||||
".($mark_update ? "NOW()" : "NULL").",
|
||||
".($game_key === '' ? "NULL" : "'".$db->realEscapeSingle($game_key)."'").",
|
||||
".($install_path === '' ? "NULL" : "'".$db->realEscapeSingle($install_path)."'")."
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
install_count=install_count+1,
|
||||
title=IF(VALUES(title) IS NULL OR VALUES(title)='', title, VALUES(title)),
|
||||
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)),
|
||||
local_cache_path=IF(VALUES(local_cache_path) IS NULL OR VALUES(local_cache_path)='', local_cache_path, VALUES(local_cache_path))"
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function scm_extract_workshop_item_id($value)
|
||||
{
|
||||
$value = trim((string)$value);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ scm_workshop_test_assert(empty($invalid), 'valid mixed Workshop input has no inv
|
|||
$ids = scm_parse_workshop_ids("abc, https://steamcommunity.com/sharedfiles/filedetails/?id=0, 123", $invalid);
|
||||
scm_workshop_test_assert($ids === array('123'), 'keeps valid IDs when invalid values are present');
|
||||
scm_workshop_test_assert(count($invalid) === 2, 'reports invalid Workshop entries');
|
||||
scm_workshop_test_assert(scm_normalize_workshop_update_policy('download_only') === 'download_only', 'accepts download-only Workshop update policy');
|
||||
scm_workshop_test_assert(scm_normalize_workshop_update_policy('bad_policy') === 'manual', 'invalid Workshop update policy falls back to manual');
|
||||
scm_workshop_test_assert(strpos(scm_workshop_catalog_sort_sql('install_count'), 'install_count DESC') !== false, 'catalog sort supports install count');
|
||||
|
||||
$linuxHome = array(
|
||||
'home_id' => 10,
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
|
|||
return true;
|
||||
}
|
||||
|
||||
function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $raw_ids, array $selected_ids, &$message, &$is_error, $addon_id = 0)
|
||||
function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $raw_ids, array $selected_ids, &$message, &$is_error, $addon_id = 0, array $options = array())
|
||||
{
|
||||
$message = '';
|
||||
$is_error = true;
|
||||
|
|
@ -341,6 +341,7 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
|
|||
$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_record_catalog_items($db, $resolved_app_id, $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), 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 item(s) installed successfully. Manifest: '.scm_h(isset($details['manifest_path']) ? $details['manifest_path'] : '').' Log: '.scm_h(isset($details['log_path']) ? $details['log_path'] : '');
|
||||
|
|
@ -352,13 +353,39 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($action === 'update_selected' || $action === 'remove_selected') {
|
||||
if ($action === 'enable_selected' || $action === 'disable_selected' || $action === 'save_update_policy') {
|
||||
$item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids));
|
||||
if (empty($item_ids)) {
|
||||
$message = 'Select one or more saved Workshop IDs.';
|
||||
return false;
|
||||
}
|
||||
$target_action = ($action === 'remove_selected') ? 'remove' : 'update';
|
||||
if ($action === 'save_update_policy') {
|
||||
$policy = isset($options['update_policy']) ? (string)$options['update_policy'] : 'manual';
|
||||
if (!scm_workshop_set_update_policy($db, $home_id, $item_ids, $policy)) {
|
||||
$message = 'Failed to save Workshop update policy.';
|
||||
return false;
|
||||
}
|
||||
$is_error = false;
|
||||
$message = 'Workshop update policy saved for selected item(s).';
|
||||
return true;
|
||||
}
|
||||
$enabled = ($action === 'enable_selected') ? 1 : 0;
|
||||
if (!scm_workshop_set_enabled($db, $home_id, $item_ids, $enabled)) {
|
||||
$message = 'Failed to update Workshop enabled state.';
|
||||
return false;
|
||||
}
|
||||
$is_error = false;
|
||||
$message = $enabled ? 'Selected Workshop item(s) enabled.' : 'Selected Workshop item(s) disabled.';
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($action === 'update_selected' || $action === 'remove_selected' || $action === 'download_selected') {
|
||||
$item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids));
|
||||
if (empty($item_ids)) {
|
||||
$message = 'Select one or more saved Workshop IDs.';
|
||||
return false;
|
||||
}
|
||||
$target_action = ($action === 'remove_selected') ? 'remove' : (($action === 'download_selected') ? 'download_only' : '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 = '';
|
||||
|
|
@ -367,12 +394,24 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
|
|||
if ($ok) {
|
||||
if ($target_action === 'remove') {
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'removed', null, false, true);
|
||||
} elseif ($target_action === 'download_only') {
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'downloaded', null, false, true);
|
||||
scm_workshop_set_update_policy($db, $home_id, $item_ids, 'install_on_restart');
|
||||
scm_workshop_record_catalog_items($db, isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '', $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true);
|
||||
} else {
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true);
|
||||
scm_workshop_record_catalog_items($db, isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '', $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true);
|
||||
}
|
||||
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 item(s) removed.' : 'Selected Workshop item(s) updated successfully.') . ' Log: ' . scm_h(isset($details['log_path']) ? $details['log_path'] : '');
|
||||
if ($target_action === 'remove') {
|
||||
$message = 'Selected Workshop item(s) removed.';
|
||||
} elseif ($target_action === 'download_only') {
|
||||
$message = 'Selected Workshop item(s) downloaded and marked for install on next restart.';
|
||||
} else {
|
||||
$message = 'Selected Workshop item(s) updated successfully.';
|
||||
}
|
||||
$message .= ' 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);
|
||||
|
|
@ -404,6 +443,7 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r
|
|||
$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_record_catalog_items($db, isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '', $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), 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 item(s) updated successfully. Log: ' . scm_h(isset($details['log_path']) ? $details['log_path'] : '');
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ function exec_ogp_module() {
|
|||
$message = '';
|
||||
$is_error = false;
|
||||
$entered_ids = '';
|
||||
$catalog_sort = isset($_REQUEST['catalog_sort']) ? (string)$_REQUEST['catalog_sort'] : 'last_installed';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$posted_home_id = isset($_POST['home_id']) ? (int)$_POST['home_id'] : 0;
|
||||
|
|
@ -64,6 +65,9 @@ function exec_ogp_module() {
|
|||
$selected_ids = isset($_POST['selected_ids']) ? $_POST['selected_ids'] : array();
|
||||
$action = isset($_POST['workshop_action']) ? (string)$_POST['workshop_action'] : '';
|
||||
$posted_addon_id = isset($_POST['addon_id']) ? (int)$_POST['addon_id'] : 0;
|
||||
$options = array(
|
||||
'update_policy' => isset($_POST['update_policy']) ? (string)$_POST['update_policy'] : 'manual',
|
||||
);
|
||||
|
||||
if ($posted_home_id !== $home_id) {
|
||||
$is_error = true;
|
||||
|
|
@ -74,11 +78,14 @@ function exec_ogp_module() {
|
|||
$message = 'Invalid CSRF token for workshop action.';
|
||||
}
|
||||
else {
|
||||
scm_workshop_handle_action($db, $home_info, $user_id, $action, $entered_ids, (array)$selected_ids, $message, $is_error, $posted_addon_id > 0 ? $posted_addon_id : $addon_id);
|
||||
scm_workshop_handle_action($db, $home_info, $user_id, $action, $entered_ids, (array)$selected_ids, $message, $is_error, $posted_addon_id > 0 ? $posted_addon_id : $addon_id, $options);
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
$csrf_token = scm_get_csrf_token();
|
||||
|
||||
echo "<h2>Workshop Mods: " . scm_h($home_info['home_name']) . "</h2>";
|
||||
|
|
@ -135,6 +142,7 @@ function exec_ogp_module() {
|
|||
<th>Title</th>
|
||||
<th>Enabled</th>
|
||||
<th>Order</th>
|
||||
<th>Update Policy</th>
|
||||
<th>State</th>
|
||||
<th>Install Path</th>
|
||||
<th>Last Installed</th>
|
||||
|
|
@ -142,7 +150,7 @@ function exec_ogp_module() {
|
|||
<th>Last Error</th>
|
||||
</tr>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr><td colspan='10' class='info'>No Workshop items saved for this server yet.</td></tr>
|
||||
<tr><td colspan='11' class='info'>No Workshop items saved for this server yet.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ((array)$rows as $row): ?>
|
||||
<tr>
|
||||
|
|
@ -151,6 +159,7 @@ function exec_ogp_module() {
|
|||
<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(isset($row['update_policy']) ? $row['update_policy'] : 'manual'); ?></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>
|
||||
|
|
@ -163,13 +172,51 @@ function exec_ogp_module() {
|
|||
<br>
|
||||
<table class='center'>
|
||||
<tr>
|
||||
<td>
|
||||
<select name='update_policy'>
|
||||
<?php foreach (scm_get_workshop_update_policies() as $policy_key => $policy_label): ?>
|
||||
<option value='<?php echo scm_h($policy_key); ?>'><?php echo scm_h($policy_label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td><button type='submit' name='workshop_action' value='save_update_policy'>Save Policy</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='update_selected'>Update Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='download_selected'>Download Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='remove_selected'>Remove Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='enable_selected'>Enable Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='disable_selected'>Disable Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='update_all'>Update All</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</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>
|
||||
<table class='center'>
|
||||
<tr>
|
||||
<th>Workshop ID</th>
|
||||
<th><a href='?m=addonsmanager&p=workshop_content&home_id=<?php echo (int)$home_id; ?>&mod_id=<?php echo (int)$mod_id; ?>&ip=<?php echo scm_h($ip); ?>&port=<?php echo scm_h($port); ?>&addon_id=<?php echo (int)$addon_id; ?>&catalog_sort=name'>Name</a></th>
|
||||
<th><a href='?m=addonsmanager&p=workshop_content&home_id=<?php echo (int)$home_id; ?>&mod_id=<?php echo (int)$mod_id; ?>&ip=<?php echo scm_h($ip); ?>&port=<?php echo scm_h($port); ?>&addon_id=<?php echo (int)$addon_id; ?>&catalog_sort=install_count'>Install Count</a></th>
|
||||
<th><a href='?m=addonsmanager&p=workshop_content&home_id=<?php echo (int)$home_id; ?>&mod_id=<?php echo (int)$mod_id; ?>&ip=<?php echo scm_h($ip); ?>&port=<?php echo scm_h($port); ?>&addon_id=<?php echo (int)$addon_id; ?>&catalog_sort=published_date'>Published</a></th>
|
||||
<th><a href='?m=addonsmanager&p=workshop_content&home_id=<?php echo (int)$home_id; ?>&mod_id=<?php echo (int)$mod_id; ?>&ip=<?php echo scm_h($ip); ?>&port=<?php echo scm_h($port); ?>&addon_id=<?php echo (int)$addon_id; ?>&catalog_sort=last_updated'>Last Updated</a></th>
|
||||
<th><a href='?m=addonsmanager&p=workshop_content&home_id=<?php echo (int)$home_id; ?>&mod_id=<?php echo (int)$mod_id; ?>&ip=<?php echo scm_h($ip); ?>&port=<?php echo scm_h($port); ?>&addon_id=<?php echo (int)$addon_id; ?>&catalog_sort=last_installed'>Last Installed</a></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>
|
||||
<?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($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>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
|
||||
<form method='get' action=''>
|
||||
<input type='hidden' name='m' value='addonsmanager' />
|
||||
<input type='hidden' name='p' value='user_addons' />
|
||||
|
|
|
|||
|
|
@ -120,6 +120,10 @@ function get_server_content_scheduled_actions() {
|
|||
'server_content_update_all',
|
||||
'server_content_validate_files',
|
||||
'server_content_backup_before_update',
|
||||
'workshop_update',
|
||||
'workshop_update_and_restart',
|
||||
'workshop_download_only',
|
||||
'workshop_install_pending_on_restart',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -131,11 +135,20 @@ function build_cron_scheduler_command($panelURL, $token, $game_home, $action) {
|
|||
if(in_array($action, get_server_content_scheduled_actions()))
|
||||
{
|
||||
$options = array('triggered_by' => 'scheduler');
|
||||
if($action == 'server_content_install_updates_and_restart')
|
||||
if($action == 'server_content_install_updates_and_restart' || $action == 'workshop_update_and_restart')
|
||||
{
|
||||
$options['safe_restart'] = true;
|
||||
$options['restart_delay_seconds'] = 60;
|
||||
}
|
||||
if($action == 'workshop_download_only')
|
||||
{
|
||||
$options['workshop_action'] = 'download_only';
|
||||
}
|
||||
if($action == 'workshop_install_pending_on_restart')
|
||||
{
|
||||
$options['workshop_action'] = 'update';
|
||||
$options['pending_only'] = true;
|
||||
}
|
||||
$options_json = urlencode(json_encode($options));
|
||||
return "wget -qO- \"{$panelURL}/ogp_api.php?server_content/run_scheduled_action&token={$token}&home_id={$home_id}&action={$action}&options={$options_json}\" --no-check-certificate > /dev/null 2>&1";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ Primary categories:
|
|||
| updates | `steam_cmd`, `component_update`, `stop_update` |
|
||||
| system | `exec`, `sudo_exec`, `rebootnow`, `what_os`, `discover_ips`, `mon_stats` |
|
||||
| scheduler | `scheduler_add_task`, `scheduler_edit_task`, `scheduler_del_task`, `scheduler_list_tasks` |
|
||||
| content | `steam_workshop`, `get_workshop_mods_info` |
|
||||
| content | `writefile` + `exec` manifest/script orchestration; legacy `steam_workshop`, `get_workshop_mods_info` |
|
||||
|
||||
See the full command table in:
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ Most Panel modules do not build XML-RPC directly. They call `OGPRemoteLibrary`.
|
|||
| `remote_query()` | `remote_query` | `gamemanager`, dashboards |
|
||||
| `component_update()` | `component_update` | update/admin pages |
|
||||
| `scheduler_*()` | `scheduler_*` | `cron` |
|
||||
| `steam_workshop()` | `steam_workshop` | legacy workshop path |
|
||||
| `steam_workshop()` | `steam_workshop` | legacy workshop path only |
|
||||
|
||||
## External Panel API
|
||||
|
||||
|
|
@ -207,7 +207,25 @@ Common manifest fields:
|
|||
- `items`
|
||||
- `options`
|
||||
|
||||
For Workshop-specific manifests, the broader fields documented in `WORKSHOP_SYSTEM.md` also apply, including app IDs, install strategy, and target paths.
|
||||
For Workshop-specific manifests, the broader fields documented in `WORKSHOP_SYSTEM.md` also apply, including app IDs, install strategy, update policy, pending action, and target paths.
|
||||
|
||||
Current Workshop actions:
|
||||
|
||||
- `install`
|
||||
- `update`
|
||||
- `check_updates`
|
||||
- `download_only`
|
||||
- `validate_files`
|
||||
- `remove`
|
||||
|
||||
Current Scheduler/API action aliases:
|
||||
|
||||
- `workshop_update`
|
||||
- `workshop_update_and_restart`
|
||||
- `workshop_download_only`
|
||||
- `workshop_install_pending_on_restart`
|
||||
|
||||
The legacy `steam_workshop/install` API route and agent `steam_workshop` RPC remain compatibility-only. New user-facing Workshop work should use `addonsmanager` / Server Content Manager.
|
||||
|
||||
## Status Contract
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ Current preferred implementation path:
|
|||
|
||||
- `addonsmanager` stages a manifest and helper script through `writefile`
|
||||
- it executes the helper through `exec`
|
||||
- it records per-server items and policies in Panel database tables
|
||||
- it uses `steam_workshop` only as legacy compatibility, not as the primary workflow
|
||||
- no new Workshop-specific business logic should be added to agents for the current design
|
||||
|
||||
## Shell And System Commands
|
||||
|
||||
|
|
@ -172,6 +174,10 @@ The built-in action names handled by the Panel-generated API URLs are:
|
|||
- `server_content_update_all`
|
||||
- `server_content_validate_files`
|
||||
- `server_content_backup_before_update`
|
||||
- `workshop_update`
|
||||
- `workshop_update_and_restart`
|
||||
- `workshop_download_only`
|
||||
- `workshop_install_pending_on_restart`
|
||||
|
||||
## Panel Wrapper Map
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Status
|
||||
|
||||
Accepted, Phase 1 implementation active
|
||||
Accepted, Panel-side orchestration active
|
||||
|
||||
## Decision
|
||||
|
||||
|
|
@ -10,6 +10,8 @@ Accepted, Phase 1 implementation active
|
|||
|
||||
Phase 1 implements this decision by routing the user-facing Workshop install flow through `addonsmanager/workshop_content.php` and suppressing the standalone `steam_workshop` monitor button.
|
||||
|
||||
The current implementation keeps Workshop business logic in the Panel. Agents are generic executors: the Panel writes manifests, stages scripts, invokes `exec`, reads logs/results, and updates database state. New first-class Workshop subsystems should not be added to `Agent-Windows` or `Agent_Linux` unless a future decision explicitly changes this.
|
||||
|
||||
## Reasoning
|
||||
|
||||
- `addonsmanager` already has the richer schema and more complete product direction.
|
||||
|
|
@ -31,8 +33,12 @@ Phase 1 implements this decision by routing the user-facing Workshop install flo
|
|||
- Workshop input accepts numeric IDs or Steam URLs, then stores numeric IDs only.
|
||||
- Manifests are written under the server home in `gsp_server_content`.
|
||||
- Bundled Linux/Cygwin scripts are copied from the Panel module to an agent-managed folder under the server home before execution.
|
||||
- These scripts are Panel-owned deployment artifacts, not persistent agent features.
|
||||
- Default script names are treated as bundled handlers, not as existing agent paths.
|
||||
- Missing custom scripts fall back to bundled handlers and log the fallback.
|
||||
- Known Workshop items are cataloged in `server_content_workshop_catalog`.
|
||||
- Per-server item state, enablement, load order, update policy, and pending action are stored in `server_content_workshop`.
|
||||
- Scheduler integrates via `workshop_update`, `workshop_update_and_restart`, `workshop_download_only`, and `workshop_install_pending_on_restart`.
|
||||
- Generic content installs under `{SERVER_ROOT}/workshop/{MOD_FOLDER}` by default.
|
||||
- DayZ/Arma-style installs default to `@<workshop_id>` folders and copy `.bikey` files into `keys` when present.
|
||||
- Startup parameter generation remains a later phase.
|
||||
|
|
@ -40,6 +46,21 @@ Phase 1 implements this decision by routing the user-facing Workshop install flo
|
|||
- The Panel helpers read `workshop_support` first, then tolerate older direct tags only as compatibility fallbacks.
|
||||
- Arma 3 Linux and Windows configs declare Workshop app ID `107410` through `workshop_support`.
|
||||
|
||||
## Reference Module Findings
|
||||
|
||||
The old `reference/Module-Steam_Workshop` module stored per-game profile XML under its own module folder, required admin-managed Workshop mod lists for some workflows, called dedicated agent RPCs such as `steam_workshop`, and used regex/profile rules to mutate game config values. Useful lessons retained:
|
||||
|
||||
- Customers should be able to paste one or more Workshop IDs or URLs.
|
||||
- The Panel should store per-server installed Workshop rows.
|
||||
- SteamCMD command generation should be controlled by admin/game configuration, not customer shell text.
|
||||
|
||||
Rejected legacy behavior:
|
||||
|
||||
- making `steam_workshop` the primary user module
|
||||
- requiring admins to pre-enter every Workshop item ID
|
||||
- mutating base game XML per customer install
|
||||
- adding new Workshop-specific agent business logic
|
||||
|
||||
## Validation
|
||||
|
||||
Current validation commands:
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ The agent stores the cron entry and executes it locally.
|
|||
| `server_content_update_all` | same | same | `cron`, `addonsmanager` | aggregate update flow |
|
||||
| `server_content_validate_files` | same | same | `cron`, `addonsmanager` | validation flow |
|
||||
| `server_content_backup_before_update` | same | same | `cron`, `addonsmanager`, backup-related helpers | backup hook before content update |
|
||||
| `workshop_update` | same | same | `cron`, `addonsmanager` | Server Content Manager Workshop update flow |
|
||||
| `workshop_update_and_restart` | same | same | `cron`, `addonsmanager`, `gamemanager` | Workshop update plus safe restart request |
|
||||
| `workshop_download_only` | same | same | `cron`, `addonsmanager` | SteamCMD download/cache without copying into live mod folders |
|
||||
| `workshop_install_pending_on_restart` | same | same | `cron`, `addonsmanager` | Install Workshop items marked `pending_action=install_on_restart` |
|
||||
|
||||
## Agent Scheduler RPCs
|
||||
|
||||
|
|
|
|||
|
|
@ -33,3 +33,35 @@ Important references:
|
|||
- clear logs and results
|
||||
- no customer raw shell commands by default
|
||||
|
||||
## Server Content / Workshop Actions
|
||||
|
||||
The Scheduler must use Server Content Manager actions for Workshop automation. Do not build a separate Workshop scheduler.
|
||||
|
||||
Current Server Content actions:
|
||||
|
||||
| Action Key | Purpose |
|
||||
|---|---|
|
||||
| `server_content_check_updates` | Check content update state. |
|
||||
| `server_content_check_workshop_updates` | Check Workshop update state. |
|
||||
| `server_content_install_updates_if_stopped` | Install only when the server is stopped. |
|
||||
| `server_content_install_updates_next_restart` | Queue updates for the next restart. |
|
||||
| `server_content_install_updates_now` | Install available updates immediately. |
|
||||
| `server_content_install_updates_and_restart` | Install updates, wait, then restart. |
|
||||
| `server_content_update_all` | Update all server content records. |
|
||||
| `server_content_validate_files` | Validate server content files. |
|
||||
| `server_content_backup_before_update` | Backup before applying updates. |
|
||||
|
||||
Workshop-specific aliases:
|
||||
|
||||
| Action Key | Purpose |
|
||||
|---|---|
|
||||
| `workshop_update` | Update installed Workshop items through Server Content Manager. |
|
||||
| `workshop_update_and_restart` | Update Workshop items and request a safe restart. |
|
||||
| `workshop_download_only` | Download/cache Workshop items without installing into live folders. |
|
||||
| `workshop_install_pending_on_restart` | Install items marked as pending during the restart/maintenance window. |
|
||||
|
||||
Implementation references:
|
||||
|
||||
- `Panel/modules/cron/shared_cron_functions.php`
|
||||
- `Panel/modules/addonsmanager/server_content_actions.php`
|
||||
- `Panel/modules/addonsmanager/workshop_action.php`
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ Active workflow:
|
|||
3. Open the `Steam Workshop Mods` category.
|
||||
4. Paste one or more Steam Workshop URLs or numeric Workshop IDs.
|
||||
5. Click `Install / Queue`.
|
||||
6. The Panel validates input, stores numeric Workshop IDs, writes a manifest, syncs the install script, executes it through the agent, and shows the result.
|
||||
6. The Panel validates input, stores numeric Workshop IDs, writes a manifest, syncs the install script, executes it through the agent's existing `exec` primitive, and shows the result.
|
||||
7. Installed items can be enabled/disabled, updated, removed, downloaded without immediate install, assigned an update policy, and later used by Scheduler actions.
|
||||
|
||||
Accepted input examples:
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ The admin template defines capability and policy. The customer supplies only Wor
|
|||
- or `{SERVER_HOME}/gsp_server_content/scripts/workshop/generic_steam_workshop_windows_cygwin.sh`
|
||||
6. The agent runs:
|
||||
- `bash <script> <manifest>`
|
||||
7. The script runs SteamCMD, copies downloaded content into the target mod folder, logs output, and copies `.bikey` files for DayZ/Arma-style strategies.
|
||||
7. The script runs SteamCMD, copies downloaded content into the target mod folder when the action requires install, logs output, and copies `.bikey` files for DayZ/Arma-style strategies.
|
||||
|
||||
Current repair:
|
||||
|
||||
|
|
@ -65,6 +66,8 @@ Current repair:
|
|||
- Missing custom scripts fall back to the bundled generic handler and log a warning.
|
||||
- Generic installs default to `{SERVER_ROOT}/workshop/{MOD_FOLDER}`.
|
||||
- DayZ/Arma installs keep `{SERVER_ROOT}/{MOD_FOLDER}` for `@<workshop_id>` compatibility.
|
||||
- `download_only` and `validate_files` are accepted script actions and do not copy into live mod folders.
|
||||
- `remove` does not require SteamCMD.
|
||||
|
||||
## Manifest Fields
|
||||
|
||||
|
|
@ -121,6 +124,8 @@ Key-copy behavior:
|
|||
- `install_strategy`
|
||||
- `enabled`
|
||||
- `load_order`
|
||||
- `update_policy`
|
||||
- `pending_action`
|
||||
- `install_state`
|
||||
- `last_installed_at`
|
||||
- `last_updated_at`
|
||||
|
|
@ -131,9 +136,40 @@ Phase 1 states:
|
|||
- `queued`
|
||||
- `installing`
|
||||
- `installed`
|
||||
- `downloaded`
|
||||
- `failed`
|
||||
- `removed`
|
||||
|
||||
`server_content_workshop_catalog` tracks known/common items:
|
||||
|
||||
- `workshop_id`
|
||||
- `app_id`
|
||||
- `title`
|
||||
- `install_count`
|
||||
- `first_seen`
|
||||
- `last_installed`
|
||||
- `last_updated`
|
||||
- `published_date`
|
||||
- `tags`
|
||||
- `game_key`
|
||||
- `local_cache_path`
|
||||
|
||||
Supported update policies:
|
||||
|
||||
- `manual`
|
||||
- `scheduled`
|
||||
- `update_now`
|
||||
- `update_and_restart`
|
||||
- `download_only`
|
||||
- `install_on_restart`
|
||||
|
||||
Scheduler action keys:
|
||||
|
||||
- `workshop_update`
|
||||
- `workshop_update_and_restart`
|
||||
- `workshop_download_only`
|
||||
- `workshop_install_pending_on_restart`
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Customer input is reduced to numeric Workshop IDs only.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,15 @@ Important files:
|
|||
- `Panel/modules/steam_workshop/module.php`
|
||||
- `Panel/modules/steam_workshop/agent_update_workshop.php`
|
||||
|
||||
## Phase 1 Implemented Behavior
|
||||
## Current Implemented Behavior
|
||||
|
||||
Workshop is Panel-side orchestration. The active workflow lives in Server Content Manager and uses existing agent primitives only:
|
||||
|
||||
- Panel writes manifests with validated numeric Workshop IDs.
|
||||
- Panel deploys an OS-appropriate bundled handler script into the server home.
|
||||
- Panel invokes the handler through the existing authenticated agent `exec` RPC.
|
||||
- Agents do not need new Workshop-specific business logic.
|
||||
- Legacy agent RPCs from the old `steam_workshop` module remain compatibility-only and are not the primary path.
|
||||
|
||||
The active user workflow is now `addonsmanager` -> `workshop_content`.
|
||||
|
||||
|
|
@ -39,7 +47,7 @@ The Panel syncs the bundled install script to:
|
|||
- `{SERVER_HOME}/gsp_server_content/scripts/workshop/generic_steam_workshop_linux.sh`
|
||||
- `{SERVER_HOME}/gsp_server_content/scripts/workshop/generic_steam_workshop_windows_cygwin.sh`
|
||||
|
||||
The agent executes the synced script with the manifest path. Customers do not need to place scripts manually on the agent.
|
||||
The agent executes the synced script with the manifest path by using the existing generic command execution path. Customers do not need to place scripts manually on the agent.
|
||||
|
||||
Script selection rules:
|
||||
|
||||
|
|
@ -108,6 +116,8 @@ The Panel helper parser reads `workshop_support` first. Older direct tags are to
|
|||
- `install_strategy`
|
||||
- `enabled`
|
||||
- `load_order`
|
||||
- `update_policy`
|
||||
- `pending_action`
|
||||
- `install_state`
|
||||
- `last_installed_at`
|
||||
- `last_updated_at`
|
||||
|
|
@ -118,9 +128,26 @@ Current install states used by Phase 1:
|
|||
- `queued`
|
||||
- `installing`
|
||||
- `installed`
|
||||
- `downloaded`
|
||||
- `failed`
|
||||
- `removed`
|
||||
|
||||
`server_content_workshop_catalog` tracks known/common Workshop items seen through Server Content Manager:
|
||||
|
||||
- `workshop_id`
|
||||
- `app_id`
|
||||
- `title`
|
||||
- `install_count`
|
||||
- `first_seen`
|
||||
- `last_installed`
|
||||
- `last_updated`
|
||||
- `published_date`
|
||||
- `tags`
|
||||
- `game_key`
|
||||
- `local_cache_path`
|
||||
|
||||
The catalog is Panel-side and does not require Steam Web API metadata. Metadata can be added later.
|
||||
|
||||
## What Exists Today
|
||||
|
||||
The current direction already supports:
|
||||
|
|
@ -136,12 +163,39 @@ The current direction already supports:
|
|||
## Main Limitations
|
||||
|
||||
- Workshop metadata is still incomplete.
|
||||
- load order is tracked but not yet a full drag-and-drop or startup-param UX concept.
|
||||
- enable/disable is stored but does not yet regenerate startup parameters.
|
||||
- Load order is tracked but not yet a full drag-and-drop or startup-param UX concept.
|
||||
- Enable/disable is exposed and stored but does not yet regenerate startup parameters.
|
||||
- update/remove are synchronous and should become background jobs.
|
||||
- caching and cleanup policy need product-level design, not just ad hoc scripts.
|
||||
- `-mod=` / `-serverMod=` generation still needs a safe structured implementation.
|
||||
|
||||
## Scheduler Integration
|
||||
|
||||
Workshop updates use the existing `cron` / Scheduler system. No second Workshop scheduler should be created.
|
||||
|
||||
Supported scheduler action keys:
|
||||
|
||||
- `workshop_update`
|
||||
- `workshop_update_and_restart`
|
||||
- `workshop_download_only`
|
||||
- `workshop_install_pending_on_restart`
|
||||
|
||||
Compatibility Server Content keys remain available:
|
||||
|
||||
- `server_content_check_workshop_updates`
|
||||
- `server_content_update_workshop`
|
||||
- `server_content_install_updates_next_restart`
|
||||
- `server_content_install_updates_and_restart`
|
||||
|
||||
Per-item update policy values stored on `server_content_workshop.update_policy`:
|
||||
|
||||
- `manual`
|
||||
- `scheduled`
|
||||
- `update_now`
|
||||
- `update_and_restart`
|
||||
- `download_only`
|
||||
- `install_on_restart`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Meaning | Fix |
|
||||
|
|
@ -192,3 +246,12 @@ Important manifest fields:
|
|||
- `extra.keys_target_path`
|
||||
|
||||
Both bundled handlers validate numeric item IDs, keep writes under the server home, use SteamCMD, copy files into the resolved target path, and copy `.bikey` files for DayZ/Arma strategies when enabled.
|
||||
|
||||
Bundled handler actions:
|
||||
|
||||
- `install` - download with SteamCMD, copy/install into target path.
|
||||
- `update` - validate/download with SteamCMD, copy/install into target path.
|
||||
- `check_updates` - validate/download only; does not alter live mod folders.
|
||||
- `download_only` - download/cache only and leave install pending.
|
||||
- `validate_files` - SteamCMD validate/download only.
|
||||
- `remove` - move the installed target folder into `gsp_server_content/workshop/removed/`; this does not require SteamCMD.
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ XML definitions also feed:
|
|||
|
||||
Workshop-enabled games must use the canonical `workshop_support` block. Loose top-level tags such as `workshop_app_id` are compatibility parser fallbacks only and should not be used in new game XML because schema validation is intentionally strict.
|
||||
|
||||
The `workshop_support` block is a capability declaration only. It does not install mods by itself and it does not create an agent-side Workshop subsystem. Server Content Manager reads these values, writes a per-server manifest, stages the Panel-bundled handler, and calls the agent's existing generic execution primitives.
|
||||
|
||||
Example:
|
||||
|
||||
```xml
|
||||
|
|
@ -128,6 +130,12 @@ Supported `install_strategy` values:
|
|||
|
||||
`workshop_app_id` is the Steam Workshop app ID used by `steamcmd +workshop_download_item`. It is not automatically the same as a dedicated server installer app ID. For Arma 3, Workshop content uses `107410` while the dedicated server installer remains defined on the normal mod installer entry.
|
||||
|
||||
Ordering rule:
|
||||
|
||||
- `workshop_support` belongs after `game_name` and before `server_exec_name` in the current schema sequence.
|
||||
- New XML files should not add top-level Workshop tags.
|
||||
- If `install_path` is omitted, Server Content Manager defaults to `{SERVER_ROOT}/workshop/{MOD_FOLDER}` or `{SERVER_ROOT}/{MOD_FOLDER}` for DayZ/Arma strategies.
|
||||
|
||||
The current XML schema is validated by:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ Known tables used by the module:
|
|||
|
||||
- `addons`
|
||||
- `server_content_workshop`
|
||||
- `server_content_workshop_catalog`
|
||||
- `server_content_manifest`
|
||||
- `server_content_install_history`
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ The module can already represent several content types, including:
|
|||
- config packs
|
||||
- future profile-type content
|
||||
|
||||
For Workshop items, the current flow lets users enter Workshop IDs or full Steam Workshop URLs and routes the install through module pages and agent-side scripts.
|
||||
For Workshop items, the current flow lets users enter Workshop IDs or full Steam Workshop URLs and routes the install through module pages, staged manifests, and Panel-bundled scripts executed through existing agent primitives.
|
||||
|
||||
## Workshop Phase 1 Flow
|
||||
|
||||
|
|
@ -47,9 +48,11 @@ For Workshop items, the current flow lets users enter Workshop IDs or full Steam
|
|||
6. Panel parses IDs, rejects invalid entries, and records rows in `server_content_workshop`.
|
||||
7. Panel writes a manifest to `{SERVER_HOME}/gsp_server_content/workshop_manifest.json`.
|
||||
8. Panel syncs the bundled Linux or Cygwin script into `{SERVER_HOME}/gsp_server_content/scripts/workshop/`.
|
||||
9. Agent executes the script with the manifest path.
|
||||
9. Agent executes the script with the manifest path through the existing authenticated `exec` RPC.
|
||||
10. Script runs SteamCMD, copies Workshop content into the configured target path, copies DayZ/Arma `.bikey` files when applicable, and writes a log under `gsp_server_content`.
|
||||
|
||||
The agents are intentionally generic executors in this design. New Workshop business logic should not be added to `Agent-Windows` or `Agent_Linux`; use `remote_writefile`, `exec`, log reads, and normal start/stop/restart primitives instead.
|
||||
|
||||
Current script fallback behavior:
|
||||
|
||||
- Admin-defined custom scripts are supported when they exist on the agent.
|
||||
|
|
@ -92,10 +95,34 @@ SteamCMD requirements:
|
|||
|
||||
The legacy `steam_workshop` monitor button is intentionally suppressed so users are not sent to the deprecated standalone module.
|
||||
|
||||
## Current User Controls
|
||||
|
||||
The `workshop_content.php` page supports:
|
||||
|
||||
- direct install by numeric ID or Steam Workshop URL
|
||||
- installed item list
|
||||
- enable/disable selected items
|
||||
- update selected items
|
||||
- remove selected items
|
||||
- download selected items without installing immediately
|
||||
- update all saved Workshop items
|
||||
- per-item update policy storage
|
||||
- known/common item catalog sorted by name, install count, published date, last updated, or last installed
|
||||
|
||||
Update policies are stored as data for Scheduler/automation:
|
||||
|
||||
- `manual`
|
||||
- `scheduled`
|
||||
- `update_now`
|
||||
- `update_and_restart`
|
||||
- `download_only`
|
||||
- `install_on_restart`
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- Workshop and content metadata is still partial.
|
||||
- Load order and enable/disable are tracked but not wired into startup-parameter generation yet.
|
||||
- Load order is tracked but not yet reorderable through a polished drag-and-drop UI.
|
||||
- Enable/disable is tracked but not wired into startup-parameter generation yet.
|
||||
- Async install job progress should be more visible.
|
||||
- Install strategies are still being broadened and need consistent game-specific rules.
|
||||
- DayZ/Arma style key-copy is implemented for Phase 1; startup-param behavior still needs a stronger canonical implementation.
|
||||
|
|
|
|||
|
|
@ -80,6 +80,12 @@ Validate changes with:
|
|||
php Panel/modules/config_games/tests/validate_server_configs.php
|
||||
```
|
||||
|
||||
Important rules:
|
||||
|
||||
- Place `workshop_support` after `game_name` and before `server_exec_name`.
|
||||
- Do not add loose top-level tags such as `workshop_app_id`; helper code may tolerate them for old configs, but schema-valid XML should use the canonical block.
|
||||
- XML declares capability only. Server Content Manager owns the Panel-side install orchestration and uses agents only for generic file/command execution.
|
||||
|
||||
## Suggested Future Improvements
|
||||
|
||||
- extend XML capability model
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue