From 82cbc206eb88d489ecf965a74c01864086e37835 Mon Sep 17 00:00:00 2001 From: Frank Harris Date: Sat, 6 Jun 2026 17:18:49 -0500 Subject: [PATCH] doc changes and reference folder --- Panel/deploy_gsp.sh | 158 ----- Panel/gsp | 1 - .../addonsmanager/server_content_helpers.php | 107 ++- .../tests/workshop_helpers_test.php | 12 +- .../modules/addonsmanager/workshop_action.php | 10 +- .../config_games/schema_server_config.xml | 72 ++ .../server_configs/arma3_linux64.xml | 17 +- .../server_configs/arma3_win64.xml | 17 +- .../tests/validate_server_configs.php | 60 ++ Panel/modules/steam_workshop/admin.php | 364 ---------- .../steam_workshop/agent_update_workshop.php | 569 --------------- .../steam_workshop/includes/functions.php | 652 ------------------ Panel/modules/steam_workshop/install.sql | 89 --- Panel/modules/steam_workshop/module.php | 248 ------- .../steam_workshop/monitor_buttons.php | 44 -- Panel/modules/steam_workshop/navigation.xml | 6 - Panel/modules/steam_workshop/user.php | 558 --------------- README.md | 5 + docs/agents/LINUX_AGENT.md | 12 + docs/agents/WINDOWS_AGENT.md | 12 + docs/architecture/API_REFERENCE.md | 371 ++++++---- docs/architecture/LIBRARY_REFERENCE.md | 159 +++++ docs/architecture/MODULE_DEPENDENCIES.md | 148 ++++ docs/architecture/PANEL_AGENT_COMMANDS.md | 208 ++++++ docs/decisions/0004-workshop-system.md | 16 +- docs/development/CODEX_GUIDE.md | 21 +- docs/features/SCHEDULER_ACTIONS.md | 103 +++ docs/features/USER_API.md | 171 +++++ docs/features/WORKSHOP_SYSTEM.md | 52 ++ docs/features/XML_SYSTEM.md | 46 +- docs/modules/SERVER_CONTENT_MANAGER.md | 34 + docs/modules/config_games.md | 26 +- reference/Module-Steam_Workshop | 1 + 33 files changed, 1514 insertions(+), 2855 deletions(-) delete mode 100644 Panel/deploy_gsp.sh delete mode 120000 Panel/gsp create mode 100644 Panel/modules/config_games/tests/validate_server_configs.php delete mode 100644 Panel/modules/steam_workshop/admin.php delete mode 100644 Panel/modules/steam_workshop/agent_update_workshop.php delete mode 100644 Panel/modules/steam_workshop/includes/functions.php delete mode 100644 Panel/modules/steam_workshop/install.sql delete mode 100644 Panel/modules/steam_workshop/module.php delete mode 100644 Panel/modules/steam_workshop/monitor_buttons.php delete mode 100644 Panel/modules/steam_workshop/navigation.xml delete mode 100644 Panel/modules/steam_workshop/user.php create mode 100644 docs/architecture/LIBRARY_REFERENCE.md create mode 100644 docs/architecture/MODULE_DEPENDENCIES.md create mode 100644 docs/architecture/PANEL_AGENT_COMMANDS.md create mode 100644 docs/features/SCHEDULER_ACTIONS.md create mode 100644 docs/features/USER_API.md create mode 160000 reference/Module-Steam_Workshop diff --git a/Panel/deploy_gsp.sh b/Panel/deploy_gsp.sh deleted file mode 100644 index fafc0a7e..00000000 --- a/Panel/deploy_gsp.sh +++ /dev/null @@ -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." diff --git a/Panel/gsp b/Panel/gsp deleted file mode 120000 index 85894e94..00000000 --- a/Panel/gsp +++ /dev/null @@ -1 +0,0 @@ -gsp \ No newline at end of file diff --git a/Panel/modules/addonsmanager/server_content_helpers.php b/Panel/modules/addonsmanager/server_content_helpers.php index 3c6f1162..45b2c2dc 100644 --- a/Panel/modules/addonsmanager/server_content_helpers.php +++ b/Panel/modules/addonsmanager/server_content_helpers.php @@ -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); diff --git a/Panel/modules/addonsmanager/tests/workshop_helpers_test.php b/Panel/modules/addonsmanager/tests/workshop_helpers_test.php index 1ee45db1..ef3cce03 100644 --- a/Panel/modules/addonsmanager/tests/workshop_helpers_test.php +++ b/Panel/modules/addonsmanager/tests/workshop_helpers_test.php @@ -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('/agent/custom/workshop.sh'); +$configuredXml = simplexml_load_string('/agent/custom/workshop.sh'); $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('107410'); -scm_workshop_test_assert(scm_extract_workshop_app_id($appXml) === '107410', 'extracts explicit Workshop app ID from game XML'); +$appXml = simplexml_load_string('107410107410arma_mod_folder{SERVER_ROOT}/{MOD_FOLDER}{SERVER_ROOT}/keys'); +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"; diff --git a/Panel/modules/addonsmanager/workshop_action.php b/Panel/modules/addonsmanager/workshop_action.php index 7a922943..ca358c95 100644 --- a/Panel/modules/addonsmanager/workshop_action.php +++ b/Panel/modules/addonsmanager/workshop_action.php @@ -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, diff --git a/Panel/modules/config_games/schema_server_config.xml b/Panel/modules/config_games/schema_server_config.xml index c985709d..765be59d 100644 --- a/Panel/modules/config_games/schema_server_config.xml +++ b/Panel/modules/config_games/schema_server_config.xml @@ -268,6 +268,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -278,6 +349,7 @@ + diff --git a/Panel/modules/config_games/server_configs/arma3_linux64.xml b/Panel/modules/config_games/server_configs/arma3_linux64.xml index c0e85c6e..e9e0f361 100644 --- a/Panel/modules/config_games/server_configs/arma3_linux64.xml +++ b/Panel/modules/config_games/server_configs/arma3_linux64.xml @@ -2,7 +2,22 @@ arma3_linux64 steamcmd Arma 3 - 107410 + + 1 + steam + 107410 + 107410 + steamcmd + arma_mod_folder + {SERVER_ROOT}/{MOD_FOLDER} + -mod={MOD_LIST} + ; + @ + + {MOD_PATH}/keys/*.bikey + {SERVER_ROOT}/keys + + arma3server_x64 %CONFIG% %CFG% %PROFILES% %NAME% %IP% %PORT% %PLAYERS% %MODLIST% %SERVERMODLIST% %AUTOINIT% diff --git a/Panel/modules/config_games/server_configs/arma3_win64.xml b/Panel/modules/config_games/server_configs/arma3_win64.xml index 52fd77aa..dccccb8b 100644 --- a/Panel/modules/config_games/server_configs/arma3_win64.xml +++ b/Panel/modules/config_games/server_configs/arma3_win64.xml @@ -2,7 +2,22 @@ arma3_win64 steamcmd Arma 3 - 107410 + + 1 + steam + 107410 + 107410 + steamcmd + arma_mod_folder + {SERVER_ROOT}/{MOD_FOLDER} + -mod={MOD_LIST} + ; + @ + + {MOD_PATH}/keys/*.bikey + {SERVER_ROOT}/keys + + arma3server.exe -profiles=profile -name=player -config=profile\server.cfg -cfg=profile\basic.cfg %PORT% %PLAYERS% %RANKING% %AUTOINIT% %DEBUG% %MODS% %SERVERMODS% diff --git a/Panel/modules/config_games/tests/validate_server_configs.php b/Panel/modules/config_games/tests/validate_server_configs.php new file mode 100644 index 00000000..4c613105 --- /dev/null +++ b/Panel/modules/config_games/tests/validate_server_configs.php @@ -0,0 +1,60 @@ +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"; diff --git a/Panel/modules/steam_workshop/admin.php b/Panel/modules/steam_workshop/admin.php deleted file mode 100644 index fe19608a..00000000 --- a/Panel/modules/steam_workshop/admin.php +++ /dev/null @@ -1,364 +0,0 @@ -Steam Workshop – Admin'; - 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); - ?> -
-

- 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. -

- -
- -
- - -

No profiles yet. Click Sync Profiles to create them from installed game configs.

- - - - - - - - - - - - - - - - - - - - - - - - -
Config NameGame NameSteam App IDWorkshop App IDEnabledActions
Yes' : 'No' ?>Edit
- -
- -

« Back to profile list

-

Edit Profile:

- -
-
- Placeholder tokens: - {SERVER_ROOT} {HOME_ID} {STEAM_APP_ID} {WORKSHOP_APP_ID} {MOD_FOLDER} -
- -
-

XML-Assisted Defaults

-

Use values detected from this game XML. Existing values are not overwritten unless you explicitly allow it.

-
- - -
- - -
- Detected values: -
    -
  • Steam App ID:
  • -
  • Workshop App ID:
  • -
  • SteamCMD path:
  • -
  • Workshop download dir:
  • -
  • Server root:
  • -
  • Mod install path:
  • -
-
- - - -
-
- -
- -
- - -
-

Global Profile Defaults

-
- - - - - - -
-
- -
-

Path Templates

-

Use placeholders so paths stay portable between server homes.

-
- - - -
-
- -
-

Per-Game Runtime Values

-
- - - - -
-
- -
-

Optional Script Templates

- - -
- -
-

Notes

- -
- -
-

Default Workshop Behavior for New Servers

-

- 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). -

-
- - -
-
- -

- - Cancel -

-
-
- - .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} - '; -} diff --git a/Panel/modules/steam_workshop/agent_update_workshop.php b/Panel/modules/steam_workshop/agent_update_workshop.php deleted file mode 100644 index 84e4edd4..00000000 --- a/Panel/modules/steam_workshop/agent_update_workshop.php +++ /dev/null @@ -1,569 +0,0 @@ -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 //. - $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; -} diff --git a/Panel/modules/steam_workshop/includes/functions.php b/Panel/modules/steam_workshop/includes/functions.php deleted file mode 100644 index d32ade36..00000000 --- a/Panel/modules/steam_workshop/includes/functions.php +++ /dev/null @@ -1,652 +0,0 @@ -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 '
' - . htmlspecialchars($msg, ENT_QUOTES, 'UTF-8') . '
'; -} - -/** - * Render a short inline error banner. - * - * @param string $msg - * @return void - */ -function sw_error($msg) -{ - echo '
' - . htmlspecialchars($msg, ENT_QUOTES, 'UTF-8') . '
'; -} - -/** - * 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); -} diff --git a/Panel/modules/steam_workshop/install.sql b/Panel/modules/steam_workshop/install.sql deleted file mode 100644 index c0ce0bdb..00000000 --- a/Panel/modules/steam_workshop/install.sql +++ /dev/null @@ -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); diff --git a/Panel/modules/steam_workshop/module.php b/Panel/modules/steam_workshop/module.php deleted file mode 100644 index 495a0249..00000000 --- a/Panel/modules/steam_workshop/module.php +++ /dev/null @@ -1,248 +0,0 @@ -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'), -); diff --git a/Panel/modules/steam_workshop/monitor_buttons.php b/Panel/modules/steam_workshop/monitor_buttons.php deleted file mode 100644 index 4220cb83..00000000 --- a/Panel/modules/steam_workshop/monitor_buttons.php +++ /dev/null @@ -1,44 +0,0 @@ - diff --git a/Panel/modules/steam_workshop/navigation.xml b/Panel/modules/steam_workshop/navigation.xml deleted file mode 100644 index 29a5deda..00000000 --- a/Panel/modules/steam_workshop/navigation.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Panel/modules/steam_workshop/user.php b/Panel/modules/steam_workshop/user.php deleted file mode 100644 index d57abb65..00000000 --- a/Panel/modules/steam_workshop/user.php +++ /dev/null @@ -1,558 +0,0 @@ -Steam Workshop – Mod Manager'; - - $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 '

Steam Workshop is not enabled for this game.

'; - echo '

An administrator must enable Workshop support for this game under ' - . 'Steam Workshop › Admin.

'; - 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; - ?> - - -
-

- Server: -   Game: -   Workshop Profile: -

-

- Queue updates from this page. The server agent applies queued updates automatically. -

-
- -
-

Add Workshop Mod

-
- -
- - - -
-

- -

-
-
- -
-

Update Queue & Last Result

-

- Enabled mods: -   Queued: -   Installed: -   Failed: -

-

- Last update time: -

- -

Last error:

- -
- - -
-
- -
-

Workshop Behavior Settings

-
- -
- - - -
-

- -

-
-
- -
-

Installed Mods ()

- -

No mods added yet. Use the form above to add Workshop IDs.

- -
- - - - - - - - - - - - - - - - - - $mod): ?> - 70) - ? (substr($mod['last_error'], 0, 67) . '...') - : $mod['last_error']; - } - ?> - - - - - - - - - - - - - - - -
#Workshop IDMod NameFolder NameTypeEnabledStatusLast UpdateLast ErrorOrderActions
-
- - - -
- - - - -
- - - -
-
- Installed'; - } elseif ($s === 'queued') { - echo 'Queued'; - } elseif ($s === 'failed') { - echo 'Failed'; - } elseif ($s === 'updating') { - echo 'Updating'; - } else { - echo 'Not installed'; - } - ?> - -
- - - -
-
- - - -
-
-
- - - -
-
-
- -
- - 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` diff --git a/docs/architecture/LIBRARY_REFERENCE.md b/docs/architecture/LIBRARY_REFERENCE.md new file mode 100644 index 00000000..6a12fa25 --- /dev/null +++ b/docs/architecture/LIBRARY_REFERENCE.md @@ -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` diff --git a/docs/architecture/MODULE_DEPENDENCIES.md b/docs/architecture/MODULE_DEPENDENCIES.md new file mode 100644 index 00000000..e5201150 --- /dev/null +++ b/docs/architecture/MODULE_DEPENDENCIES.md @@ -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' \\)` diff --git a/docs/architecture/PANEL_AGENT_COMMANDS.md b/docs/architecture/PANEL_AGENT_COMMANDS.md new file mode 100644 index 00000000..48b2d202 --- /dev/null +++ b/docs/architecture/PANEL_AGENT_COMMANDS.md @@ -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` diff --git a/docs/decisions/0004-workshop-system.md b/docs/decisions/0004-workshop-system.md index 7a9f58b4..a7538698 100644 --- a/docs/decisions/0004-workshop-system.md +++ b/docs/decisions/0004-workshop-system.md @@ -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 `@` 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 +``` diff --git a/docs/development/CODEX_GUIDE.md b/docs/development/CODEX_GUIDE.md index 1b13c35f..5fb9ceda 100644 --- a/docs/development/CODEX_GUIDE.md +++ b/docs/development/CODEX_GUIDE.md @@ -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 diff --git a/docs/features/SCHEDULER_ACTIONS.md b/docs/features/SCHEDULER_ACTIONS.md new file mode 100644 index 00000000..dfe64f60 --- /dev/null +++ b/docs/features/SCHEDULER_ACTIONS.md @@ -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` diff --git a/docs/features/USER_API.md b/docs/features/USER_API.md new file mode 100644 index 00000000..edde3c87 --- /dev/null +++ b/docs/features/USER_API.md @@ -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` diff --git a/docs/features/WORKSHOP_SYSTEM.md b/docs/features/WORKSHOP_SYSTEM.md index 47368872..a885bc78 100644 --- a/docs/features/WORKSHOP_SYSTEM.md +++ b/docs/features/WORKSHOP_SYSTEM.md @@ -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 + + 1 + steam + 107410 + 107410 + steamcmd + arma_mod_folder + {SERVER_ROOT}/{MOD_FOLDER} + -mod={MOD_LIST} + ; + @ + + {MOD_PATH}/keys/*.bikey + {SERVER_ROOT}/keys + + +``` + +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. diff --git a/docs/features/XML_SYSTEM.md b/docs/features/XML_SYSTEM.md index af6a82db..5d96e085 100644 --- a/docs/features/XML_SYSTEM.md +++ b/docs/features/XML_SYSTEM.md @@ -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 + + 1 + steam + 107410 + 107410 + steamcmd + arma_mod_folder + {SERVER_ROOT}/{MOD_FOLDER} + -mod={MOD_LIST} + ; + @ + + {MOD_PATH}/keys/*.bikey + {SERVER_ROOT}/keys + + +``` + +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 ``` - diff --git a/docs/modules/SERVER_CONTENT_MANAGER.md b/docs/modules/SERVER_CONTENT_MANAGER.md index 150a7657..7be60fe8 100644 --- a/docs/modules/SERVER_CONTENT_MANAGER.md +++ b/docs/modules/SERVER_CONTENT_MANAGER.md @@ -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 `@` folder compatibility +Game XML fallback should use the canonical `workshop_support` block: + +```xml + + 1 + steam + 107410 + 107410 + steamcmd + arma_mod_folder + {SERVER_ROOT}/{MOD_FOLDER} + -mod={MOD_LIST} + ; + @ + + {MOD_PATH}/keys/*.bikey + {SERVER_ROOT}/keys + + +``` + +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`. diff --git a/docs/modules/config_games.md b/docs/modules/config_games.md index e1eb014b..f3de230c 100644 --- a/docs/modules/config_games.md +++ b/docs/modules/config_games.md @@ -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 - diff --git a/reference/Module-Steam_Workshop b/reference/Module-Steam_Workshop new file mode 160000 index 00000000..aae0acb6 --- /dev/null +++ b/reference/Module-Steam_Workshop @@ -0,0 +1 @@ +Subproject commit aae0acb6e11947d02157d0c9d69561843d7d3cba