patch workshop

This commit is contained in:
Frank Harris 2026-06-09 09:33:53 -05:00
parent c8287596b5
commit 126c12b04e
4 changed files with 701 additions and 95 deletions

View file

@ -1,133 +1,119 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -u set -u
MANIFEST="${1:-}" MANIFEST="${1:-}"
if [ -z "$MANIFEST" ] || [ ! -f "$MANIFEST" ]; then if [ -z "$MANIFEST" ] || [ ! -f "$MANIFEST" ]; then
echo "ERROR: Workshop manifest missing: $MANIFEST" echo "ERROR: Workshop manifest missing: $MANIFEST"
exit 2 exit 2
fi fi
MANIFEST_DIR="$(dirname "$MANIFEST")" MANIFEST_DIR="$(dirname "$MANIFEST")"
LOG="$MANIFEST_DIR/workshop_install.log" LOG="$MANIFEST_DIR/workshop_install.log"
touch "$LOG" touch "$LOG"
log() { log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"
} }
json_value() {
json_string() { local key="$1"
local key="$1" perl -0777 -ne '
perl -0777 -ne ' my $k = shift @ARGV;
my $key = shift @ARGV; if (/"\Q$k\E"\s*:\s*"([^"]*)"/s) {
if (/"\Q$key\E"\s*:\s*"([^"]*)"/s) { my $v = $1;
my $v = $1; $v =~ s/\\\//\//g;
$v =~ s/\\\//\//g; print $v;
print $v; exit;
} }
' "$key" "$MANIFEST" if (/"\Q$k\E"\s*:\s*([0-9]+)/s) {
print $1;
exit;
}
' "$key" "$MANIFEST"
} }
json_items() { json_items() {
perl -0777 -ne ' perl -0777 -ne '
if (/"items"\s*:\s*\[(.*?)\]/s) { if (/"items"\s*:\s*\[(.*?)\]/s) {
my $x = $1; my $x = $1;
while ($x =~ /"([0-9]{3,20})"/g) { print "$1\n"; } while ($x =~ /"([0-9]{3,20})"/g) { print "$1\n"; }
while ($x =~ /(?<![0-9])([0-9]{3,20})(?![0-9])/g) { print "$1\n"; } while ($x =~ /(?<![0-9])([0-9]{3,20})(?![0-9])/g) { print "$1\n"; }
} }
' "$MANIFEST" | awk '!seen[$0]++' ' "$MANIFEST" | awk '!seen[$0]++'
} }
ACTION="$(json_value action)"
ACTION="$(json_string action)" APPID="$(json_value workshop_app_id)"
APPID="$(json_string workshop_app_id)" STEAM_APPID="$(json_value steam_app_id)"
SERVER_PATH="$(json_string server_path)" SERVER_PATH="$(json_value server_path)"
[ -z "$ACTION" ] && ACTION="install" [ -z "$ACTION" ] && ACTION="install"
[ -z "$APPID" ] && APPID="$STEAM_APPID"
[ -z "$SERVER_PATH" ] && SERVER_PATH="$(pwd)" [ -z "$SERVER_PATH" ] && SERVER_PATH="$(pwd)"
if [ -z "$APPID" ]; then if [ -z "$APPID" ]; then
log "ERROR: workshop_app_id missing from manifest." log "ERROR: workshop_app_id missing from manifest."
exit 3 echo "Manifest debug:"
sed -n '1,80p' "$MANIFEST"
exit 3
fi fi
ITEMS="$(json_items)" ITEMS="$(json_items)"
if [ -z "$ITEMS" ]; then if [ -z "$ITEMS" ]; then
log "ERROR: no Workshop item IDs found in manifest." log "ERROR: no Workshop item IDs found in manifest."
exit 4 echo "Manifest debug:"
sed -n '1,80p' "$MANIFEST"
exit 4
fi fi
find_steamcmd() { find_steamcmd() {
for c in "${STEAMCMD_PATH:-}" steamcmd steamcmd.sh steamcmd.exe \ for c in "${STEAMCMD_PATH:-}" steamcmd steamcmd.sh steamcmd.exe \
/OGP/steamcmd/steamcmd.exe /OGP/steamcmd/steamcmd.sh \ /OGP/steamcmd/steamcmd.exe /OGP/steamcmd/steamcmd.sh \
"$SERVER_PATH/steamcmd/steamcmd.exe" "$SERVER_PATH/steamcmd/steamcmd.sh" \ "$SERVER_PATH/steamcmd/steamcmd.exe" "$SERVER_PATH/steamcmd/steamcmd.sh" \
/home/gameserver/steamcmd/steamcmd.sh "$HOME/steamcmd/steamcmd.sh" "$HOME/steamcmd/steamcmd.exe" /home/gameserver/steamcmd/steamcmd.sh "$HOME/steamcmd/steamcmd.sh" "$HOME/steamcmd/steamcmd.exe"
do do
[ -z "$c" ] && continue [ -z "$c" ] && continue
if command -v "$c" >/dev/null 2>&1; then command -v "$c"; return 0; fi if command -v "$c" >/dev/null 2>&1; then command -v "$c"; return 0; fi
if [ -f "$c" ]; then echo "$c"; return 0; fi if [ -f "$c" ]; then echo "$c"; return 0; fi
done done
return 1 return 1
} }
log "GSP Workshop job starting. action=$ACTION appid=$APPID server_path=$SERVER_PATH" log "GSP Workshop job starting. action=$ACTION appid=$APPID server_path=$SERVER_PATH"
if [ "$ACTION" = "remove" ]; then if [ "$ACTION" = "remove" ]; then
for id in $ITEMS; do for id in $ITEMS; do
log "Removing Workshop item files for $id if present." log "Removing Workshop item files for $id if present."
rm -rf "$SERVER_PATH/@$id" "$SERVER_PATH/workshop/@$id" "$SERVER_PATH/steamapps/workshop/content/$APPID/$id" rm -rf "$SERVER_PATH/@$id" "$SERVER_PATH/workshop/@$id" "$SERVER_PATH/steamapps/workshop/content/$APPID/$id"
done done
log "Remove job complete." log "Remove job complete."
exit 0 exit 0
fi fi
STEAMCMD="$(find_steamcmd)" || { STEAMCMD="$(find_steamcmd)" || {
log "ERROR: steamcmd was not found. Install SteamCMD on the agent host or set STEAMCMD_PATH." log "ERROR: steamcmd was not found. Install SteamCMD on the agent host or set STEAMCMD_PATH."
exit 127 exit 127
} }
RUNSCRIPT="$MANIFEST_DIR/steamcmd_workshop_$$.txt" RUNSCRIPT="$MANIFEST_DIR/steamcmd_workshop_$$.txt"
{ {
echo "@ShutdownOnFailedCommand 0" echo "@ShutdownOnFailedCommand 0"
echo "@NoPromptForPassword 1" echo "@NoPromptForPassword 1"
echo "login anonymous" echo "login anonymous"
echo "force_install_dir $SERVER_PATH" echo "force_install_dir $SERVER_PATH"
for id in $ITEMS; do for id in $ITEMS; do
echo "workshop_download_item $APPID $id validate" echo "workshop_download_item $APPID $id validate"
done done
echo "quit" echo "quit"
} > "$RUNSCRIPT" } > "$RUNSCRIPT"
log "Using SteamCMD: $STEAMCMD" log "Using SteamCMD: $STEAMCMD"
log "Running SteamCMD runscript: $RUNSCRIPT" log "Running SteamCMD runscript: $RUNSCRIPT"
"$STEAMCMD" +runscript "$RUNSCRIPT" 2>&1 | tee -a "$LOG" "$STEAMCMD" +runscript "$RUNSCRIPT" 2>&1 | tee -a "$LOG"
rc=${PIPESTATUS[0]} rc=${PIPESTATUS[0]}
if [ "$rc" -ne 0 ]; then if [ "$rc" -ne 0 ]; then
log "ERROR: SteamCMD failed with exit $rc" log "ERROR: SteamCMD failed with exit $rc"
exit "$rc" exit "$rc"
fi fi
for id in $ITEMS; do for id in $ITEMS; do
SRC="$SERVER_PATH/steamapps/workshop/content/$APPID/$id" SRC="$SERVER_PATH/steamapps/workshop/content/$APPID/$id"
DST="$SERVER_PATH/@$id" DST="$SERVER_PATH/@$id"
if [ ! -d "$SRC" ]; then
if [ ! -d "$SRC" ]; then log "ERROR: downloaded Workshop source not found: $SRC"
log "ERROR: downloaded Workshop source not found: $SRC" exit 5
exit 5 fi
fi log "Installing Workshop item $id to $DST"
rm -rf "$DST"
log "Installing Workshop item $id to $DST" cp -a "$SRC" "$DST"
rm -rf "$DST" if [ -d "$DST/keys" ]; then
cp -a "$SRC" "$DST" mkdir -p "$SERVER_PATH/keys"
find "$DST/keys" -type f -iname '*.bikey' -exec cp -f {} "$SERVER_PATH/keys/" \;
if [ -d "$DST/keys" ]; then fi
mkdir -p "$SERVER_PATH/keys"
find "$DST/keys" -type f -iname '*.bikey' -exec cp -f {} "$SERVER_PATH/keys/" \;
fi
done done
log "Workshop job complete." log "Workshop job complete."
exit 0 exit 0

View file

@ -0,0 +1,133 @@
#!/usr/bin/env bash
set -u
MANIFEST="${1:-}"
if [ -z "$MANIFEST" ] || [ ! -f "$MANIFEST" ]; then
echo "ERROR: Workshop manifest missing: $MANIFEST"
exit 2
fi
MANIFEST_DIR="$(dirname "$MANIFEST")"
LOG="$MANIFEST_DIR/workshop_install.log"
touch "$LOG"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"
}
json_string() {
local key="$1"
perl -0777 -ne '
my $key = shift @ARGV;
if (/"\Q$key\E"\s*:\s*"([^"]*)"/s) {
my $v = $1;
$v =~ s/\\\//\//g;
print $v;
}
' "$key" "$MANIFEST"
}
json_items() {
perl -0777 -ne '
if (/"items"\s*:\s*\[(.*?)\]/s) {
my $x = $1;
while ($x =~ /"([0-9]{3,20})"/g) { print "$1\n"; }
while ($x =~ /(?<![0-9])([0-9]{3,20})(?![0-9])/g) { print "$1\n"; }
}
' "$MANIFEST" | awk '!seen[$0]++'
}
ACTION="$(json_string action)"
APPID="$(json_string workshop_app_id)"
SERVER_PATH="$(json_string server_path)"
[ -z "$ACTION" ] && ACTION="install"
[ -z "$SERVER_PATH" ] && SERVER_PATH="$(pwd)"
if [ -z "$APPID" ]; then
log "ERROR: workshop_app_id missing from manifest."
exit 3
fi
ITEMS="$(json_items)"
if [ -z "$ITEMS" ]; then
log "ERROR: no Workshop item IDs found in manifest."
exit 4
fi
find_steamcmd() {
for c in "${STEAMCMD_PATH:-}" steamcmd steamcmd.sh steamcmd.exe \
/OGP/steamcmd/steamcmd.exe /OGP/steamcmd/steamcmd.sh \
"$SERVER_PATH/steamcmd/steamcmd.exe" "$SERVER_PATH/steamcmd/steamcmd.sh" \
/home/gameserver/steamcmd/steamcmd.sh "$HOME/steamcmd/steamcmd.sh" "$HOME/steamcmd/steamcmd.exe"
do
[ -z "$c" ] && continue
if command -v "$c" >/dev/null 2>&1; then command -v "$c"; return 0; fi
if [ -f "$c" ]; then echo "$c"; return 0; fi
done
return 1
}
log "GSP Workshop job starting. action=$ACTION appid=$APPID server_path=$SERVER_PATH"
if [ "$ACTION" = "remove" ]; then
for id in $ITEMS; do
log "Removing Workshop item files for $id if present."
rm -rf "$SERVER_PATH/@$id" "$SERVER_PATH/workshop/@$id" "$SERVER_PATH/steamapps/workshop/content/$APPID/$id"
done
log "Remove job complete."
exit 0
fi
STEAMCMD="$(find_steamcmd)" || {
log "ERROR: steamcmd was not found. Install SteamCMD on the agent host or set STEAMCMD_PATH."
exit 127
}
RUNSCRIPT="$MANIFEST_DIR/steamcmd_workshop_$$.txt"
{
echo "@ShutdownOnFailedCommand 0"
echo "@NoPromptForPassword 1"
echo "login anonymous"
echo "force_install_dir $SERVER_PATH"
for id in $ITEMS; do
echo "workshop_download_item $APPID $id validate"
done
echo "quit"
} > "$RUNSCRIPT"
log "Using SteamCMD: $STEAMCMD"
log "Running SteamCMD runscript: $RUNSCRIPT"
"$STEAMCMD" +runscript "$RUNSCRIPT" 2>&1 | tee -a "$LOG"
rc=${PIPESTATUS[0]}
if [ "$rc" -ne 0 ]; then
log "ERROR: SteamCMD failed with exit $rc"
exit "$rc"
fi
for id in $ITEMS; do
SRC="$SERVER_PATH/steamapps/workshop/content/$APPID/$id"
DST="$SERVER_PATH/@$id"
if [ ! -d "$SRC" ]; then
log "ERROR: downloaded Workshop source not found: $SRC"
exit 5
fi
log "Installing Workshop item $id to $DST"
rm -rf "$DST"
cp -a "$SRC" "$DST"
if [ -d "$DST/keys" ]; then
mkdir -p "$SERVER_PATH/keys"
find "$DST/keys" -type f -iname '*.bikey' -exec cp -f {} "$SERVER_PATH/keys/" \;
fi
done
log "Workshop job complete."
exit 0

