codex steam workshop integrarion
This commit is contained in:
parent
3cefad183d
commit
c687165132
12 changed files with 629 additions and 84 deletions
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
);
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 463939057 ...'><?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 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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&p=user&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.
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
Accepted, Phase 1 implementation started
|
||||
|
||||
## Decision
|
||||
|
||||
`Panel/modules/addonsmanager` should remain the primary future home for Workshop items, mods, add-ons, and server content. `steam_workshop` should remain a deprecated compatibility layer only.
|
||||
|
||||
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.
|
||||
|
||||
## Reasoning
|
||||
|
||||
- `addonsmanager` already has the richer schema and more complete product direction.
|
||||
|
|
@ -24,3 +26,10 @@ Accepted
|
|||
- `steam_workshop` is explicitly deprecated in the codebase.
|
||||
- Separate modules would fragment user workflows and duplicate install logic.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- 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.
|
||||
- DayZ/Arma-style installs default to `@<workshop_id>` folders and copy `.bikey` files into `keys` when present.
|
||||
- Startup parameter generation remains a later phase.
|
||||
|
|
|
|||
167
docs/features/WORKSHOP_PHASE1_IMPLEMENTATION.md
Normal file
167
docs/features/WORKSHOP_PHASE1_IMPLEMENTATION.md
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
# Workshop Phase 1 Implementation
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 1 makes the active Server Content Manager Workshop path usable for DayZ/Arma-style Workshop installs without reviving the deprecated standalone `steam_workshop` user workflow.
|
||||
|
||||
Active workflow:
|
||||
|
||||
`Game Monitor` -> `Server Content` -> `Steam Workshop Mods`
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `Panel/modules/addonsmanager/server_content_helpers.php`
|
||||
- `Panel/modules/addonsmanager/workshop_action.php`
|
||||
- `Panel/modules/addonsmanager/workshop_content.php`
|
||||
- `Panel/modules/addonsmanager/addons_manager.php`
|
||||
- `Panel/modules/addonsmanager/module.php`
|
||||
- `Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh`
|
||||
- `Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh`
|
||||
- `Panel/modules/steam_workshop/monitor_buttons.php`
|
||||
|
||||
## User Flow
|
||||
|
||||
1. Open a server from the Panel.
|
||||
2. Click `Server Content`.
|
||||
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.
|
||||
|
||||
Accepted input examples:
|
||||
|
||||
- `450814997`
|
||||
- `https://steamcommunity.com/sharedfiles/filedetails/?id=450814997`
|
||||
|
||||
Invalid text is rejected before reaching the manifest or shell script.
|
||||
|
||||
## Admin Flow
|
||||
|
||||
1. Create a Server Content template in `addonsmanager`.
|
||||
2. Set content type to `Steam Workshop Mods`.
|
||||
3. Configure `Workshop App ID` when the game XML/profile does not provide it.
|
||||
4. Leave `Default Workshop IDs` blank for normal user-supplied installs.
|
||||
5. Optionally configure a target path template.
|
||||
|
||||
The admin template defines capability and policy. The customer supplies only Workshop item IDs or URLs.
|
||||
|
||||
## Install Flow
|
||||
|
||||
1. `workshop_content.php` receives the form post.
|
||||
2. `workshop_action.php` parses URLs/IDs and records rows in `server_content_workshop`.
|
||||
3. The Panel builds a manifest with per-item install details.
|
||||
4. The manifest is written to:
|
||||
- `{SERVER_HOME}/gsp_server_content/workshop_manifest.json`
|
||||
5. The correct bundled script is copied to:
|
||||
- `{SERVER_HOME}/gsp_server_content/scripts/workshop/generic_steam_workshop_linux.sh`
|
||||
- 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.
|
||||
|
||||
## Manifest Fields
|
||||
|
||||
Phase 1 manifests include:
|
||||
|
||||
- `manifest_version`
|
||||
- `action`
|
||||
- `home_id`
|
||||
- `home_cfg_id`
|
||||
- `game_path`
|
||||
- `server_path`
|
||||
- `workshop_app_id`
|
||||
- `steam_app_id`
|
||||
- `items`
|
||||
- `item_details`
|
||||
- `install_strategy`
|
||||
- `target_path`
|
||||
- `extra`
|
||||
|
||||
Each `item_details` entry includes:
|
||||
|
||||
- `workshop_item_id`
|
||||
- `folder_name`
|
||||
- `target_path_template`
|
||||
- `target_path_resolved`
|
||||
- `install_strategy`
|
||||
- `copy_keys`
|
||||
- `keys_target_path`
|
||||
|
||||
## DayZ / Arma Behavior
|
||||
|
||||
The Panel detects `dayz_mod_folder` or `arma_mod_folder` from the game key/name/config file when no explicit strategy exists.
|
||||
|
||||
Default install folder:
|
||||
|
||||
- `@<workshop_id>`
|
||||
|
||||
Key-copy behavior:
|
||||
|
||||
- `.bikey` files found anywhere in the installed mod folder are copied to `{SERVER_HOME}/keys`.
|
||||
- Missing `.bikey` files are logged and do not fail the install.
|
||||
|
||||
## Database Tracking
|
||||
|
||||
`server_content_workshop` now tracks:
|
||||
|
||||
- `content_id`
|
||||
- `install_path`
|
||||
- `install_strategy`
|
||||
- `enabled`
|
||||
- `load_order`
|
||||
- `install_state`
|
||||
- `last_installed_at`
|
||||
- `last_updated_at`
|
||||
- `last_error`
|
||||
|
||||
Phase 1 states:
|
||||
|
||||
- `queued`
|
||||
- `installing`
|
||||
- `installed`
|
||||
- `failed`
|
||||
- `removed`
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Customer input is reduced to numeric Workshop IDs only.
|
||||
- Invalid text is rejected.
|
||||
- Target paths are generated from trusted templates and checked to stay under the server home.
|
||||
- Scripts are copied from the Panel module source into a GSP-controlled folder under the server home.
|
||||
- Customers do not supply shell commands.
|
||||
- Admin-defined post-install scripts remain possible but should be treated as trusted admin configuration only.
|
||||
- Steam credentials are not introduced by Phase 1; scripts use anonymous login.
|
||||
|
||||
## Startup Parameters
|
||||
|
||||
Phase 1 does not rewrite startup parameter generation.
|
||||
|
||||
Current DayZ/Arma XML files already expose user-editable `-mod=` / `-serverMod=` parameters through `server_params`.
|
||||
|
||||
Phase 2 should generate structured mod lists from enabled `server_content_workshop` rows ordered by `load_order`, avoid duplicate `-mod=` entries, and preserve existing user parameters.
|
||||
|
||||
## Validation Performed
|
||||
|
||||
- PHP syntax checks passed for changed PHP files.
|
||||
- Bash syntax checks passed for both bundled Workshop scripts.
|
||||
- Parser test confirmed:
|
||||
- numeric ID accepted
|
||||
- full Steam URL accepted
|
||||
- invalid text rejected
|
||||
- Temporary manifest test with fake SteamCMD confirmed:
|
||||
- Linux script installs into `@<workshop_id>`
|
||||
- Windows/Cygwin script installs into `@<workshop_id>`
|
||||
- `.bikey` files are copied to `keys`
|
||||
- useful logs are written under `gsp_server_content`
|
||||
|
||||
## Phase 2 Work
|
||||
|
||||
- Add real asynchronous install jobs and progress polling.
|
||||
- Resolve Workshop item titles/metadata.
|
||||
- Add load-order UI.
|
||||
- Wire enable/disable and load order into safe startup parameter generation.
|
||||
- Support separate client mods and server-only mods.
|
||||
- Add update-all and repair semantics that can preserve old installs until success.
|
||||
- Add cache policy and cleanup controls.
|
||||
- Add admin XML/schema fields for explicit install strategy and key-copy behavior.
|
||||
- Add Steam account credential support without leaking secrets.
|
||||
|
|
@ -14,9 +14,72 @@ Important files:
|
|||
- `Panel/modules/addonsmanager/addons_manager.php`
|
||||
- `Panel/modules/addonsmanager/workshop_content.php`
|
||||
- `Panel/modules/addonsmanager/workshop_action.php`
|
||||
- `Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh`
|
||||
- `Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh`
|
||||
- `Panel/modules/steam_workshop/module.php`
|
||||
- `Panel/modules/steam_workshop/agent_update_workshop.php`
|
||||
|
||||
## Phase 1 Implemented Behavior
|
||||
|
||||
The active user workflow is now `addonsmanager` -> `workshop_content`.
|
||||
|
||||
Users can enter either:
|
||||
|
||||
- a numeric Workshop item ID
|
||||
- a full Steam Workshop URL containing `id=<number>`
|
||||
|
||||
The Panel extracts and stores only numeric Workshop IDs. Invalid text is rejected before any manifest or shell command is built.
|
||||
|
||||
The Panel writes a manifest under the server home:
|
||||
|
||||
- `{SERVER_HOME}/gsp_server_content/workshop_manifest.json`
|
||||
|
||||
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 manifest includes:
|
||||
|
||||
- `home_id`
|
||||
- server/game path
|
||||
- `workshop_app_id`
|
||||
- Workshop item IDs
|
||||
- per-item target paths
|
||||
- install strategy
|
||||
- key-copy settings
|
||||
- content template metadata
|
||||
|
||||
DayZ/Arma-style installs default to `dayz_mod_folder` or `arma_mod_folder` based on the game key/name/config file. Those strategies install to `@<workshop_id>` by default and copy `.bikey` files into the server `keys` folder when found. Missing key files are logged but do not fail the install.
|
||||
|
||||
## Database State
|
||||
|
||||
`server_content_workshop` tracks:
|
||||
|
||||
- `content_id`
|
||||
- `home_id`
|
||||
- `workshop_app_id`
|
||||
- `workshop_item_id`
|
||||
- `title`
|
||||
- `install_path`
|
||||
- `install_strategy`
|
||||
- `enabled`
|
||||
- `load_order`
|
||||
- `install_state`
|
||||
- `last_installed_at`
|
||||
- `last_updated_at`
|
||||
- `last_error`
|
||||
|
||||
Current install states used by Phase 1:
|
||||
|
||||
- `queued`
|
||||
- `installing`
|
||||
- `installed`
|
||||
- `failed`
|
||||
- `removed`
|
||||
|
||||
## What Exists Today
|
||||
|
||||
The current direction already supports:
|
||||
|
|
@ -32,10 +95,11 @@ The current direction already supports:
|
|||
## Main Limitations
|
||||
|
||||
- Workshop metadata is still incomplete.
|
||||
- load order is not yet a full first-class UX concept.
|
||||
- update/uninstall/enable/disable flows need a cleaner product model.
|
||||
- DayZ/Arma-specific folder and key-copy behavior needs a stronger canonical path.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Recommended Mental Model
|
||||
|
||||
|
|
@ -50,4 +114,3 @@ Use `addonsmanager` as the main future home for:
|
|||
- install history
|
||||
|
||||
Treat `steam_workshop` as a legacy bridge for migration only.
|
||||
|
||||
|
|
|
|||
|
|
@ -35,15 +35,30 @@ The module can already represent several content types, including:
|
|||
- config packs
|
||||
- future profile-type content
|
||||
|
||||
For Workshop items, the current flow lets users enter IDs 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 and agent-side scripts.
|
||||
|
||||
## Workshop Phase 1 Flow
|
||||
|
||||
1. Admin creates a Server Content template with install method `steam_workshop`.
|
||||
2. Admin configures the Workshop App ID on the template or relies on the game XML/profile fallback.
|
||||
3. User opens `Server Content` from the game monitor.
|
||||
4. User selects the Steam Workshop Mods category.
|
||||
5. User enters one or more Workshop URLs or numeric IDs.
|
||||
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.
|
||||
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 legacy `steam_workshop` monitor button is intentionally suppressed so users are not sent to the deprecated standalone module.
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- Workshop and content metadata is still partial.
|
||||
- Load order and enable/disable behavior need a cleaner first-class model.
|
||||
- Load order and enable/disable are 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 and startup-param behavior needs a stronger canonical implementation.
|
||||
- DayZ/Arma style key-copy is implemented for Phase 1; startup-param behavior still needs a stronger canonical implementation.
|
||||
- Cache and cleanup policy need a clearer product design.
|
||||
|
||||
## Where To Start Reading
|
||||
|
|
@ -67,4 +82,3 @@ This module is the right place for:
|
|||
- install history
|
||||
|
||||
The old `steam_workshop` module should be treated as a deprecated compatibility layer, not the main future path.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue