doc changes and reference folder
This commit is contained in:
parent
11691a5876
commit
82cbc206eb
33 changed files with 1514 additions and 2855 deletions
|
|
@ -1,158 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# GSP Deployment Script
|
||||
# =====================
|
||||
# This script deploys the Game Server Panel (GSP) from GitHub to a web server.
|
||||
#
|
||||
# HOW IT WORKS:
|
||||
# 1. Clones/updates the GSP repository to a staging directory
|
||||
# 2. Syncs files to the web root using rsync (preserving configs)
|
||||
# 3. Sets proper permissions for OGP panel operation
|
||||
#
|
||||
# CONFIGURATION:
|
||||
# All settings can be configured via environment variables or by editing
|
||||
# the defaults in the "Config" section below.
|
||||
#
|
||||
# ENVIRONMENT VARIABLES:
|
||||
# - REPO_URL: Git repository URL (default: https://github.com/GameServerPanel/GSP.git)
|
||||
# - STAGE_DIR: Staging directory for git clone (default: $HOME/gsp_stage)
|
||||
# - WEB_ROOT: Live web server directory (default: /var/www/html/panel)
|
||||
# - OWNER: File owner user (default: www-data)
|
||||
# - GROUP: File owner group (default: www-data)
|
||||
# - SUDO: Command prefix for privilege escalation (default: sudo, set empty to skip)
|
||||
# - DRY_RUN: Set to 1 to test without making changes (default: 0)
|
||||
#
|
||||
# EXAMPLE USAGE:
|
||||
# # Use defaults:
|
||||
# ./deploy_gsp.sh
|
||||
#
|
||||
# # Custom web root:
|
||||
# WEB_ROOT=/home/panel/public_html ./deploy_gsp.sh
|
||||
#
|
||||
# # Dry run to test:
|
||||
# DRY_RUN=1 ./deploy_gsp.sh
|
||||
#
|
||||
# # Different user/group:
|
||||
# OWNER=apache GROUP=apache ./deploy_gsp.sh
|
||||
#
|
||||
set -Eeuo pipefail
|
||||
umask 022
|
||||
|
||||
# ---------- Config (override via env if you like) ----------
|
||||
REPO_URL="${REPO_URL:-https://github.com/GameServerPanel/GSP.git}"
|
||||
STAGE_DIR="${STAGE_DIR:-$HOME/gsp_stage}" # keeps clone in your home folder
|
||||
WEB_ROOT="${WEB_ROOT:-/var/www/html/panel}" # live site root
|
||||
OWNER="${OWNER:-www-data}"
|
||||
GROUP="${GROUP:-www-data}"
|
||||
SUDO="${SUDO:-sudo}" # set SUDO= to skip sudo if not needed
|
||||
DRY_RUN="${DRY_RUN:-0}" # set DRY_RUN=1 to test without writing
|
||||
|
||||
# Never overwrite these:
|
||||
EXCLUDES=(
|
||||
".git/"
|
||||
"includes/config.inc.php"
|
||||
"modules/billing/includes/config.inc.php"
|
||||
)
|
||||
|
||||
# ---------- Helpers ----------
|
||||
log(){ printf '[%s] %s\n' "$(date +'%F %T')" "$*"; }
|
||||
trap 'rc=$?; log "ERROR on line $LINENO (exit $rc)"; exit $rc' ERR
|
||||
|
||||
# ---------- Requirements ----------
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
log "Installing git + rsync..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
$SUDO apt-get update && $SUDO apt-get install -y git rsync
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
$SUDO dnf install -y git rsync
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
$SUDO yum install -y git rsync
|
||||
else
|
||||
log "git/rsync required; please install manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- Prepare stage clone in home folder ----------
|
||||
log "Stage dir: $STAGE_DIR"
|
||||
mkdir -p "$STAGE_DIR"
|
||||
if [[ ! -d "$STAGE_DIR/.git" ]]; then
|
||||
log "Cloning $REPO_URL ..."
|
||||
git clone --depth 1 "$REPO_URL" "$STAGE_DIR"
|
||||
else
|
||||
log "Fetching latest from origin..."
|
||||
git -C "$STAGE_DIR" fetch --all --prune
|
||||
fi
|
||||
|
||||
# Determine default branch (origin/HEAD), fallback to main/master
|
||||
DEFAULT_BRANCH="$(git -C "$STAGE_DIR" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)"
|
||||
DEFAULT_BRANCH="${DEFAULT_BRANCH#origin/}"
|
||||
if [[ -z "${DEFAULT_BRANCH:-}" ]]; then
|
||||
if git -C "$STAGE_DIR" ls-remote --exit-code --heads origin main >/dev/null 2>&1; then
|
||||
DEFAULT_BRANCH="main"
|
||||
else
|
||||
DEFAULT_BRANCH="master"
|
||||
fi
|
||||
fi
|
||||
log "Default branch: $DEFAULT_BRANCH"
|
||||
|
||||
# Reset stage to remote HEAD
|
||||
git -C "$STAGE_DIR" checkout -B "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
|
||||
git -C "$STAGE_DIR" reset --hard "origin/$DEFAULT_BRANCH"
|
||||
git -C "$STAGE_DIR" clean -fdx
|
||||
COMMIT="$(git -C "$STAGE_DIR" rev-parse --short HEAD)"
|
||||
log "Prepared commit: $COMMIT"
|
||||
|
||||
# ---------- Rsync to webroot (preserve configs) ----------
|
||||
RSYNC_ARGS=(-a --delete --omit-dir-times --human-readable --progress --itemize-changes)
|
||||
for e in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$e"); done
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
RSYNC_ARGS+=(--dry-run)
|
||||
log "DRY RUN enabled — no changes will be written."
|
||||
fi
|
||||
|
||||
log "Syncing to $WEB_ROOT ..."
|
||||
$SUDO mkdir -p "$WEB_ROOT"
|
||||
$SUDO rsync "${RSYNC_ARGS[@]}" "$STAGE_DIR"/ "$WEB_ROOT"/
|
||||
|
||||
# ---------- Permissions tuned for OGP panel ----------
|
||||
WEB_USER="${OWNER:-www-data}"
|
||||
WEB_GROUP="${GROUP:-www-data}"
|
||||
|
||||
log "Setting base permissions (OGP-safe)…"
|
||||
# Base ownership
|
||||
$SUDO chown -R "$OWNER:$GROUP" "$WEB_ROOT"
|
||||
|
||||
# Safe defaults: dirs 755, files 644 (batched; no “arg list too long”)
|
||||
$SUDO find "$WEB_ROOT" -type d -exec chmod 755 {} +
|
||||
$SUDO find "$WEB_ROOT" -type f -exec chmod 644 {} +
|
||||
|
||||
# Writable dirs for OGP
|
||||
WRITABLE_NAMES="templates_c cache logs uploads storage tmp"
|
||||
for name in $WRITABLE_NAMES; do
|
||||
$SUDO find "$WEB_ROOT" -type d -name "$name" -print0 | while IFS= read -r -d '' d; do
|
||||
log "Making writable dir: $d"
|
||||
$SUDO chown -R "$OWNER:$GROUP" "$d"
|
||||
$SUDO chmod -R 2775 "$d"
|
||||
if command -v setfacl >/dev/null 2>&1; then
|
||||
$SUDO setfacl -R -m g:$GROUP:rwx -m d:g:$GROUP:rwx "$d" || true
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Keep your configs tight (and preserved from rsync by the script’s excludes)
|
||||
# If the panel needs to write them via web UI, relax to 660 and owner www-data.
|
||||
CFG1="$WEB_ROOT/includes/config.inc.php"
|
||||
CFG2="$WEB_ROOT/modules/billing/includes/config.inc.php"
|
||||
for cfg in "$CFG1" "$CFG2"; do
|
||||
if [[ -f "$cfg" ]]; then
|
||||
$SUDO chown "$WEB_USER:$WEB_GROUP" "$cfg"
|
||||
$SUDO chmod 640 "$cfg"
|
||||
fi
|
||||
done
|
||||
|
||||
# Ensure billing folder itself is Apache-friendly (readable/executable)
|
||||
$SUDO find "$WEB_ROOT/modules/billing" -type d -print0 | xargs -0 -r $SUDO chmod 755
|
||||
$SUDO find "$WEB_ROOT/modules/billing" -type f -print0 | xargs -0 -r $SUDO chmod 644
|
||||
|
||||
log "Permissions set for OGP panel + billing."
|
||||
|
|
@ -1 +0,0 @@
|
|||
gsp
|
||||
|
|
@ -223,6 +223,12 @@ function scm_get_workshop_manifest_path(array $home_info)
|
|||
|
||||
function scm_extract_workshop_app_id($server_xml)
|
||||
{
|
||||
if (isset($server_xml->workshop_support->workshop_app_id)) {
|
||||
$value = trim((string)$server_xml->workshop_support->workshop_app_id);
|
||||
if ($value !== '' && preg_match('/^[0-9]+$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
$candidates = array(
|
||||
'workshop_app_id',
|
||||
'workshop_appid',
|
||||
|
|
@ -240,9 +246,53 @@ function scm_extract_workshop_app_id($server_xml)
|
|||
return "";
|
||||
}
|
||||
|
||||
function scm_extract_workshop_steam_app_id($server_xml)
|
||||
{
|
||||
if (isset($server_xml->workshop_support->steam_app_id)) {
|
||||
$value = trim((string)$server_xml->workshop_support->steam_app_id);
|
||||
if ($value !== '' && preg_match('/^[0-9]+$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function scm_extract_workshop_install_path($server_xml)
|
||||
{
|
||||
if (isset($server_xml->workshop_support->install_path)) {
|
||||
$value = trim((string)$server_xml->workshop_support->install_path);
|
||||
if ($value !== '' && preg_match('/^[^\\r\\n\\0]+$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function scm_workshop_xml_bool($value, $default = false)
|
||||
{
|
||||
$value = strtolower(trim((string)$value));
|
||||
if ($value === '') {
|
||||
return (bool)$default;
|
||||
}
|
||||
if (in_array($value, array('1', 'yes', 'true', 'on'), true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($value, array('0', 'no', 'false', 'off'), true)) {
|
||||
return false;
|
||||
}
|
||||
return (bool)$default;
|
||||
}
|
||||
|
||||
function scm_get_workshop_script_path(array $home_info, $server_xml)
|
||||
{
|
||||
$key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux';
|
||||
$nested_key = scm_is_windows_home($home_info) ? 'script_windows' : 'script_linux';
|
||||
if (isset($server_xml->workshop_support->$nested_key)) {
|
||||
$xml_path = trim((string)$server_xml->workshop_support->$nested_key);
|
||||
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return $xml_path;
|
||||
}
|
||||
}
|
||||
if (isset($server_xml->$key)) {
|
||||
$xml_path = trim((string)$server_xml->$key);
|
||||
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
|
|
@ -255,14 +305,20 @@ function scm_get_workshop_script_path(array $home_info, $server_xml)
|
|||
function scm_get_configured_workshop_script_path(array $home_info, $server_xml)
|
||||
{
|
||||
$key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux';
|
||||
if (!isset($server_xml->$key)) {
|
||||
return '';
|
||||
$nested_key = scm_is_windows_home($home_info) ? 'script_windows' : 'script_linux';
|
||||
if (isset($server_xml->workshop_support->$nested_key)) {
|
||||
$xml_path = trim((string)$server_xml->workshop_support->$nested_key);
|
||||
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return $xml_path;
|
||||
}
|
||||
}
|
||||
$xml_path = trim((string)$server_xml->$key);
|
||||
if ($xml_path === '' || !preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return '';
|
||||
if (isset($server_xml->$key)) {
|
||||
$xml_path = trim((string)$server_xml->$key);
|
||||
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return $xml_path;
|
||||
}
|
||||
}
|
||||
return $xml_path;
|
||||
return '';
|
||||
}
|
||||
|
||||
function scm_get_bundled_workshop_script_source(array $home_info)
|
||||
|
|
@ -658,13 +714,16 @@ function scm_build_workshop_runtime_context($db, array $home_info, $server_xml,
|
|||
if ($fallback_workshop_app_id === '') {
|
||||
$fallback_workshop_app_id = scm_extract_workshop_app_id($server_xml);
|
||||
}
|
||||
$steam_app_id = (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '';
|
||||
$steam_app_id = (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : scm_extract_workshop_steam_app_id($server_xml);
|
||||
$folder_name = ($optional_folder_name !== '') ? $optional_folder_name : '@' . $workshop_item_id;
|
||||
$effective_template = $target_path_template;
|
||||
if ($effective_template === '') {
|
||||
$effective_template = (is_array($fallback_profile) && !empty($fallback_profile['install_path_template']))
|
||||
? (string)$fallback_profile['install_path_template']
|
||||
: scm_get_default_workshop_target_template($install_strategy);
|
||||
if (is_array($fallback_profile) && !empty($fallback_profile['install_path_template'])) {
|
||||
$effective_template = (string)$fallback_profile['install_path_template'];
|
||||
} else {
|
||||
$xml_install_path = scm_extract_workshop_install_path($server_xml);
|
||||
$effective_template = $xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy);
|
||||
}
|
||||
}
|
||||
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => isset($server_xml->exe_location) ? (string)$server_xml->exe_location : ''), array(
|
||||
'WORKSHOP_ID' => $workshop_item_id,
|
||||
|
|
@ -695,6 +754,12 @@ function scm_detect_workshop_install_strategy(array $home_info, $server_xml, arr
|
|||
return strtolower($strategy);
|
||||
}
|
||||
}
|
||||
if (isset($server_xml->workshop_support->install_strategy)) {
|
||||
$strategy = trim((string)$server_xml->workshop_support->install_strategy);
|
||||
if ($strategy !== '' && preg_match('/^[a-z0-9_\-]+$/i', $strategy)) {
|
||||
return strtolower($strategy);
|
||||
}
|
||||
}
|
||||
foreach (array('workshop_install_strategy', 'install_strategy') as $tag) {
|
||||
if (isset($server_xml->$tag)) {
|
||||
$strategy = trim((string)$server_xml->$tag);
|
||||
|
|
@ -718,15 +783,33 @@ function scm_detect_workshop_install_strategy(array $home_info, $server_xml, arr
|
|||
|
||||
function scm_workshop_should_copy_keys($server_xml, $install_strategy)
|
||||
{
|
||||
if (isset($server_xml->workshop_support->copy_keys)) {
|
||||
$attrs = $server_xml->workshop_support->copy_keys->attributes();
|
||||
if (isset($attrs['enabled'])) {
|
||||
return scm_workshop_xml_bool((string)$attrs['enabled'], false);
|
||||
}
|
||||
}
|
||||
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 scm_workshop_xml_bool((string)$server_xml->$tag, false);
|
||||
}
|
||||
}
|
||||
return in_array((string)$install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true);
|
||||
}
|
||||
|
||||
function scm_workshop_keys_target_path($server_xml, array $home_info)
|
||||
{
|
||||
$template = '';
|
||||
if (isset($server_xml->workshop_support->copy_keys->target_path)) {
|
||||
$template = trim((string)$server_xml->workshop_support->copy_keys->target_path);
|
||||
}
|
||||
if ($template === '') {
|
||||
$template = '{SERVER_ROOT}/keys';
|
||||
}
|
||||
$map = scm_build_placeholder_map($home_info);
|
||||
return scm_apply_placeholders($template, $map);
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ scm_workshop_test_assert($script === '/cygdrive/c/OGP_User_Files/11/gsp_server_c
|
|||
scm_workshop_test_assert(isset($windowsRemote->files[$script]), 'writes Windows/Cygwin bundled script to fake agent');
|
||||
scm_workshop_test_assert($error === '', 'default Windows/Cygwin script staging does not report missing script');
|
||||
|
||||
$configuredXml = simplexml_load_string('<game_config><workshop_script_linux>/agent/custom/workshop.sh</workshop_script_linux></game_config>');
|
||||
$configuredXml = simplexml_load_string('<game_config><workshop_support><script_linux>/agent/custom/workshop.sh</script_linux></workshop_support></game_config>');
|
||||
$customRemote = new ScmWorkshopFakeRemote();
|
||||
$customRemote->existing[] = '/agent/custom/workshop.sh';
|
||||
$script = scm_prepare_workshop_script_for_agent($customRemote, $linuxHome, $configuredXml, $error);
|
||||
|
|
@ -110,7 +110,13 @@ $runtime = scm_build_workshop_runtime_context(new stdClass(), $linuxHome, $empty
|
|||
scm_workshop_test_assert($runtime['target_path_template'] === '{SERVER_ROOT}/{MOD_FOLDER}', 'Arma strategy keeps @mod folder at server root');
|
||||
scm_workshop_test_assert($runtime['target_path_resolved'] === '/srv/games/arma3/@450814997', 'Arma target path resolves to root @WorkshopID folder');
|
||||
|
||||
$appXml = simplexml_load_string('<game_config><workshop_app_id>107410</workshop_app_id></game_config>');
|
||||
scm_workshop_test_assert(scm_extract_workshop_app_id($appXml) === '107410', 'extracts explicit Workshop app ID from game XML');
|
||||
$appXml = simplexml_load_string('<game_config><workshop_support><steam_app_id>107410</steam_app_id><workshop_app_id>107410</workshop_app_id><install_strategy>arma_mod_folder</install_strategy><install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path><copy_keys enabled="1"><target_path>{SERVER_ROOT}/keys</target_path></copy_keys></workshop_support></game_config>');
|
||||
scm_workshop_test_assert(scm_extract_workshop_app_id($appXml) === '107410', 'extracts canonical Workshop app ID from game XML');
|
||||
scm_workshop_test_assert(scm_extract_workshop_steam_app_id($appXml) === '107410', 'extracts canonical Steam app ID from game XML');
|
||||
scm_workshop_test_assert(scm_detect_workshop_install_strategy($linuxHome, $appXml) === 'arma_mod_folder', 'extracts canonical Workshop install strategy from game XML');
|
||||
$runtime = scm_build_workshop_runtime_context(new stdClass(), $linuxHome, $appXml, array('workshop_item_id' => '450814997'), $message);
|
||||
scm_workshop_test_assert($runtime['target_path_template'] === '{SERVER_ROOT}/{MOD_FOLDER}', 'canonical Workshop install_path controls target template');
|
||||
scm_workshop_test_assert($runtime['target_path_resolved'] === '/srv/games/arma3/@450814997', 'canonical Workshop install_path resolves under server root');
|
||||
scm_workshop_test_assert(scm_workshop_should_copy_keys($appXml, 'copy_to_mod_folder') === true, 'canonical copy_keys enabled flag is honored');
|
||||
|
||||
echo "All Workshop helper smoke tests passed.\n";
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
|||
{
|
||||
$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 = '';
|
||||
|
|
@ -127,13 +129,16 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
|||
'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',
|
||||
'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,
|
||||
|
|
@ -141,7 +146,8 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
|||
'server_root' => rtrim((string)$home_info['home_path'], '/'),
|
||||
'install_strategy' => $install_strategy,
|
||||
'copy_keys' => $copy_keys ? 1 : 0,
|
||||
'target_path_template' => isset($template['target_path_template']) && trim((string)$template['target_path_template']) !== '' ? trim((string)$template['target_path_template']) : scm_get_default_workshop_target_template($install_strategy),
|
||||
'target_path_template' => isset($template['target_path_template']) && trim((string)$template['target_path_template']) !== '' ? trim((string)$template['target_path_template']) : ($xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy)),
|
||||
'keys_target_path' => $keys_target_path,
|
||||
'post_install_script' => isset($template['post_script']) ? trim((string)$template['post_script']) : '',
|
||||
'launch_param_additions' => isset($template['launch_param_additions']) ? trim((string)$template['launch_param_additions']) : '',
|
||||
'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0,
|
||||
|
|
|
|||
|
|
@ -268,6 +268,77 @@
|
|||
<xs:element name="file" type="files_type" maxOccurs="unbounded" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- Canonical Workshop / Server Content capability declaration.
|
||||
The Server Content Manager (addonsmanager) reads this block to build
|
||||
safe Workshop manifests for the agents. Keep customer-supplied Workshop
|
||||
item IDs out of XML; users enter those through the Panel UI. -->
|
||||
<xs:simpleType name="boolean_flag_type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="0" />
|
||||
<xs:enumeration value="1" />
|
||||
<xs:enumeration value="false" />
|
||||
<xs:enumeration value="true" />
|
||||
<xs:enumeration value="no" />
|
||||
<xs:enumeration value="yes" />
|
||||
<xs:enumeration value="off" />
|
||||
<xs:enumeration value="on" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="workshop_provider_type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="steam" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="workshop_download_method_type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="steamcmd" />
|
||||
<xs:enumeration value="game_managed_workshop" />
|
||||
<xs:enumeration value="manual" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="workshop_install_strategy_type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="game_managed_workshop" />
|
||||
<xs:enumeration value="steamcmd_download_only" />
|
||||
<xs:enumeration value="copy_to_game_root" />
|
||||
<xs:enumeration value="copy_to_mod_folder" />
|
||||
<xs:enumeration value="dayz_mod_folder" />
|
||||
<xs:enumeration value="arma_mod_folder" />
|
||||
<xs:enumeration value="config_only" />
|
||||
<xs:enumeration value="custom_scripted_install" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="workshop_copy_keys_type">
|
||||
<xs:sequence>
|
||||
<xs:element name="source_pattern" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="target_path" type="xs:string" minOccurs="0" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="enabled" type="boolean_flag_type" use="optional" />
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="workshop_support_type">
|
||||
<xs:sequence>
|
||||
<xs:element name="enabled" type="boolean_flag_type" minOccurs="0" />
|
||||
<xs:element name="provider" type="workshop_provider_type" minOccurs="0" />
|
||||
<xs:element name="steam_app_id" type="xs:positiveInteger" minOccurs="0" />
|
||||
<xs:element name="workshop_app_id" type="xs:positiveInteger" minOccurs="0" />
|
||||
<xs:element name="download_method" type="workshop_download_method_type" minOccurs="0" />
|
||||
<xs:element name="install_strategy" type="workshop_install_strategy_type" minOccurs="0" />
|
||||
<xs:element name="install_path" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="startup_param_format" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="mod_separator" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="mod_prefix" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="mod_folder_format" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="copy_keys" type="workshop_copy_keys_type" minOccurs="0" />
|
||||
<xs:element name="script_linux" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="script_windows" type="xs:string" minOccurs="0" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- The main of the template -->
|
||||
<xs:complexType name="server_config_type">
|
||||
|
|
@ -278,6 +349,7 @@
|
|||
<xs:element name="gameq_query_name" type="nonEmptyString" minOccurs="0" />
|
||||
<xs:element name="installer" type="nonEmptyString" minOccurs="0" />
|
||||
<xs:element name="game_name" type="nonEmptyString" />
|
||||
<xs:element name="workshop_support" type="workshop_support_type" minOccurs="0" />
|
||||
<xs:element name="server_exec_name" type="nonEmptyString" />
|
||||
<xs:element name="query_port" type="query_port_type" minOccurs="0" />
|
||||
<xs:element name="cli_template" type="nonEmptyString" minOccurs="0" />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,22 @@
|
|||
<game_key>arma3_linux64</game_key>
|
||||
<installer>steamcmd</installer>
|
||||
<game_name>Arma 3</game_name>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<workshop_support>
|
||||
<enabled>1</enabled>
|
||||
<provider>steam</provider>
|
||||
<steam_app_id>107410</steam_app_id>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<download_method>steamcmd</download_method>
|
||||
<install_strategy>arma_mod_folder</install_strategy>
|
||||
<install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path>
|
||||
<startup_param_format>-mod={MOD_LIST}</startup_param_format>
|
||||
<mod_separator>;</mod_separator>
|
||||
<mod_prefix>@</mod_prefix>
|
||||
<copy_keys enabled="1">
|
||||
<source_pattern>{MOD_PATH}/keys/*.bikey</source_pattern>
|
||||
<target_path>{SERVER_ROOT}/keys</target_path>
|
||||
</copy_keys>
|
||||
</workshop_support>
|
||||
<server_exec_name>arma3server_x64</server_exec_name>
|
||||
<cli_template>%CONFIG% %CFG% %PROFILES% %NAME% %IP% %PORT% %PLAYERS% %MODLIST% %SERVERMODLIST% %AUTOINIT%</cli_template>
|
||||
<cli_params>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,22 @@
|
|||
<game_key>arma3_win64</game_key>
|
||||
<installer>steamcmd</installer>
|
||||
<game_name>Arma 3</game_name>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<workshop_support>
|
||||
<enabled>1</enabled>
|
||||
<provider>steam</provider>
|
||||
<steam_app_id>107410</steam_app_id>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<download_method>steamcmd</download_method>
|
||||
<install_strategy>arma_mod_folder</install_strategy>
|
||||
<install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path>
|
||||
<startup_param_format>-mod={MOD_LIST}</startup_param_format>
|
||||
<mod_separator>;</mod_separator>
|
||||
<mod_prefix>@</mod_prefix>
|
||||
<copy_keys enabled="1">
|
||||
<source_pattern>{MOD_PATH}/keys/*.bikey</source_pattern>
|
||||
<target_path>{SERVER_ROOT}/keys</target_path>
|
||||
</copy_keys>
|
||||
</workshop_support>
|
||||
<server_exec_name>arma3server.exe</server_exec_name>
|
||||
<cli_template>-profiles=profile -name=player -config=profile\server.cfg -cfg=profile\basic.cfg %PORT% %PLAYERS% %RANKING% %AUTOINIT% %DEBUG% %MODS% %SERVERMODS%</cli_template>
|
||||
<cli_params>
|
||||
|
|
|
|||
60
Panel/modules/config_games/tests/validate_server_configs.php
Normal file
60
Panel/modules/config_games/tests/validate_server_configs.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
/*
|
||||
* Validates all game server XML configuration files against the bundled
|
||||
* schema. This is a developer smoke test; it does not load the full Panel.
|
||||
*/
|
||||
|
||||
$baseDir = dirname(__DIR__);
|
||||
$schema = $baseDir . '/schema_server_config.xml';
|
||||
$configDir = $baseDir . '/server_configs';
|
||||
|
||||
if (!is_file($schema)) {
|
||||
fwrite(STDERR, "Schema not found: {$schema}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$files = glob($configDir . '/*.xml');
|
||||
sort($files);
|
||||
|
||||
if (empty($files)) {
|
||||
fwrite(STDERR, "No XML files found under: {$configDir}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$failed = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
$doc = new DOMDocument();
|
||||
$doc->preserveWhiteSpace = false;
|
||||
$doc->formatOutput = false;
|
||||
|
||||
if (!$doc->load($file)) {
|
||||
$failed++;
|
||||
echo "FAIL " . basename($file) . "\n";
|
||||
foreach (libxml_get_errors() as $error) {
|
||||
echo " XML parse: " . trim($error->message) . "\n";
|
||||
}
|
||||
libxml_clear_errors();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$doc->schemaValidate($schema)) {
|
||||
$failed++;
|
||||
echo "FAIL " . basename($file) . "\n";
|
||||
foreach (libxml_get_errors() as $error) {
|
||||
echo " Schema: " . trim($error->message) . "\n";
|
||||
}
|
||||
libxml_clear_errors();
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "PASS " . basename($file) . "\n";
|
||||
}
|
||||
|
||||
if ($failed > 0) {
|
||||
echo "XML validation failed for {$failed} file(s).\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "All server config XML files validated successfully.\n";
|
||||
|
|
@ -1,364 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: Admin profile management
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
if (!defined('SERVER_CONFIG_LOCATION')) {
|
||||
require_once __DIR__ . '/../../config_games/server_config_parser.php';
|
||||
}
|
||||
|
||||
function exec_ogp_module()
|
||||
{
|
||||
global $db;
|
||||
|
||||
echo '<h2>Steam Workshop – Admin</h2>';
|
||||
sw_admin_print_styles();
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_profile'])) {
|
||||
sw_admin_save_profile($db);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sync_profiles'])) {
|
||||
$n = sw_sync_profiles($db);
|
||||
sw_success("Sync complete. $n new profile(s) created.");
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['detect_defaults'])) {
|
||||
$profile = sw_get_profile_by_id($db, (int)($_POST['id'] ?? 0));
|
||||
if (!$profile) {
|
||||
sw_error('Profile not found.');
|
||||
sw_admin_list($db);
|
||||
return;
|
||||
}
|
||||
$detected = sw_detect_profile_defaults_from_xml($profile['config_name']);
|
||||
if (empty($detected)) {
|
||||
sw_error('No Steam defaults were detected in this game XML. You can still enter values manually.');
|
||||
} else {
|
||||
sw_success('Detected XML defaults. Review and apply when ready.');
|
||||
}
|
||||
sw_admin_edit_form($profile, $detected, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['apply_detected_defaults'])) {
|
||||
$profile = sw_get_profile_by_id($db, (int)($_POST['id'] ?? 0));
|
||||
if (!$profile) {
|
||||
sw_error('Profile not found.');
|
||||
sw_admin_list($db);
|
||||
return;
|
||||
}
|
||||
$detected = sw_detect_profile_defaults_from_xml($profile['config_name']);
|
||||
if (empty($detected)) {
|
||||
sw_error('No Steam defaults were detected in this game XML.');
|
||||
sw_admin_edit_form($profile);
|
||||
return;
|
||||
}
|
||||
|
||||
$overwrite = isset($_POST['overwrite_existing']) && $_POST['overwrite_existing'] === '1';
|
||||
$updated = sw_apply_detected_profile_defaults($db, $profile, $detected, $overwrite);
|
||||
if ($updated > 0) {
|
||||
$overwriteMessage = $overwrite
|
||||
? ' Existing values were allowed to be overwritten.'
|
||||
: ' Existing non-empty values were kept.';
|
||||
sw_success("Applied $updated detected default value(s)." . $overwriteMessage);
|
||||
} else {
|
||||
sw_success('No profile values needed updating based on current overwrite setting.');
|
||||
}
|
||||
|
||||
$profile = sw_get_profile_by_id($db, (int)$profile['id']);
|
||||
sw_admin_edit_form($profile, $detected, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($action === 'edit' && isset($_GET['id'])) {
|
||||
$profile = sw_get_profile_by_id($db, (int)$_GET['id']);
|
||||
if ($profile) {
|
||||
sw_admin_edit_form($profile);
|
||||
} else {
|
||||
sw_error('Profile not found.');
|
||||
sw_admin_list($db);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sw_admin_list($db);
|
||||
}
|
||||
|
||||
function sw_admin_save_profile($db)
|
||||
{
|
||||
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||
if (!$id) {
|
||||
sw_error('Invalid profile ID.');
|
||||
sw_admin_list($db);
|
||||
return;
|
||||
}
|
||||
|
||||
$profile = sw_get_profile_by_id($db, $id);
|
||||
if (!$profile) {
|
||||
sw_error('Profile not found.');
|
||||
sw_admin_list($db);
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = array(
|
||||
'enabled' => isset($_POST['enabled']) ? 1 : 0,
|
||||
'steam_app_id' => trim($_POST['steam_app_id'] ?? ''),
|
||||
'workshop_app_id' => trim($_POST['workshop_app_id'] ?? ''),
|
||||
'steam_login_required' => isset($_POST['steam_login_required']) ? 1 : 0,
|
||||
'steamcmd_login_mode' => (($_POST['steamcmd_login_mode'] ?? 'anonymous') === 'account') ? 'account' : 'anonymous',
|
||||
'steamcmd_path' => trim($_POST['steamcmd_path'] ?? ''),
|
||||
'workshop_download_dir_template' => trim($_POST['workshop_download_dir_template'] ?? ''),
|
||||
'server_root_template' => trim($_POST['server_root_template'] ?? ''),
|
||||
'install_path_template' => trim($_POST['install_path_template'] ?? ''),
|
||||
'folder_naming_format' => trim($_POST['folder_naming_format'] ?? ''),
|
||||
'mod_launch_param_template' => trim($_POST['mod_launch_param_template'] ?? '-mod='),
|
||||
'servermod_launch_param_template' => trim($_POST['servermod_launch_param_template'] ?? '-serverMod='),
|
||||
'install_script_template' => trim($_POST['install_script_template'] ?? ''),
|
||||
'update_script_template' => trim($_POST['update_script_template'] ?? ''),
|
||||
'copy_bikeys_enabled' => isset($_POST['copy_bikeys_enabled']) ? 1 : 0,
|
||||
'notes' => trim($_POST['notes'] ?? ''),
|
||||
);
|
||||
|
||||
// Per-profile default behavior fields
|
||||
$valid_update_modes = array('manual', 'on_restart', 'before_start');
|
||||
$valid_restart_behaviors = array('none', 'if_stopped');
|
||||
$posted_um = $_POST['default_update_mode'] ?? 'manual';
|
||||
$posted_rb = $_POST['default_restart_behavior'] ?? 'none';
|
||||
$fields['default_update_mode'] = in_array($posted_um, $valid_update_modes, true) ? $posted_um : 'manual';
|
||||
$fields['default_restart_behavior'] = in_array($posted_rb, $valid_restart_behaviors, true) ? $posted_rb : 'none';
|
||||
|
||||
$setParts = array();
|
||||
foreach ($fields as $col => $val) {
|
||||
$setParts[] = "`$col` = '" . $db->realEscapeSingle($val) . "'";
|
||||
}
|
||||
$setParts[] = "`updated_at` = NOW()";
|
||||
|
||||
$ok = $db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_game_profiles') . "
|
||||
SET " . implode(', ', $setParts) . "
|
||||
WHERE `id` = $id LIMIT 1"
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
sw_success('Profile saved.');
|
||||
} else {
|
||||
sw_error('Failed to save profile.');
|
||||
}
|
||||
|
||||
$profile = sw_get_profile_by_id($db, $id);
|
||||
if ($profile) {
|
||||
sw_admin_edit_form($profile);
|
||||
} else {
|
||||
sw_admin_list($db);
|
||||
}
|
||||
}
|
||||
|
||||
function sw_admin_list($db)
|
||||
{
|
||||
$profiles = sw_get_profiles($db);
|
||||
?>
|
||||
<div class="sw-admin-panel">
|
||||
<p class="sw-muted">
|
||||
Profiles map game XML configs to Steam Workshop defaults. Sync to create missing profiles, then edit each profile for game-specific paths and launch templates.
|
||||
</p>
|
||||
|
||||
<form method="post" style="margin-bottom:12px;">
|
||||
<button type="submit" name="sync_profiles" value="1" class="button"
|
||||
onclick="return confirm('Sync workshop profiles from all game config XMLs?');">Sync Profiles from XML Configs</button>
|
||||
</form>
|
||||
|
||||
<?php if (empty($profiles)): ?>
|
||||
<p>No profiles yet. Click <em>Sync Profiles</em> to create them from installed game configs.</p>
|
||||
<?php else: ?>
|
||||
<table class="sw-admin-table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Config Name</th>
|
||||
<th>Game Name</th>
|
||||
<th style="text-align:center;">Steam App ID</th>
|
||||
<th style="text-align:center;">Workshop App ID</th>
|
||||
<th style="text-align:center;">Enabled</th>
|
||||
<th style="text-align:center;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($profiles as $p): ?>
|
||||
<tr>
|
||||
<td><code><?= sw_h($p['config_name']) ?></code></td>
|
||||
<td><?= sw_h($p['game_name']) ?></td>
|
||||
<td style="text-align:center;"><?= sw_h($p['steam_app_id']) ?></td>
|
||||
<td style="text-align:center;"><?= sw_h($p['workshop_app_id']) ?></td>
|
||||
<td style="text-align:center;"><?= $p['enabled'] ? '<span class="sw-state-on">Yes</span>' : '<span class="sw-state-off">No</span>' ?></td>
|
||||
<td style="text-align:center;"><a class="button small" href="home.php?m=steam_workshop&p=admin&action=edit&id=<?= (int)$p['id'] ?>">Edit</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
function sw_admin_edit_form(array $profile, array $detected = array(), $showDetectedBox = false)
|
||||
{
|
||||
$id = (int)$profile['id'];
|
||||
?>
|
||||
<p><a href="home.php?m=steam_workshop&p=admin">« Back to profile list</a></p>
|
||||
<h3>Edit Profile: <?= sw_h($profile['config_name']) ?> – <?= sw_h($profile['game_name']) ?></h3>
|
||||
|
||||
<div class="sw-admin-panel">
|
||||
<div class="sw-note">
|
||||
<strong>Placeholder tokens:</strong>
|
||||
<code>{SERVER_ROOT}</code> <code>{HOME_ID}</code> <code>{STEAM_APP_ID}</code> <code>{WORKSHOP_APP_ID}</code> <code>{MOD_FOLDER}</code>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>XML-Assisted Defaults</h4>
|
||||
<p class="sw-muted">Use values detected from this game XML. Existing values are not overwritten unless you explicitly allow it.</p>
|
||||
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>" style="display:inline-block; margin-right:8px;">
|
||||
<input type="hidden" name="id" value="<?= $id ?>">
|
||||
<button type="submit" name="detect_defaults" value="1" class="button">Detect from XML</button>
|
||||
</form>
|
||||
|
||||
<?php if ($showDetectedBox && !empty($detected)): ?>
|
||||
<div class="sw-detected-box">
|
||||
<strong>Detected values:</strong>
|
||||
<ul>
|
||||
<li>Steam App ID: <code><?= sw_h($detected['steam_app_id'] ?? '') ?></code></li>
|
||||
<li>Workshop App ID: <code><?= sw_h($detected['workshop_app_id'] ?? '') ?></code></li>
|
||||
<li>SteamCMD path: <code><?= sw_h($detected['steamcmd_path'] ?? '') ?></code></li>
|
||||
<li>Workshop download dir: <code><?= sw_h($detected['workshop_download_dir_template'] ?? '') ?></code></li>
|
||||
<li>Server root: <code><?= sw_h($detected['server_root_template'] ?? '') ?></code></li>
|
||||
<li>Mod install path: <code><?= sw_h($detected['install_path_template'] ?? '') ?></code></li>
|
||||
</ul>
|
||||
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>">
|
||||
<input type="hidden" name="id" value="<?= $id ?>">
|
||||
<label style="display:block;margin-bottom:8px;">
|
||||
<input type="checkbox" name="overwrite_existing" value="1">
|
||||
Allow overwrite of existing non-empty values.
|
||||
</label>
|
||||
<button type="submit" name="apply_detected_defaults" value="1" class="button">Refresh defaults from XML</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>">
|
||||
<input type="hidden" name="id" value="<?= $id ?>">
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Global Profile Defaults</h4>
|
||||
<div class="sw-grid">
|
||||
<label><span>Enabled</span><input type="checkbox" name="enabled" value="1" <?= $profile['enabled'] ? 'checked' : '' ?>></label>
|
||||
<label><span>Steam App ID</span><input type="text" name="steam_app_id" value="<?= sw_h($profile['steam_app_id']) ?>" placeholder="Detected from XML when available"></label>
|
||||
<label><span>Workshop App ID</span><input type="text" name="workshop_app_id" value="<?= sw_h($profile['workshop_app_id']) ?>" placeholder="Detected from XML when available"></label>
|
||||
<label><span>SteamCMD Path</span><input type="text" name="steamcmd_path" value="<?= sw_h($profile['steamcmd_path']) ?>" placeholder="/home/gameserver/steamcmd/steamcmd.sh"></label>
|
||||
<label><span>Steam Login Required</span><input type="checkbox" name="steam_login_required" value="1" <?= $profile['steam_login_required'] ? 'checked' : '' ?>></label>
|
||||
<label><span>SteamCMD Login Mode</span>
|
||||
<select name="steamcmd_login_mode">
|
||||
<option value="anonymous" <?= $profile['steamcmd_login_mode'] === 'anonymous' ? 'selected' : '' ?>>anonymous</option>
|
||||
<option value="account" <?= $profile['steamcmd_login_mode'] === 'account' ? 'selected' : '' ?>>account</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Path Templates</h4>
|
||||
<p class="sw-muted">Use placeholders so paths stay portable between server homes.</p>
|
||||
<div class="sw-grid">
|
||||
<label><span>Workshop Download Directory</span><input type="text" name="workshop_download_dir_template" value="<?= sw_h($profile['workshop_download_dir_template']) ?>" placeholder="{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}"></label>
|
||||
<label><span>Server Root</span><input type="text" name="server_root_template" value="<?= sw_h($profile['server_root_template']) ?>" placeholder="{SERVER_ROOT}"></label>
|
||||
<label><span>Mod Install Path</span><input type="text" name="install_path_template" value="<?= sw_h($profile['install_path_template']) ?>" placeholder="{SERVER_ROOT}/{MOD_FOLDER}"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Per-Game Runtime Values</h4>
|
||||
<div class="sw-grid">
|
||||
<label><span>Folder Naming Format</span><input type="text" name="folder_naming_format" value="<?= sw_h($profile['folder_naming_format']) ?>" placeholder="@{MOD_NAME}"></label>
|
||||
<label><span>Client Launch Param</span><input type="text" name="mod_launch_param_template" value="<?= sw_h($profile['mod_launch_param_template']) ?>" placeholder="-mod="></label>
|
||||
<label><span>Server Launch Param</span><input type="text" name="servermod_launch_param_template" value="<?= sw_h($profile['servermod_launch_param_template']) ?>" placeholder="-serverMod="></label>
|
||||
<label><span>Copy .bikey files</span><input type="checkbox" name="copy_bikeys_enabled" value="1" <?= $profile['copy_bikeys_enabled'] ? 'checked' : '' ?>></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Optional Script Templates</h4>
|
||||
<label><span>Install Script Template</span><textarea name="install_script_template" rows="6"><?= sw_h($profile['install_script_template']) ?></textarea></label>
|
||||
<label><span>Update Script Template</span><textarea name="update_script_template" rows="6"><?= sw_h($profile['update_script_template']) ?></textarea></label>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Notes</h4>
|
||||
<label><textarea name="notes" rows="4"><?= sw_h($profile['notes']) ?></textarea></label>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Default Workshop Behavior for New Servers</h4>
|
||||
<p class="sw-muted">
|
||||
These defaults are applied when a user enables Workshop on a server that has no saved behavior settings yet.
|
||||
Users can always override them on their own server pages.
|
||||
All defaults are intentionally set to the safest option (manual / no automatic restart).
|
||||
</p>
|
||||
<div class="sw-grid">
|
||||
<label>
|
||||
<span>Default Install / Update Mode</span>
|
||||
<select name="default_update_mode">
|
||||
<option value="manual" <?= (($profile['default_update_mode'] ?? 'manual') === 'manual') ? 'selected' : '' ?>>Manual only (safe default)</option>
|
||||
<option value="on_restart" <?= (($profile['default_update_mode'] ?? 'manual') === 'on_restart') ? 'selected' : '' ?>>On next restart</option>
|
||||
<option value="before_start" <?= (($profile['default_update_mode'] ?? 'manual') === 'before_start') ? 'selected' : '' ?>>Before every server start</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Default Restart Behavior</span>
|
||||
<select name="default_restart_behavior">
|
||||
<option value="none" <?= (($profile['default_restart_behavior'] ?? 'none') === 'none') ? 'selected' : '' ?>>Do not restart automatically (safe default)</option>
|
||||
<option value="if_stopped" <?= (($profile['default_restart_behavior'] ?? 'none') === 'if_stopped') ? 'selected' : '' ?>>Restart only if server is stopped</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<button type="submit" name="save_profile" value="1" class="button">Save Profile</button>
|
||||
<a href="home.php?m=steam_workshop&p=admin" class="button">Cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
function sw_admin_print_styles()
|
||||
{
|
||||
static $printed = false;
|
||||
if ($printed) {
|
||||
return;
|
||||
}
|
||||
$printed = true;
|
||||
echo '<style>
|
||||
.sw-admin-panel{background:#171717;border:1px solid #2d2d2d;border-radius:6px;padding:14px;margin:10px 0;color:#e7e7e7}
|
||||
.sw-admin-table{border-collapse:collapse;background:#121212}
|
||||
.sw-admin-table th,.sw-admin-table td{border:1px solid #2c2c2c;padding:8px}
|
||||
.sw-admin-table thead th{background:#232323;color:#fff}
|
||||
.sw-state-on{color:#78d978;font-weight:700}
|
||||
.sw-state-off{color:#9a9a9a}
|
||||
.sw-section{margin-top:14px;padding:12px;border:1px solid #2f2f2f;border-radius:4px;background:#111}
|
||||
.sw-section h4{margin:0 0 8px 0;color:#f6f6f6}
|
||||
.sw-note{margin-bottom:10px;background:#202020;border-left:3px solid #3f80d0;padding:10px}
|
||||
.sw-muted{color:#b3b3b3}
|
||||
.sw-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:10px}
|
||||
.sw-grid label, .sw-section > label{display:block}
|
||||
.sw-grid span, .sw-section span{display:block;font-size:12px;color:#bdbdbd;margin-bottom:4px}
|
||||
.sw-grid input[type=text], .sw-grid select, .sw-section textarea{width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #3a3a3a;color:#eee;padding:7px;border-radius:4px}
|
||||
.sw-grid input[type=checkbox]{transform:scale(1.1);margin-top:4px}
|
||||
.sw-detected-box{margin-top:10px;padding:10px;background:#1d2a1d;border:1px solid #335933;border-radius:4px}
|
||||
.sw-detected-box ul{margin:8px 0 10px 18px}
|
||||
.sw-detected-box code,.sw-note code{background:#0b0b0b;padding:1px 4px;border-radius:3px;color:#9fd4ff}
|
||||
</style>';
|
||||
}
|
||||
|
|
@ -1,569 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: Agent CLI update script
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* This file must only be run from the command line (CLI).
|
||||
* Do NOT expose it through a web server.
|
||||
*
|
||||
* Usage:
|
||||
* php agent_update_workshop.php --home-id=123
|
||||
* php agent_update_workshop.php --all
|
||||
*
|
||||
* The script connects to the panel database, reads the list of enabled mods
|
||||
* for the specified server(s), downloads/updates each mod via SteamCMD,
|
||||
* copies mod folders into the server root, copies .bikey files into the
|
||||
* server keys/ directory, and updates the install status in the database.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
// ── Safety: CLI only ──────────────────────────────────────────────────────
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
exit("This script may only be run from the command line.\n");
|
||||
}
|
||||
|
||||
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
||||
$panel_root = realpath(__DIR__ . '/../../..');
|
||||
if (!$panel_root || !is_dir($panel_root)) {
|
||||
fwrite(STDERR, "ERROR: Cannot locate panel root from " . __DIR__ . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$config_file = $panel_root . '/includes/config.inc.php';
|
||||
if (!is_file($config_file)) {
|
||||
fwrite(STDERR, "ERROR: Panel config not found: $config_file\n");
|
||||
fwrite(STDERR, " Copy includes/config.inc.php.example to includes/config.inc.php and set credentials.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require_once $config_file;
|
||||
require_once $panel_root . '/includes/helpers.php';
|
||||
require_once $panel_root . '/includes/database_mysqli.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
// ── Database connection ───────────────────────────────────────────────────
|
||||
// Variables $db_host, $db_user, $db_pass, $db_name, $table_prefix, $db_type,
|
||||
// $db_port come from config.inc.php (loaded above).
|
||||
$db = createDatabaseConnection(
|
||||
$db_type,
|
||||
$db_host,
|
||||
$db_user,
|
||||
$db_pass,
|
||||
$db_name,
|
||||
$table_prefix,
|
||||
isset($db_port) ? $db_port : null
|
||||
);
|
||||
|
||||
if (!is_object($db)) {
|
||||
$error_text = '';
|
||||
get_db_error_text($db, $error_text);
|
||||
fwrite(STDERR, "ERROR: Database connection failed: $error_text\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ── Argument parsing ──────────────────────────────────────────────────────
|
||||
$opts = getopt('', array('home-id:', 'all', 'dry-run'));
|
||||
|
||||
$do_all = array_key_exists('all', $opts);
|
||||
$dry_run = array_key_exists('dry-run', $opts);
|
||||
$target_id = isset($opts['home-id']) ? (int)$opts['home-id'] : 0;
|
||||
|
||||
if (!$do_all && !$target_id) {
|
||||
fwrite(STDERR, "Usage:\n");
|
||||
fwrite(STDERR, " php agent_update_workshop.php --home-id=123\n");
|
||||
fwrite(STDERR, " php agent_update_workshop.php --all\n");
|
||||
fwrite(STDERR, " Add --dry-run to simulate without running SteamCMD or copying files.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($dry_run) {
|
||||
echo "[DRY RUN] No files will be modified and SteamCMD will not be called.\n";
|
||||
}
|
||||
|
||||
// ── Collect home IDs to process ───────────────────────────────────────────
|
||||
if ($do_all) {
|
||||
// Find all home_ids that have at least one enabled queued mod with an enabled profile.
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT DISTINCT m.home_id
|
||||
FROM " . sw_table('steam_workshop_server_mods') . " m
|
||||
JOIN " . sw_table('steam_workshop_game_profiles') . " p ON p.id = m.profile_id
|
||||
WHERE m.enabled = 1 AND p.enabled = 1 AND m.install_status = 'queued'"
|
||||
);
|
||||
$home_ids = $rows ? array_column($rows, 'home_id') : array();
|
||||
} else {
|
||||
$home_ids = array($target_id);
|
||||
}
|
||||
|
||||
if (empty($home_ids)) {
|
||||
echo "No servers with enabled Workshop mods found.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$overall_success = true;
|
||||
|
||||
foreach ($home_ids as $home_id) {
|
||||
$home_id = (int)$home_id;
|
||||
echo "\n=== Processing home_id=$home_id ===\n";
|
||||
|
||||
$ok = sw_agent_process_home($db, $home_id, $dry_run);
|
||||
if (!$ok) {
|
||||
$overall_success = false;
|
||||
echo " [WARN] One or more errors occurred for home_id=$home_id.\n";
|
||||
}
|
||||
}
|
||||
|
||||
exit($overall_success ? 0 : 1);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Core logic
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Process all enabled mods for one server home.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @param bool $dry_run
|
||||
* @return bool true if all mods processed without errors
|
||||
*/
|
||||
function sw_agent_process_home($db, $home_id, $dry_run)
|
||||
{
|
||||
// Load server info
|
||||
$home = sw_get_home_info($db, $home_id);
|
||||
if (!$home) {
|
||||
echo " [ERROR] Server home $home_id not found in database.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
echo " Server: " . $home['home_name'] . " (game: " . $home['game_name'] . ")\n";
|
||||
echo " Path: " . $home['home_path'] . "\n";
|
||||
|
||||
// Resolve Workshop profile via game config key
|
||||
$profile = sw_get_profile_for_home($db, $home_id);
|
||||
if (!$profile) {
|
||||
echo " [SKIP] No enabled Workshop profile for this game type.\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
echo " Profile: " . $profile['config_name'] . " (workshop_app_id=" . $profile['workshop_app_id'] . ")\n";
|
||||
|
||||
// Build common template variables
|
||||
$server_root = sw_apply_template(
|
||||
$profile['server_root_template'] ?: $home['home_path'],
|
||||
sw_agent_tpl_vars($home, $profile)
|
||||
);
|
||||
$server_root = rtrim($server_root, '/');
|
||||
|
||||
// Load queued+enabled mods, sorted by sort_order
|
||||
$mods = sw_agent_get_queued_mods($db, $home_id);
|
||||
|
||||
if (empty($mods)) {
|
||||
echo " No queued Workshop updates for this server.\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
$keys_dir = $server_root . '/keys';
|
||||
if (!$dry_run && !is_dir($keys_dir)) {
|
||||
@mkdir($keys_dir, 0755, true);
|
||||
}
|
||||
|
||||
$all_ok = true;
|
||||
|
||||
foreach ($mods as $mod) {
|
||||
$mod_id = (int)$mod['id'];
|
||||
$workshop_id = $mod['workshop_id'];
|
||||
echo "\n Mod: " . ($mod['mod_name'] ?: $workshop_id) . " [ID=$workshop_id]\n";
|
||||
|
||||
// Mark as updating
|
||||
if (!$dry_run) {
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'updating', `last_error` = NULL, `updated_at` = NOW()
|
||||
WHERE `id` = $mod_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
// Build template vars for this mod
|
||||
$folder_name = $mod['folder_name'] ?: ('@' . $workshop_id);
|
||||
$tpl_vars = array_merge(
|
||||
sw_agent_tpl_vars($home, $profile),
|
||||
array(
|
||||
'WORKSHOP_ID' => $workshop_id,
|
||||
'MOD_NAME' => $mod['mod_name'] ?: $workshop_id,
|
||||
'FOLDER_NAME' => $folder_name,
|
||||
'MOD_FOLDER' => $folder_name,
|
||||
'SERVER_ROOT' => $server_root,
|
||||
'WORKSHOP_DOWNLOAD_DIR' => sw_apply_template(
|
||||
$profile['workshop_download_dir_template']
|
||||
?: ($server_root . '/steamapps/workshop/content/' . $profile['workshop_app_id']),
|
||||
array(
|
||||
'SERVER_ROOT' => $server_root,
|
||||
'WORKSHOP_APP_ID' => $profile['workshop_app_id'],
|
||||
'HOME_ID' => $home['home_id'],
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$download_dir = $tpl_vars['WORKSHOP_DOWNLOAD_DIR'];
|
||||
$mod_cache = rtrim($download_dir, '/') . '/' . $workshop_id;
|
||||
|
||||
// 1. Download / update via SteamCMD
|
||||
$cmd_result = sw_agent_steamcmd_download($mod, $profile, $tpl_vars, $dry_run);
|
||||
if (!$cmd_result['ok']) {
|
||||
$err = $cmd_result['error'];
|
||||
echo " [ERROR] SteamCMD failed: $err\n";
|
||||
if (!$dry_run) {
|
||||
$safe_err = $db->realEscapeSingle($err);
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'failed', `last_error` = '$safe_err', `updated_at` = NOW()
|
||||
WHERE `id` = $mod_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
$all_ok = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Copy / sync mod folder to server root
|
||||
$install_path = sw_apply_template(
|
||||
$profile['install_path_template'] ?: ($server_root . '/{MOD_FOLDER}'),
|
||||
$tpl_vars
|
||||
);
|
||||
|
||||
$copy_ok = sw_agent_copy_mod($mod_cache, $install_path, $dry_run);
|
||||
if (!$copy_ok) {
|
||||
$err = "Failed to copy mod from $mod_cache to $install_path";
|
||||
echo " [ERROR] $err\n";
|
||||
if (!$dry_run) {
|
||||
$safe_err = $db->realEscapeSingle($err);
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'failed', `last_error` = '$safe_err', `updated_at` = NOW()
|
||||
WHERE `id` = $mod_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
$all_ok = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Copy .bikey files to server keys/ directory
|
||||
if (!empty($profile['copy_bikeys_enabled'])) {
|
||||
sw_agent_copy_bikeys($install_path, $keys_dir, $dry_run);
|
||||
}
|
||||
|
||||
// 4. Mark as installed
|
||||
if (!$dry_run) {
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'installed',
|
||||
`last_installed_at` = NOW(),
|
||||
`last_updated_at` = NOW(),
|
||||
`last_error` = NULL,
|
||||
`updated_at` = NOW()
|
||||
WHERE `id` = $mod_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
echo " [OK] Installed → $install_path\n";
|
||||
}
|
||||
|
||||
return $all_ok;
|
||||
}
|
||||
|
||||
function sw_agent_get_queued_mods($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `home_id` = $home_id
|
||||
AND `enabled` = 1
|
||||
AND `install_status` = 'queued'
|
||||
ORDER BY `sort_order` ASC, `id` ASC"
|
||||
);
|
||||
return $rows ? $rows : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the standard template variable map for a given home + profile.
|
||||
*
|
||||
* @param array $home
|
||||
* @param array $profile
|
||||
* @return array
|
||||
*/
|
||||
function sw_agent_tpl_vars(array $home, array $profile)
|
||||
{
|
||||
return array(
|
||||
'HOME_ID' => $home['home_id'],
|
||||
'SERVER_ID' => $home['home_id'],
|
||||
'REMOTE_SERVER_ID' => $home['remote_server_id'],
|
||||
'GAME_NAME' => $home['game_name'],
|
||||
'CONFIG_NAME' => $home['game_key'],
|
||||
'STEAM_APP_ID' => $profile['steam_app_id'],
|
||||
'WORKSHOP_APP_ID' => $profile['workshop_app_id'],
|
||||
'STEAMCMD_PATH' => $profile['steamcmd_path'],
|
||||
'SERVER_ROOT' => $home['home_path'],
|
||||
'INSTALL_PATH' => $home['home_path'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run SteamCMD to download / update a single Workshop item.
|
||||
*
|
||||
* Uses the profile's update_script_template if set; otherwise falls back to
|
||||
* a standard anonymous or authenticated +workshop_download_item invocation.
|
||||
*
|
||||
* @param array $mod
|
||||
* @param array $profile
|
||||
* @param array $tpl_vars
|
||||
* @param bool $dry_run
|
||||
* @return array ['ok' => bool, 'error' => string]
|
||||
*/
|
||||
function sw_agent_steamcmd_download(array $mod, array $profile, array $tpl_vars, $dry_run)
|
||||
{
|
||||
$steamcmd = $profile['steamcmd_path'] ?: '/home/gameserver/steamcmd/steamcmd.sh';
|
||||
$workshop_id = $mod['workshop_id'];
|
||||
$app_id = $profile['workshop_app_id'];
|
||||
$dl_dir = $tpl_vars['WORKSHOP_DOWNLOAD_DIR'];
|
||||
|
||||
if (!empty($profile['update_script_template'])) {
|
||||
// Admin has provided a custom update script.
|
||||
$script_body = sw_apply_template($profile['update_script_template'], $tpl_vars);
|
||||
return sw_agent_run_script($script_body, $dry_run);
|
||||
}
|
||||
|
||||
// Build default SteamCMD command.
|
||||
// +force_install_dir is set to the parent of the workshop content so that
|
||||
// SteamCMD places files in <dl_dir>/<workshop_id>/.
|
||||
$parent_dir = dirname($dl_dir); // .../steamapps/workshop/content
|
||||
|
||||
if ($profile['steamcmd_login_mode'] === 'account') {
|
||||
// When account login is required the operator must supply credentials
|
||||
// in the update_script_template. We cannot safely store a password here.
|
||||
return array(
|
||||
'ok' => false,
|
||||
'error' => "Account login is required for this profile but no update_script_template is set. "
|
||||
. "Add a custom update_script_template in the admin profile that includes SteamCMD login credentials.",
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that steamcmd exists
|
||||
if (!$dry_run) {
|
||||
if (!is_file($steamcmd)) {
|
||||
return array('ok' => false, 'error' => "SteamCMD not found: $steamcmd");
|
||||
}
|
||||
if (!is_executable($steamcmd)) {
|
||||
return array('ok' => false, 'error' => "SteamCMD is not executable: $steamcmd");
|
||||
}
|
||||
}
|
||||
|
||||
// Build argument list; escape each argument individually.
|
||||
$args = array(
|
||||
escapeshellarg($steamcmd),
|
||||
'+force_install_dir', escapeshellarg($parent_dir),
|
||||
'+login', 'anonymous',
|
||||
'+workshop_download_item', escapeshellarg($app_id), escapeshellarg($workshop_id),
|
||||
'+quit',
|
||||
);
|
||||
$cmd = implode(' ', $args);
|
||||
|
||||
echo " SteamCMD: $cmd\n";
|
||||
|
||||
if ($dry_run) {
|
||||
echo " [DRY RUN] Skipping SteamCMD execution.\n";
|
||||
return array('ok' => true, 'error' => '');
|
||||
}
|
||||
|
||||
$output = array();
|
||||
$return_var = 0;
|
||||
exec($cmd . ' 2>&1', $output, $return_var);
|
||||
|
||||
foreach ($output as $line) {
|
||||
echo " $line\n";
|
||||
}
|
||||
|
||||
if ($return_var !== 0) {
|
||||
return array(
|
||||
'ok' => false,
|
||||
'error' => "SteamCMD exited with code $return_var. " . implode(' ', array_slice($output, -3)),
|
||||
);
|
||||
}
|
||||
|
||||
return array('ok' => true, 'error' => '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a shell script body.
|
||||
* The script is written to a temporary file and executed with /bin/sh.
|
||||
*
|
||||
* @param string $script_body
|
||||
* @param bool $dry_run
|
||||
* @return array ['ok' => bool, 'error' => string]
|
||||
*/
|
||||
function sw_agent_run_script($script_body, $dry_run)
|
||||
{
|
||||
if ($dry_run) {
|
||||
echo " [DRY RUN] Would execute script:\n";
|
||||
foreach (explode("\n", $script_body) as $line) {
|
||||
echo " $line\n";
|
||||
}
|
||||
return array('ok' => true, 'error' => '');
|
||||
}
|
||||
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'sw_agent_');
|
||||
if (!$tmp) {
|
||||
return array('ok' => false, 'error' => 'Could not create temporary file for script.');
|
||||
}
|
||||
|
||||
file_put_contents($tmp, "#!/bin/sh\nset -e\n" . $script_body);
|
||||
chmod($tmp, 0700);
|
||||
|
||||
$output = array();
|
||||
$return_var = 0;
|
||||
exec('/bin/sh ' . escapeshellarg($tmp) . ' 2>&1', $output, $return_var);
|
||||
@unlink($tmp);
|
||||
|
||||
foreach ($output as $line) {
|
||||
echo " $line\n";
|
||||
}
|
||||
|
||||
if ($return_var !== 0) {
|
||||
return array(
|
||||
'ok' => false,
|
||||
'error' => "Script exited with code $return_var. " . implode(' ', array_slice($output, -3)),
|
||||
);
|
||||
}
|
||||
|
||||
return array('ok' => true, 'error' => '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy (rsync-style) the downloaded mod folder into the server root.
|
||||
* Uses rsync when available, falls back to recursive PHP copy.
|
||||
*
|
||||
* @param string $src Downloaded mod folder (e.g. .../content/221100/2863534533)
|
||||
* @param string $dst Target path in server root (e.g. /servers/123/@CF)
|
||||
* @param bool $dry_run
|
||||
* @return bool
|
||||
*/
|
||||
function sw_agent_copy_mod($src, $dst, $dry_run)
|
||||
{
|
||||
echo " Copy: $src → $dst\n";
|
||||
|
||||
if (!$dry_run && !is_dir($src)) {
|
||||
echo " [WARN] Source directory not found: $src\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($dry_run) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_dir($dst)) {
|
||||
if (!@mkdir($dst, 0755, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Try rsync first (preserves permissions, handles deletes cleanly).
|
||||
if (sw_agent_cmd_exists('rsync')) {
|
||||
$cmd = 'rsync -a --delete '
|
||||
. escapeshellarg(rtrim($src, '/') . '/') . ' '
|
||||
. escapeshellarg(rtrim($dst, '/') . '/') . ' 2>&1';
|
||||
exec($cmd, $out, $ret);
|
||||
if ($ret === 0) {
|
||||
return true;
|
||||
}
|
||||
echo " [WARN] rsync failed (exit $ret); falling back to PHP copy.\n";
|
||||
}
|
||||
|
||||
// PHP recursive copy fallback.
|
||||
return sw_agent_recursive_copy($src, $dst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all .bikey files found recursively under $mod_dir/keys/ into $keys_dir.
|
||||
*
|
||||
* @param string $mod_dir Installed mod directory
|
||||
* @param string $keys_dir Server keys directory
|
||||
* @param bool $dry_run
|
||||
* @return void
|
||||
*/
|
||||
function sw_agent_copy_bikeys($mod_dir, $keys_dir, $dry_run)
|
||||
{
|
||||
// Search in common key locations within the mod folder.
|
||||
$search_dirs = array(
|
||||
$mod_dir . '/keys',
|
||||
$mod_dir . '/Keys',
|
||||
$mod_dir . '/key',
|
||||
$mod_dir . '/Key',
|
||||
);
|
||||
|
||||
$found = 0;
|
||||
foreach ($search_dirs as $kdir) {
|
||||
if (!is_dir($kdir)) {
|
||||
continue;
|
||||
}
|
||||
foreach (glob($kdir . '/*.bikey') as $bikey) {
|
||||
$target = $keys_dir . '/' . basename($bikey);
|
||||
echo " .bikey: " . basename($bikey) . " → $keys_dir/\n";
|
||||
if (!$dry_run) {
|
||||
@copy($bikey, $target);
|
||||
}
|
||||
$found++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($found === 0) {
|
||||
echo " (no .bikey files found in mod keys/ folder)\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if a command is available in PATH.
|
||||
*
|
||||
* @param string $cmd
|
||||
* @return bool
|
||||
*/
|
||||
function sw_agent_cmd_exists($cmd)
|
||||
{
|
||||
$which = trim((string)shell_exec('which ' . escapeshellarg($cmd) . ' 2>/dev/null'));
|
||||
return !empty($which);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copy directory $src into $dst (creating $dst if needed).
|
||||
*
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
function sw_agent_recursive_copy($src, $dst)
|
||||
{
|
||||
$dir = @opendir($src);
|
||||
if (!$dir) {
|
||||
return false;
|
||||
}
|
||||
if (!is_dir($dst)) {
|
||||
@mkdir($dst, 0755, true);
|
||||
}
|
||||
while (false !== ($file = readdir($dir))) {
|
||||
if ($file === '.' || $file === '..') {
|
||||
continue;
|
||||
}
|
||||
$s = $src . '/' . $file;
|
||||
$d = $dst . '/' . $file;
|
||||
if (is_dir($s)) {
|
||||
sw_agent_recursive_copy($s, $d);
|
||||
} else {
|
||||
copy($s, $d);
|
||||
}
|
||||
}
|
||||
closedir($dir);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,652 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: shared helper functions
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
function sw_db_prefix()
|
||||
{
|
||||
if (defined('DB_PREFIX') && DB_PREFIX !== '') {
|
||||
return DB_PREFIX;
|
||||
}
|
||||
if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') {
|
||||
return $GLOBALS['db_prefix'];
|
||||
}
|
||||
if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') {
|
||||
return $GLOBALS['table_prefix'];
|
||||
}
|
||||
return 'gsp_';
|
||||
}
|
||||
|
||||
function sw_table($tableName)
|
||||
{
|
||||
return '`' . sw_db_prefix() . $tableName . '`';
|
||||
}
|
||||
|
||||
// ── Profile helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return all rows from steam_workshop_game_profiles ordered by game_name.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_profiles($db)
|
||||
{
|
||||
return $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_game_profiles') . "
|
||||
ORDER BY `game_name` ASC, `config_name` ASC"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single profile row by primary key.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $id
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_profile_by_id($db, $id)
|
||||
{
|
||||
$id = (int)$id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_game_profiles') . "
|
||||
WHERE `id` = $id LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single profile row by config_name (= game_key from XML).
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param string $config_name
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_profile_by_config_name($db, $config_name)
|
||||
{
|
||||
$safe = $db->realEscapeSingle($config_name);
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_game_profiles') . "
|
||||
WHERE `config_name` = '$safe' LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Workshop profile that applies to a server home.
|
||||
* Resolves: home_id → config_homes.game_key → workshop profile.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @return array|false profile row or false when none found / not enabled
|
||||
*/
|
||||
function sw_get_profile_for_home($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT p.*
|
||||
FROM " . sw_table('steam_workshop_game_profiles') . " p
|
||||
JOIN " . sw_table('config_homes') . " c
|
||||
ON c.`game_key` = p.`config_name`
|
||||
JOIN " . sw_table('server_homes') . " s
|
||||
ON s.`home_cfg_id` = c.`home_cfg_id`
|
||||
WHERE s.`home_id` = $home_id
|
||||
AND p.`enabled` = 1
|
||||
LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
// ── Mod helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return all mods for a server, sorted by sort_order ASC.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_server_mods($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
return $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `home_id` = $home_id
|
||||
ORDER BY `sort_order` ASC, `id` ASC"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single mod row by primary key.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $id
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_mod_by_id($db, $id)
|
||||
{
|
||||
$id = (int)$id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `id` = $id LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
// ── Server / ownership helpers ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return server_homes row joined with config_homes and remote_servers
|
||||
* for the given home_id, or false when not found.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_home_info($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT s.*, c.`game_key`, c.`game_name`, r.`agent_ip`, r.`agent_port`
|
||||
FROM " . sw_table('server_homes') . " s
|
||||
JOIN " . sw_table('config_homes') . " c ON c.`home_cfg_id` = s.`home_cfg_id`
|
||||
JOIN " . sw_table('remote_servers') . " r ON r.`remote_server_id` = s.`remote_server_id`
|
||||
WHERE s.`home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the current session user is allowed to manage this home.
|
||||
* Admins always pass. Regular users/subusers must have an entry in
|
||||
* user_homes (or be the user_id_main).
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $user_id
|
||||
* @param int $home_id
|
||||
* @return bool
|
||||
*/
|
||||
function sw_user_owns_home($db, $user_id, $home_id)
|
||||
{
|
||||
if (!isset($_SESSION['users_group'])) {
|
||||
return false;
|
||||
}
|
||||
if ($_SESSION['users_group'] === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$user_id = (int)$user_id;
|
||||
$home_id = (int)$home_id;
|
||||
|
||||
// Direct owner
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT 1 FROM " . sw_table('server_homes') . "
|
||||
WHERE `home_id` = $home_id AND `user_id_main` = $user_id LIMIT 1"
|
||||
);
|
||||
if ($rows) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Assigned via user_homes
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT 1 FROM " . sw_table('user_homes') . "
|
||||
WHERE `home_id` = $home_id AND `user_id` = $user_id LIMIT 1"
|
||||
);
|
||||
if ($rows) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Assigned via group
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT 1 FROM " . sw_table('user_group_homes') . " ugh
|
||||
JOIN " . sw_table('user_groups') . " ug ON ug.`group_id` = ugh.`group_id`
|
||||
WHERE ugh.`home_id` = $home_id AND ug.`user_id` = $user_id LIMIT 1"
|
||||
);
|
||||
return (bool)$rows;
|
||||
}
|
||||
|
||||
// ── Game-config helpers ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return an array of all game configs from the XML files.
|
||||
* Each element is a SimpleXMLElement (game_config root node).
|
||||
*
|
||||
* @return SimpleXMLElement[]
|
||||
*/
|
||||
function sw_get_all_game_configs()
|
||||
{
|
||||
if (!defined('SERVER_CONFIG_LOCATION')) {
|
||||
// server_config_parser.php defines this; load it if not already done.
|
||||
if (file_exists(__DIR__ . '/../../config_games/server_config_parser.php')) {
|
||||
require_once __DIR__ . '/../../config_games/server_config_parser.php';
|
||||
} else {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
$configs = array();
|
||||
foreach (glob(SERVER_CONFIG_LOCATION . '*.xml') as $file) {
|
||||
$xml = read_server_config($file);
|
||||
if ($xml !== false) {
|
||||
$configs[] = $xml;
|
||||
}
|
||||
}
|
||||
return $configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure every game config has a matching row in steam_workshop_game_profiles.
|
||||
* Only creates rows that are missing; never overwrites existing data.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @return int number of new rows inserted
|
||||
*/
|
||||
function sw_sync_profiles($db)
|
||||
{
|
||||
$configs = sw_get_all_game_configs();
|
||||
$created = 0;
|
||||
|
||||
foreach ($configs as $xml) {
|
||||
$config_name = (string)$xml->game_key;
|
||||
$game_name = (string)$xml->game_name;
|
||||
|
||||
if (empty($config_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = sw_get_profile_by_config_name($db, $config_name);
|
||||
if ($existing) {
|
||||
continue; // already have a profile for this game config
|
||||
}
|
||||
|
||||
$safe_config = $db->realEscapeSingle($config_name);
|
||||
$safe_name = $db->realEscapeSingle($game_name);
|
||||
$ok = $db->query("INSERT IGNORE INTO " . sw_table('steam_workshop_game_profiles') . " (`config_name`, `game_name`, `enabled`) VALUES ('$safe_config', '$safe_name', 0)");
|
||||
if ($ok) {
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
return $created;
|
||||
}
|
||||
|
||||
// ── Template / launch-param helpers ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Replace {PLACEHOLDER} tokens in $template with values from $vars.
|
||||
* Unknown tokens are left intact so admins can spot missing values.
|
||||
*
|
||||
* @param string $template
|
||||
* @param array $vars associative: 'PLACEHOLDER' => 'value'
|
||||
* @return string
|
||||
*/
|
||||
function sw_apply_template($template, array $vars)
|
||||
{
|
||||
$search = array();
|
||||
$replace = array();
|
||||
foreach ($vars as $key => $value) {
|
||||
$search[] = '{' . $key . '}';
|
||||
$replace[] = (string)$value;
|
||||
}
|
||||
return str_replace($search, $replace, $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the -mod= and -serverMod= launch parameter strings from an ordered
|
||||
* list of enabled mods and the game profile.
|
||||
*
|
||||
* Returns an associative array:
|
||||
* 'mod' => '-mod=@Mod1;@Mod2' (client mods)
|
||||
* 'servermod' => '-serverMod=@ServerOnly' (server-side mods)
|
||||
* 'combined' => '-mod=... -serverMod=...' (ready-to-paste)
|
||||
*
|
||||
* @param array $mods rows from steam_workshop_server_mods (must be pre-filtered
|
||||
* for enabled = 1 and sorted by sort_order)
|
||||
* @param array $profile row from steam_workshop_game_profiles
|
||||
* @return array
|
||||
*/
|
||||
function sw_generate_launch_params(array $mods, array $profile)
|
||||
{
|
||||
$mod_param = trim($profile['mod_launch_param_template'] ?? '-mod=');
|
||||
$servermod_param = trim($profile['servermod_launch_param_template'] ?? '-serverMod=');
|
||||
|
||||
$client_folders = array();
|
||||
$server_folders = array();
|
||||
|
||||
foreach ($mods as $mod) {
|
||||
if (empty($mod['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$folder = !empty($mod['folder_name']) ? $mod['folder_name'] : ('@' . $mod['workshop_id']);
|
||||
if ($mod['mod_type'] === 'server') {
|
||||
$server_folders[] = $folder;
|
||||
} else {
|
||||
$client_folders[] = $folder;
|
||||
}
|
||||
}
|
||||
|
||||
$mod_str = $client_folders ? ($mod_param . implode(';', $client_folders)) : '';
|
||||
$servermod_str = $server_folders ? ($servermod_param . implode(';', $server_folders)) : '';
|
||||
$combined = trim($mod_str . ' ' . $servermod_str);
|
||||
|
||||
return array(
|
||||
'mod' => $mod_str,
|
||||
'servermod' => $servermod_str,
|
||||
'combined' => $combined,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Server behavior settings helpers ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return the workshop behavior settings row for a home, or an array of
|
||||
* safe defaults when no row exists yet.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @return array
|
||||
*/
|
||||
function sw_get_server_settings($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_server_settings') . "
|
||||
WHERE `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
if ($rows && isset($rows[0]) && is_array($rows[0])) {
|
||||
$settings = $rows[0];
|
||||
|
||||
// Runtime normalization is kept as a fallback for legacy/manual rows that
|
||||
// were not updated via module migrations.
|
||||
$legacyUpdateMap = array(
|
||||
'scheduled' => 'manual',
|
||||
);
|
||||
$legacyRestartMap = array(
|
||||
'if_empty' => 'if_stopped',
|
||||
'next_restart' => 'if_stopped',
|
||||
'immediate' => 'none',
|
||||
);
|
||||
$legacyScheduleMap = array(
|
||||
'hourly' => 'daily',
|
||||
);
|
||||
|
||||
if (isset($legacyUpdateMap[$settings['update_mode'] ?? ''])) {
|
||||
$settings['update_mode'] = $legacyUpdateMap[$settings['update_mode']];
|
||||
}
|
||||
if (isset($legacyRestartMap[$settings['restart_behavior'] ?? ''])) {
|
||||
$settings['restart_behavior'] = $legacyRestartMap[$settings['restart_behavior']];
|
||||
}
|
||||
if (isset($legacyScheduleMap[$settings['schedule_interval'] ?? ''])) {
|
||||
$settings['schedule_interval'] = $legacyScheduleMap[$settings['schedule_interval']];
|
||||
}
|
||||
|
||||
$validUpdateModes = array('manual', 'on_restart', 'before_start');
|
||||
$validRestartBehaviors = array('none', 'if_stopped');
|
||||
$validIntervals = array('disabled', 'daily', 'weekly');
|
||||
|
||||
if (!in_array($settings['update_mode'] ?? '', $validUpdateModes, true)) {
|
||||
$settings['update_mode'] = 'manual';
|
||||
}
|
||||
if (!in_array($settings['restart_behavior'] ?? '', $validRestartBehaviors, true)) {
|
||||
$settings['restart_behavior'] = 'none';
|
||||
}
|
||||
if (!in_array($settings['schedule_interval'] ?? '', $validIntervals, true)) {
|
||||
$settings['schedule_interval'] = 'disabled';
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
// Safe defaults – manual only, no automatic restarts, schedule disabled
|
||||
return array(
|
||||
'home_id' => $home_id,
|
||||
'update_mode' => 'manual',
|
||||
'restart_behavior' => 'none',
|
||||
'schedule_interval' => 'disabled',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert the workshop behavior settings for a server home.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @param array $data keys: update_mode, restart_behavior, schedule_interval
|
||||
* @return bool
|
||||
*/
|
||||
function sw_save_server_settings($db, $home_id, array $data)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
|
||||
$valid_update_modes = array('manual', 'on_restart', 'before_start');
|
||||
$valid_restart_behaviors = array('none', 'if_stopped');
|
||||
$valid_intervals = array('disabled', 'daily', 'weekly');
|
||||
|
||||
$update_mode = in_array($data['update_mode'] ?? '', $valid_update_modes, true) ? $data['update_mode'] : 'manual';
|
||||
$restart_behavior = in_array($data['restart_behavior'] ?? '', $valid_restart_behaviors, true) ? $data['restart_behavior'] : 'none';
|
||||
$schedule_interval = in_array($data['schedule_interval'] ?? '', $valid_intervals, true) ? $data['schedule_interval'] : 'disabled';
|
||||
|
||||
$safe_um = $db->realEscapeSingle($update_mode);
|
||||
$safe_rb = $db->realEscapeSingle($restart_behavior);
|
||||
$safe_si = $db->realEscapeSingle($schedule_interval);
|
||||
|
||||
return (bool)$db->query(
|
||||
"INSERT INTO " . sw_table('steam_workshop_server_settings') . "
|
||||
(`home_id`, `update_mode`, `restart_behavior`, `hot_load`,
|
||||
`warning_minutes`, `schedule_interval`, `created_at`, `updated_at`)
|
||||
VALUES ($home_id, '$safe_um', '$safe_rb', 'disabled',
|
||||
0, '$safe_si', NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`update_mode` = '$safe_um',
|
||||
`restart_behavior` = '$safe_rb',
|
||||
`hot_load` = 'disabled',
|
||||
`warning_minutes` = 0,
|
||||
`schedule_interval` = '$safe_si',
|
||||
`updated_at` = NOW()"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Output helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Render a short inline success banner.
|
||||
*
|
||||
* @param string $msg
|
||||
* @return void
|
||||
*/
|
||||
function sw_success($msg)
|
||||
{
|
||||
echo '<div style="background:#d4edda;border:1px solid #c3e6cb;color:#155724;padding:8px 12px;margin:8px 0;border-radius:4px;">'
|
||||
. htmlspecialchars($msg, ENT_QUOTES, 'UTF-8') . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a short inline error banner.
|
||||
*
|
||||
* @param string $msg
|
||||
* @return void
|
||||
*/
|
||||
function sw_error($msg)
|
||||
{
|
||||
echo '<div style="background:#f8d7da;border:1px solid #f5c6cb;color:#721c24;padding:8px 12px;margin:8px 0;border-radius:4px;">'
|
||||
. htmlspecialchars($msg, ENT_QUOTES, 'UTF-8') . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a value for HTML output.
|
||||
*
|
||||
* @param mixed $v
|
||||
* @return string
|
||||
*/
|
||||
function sw_h($v)
|
||||
{
|
||||
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function sw_detect_profile_defaults_from_xml($configName)
|
||||
{
|
||||
$configName = trim((string)$configName);
|
||||
if ($configName === '') {
|
||||
return array();
|
||||
}
|
||||
|
||||
$matched = null;
|
||||
foreach (sw_get_all_game_configs() as $xml) {
|
||||
if ((string)$xml->game_key === $configName) {
|
||||
$matched = $xml;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$matched) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$steamAppId = '';
|
||||
if (isset($matched->mods->mod)) {
|
||||
foreach ($matched->mods->mod as $mod) {
|
||||
$candidate = trim((string)$mod->installer_name);
|
||||
if ($candidate !== '' && preg_match('/^\d+$/', $candidate)) {
|
||||
$steamAppId = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$workshopAppId = '';
|
||||
foreach (array('workshop_app_id', 'workshop_appid', 'steam_workshop_app_id', 'steam_workshop_appid') as $tag) {
|
||||
if (isset($matched->$tag)) {
|
||||
$candidate = trim((string)$matched->$tag);
|
||||
if ($candidate !== '' && preg_match('/^\d+$/', $candidate)) {
|
||||
$workshopAppId = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$xmlBlob = $matched->asXML();
|
||||
if ($workshopAppId === '' && $xmlBlob !== false && preg_match('/steamapps\/workshop\/content\/(\d+)/i', $xmlBlob, $m)) {
|
||||
$workshopAppId = $m[1];
|
||||
}
|
||||
if ($workshopAppId === '') {
|
||||
$workshopAppId = $steamAppId;
|
||||
}
|
||||
|
||||
$gameKey = strtolower(trim((string)$matched->game_key));
|
||||
$gameName = strtolower(trim((string)$matched->game_name));
|
||||
$installPathTemplate = (strpos($gameKey . ' ' . $gameName, 'arma') !== false || strpos($gameKey . ' ' . $gameName, 'dayz') !== false)
|
||||
? '{SERVER_ROOT}/{MOD_FOLDER}'
|
||||
: '{SERVER_ROOT}/workshop/{MOD_FOLDER}';
|
||||
|
||||
return array(
|
||||
'steam_app_id' => $steamAppId,
|
||||
'workshop_app_id' => $workshopAppId,
|
||||
'steamcmd_path' => '/home/gameserver/steamcmd/steamcmd.sh',
|
||||
'server_root_template' => '{SERVER_ROOT}',
|
||||
'workshop_download_dir_template' => '{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}',
|
||||
'install_path_template' => $installPathTemplate,
|
||||
);
|
||||
}
|
||||
|
||||
function sw_apply_detected_profile_defaults($db, array $profile, array $detected, $overwriteExisting = false)
|
||||
{
|
||||
$columns = array(
|
||||
'steam_app_id',
|
||||
'workshop_app_id',
|
||||
'steamcmd_path',
|
||||
'workshop_download_dir_template',
|
||||
'server_root_template',
|
||||
'install_path_template',
|
||||
);
|
||||
$setParts = array();
|
||||
$updated = 0;
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (!array_key_exists($column, $detected) || $detected[$column] === '') {
|
||||
continue;
|
||||
}
|
||||
$current = trim((string)($profile[$column] ?? ''));
|
||||
if (!$overwriteExisting && $current !== '') {
|
||||
continue;
|
||||
}
|
||||
if ($current === $detected[$column]) {
|
||||
continue;
|
||||
}
|
||||
$setParts[] = "`$column` = '" . $db->realEscapeSingle($detected[$column]) . "'";
|
||||
$updated++;
|
||||
}
|
||||
|
||||
if (empty($setParts)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$setParts[] = "`updated_at` = NOW()";
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_game_profiles') . "
|
||||
SET " . implode(', ', $setParts) . "
|
||||
WHERE `id` = " . (int)$profile['id'] . " LIMIT 1"
|
||||
);
|
||||
return $updated;
|
||||
}
|
||||
|
||||
function steam_workshop_resolve_paths($db, array $home_info, $workshop_id, $target_path_template = '', $optional_folder_name = '', $workshop_app_id_override = '')
|
||||
{
|
||||
$profile = sw_get_profile_for_home($db, (int)$home_info['home_id']);
|
||||
if (!$profile || !is_array($profile)) {
|
||||
return array('ok' => false, 'error' => 'No enabled Steam Workshop profile for this server home.');
|
||||
}
|
||||
$workshop_id = trim((string)$workshop_id);
|
||||
if ($workshop_id === '' || !preg_match('/^[0-9]+$/', $workshop_id)) {
|
||||
return array('ok' => false, 'error' => 'Invalid Workshop ID.');
|
||||
}
|
||||
$workshop_app_id = trim((string)$workshop_app_id_override);
|
||||
if ($workshop_app_id === '') {
|
||||
$workshop_app_id = (string)$profile['workshop_app_id'];
|
||||
}
|
||||
$folder_name = trim((string)$optional_folder_name);
|
||||
if ($folder_name === '') {
|
||||
$folder_name = '@' . $workshop_id;
|
||||
}
|
||||
$vars = array(
|
||||
'HOME_ID' => (int)$home_info['home_id'],
|
||||
'SERVER_ROOT' => rtrim((string)$home_info['home_path'], '/'),
|
||||
'GAME_ROOT' => rtrim((string)$home_info['home_path'], '/'),
|
||||
'WORKSHOP_ID' => $workshop_id,
|
||||
'WORKSHOP_APP_ID' => $workshop_app_id,
|
||||
'STEAM_APP_ID' => (string)$profile['steam_app_id'],
|
||||
'FOLDER_NAME' => $folder_name,
|
||||
'MOD_FOLDER' => $folder_name,
|
||||
);
|
||||
$target_template = trim((string)$target_path_template);
|
||||
if ($target_template === '') {
|
||||
$target_template = !empty($profile['install_path_template']) ? (string)$profile['install_path_template'] : '{SERVER_ROOT}/workshop/{MOD_FOLDER}';
|
||||
}
|
||||
$resolved_target = sw_apply_template($target_template, $vars);
|
||||
return array(
|
||||
'ok' => true,
|
||||
'profile' => $profile,
|
||||
'workshop_id' => $workshop_id,
|
||||
'workshop_app_id' => $workshop_app_id,
|
||||
'steam_app_id' => (string)$profile['steam_app_id'],
|
||||
'folder_name' => $folder_name,
|
||||
'target_path_template' => $target_template,
|
||||
'target_path_resolved' => $resolved_target,
|
||||
'vars' => $vars,
|
||||
);
|
||||
}
|
||||
|
||||
function steam_workshop_download_item($db, array $home_info, $workshop_id, $target_path_template = '', array $options = array())
|
||||
{
|
||||
$optional_folder_name = isset($options['optional_folder_name']) ? $options['optional_folder_name'] : '';
|
||||
$workshop_app_id = isset($options['workshop_app_id']) ? $options['workshop_app_id'] : '';
|
||||
return steam_workshop_resolve_paths($db, $home_info, $workshop_id, $target_path_template, $optional_folder_name, $workshop_app_id);
|
||||
}
|
||||
|
||||
function steam_workshop_install_item_to_home($db, array $home_info, $workshop_id, $target_path_template = '', array $options = array())
|
||||
{
|
||||
return steam_workshop_download_item($db, $home_info, $workshop_id, $target_path_template, $options);
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
-- GSP Steam Workshop – Manual SQL Reference
|
||||
-- =========================================
|
||||
-- Replace PREFIX_ with your actual table prefix (e.g. gsp_).
|
||||
-- Compatible with MySQL 5.7 and MySQL 8.0.
|
||||
-- Do NOT hardcode any database name here.
|
||||
-- Run in the panel database.
|
||||
|
||||
-- ── Drop legacy tables (if upgrading from the old adapter-based implementation) ──
|
||||
DROP TABLE IF EXISTS `PREFIX_workshop_game_profiles`;
|
||||
DROP TABLE IF EXISTS `PREFIX_workshop_cache`;
|
||||
DROP TABLE IF EXISTS `PREFIX_server_workshop_mods`;
|
||||
DROP TABLE IF EXISTS `PREFIX_server_workshop_settings`;
|
||||
|
||||
-- ── Create new tables ─────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PREFIX_steam_workshop_game_profiles` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`config_name` VARCHAR(100) NOT NULL,
|
||||
`game_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`steam_app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`workshop_app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`steam_login_required` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`steamcmd_login_mode` ENUM('anonymous','account') NOT NULL DEFAULT 'anonymous',
|
||||
`steamcmd_path` VARCHAR(512) NOT NULL DEFAULT '/home/gameserver/steamcmd/steamcmd.sh',
|
||||
`workshop_download_dir_template` TEXT NULL,
|
||||
`server_root_template` TEXT NULL,
|
||||
`install_path_template` TEXT NULL,
|
||||
`folder_naming_format` VARCHAR(64) NOT NULL DEFAULT '@{MOD_NAME}',
|
||||
`mod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-mod=',
|
||||
`servermod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-serverMod=',
|
||||
`install_script_template` TEXT NULL,
|
||||
`update_script_template` TEXT NULL,
|
||||
`copy_bikeys_enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`notes` TEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_config_name` (`config_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PREFIX_steam_workshop_server_mods` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`home_id` INT NOT NULL,
|
||||
`profile_id` INT NOT NULL,
|
||||
`workshop_id` VARCHAR(64) NOT NULL,
|
||||
`mod_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`folder_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`mod_type` ENUM('client','server') NOT NULL DEFAULT 'client',
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`install_status` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`last_installed_at` DATETIME NULL,
|
||||
`last_updated_at` DATETIME NULL,
|
||||
`last_error` TEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── Example: DayZ profile ────────────────────────────────────────────────
|
||||
-- After running the above, insert an example DayZ profile.
|
||||
-- Adjust config_name to match your actual DayZ game_key from config_homes.
|
||||
-- (Run `SELECT game_key, game_name FROM PREFIX_config_homes WHERE game_name LIKE '%DayZ%';`
|
||||
-- to find the right config_name.)
|
||||
--
|
||||
-- INSERT INTO `PREFIX_steam_workshop_game_profiles`
|
||||
-- (`config_name`, `game_name`, `enabled`,
|
||||
-- `steam_app_id`, `workshop_app_id`,
|
||||
-- `steamcmd_path`,
|
||||
-- `workshop_download_dir_template`,
|
||||
-- `server_root_template`,
|
||||
-- `install_path_template`,
|
||||
-- `folder_naming_format`,
|
||||
-- `mod_launch_param_template`,
|
||||
-- `servermod_launch_param_template`,
|
||||
-- `copy_bikeys_enabled`)
|
||||
-- VALUES
|
||||
-- ('dayz_win64', 'DayZ', 1,
|
||||
-- '223350', '221100',
|
||||
-- '/home/gameserver/steamcmd/steamcmd.sh',
|
||||
-- '{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}',
|
||||
-- '/home/gameserver/servers/{HOME_ID}',
|
||||
-- '{SERVER_ROOT}/{MOD_FOLDER}',
|
||||
-- '@{MOD_NAME}',
|
||||
-- '-mod=',
|
||||
-- '-serverMod=',
|
||||
-- 1);
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop module
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
$module_title = "Steam Workshop";
|
||||
$module_version = "3.3";
|
||||
$db_version = 5;
|
||||
$module_required = FALSE;
|
||||
// DEPRECATED: The Steam Workshop standalone module has been superseded by
|
||||
// Server Content Manager (addonsmanager). Navigation access is removed so
|
||||
// users are directed to the new unified workshop workflow. The DB tables
|
||||
// and helper functions are preserved for backward compatibility.
|
||||
// See Panel/modules/addonsmanager/ for the replacement implementation.
|
||||
$module_menus = array();
|
||||
|
||||
if (!function_exists('sw_module_db_prefix')) {
|
||||
function sw_module_db_prefix()
|
||||
{
|
||||
if (defined('DB_PREFIX') && DB_PREFIX !== '') {
|
||||
return DB_PREFIX;
|
||||
}
|
||||
if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') {
|
||||
return $GLOBALS['db_prefix'];
|
||||
}
|
||||
if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') {
|
||||
return $GLOBALS['table_prefix'];
|
||||
}
|
||||
return 'gsp_';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sw_module_table_name')) {
|
||||
function sw_module_table_name($table)
|
||||
{
|
||||
$prefix = preg_replace('/[^a-zA-Z0-9_]/', '', (string)sw_module_db_prefix());
|
||||
$name = preg_replace('/[^a-zA-Z0-9_]/', '', (string)$table);
|
||||
if ($prefix === '') {
|
||||
$prefix = 'gsp_';
|
||||
}
|
||||
return $prefix . $name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sw_module_table')) {
|
||||
function sw_module_table($table)
|
||||
{
|
||||
return '`' . sw_module_table_name($table) . '`';
|
||||
}
|
||||
}
|
||||
|
||||
$install_queries = array();
|
||||
|
||||
$legacyDrops = array(
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('workshop_game_profiles'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('workshop_cache'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('server_workshop_mods'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('server_workshop_settings'),
|
||||
);
|
||||
|
||||
$schemaCreate = array(
|
||||
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_game_profiles') . " (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`config_name` VARCHAR(100) NOT NULL,
|
||||
`game_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`steam_app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`workshop_app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`steam_login_required` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`steamcmd_login_mode` ENUM('anonymous','account') NOT NULL DEFAULT 'anonymous',
|
||||
`steamcmd_path` VARCHAR(512) NOT NULL DEFAULT '/home/gameserver/steamcmd/steamcmd.sh',
|
||||
`workshop_download_dir_template` TEXT NULL,
|
||||
`server_root_template` TEXT NULL,
|
||||
`install_path_template` TEXT NULL,
|
||||
`folder_naming_format` VARCHAR(64) NOT NULL DEFAULT '@{MOD_NAME}',
|
||||
`mod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-mod=',
|
||||
`servermod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-serverMod=',
|
||||
`install_script_template` TEXT NULL,
|
||||
`update_script_template` TEXT NULL,
|
||||
`copy_bikeys_enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`notes` TEXT NULL,
|
||||
`default_update_mode` ENUM('manual','on_restart','before_start') NOT NULL DEFAULT 'manual',
|
||||
`default_restart_behavior` ENUM('none','if_stopped') NOT NULL DEFAULT 'none',
|
||||
`default_hot_load` ENUM('disabled','attempt') NOT NULL DEFAULT 'disabled',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_config_name` (`config_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_server_mods') . " (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`home_id` INT NOT NULL,
|
||||
`profile_id` INT NOT NULL,
|
||||
`workshop_id` VARCHAR(64) NOT NULL,
|
||||
`mod_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`folder_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`mod_type` ENUM('client','server') NOT NULL DEFAULT 'client',
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`install_status` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`last_installed_at` DATETIME NULL,
|
||||
`last_updated_at` DATETIME NULL,
|
||||
`last_error` TEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_server_settings') . " (
|
||||
`home_id` INT NOT NULL,
|
||||
`update_mode` ENUM('manual','on_restart','before_start')
|
||||
NOT NULL DEFAULT 'manual',
|
||||
`restart_behavior` ENUM('none','if_stopped')
|
||||
NOT NULL DEFAULT 'none',
|
||||
`hot_load` ENUM('disabled','attempt')
|
||||
NOT NULL DEFAULT 'disabled',
|
||||
`warning_minutes` INT NOT NULL DEFAULT 0,
|
||||
`schedule_interval` ENUM('disabled','daily','weekly') NOT NULL DEFAULT 'disabled',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`home_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
);
|
||||
|
||||
$install_queries[0] = array_merge($legacyDrops, $schemaCreate);
|
||||
$install_queries[3] = array_merge($legacyDrops, $schemaCreate);
|
||||
$install_queries[4] = array();
|
||||
|
||||
$install_queries[5] = array(
|
||||
function ($db) {
|
||||
$table = sw_module_table_name('steam_workshop_server_settings');
|
||||
$exists = $db->resultQuery(
|
||||
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" . $db->realEscapeSingle($table) . "'"
|
||||
);
|
||||
if (!$exists || (int)($exists[0]['cnt'] ?? 0) === 0) {
|
||||
return (bool)$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_server_settings') . " (
|
||||
`home_id` INT NOT NULL,
|
||||
`update_mode` ENUM('manual','on_restart','before_start') NOT NULL DEFAULT 'manual',
|
||||
`restart_behavior` ENUM('none','if_stopped') NOT NULL DEFAULT 'none',
|
||||
`hot_load` ENUM('disabled','attempt') NOT NULL DEFAULT 'disabled',
|
||||
`warning_minutes` INT NOT NULL DEFAULT 0,
|
||||
`schedule_interval` ENUM('disabled','daily','weekly') NOT NULL DEFAULT 'disabled',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`home_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
);
|
||||
}
|
||||
|
||||
$ok = true;
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
SET `update_mode` = CASE
|
||||
WHEN `update_mode` = 'scheduled' THEN 'manual'
|
||||
ELSE `update_mode`
|
||||
END"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
SET `restart_behavior` = CASE
|
||||
WHEN `restart_behavior` IN ('if_empty','next_restart') THEN 'if_stopped'
|
||||
WHEN `restart_behavior` = 'immediate' THEN 'none'
|
||||
ELSE `restart_behavior`
|
||||
END"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
SET `schedule_interval` = CASE
|
||||
WHEN `schedule_interval` = 'hourly' THEN 'daily'
|
||||
WHEN `schedule_interval` IS NULL OR `schedule_interval` = '' THEN 'disabled'
|
||||
ELSE `schedule_interval`
|
||||
END"
|
||||
);
|
||||
// The simplified workflow intentionally hard-disables these legacy fields
|
||||
// for every row so old unsupported behaviors cannot be re-enabled.
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
SET `hot_load` = 'disabled', `warning_minutes` = 0"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
MODIFY `update_mode` ENUM('manual','on_restart','before_start') NOT NULL DEFAULT 'manual'"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
MODIFY `restart_behavior` ENUM('none','if_stopped') NOT NULL DEFAULT 'none'"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
MODIFY `schedule_interval` ENUM('disabled','daily','weekly') NOT NULL DEFAULT 'disabled'"
|
||||
);
|
||||
|
||||
return $ok;
|
||||
},
|
||||
function ($db) {
|
||||
$profileTable = sw_module_table_name('steam_workshop_game_profiles');
|
||||
$exists = $db->resultQuery(
|
||||
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" . $db->realEscapeSingle($profileTable) . "'"
|
||||
);
|
||||
if (!$exists || (int)($exists[0]['cnt'] ?? 0) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$ok = true;
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_game_profiles') . "
|
||||
SET `default_update_mode` = CASE
|
||||
WHEN `default_update_mode` = 'scheduled' THEN 'manual'
|
||||
ELSE `default_update_mode`
|
||||
END"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_game_profiles') . "
|
||||
SET `default_restart_behavior` = CASE
|
||||
WHEN `default_restart_behavior` IN ('if_empty','next_restart') THEN 'if_stopped'
|
||||
WHEN `default_restart_behavior` = 'immediate' THEN 'none'
|
||||
ELSE `default_restart_behavior`
|
||||
END"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_game_profiles') . "
|
||||
MODIFY `default_update_mode` ENUM('manual','on_restart','before_start') NOT NULL DEFAULT 'manual'"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_game_profiles') . "
|
||||
MODIFY `default_restart_behavior` ENUM('none','if_stopped') NOT NULL DEFAULT 'none'"
|
||||
);
|
||||
|
||||
return $ok;
|
||||
},
|
||||
);
|
||||
|
||||
$uninstall_queries = array(
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('steam_workshop_server_settings'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('steam_workshop_server_mods'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('steam_workshop_game_profiles'),
|
||||
);
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: monitor page button
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* Adds a "Steam Workshop" button to the game/server monitor page when:
|
||||
* - the game's Workshop profile is enabled in steam_workshop_game_profiles, AND
|
||||
* - the current user owns the server or is an admin.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
$module_buttons = array();
|
||||
|
||||
if (!function_exists('sw_monitor_db_prefix')) {
|
||||
function sw_monitor_db_prefix()
|
||||
{
|
||||
if (defined('DB_PREFIX') && DB_PREFIX !== '') {
|
||||
return DB_PREFIX;
|
||||
}
|
||||
if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') {
|
||||
return $GLOBALS['db_prefix'];
|
||||
}
|
||||
if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') {
|
||||
return $GLOBALS['table_prefix'];
|
||||
}
|
||||
return 'gsp_';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sw_monitor_table')) {
|
||||
function sw_monitor_table($name)
|
||||
{
|
||||
return '`' . sw_monitor_db_prefix() . $name . '`';
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
?>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<navigation>
|
||||
<!-- Admin: manage Steam Workshop game profiles -->
|
||||
<page key="admin" file="admin.php" access="admin" />
|
||||
<!-- User: manage per-server mods -->
|
||||
<page key="user" file="user.php" access="admin,user,subuser" />
|
||||
</navigation>
|
||||
|
|
@ -1,558 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: User mod management
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* Accessible via: home.php?m=steam_workshop&p=user&home_id=123
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
function exec_ogp_module()
|
||||
{
|
||||
global $db;
|
||||
|
||||
echo '<h2>Steam Workshop – Mod Manager</h2>';
|
||||
|
||||
$home_id = isset($_REQUEST['home_id']) ? (int)$_REQUEST['home_id'] : 0;
|
||||
|
||||
if (!$home_id) {
|
||||
sw_error('No server selected. Please access this page from your game server manager.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ownership check
|
||||
if (!sw_user_owns_home($db, (int)$_SESSION['user_id'], $home_id)) {
|
||||
sw_error('Access denied. You do not own this server.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load server info
|
||||
$home = sw_get_home_info($db, $home_id);
|
||||
if (!$home) {
|
||||
sw_error('Server not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find matching Workshop profile
|
||||
$profile = sw_get_profile_for_home($db, $home_id);
|
||||
if (!$profile) {
|
||||
echo '<p>Steam Workshop is not enabled for this game.</p>';
|
||||
echo '<p>An administrator must enable Workshop support for this game under '
|
||||
. '<em>Steam Workshop › Admin</em>.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? ($_GET['action'] ?? '');
|
||||
|
||||
// ── POST handlers ─────────────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
switch ($action) {
|
||||
case 'add_mod':
|
||||
sw_user_add_mod($db, $home_id, $profile);
|
||||
break;
|
||||
case 'save_mod':
|
||||
sw_user_save_mod($db, $home_id);
|
||||
break;
|
||||
case 'delete_mod':
|
||||
sw_user_delete_mod($db, $home_id);
|
||||
break;
|
||||
case 'toggle_mod':
|
||||
sw_user_toggle_mod($db, $home_id);
|
||||
break;
|
||||
case 'move_up':
|
||||
case 'move_down':
|
||||
sw_user_reorder_mod($db, $home_id, $action);
|
||||
break;
|
||||
case 'queue_update':
|
||||
sw_user_queue_update($db, $home_id);
|
||||
break;
|
||||
case 'save_settings':
|
||||
sw_user_save_settings($db, $home_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render page ───────────────────────────────────────────────────
|
||||
sw_user_render($db, $home_id, $home, $profile);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST action handlers
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function sw_user_add_mod($db, $home_id, array $profile)
|
||||
{
|
||||
$workshop_id = trim($_POST['workshop_id'] ?? '');
|
||||
if (!preg_match('/^\d{1,20}$/', $workshop_id)) {
|
||||
sw_error('Invalid Workshop ID – must be a numeric Steam Workshop item ID.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplicates
|
||||
$safe_wid = $db->realEscapeSingle($workshop_id);
|
||||
$exists = $db->resultQuery(
|
||||
"SELECT id FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `home_id` = $home_id AND `workshop_id` = '$safe_wid' LIMIT 1"
|
||||
);
|
||||
if ($exists) {
|
||||
sw_error("Workshop ID $workshop_id is already in the list.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine next sort_order
|
||||
$last = $db->resultQuery(
|
||||
"SELECT MAX(`sort_order`) AS m FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `home_id` = $home_id"
|
||||
);
|
||||
$sort = ($last && isset($last[0]['m'])) ? ((int)$last[0]['m'] + 1) : 0;
|
||||
|
||||
$mod_name = $db->realEscapeSingle(trim($_POST['mod_name'] ?? ''));
|
||||
$mod_type = (($_POST['mod_type'] ?? 'client') === 'server') ? 'server' : 'client';
|
||||
$profile_id = (int)$profile['id'];
|
||||
|
||||
// Auto-generate folder name from naming format template
|
||||
$folder_name = sw_apply_template(
|
||||
$profile['folder_naming_format'],
|
||||
array(
|
||||
'MOD_NAME' => !empty($mod_name) ? $mod_name : $workshop_id,
|
||||
'WORKSHOP_ID' => $workshop_id,
|
||||
'WORKSHOP_APP_ID'=> $profile['workshop_app_id'],
|
||||
)
|
||||
);
|
||||
$safe_fname = $db->realEscapeSingle($folder_name);
|
||||
$safe_mname = $mod_name; // already escaped above via realEscapeSingle
|
||||
|
||||
$ok = $db->query(
|
||||
"INSERT INTO " . sw_table('steam_workshop_server_mods') . "
|
||||
(`home_id`, `profile_id`, `workshop_id`, `mod_name`, `folder_name`,
|
||||
`mod_type`, `sort_order`, `enabled`, `install_status`, `created_at`)
|
||||
VALUES ($home_id, $profile_id, '$safe_wid', '$safe_mname', '$safe_fname',
|
||||
'$mod_type', $sort, 1, '', NOW())"
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
sw_success("Workshop mod $workshop_id added.");
|
||||
} else {
|
||||
sw_error('Failed to add mod.');
|
||||
}
|
||||
}
|
||||
|
||||
function sw_user_save_mod($db, $home_id)
|
||||
{
|
||||
$mod_id = (int)($_POST['mod_id'] ?? 0);
|
||||
if (!$mod_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mod = sw_get_mod_by_id($db, $mod_id);
|
||||
if (!$mod || (int)$mod['home_id'] !== $home_id) {
|
||||
sw_error('Mod not found or access denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
$mod_name = $db->realEscapeSingle(trim($_POST['mod_name'] ?? ''));
|
||||
$folder_name = $db->realEscapeSingle(trim($_POST['folder_name'] ?? ''));
|
||||
$mod_type = (($_POST['mod_type'] ?? 'client') === 'server') ? 'server' : 'client';
|
||||
|
||||
if (empty($folder_name)) {
|
||||
sw_error('Folder name cannot be empty.');
|
||||
return;
|
||||
}
|
||||
|
||||
$ok = $db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `mod_name` = '$mod_name',
|
||||
`folder_name` = '$folder_name',
|
||||
`mod_type` = '$mod_type',
|
||||
`updated_at` = NOW()
|
||||
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
sw_success('Mod updated.');
|
||||
} else {
|
||||
sw_error('Failed to update mod.');
|
||||
}
|
||||
}
|
||||
|
||||
function sw_user_delete_mod($db, $home_id)
|
||||
{
|
||||
$mod_id = (int)($_POST['mod_id'] ?? 0);
|
||||
if (!$mod_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mod = sw_get_mod_by_id($db, $mod_id);
|
||||
if (!$mod || (int)$mod['home_id'] !== $home_id) {
|
||||
sw_error('Mod not found or access denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
$db->query(
|
||||
"DELETE FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
sw_success('Mod removed from list.');
|
||||
}
|
||||
|
||||
function sw_user_toggle_mod($db, $home_id)
|
||||
{
|
||||
$mod_id = (int)($_POST['mod_id'] ?? 0);
|
||||
if (!$mod_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mod = sw_get_mod_by_id($db, $mod_id);
|
||||
if (!$mod || (int)$mod['home_id'] !== $home_id) {
|
||||
sw_error('Mod not found or access denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
$new_state = $mod['enabled'] ? 0 : 1;
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `enabled` = $new_state, `updated_at` = NOW()
|
||||
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
function sw_user_reorder_mod($db, $home_id, $direction)
|
||||
{
|
||||
$mod_id = (int)($_POST['mod_id'] ?? 0);
|
||||
if (!$mod_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mod = sw_get_mod_by_id($db, $mod_id);
|
||||
if (!$mod || (int)$mod['home_id'] !== $home_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mods = sw_get_server_mods($db, $home_id);
|
||||
if (!$mods) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalise sort_order to 0-based sequential integers
|
||||
$sorted = array_values($mods);
|
||||
foreach ($sorted as $idx => $m) {
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `sort_order` = $idx
|
||||
WHERE `id` = " . (int)$m['id'] . " AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
// Find the position of the target mod
|
||||
$pos = -1;
|
||||
foreach ($sorted as $idx => $m) {
|
||||
if ((int)$m['id'] === $mod_id) {
|
||||
$pos = $idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($pos < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($direction === 'move_up' && $pos > 0) {
|
||||
$swap_pos = $pos - 1;
|
||||
} elseif ($direction === 'move_down' && $pos < (count($sorted) - 1)) {
|
||||
$swap_pos = $pos + 1;
|
||||
} else {
|
||||
return; // already at boundary
|
||||
}
|
||||
|
||||
$swap_id = (int)$sorted[$swap_pos]['id'];
|
||||
|
||||
// Swap sort_order values
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `sort_order` = $swap_pos
|
||||
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `sort_order` = $pos
|
||||
WHERE `id` = $swap_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
function sw_user_queue_update($db, $home_id)
|
||||
{
|
||||
// Mark all enabled mods as 'queued' so the agent picks them up.
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'queued', `updated_at` = NOW()
|
||||
WHERE `home_id` = $home_id AND `enabled` = 1"
|
||||
);
|
||||
sw_success('All enabled mods were queued. Updates are processed automatically by the server agent.');
|
||||
}
|
||||
|
||||
function sw_user_save_settings($db, $home_id)
|
||||
{
|
||||
$ok = sw_save_server_settings($db, $home_id, array(
|
||||
'update_mode' => $_POST['update_mode'] ?? 'manual',
|
||||
'restart_behavior' => $_POST['restart_behavior'] ?? 'none',
|
||||
'schedule_interval' => $_POST['schedule_interval'] ?? 'disabled',
|
||||
));
|
||||
|
||||
if ($ok) {
|
||||
sw_success('Workshop behavior settings saved.');
|
||||
} else {
|
||||
sw_error('Failed to save settings.');
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Render
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function sw_user_render($db, $home_id, array $home, array $profile)
|
||||
{
|
||||
$mods = sw_get_server_mods($db, $home_id) ?: array();
|
||||
$settings = sw_get_server_settings($db, $home_id);
|
||||
$queuedCount = 0;
|
||||
$failedCount = 0;
|
||||
$installedCount = 0;
|
||||
$latestUpdateAt = '';
|
||||
$latestError = '';
|
||||
foreach ($mods as $mod) {
|
||||
if (($mod['install_status'] ?? '') === 'queued') {
|
||||
$queuedCount++;
|
||||
} elseif (($mod['install_status'] ?? '') === 'failed') {
|
||||
$failedCount++;
|
||||
} elseif (($mod['install_status'] ?? '') === 'installed') {
|
||||
$installedCount++;
|
||||
}
|
||||
if (!empty($mod['last_updated_at']) && $mod['last_updated_at'] > $latestUpdateAt) {
|
||||
$latestUpdateAt = $mod['last_updated_at'];
|
||||
}
|
||||
if (($mod['install_status'] ?? '') === 'failed' && !empty($mod['last_error']) && $latestError === '') {
|
||||
$latestError = $mod['last_error'];
|
||||
}
|
||||
}
|
||||
|
||||
$base_url = 'home.php?m=steam_workshop&p=user&home_id=' . $home_id;
|
||||
?>
|
||||
<style>
|
||||
.sw-user-panel{background:#161616;border:1px solid #2f2f2f;border-radius:6px;padding:14px;margin:10px 0;color:#ececec}
|
||||
.sw-user-panel h3{margin:0 0 10px 0;color:#fff}
|
||||
.sw-user-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
|
||||
.sw-user-grid label{display:block}
|
||||
.sw-user-grid span{display:block;font-size:12px;color:#bebebe;margin-bottom:4px}
|
||||
.sw-user-grid input[type=text],.sw-user-grid select{width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #3a3a3a;color:#f0f0f0;padding:7px;border-radius:4px}
|
||||
.sw-user-table-wrap{overflow-x:auto}
|
||||
.sw-user-table{width:100%;border-collapse:collapse;min-width:860px}
|
||||
.sw-user-table th,.sw-user-table td{border:1px solid #353535;padding:8px;vertical-align:middle}
|
||||
.sw-user-table th{background:#202020;text-align:left;color:#fff}
|
||||
.sw-status-ok{color:#7cdc7c;font-weight:700}
|
||||
.sw-status-queued{color:#ffca63;font-weight:700}
|
||||
.sw-status-failed{color:#ff8484;font-weight:700}
|
||||
.sw-status-progress{color:#8cc7ff;font-weight:700}
|
||||
.sw-muted{color:#b4b4b4}
|
||||
</style>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<p>
|
||||
<strong>Server:</strong> <?= sw_h($home['home_name']) ?>
|
||||
<strong>Game:</strong> <?= sw_h($home['game_name']) ?>
|
||||
<strong>Workshop Profile:</strong> <?= sw_h($profile['config_name']) ?>
|
||||
</p>
|
||||
<p class="sw-muted" style="margin-bottom:0;">
|
||||
Queue updates from this page. The server agent applies queued updates automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<h3>Add Workshop Mod</h3>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>">
|
||||
<input type="hidden" name="action" value="add_mod">
|
||||
<div class="sw-user-grid">
|
||||
<label>
|
||||
<span>Workshop ID</span>
|
||||
<input type="text" id="workshop_id" name="workshop_id" placeholder="e.g. 2863534533" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Display Name (optional)</span>
|
||||
<input type="text" id="add_mod_name" name="mod_name" placeholder="e.g. CF">
|
||||
</label>
|
||||
<label>
|
||||
<span>Mod Type</span>
|
||||
<select id="add_mod_type" name="mod_type">
|
||||
<option value="client">Client mod</option>
|
||||
<option value="server">Server-side only</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p style="margin:12px 0 0;">
|
||||
<button type="submit" class="button">Add Mod</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<h3>Update Queue & Last Result</h3>
|
||||
<p>
|
||||
<strong>Enabled mods:</strong> <?= count(array_filter($mods, function ($m) { return !empty($m['enabled']); })) ?>
|
||||
<strong>Queued:</strong> <?= $queuedCount ?>
|
||||
<strong>Installed:</strong> <?= $installedCount ?>
|
||||
<strong>Failed:</strong> <?= $failedCount ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Last update time:</strong> <?= $latestUpdateAt ? sw_h($latestUpdateAt) : 'Never' ?>
|
||||
</p>
|
||||
<?php if ($latestError !== ''): ?>
|
||||
<p><strong>Last error:</strong> <?= sw_h($latestError) ?></p>
|
||||
<?php endif; ?>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>">
|
||||
<input type="hidden" name="action" value="queue_update">
|
||||
<button type="submit" class="button" onclick="return confirm('Queue all enabled mods for update?');">Queue Update for All Enabled Mods</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<h3>Workshop Behavior Settings</h3>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>">
|
||||
<input type="hidden" name="action" value="save_settings">
|
||||
<div class="sw-user-grid">
|
||||
<label>
|
||||
<span>Update Mode</span>
|
||||
<select name="update_mode">
|
||||
<option value="manual" <?= ($settings['update_mode'] === 'manual') ? 'selected' : '' ?>>Manual only</option>
|
||||
<option value="on_restart" <?= ($settings['update_mode'] === 'on_restart') ? 'selected' : '' ?>>On next restart</option>
|
||||
<option value="before_start" <?= ($settings['update_mode'] === 'before_start') ? 'selected' : '' ?>>Before server start</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Restart Behavior</span>
|
||||
<select name="restart_behavior">
|
||||
<option value="none" <?= ($settings['restart_behavior'] === 'none') ? 'selected' : '' ?>>Never restart automatically</option>
|
||||
<option value="if_stopped" <?= ($settings['restart_behavior'] === 'if_stopped') ? 'selected' : '' ?>>Restart only if server is stopped</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Scheduled Checks</span>
|
||||
<select name="schedule_interval">
|
||||
<option value="disabled" <?= ($settings['schedule_interval'] === 'disabled') ? 'selected' : '' ?>>Disabled</option>
|
||||
<option value="daily" <?= ($settings['schedule_interval'] === 'daily') ? 'selected' : '' ?>>Daily</option>
|
||||
<option value="weekly" <?= ($settings['schedule_interval'] === 'weekly') ? 'selected' : '' ?>>Weekly</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p style="margin:12px 0 0;">
|
||||
<button type="submit" class="button">Save Behavior Settings</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<h3>Installed Mods (<?= count($mods) ?>)</h3>
|
||||
<?php if (empty($mods)): ?>
|
||||
<p>No mods added yet. Use the form above to add Workshop IDs.</p>
|
||||
<?php else: ?>
|
||||
<div class="sw-user-table-wrap">
|
||||
<table class="sw-user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Workshop ID</th>
|
||||
<th>Mod Name</th>
|
||||
<th>Folder Name</th>
|
||||
<th>Type</th>
|
||||
<th>Enabled</th>
|
||||
<th>Status</th>
|
||||
<th>Last Update</th>
|
||||
<th>Last Error</th>
|
||||
<th>Order</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($mods as $idx => $mod): ?>
|
||||
<?php
|
||||
$shortError = '';
|
||||
if (!empty($mod['last_error'])) {
|
||||
$shortError = (strlen($mod['last_error']) > 70)
|
||||
? (substr($mod['last_error'], 0, 67) . '...')
|
||||
: $mod['last_error'];
|
||||
}
|
||||
?>
|
||||
<tr style="<?= !$mod['enabled'] ? 'opacity:0.55;' : '' ?>">
|
||||
<td><?= $idx + 1 ?></td>
|
||||
<td style="font-family:monospace;"><?= sw_h($mod['workshop_id']) ?></td>
|
||||
<td>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
|
||||
<input type="hidden" name="action" value="save_mod">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<input type="text" name="mod_name" value="<?= sw_h($mod['mod_name']) ?>" style="width:120px;">
|
||||
</td>
|
||||
<td><input type="text" name="folder_name" value="<?= sw_h($mod['folder_name']) ?>" style="width:120px;"></td>
|
||||
<td>
|
||||
<select name="mod_type" style="width:120px;">
|
||||
<option value="client" <?= $mod['mod_type'] === 'client' ? 'selected' : '' ?>>Client</option>
|
||||
<option value="server" <?= $mod['mod_type'] === 'server' ? 'selected' : '' ?>>Server</option>
|
||||
</select>
|
||||
<button type="submit" class="button small">Save</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
|
||||
<input type="hidden" name="action" value="toggle_mod">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<button type="submit" class="button small" style="<?= $mod['enabled'] ? 'background:#5cb85c;color:#fff;' : '' ?>"><?= $mod['enabled'] ? 'On' : 'Off' ?></button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$s = $mod['install_status'];
|
||||
if ($s === 'installed') {
|
||||
echo '<span class="sw-status-ok">Installed</span>';
|
||||
} elseif ($s === 'queued') {
|
||||
echo '<span class="sw-status-queued">Queued</span>';
|
||||
} elseif ($s === 'failed') {
|
||||
echo '<span class="sw-status-failed">Failed</span>';
|
||||
} elseif ($s === 'updating') {
|
||||
echo '<span class="sw-status-progress">Updating</span>';
|
||||
} else {
|
||||
echo '<span class="sw-muted">Not installed</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?= !empty($mod['last_updated_at']) ? sw_h($mod['last_updated_at']) : '-' ?></td>
|
||||
<td title="<?= sw_h($mod['last_error'] ?? '') ?>"><?= $shortError !== '' ? sw_h($shortError) : '-' ?></td>
|
||||
<td style="white-space:nowrap;">
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
|
||||
<input type="hidden" name="action" value="move_up">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<button type="submit" class="button small" <?= $idx === 0 ? 'disabled' : '' ?>>▲</button>
|
||||
</form>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
|
||||
<input type="hidden" name="action" value="move_down">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<button type="submit" class="button small" <?= $idx === (count($mods) - 1) ? 'disabled' : '' ?>>▼</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
|
||||
<input type="hidden" name="action" value="delete_mod">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<button type="submit" class="button small danger" onclick="return confirm('Remove this mod from the list?');" style="background:#d9534f;color:#fff;">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
|
@ -50,11 +50,16 @@ Start with:
|
|||
- [docs/architecture/REPOSITORY_OVERVIEW.md](docs/architecture/REPOSITORY_OVERVIEW.md)
|
||||
- [docs/architecture/PANEL_AGENT_FLOW.md](docs/architecture/PANEL_AGENT_FLOW.md)
|
||||
- [docs/architecture/API_REFERENCE.md](docs/architecture/API_REFERENCE.md)
|
||||
- [docs/architecture/PANEL_AGENT_COMMANDS.md](docs/architecture/PANEL_AGENT_COMMANDS.md)
|
||||
- [docs/architecture/MODULE_DEPENDENCIES.md](docs/architecture/MODULE_DEPENDENCIES.md)
|
||||
- [docs/architecture/LIBRARY_REFERENCE.md](docs/architecture/LIBRARY_REFERENCE.md)
|
||||
- [docs/modules/MODULE_INDEX.md](docs/modules/MODULE_INDEX.md)
|
||||
- [docs/features/STATUS_SYSTEM.md](docs/features/STATUS_SYSTEM.md)
|
||||
- [docs/features/XML_SYSTEM.md](docs/features/XML_SYSTEM.md)
|
||||
- [docs/features/WORKSHOP_SYSTEM.md](docs/features/WORKSHOP_SYSTEM.md)
|
||||
- [docs/features/SCHEDULER_SYSTEM.md](docs/features/SCHEDULER_SYSTEM.md)
|
||||
- [docs/features/SCHEDULER_ACTIONS.md](docs/features/SCHEDULER_ACTIONS.md)
|
||||
- [docs/features/USER_API.md](docs/features/USER_API.md)
|
||||
|
||||
Additional architecture and decision records:
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,18 @@ Relevant function:
|
|||
|
||||
The agent reads screen logs and may also copy a local log file into the game home. Logs should be treated as runtime output, not as a state store.
|
||||
|
||||
## Workshop / Server Content
|
||||
|
||||
The primary Workshop workflow is owned by the Panel `addonsmanager`, not the legacy `steam_workshop` RPC. For Linux servers the Panel:
|
||||
|
||||
1. writes `gsp_server_content/workshop_manifest.json` under the server home
|
||||
2. stages `generic_steam_workshop_linux.sh` under `gsp_server_content/scripts/workshop/`
|
||||
3. invokes the staged script through the authenticated `exec` RPC
|
||||
|
||||
The staged script uses Python and SteamCMD, validates numeric Workshop IDs, keeps writes under the server home, logs to `gsp_server_content/workshop_install.log`, and supports DayZ/Arma-style `@mod` folders plus `.bikey` copying.
|
||||
|
||||
The older `steam_workshop` XML-RPC method remains for legacy compatibility only and should not be treated as the primary customer workflow.
|
||||
|
||||
## Scheduler
|
||||
|
||||
Linux scheduler functions live in `ogp_agent.pl`:
|
||||
|
|
|
|||
|
|
@ -107,6 +107,18 @@ Relevant function:
|
|||
|
||||
Windows/Cygwin logs come from screen logs and/or local copies. Log retrieval should remain compatible with the Panel's AJAX log view.
|
||||
|
||||
## Workshop / Server Content
|
||||
|
||||
The primary Workshop workflow is owned by the Panel `addonsmanager`, not the legacy `steam_workshop` RPC. For Windows/Cygwin servers the Panel:
|
||||
|
||||
1. writes `gsp_server_content/workshop_manifest.json` under the server home
|
||||
2. stages `generic_steam_workshop_windows_cygwin.sh` under `gsp_server_content/scripts/workshop/`
|
||||
3. invokes the staged script through the authenticated `exec` RPC
|
||||
|
||||
The staged script uses Python and SteamCMD, validates numeric Workshop IDs, keeps writes under the server home, logs to `gsp_server_content/workshop_install_windows.log`, and supports DayZ/Arma-style `@mod` folders plus `.bikey` copying.
|
||||
|
||||
The older `steam_workshop` XML-RPC method remains for legacy compatibility only and should not be treated as the primary customer workflow.
|
||||
|
||||
## Scheduler
|
||||
|
||||
Relevant functions:
|
||||
|
|
|
|||
|
|
@ -1,166 +1,267 @@
|
|||
# API Reference
|
||||
|
||||
## Overview
|
||||
## Scope
|
||||
|
||||
Panel-Agent communication uses XML-RPC over HTTP.
|
||||
This is the master reference for:
|
||||
|
||||
Main transport wrapper:
|
||||
- Panel -> agent XML-RPC
|
||||
- external Panel API routes
|
||||
- public status API
|
||||
- Website payment/webhook endpoints
|
||||
|
||||
- `Panel/includes/lib_remote.php`
|
||||
Detailed command catalog:
|
||||
|
||||
Main agent implementation files:
|
||||
- `docs/architecture/PANEL_AGENT_COMMANDS.md`
|
||||
|
||||
- `Agent_Linux/ogp_agent.pl`
|
||||
- `Agent-Windows/ogp_agent.pl`
|
||||
Scheduler action catalog:
|
||||
|
||||
The Panel encrypts arguments before sending them to the agent. The agent decodes them, performs the requested action locally, then returns a status or payload.
|
||||
- `docs/features/SCHEDULER_ACTIONS.md`
|
||||
|
||||
## General Call Pattern
|
||||
## Architecture Overview
|
||||
|
||||
```text
|
||||
Panel module
|
||||
-> OGPRemoteLibrary method in lib_remote.php
|
||||
-> XML-RPC request to agent /RPC2
|
||||
-> agent subroutine in ogp_agent.pl
|
||||
-> local screen/process/file/network action
|
||||
-> return code or structured payload
|
||||
home.php module request
|
||||
-> module PHP page
|
||||
-> shared include libraries
|
||||
-> database layer
|
||||
-> optional agent RPC through lib_remote.php
|
||||
|
||||
external automation
|
||||
-> Panel/ogp_api.php
|
||||
-> api_* handler
|
||||
-> same module/library logic
|
||||
-> optional agent RPC
|
||||
|
||||
public node status
|
||||
-> Panel/status_api.php
|
||||
-> direct remote host probe + optional agent stats
|
||||
|
||||
website checkout/webhook
|
||||
-> Website/api/* or Website/webhook.php
|
||||
-> billing runtime
|
||||
-> DB and payment gateway APIs
|
||||
```
|
||||
|
||||
## Core RPC Methods
|
||||
## Panel -> Agent XML-RPC
|
||||
|
||||
| RPC / Wrapper | Purpose | Common Callers | Notes |
|
||||
|---|---|---|---|
|
||||
| `status_chk` | Quick agent reachability check | Panel health checks | Returns a lightweight online/offline/encryption state. |
|
||||
| `rfile_exists` | Test if a remote file exists | File/content helpers | Used for safe file checks before read/write actions. |
|
||||
| `get_log` | Fetch screen or console log text | `gamemanager` | Returns log content plus a status code. |
|
||||
| `remote_stop_server` / `stop_server` | Stop a game server | `gamemanager`, scheduler actions | Takes control protocol and home path data. |
|
||||
| `remote_send_rcon_command` / `send_rcon_command` | Send RCON/console command | `gamemanager`, `rcon`, scheduler warnings | Useful for warning messages and admin commands. |
|
||||
| `remote_readfile` / `readfile` | Read a remote file | File editor, config tools | Must remain path-safe. |
|
||||
| `remote_writefile` / `writefile` | Write a remote file | File editor, config tools | Must remain path-safe. |
|
||||
| `universal_start` | Start a game server | `gamemanager` | Launches a managed `screen` session. |
|
||||
| `is_screen_running` | Test whether a managed screen session exists | `gamemanager`, monitor pages | Legacy/simple screen check, not full readiness. |
|
||||
| `remote_server_status` / `server_status` | Structured server status | `gamemanager` | The preferred source for online/start/stop state. |
|
||||
| `remote_restart_server` / `restart_server` | Restart a server | `gamemanager` | Intended to be stop, wait, start. |
|
||||
| `remote_query` | Query game metadata | `gamemanager` | Optional metadata only. |
|
||||
| `exec` | Execute a constrained agent-side command | `gamemanager` fallback status checks, admin tooling | The Game Monitor currently uses this only as a fallback to test whether the configured game port is listening with `ss`/`netstat`. Customer input must not be passed through this path. |
|
||||
| `steam_cmd` / `steam` / `automatic_steam_update` | SteamCMD-based update flows | Update actions, Workshop/install tools | Used by Steam-based server maintenance. |
|
||||
| `start_file_download` | Download external content | Update/content tools | Returns a PID or progress handle. |
|
||||
| `uncompress_file` | Extract archives | Content installers | Used for zip/tar package installs. |
|
||||
| `remote_dirlist` / `remote_dirlistfm` | Read remote directory listings | File manager | Used for browse views. |
|
||||
| `cpu_count`, `renice_process`, `force_cpu` | System/process control helpers | Update/start flows | Used during server launch optimization. |
|
||||
| `clone_home` / `remove_home` | Duplicate or delete a server home | Provisioning/admin tools | Used during provisioning and teardown. |
|
||||
| `secure_path`, `get_chattr` | Path safety and attribute helpers | File and security tools | Helps enforce control-path rules. |
|
||||
| `ftp_mgr` | FTP management helper | FTP module | Used to manage server FTP state. |
|
||||
| `compress_files` | Archive files | Backup/content tools | Candidate building block for backups. |
|
||||
| `start_fastdl` / `stop_fastdl` / `restart_fastdl` / `fastdl_status` | FastDL service management | `fast_download` | Source/GoldSrc web distribution support. |
|
||||
| `scheduler_*` methods | Task list and task CRUD | `cron` | Agent-owned scheduler implementation. |
|
||||
| `agent_restart` | Restart the agent itself | Admin maintenance | Node maintenance action. |
|
||||
| `component_update` | Queue a Git-based agent code update | Update module | Admin-only. Uses encrypted XML-RPC, validates repo/branch/source/destination, launches a detached updater, preserves agent config/runtime/server data, and returns queued status plus an agent log path. |
|
||||
| `shell_action` | Run a shell action in a controlled way | Advanced agent operations | Should remain tightly permissioned. |
|
||||
| `send_steam_guard_code` | Submit Steam Guard code | Steam authenticated installs | Used for authenticated SteamCMD workflows. |
|
||||
| `steam_workshop` / `get_workshop_mods_info` | Workshop-related helper calls | Workshop/content flows | Active in current module work, but still being consolidated. |
|
||||
Transport:
|
||||
|
||||
## Status And Return Patterns
|
||||
- XML-RPC over HTTP
|
||||
- endpoint path `/RPC2`
|
||||
- wrapper `Panel/includes/lib_remote.php`
|
||||
- server implementations in `Agent_Linux/ogp_agent.pl` and `Agent-Windows/ogp_agent.pl`
|
||||
|
||||
Return values differ by method, but common patterns are:
|
||||
Primary categories:
|
||||
|
||||
- `1` or positive values for success
|
||||
- `0` for offline/unavailable
|
||||
- `-1` or another negative value for error
|
||||
- structured arrays/hashes for richer status methods
|
||||
| Category | Examples |
|
||||
|---|---|
|
||||
| lifecycle | `server_status`, `universal_start`, `stop_server`, `restart_server` |
|
||||
| files | `readfile`, `writefile`, `dirlist`, `get_file_part` |
|
||||
| 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` |
|
||||
|
||||
`remote_server_status` is the important structured response. It should return the agent as source of truth for:
|
||||
See the full command table in:
|
||||
|
||||
- `docs/architecture/PANEL_AGENT_COMMANDS.md`
|
||||
|
||||
## Panel Wrapper Methods
|
||||
|
||||
Most Panel modules do not build XML-RPC directly. They call `OGPRemoteLibrary`.
|
||||
|
||||
| Wrapper Method | Agent Command | Common Modules |
|
||||
|---|---|---|
|
||||
| `status_chk()` | `quick_chk` | `cron`, `server`, `status`, `dashboard`, update pages |
|
||||
| `remote_server_status()` | `server_status` | `gamemanager` |
|
||||
| `universal_start()` | `universal_start` | `gamemanager`, API start |
|
||||
| `remote_stop_server()` | `stop_server` | `gamemanager`, billing, API stop |
|
||||
| `remote_restart_server()` | `restart_server` | `gamemanager`, API restart |
|
||||
| `remote_send_rcon_command()` | `send_rcon_command` | `gamemanager`, `rcon`, util helpers |
|
||||
| `remote_readfile()` / `remote_writefile()` | `readfile` / `writefile` | `litefm`, `editconfigfiles`, `addonsmanager`, `mysql`, `server` |
|
||||
| `remote_query()` | `remote_query` | `gamemanager`, dashboards |
|
||||
| `component_update()` | `component_update` | update/admin pages |
|
||||
| `scheduler_*()` | `scheduler_*` | `cron` |
|
||||
| `steam_workshop()` | `steam_workshop` | legacy workshop path |
|
||||
|
||||
## External Panel API
|
||||
|
||||
Primary endpoint:
|
||||
|
||||
- `Panel/ogp_api.php`
|
||||
|
||||
Auth model:
|
||||
|
||||
- token-based
|
||||
- token issued by `token/create`
|
||||
- requests can be GET, POST, or JSON body
|
||||
- host allowlist can be enforced
|
||||
|
||||
Top-level API handlers implemented in `ogp_api.php`:
|
||||
|
||||
| Handler Function | Route Prefix | Purpose |
|
||||
|---|---|---|
|
||||
| `api_token()` | `token/*` | create/test tokens |
|
||||
| `api_server()` | `server/*` | remote agent CRUD/status |
|
||||
| `api_user_games()` | `user_games/*` | home provisioning and listing |
|
||||
| `api_user_admin()` | `user_admin/*` | user CRUD and assignments |
|
||||
| `api_gamemanager_admin()` | `gamemanager_admin/*` | admin game manager actions |
|
||||
| `api_gamemanager()` | `gamemanager/*` | start/stop/restart/update/RCON |
|
||||
| `api_litefm()` | `litefm/*` | file listing/read/write/delete |
|
||||
| `api_addonsmanager()` | `addonsmanager/*` | content template listing/install |
|
||||
| `api_server_content()` | `server_content/*` | scheduled content actions |
|
||||
| `api_steam_workshop()` | `steam_workshop/*` | legacy workshop installs |
|
||||
| `api_setting()` | `setting/*` | setting read helper |
|
||||
|
||||
Detailed route list:
|
||||
|
||||
| Route | Purpose |
|
||||
|---|---|
|
||||
| `token/create`, `token/test` | auth bootstrap |
|
||||
| `server/list`, `server/status`, `server/restart`, `server/create`, `server/remove`, `server/add_ip`, `server/remove_ip`, `server/list_ips`, `server/edit_ip` | remote host management |
|
||||
| `user_games/list_games`, `user_games/list_servers`, `user_games/create`, `user_games/clone`, `user_games/set_expiration` | server-home management |
|
||||
| `user_admin/list`, `user_admin/get`, `user_admin/create`, `user_admin/remove`, `user_admin/set_expiration`, `user_admin/list_assigned`, `user_admin/assign`, `user_admin/remove_assign` | user management |
|
||||
| `gamemanager/start`, `gamemanager/stop`, `gamemanager/restart`, `gamemanager/rcon`, `gamemanager/update` | lifecycle automation |
|
||||
| `gamemanager_admin/reorder` | admin ordering helper |
|
||||
| `litefm/list`, `litefm/get`, `litefm/save`, `litefm/remove` | remote file operations |
|
||||
| `addonsmanager/list`, `addonsmanager/install` | add-on templates |
|
||||
| `steam_workshop/install` | legacy Workshop install |
|
||||
| `server_content/run_scheduled_action` | typed content action trigger |
|
||||
| `setting/get` | panel setting read |
|
||||
|
||||
See:
|
||||
|
||||
- `docs/features/USER_API.md`
|
||||
|
||||
## Public Status API
|
||||
|
||||
Endpoint:
|
||||
|
||||
- `Panel/status_api.php`
|
||||
|
||||
Purpose:
|
||||
|
||||
- public or semi-public read-only summary of remote nodes
|
||||
|
||||
Auth:
|
||||
|
||||
- shared query token
|
||||
|
||||
Behavior:
|
||||
|
||||
- probes remote agents with TCP
|
||||
- optionally calls agent stats method names if available
|
||||
- caches JSON locally for 30 seconds
|
||||
|
||||
Return shape:
|
||||
|
||||
- `generated_at`
|
||||
- `nodes[]`
|
||||
- `name`
|
||||
- `host`
|
||||
- `agent_port`
|
||||
- `online`
|
||||
- `cpu_percent`
|
||||
- `mem_percent`
|
||||
- `disk_percent`
|
||||
|
||||
## Website API And Webhooks
|
||||
|
||||
| Endpoint | Purpose | Auth / Verification |
|
||||
|---|---|---|
|
||||
| `Website/api/create_order.php` | create PayPal order | storefront checkout/session context |
|
||||
| `Website/api/capture_order.php` | capture PayPal order | storefront checkout/session context |
|
||||
| `Website/api/log_error.php` | client-side error logging | open endpoint, writes log |
|
||||
| `Website/webhook.php` | verified PayPal webhook processing | OAuth + webhook signature verification |
|
||||
| `Website/paypal/webhook.php` | compatibility wrapper to active webhook runtime | same |
|
||||
|
||||
## Internal Module Endpoints
|
||||
|
||||
These are not public APIs in the same sense as `ogp_api.php`, but they matter architecturally.
|
||||
|
||||
| Endpoint | Module | Purpose |
|
||||
|---|---|---|
|
||||
| `Panel/modules/gamemanager/get_server_log.php` | `gamemanager` | AJAX log refresh |
|
||||
| `Panel/modules/dashboard/updateWidgets.php` | `dashboard` | dashboard async refresh |
|
||||
| `Panel/modules/tickets/notificationCount.php` | `tickets` | unread count |
|
||||
| `Panel/modules/cron/events.php` | `cron` | scheduler log refresh |
|
||||
| `Panel/modules/litefm/get_file.php` | `litefm` | chunked file downloads |
|
||||
|
||||
## Workshop Manifest Contract
|
||||
|
||||
Current preferred Workshop/content flow:
|
||||
|
||||
1. Panel validates Workshop IDs
|
||||
2. Panel writes manifest JSON to server home
|
||||
3. Panel stages bundled helper script to agent host via `writefile`
|
||||
4. Panel runs helper with `exec`
|
||||
5. Agent logs/install output is returned to Panel
|
||||
|
||||
Common manifest fields:
|
||||
|
||||
- `manifest_version`
|
||||
- `home_id`
|
||||
- `home_cfg_id`
|
||||
- `content_type`
|
||||
- `action`
|
||||
- `items`
|
||||
- `options`
|
||||
|
||||
For Workshop-specific manifests, the broader fields documented in `WORKSHOP_SYSTEM.md` also apply, including app IDs, install strategy, and target paths.
|
||||
|
||||
## Status Contract
|
||||
|
||||
`server_status` is the preferred runtime contract.
|
||||
|
||||
Expected fields:
|
||||
|
||||
- `status`
|
||||
- `ready`
|
||||
- `process_running`
|
||||
- `session_running`
|
||||
- `game_port_listening`
|
||||
- `query_port_listening`
|
||||
- `rcon_port_listening`
|
||||
- `pid`
|
||||
- `session_name`
|
||||
- `ip`
|
||||
- `port`
|
||||
- `query_port`
|
||||
- `rcon_port`
|
||||
- `last_error`
|
||||
- `query_info`
|
||||
|
||||
State meanings:
|
||||
|
||||
- `OFFLINE`
|
||||
- `STARTING`
|
||||
- `ONLINE`
|
||||
- `STARTING`
|
||||
- `STOPPING`
|
||||
- `OFFLINE`
|
||||
- `UNRESPONSIVE`
|
||||
- `UNKNOWN`
|
||||
|
||||
The Panel Game Monitor has a defensive fallback path while the structured status system is still being hardened:
|
||||
Rules:
|
||||
|
||||
```text
|
||||
remote_server_status if available
|
||||
-> is_screen_running fallback
|
||||
-> agent-side port listening check through exec
|
||||
-> LGSL/GameQ only for optional player/map/query metadata
|
||||
```
|
||||
- query failure alone must not imply offline
|
||||
- agent process/session or game-port evidence is sufficient for online
|
||||
- `UNKNOWN` is reserved for unreachable or inconclusive agent state
|
||||
|
||||
A confirmed managed screen/session, process flag, or listening game port is enough for the Panel to display `ONLINE`. Query failure alone must not produce `OFFLINE` or `UNKNOWN`.
|
||||
## Scheduler-As-API Contract
|
||||
|
||||
Current compatibility behavior:
|
||||
Agent cron jobs often execute URLs like:
|
||||
|
||||
- New agents should expose `server_status` / `remote_server_status`.
|
||||
- Older agents may only expose `is_screen_running` and generic `exec`.
|
||||
- The Panel therefore falls back to `is_screen_running` and an agent-side port check.
|
||||
- Agents report `ONLINE` when the configured game port is listening even if the managed screen/session cannot be found, with `last_error` carrying the warning.
|
||||
- LGSL/GameQ/Steam query data is optional metadata only.
|
||||
- `ogp_api.php?gamemanager/start`
|
||||
- `ogp_api.php?gamemanager/stop`
|
||||
- `ogp_api.php?gamemanager/restart`
|
||||
- `ogp_api.php?gamemanager/update&type=steam`
|
||||
- `ogp_api.php?server_content/run_scheduled_action`
|
||||
|
||||
## Sequence Diagrams
|
||||
This means `ogp_api.php` is part of the scheduler runtime and must stay backward compatible with those generated URLs.
|
||||
|
||||
### Start
|
||||
## Search Coverage Used For This Document
|
||||
|
||||
```text
|
||||
Panel
|
||||
-> universal_start
|
||||
Agent
|
||||
-> create screen session
|
||||
-> launch server command
|
||||
Panel
|
||||
-> remote_server_status polling
|
||||
Agent
|
||||
-> session/process + port checks
|
||||
Panel
|
||||
-> render STARTING / ONLINE / UNRESPONSIVE
|
||||
```
|
||||
|
||||
### Stop
|
||||
|
||||
```text
|
||||
Panel
|
||||
-> remote_stop_server
|
||||
Agent
|
||||
-> graceful stop command if available
|
||||
-> wait for session/process exit
|
||||
-> escalate to kill if needed
|
||||
Panel
|
||||
-> remote_server_status polling
|
||||
Agent
|
||||
-> confirm OFFLINE or UNRESPONSIVE
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```text
|
||||
Panel log page
|
||||
-> get_log
|
||||
Agent
|
||||
-> read screen log or console log
|
||||
Panel
|
||||
-> refresh dedicated preformatted log panel via AJAX
|
||||
```
|
||||
|
||||
### Remote Agent Component Update
|
||||
|
||||
```text
|
||||
Panel update page
|
||||
-> component_update(payload)
|
||||
Agent
|
||||
-> validate repo, branch, source folder, destination
|
||||
-> write detached updater script
|
||||
-> return queued + log path
|
||||
Updater script
|
||||
-> clone/fetch repository into staging
|
||||
-> copy Agent_Linux or Agent-Windows folder only
|
||||
-> preserve Cfg, ServerFiles, Schedule, logs, steamcmd, startups, pid files
|
||||
-> validate ogp_agent.pl
|
||||
-> restart agent through systemd or screen fallback
|
||||
```
|
||||
|
||||
## Error Handling Notes
|
||||
|
||||
- Do not treat query failure as a start failure by itself.
|
||||
- Do not treat marker-file presence as the source of truth.
|
||||
- Do not expose raw shell execution to customers through generic RPC routes.
|
||||
- When a method returns a payload, record the error text in the Panel UI if the operation fails.
|
||||
- `sed -n '1,240p' Panel/ogp_api.php`
|
||||
- `rg -n "^function api_" Panel/ogp_api.php`
|
||||
- `sed -n '1,240p' Panel/status_api.php`
|
||||
- `find Website/api -maxdepth 1 -type f`
|
||||
- `sed -n '1,220p' Website/webhook.php`
|
||||
|
|
|
|||
159
docs/architecture/LIBRARY_REFERENCE.md
Normal file
159
docs/architecture/LIBRARY_REFERENCE.md
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
# Library Reference
|
||||
|
||||
## Scope
|
||||
|
||||
This file documents the shared Panel libraries and helper entrypoints that multiple modules reuse.
|
||||
|
||||
Primary directories:
|
||||
|
||||
- `Panel/includes/`
|
||||
- `Panel/protocol/`
|
||||
|
||||
No separate `Panel/libraries/` or `Panel/common/` tree is currently used as the main shared layer.
|
||||
|
||||
## Core Include Files
|
||||
|
||||
| File | Purpose | Used By |
|
||||
|---|---|---|
|
||||
| `Panel/includes/config.inc.php` | Main runtime configuration | global bootstrap, APIs, installers |
|
||||
| `Panel/includes/database.php` | DB abstraction selector | bootstrap |
|
||||
| `Panel/includes/database_mysqli.php` | Main database implementation and business-query layer | almost all stateful modules |
|
||||
| `Panel/includes/functions.php` | general utility helpers, formatting, logging, Discord webhook helper | global |
|
||||
| `Panel/includes/helpers.php` | session, auth, panel helper functions | global |
|
||||
| `Panel/includes/html_functions.php` | HTML rendering helpers | module UIs |
|
||||
| `Panel/includes/form_table_class.php` | form builder helper | admin and config forms |
|
||||
| `Panel/includes/lang.php` | translation loading | global |
|
||||
| `Panel/includes/lib_remote.php` | XML-RPC wrapper for agent commands | every agent-aware module |
|
||||
| `Panel/includes/navig.php` | module/page routing | main panel request flow |
|
||||
| `Panel/includes/view.php` | page shell and shared JS injection | main panel request flow |
|
||||
| `Panel/includes/refreshed.php` | refresh helper | cron/events and refresh-style pages |
|
||||
| `Panel/includes/debug.php` | debug support | limited / diagnostic use |
|
||||
| `Panel/includes/ip_in_range.php` | CIDR / IP-range helper | API host authorization |
|
||||
| `Panel/includes/api_functions.php` | `ogp_api.php` argument maps and API-side helper logic | external API, scheduler URLs |
|
||||
|
||||
## `lib_remote.php` Wrapper Surface
|
||||
|
||||
`Panel/includes/lib_remote.php` is the most important shared library in the repository.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- XML-RPC request creation
|
||||
- encryption of parameters
|
||||
- RPC request transport
|
||||
- decoding return payloads
|
||||
- compatibility fallbacks for older agents
|
||||
|
||||
Major consumers:
|
||||
|
||||
- `gamemanager`
|
||||
- `cron`
|
||||
- `server`
|
||||
- `user_games`
|
||||
- `addonsmanager`
|
||||
- `litefm`
|
||||
- `ftp`
|
||||
- `mysql`
|
||||
- `status`
|
||||
- `dashboard`
|
||||
- `util`
|
||||
|
||||
See:
|
||||
|
||||
- `docs/architecture/PANEL_AGENT_COMMANDS.md`
|
||||
|
||||
## Database Layer
|
||||
|
||||
`Panel/includes/database_mysqli.php` is more than a raw DB adapter. It acts as a domain service layer.
|
||||
|
||||
It owns:
|
||||
|
||||
- users and roles
|
||||
- remote servers
|
||||
- game homes
|
||||
- config homes and mods
|
||||
- API tokens
|
||||
- widget settings
|
||||
- many module-specific table helpers
|
||||
|
||||
Practical rule:
|
||||
|
||||
- before adding direct SQL inside a module, check whether `database_mysqli.php` already exposes the needed operation
|
||||
|
||||
## API Helper Layer
|
||||
|
||||
`Panel/includes/api_functions.php` supports `Panel/ogp_api.php`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- endpoint argument definitions
|
||||
- startup command generation
|
||||
- RCON dispatch helper
|
||||
- API host authorization checks
|
||||
|
||||
This file is also indirectly used by the scheduler because cron jobs call back into `ogp_api.php`.
|
||||
|
||||
## XML / Game Config Parsing
|
||||
|
||||
Shared files:
|
||||
|
||||
- `Panel/modules/config_games/server_config_parser.php`
|
||||
- `Panel/modules/config_games/schema_server_config.xml`
|
||||
- `Panel/modules/config_games/xml_tag_descriptions.php`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- validate game XML files
|
||||
- load startup/query/install metadata
|
||||
- expose game config values to `gamemanager`, `user_games`, `addonsmanager`, `dsi`, and APIs
|
||||
|
||||
## Protocol Libraries
|
||||
|
||||
| Path | Purpose | Used By |
|
||||
|---|---|---|
|
||||
| `Panel/protocol/lgsl/` | LGSL query implementation | `gamemanager`, `dsi`, XML helpers |
|
||||
| `Panel/protocol/GameQ/` | GameQ query implementation | `gamemanager`, `dsi`, XML helpers |
|
||||
|
||||
Important note:
|
||||
|
||||
- query libraries provide optional metadata
|
||||
- they are not the source of truth for runtime status
|
||||
|
||||
## Bundled Third-Party Libraries In Modules
|
||||
|
||||
These are module-local, not general-purpose shared libs, but they matter for architecture.
|
||||
|
||||
| Path | Purpose | Main Module |
|
||||
|---|---|---|
|
||||
| `Panel/modules/ftp/` bundled net2ftp code | browser-based FTP UI | `ftp` |
|
||||
| `Panel/modules/TS3Admin/` | TeamSpeak admin webapp | `TS3Admin` |
|
||||
| `Panel/modules/teamspeak3/ts3admin.class.php` and related files | TeamSpeak integration | `teamspeak3` |
|
||||
| `Panel/modules/gamemanager/MinecraftRcon.class.php` | Minecraft-specific RCON client | `gamemanager` |
|
||||
| `Panel/modules/news/include/library/HTMLPurifier/` | content sanitization | `news` |
|
||||
|
||||
## Filesystem And Update Helpers
|
||||
|
||||
Notable update-related shared logic currently lives in:
|
||||
|
||||
- `Panel/modules/administration/panel_update.php`
|
||||
- `Panel/modules/update/updating.php`
|
||||
- `Panel/modules/update/patch_manager.php`
|
||||
- `Panel/modules/update/post_update.php`
|
||||
|
||||
These are not in `includes/`, but they function as shared update infrastructure used by the admin-facing update UI.
|
||||
|
||||
## Consumers By Concern
|
||||
|
||||
| Concern | Primary Shared Files |
|
||||
|---|---|
|
||||
| Agent RPC | `includes/lib_remote.php` |
|
||||
| Database | `includes/database_mysqli.php` |
|
||||
| API | `includes/api_functions.php`, `ogp_api.php` |
|
||||
| Routing / page shell | `includes/navig.php`, `includes/view.php` |
|
||||
| XML config | `modules/config_games/server_config_parser.php` |
|
||||
| Query metadata | `protocol/lgsl`, `protocol/GameQ` |
|
||||
| Auth/session helpers | `includes/helpers.php`, `includes/functions.php` |
|
||||
|
||||
## Search Coverage Used For This Document
|
||||
|
||||
- `find Panel/includes -maxdepth 1 -type f`
|
||||
- `rg -n "require_once\\('includes/lib_remote.php'|require_once\\(\"modules/config_games/server_config_parser.php\"|require_once\\('protocol/" Panel/modules`
|
||||
148
docs/architecture/MODULE_DEPENDENCIES.md
Normal file
148
docs/architecture/MODULE_DEPENDENCIES.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# Module Dependencies
|
||||
|
||||
## Scope
|
||||
|
||||
This file maps how Panel modules depend on:
|
||||
|
||||
- agents
|
||||
- shared libraries
|
||||
- other modules
|
||||
- database tables
|
||||
- AJAX endpoints
|
||||
- external services
|
||||
|
||||
It answers the practical question: what can call what?
|
||||
|
||||
## Core Dependency Layers
|
||||
|
||||
```text
|
||||
User / admin UI
|
||||
-> home.php module routing
|
||||
-> module page PHP files
|
||||
-> shared include libraries
|
||||
-> database layer
|
||||
-> agent RPC wrapper (lib_remote.php)
|
||||
-> Linux / Windows agents
|
||||
|
||||
AJAX / external API
|
||||
-> module endpoint PHP files or ogp_api.php / status_api.php
|
||||
-> same shared libraries and DB layer
|
||||
-> agent RPC where needed
|
||||
```
|
||||
|
||||
## Shared Internal Hubs
|
||||
|
||||
| Hub | Called By | Purpose |
|
||||
|---|---|---|
|
||||
| `Panel/includes/lib_remote.php` | `gamemanager`, `cron`, `server`, `user_games`, `litefm`, `ftp`, `mysql`, `addonsmanager`, `status`, `dashboard`, `util`, `fast_download`, `billing`, `editconfigfiles` | Agent XML-RPC transport |
|
||||
| `Panel/modules/config_games/server_config_parser.php` | `gamemanager`, `user_games`, `addonsmanager`, `cron`, `dsi`, `config_games`, API | Game XML loading and interpretation |
|
||||
| `Panel/includes/database_mysqli.php` | effectively all stateful modules | Database abstraction and business queries |
|
||||
| `Panel/includes/functions.php` and `helpers.php` | global | auth, logging, formatting, small helpers |
|
||||
| `Panel/includes/view.php` and `navig.php` | routing / page shell | module dispatch and page rendering |
|
||||
|
||||
## Module Dependency Table
|
||||
|
||||
| Module | Calls Agent | Calls Other Modules / Files | Shared Libraries | Major DB / State | AJAX / API Endpoints | External Integrations |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `administration` | Indirectly, for update and logger views | `panel_update.php`, backup/log helpers | `functions.php`, DB layer, `lib_remote.php` | settings, remote server metadata | `watch_logger.php` | Git / Forgejo, filesystem |
|
||||
| `addonsmanager` | Yes | `workshop_action.php`, `server_content_actions.php`, `server_content_helpers.php`, `config_games` | `lib_remote.php`, XML parser | `addons`, `server_content_manifest`, `server_content_workshop` | `workshop_action.php` | SteamCMD, Workshop, archive installers |
|
||||
| `backup-restore` | Limited / indirect | local helper function file | DB layer, filesystem helpers | backup metadata if present | module pages | zip/tar filesystem work |
|
||||
| `billing` | Yes for server provisioning/expiry actions | embeds storefront runtime; shares logic with `Website/` | DB layer, `lib_remote.php`, payment helpers | orders, invoices, coupons, server provisioning state | webhook and checkout entrypoints under module and website | PayPal, Stripe/manual gateways, email |
|
||||
| `circular` | No | own helper file | DB layer | announcement/content records | `show_circular.php` | none |
|
||||
| `config_games` | No direct agent use in main UI | `server_config_parser.php`, `cli-params.php`, `set_mods.php`, `set_params.php` | XML parser, DB layer | `config_homes`, `config_mods` | XML editor-like pages | GameQ, LGSL references |
|
||||
| `cron` | Yes | `shared_cron_functions.php`, uses `ogp_api.php` URLs as scheduled payloads | `lib_remote.php`, XML parser, DB layer | Panel-side scheduler intent; agent-owned cron entries | `events.php`, `thetime.php`, `user_cron.php` | agent scheduler, `wget`, panel API |
|
||||
| `dashboard` | Yes | `query_ref.php`, `updateWidgets.php` | `lib_remote.php`, DB layer | widget prefs, server overview | `updateWidgets.php` | query protocols |
|
||||
| `dsi` | Yes | shared DSI includes, `gamemanager/home_handling_functions.php`, XML parser | `lib_remote.php`, XML parser, GameQ, LGSL | server and game metadata | `image.php`, list/admin pages | GameQ, LGSL, GeoIP |
|
||||
| `editconfigfiles` | Yes | its own helpers/config lists | `lib_remote.php`, DB layer | file edit permissions by home | `modify.php` | remote file editing |
|
||||
| `faq` | No | RSS/parser helper | DB/content layer | FAQ/news content | `faq.js` | RSS parsing |
|
||||
| `fast_download` | Yes | admin/user pages, helper file | `lib_remote.php`, DB layer | fastdl config aliases | module pages | web server / FastDL service |
|
||||
| `ftp` | Yes | bundled net2ftp app files, admin wrapper | `lib_remote.php`, DB layer | remote server FTP config | admin and embedded FTP UI | Pure-FTPd / FTP service, net2ftp |
|
||||
| `gamemanager` | Yes | `home_handling_functions.php`, start/stop/restart/log pages | `lib_remote.php`, XML parser, helpers | server homes, last params, runtime status | `get_server_log.php`, `log.php`, `rcon.php` | screen, RCON, query protocols, SteamCMD |
|
||||
| `lgsl_with_img_mod` | No agent by default | own LGSL pages | LGSL | server query presentation | `feed.php`, `image.php` | LGSL |
|
||||
| `litefm` | Yes | file manager pages and helpers | `lib_remote.php`, session helpers | session download state | `fm_dir.php`, `fm_read.php`, `fm_write.php`, `get_file.php` | remote file management |
|
||||
| `lostpwd` | No | own page | DB layer, mail helpers | users | form endpoints | email |
|
||||
| `modulemanager` | No agent | `module_handling.php`, update modules | filesystem, DB layer | installed module metadata | add/delete/update pages | filesystem |
|
||||
| `mysql` | Yes | own DB server management pages | `lib_remote.php`, DB layer | mysql server records, dumps | `get_dump.php` | MySQL server administration |
|
||||
| `news` | No | admin upload/config pages | DB/content libs | news posts | upload/admin pages | file upload |
|
||||
| `rcon` | Yes | standalone RCON page | `lib_remote.php`, XML parser | server home lookup | module page | RCON |
|
||||
| `register` | No agent | registration helper pages | DB layer | users | `register-exec.php` | captcha/email if configured |
|
||||
| `server` | Yes | add/edit/restart/reboot/firewall/view_log pages | `lib_remote.php`, DB layer | `remote_servers`, IP allocation | `mon_stats.php`, `view_log.php` | agent lifecycle, firewall, reboot |
|
||||
| `settings` | No agent by default | `api_hosts.php`, theme/settings pages | DB layer, API helpers | `settings`, authorized-host files | settings pages | Discord webhook config, API host files |
|
||||
| `status` | Yes | `modules/status/include/*` | `lib_remote.php`, config helpers | remote server list | embedded include pages | CPU/RAM/disk/uptime monitoring |
|
||||
| `subusers` | No direct agent | own CRUD pages | DB layer | users, permissions | form endpoints | none |
|
||||
| `support` | No agent | own page | DB layer, `functions.php` | support requests | support form page | Discord webhook |
|
||||
| `teamspeak3` | No generic agent RPC | own TS3 admin lib set | TS3-specific classes, DB layer | TS3 homes/config | TS3 pages | TeamSpeak query/admin |
|
||||
| `tickets` | No agent by default | attachment, rating, settings, count, submit, view pages | DB layer | tickets, attachments, ratings | `notificationCount.php`, `submitTicket.php`, `downloadAttachment.php` | email/notifications if configured |
|
||||
| `TS3Admin` | No generic agent RPC | bundled TS3 admin web app | TS3 admin classes | TS3 runtime/admin data | JS-driven views | TeamSpeak admin interface |
|
||||
| `tshock` | No generic agent RPC | shared TShock helpers | DB layer | Terraria/TShock tokens/config | create token and management pages | TShock REST/API |
|
||||
| `update` | Indirectly through `administration/panel_update.php` and agent `component_update` | `updating.php`, patch manager | filesystem, DB/settings, `lib_remote.php` | update settings, backups, last update files | module pages | Git / Forgejo |
|
||||
| `user_admin` | No direct agent | user/group CRUD pages | DB layer | users, groups, assignments | form endpoints | none |
|
||||
| `user_games` | Yes | provisioning, clone, migrate, custom fields, browser | `lib_remote.php`, XML parser, DB layer | `server_homes`, `user_homes`, IP allocations, configs | `get_size.php`, browser pages | filesystem, provisioning, rsync |
|
||||
| `util` | Yes | addadmin helpers, SteamID tools, network tools | `lib_remote.php`, DB layer | server lookup | helper pages | RCON, Steam ID conversion |
|
||||
|
||||
## Module-To-Module Relationships
|
||||
|
||||
| Source Module | Target Module / File | Why |
|
||||
|---|---|---|
|
||||
| `gamemanager` | `config_games/server_config_parser.php` | build startup commands, query definitions, control protocol metadata |
|
||||
| `gamemanager` | `addonsmanager/monitor_buttons.php` and content helpers indirectly | content/update actions shown on monitor pages |
|
||||
| `cron` | `addonsmanager/server_content_actions.php` via `ogp_api.php?server_content/run_scheduled_action` | scheduled content checks and installs |
|
||||
| `cron` | `gamemanager` via `ogp_api.php?gamemanager/*` | start, stop, restart, Steam auto update |
|
||||
| `user_games` | `gamemanager/home_handling_functions.php` | delete / teardown and lifecycle-related helpers |
|
||||
| `user_games` | `billing/create_servers.php` | provisioning integration |
|
||||
| `dsi` | `gamemanager/home_handling_functions.php` | shared game-home status logic |
|
||||
| `update` | `administration/panel_update.php` | actual admin-facing updater implementation |
|
||||
| `settings` | `api_functions.php` | authorized host list UI for external API |
|
||||
| `billing` | `Website/` mirrored runtime | storefront and payment logic is shared between panel module and public site |
|
||||
|
||||
## AJAX And Async Endpoints
|
||||
|
||||
| Endpoint | Owning Module | Purpose |
|
||||
|---|---|---|
|
||||
| `Panel/modules/gamemanager/get_server_log.php` | `gamemanager` | live log refresh |
|
||||
| `Panel/modules/dashboard/updateWidgets.php` | `dashboard` | dashboard widget refresh |
|
||||
| `Panel/modules/tickets/notificationCount.php` | `tickets` | unread/support badge count |
|
||||
| `Panel/modules/cron/events.php` | `cron` | scheduler log refresh |
|
||||
| `Panel/modules/litefm/get_file.php` | `litefm` | streamed download chunks |
|
||||
| `Panel/status_api.php` | architecture / public status | panel-local public node summary API |
|
||||
| `Panel/ogp_api.php` | architecture / external API | token-authenticated external automation API |
|
||||
|
||||
## Major Database Ownership
|
||||
|
||||
This is not a full schema map. It identifies ownership boundaries.
|
||||
|
||||
| Table / Group | Main Owners |
|
||||
|---|---|
|
||||
| `settings` | `settings`, `update`, general bootstrap |
|
||||
| `users`, `user_groups`, `user_homes` | `user_admin`, `subusers`, `register`, auth/session flows |
|
||||
| `remote_servers`, `remote_server_ips` | `server`, provisioning, `status`, `dashboard` |
|
||||
| `server_homes`, `home_ip_ports`, `game_mods` | `user_games`, `gamemanager`, `cron`, provisioning |
|
||||
| `config_homes`, `config_mods` | `config_games`, `user_games`, `gamemanager` |
|
||||
| `api_tokens` | `ogp_api.php`, user session helpers |
|
||||
| `server_content_manifest`, `server_content_workshop`, `addons` | `addonsmanager` |
|
||||
| billing/order/invoice/coupon tables | `billing`, `Website/` |
|
||||
| ticket/support tables | `tickets`, `support` |
|
||||
| widget / dashboard tables | `dashboard` |
|
||||
|
||||
## External Integration Map
|
||||
|
||||
| Integration | Primary Modules |
|
||||
|---|---|
|
||||
| SteamCMD | `gamemanager`, `addonsmanager`, Workshop flows, agents |
|
||||
| Steam Workshop | `addonsmanager`, legacy `steam_workshop`, agents |
|
||||
| GameQ / LGSL | `gamemanager`, `dashboard`, `dsi`, XML config |
|
||||
| RCON | `gamemanager`, `rcon`, `util`, scheduler-triggered API calls |
|
||||
| Git / Forgejo | `update`, `administration/panel_update.php`, agent `component_update` |
|
||||
| FTP / Pure-FTPd / net2ftp | `ftp`, provisioning |
|
||||
| Discord webhooks | `settings`, `support`, shared `functions.php::discordmsg` |
|
||||
| PayPal / Stripe / manual payments | `billing`, `Website/api/*`, `Website/webhook.php` |
|
||||
| TeamSpeak | `teamspeak3`, `TS3Admin` |
|
||||
| TShock | `tshock` |
|
||||
|
||||
## Search Coverage Used For This Document
|
||||
|
||||
Key searches:
|
||||
|
||||
- `rg -n "new OGPRemoteLibrary|->remote_|->scheduler_|->component_update|->steam_workshop" Panel/modules Panel/includes`
|
||||
- `rg -n "require|require_once|include|include_once" Panel/modules -g '*.php'`
|
||||
- `find Panel/modules -maxdepth 2 -type f \\( -name '*.php' -o -name '*.js' \\)`
|
||||
208
docs/architecture/PANEL_AGENT_COMMANDS.md
Normal file
208
docs/architecture/PANEL_AGENT_COMMANDS.md
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# Panel-Agent Commands
|
||||
|
||||
## Scope
|
||||
|
||||
This file is the command catalog for the XML-RPC surface between the Panel and the agents.
|
||||
|
||||
Primary files:
|
||||
|
||||
- `Panel/includes/lib_remote.php`
|
||||
- `Agent_Linux/ogp_agent.pl`
|
||||
- `Agent-Windows/ogp_agent.pl`
|
||||
|
||||
Transport:
|
||||
|
||||
- Panel -> agent uses XML-RPC over HTTP to `/RPC2`
|
||||
- Arguments are encrypted by `OGPRemoteLibrary`
|
||||
- Agents validate the encryption marker before executing commands
|
||||
|
||||
## Command Dispatch
|
||||
|
||||
Linux dispatch table:
|
||||
|
||||
- `Agent_Linux/ogp_agent.pl`
|
||||
|
||||
Windows dispatch table:
|
||||
|
||||
- `Agent-Windows/ogp_agent.pl`
|
||||
|
||||
Important parity note:
|
||||
|
||||
- Linux exposes `renice_process`
|
||||
- Linux exposes `lock_additional_files`
|
||||
- Windows does not expose those two commands in its current dispatcher
|
||||
|
||||
## Return Conventions
|
||||
|
||||
Common patterns:
|
||||
|
||||
- `1`: success
|
||||
- `0`: false, offline, or not running depending on command
|
||||
- negative values: error or unreachable state
|
||||
- arrays / hashes: structured payloads for newer commands
|
||||
- base64-encoded text payloads are common for file, shell, and stats responses
|
||||
|
||||
## Lifecycle And Status Commands
|
||||
|
||||
| Agent Command | Purpose | Key Arguments | Returns | Windows | Linux | Panel Callers | Security Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `quick_chk` | Lightweight reachability and encryption check | none | `1`, `0`, or error | Yes | Yes | `lib_remote.php::status_chk`, most modules before remote work | Safe probe; no server-side mutation |
|
||||
| `is_screen_running` | Legacy managed-session check | `screen_type`, `home_id` | `1`, `0`, `-1` | Yes | Yes | `gamemanager`, status fallback, legacy monitor code | Session existence only; not a full readiness check |
|
||||
| `server_status` | Structured server state check | `home_id`, `server_ip`, `server_port`, `query_port`, `rcon_port`, `startup_timeout`, `state_hint` | hash with state flags | Yes | Yes | `gamemanager/home_handling_functions.php`, `server_monitor.php` | Preferred source of truth for runtime state |
|
||||
| `universal_start` | Start a game server | `home_id`, `game_home`, `game_binary`, `run_dir`, `startup_cmd`, `server_port`, `server_ip`, `cpu`, `nice`, `preStart`, `envVars`, `game_key`, `console_log` | `1`, `-1`, `-2` | Yes | Yes | `gamemanager/start_server.php`, restart flows, API start | Starts managed `screen` session and game command |
|
||||
| `stop_server` | Stop a running server | `home_id`, `server_ip`, `server_port`, `control_protocol`, `control_password`, `control_type`, `game_home` | `1`, `0`, negative error | Yes | Yes | `gamemanager/stop_server.php`, restart flows, API stop, billing expiry flows | Can issue control-protocol commands and escalation kills |
|
||||
| `restart_server` | Restart a running server | start + stop argument set | `1`, `0`, `-1`, `-2` | Yes | Yes | `gamemanager/restart_server.php`, API restart, scheduler | Depends on both stop and start paths working |
|
||||
| `send_rcon_command` | Send RCON / console command | `home_id`, `server_ip`, `server_port`, `control_protocol`, `control_password`, `control_type`, `command`, `return` | `1`, `-10`, error | Yes | Yes | `gamemanager/rcon.php`, `rcon`, util helpers, API rcon | Admin/customer command surface; must not accept untrusted shell text |
|
||||
| `remote_query` | Query game metadata | `protocol`, `game_type`, `ip`, `c_port`, `q_port`, `s_port` | base64 payload or null/error | Yes | Yes | `gamemanager`, dashboard, DSI | Metadata only; not source of truth for online/offline |
|
||||
| `renice_process` | Adjust process priority | process / nice inputs | success / error | No | Yes | `lib_remote.php::renice_process`, startup tuning | Linux-only; process control |
|
||||
| `cpu_count` | Return CPU/core count | none | integer | Yes | Yes | start flows, provisioning, tuning UIs | Read-only system info |
|
||||
|
||||
## File And Filesystem Commands
|
||||
|
||||
| Agent Command | Purpose | Key Arguments | Returns | Windows | Linux | Panel Callers | Security Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `rfile_exists` | Check remote file existence | `path` | `1` / `0` | Yes | Yes | file tools, content helpers | Must remain path-safe |
|
||||
| `readfile` | Read remote file | `path` | status + base64 data | Yes | Yes | `litefm`, `gamemanager`, `editconfigfiles`, `mysql`, `server`, API helpers | Reads customer server files and some agent logs |
|
||||
| `writefile` | Write remote file | `path`, `content` | `1` / error | Yes | Yes | `litefm`, `editconfigfiles`, `gamemanager`, `addonsmanager`, `mysql`, `fast_download`, `server` | High-risk write path; caller must validate target path |
|
||||
| `dirlist` | Directory listing | `path` | array / error | Yes | Yes | `gamemanager/mini_start.php`, older browsers | Read-only |
|
||||
| `dirlistfm` | File-manager-specific directory listing | `path` | array / error | Yes | Yes | `litefm`, `user_games/browser.php` | Read-only |
|
||||
| `get_file_part` | Stream file in chunks | `path`, `offset` | next offset + chunk | Yes | Yes | `litefm/get_file.php` | Download helper for large files |
|
||||
| `secure_path` | Agent path validation helper | `action`, `path` | base64 payload / validation result | Yes | Yes | update/content/security flows | Important for safe agent-side path operations |
|
||||
| `get_chattr` | Read filesystem attributes | `path` | attributes text | Yes | Yes | admin/security tools | Mostly Linux semantics |
|
||||
| `lock_additional_files` | Lock or unlock extra files | `game_home`, `filesToLockUnlock`, `action` | success / error | No | Yes | `lib_remote.php::lock_additional_home_files` | Linux-only file-attribute helper |
|
||||
| `clone_home` | Copy a game home | `source_home`, `dest_home`, `owner` | `1`, `0`, `-1` | Yes | Yes | `user_games/clone_home.php`, provisioning | Long-running filesystem mutation |
|
||||
| `remove_home` | Delete a game home | `game_home_del` | `1`, `0`, `-1` | Yes | Yes | `user_games/del_home.php`, teardown flows | Destructive; must be admin or owner-controlled |
|
||||
| `compress_files` | Build archive from remote files | `files`, `destination`, `archive_name`, `archive_type` | success / error | Yes | Yes | backup/content/fastdl flows | Archive path must stay inside allowed roots |
|
||||
| `uncompress_file` | Extract archive | archive inputs | success / error | Yes | Yes | installers, content tools | Extraction path must be constrained |
|
||||
|
||||
## Download, Install, And Update Commands
|
||||
|
||||
| Agent Command | Purpose | Key Arguments | Returns | Windows | Linux | Panel Callers | Security Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `steam_cmd` | Run SteamCMD-based install/update | steam-specific arguments | status code | Yes | Yes | `gamemanager/update_server.php`, API update | Can change server binaries and Steam content |
|
||||
| `fetch_steam_version` | Fetch Steam app version metadata | app arguments | version string / error | Yes | Yes | update pages | Read-only Steam helper |
|
||||
| `installed_steam_version` | Read installed Steam app version | path/app inputs | version string / error | Yes | Yes | update pages | Read-only |
|
||||
| `automatic_steam_update` | Auto-update via SteamCMD | update args | status code | Yes | Yes | scheduler/API update path | Mutation of game install |
|
||||
| `start_file_download` | Begin remote file download | URL and target args | pid / handle / status | Yes | Yes | installers, update/content flows | External network fetch; validate destination |
|
||||
| `is_file_download_in_progress` | Poll active download | download handle | status | Yes | Yes | update/content flows | Read-only |
|
||||
| `start_rsync_install` | Begin rsync-based install/copy | rsync args | status | Yes | Yes | migration/provisioning | Long-running copy action |
|
||||
| `rsync_progress` | Poll rsync copy | rsync handle | progress | Yes | Yes | migration/provisioning | Read-only |
|
||||
| `master_server_update` | Legacy master server update helper | update args | status | Yes | Yes | legacy update flows | Administrative |
|
||||
| `stop_update` | Abort active update | `home_id` | `1` / error | Yes | Yes | update UI, `lib_remote.php::stop_update` | Mutates running install/update |
|
||||
| `component_update` | Update agent code from Git staging | encrypted JSON payload | structured hash | Yes | Yes | `administration/panel_update.php` | Admin-only; must preserve config and hosted server data |
|
||||
| `agent_restart` | Restart the agent service/process | marker arg | `1` / error | Yes | Yes | `server/restart.php` | Admin-only node control |
|
||||
|
||||
## Workshop And Server Content Commands
|
||||
|
||||
| Agent Command | Purpose | Key Arguments | Returns | Windows | Linux | Panel Callers | Security Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `steam_workshop` | Legacy direct Workshop installer | `home_id`, `mods_full_path`, `workshop_id`, `mods_list`, regex/config args, auth args, download args | `1`, `-1`, `-2`, `-3`, `-4` | Yes | Yes | legacy `steam_workshop` module and compatibility helpers | High-risk legacy surface; keep compatibility but prefer Server Content Manager |
|
||||
| `get_workshop_mods_info` | Enumerate installed Workshop mods | none / `mods_info` marker | status + mod list | Yes | Yes | Workshop UIs, compatibility views | Read-only |
|
||||
|
||||
Current preferred implementation path:
|
||||
|
||||
- `addonsmanager` stages a manifest and helper script through `writefile`
|
||||
- it executes the helper through `exec`
|
||||
- it uses `steam_workshop` only as legacy compatibility, not as the primary workflow
|
||||
|
||||
## Shell And System Commands
|
||||
|
||||
| Agent Command | Purpose | Key Arguments | Returns | Windows | Linux | Panel Callers | Security Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `exec` | Execute a constrained command and return output | `command` | base64 text output | Yes | Yes | status fallback, content helpers, provisioning, misc admin tools | Must never receive raw customer shell input |
|
||||
| `sudo_exec` | Execute privileged command | `command` | base64 text output | Yes | Yes | advanced admin tooling | Highest-risk command; admin-only |
|
||||
| `shell_action` | Structured shell action abstraction | `action`, `arguments` | array or text | Yes | Yes | newer advanced operations | Safer than raw shell when used correctly |
|
||||
| `rebootnow` | Reboot remote host | none | status | Yes | Yes | `server/reboot.php` | Node-level destructive action |
|
||||
| `what_os` | Return agent OS identifier | none | OS string | Yes | Yes | provisioning, workshop script selection, path handling | Read-only |
|
||||
| `discover_ips` | Return agent IP list | marker arg | CSV / array | Yes | Yes | server provisioning | Read-only |
|
||||
| `mon_stats` | Return agent monitoring stats | marker arg | base64 stats text | Yes | Yes | `server/mon_stats.php`, `status` | Read-only system metrics |
|
||||
|
||||
## FTP And Fast Download Commands
|
||||
|
||||
| Agent Command | Purpose | Key Arguments | Returns | Windows | Linux | Panel Callers | Security Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `ftp_mgr` | Manage FTP account state | `action`, `login`, `password`, `home_path` | text/status | Yes | Yes | `ftp/ftp_admin.php`, provisioning | Touches service credentials and user state |
|
||||
| `stop_fastdl` | Stop FastDL service | none | status | Yes | Yes | `fast_download` admin UI | Node service control |
|
||||
| `restart_fastdl` | Restart FastDL service | none | status | Yes | Yes | `fast_download` admin UI | Node service control |
|
||||
| `fastdl_status` | Check FastDL status | none | status | Yes | Yes | `fast_download` | Read-only |
|
||||
| `fastdl_get_aliases` | List FastDL aliases | none | alias list | Yes | Yes | `fast_download` | Read-only |
|
||||
| `fastdl_add_alias` | Add FastDL alias | alias args | status | Yes | Yes | `fast_download` | Writes service config |
|
||||
| `fastdl_del_alias` | Delete FastDL alias | alias list | status | Yes | Yes | `fast_download` | Writes service config |
|
||||
| `fastdl_get_info` | Return FastDL config info | none | info payload | Yes | Yes | `fast_download` | Read-only |
|
||||
| `fastdl_create_config` | Build FastDL config | bind/config args | status | Yes | Yes | `fast_download` | Writes service config |
|
||||
|
||||
## Scheduler Commands
|
||||
|
||||
| Agent Command | Purpose | Key Arguments | Returns | Windows | Linux | Panel Callers | Security Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `scheduler_add_task` | Add cron entry | `job` | status | Yes | Yes | `cron/cron.php`, `cron/user_cron.php` | Agent executes stored commands locally |
|
||||
| `scheduler_edit_task` | Modify cron entry | `job_id`, `job` | status | Yes | Yes | `cron/cron.php`, token refresh helpers | Same risks as add |
|
||||
| `scheduler_del_task` | Remove cron entry | `job_id` or comma list | status | Yes | Yes | `cron/cron.php`, cleanup helpers | Destructive |
|
||||
| `scheduler_list_tasks` | Return cron table | none | map of job IDs to cron lines | Yes | Yes | `cron/shared_cron_functions.php`, `cron/events.php` | Read-only |
|
||||
|
||||
Important implementation note:
|
||||
|
||||
- the Panel scheduler stores intent in the UI
|
||||
- the agent owns execution timing and the actual cron entries
|
||||
- many agent cron commands are `wget` calls back into `Panel/ogp_api.php`
|
||||
|
||||
## Internal Agent Scheduler Actions
|
||||
|
||||
These are not XML-RPC command names, but they are part of the runtime architecture inside both agents:
|
||||
|
||||
- `scheduler_dispatcher`
|
||||
- `scheduler_server_action`
|
||||
- `scheduler_log_events`
|
||||
- `scheduler_read_tasks`
|
||||
- `scheduler_stop`
|
||||
|
||||
The built-in action names handled by the Panel-generated API URLs are:
|
||||
|
||||
- `start`
|
||||
- `stop`
|
||||
- `restart`
|
||||
- `steam_auto_update`
|
||||
- `server_content_check_updates`
|
||||
- `server_content_check_workshop_updates`
|
||||
- `server_content_install_updates_if_stopped`
|
||||
- `server_content_install_updates_next_restart`
|
||||
- `server_content_install_updates_now`
|
||||
- `server_content_install_updates_and_restart`
|
||||
- `server_content_notify_updates_only`
|
||||
- `server_content_update_all`
|
||||
- `server_content_validate_files`
|
||||
- `server_content_backup_before_update`
|
||||
|
||||
## Panel Wrapper Map
|
||||
|
||||
Primary wrapper file:
|
||||
|
||||
- `Panel/includes/lib_remote.php`
|
||||
|
||||
Notable wrapper methods:
|
||||
|
||||
| Wrapper Method | XML-RPC Command |
|
||||
|---|---|
|
||||
| `status_chk()` | `quick_chk` |
|
||||
| `remote_server_status()` | `server_status` |
|
||||
| `universal_start()` | `universal_start` |
|
||||
| `remote_stop_server()` | `stop_server` |
|
||||
| `remote_restart_server()` | `restart_server` |
|
||||
| `remote_send_rcon_command()` | `send_rcon_command` |
|
||||
| `remote_readfile()` | `readfile` |
|
||||
| `remote_writefile()` | `writefile` |
|
||||
| `remote_query()` | `remote_query` |
|
||||
| `scheduler_add_task()` | `scheduler_add_task` |
|
||||
| `scheduler_edit_task()` | `scheduler_edit_task` |
|
||||
| `scheduler_del_task()` | `scheduler_del_task` |
|
||||
| `scheduler_list_tasks()` | `scheduler_list_tasks` |
|
||||
| `component_update()` | `component_update` |
|
||||
| `steam_workshop()` | `steam_workshop` |
|
||||
|
||||
## Search Coverage Used For This Document
|
||||
|
||||
Commands were confirmed from:
|
||||
|
||||
- `rg -n "methods =>" Agent_Linux Agent-Windows`
|
||||
- `rg -n "^sub .*without_decrypt|^sub component_update|^sub remote_query|^sub steam_workshop|^sub scheduler_" Agent_Linux/ogp_agent.pl Agent-Windows/ogp_agent.pl`
|
||||
- `rg -n "public function .*\\(" Panel/includes/lib_remote.php`
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Status
|
||||
|
||||
Accepted, Phase 1 implementation started
|
||||
Accepted, Phase 1 implementation active
|
||||
|
||||
## Decision
|
||||
|
||||
|
|
@ -36,3 +36,17 @@ Phase 1 implements this decision by routing the user-facing Workshop install flo
|
|||
- 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.
|
||||
- Game XML uses the canonical `workshop_support` block. The schema validates this block and no longer requires loose top-level Workshop tags.
|
||||
- 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`.
|
||||
|
||||
## Validation
|
||||
|
||||
Current validation commands:
|
||||
|
||||
```bash
|
||||
php Panel/modules/addonsmanager/tests/workshop_helpers_test.php
|
||||
php Panel/modules/config_games/tests/validate_server_configs.php
|
||||
bash -n Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh
|
||||
bash -n Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh
|
||||
```
|
||||
|
|
|
|||
|
|
@ -18,14 +18,19 @@ This file is the first stop for future Codex sessions working in this repository
|
|||
1. `docs/architecture/REPOSITORY_OVERVIEW.md`
|
||||
2. `docs/architecture/PANEL_AGENT_FLOW.md`
|
||||
3. `docs/architecture/API_REFERENCE.md`
|
||||
4. `docs/modules/MODULE_INDEX.md`
|
||||
5. `docs/modules/GAMEMANAGER.md`
|
||||
6. `docs/features/STATUS_SYSTEM.md`
|
||||
7. `docs/features/XML_SYSTEM.md`
|
||||
8. `docs/modules/SCHEDULER.md`
|
||||
9. `docs/modules/SERVER_CONTENT_MANAGER.md`
|
||||
10. `docs/decisions/`
|
||||
11. `docs/games/`
|
||||
4. `docs/architecture/PANEL_AGENT_COMMANDS.md`
|
||||
5. `docs/architecture/MODULE_DEPENDENCIES.md`
|
||||
6. `docs/architecture/LIBRARY_REFERENCE.md`
|
||||
7. `docs/modules/MODULE_INDEX.md`
|
||||
8. `docs/modules/GAMEMANAGER.md`
|
||||
9. `docs/features/STATUS_SYSTEM.md`
|
||||
10. `docs/features/XML_SYSTEM.md`
|
||||
11. `docs/modules/SCHEDULER.md`
|
||||
12. `docs/features/SCHEDULER_ACTIONS.md`
|
||||
13. `docs/modules/SERVER_CONTENT_MANAGER.md`
|
||||
14. `docs/features/USER_API.md`
|
||||
15. `docs/decisions/`
|
||||
16. `docs/games/`
|
||||
|
||||
## Important Files By Topic
|
||||
|
||||
|
|
|
|||
103
docs/features/SCHEDULER_ACTIONS.md
Normal file
103
docs/features/SCHEDULER_ACTIONS.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Scheduler Actions
|
||||
|
||||
## Scope
|
||||
|
||||
This file is the command reference for the current scheduler system.
|
||||
|
||||
Primary files:
|
||||
|
||||
- `Panel/modules/cron/cron.php`
|
||||
- `Panel/modules/cron/user_cron.php`
|
||||
- `Panel/modules/cron/shared_cron_functions.php`
|
||||
- `Agent_Linux/ogp_agent.pl`
|
||||
- `Agent-Windows/ogp_agent.pl`
|
||||
|
||||
## Current Model
|
||||
|
||||
The Panel scheduler builds a cron expression and a command string.
|
||||
|
||||
In the common case, that command string is a `wget` call back into:
|
||||
|
||||
- `Panel/ogp_api.php`
|
||||
|
||||
The agent stores the cron entry and executes it locally.
|
||||
|
||||
## Action Catalog
|
||||
|
||||
| Action Key | Built By | Effective Runtime Target | Modules Affected | Agent Calls Eventually Performed |
|
||||
|---|---|---|---|---|
|
||||
| `start` | `cron/shared_cron_functions.php` | `ogp_api.php?gamemanager/start` | `cron`, `gamemanager` | `universal_start` |
|
||||
| `stop` | same | `ogp_api.php?gamemanager/stop` | `cron`, `gamemanager` | `stop_server` |
|
||||
| `restart` | same | `ogp_api.php?gamemanager/restart` | `cron`, `gamemanager` | `restart_server` |
|
||||
| `steam_auto_update` | same | `ogp_api.php?gamemanager/update&type=steam` | `cron`, `gamemanager` | `steam_cmd` / auto-update path |
|
||||
| `server_content_check_updates` | same | `ogp_api.php?server_content/run_scheduled_action` | `cron`, `addonsmanager` | server-content manifest flow, remote `exec` / helper scripts |
|
||||
| `server_content_check_workshop_updates` | same | same | `cron`, `addonsmanager` | Workshop/content check flow |
|
||||
| `server_content_install_updates_if_stopped` | same | same | `cron`, `addonsmanager`, `gamemanager` | conditional install |
|
||||
| `server_content_install_updates_next_restart` | same | same | `cron`, `addonsmanager`, `gamemanager` | deferred install marker |
|
||||
| `server_content_install_updates_now` | same | same | `cron`, `addonsmanager` | immediate content install |
|
||||
| `server_content_install_updates_and_restart` | same | same | `cron`, `addonsmanager`, `gamemanager` | content update plus restart |
|
||||
| `server_content_notify_updates_only` | same | same | `cron`, `addonsmanager` | check-and-notify |
|
||||
| `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 |
|
||||
|
||||
## Agent Scheduler RPCs
|
||||
|
||||
| RPC | Purpose |
|
||||
|---|---|
|
||||
| `scheduler_add_task` | add cron line |
|
||||
| `scheduler_edit_task` | update cron line |
|
||||
| `scheduler_del_task` | delete cron line |
|
||||
| `scheduler_list_tasks` | list cron lines |
|
||||
|
||||
## Internal Agent Scheduler Subroutines
|
||||
|
||||
| Subroutine | Purpose |
|
||||
|---|---|
|
||||
| `scheduler_dispatcher` | top-level cron callback |
|
||||
| `scheduler_server_action` | execute parsed action |
|
||||
| `scheduler_log_events` | append `scheduler.log` |
|
||||
| `scheduler_read_tasks` | reload current cron entries |
|
||||
| `scheduler_stop` | stop and rebuild cron object |
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
```text
|
||||
User/admin saves scheduler job in Panel
|
||||
-> Panel builds cron line
|
||||
-> Panel sends cron line to agent with scheduler_add_task/edit_task
|
||||
-> Agent stores job
|
||||
-> Agent executes cron job later
|
||||
-> cron job usually calls ogp_api.php
|
||||
-> ogp_api.php dispatches to gamemanager or server_content action
|
||||
-> those paths may call the agent again for actual server/content work
|
||||
```
|
||||
|
||||
This means the current scheduler is two-hop:
|
||||
|
||||
1. agent cron executes a Panel API URL
|
||||
2. the Panel API route often calls back to the same or another agent
|
||||
|
||||
## Logging
|
||||
|
||||
Current observable logs:
|
||||
|
||||
- agent-side `scheduler.log`
|
||||
- panel-side UI through `Panel/modules/cron/events.php`
|
||||
- module-specific logs from `gamemanager` or `addonsmanager`
|
||||
|
||||
## Limitations
|
||||
|
||||
| Limitation | Effect |
|
||||
|---|---|
|
||||
| string-based cron commands | weaker typing and validation |
|
||||
| action permissions are implicit | customer-safe vs admin-only is not strongly modeled |
|
||||
| result storage is agent-log-centric | poor user-facing job history |
|
||||
| jobs depend on Panel URL/token validity | token rotations require cron rewrite |
|
||||
| many actions are API callbacks, not local structured tasks | more moving parts and harder debugging |
|
||||
|
||||
## Search Coverage Used For This Document
|
||||
|
||||
- `sed -n '1,260p' Panel/modules/cron/shared_cron_functions.php`
|
||||
- `rg -n "scheduler_" Agent_Linux/ogp_agent.pl Agent-Windows/ogp_agent.pl`
|
||||
- `rg -n "gamemanager/(start|stop|restart)|server_content/run_scheduled_action" Panel/modules/cron`
|
||||
171
docs/features/USER_API.md
Normal file
171
docs/features/USER_API.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# User API
|
||||
|
||||
## Scope
|
||||
|
||||
This file documents the externally reachable API and webhook surfaces discovered in the repository.
|
||||
|
||||
Primary files:
|
||||
|
||||
- `Panel/ogp_api.php`
|
||||
- `Panel/status_api.php`
|
||||
- `Website/api/create_order.php`
|
||||
- `Website/api/capture_order.php`
|
||||
- `Website/api/log_error.php`
|
||||
- `Website/webhook.php`
|
||||
- `Website/paypal/webhook.php`
|
||||
|
||||
## Panel Automation API
|
||||
|
||||
Main endpoint:
|
||||
|
||||
- `Panel/ogp_api.php`
|
||||
|
||||
Transport:
|
||||
|
||||
- GET, POST, or JSON request body
|
||||
- response is usually JSON
|
||||
- `setting/get` returns plain text
|
||||
|
||||
Authentication:
|
||||
|
||||
- token-based
|
||||
- token created through `token/create`
|
||||
- host allowlist can be enforced with `api_authorized.hosts` and `api_authorized.fwd_hosts`
|
||||
|
||||
Important notes:
|
||||
|
||||
- the API is not a public anonymous API
|
||||
- some routes are meaningful for normal users
|
||||
- many routes are effectively admin-only because they mutate remote servers, create homes, or manage users
|
||||
|
||||
### Authentication Endpoints
|
||||
|
||||
| Endpoint | Auth | Purpose | Parameters | Returns |
|
||||
|---|---|---|---|---|
|
||||
| `ogp_api.php?token/create` | panel username/password | issue API token | `user`, `password` | JSON token payload |
|
||||
| `ogp_api.php?token/test` | token | verify token | `token` | role/status |
|
||||
|
||||
### User-Visible Game Server Actions
|
||||
|
||||
| Endpoint | Auth | Purpose | Parameters | Returns |
|
||||
|---|---|---|---|---|
|
||||
| `gamemanager/start` | token + home access | start server | `ip`, `port`, optional `mod_key` | JSON status |
|
||||
| `gamemanager/stop` | token + home access | stop server | `ip`, `port`, optional `mod_key` | JSON status |
|
||||
| `gamemanager/restart` | token + home access | restart server | `ip`, `port`, optional `mod_key` | JSON status |
|
||||
| `gamemanager/rcon` | token + home access | send RCON/console command | `ip`, `port`, optional `mod_key`, `command` | JSON command result |
|
||||
| `gamemanager/update` | token + home access | update server | `ip`, `port`, optional `mod_key`, `type`, optional `manual_url` | JSON status |
|
||||
| `litefm/list` | token + home access | list files | `ip`, `port`, `relative_path` | JSON listing |
|
||||
| `litefm/get` | token + home access | read file | `ip`, `port`, `relative_path` | JSON file content |
|
||||
| `litefm/save` | token + home access | write file | `ip`, `port`, `relative_path`, `contents` | JSON status |
|
||||
| `litefm/remove` | token + home access | delete file | `ip`, `port`, `relative_path` | JSON status |
|
||||
| `addonsmanager/list` | token | list add-on templates | `token` | JSON list |
|
||||
| `addonsmanager/install` | token + home access | install named add-on | `ip`, `port`, `addon_id` | JSON status |
|
||||
| `steam_workshop/install` | token + home access | legacy Workshop install | `ip`, `port`, optional `mod_key`, `mods_list` | JSON status |
|
||||
| `server_content/run_scheduled_action` | token + home access | trigger typed server-content action | `home_id`, `action`, optional `options` | JSON status |
|
||||
|
||||
### Admin-Oriented API Routes
|
||||
|
||||
| Endpoint | Auth | Purpose | Parameters | Returns |
|
||||
|---|---|---|---|---|
|
||||
| `server/list` | admin token | list remote agents | `token` | JSON list |
|
||||
| `server/status` | admin token | status of remote agent | `remote_server_id` | JSON status |
|
||||
| `server/restart` | admin token | restart agent | `remote_server_id` | JSON status |
|
||||
| `server/create` | admin token | create remote agent record | agent connection fields | JSON status |
|
||||
| `server/remove` | admin token | remove remote agent record | `remote_server_id` | JSON status |
|
||||
| `server/add_ip` | admin token | add IP to agent | `remote_server_id`, `ip` | JSON status |
|
||||
| `server/remove_ip` | admin token | remove IP from agent | `remote_server_id`, `ip` | JSON status |
|
||||
| `server/list_ips` | admin token | list assigned IPs | `remote_server_id` | JSON list |
|
||||
| `server/edit_ip` | admin token | edit assigned IP | `remote_server_id`, `old_ip`, `new_ip` | JSON status |
|
||||
| `user_games/list_games` | token | list game configs | `system`, `architecture` | JSON list |
|
||||
| `user_games/list_servers` | token | list homes visible to token | none | JSON list |
|
||||
| `user_games/create` | admin token | create game home | remote server, config, port, passwords, slots, affinity, nice | JSON status |
|
||||
| `user_games/clone` | admin token | clone home | origin + new home fields | JSON status |
|
||||
| `user_games/set_expiration` | admin token | change home expiry | `home_id`, `timestamp` | JSON status |
|
||||
| `user_admin/*` | admin token | user CRUD and assignments | varies | JSON status |
|
||||
| `gamemanager_admin/reorder` | admin token | reorder homes in UI | token | JSON status |
|
||||
| `setting/get` | token | read setting | `setting_name` | plain text or `-1` |
|
||||
|
||||
## Public Status API
|
||||
|
||||
Endpoint:
|
||||
|
||||
- `Panel/status_api.php?token=...`
|
||||
|
||||
Authentication:
|
||||
|
||||
- shared query token stored in `status_api_local.php`
|
||||
|
||||
Purpose:
|
||||
|
||||
- public, read-only node summary
|
||||
- intended for lightweight dashboards or public status pages
|
||||
|
||||
Behavior:
|
||||
|
||||
- caches agent stats locally for 30 seconds
|
||||
- probes agents with TCP reachability
|
||||
- normalizes CPU, memory, and disk stats when available
|
||||
|
||||
Returns:
|
||||
|
||||
- JSON object with `generated_at` and `nodes[]`
|
||||
|
||||
## Scheduler-As-API
|
||||
|
||||
The scheduler does not call agents directly at runtime. It stores cron lines on the agent that usually call back into:
|
||||
|
||||
- `Panel/ogp_api.php?gamemanager/*`
|
||||
- `Panel/ogp_api.php?server_content/run_scheduled_action`
|
||||
|
||||
This makes `ogp_api.php` part of the internal scheduler runtime contract.
|
||||
|
||||
## Website API Endpoints
|
||||
|
||||
### Payment Creation And Capture
|
||||
|
||||
| Endpoint | Auth | Purpose | Parameters | Returns |
|
||||
|---|---|---|---|---|
|
||||
| `Website/api/create_order.php` | storefront session / checkout context | create PayPal order | checkout/cart payload | JSON PayPal order response |
|
||||
| `Website/api/capture_order.php` | storefront session / checkout context | capture approved PayPal order | order/capture payload | JSON capture result |
|
||||
|
||||
These are thin compatibility wrappers that dispatch into the current billing runtime selected by:
|
||||
|
||||
- `Website/_compat_include.php`
|
||||
- `website_billing_runtime_file(...)`
|
||||
|
||||
### Client Error Logging
|
||||
|
||||
| Endpoint | Auth | Purpose | Parameters | Returns |
|
||||
|---|---|---|---|---|
|
||||
| `Website/api/log_error.php` | none | receive cart/client JS error payloads | JSON body | JSON `{status: logged}` or error |
|
||||
|
||||
Security note:
|
||||
|
||||
- this endpoint is intentionally open
|
||||
- it writes to `Website/logs/client_errors.log`
|
||||
- rate limiting is not obvious in the current implementation
|
||||
|
||||
### Webhooks
|
||||
|
||||
| Endpoint | Source | Purpose | Auth Model |
|
||||
|---|---|---|---|
|
||||
| `Website/webhook.php` | PayPal | verify and process payment webhook | PayPal OAuth + webhook signature verification |
|
||||
| `Website/paypal/webhook.php` | PayPal | compatibility entrypoint forwarding to `Website/webhook.php` runtime | same |
|
||||
| `Panel/modules/billing/webhook.php` | payment runtime compatibility | billing-side webhook entrypoint | gateway-specific |
|
||||
|
||||
## Security Controls
|
||||
|
||||
| Control | Where |
|
||||
|---|---|
|
||||
| token auth | `Panel/ogp_api.php` |
|
||||
| host allowlist | `api_authorized.hosts`, `api_authorized.fwd_hosts`, `settings/api_hosts.php` |
|
||||
| role / ownership checks | inside `api_*` handlers in `ogp_api.php` |
|
||||
| webhook signature verification | `Website/webhook.php` |
|
||||
|
||||
## Search Coverage Used For This Document
|
||||
|
||||
- `rg -n "^function api_" Panel/ogp_api.php`
|
||||
- `sed -n '1,240p' Panel/ogp_api.php`
|
||||
- `sed -n '1,240p' Panel/status_api.php`
|
||||
- `find Website/api -maxdepth 1 -type f`
|
||||
- `sed -n '1,220p' Website/webhook.php`
|
||||
|
|
@ -68,9 +68,33 @@ Default install paths:
|
|||
App ID rules:
|
||||
|
||||
- `workshop_app_id` must come from a Server Content template, Steam Workshop profile, or game XML.
|
||||
- Game XML should declare Workshop support in the canonical `workshop_support` block.
|
||||
- Do not silently use the dedicated server Steam app ID as the Workshop app ID unless a legacy profile explicitly does so.
|
||||
- Arma 3 XML declares Workshop app ID `107410`; its dedicated server Steam app ID remains `233780`.
|
||||
|
||||
Canonical XML:
|
||||
|
||||
```xml
|
||||
<workshop_support>
|
||||
<enabled>1</enabled>
|
||||
<provider>steam</provider>
|
||||
<steam_app_id>107410</steam_app_id>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<download_method>steamcmd</download_method>
|
||||
<install_strategy>arma_mod_folder</install_strategy>
|
||||
<install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path>
|
||||
<startup_param_format>-mod={MOD_LIST}</startup_param_format>
|
||||
<mod_separator>;</mod_separator>
|
||||
<mod_prefix>@</mod_prefix>
|
||||
<copy_keys enabled="1">
|
||||
<source_pattern>{MOD_PATH}/keys/*.bikey</source_pattern>
|
||||
<target_path>{SERVER_ROOT}/keys</target_path>
|
||||
</copy_keys>
|
||||
</workshop_support>
|
||||
```
|
||||
|
||||
The Panel helper parser reads `workshop_support` first. Older direct tags are tolerated only as a compatibility fallback in helper code; they are not the canonical XML format.
|
||||
|
||||
## Database State
|
||||
|
||||
`server_content_workshop` tracks:
|
||||
|
|
@ -140,3 +164,31 @@ Use `addonsmanager` as the main future home for:
|
|||
- install history
|
||||
|
||||
Treat `steam_workshop` as a legacy bridge for migration only.
|
||||
|
||||
## Panel-Agent Contract
|
||||
|
||||
Phase 1 does not use the legacy `steam_workshop` XML-RPC method for the primary user workflow. Instead:
|
||||
|
||||
1. Panel parses customer input into numeric Workshop IDs.
|
||||
2. Panel writes `{SERVER_HOME}/gsp_server_content/workshop_manifest.json`.
|
||||
3. Panel stages the OS-appropriate bundled handler under `{SERVER_HOME}/gsp_server_content/scripts/workshop/`.
|
||||
4. Panel invokes the handler through the existing authenticated agent `exec` RPC.
|
||||
5. The handler writes `workshop_install.log` or `workshop_install_windows.log` under `gsp_server_content`.
|
||||
6. Panel updates `server_content_workshop.install_state` from queued/installing to installed/failed/removed.
|
||||
|
||||
Important manifest fields:
|
||||
|
||||
- `home_id`
|
||||
- `home_cfg_id`
|
||||
- `game_path`
|
||||
- `server_path`
|
||||
- `workshop_app_id`
|
||||
- `steam_app_id`
|
||||
- `items`
|
||||
- `item_details`
|
||||
- `install_strategy`
|
||||
- `target_path`
|
||||
- `extra.copy_keys`
|
||||
- `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.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ The schema supports:
|
|||
- query port calculation
|
||||
- control protocol selection
|
||||
- mod definitions
|
||||
- Workshop / Server Content capability declarations
|
||||
- custom fields
|
||||
- server parameter groups
|
||||
- text replacement helpers
|
||||
|
|
@ -89,6 +90,50 @@ XML definitions also feed:
|
|||
- reserved ports
|
||||
- mod or content behavior
|
||||
|
||||
## Workshop / Server Content Capability
|
||||
|
||||
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.
|
||||
|
||||
Example:
|
||||
|
||||
```xml
|
||||
<workshop_support>
|
||||
<enabled>1</enabled>
|
||||
<provider>steam</provider>
|
||||
<steam_app_id>107410</steam_app_id>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<download_method>steamcmd</download_method>
|
||||
<install_strategy>arma_mod_folder</install_strategy>
|
||||
<install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path>
|
||||
<startup_param_format>-mod={MOD_LIST}</startup_param_format>
|
||||
<mod_separator>;</mod_separator>
|
||||
<mod_prefix>@</mod_prefix>
|
||||
<copy_keys enabled="1">
|
||||
<source_pattern>{MOD_PATH}/keys/*.bikey</source_pattern>
|
||||
<target_path>{SERVER_ROOT}/keys</target_path>
|
||||
</copy_keys>
|
||||
</workshop_support>
|
||||
```
|
||||
|
||||
Supported `install_strategy` values:
|
||||
|
||||
- `game_managed_workshop`
|
||||
- `steamcmd_download_only`
|
||||
- `copy_to_game_root`
|
||||
- `copy_to_mod_folder`
|
||||
- `dayz_mod_folder`
|
||||
- `arma_mod_folder`
|
||||
- `config_only`
|
||||
- `custom_scripted_install`
|
||||
|
||||
`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.
|
||||
|
||||
The current XML schema is validated by:
|
||||
|
||||
```bash
|
||||
php Panel/modules/config_games/tests/validate_server_configs.php
|
||||
```
|
||||
|
||||
## Recommended Mental Model
|
||||
|
||||
Think of the XML system as the capability definition layer:
|
||||
|
|
@ -102,4 +147,3 @@ game XML
|
|||
-> docs links
|
||||
-> scheduler and status hints
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,29 @@ Current default install paths:
|
|||
- Generic Steam Workshop content: `{SERVER_ROOT}/workshop/{MOD_FOLDER}`
|
||||
- DayZ / Arma strategy content: `{SERVER_ROOT}/{MOD_FOLDER}` for root `@<workshop_id>` folder compatibility
|
||||
|
||||
Game XML fallback should use the canonical `workshop_support` block:
|
||||
|
||||
```xml
|
||||
<workshop_support>
|
||||
<enabled>1</enabled>
|
||||
<provider>steam</provider>
|
||||
<steam_app_id>107410</steam_app_id>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<download_method>steamcmd</download_method>
|
||||
<install_strategy>arma_mod_folder</install_strategy>
|
||||
<install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path>
|
||||
<startup_param_format>-mod={MOD_LIST}</startup_param_format>
|
||||
<mod_separator>;</mod_separator>
|
||||
<mod_prefix>@</mod_prefix>
|
||||
<copy_keys enabled="1">
|
||||
<source_pattern>{MOD_PATH}/keys/*.bikey</source_pattern>
|
||||
<target_path>{SERVER_ROOT}/keys</target_path>
|
||||
</copy_keys>
|
||||
</workshop_support>
|
||||
```
|
||||
|
||||
The Panel helper parser reads this block first and only tolerates old direct tags as an internal compatibility fallback.
|
||||
|
||||
SteamCMD requirements:
|
||||
|
||||
- Linux agents need SteamCMD available at the configured profile/template path, `STEAMCMD_PATH`, `/home/gameserver/steamcmd/steamcmd.sh`, or in `PATH`.
|
||||
|
|
@ -99,3 +122,14 @@ 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.
|
||||
|
||||
## Validation
|
||||
|
||||
Relevant smoke tests:
|
||||
|
||||
```bash
|
||||
php Panel/modules/addonsmanager/tests/workshop_helpers_test.php
|
||||
php Panel/modules/config_games/tests/validate_server_configs.php
|
||||
```
|
||||
|
||||
`validate_server_configs.php` validates every XML file under `Panel/modules/config_games/server_configs/` against `schema_server_config.xml`.
|
||||
|
|
|
|||
|
|
@ -53,10 +53,33 @@ Game XML definitions, CLI parameter generation, mod definitions, and custom fiel
|
|||
|
||||
## Missing Functionality
|
||||
|
||||
- first-class workshop/content capability declarations
|
||||
- first-class scheduler capability declarations
|
||||
- richer docs metadata
|
||||
|
||||
## Workshop Capability Declarations
|
||||
|
||||
The XML schema now supports a first-class `workshop_support` block for Steam Workshop / Server Content Manager metadata. This is the only canonical XML format for Workshop-enabled game configs.
|
||||
|
||||
Key fields:
|
||||
|
||||
- `enabled`
|
||||
- `provider`
|
||||
- `steam_app_id`
|
||||
- `workshop_app_id`
|
||||
- `download_method`
|
||||
- `install_strategy`
|
||||
- `install_path`
|
||||
- `startup_param_format`
|
||||
- `mod_separator`
|
||||
- `mod_prefix`
|
||||
- `copy_keys`
|
||||
|
||||
Validate changes with:
|
||||
|
||||
```bash
|
||||
php Panel/modules/config_games/tests/validate_server_configs.php
|
||||
```
|
||||
|
||||
## Suggested Future Improvements
|
||||
|
||||
- extend XML capability model
|
||||
|
|
@ -66,4 +89,3 @@ Game XML definitions, CLI parameter generation, mod definitions, and custom fiel
|
|||
## Recommendation
|
||||
|
||||
- Keep / Improve
|
||||
|
||||
|
|
|
|||
1
reference/Module-Steam_Workshop
Submodule
1
reference/Module-Steam_Workshop
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit aae0acb6e11947d02157d0c9d69561843d7d3cba
|
||||
Loading…
Add table
Add a link
Reference in a new issue