View file

@ -167,6 +167,18 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
} }
$manifest_dir = dirname($manifest_path); $manifest_dir = dirname($manifest_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'] : scm_extract_workshop_steam_app_id($server_xml);
if ($workshop_app_id === '' && $steam_app_id !== '') {
$workshop_app_id = $steam_app_id;
}
if ($steam_app_id === '' && $workshop_app_id !== '') {
$steam_app_id = $workshop_app_id;
}
if ($workshop_app_id === '' || !preg_match('/^[0-9]+$/', $workshop_app_id)) {
$error = 'Workshop App ID is missing from the game XML workshop_support block.';
return false;
}
$manifest = array( $manifest = array(
'manifest_version' => 1, 'manifest_version' => 1,
'action' => (string)$action, 'action' => (string)$action,
@ -174,8 +186,8 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
'home_cfg_id' => (int)$home_info['home_cfg_id'], 'home_cfg_id' => (int)$home_info['home_cfg_id'],
'game_path' => $home_path, 'game_path' => $home_path,
'server_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)), 'workshop_app_id' => $workshop_app_id,
'steam_app_id' => !empty($extra_manifest['steam_app_id']) ? (string)$extra_manifest['steam_app_id'] : '', 'steam_app_id' => $steam_app_id,
'items' => array_values($item_ids), 'items' => array_values($item_ids),
'item_details' => !empty($extra_manifest['item_details']) && is_array($extra_manifest['item_details']) ? $extra_manifest['item_details'] : array(), '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'] : '', 'install_strategy' => !empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : '',

View file

@ -0,0 +1,475 @@
<?php
/*
*
* GSP - Workshop Content actions (Phase 1)
*
*/
require_once("includes/lib_remote.php");
require_once("modules/config_games/server_config_parser.php");
require_once(dirname(__FILE__) . '/server_content_helpers.php');
function scm_workshop_log_action($db, $home_id, $user_id, $message)
{
$db->logger("server_content_workshop home_id=".(int)$home_id." user_id=".(int)$user_id." ".$message);
}
function scm_workshop_update_rows_state($db, $home_id, array $item_ids, $state, $error = null, $mark_install = false, $mark_update = false)
{
if (empty($item_ids)) {
return true;
}
$escaped_ids = array();
foreach ($item_ids as $item_id) {
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
}
$set = array(
"install_state='" . $db->realEscapeSingle($state) . "'",
"updated_at=NOW()",
);
if ($mark_install) {
$set[] = "last_installed_at=NOW()";
}
if ($mark_update) {
$set[] = "last_updated_at=NOW()";
}
if ($error === null) {
$set[] = "last_error=NULL";
} else {
$set[] = "last_error='" . $db->realEscapeSingle($error) . "'";
}
$query = "UPDATE `".OGP_DB_PREFIX."server_content_workshop`
SET ".implode(", ", $set)."
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")";
return (bool)$db->query($query);
}
function scm_workshop_filter_existing_ids($db, $home_id, array $item_ids)
{
if (empty($item_ids)) {
return array();
}
$escaped_ids = array();
foreach ($item_ids as $item_id) {
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
}
$rows = $db->resultQuery(
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")"
);
$allowed = array();
if (is_array($rows)) {
foreach ((array)$rows as $row) {
$allowed[(string)$row['workshop_item_id']] = (string)$row['workshop_item_id'];
}
}
return array_values($allowed);
}
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, 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);
$xml_install_path = scm_extract_workshop_install_path($server_xml);
$keys_target_path = scm_workshop_keys_target_path($server_xml, $home_info);
$item_details = array();
$resolved_app_id = '';
$steam_app_id = '';
foreach ($item_ids as $item_id) {
$payload = array(
'workshop_item_id' => (string)$item_id,
'install_strategy' => $install_strategy,
);
$message = '';
$runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $payload, $message);
if ($runtime === false) {
$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'] : scm_get_default_workshop_target_template($install_strategy),
'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' => $keys_target_path,
);
}
if ($resolved_app_id === '') {
$resolved_app_id = scm_extract_workshop_app_id($server_xml);
}
if ($steam_app_id === '') {
$steam_app_id = scm_extract_workshop_steam_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' => $xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy),
'keys_target_path' => $keys_target_path,
'post_install_script' => scm_workshop_post_install_action($server_xml),
'launch_param_additions' => isset($server_xml->workshop_support->startup_param_format) ? trim((string)$server_xml->workshop_support->startup_param_format) : '',
'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0,
'content_template_name' => isset($template['name']) ? (string)$template['name'] : '',
'item_details' => $item_details,
);
}
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;
}
$manifest_path = scm_get_workshop_manifest_path($home_info);
if ($manifest_path === false) {
$error = 'Manifest path validation failed for this server home.';
return false;
}
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
if (!scm_path_is_under_home($home_path, $manifest_path)) {
$error = 'Manifest path is outside of the server home.';
return false;
}
$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'] : scm_get_default_workshop_target_template(!empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : ''),
'generated_at' => date('Y-m-d H:i:s'),
);
if (!empty($extra_manifest)) {
$manifest['extra'] = $extra_manifest;
}
$manifest_json = json_encode($manifest);
if ($manifest_json === false) {
$error = 'Failed to encode workshop manifest JSON.';
return false;
}
$remote = new OGPRemoteLibrary(
$home_info['agent_ip'],
$home_info['agent_port'],
$home_info['encryption_key'],
$home_info['timeout']
);
$remote->exec("mkdir -p " . escapeshellarg($manifest_dir));
if ((int)$remote->remote_writefile($manifest_path, $manifest_json) !== 1) {
$error = 'Failed to write workshop manifest to remote server.';
return false;
}
$script_path = scm_prepare_workshop_script_for_agent($remote, $home_info, $server_xml, $error);
if ($script_path === false) {
return false;
}
$command = "bash " . escapeshellarg($script_path) . " " . escapeshellarg($manifest_path) . " ; echo __GSP_WORKSHOP_EXIT:$?";
$output = $remote->exec($command);
if (!is_string($output) || $output === '') {
$error = 'Workshop script did not return an execution status.';
return false;
}
if (!preg_match('/__GSP_WORKSHOP_EXIT:(\d+)/', $output, $matches)) {
$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);
return false;
}
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, array $options = array())
{
$message = '';
$is_error = true;
if (!scm_ensure_workshop_schema($db)) {
$message = 'Workshop schema migration failed.';
return false;
}
scm_ensure_phase2_schema($db);
$home_id = (int)$home_info['home_id'];
$user_id = (int)$user_id;
$addon_id = (int)$addon_id;
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
if ($server_xml === false) {
$message = 'Unable to read server configuration for workshop action.';
return false;
}
if (!scm_workshop_is_supported($server_xml)) {
$message = 'This game XML does not enable Steam Workshop support. Add a valid workshop_support block before installing Workshop items.';
return false;
}
$template = scm_workshop_get_content_template($db, $addon_id);
if ($action === 'install_new') {
$invalid = array();
$item_ids = scm_parse_workshop_ids($raw_ids, $invalid);
if (!empty($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 Steam Workshop ID or Workshop URL.';
return false;
}
$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 from the game XML workshop_support block.';
return false;
}
// Check whether the content_id column exists (added in db_version 6).
$has_content_id_col = (bool)$db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '" . $db->realEscapeSingle(OGP_DB_PREFIX . 'server_content_workshop') . "'
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_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)."',
'".$db->realEscapeSingle($install_path)."',
'".$db->realEscapeSingle($install_strategy)."',
1,
".$next_order.",
'queued',
".$user_id.",
NOW(),
NOW()
" . $content_id_val . "
)
ON DUPLICATE KEY UPDATE
home_cfg_id=VALUES(home_cfg_id),
remote_server_id=VALUES(remote_server_id),
workshop_app_id=VALUES(workshop_app_id),
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);
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$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_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'] : '');
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=failed error=".$error);
$message = $error;
return false;
}
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;
}
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 === 'remove_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 to remove.';
return false;
}
$escaped_ids = array();
foreach ($item_ids as $item_id) {
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
}
$db->query(
"DELETE FROM `" . OGP_DB_PREFIX . "server_content_workshop`
WHERE home_id=" . (int)$home_id . "
AND workshop_item_id IN (" . implode(",", $escaped_ids) . ")"
);
scm_workshop_log_action($db, $home_id, $user_id, "remove_selected ids=" . implode(',', $item_ids) . " status=db_removed");
$is_error = false;
$message = 'Selected Workshop item(s) removed from this server list. Installed files, if any, can be cleaned up separately.';
return true;
}
if ($action === 'update_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 = '';
$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);
} 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;
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);
scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=failed error=".$error);
$message = $error;
return false;
}
if ($action === 'update_all') {
$rows = $db->resultQuery(
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
WHERE home_id=".$home_id." AND install_state<>'removed'"
);
$item_ids = array();
if (is_array($rows)) {
foreach ((array)$rows as $row) {
$item_ids[] = (string)$row['workshop_item_id'];
}
}
$item_ids = scm_parse_selected_workshop_ids($item_ids);
if (empty($item_ids)) {
$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 = '';
$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_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'] : '');
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=failed error=".$error);
$message = $error;
return false;
}
$message = 'Invalid workshop action.';
return false;
}