Simplify Steam Workshop UX and wire panel docs links

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/1575c81b-f8a7-433a-8f3b-e068c0992c18

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-08 12:55:27 +00:00 committed by GitHub
parent 5fc301e632
commit 01ad93a11a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 566 additions and 528 deletions

View file

@ -1,5 +1,9 @@
# Changelog
## 2026-05-08
- **Steam Workshop reliability + UI simplification:** Removed customer CLI/update scripting instructions from Workshop user flow, reduced per-server behavior options to supported modes only, switched remaining Steam Workshop SQL references to prefix helpers (no `OGP_DB_PREFIX` strings), hardened queued-update agent processing (`queued → updating → installed/failed`) with clearer error persistence, and refreshed monitor/support documentation links to open game-specific docs (fallback to docs index) in a new tab.
- **Billing docs routing refresh:** Updated docs browser links/icons to root-relative storefront paths (`/docs.php`, `/docs/...`) and removed stale hardcoded panel host guidance from getting-started documentation.
## 2026-05-07
- **README + storefront mobile/cart pricing fixes:** Rewrote the root README for GSP positioning, hardened storefront mobile responsiveness (login, order, cart, shared header guardrails), fixed add-to-cart price persistence for low-decimal paid items, aligned cart/free-checkout math to `total_due` values, and refreshed the canonical storefront footer timestamp.
- **Billing/cart/storefront stability pass:** Hardened `add_to_cart.php` to build schema-compatible invoice inserts dynamically (including legacy installs missing `period_start`), fixed free-checkout DB close handling so wrapper objects are never passed to `mysqli_close()`, switched cart/free-total decisions to cent-based math so low nonzero prices (e.g. $0.02) never show as FREE, improved canonical game deduplication + OS variant matching in storefront list/order pages, and aligned Steam Workshop behavior labels with the new restart/update wording.

View file

@ -7,3 +7,4 @@
- Add a side-by-side before/after diff preview panel to the config_games top-level XML section editor before section saves.
- Add an integration smoke test that exercises paid checkout, free checkout, and add-to-cart on installs with/without `period_start` to prevent billing schema drift regressions.
- Add a storefront visual-regression check at 375px and 430px breakpoints covering login, order, and cart pages to prevent mobile overflow regressions.
- Complete a full pass over all `modules/billing/docs/*` game guides to standardize OS/Workshop/RCON capability statements against current XML-backed server support.

View file

@ -19,6 +19,7 @@ $docsDir = __DIR__ . '/docs';
// Get action and doc parameters
$action = $_GET['action'] ?? 'list';
$doc = $_GET['doc'] ?? '';
$docsPagePath = '/docs.php';
/**
* Get all documentation folders with their metadata
@ -63,9 +64,9 @@ function getDocCategories($docsDir) {
// Find icon file
$icon = '';
if (file_exists($folderPath . '/icon.png')) {
$icon = 'docs/' . $folder . '/icon.png';
$icon = '/docs/' . $folder . '/icon.png';
} elseif (file_exists($folderPath . '/icon.jpg')) {
$icon = 'docs/' . $folder . '/icon.jpg';
$icon = '/docs/' . $folder . '/icon.jpg';
}
$categories[] = [
@ -137,7 +138,7 @@ uksort($grouped, function($a, $b) use ($categoryOrder) {
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title><?php echo $action === 'view' ? 'Documentation' : 'Documentation - GameServers.World'; ?></title>
<title><?php echo $action === 'view' ? 'Documentation' : 'Documentation - GSP'; ?></title>
<link rel="stylesheet" href="css/header.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
@ -351,7 +352,7 @@ uksort($grouped, function($a, $b) use ($categoryOrder) {
<div class="container">
<?php if ($action === 'view' && !empty($doc)): ?>
<!-- View specific documentation -->
<a href="docs.php" class="back-button"> Back to Documentation List</a>
<a href="<?php echo htmlspecialchars($docsPagePath, ENT_QUOTES, 'UTF-8'); ?>" class="back-button"> Back to Documentation List</a>
<div class="doc-view-container">
<?php
@ -401,7 +402,7 @@ uksort($grouped, function($a, $b) use ($categoryOrder) {
<div class="docs-grid">
<?php foreach ((array)$docs as $doc): ?>
<a href="docs.php?action=view&doc=<?php echo urlencode($doc['folder']); ?>" class="doc-card">
<a href="<?php echo htmlspecialchars($docsPagePath . '?action=view&doc=' . urlencode($doc['folder']), ENT_QUOTES, 'UTF-8'); ?>" class="doc-card">
<div class="doc-icon-wrapper">
<?php if (!empty($doc['icon'])): ?>
<img src="<?php echo htmlspecialchars($doc['icon']); ?>" alt="" class="doc-icon">
@ -428,4 +429,3 @@ uksort($grouped, function($a, $b) use ($categoryOrder) {
</div>
</body>
</html>

View file

@ -12,7 +12,7 @@
<p>Once your payment is processed, you'll receive:</p>
<ul>
<li>A confirmation email with your server details</li>
<li>Access to the control panel at <a href="http://panel.iaregamer.com" target="_blank">panel.iaregamer.com</a></li>
<li>Access to your control panel login page</li>
<li>FTP credentials for file management</li>
<li>Server IP address and port</li>
</ul>
@ -21,7 +21,7 @@
<h3>Control Panel</h3>
<ol>
<li>Visit <a href="http://panel.iaregamer.com" target="_blank">panel.iaregamer.com</a></li>
<li>Open your panel login URL (provided in your service welcome message)</li>
<li>Log in with your account credentials</li>
<li>Select your server from "My Servers"</li>
<li>Use the control panel to start, stop, restart, and configure your server</li>
@ -80,7 +80,7 @@
<ul>
<li>Check the game-specific documentation for your server type</li>
<li>Review the troubleshooting guides</li>
<li>Contact support through your account dashboard</li>
<li>Open a support request from the panel support page</li>
<li>Check our community forums for tips and solutions</li>
</ul>

View file

@ -1 +1 @@
Last Updated at 12:44pm on 2026-05-07
Last Updated at 12:47pm on 2026-05-08

View file

@ -154,6 +154,18 @@ function get_sync_name($server_xml)
return $sync_name;
}
function gsp_docs_url_for_game_key($game_key)
{
$game_key = trim((string)$game_key);
if ($game_key !== '') {
$docPath = __DIR__ . '/../billing/docs/' . $game_key . '/index.php';
if (is_file($docPath)) {
return '/docs.php?action=view&doc=' . rawurlencode($game_key);
}
}
return '/docs.php';
}
function exec_ogp_module() {
global $db, $settings, $loggedInUserInfo;
echo "<h2 class='gameMonitor " . ($db->isAdmin( $_SESSION['user_id'] ) ? "isAdminUser" : "") . "'>". get_lang("game_monitor") ."</h2>";
@ -260,6 +272,15 @@ $home_info = $db->getGameHomeWithoutMods($home_id);
$show_all = FALSE;
}
$docsTarget = '/docs.php';
if (is_array($home_info) && !empty($home_info['game_key'])) {
$docsTarget = gsp_docs_url_for_game_key($home_info['game_key']);
}
echo "<div style='margin:10px 0 16px 0;padding:10px 12px;border:1px solid #2d2d2d;border-radius:6px;background:#171717;'>"
. "<a href='" . htmlspecialchars($docsTarget, ENT_QUOTES, 'UTF-8') . "' target='_blank' rel='noopener noreferrer' style='color:#8cb9ff;text-decoration:none;font-weight:600;'>Game Documentation</a>"
. "<span style='color:#a9a9a9;margin-left:8px;'>Opens setup and troubleshooting docs in a new tab.</span>"
. "</div>";
require("protocol/lgsl/lgsl_protocol.php");
$info = $db->getUserById($_SESSION['user_id']);

View file

@ -125,15 +125,12 @@ function sw_admin_save_profile($db)
);
// Per-profile default behavior fields
$valid_update_modes = array('manual', 'on_restart', 'before_start', 'scheduled');
$valid_restart_behaviors = array('none', 'if_empty', 'immediate', 'next_restart');
$valid_hot_load = array('disabled', 'attempt');
$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';
$posted_hl = $_POST['default_hot_load'] ?? 'disabled';
$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';
$fields['default_hot_load'] = in_array($posted_hl, $valid_hot_load, true) ? $posted_hl : 'disabled';
$setParts = array();
foreach ($fields as $col => $val) {
@ -307,7 +304,7 @@ function sw_admin_edit_form(array $profile, array $detected = array(), $showDete
<p class="sw-muted">
These defaults are applied when a user enables Workshop on a server that has no saved behavior settings yet.
Users can always override them on their own server pages.
All defaults are intentionally set to the safest option (manual / no auto-restart / hot-load off).
All defaults are intentionally set to the safest option (manual / no automatic restart).
</p>
<div class="sw-grid">
<label>
@ -316,23 +313,13 @@ function sw_admin_edit_form(array $profile, array $detected = array(), $showDete
<option value="manual" <?= (($profile['default_update_mode'] ?? 'manual') === 'manual') ? 'selected' : '' ?>>Manual only (safe default)</option>
<option value="on_restart" <?= (($profile['default_update_mode'] ?? 'manual') === 'on_restart') ? 'selected' : '' ?>>On next restart</option>
<option value="before_start" <?= (($profile['default_update_mode'] ?? 'manual') === 'before_start') ? 'selected' : '' ?>>Before every server start</option>
<option value="scheduled" <?= (($profile['default_update_mode'] ?? 'manual') === 'scheduled') ? 'selected' : '' ?>>Scheduled update check</option>
</select>
</label>
<label>
<span>Default Restart Behavior</span>
<select name="default_restart_behavior">
<option value="none" <?= (($profile['default_restart_behavior'] ?? 'none') === 'none') ? 'selected' : '' ?>>Do not restart automatically (safe default)</option>
<option value="if_empty" <?= (($profile['default_restart_behavior'] ?? 'none') === 'if_empty') ? 'selected' : '' ?>>Restart if empty</option>
<option value="immediate" <?= (($profile['default_restart_behavior'] ?? 'none') === 'immediate') ? 'selected' : '' ?>>Restart after warning</option>
<option value="next_restart" <?= (($profile['default_restart_behavior'] ?? 'none') === 'next_restart') ? 'selected' : '' ?>>Install on next manual restart only</option>
</select>
</label>
<label>
<span>Default Hot-Load</span>
<select name="default_hot_load">
<option value="disabled" <?= (($profile['default_hot_load'] ?? 'disabled') === 'disabled') ? 'selected' : '' ?>>Disabled (safe default)</option>
<option value="attempt" <?= (($profile['default_hot_load'] ?? 'disabled') === 'attempt') ? 'selected' : '' ?>>Attempt hot-load if game supports it</option>
<option value="if_stopped" <?= (($profile['default_restart_behavior'] ?? 'none') === 'if_stopped') ? 'selected' : '' ?>>Restart only if server is stopped</option>
</select>
</label>
</div>

View file

@ -87,12 +87,12 @@ if ($dry_run) {
// ── Collect home IDs to process ───────────────────────────────────────────
if ($do_all) {
// Find all home_ids that have at least one enabled mod with a valid enabled profile.
// Find all home_ids that have at least one enabled queued mod with an enabled profile.
$rows = $db->resultQuery(
"SELECT DISTINCT m.home_id
FROM `OGP_DB_PREFIXsteam_workshop_server_mods` m
JOIN `OGP_DB_PREFIXsteam_workshop_game_profiles` p ON p.id = m.profile_id
WHERE m.enabled = 1 AND p.enabled = 1"
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 {
@ -159,14 +159,11 @@ function sw_agent_process_home($db, $home_id, $dry_run)
);
$server_root = rtrim($server_root, '/');
// Load enabled mods, sorted by sort_order
$mods = sw_get_server_mods($db, $home_id) ?: array();
$mods = array_filter($mods, function ($m) {
return !empty($m['enabled']);
});
// Load queued+enabled mods, sorted by sort_order
$mods = sw_agent_get_queued_mods($db, $home_id);
if (empty($mods)) {
echo " No enabled mods.\n";
echo " No queued Workshop updates for this server.\n";
return true;
}
@ -185,7 +182,7 @@ function sw_agent_process_home($db, $home_id, $dry_run)
// Mark as updating
if (!$dry_run) {
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
"UPDATE " . sw_table('steam_workshop_server_mods') . "
SET `install_status` = 'updating', `last_error` = NULL, `updated_at` = NOW()
WHERE `id` = $mod_id LIMIT 1"
);
@ -224,7 +221,7 @@ function sw_agent_process_home($db, $home_id, $dry_run)
if (!$dry_run) {
$safe_err = $db->realEscapeSingle($err);
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
"UPDATE " . sw_table('steam_workshop_server_mods') . "
SET `install_status` = 'failed', `last_error` = '$safe_err', `updated_at` = NOW()
WHERE `id` = $mod_id LIMIT 1"
);
@ -246,7 +243,7 @@ function sw_agent_process_home($db, $home_id, $dry_run)
if (!$dry_run) {
$safe_err = $db->realEscapeSingle($err);
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
"UPDATE " . sw_table('steam_workshop_server_mods') . "
SET `install_status` = 'failed', `last_error` = '$safe_err', `updated_at` = NOW()
WHERE `id` = $mod_id LIMIT 1"
);
@ -263,7 +260,7 @@ function sw_agent_process_home($db, $home_id, $dry_run)
// 4. Mark as installed
if (!$dry_run) {
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
"UPDATE " . sw_table('steam_workshop_server_mods') . "
SET `install_status` = 'installed',
`last_installed_at` = NOW(),
`last_updated_at` = NOW(),
@ -276,27 +273,22 @@ function sw_agent_process_home($db, $home_id, $dry_run)
echo " [OK] Installed → $install_path\n";
}
// 5. Print launch parameters
$enabled_mods = array_values(array_filter(
sw_get_server_mods($db, $home_id) ?: array(),
function ($m) { return !empty($m['enabled']); }
));
$params = sw_generate_launch_params($enabled_mods, $profile);
echo "\n Generated launch parameters:\n";
if ($params['mod']) {
echo " " . $params['mod'] . "\n";
}
if ($params['servermod']) {
echo " " . $params['servermod'] . "\n";
}
if (!$params['mod'] && !$params['servermod']) {
echo " (none)\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.
*
@ -361,7 +353,7 @@ function sw_agent_steamcmd_download(array $mod, array $profile, array $tpl_vars,
}
// Validate that steamcmd exists
if (!$dry_run && !is_file($steamcmd) && !is_executable($steamcmd)) {
if (!$dry_run && (!is_file($steamcmd) || !is_executable($steamcmd))) {
return array('ok' => false, 'error' => "SteamCMD not found or not executable: $steamcmd");
}

View file

@ -359,17 +359,54 @@ function sw_get_server_settings($db, $home_id)
"SELECT * FROM " . sw_table('steam_workshop_server_settings') . "
WHERE `home_id` = $home_id LIMIT 1"
);
if ($rows && isset($rows[0])) {
return $rows[0];
if ($rows && isset($rows[0]) && is_array($rows[0])) {
$settings = $rows[0];
$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']];
}
// Safe defaults manual only, no automatic restarts, hot-load off
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',
'hot_load' => 'disabled',
'warning_minutes' => 10,
'schedule_interval' => 'daily',
'schedule_interval' => 'disabled',
);
}
@ -378,41 +415,36 @@ function sw_get_server_settings($db, $home_id)
*
* @param OGPDatabase $db
* @param int $home_id
* @param array $data keys: update_mode, restart_behavior, hot_load,
* warning_minutes, schedule_interval
* @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', 'scheduled');
$valid_restart_behaviors = array('none', 'if_empty', 'immediate', 'next_restart');
$valid_hot_load = array('disabled', 'attempt');
$valid_intervals = array('hourly', 'daily', 'weekly');
$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';
$hot_load = in_array($data['hot_load'] ?? '', $valid_hot_load, true) ? $data['hot_load'] : 'disabled';
$warning_minutes = max(1, min(120, (int)($data['warning_minutes'] ?? 10)));
$schedule_interval = in_array($data['schedule_interval'] ?? '', $valid_intervals, true) ? $data['schedule_interval'] : 'daily';
$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_hl = $db->realEscapeSingle($hot_load);
$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', '$safe_hl',
$warning_minutes, '$safe_si', NOW(), NOW())
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` = '$safe_hl',
`warning_minutes` = $warning_minutes,
`hot_load` = 'disabled',
`warning_minutes` = 0,
`schedule_interval` = '$safe_si',
`updated_at` = NOW()"
);

View file

@ -9,29 +9,55 @@
* (at your option) any later version.
*/
// ── Module metadata ──────────────────────────────────────────────────────
$module_title = "Steam Workshop";
$module_version = "3.1";
$db_version = 4;
$module_version = "3.2";
$db_version = 5;
$module_required = FALSE;
$module_menus = array(
array('subpage' => 'admin', 'name' => 'Steam Workshop', 'group' => 'admin'),
);
// ── SQL helpers ──────────────────────────────────────────────────────────
// All OGP_DB_PREFIX tokens are replaced at runtime by $db->query() /
// $db->resultQuery() before the SQL reaches MySQL. Do not replace them
// here with literal strings.
if (!function_exists('sw_module_db_prefix')) {
function sw_module_db_prefix()
{
if (defined('DB_PREFIX') && DB_PREFIX !== '') {
return DB_PREFIX;
}
if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') {
return $GLOBALS['db_prefix'];
}
if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') {
return $GLOBALS['table_prefix'];
}
return 'gsp_';
}
}
$_sw_drop_old = array(
"DROP TABLE IF EXISTS `OGP_DB_PREFIXworkshop_game_profiles`",
"DROP TABLE IF EXISTS `OGP_DB_PREFIXworkshop_cache`",
"DROP TABLE IF EXISTS `OGP_DB_PREFIXserver_workshop_mods`",
"DROP TABLE IF EXISTS `OGP_DB_PREFIXserver_workshop_settings`",
if (!function_exists('sw_module_table')) {
function sw_module_table($table)
{
return '`' . sw_module_db_prefix() . $table . '`';
}
}
if (!function_exists('sw_module_table_name')) {
function sw_module_table_name($table)
{
return sw_module_db_prefix() . $table;
}
}
$install_queries = array();
$legacyDrops = array(
"DROP TABLE IF EXISTS " . sw_module_table('workshop_game_profiles'),
"DROP TABLE IF EXISTS " . sw_module_table('workshop_cache'),
"DROP TABLE IF EXISTS " . sw_module_table('server_workshop_mods'),
"DROP TABLE IF EXISTS " . sw_module_table('server_workshop_settings'),
);
$_sw_create_new = array(
"CREATE TABLE IF NOT EXISTS `OGP_DB_PREFIXsteam_workshop_game_profiles` (
$schemaCreate = array(
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_game_profiles') . " (
`id` INT NOT NULL AUTO_INCREMENT,
`config_name` VARCHAR(100) NOT NULL,
`game_name` VARCHAR(255) NOT NULL DEFAULT '',
@ -51,8 +77,8 @@ $_sw_create_new = array(
`update_script_template` TEXT NULL,
`copy_bikeys_enabled` TINYINT(1) NOT NULL DEFAULT 1,
`notes` TEXT NULL,
`default_update_mode` ENUM('manual','on_restart','before_start','scheduled') NOT NULL DEFAULT 'manual',
`default_restart_behavior` ENUM('none','if_empty','immediate','next_restart') NOT NULL DEFAULT 'none',
`default_update_mode` ENUM('manual','on_restart','before_start') NOT NULL DEFAULT 'manual',
`default_restart_behavior` ENUM('none','if_stopped') NOT NULL DEFAULT 'none',
`default_hot_load` ENUM('disabled','attempt') NOT NULL DEFAULT 'disabled',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
@ -60,7 +86,7 @@ $_sw_create_new = array(
UNIQUE KEY `uniq_config_name` (`config_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS `OGP_DB_PREFIXsteam_workshop_server_mods` (
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_server_mods') . " (
`id` INT NOT NULL AUTO_INCREMENT,
`home_id` INT NOT NULL,
`profile_id` INT NOT NULL,
@ -80,95 +106,133 @@ $_sw_create_new = array(
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS `OGP_DB_PREFIXsteam_workshop_server_settings` (
"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','scheduled')
`update_mode` ENUM('manual','on_restart','before_start')
NOT NULL DEFAULT 'manual',
`restart_behavior` ENUM('none','if_empty','immediate','next_restart')
`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 10,
`schedule_interval` VARCHAR(32) NOT NULL DEFAULT 'daily',
`warning_minutes` INT NOT NULL DEFAULT 0,
`schedule_interval` ENUM('disabled','daily','weekly') NOT NULL DEFAULT 'disabled',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
PRIMARY KEY (`home_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
);
// ── Install queries ──────────────────────────────────────────────────────
//
// $install_queries[0] runs on fresh install (module manager iterates all keys).
// Drops any legacy tables and creates the new schema.
// $install_queries[3] runs when upgrading from db_version 2 → 3.
// Same content; idempotent because of IF [NOT] EXISTS.
// $install_queries[4] runs when upgrading from db_version 3 → 4.
// Adds steam_workshop_server_settings table and default
// behavior columns on steam_workshop_game_profiles.
//
// Note: the module manager loops $install_queries[$i+1] for each step from
// current db_version up to target. Keys 1 and 2 are intentionally absent;
// the manager safely skips undefined keys (PHP returns NULL → empty array).
$install_queries[0] = array_merge($legacyDrops, $schemaCreate);
$install_queries[3] = array_merge($legacyDrops, $schemaCreate);
$install_queries[4] = array();
$install_queries = array();
$install_queries[0] = array_merge($_sw_drop_old, $_sw_create_new);
$install_queries[3] = array_merge($_sw_drop_old, $_sw_create_new);
unset($_sw_drop_old, $_sw_create_new);
// ── db_version 4: per-server behavior settings + per-profile defaults ────
//
// New table: steam_workshop_server_settings
// Stores update/restart/hot-load preferences per server home.
// Defaults are safe (manual-only, no auto-restart, hot-load disabled).
//
// Altered table: steam_workshop_game_profiles
// Adds default_update_mode, default_restart_behavior, default_hot_load so
// admins can configure defaults per game profile.
//
// All callables check INFORMATION_SCHEMA before ALTER so this is re-runnable.
$install_queries[4] = array(
// Create per-server settings table
"CREATE TABLE IF NOT EXISTS `OGP_DB_PREFIXsteam_workshop_server_settings` (
$install_queries[5] = array(
function ($db) {
$table = sw_module_table_name('steam_workshop_server_settings');
$exists = $db->resultQuery(
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" . $db->realEscapeSingle($table) . "'"
);
if (!$exists || (int)($exists[0]['cnt'] ?? 0) === 0) {
return (bool)$db->query(
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_server_settings') . " (
`home_id` INT NOT NULL,
`update_mode` ENUM('manual','on_restart','before_start','scheduled')
NOT NULL DEFAULT 'manual',
`restart_behavior` ENUM('none','if_empty','immediate','next_restart')
NOT NULL DEFAULT 'none',
`hot_load` ENUM('disabled','attempt')
NOT NULL DEFAULT 'disabled',
`warning_minutes` INT NOT NULL DEFAULT 10,
`schedule_interval` VARCHAR(32) NOT NULL DEFAULT 'daily',
`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",
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
}
// Add default_update_mode to game_profiles if missing
function($db) {
$r = $db->resultQuery("SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'OGP_DB_PREFIXsteam_workshop_game_profiles' AND COLUMN_NAME = 'default_update_mode'");
if ($r && isset($r[0]['cnt']) && (int)$r[0]['cnt'] > 0) return true;
return (bool)$db->query("ALTER TABLE `OGP_DB_PREFIXsteam_workshop_game_profiles` ADD `default_update_mode` ENUM('manual','on_restart','before_start','scheduled') NOT NULL DEFAULT 'manual' AFTER `notes`");
$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"
);
$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;
},
// Add default_restart_behavior to game_profiles if missing
function($db) {
$r = $db->resultQuery("SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'OGP_DB_PREFIXsteam_workshop_game_profiles' AND COLUMN_NAME = 'default_restart_behavior'");
if ($r && isset($r[0]['cnt']) && (int)$r[0]['cnt'] > 0) return true;
return (bool)$db->query("ALTER TABLE `OGP_DB_PREFIXsteam_workshop_game_profiles` ADD `default_restart_behavior` ENUM('none','if_empty','immediate','next_restart') NOT NULL DEFAULT 'none' AFTER `default_update_mode`");
},
// Add default_hot_load to game_profiles if missing
function($db) {
$r = $db->resultQuery("SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'OGP_DB_PREFIXsteam_workshop_game_profiles' AND COLUMN_NAME = 'default_hot_load'");
if ($r && isset($r[0]['cnt']) && (int)$r[0]['cnt'] > 0) return true;
return (bool)$db->query("ALTER TABLE `OGP_DB_PREFIXsteam_workshop_game_profiles` ADD `default_hot_load` ENUM('disabled','attempt') NOT NULL DEFAULT 'disabled' AFTER `default_restart_behavior`");
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 ─────────────────────────────────────────────────────
$uninstall_queries = array(
"DROP TABLE IF EXISTS `OGP_DB_PREFIXsteam_workshop_server_settings`",
"DROP TABLE IF EXISTS `OGP_DB_PREFIXsteam_workshop_server_mods`",
"DROP TABLE IF EXISTS `OGP_DB_PREFIXsteam_workshop_game_profiles`",
"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'),
);

View file

@ -15,12 +15,35 @@
$module_buttons = array();
if (!function_exists('sw_monitor_db_prefix')) {
function sw_monitor_db_prefix()
{
if (defined('DB_PREFIX') && DB_PREFIX !== '') {
return DB_PREFIX;
}
if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') {
return $GLOBALS['db_prefix'];
}
if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') {
return $GLOBALS['table_prefix'];
}
return 'gsp_';
}
}
if (!function_exists('sw_monitor_table')) {
function sw_monitor_table($name)
{
return '`' . sw_monitor_db_prefix() . $name . '`';
}
}
// Only show the button when a Workshop profile is enabled for this game config.
$_sw_profile = $db->resultQuery(
"SELECT p.id
FROM OGP_DB_PREFIXsteam_workshop_game_profiles p
JOIN OGP_DB_PREFIXconfig_homes c ON c.game_key = p.config_name
JOIN OGP_DB_PREFIXserver_homes s ON s.home_cfg_id = c.home_cfg_id
"SELECT p.`id`
FROM " . sw_monitor_table('steam_workshop_game_profiles') . " p
JOIN " . sw_monitor_table('config_homes') . " c ON c.`game_key` = p.`config_name`
JOIN " . sw_monitor_table('server_homes') . " s ON s.`home_cfg_id` = c.`home_cfg_id`
WHERE s.home_id = " . (int)$server_home['home_id'] . "
AND p.enabled = 1
LIMIT 1"

View file

@ -42,8 +42,7 @@ function exec_ogp_module()
// Find matching Workshop profile
$profile = sw_get_profile_for_home($db, $home_id);
if (!$profile) {
echo '<p>Steam Workshop is not enabled for this game type (<strong>'
. sw_h($home['game_name']) . '</strong>).</p>';
echo '<p>Steam Workshop is not enabled for this game.</p>';
echo '<p>An administrator must enable Workshop support for this game under '
. '<em>Steam Workshop &rsaquo; Admin</em>.</p>';
return;
@ -293,7 +292,7 @@ function sw_user_queue_update($db, $home_id)
SET `install_status` = 'queued', `updated_at` = NOW()
WHERE `home_id` = $home_id AND `enabled` = 1"
);
sw_success('All enabled mods queued for update. Run the agent to process downloads.');
sw_success('All enabled mods were queued. Updates are processed automatically by the server agent.');
}
function sw_user_save_settings($db, $home_id)
@ -301,9 +300,7 @@ 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',
'hot_load' => $_POST['hot_load'] ?? 'disabled',
'warning_minutes' => $_POST['warning_minutes'] ?? 10,
'schedule_interval' => $_POST['schedule_interval'] ?? 'daily',
'schedule_interval' => $_POST['schedule_interval'] ?? 'disabled',
));
if ($ok) {
@ -321,151 +318,209 @@ 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);
// Generate launch params from enabled mods
$enabled_mods = array_filter($mods, function ($m) {
return !empty($m['enabled']);
});
$params = sw_generate_launch_params(array_values($enabled_mods), $profile);
$queuedCount = 0;
$failedCount = 0;
$installedCount = 0;
$latestUpdateAt = '';
$latestError = '';
foreach ($mods as $mod) {
if (($mod['install_status'] ?? '') === 'queued') {
$queuedCount++;
} elseif (($mod['install_status'] ?? '') === 'failed') {
$failedCount++;
} elseif (($mod['install_status'] ?? '') === 'installed') {
$installedCount++;
}
if (!empty($mod['last_updated_at']) && $mod['last_updated_at'] > $latestUpdateAt) {
$latestUpdateAt = $mod['last_updated_at'];
}
if (($mod['install_status'] ?? '') === 'failed' && !empty($mod['last_error']) && $latestError === '') {
$latestError = $mod['last_error'];
}
}
$base_url = 'home.php?m=steam_workshop&p=user&home_id=' . $home_id;
?>
<style>
.sw-user-panel{background:#161616;border:1px solid #2f2f2f;border-radius:6px;padding:14px;margin:10px 0;color:#ececec}
.sw-user-panel h3{margin:0 0 10px 0;color:#fff}
.sw-user-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
.sw-user-grid label{display:block}
.sw-user-grid span{display:block;font-size:12px;color:#bebebe;margin-bottom:4px}
.sw-user-grid input[type=text],.sw-user-grid select{width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #3a3a3a;color:#f0f0f0;padding:7px;border-radius:4px}
.sw-user-table-wrap{overflow-x:auto}
.sw-user-table{width:100%;border-collapse:collapse;min-width:860px}
.sw-user-table th,.sw-user-table td{border:1px solid #353535;padding:8px;vertical-align:middle}
.sw-user-table th{background:#202020;text-align:left;color:#fff}
.sw-status-ok{color:#7cdc7c;font-weight:700}
.sw-status-queued{color:#ffca63;font-weight:700}
.sw-status-failed{color:#ff8484;font-weight:700}
.sw-status-progress{color:#8cc7ff;font-weight:700}
.sw-muted{color:#b4b4b4}
</style>
<p>
<div class="sw-user-panel">
<p>
<strong>Server:</strong> <?= sw_h($home['home_name']) ?>
&nbsp;&nbsp;
<strong>Game:</strong> <?= sw_h($home['game_name']) ?>
&nbsp;&nbsp;
<strong>Workshop Profile:</strong> <?= sw_h($profile['config_name']) ?>
</p>
&nbsp;&nbsp;<strong>Game:</strong> <?= sw_h($home['game_name']) ?>
&nbsp;&nbsp;<strong>Workshop Profile:</strong> <?= sw_h($profile['config_name']) ?>
</p>
<p class="sw-muted" style="margin-bottom:0;">
Queue updates from this page. The server agent applies queued updates automatically.
</p>
</div>
<!-- Add Mod form -->
<h3>Add Workshop Mod</h3>
<form method="post" action="<?= sw_h($base_url) ?>">
<div class="sw-user-panel">
<h3>Add Workshop Mod</h3>
<form method="post" action="<?= sw_h($base_url) ?>">
<input type="hidden" name="action" value="add_mod">
<table>
<tr>
<td style="padding:4px 8px;"><label for="workshop_id">Workshop ID</label></td>
<td style="padding:4px 8px;">
<input type="text" id="workshop_id" name="workshop_id" value=""
placeholder="e.g. 2863534533" style="width:180px;" required>
</td>
</tr>
<tr>
<td style="padding:4px 8px;"><label for="add_mod_name">Display Name (optional)</label></td>
<td style="padding:4px 8px;">
<input type="text" id="add_mod_name" name="mod_name" value=""
placeholder="e.g. CF" style="width:180px;">
</td>
</tr>
<tr>
<td style="padding:4px 8px;"><label for="add_mod_type">Mod Type</label></td>
<td style="padding:4px 8px;">
<div class="sw-user-grid">
<label>
<span>Workshop ID</span>
<input type="text" id="workshop_id" name="workshop_id" placeholder="e.g. 2863534533" required>
</label>
<label>
<span>Display Name (optional)</span>
<input type="text" id="add_mod_name" name="mod_name" placeholder="e.g. CF">
</label>
<label>
<span>Mod Type</span>
<select id="add_mod_type" name="mod_type">
<option value="client">Client mod (-mod=)</option>
<option value="server">Server-side only (-serverMod=)</option>
<option value="client">Client mod</option>
<option value="server">Server-side only</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td style="padding:4px 8px;">
</label>
</div>
<p style="margin:12px 0 0;">
<button type="submit" class="button">Add Mod</button>
</td>
</tr>
</table>
</form>
</p>
</form>
</div>
<hr>
<div class="sw-user-panel">
<h3>Update Queue &amp; Last Result</h3>
<p>
<strong>Enabled mods:</strong> <?= count(array_filter($mods, function ($m) { return !empty($m['enabled']); })) ?>
&nbsp;&nbsp;<strong>Queued:</strong> <?= $queuedCount ?>
&nbsp;&nbsp;<strong>Installed:</strong> <?= $installedCount ?>
&nbsp;&nbsp;<strong>Failed:</strong> <?= $failedCount ?>
</p>
<p>
<strong>Last update time:</strong> <?= $latestUpdateAt ? sw_h($latestUpdateAt) : 'Never' ?>
</p>
<?php if ($latestError !== ''): ?>
<p><strong>Last error:</strong> <?= sw_h($latestError) ?></p>
<?php endif; ?>
<form method="post" action="<?= sw_h($base_url) ?>">
<input type="hidden" name="action" value="queue_update">
<button type="submit" class="button" onclick="return confirm('Queue all enabled mods for update?');">Queue Update for All Enabled Mods</button>
</form>
</div>
<!-- Mod list -->
<h3>Installed Mods (<?= count($mods) ?>)</h3>
<div class="sw-user-panel">
<h3>Workshop Behavior Settings</h3>
<form method="post" action="<?= sw_h($base_url) ?>">
<input type="hidden" name="action" value="save_settings">
<div class="sw-user-grid">
<label>
<span>Update Mode</span>
<select name="update_mode">
<option value="manual" <?= ($settings['update_mode'] === 'manual') ? 'selected' : '' ?>>Manual only</option>
<option value="on_restart" <?= ($settings['update_mode'] === 'on_restart') ? 'selected' : '' ?>>On next restart</option>
<option value="before_start" <?= ($settings['update_mode'] === 'before_start') ? 'selected' : '' ?>>Before server start</option>
</select>
</label>
<label>
<span>Restart Behavior</span>
<select name="restart_behavior">
<option value="none" <?= ($settings['restart_behavior'] === 'none') ? 'selected' : '' ?>>Never restart automatically</option>
<option value="if_stopped" <?= ($settings['restart_behavior'] === 'if_stopped') ? 'selected' : '' ?>>Restart only if server is stopped</option>
</select>
</label>
<label>
<span>Scheduled Checks</span>
<select name="schedule_interval">
<option value="disabled" <?= ($settings['schedule_interval'] === 'disabled') ? 'selected' : '' ?>>Disabled</option>
<option value="daily" <?= ($settings['schedule_interval'] === 'daily') ? 'selected' : '' ?>>Daily</option>
<option value="weekly" <?= ($settings['schedule_interval'] === 'weekly') ? 'selected' : '' ?>>Weekly</option>
</select>
</label>
</div>
<p style="margin:12px 0 0;">
<button type="submit" class="button">Save Behavior Settings</button>
</p>
</form>
</div>
<?php if (empty($mods)): ?>
<p>No mods added yet. Use the form above to add Workshop IDs.</p>
<?php else: ?>
<form method="post" action="<?= sw_h($base_url) ?>">
<table width="100%" style="border-collapse:collapse;">
<div class="sw-user-panel">
<h3>Installed Mods (<?= count($mods) ?>)</h3>
<?php if (empty($mods)): ?>
<p>No mods added yet. Use the form above to add Workshop IDs.</p>
<?php else: ?>
<div class="sw-user-table-wrap">
<table class="sw-user-table">
<thead>
<tr style="background:#f0f0f0;">
<th style="padding:6px 8px;text-align:center;">#</th>
<th style="padding:6px 8px;text-align:left;">Workshop ID</th>
<th style="padding:6px 8px;text-align:left;">Mod Name</th>
<th style="padding:6px 8px;text-align:left;">Folder Name</th>
<th style="padding:6px 8px;text-align:center;">Type</th>
<th style="padding:6px 8px;text-align:center;">Enabled</th>
<th style="padding:6px 8px;text-align:center;">Status</th>
<th style="padding:6px 8px;text-align:center;">Order</th>
<th style="padding:6px 8px;text-align:center;">Actions</th>
<tr>
<th>#</th>
<th>Workshop ID</th>
<th>Mod Name</th>
<th>Folder Name</th>
<th>Type</th>
<th>Enabled</th>
<th>Status</th>
<th>Last Update</th>
<th>Last Error</th>
<th>Order</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($mods as $idx => $mod): ?>
<tr style="border-bottom:1px solid #ddd;<?= !$mod['enabled'] ? 'opacity:0.55;' : '' ?>">
<td style="padding:6px 8px;text-align:center;"><?= $idx + 1 ?></td>
<td style="padding:6px 8px;font-family:monospace;"><?= sw_h($mod['workshop_id']) ?></td>
<!-- Inline edit: name -->
<td style="padding:6px 8px;">
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<tr style="<?= !$mod['enabled'] ? 'opacity:0.55;' : '' ?>">
<td><?= $idx + 1 ?></td>
<td style="font-family:monospace;"><?= sw_h($mod['workshop_id']) ?></td>
<td>
<form method="post" action="<?= sw_h($base_url) ?>" style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<input type="hidden" name="action" value="save_mod">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
<input type="hidden" name="folder_name" value="<?= sw_h($mod['folder_name']) ?>">
<input type="hidden" name="mod_type" value="<?= sw_h($mod['mod_type']) ?>">
<input type="text" name="mod_name" value="<?= sw_h($mod['mod_name']) ?>"
style="width:120px;" title="Click Save to apply">
<input type="text" name="mod_name" value="<?= sw_h($mod['mod_name']) ?>" style="width:120px;">
</td>
<!-- Inline edit: folder name -->
<td style="padding:6px 8px;">
<input type="text" name="folder_name" value="<?= sw_h($mod['folder_name']) ?>"
style="width:140px;" title="Folder name inside server root">
</td>
<!-- Inline edit: mod type -->
<td style="padding:6px 8px;text-align:center;">
<select name="mod_type" style="width:100px;">
<option value="client" <?= $mod['mod_type'] === 'client' ? 'selected' : '' ?>>-mod=</option>
<option value="server" <?= $mod['mod_type'] === 'server' ? 'selected' : '' ?>>-serverMod=</option>
<td><input type="text" name="folder_name" value="<?= sw_h($mod['folder_name']) ?>" style="width:120px;"></td>
<td>
<select name="mod_type" style="width:120px;">
<option value="client" <?= $mod['mod_type'] === 'client' ? 'selected' : '' ?>>Client</option>
<option value="server" <?= $mod['mod_type'] === 'server' ? 'selected' : '' ?>>Server</option>
</select>
<button type="submit" class="button small" title="Save changes">Save</button>
<button type="submit" class="button small">Save</button>
</form>
</td>
<!-- Toggle enabled -->
<td style="padding:6px 8px;text-align:center;">
<td>
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<input type="hidden" name="action" value="toggle_mod">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
<button type="submit" class="button small"
style="<?= $mod['enabled'] ? 'background:#5cb85c;color:#fff;' : '' ?>"
title="<?= $mod['enabled'] ? 'Click to disable' : 'Click to enable' ?>">
<?= $mod['enabled'] ? 'On' : 'Off' ?>
</button>
<button type="submit" class="button small" style="<?= $mod['enabled'] ? 'background:#5cb85c;color:#fff;' : '' ?>"><?= $mod['enabled'] ? 'On' : 'Off' ?></button>
</form>
</td>
<!-- Install status -->
<td style="padding:6px 8px;text-align:center;font-size:0.85em;">
<td>
<?php
$s = $mod['install_status'];
if ($s === 'installed') {
echo '<span style="color:green;">Installed</span>';
echo '<span class="sw-status-ok">Installed</span>';
} elseif ($s === 'queued') {
echo '<span style="color:orange;">Queued</span>';
echo '<span class="sw-status-queued">Queued</span>';
} elseif ($s === 'failed') {
echo '<span style="color:red;" title="' . sw_h($mod['last_error']) . '">Failed</span>';
echo '<span class="sw-status-failed">Failed</span>';
} elseif ($s === 'updating') {
echo '<span style="color:blue;">Updating</span>';
echo '<span class="sw-status-progress">Updating</span>';
} else {
echo '<span style="color:#999;">Not installed</span>';
echo '<span class="sw-muted">Not installed</span>';
}
?>
</td>
<!-- Order buttons -->
<td style="padding:6px 8px;text-align:center;white-space:nowrap;">
<td><?= !empty($mod['last_updated_at']) ? sw_h($mod['last_updated_at']) : '-' ?></td>
<?php $shortError = !empty($mod['last_error']) ? (strlen($mod['last_error']) > 70 ? (substr($mod['last_error'], 0, 67) . '...') : $mod['last_error']) : ''; ?>
<td title="<?= sw_h($mod['last_error'] ?? '') ?>"><?= $shortError !== '' ? sw_h($shortError) : '-' ?></td>
<td style="white-space:nowrap;">
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<input type="hidden" name="action" value="move_up">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
@ -474,199 +529,23 @@ function sw_user_render($db, $home_id, array $home, array $profile)
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<input type="hidden" name="action" value="move_down">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
<button type="submit" class="button small"
<?= $idx === (count($mods) - 1) ? 'disabled' : '' ?>>&#9660;</button>
<button type="submit" class="button small" <?= $idx === (count($mods) - 1) ? 'disabled' : '' ?>>&#9660;</button>
</form>
</td>
<!-- Delete -->
<td style="padding:6px 8px;text-align:center;">
<td>
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<input type="hidden" name="action" value="delete_mod">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
<button type="submit" class="button small danger"
onclick="return confirm('Remove this mod from the list?');"
style="background:#d9534f;color:#fff;">Remove</button>
<button type="submit" class="button small danger" onclick="return confirm('Remove this mod from the list?');" style="background:#d9534f;color:#fff;">Remove</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</form>
<?php endif; ?>
<hr>
<!-- Launch params display -->
<h3>Generated Launch Parameters</h3>
<p style="color:#666;font-size:0.9em;">
Based on the enabled mods above (sorted by order). Copy these into your server startup command.
</p>
<?php if (empty($enabled_mods)): ?>
<p>No enabled mods launch parameters will be empty.</p>
<?php else: ?>
<?php if ($params['mod']): ?>
<p>
<strong>Client mods (<code>-mod=</code>):</strong><br>
<input type="text" value="<?= sw_h($params['mod']) ?>"
readonly style="width:100%;font-family:monospace;"
onclick="this.select();">
</p>
<?php endif; ?>
<?php if ($params['servermod']): ?>
<p>
<strong>Server-side mods (<code>-serverMod=</code>):</strong><br>
<input type="text" value="<?= sw_h($params['servermod']) ?>"
readonly style="width:100%;font-family:monospace;"
onclick="this.select();">
</p>
<?php endif; ?>
<p>
<strong>Combined:</strong><br>
<input type="text" value="<?= sw_h($params['combined']) ?>"
readonly style="width:100%;font-family:monospace;"
onclick="this.select();">
</p>
<?php endif; ?>
<hr>
<!-- Install/Update -->
<h3>Install / Update Mods</h3>
<p>
Clicking <strong>Queue Update</strong> marks all enabled mods as <em>queued</em>.
Then run the agent <strong>on the game server host</strong> (where SteamCMD and the game files are located)
to download and install the mods. Adjust the path to the panel's
<code>modules/steam_workshop/agent_update_workshop.php</code> for your server:
</p>
<pre style="background:#222;color:#eee;padding:10px 14px;border-radius:4px;overflow-x:auto;"
>php /path/to/panel/modules/steam_workshop/agent_update_workshop.php --home-id=<?= $home_id ?></pre>
<form method="post" action="<?= sw_h($base_url) ?>">
<input type="hidden" name="action" value="queue_update">
<button type="submit" class="button"
onclick="return confirm('Queue all enabled mods for update?');">
Queue Update for All Enabled Mods
</button>
</form>
<hr>
<!-- Workshop Behavior Settings -->
<h3>Workshop Behavior Settings</h3>
<p style="color:#888;font-size:0.9em;">
Configure how Workshop mods are installed and updated for this server.
All options default to the safest setting (manual only, no automatic restarts).
</p>
<form method="post" action="<?= sw_h($base_url) ?>">
<input type="hidden" name="action" value="save_settings">
<table style="border-collapse:collapse;width:100%;max-width:720px;">
<colgroup>
<col style="width:220px;">
<col>
<col style="width:340px;">
</colgroup>
<thead>
<tr style="background:#f0f0f0;">
<th style="padding:6px 8px;text-align:left;">Setting</th>
<th style="padding:6px 8px;text-align:left;">Value</th>
<th style="padding:6px 8px;text-align:left;">Help</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #ddd;">
<td style="padding:8px;font-weight:bold;">Install / Update Mode</td>
<td style="padding:8px;">
<select name="update_mode" style="width:100%;">
<option value="manual" <?= ($settings['update_mode'] === 'manual') ? 'selected' : '' ?>>Manual only</option>
<option value="on_restart" <?= ($settings['update_mode'] === 'on_restart') ? 'selected' : '' ?>>On next restart</option>
<option value="before_start" <?= ($settings['update_mode'] === 'before_start') ? 'selected' : '' ?>>Before every server start</option>
<option value="scheduled" <?= ($settings['update_mode'] === 'scheduled') ? 'selected' : '' ?>>Scheduled update check</option>
</select>
</td>
<td style="padding:8px;font-size:0.85em;color:#555;">
<strong>Manual only</strong> mods are only updated when you click &ldquo;Queue Update&rdquo; above.<br>
<strong>On next restart</strong> queued updates are applied the next time the server restarts.<br>
<strong>Before every start</strong> the update check runs automatically each time the server starts.<br>
<strong>Scheduled</strong> the update check runs on the interval set below (requires cron / agent).
</td>
</tr>
<tr style="border-bottom:1px solid #ddd;">
<td style="padding:8px;font-weight:bold;">Restart Behavior</td>
<td style="padding:8px;">
<select name="restart_behavior" style="width:100%;">
<option value="none" <?= ($settings['restart_behavior'] === 'none') ? 'selected' : '' ?>>Do not restart automatically</option>
<option value="if_empty" <?= ($settings['restart_behavior'] === 'if_empty') ? 'selected' : '' ?>>Restart if empty</option>
<option value="immediate" <?= ($settings['restart_behavior'] === 'immediate') ? 'selected' : '' ?>>Restart after warning</option>
<option value="next_restart" <?= ($settings['restart_behavior'] === 'next_restart') ? 'selected' : '' ?>>Install on next manual restart only</option>
</select>
</td>
<td style="padding:8px;font-size:0.85em;color:#555;">
Controls what happens when new mod updates are found.<br>
<strong>Do not restart</strong> updates are staged but the server keeps running (safe default).<br>
<strong>If empty</strong> the server is restarted only when there are zero players connected.<br>
<strong>Immediate with warning</strong> a countdown warning is broadcast, then the server restarts.<br>
<strong>Next manual restart</strong> updates are installed the next time you manually stop/start the server.
</td>
</tr>
<tr style="border-bottom:1px solid #ddd;">
<td style="padding:8px;font-weight:bold;">Hot-Load</td>
<td style="padding:8px;">
<select name="hot_load" style="width:100%;">
<option value="disabled" <?= ($settings['hot_load'] === 'disabled') ? 'selected' : '' ?>>Disabled</option>
<option value="attempt" <?= ($settings['hot_load'] === 'attempt') ? 'selected' : '' ?>>Attempt hot-load if game supports it</option>
</select>
</td>
<td style="padding:8px;font-size:0.85em;color:#555;">
<strong>Disabled</strong> no hot-loading; mod changes take effect only after a server restart (safe default).<br>
<strong>Attempt</strong> if the game supports live mod reloading (e.g. via RCON), try to hot-load instead of restarting.
</td>
</tr>
<tr style="border-bottom:1px solid #ddd;">
<td style="padding:8px;font-weight:bold;">Warning Countdown</td>
<td style="padding:8px;">
<input type="number" name="warning_minutes" min="1" max="120"
value="<?= (int)$settings['warning_minutes'] ?>"
style="width:80px;"> minutes
</td>
<td style="padding:8px;font-size:0.85em;color:#555;">
Minutes of advance warning broadcast to players before an automatic restart.<br>
Only used when <em>Restart Behavior</em> is set to <strong>Restart immediately after warning</strong>.<br>
Default: 10 minutes.
</td>
</tr>
<tr style="border-bottom:1px solid #ddd;">
<td style="padding:8px;font-weight:bold;">Scheduled Check Interval</td>
<td style="padding:8px;">
<select name="schedule_interval" style="width:100%;">
<option value="hourly" <?= ($settings['schedule_interval'] === 'hourly') ? 'selected' : '' ?>>Hourly</option>
<option value="daily" <?= ($settings['schedule_interval'] === 'daily') ? 'selected' : '' ?>>Daily (default)</option>
<option value="weekly" <?= ($settings['schedule_interval'] === 'weekly') ? 'selected' : '' ?>>Weekly</option>
</select>
</td>
<td style="padding:8px;font-size:0.85em;color:#555;">
How often the scheduled update check runs.<br>
Only used when <em>Install / Update Mode</em> is set to <strong>Scheduled update check</strong>.<br>
Requires the Workshop agent to be running via cron on the game server host.
</td>
</tr>
</tbody>
</table>
<p style="margin-top:12px;">
<button type="submit" class="button">Save Behavior Settings</button>
</p>
</form>
</div>
<?php endif; ?>
</div>
<?php
}

View file

@ -22,6 +22,18 @@
*
*/
function gsp_support_docs_url_for_game_key($game_key)
{
$game_key = trim((string)$game_key);
if ($game_key !== '') {
$docPath = __DIR__ . '/../billing/docs/' . $game_key . '/index.php';
if (is_file($docPath)) {
return '/docs.php?action=view&doc=' . rawurlencode($game_key);
}
}
return '/docs.php';
}
function exec_ogp_module() {
global $db, $settings;
@ -81,6 +93,19 @@ if (!empty($webhook)) {
} // end if submit
echo '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />';
echo "<h2>".get_lang('support')."</h2>";
$defaultDocsUrl = '/docs.php';
if (!empty($server_homes) && is_array($server_homes)) {
foreach ((array)$server_homes as $server_home_row) {
if (!empty($server_home_row['game_key'])) {
$defaultDocsUrl = gsp_support_docs_url_for_game_key($server_home_row['game_key']);
break;
}
}
}
echo "<div style='margin:0 auto 16px auto;max-width:600px;padding:10px 12px;border:1px solid #2d2d2d;border-radius:6px;background:#171717;'>"
. "<a id='support-doc-link' href='" . htmlspecialchars($defaultDocsUrl, ENT_QUOTES) . "' target='_blank' rel='noopener noreferrer' style='color:#8cb9ff;text-decoration:none;font-weight:600;'>Game Documentation</a>"
. "<span style='color:#a9a9a9;margin-left:8px;'>Open setup and troubleshooting docs in a new tab.</span>"
. "</div>";
echo '
<div style="background:#5865F2;border-radius:8px;padding:14px 20px;margin:0 auto 20px auto;max-width:600px;display:flex;align-items:center;gap:16px;box-shadow:0 2px 8px rgba(0,0,0,0.18);">
<i class="fa-brands fa-discord" style="font-size:2.4em;color:#fff;flex-shrink:0;"></i>
@ -97,7 +122,8 @@ if (!empty($webhook)) {
echo get_lang('select_server').":<br /><select name='gameserver' id='gameserver'>";
foreach ((array)$server_homes as $server_home)
{
echo "<option value='".$server_home['home_name']."'>".$server_home['home_name']."</option>";
$docUrl = gsp_support_docs_url_for_game_key($server_home['game_key'] ?? '');
echo "<option value='".htmlspecialchars($server_home['home_name'], ENT_QUOTES)."' data-doc-url='".htmlspecialchars($docUrl, ENT_QUOTES)."'>".htmlspecialchars($server_home['home_name'], ENT_QUOTES)."</option>";
}
echo "</select><br /><br />";
@ -138,6 +164,15 @@ if (!empty($webhook)) {
return false;
}
}
$(document).ready(function(){
function updateSupportDocLink(){
var selected = $('#gameserver option:selected');
var url = selected.data('doc-url') || '/docs.php';
$('#support-doc-link').attr('href', url);
}
$('#gameserver').on('change', updateSupportDocLink);
updateSupportDocLink();
});
</script>
<?php
} // End function