Implement settings fallback, XML section editor, and Steam Workshop admin/user fixes

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/b16096ca-4ef7-4bb0-80e8-658767561478

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-06 22:58:47 +00:00 committed by GitHub
parent 21c163a4b1
commit 52dba9447e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 843 additions and 353 deletions

View file

@ -273,6 +273,22 @@ function config_games_print_editor_css()
.xml-node__example code{color:#a0d0a0;background:rgba(30,150,50,0.1);padding:1px 4px;border-radius:3px}
.xml-jump-link{display:inline-block;margin-bottom:12px;padding:6px 14px;background:#1c6dd0;color:#fff;border-radius:4px;text-decoration:none;font-size:0.9rem}
.xml-jump-link:hover{background:#1f7aec;text-decoration:none}
.xml-section-grid{display:flex;flex-direction:column;gap:14px;margin-bottom:18px}
.xml-section-block{border:1px solid #303030;border-radius:6px;background:#141414;padding:12px}
.xml-section-block__head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px}
.xml-section-block__title{font-size:1.02rem;color:#f0f0f0;font-weight:600}
.xml-section-block__meta{font-size:0.8rem;color:#9f9f9f}
.xml-section-block__desc{font-size:0.86rem;color:#b0b0b0;margin:0 0 10px}
.xml-section-block textarea{width:100%;min-height:170px;background:#0f0f0f;border:1px solid #3c3c3c;border-radius:4px;color:#f7f7f7;padding:8px;font-family:monospace;font-size:0.84rem}
.xml-section-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.xml-btn{border:1px solid #3f3f3f;background:#222;color:#fff;padding:6px 10px;border-radius:4px;cursor:pointer}
.xml-btn:hover{background:#2a2a2a}
.xml-btn--primary{background:#1c6dd0;border-color:#114b99}
.xml-btn--primary:hover{background:#1f7aec}
.xml-btn--danger{background:#6b1f1f;border-color:#8d2d2d}
.xml-btn--danger:hover{background:#8d2d2d}
.xml-add-section{border:1px dashed #3a3a3a;border-radius:6px;padding:10px;margin-bottom:16px}
.xml-add-section select{min-width:260px}
</style>
CSS;
}
@ -418,6 +434,301 @@ function config_games_render_editor(SimpleXMLElement $xml)
return $html;
}
function config_games_get_config_file_path($db, $home_cfg_id)
{
$cfgInfo = $db->getGameCfg((int)$home_cfg_id);
if ($cfgInfo === FALSE) {
return false;
}
return SERVER_CONFIG_LOCATION . $cfgInfo['home_cfg_file'];
}
function config_games_parse_section_payload($sectionName, $sectionXml)
{
$sectionName = trim((string)$sectionName);
if ($sectionName === '' || !preg_match('/^[A-Za-z0-9_\\-]+$/', $sectionName)) {
return array(false, 'Invalid section name.');
}
$sectionXml = trim((string)$sectionXml);
if ($sectionXml === '') {
return array(false, 'Section XML cannot be empty.');
}
$tmpDom = new DOMDocument();
$tmpDom->preserveWhiteSpace = true;
$tmpDom->formatOutput = false;
$wrapped = '<wrapper>' . $sectionXml . '</wrapper>';
$prev = libxml_use_internal_errors(true);
libxml_clear_errors();
$ok = $tmpDom->loadXML($wrapped);
$errors = libxml_get_errors();
libxml_clear_errors();
libxml_use_internal_errors($prev);
if (!$ok) {
$msg = !empty($errors) ? trim($errors[0]->message) . ' (line ' . $errors[0]->line . ')' : 'Section XML is not well-formed.';
return array(false, $msg);
}
$elements = array();
foreach ($tmpDom->documentElement->childNodes as $child) {
if ($child instanceof DOMElement) {
$elements[] = $child;
}
}
if (count($elements) !== 1) {
return array(false, 'Section XML must contain exactly one top-level element.');
}
if ($elements[0]->tagName !== $sectionName) {
return array(false, 'Section XML root tag must be <' . htmlspecialchars($sectionName, ENT_QUOTES, 'UTF-8') . '>.');
}
return array($elements[0], '');
}
function config_games_get_top_level_sections($configFile)
{
$sections = array();
if (!file_exists($configFile)) {
return $sections;
}
$dom = new DOMDocument();
$dom->preserveWhiteSpace = true;
$dom->formatOutput = false;
if (!$dom->load($configFile)) {
return $sections;
}
$schema = config_games_schema_order();
$descriptions = config_games_tag_descriptions();
foreach ($dom->documentElement->childNodes as $child) {
if (!($child instanceof DOMElement)) {
continue;
}
$name = $child->tagName;
$sections[] = array(
'name' => $name,
'required' => ($schema[$name] ?? null) === true,
'optional' => ($schema[$name] ?? null) === false,
'xml' => $dom->saveXML($child),
'description' => $descriptions[$name]['desc'] ?? 'Top-level configuration section.',
);
}
return $sections;
}
function config_games_validate_document_or_errors(DOMDocument $dom)
{
$tmp = tempnam(sys_get_temp_dir(), 'gsp_cfg_section_');
if ($tmp === false) {
return array('Could not create temporary file for validation.');
}
$dom->save($tmp);
$errors = config_games_validate_xml_file($tmp);
@unlink($tmp);
return $errors;
}
function config_games_validate_section_update($db, $home_cfg_id, $sectionName, $sectionXml)
{
$configFile = config_games_get_config_file_path($db, $home_cfg_id);
if ($configFile === false || !file_exists($configFile)) {
return array(false, array('Configuration file not found.'));
}
list($sectionNode, $parseError) = config_games_parse_section_payload($sectionName, $sectionXml);
if ($sectionNode === false) {
return array(false, array($parseError));
}
$dom = new DOMDocument();
$dom->preserveWhiteSpace = true;
$dom->formatOutput = false;
if ($dom->load($configFile) === false) {
return array(false, array('Could not parse configuration XML.'));
}
$root = $dom->documentElement;
$import = $dom->importNode($sectionNode, true);
$replaced = false;
foreach ($root->childNodes as $child) {
if ($child instanceof DOMElement && $child->tagName === $sectionName) {
$root->replaceChild($import, $child);
$replaced = true;
break;
}
}
if (!$replaced) {
$root->appendChild($import);
}
$errors = config_games_validate_document_or_errors($dom);
if (!empty($errors)) {
return array(false, $errors);
}
return array(true, array());
}
function config_games_save_dom_and_refresh_cfg($db, $configFile, DOMDocument $dom)
{
if ($dom->save($configFile) === false) {
return array(false, array('Failed to write configuration file.'));
}
$config = read_server_config($configFile);
if ($config !== FALSE) {
$db->addGameCfg($config);
}
return array(true, array());
}
function config_games_upsert_top_level_section($db, $home_cfg_id, $sectionName, $sectionXml)
{
$configFile = config_games_get_config_file_path($db, $home_cfg_id);
if ($configFile === false || !file_exists($configFile)) {
return array(false, array('Configuration file not found.'));
}
list($sectionNode, $parseError) = config_games_parse_section_payload($sectionName, $sectionXml);
if ($sectionNode === false) {
return array(false, array($parseError));
}
$dom = new DOMDocument();
$dom->preserveWhiteSpace = true;
$dom->formatOutput = true;
if ($dom->load($configFile) === false) {
return array(false, array('Could not parse configuration XML.'));
}
$import = $dom->importNode($sectionNode, true);
$root = $dom->documentElement;
$replaced = false;
foreach ($root->childNodes as $child) {
if ($child instanceof DOMElement && $child->tagName === $sectionName) {
$root->replaceChild($import, $child);
$replaced = true;
break;
}
}
if (!$replaced) {
$schemaKeys = array_keys(config_games_schema_order());
$targetIndex = array_search($sectionName, $schemaKeys, true);
$inserted = false;
if ($targetIndex !== false) {
foreach ($root->childNodes as $child) {
if (!($child instanceof DOMElement)) {
continue;
}
$childIndex = array_search($child->tagName, $schemaKeys, true);
if ($childIndex !== false && $childIndex > $targetIndex) {
$root->insertBefore($import, $child);
$inserted = true;
break;
}
}
}
if (!$inserted) {
$root->appendChild($import);
}
}
$errors = config_games_validate_document_or_errors($dom);
if (!empty($errors)) {
return array(false, $errors);
}
return config_games_save_dom_and_refresh_cfg($db, $configFile, $dom);
}
function config_games_remove_optional_section($db, $home_cfg_id, $sectionName)
{
$schema = config_games_schema_order();
if (($schema[$sectionName] ?? null) === true) {
return array(false, array('Required sections cannot be removed: ' . $sectionName));
}
$configFile = config_games_get_config_file_path($db, $home_cfg_id);
if ($configFile === false || !file_exists($configFile)) {
return array(false, array('Configuration file not found.'));
}
$dom = new DOMDocument();
$dom->preserveWhiteSpace = true;
$dom->formatOutput = true;
if ($dom->load($configFile) === false) {
return array(false, array('Could not parse configuration XML.'));
}
$root = $dom->documentElement;
$removed = false;
foreach ($root->childNodes as $child) {
if ($child instanceof DOMElement && $child->tagName === $sectionName) {
$root->removeChild($child);
$removed = true;
break;
}
}
if (!$removed) {
return array(false, array('Section not found: ' . $sectionName));
}
$errors = config_games_validate_document_or_errors($dom);
if (!empty($errors)) {
return array(false, $errors);
}
return config_games_save_dom_and_refresh_cfg($db, $configFile, $dom);
}
function config_games_render_top_level_editor($home_cfg_id, $configFile)
{
$sections = config_games_get_top_level_sections($configFile);
$schema = config_games_schema_order();
$presentNames = array_map(function ($section) {
return $section['name'];
}, $sections);
$optionalMissing = array();
foreach ($schema as $name => $required) {
if ($required === false && !in_array($name, $presentNames, true)) {
$optionalMissing[] = $name;
}
}
echo "<h3>Section Editor</h3>";
echo "<p class='note'>Edit one top-level section at a time. Validate a block before saving. Required sections cannot be removed. Optional sections can be added or removed safely.</p>";
if (!empty($optionalMissing)) {
echo "<form class='xml-add-section' action='?m=config_games&amp;home_cfg_id=" . (int)$home_cfg_id . "' method='post'>";
echo "<input type='hidden' name='home_cfg_id' value='" . (int)$home_cfg_id . "'>";
echo "<label for='new_optional_section'>Add optional section:</label> ";
echo "<select id='new_optional_section' name='section_name'>";
foreach ($optionalMissing as $missingName) {
echo "<option value='" . htmlspecialchars($missingName, ENT_QUOTES, 'UTF-8') . "'>" . htmlspecialchars($missingName, ENT_QUOTES, 'UTF-8') . "</option>";
}
echo "</select> ";
echo "<button class='xml-btn' type='submit' name='add_optional_section' value='1'>Add Section</button>";
echo "</form>";
}
echo "<div class='xml-section-grid'>";
foreach ($sections as $section) {
$safeName = htmlspecialchars($section['name'], ENT_QUOTES, 'UTF-8');
$safeXml = htmlspecialchars((string)$section['xml'], ENT_QUOTES, 'UTF-8');
$safeDesc = htmlspecialchars((string)$section['description'], ENT_QUOTES, 'UTF-8');
$requiredText = $section['required'] ? 'Required' : 'Optional/Custom';
echo "<form class='xml-section-block' action='?m=config_games&amp;home_cfg_id=" . (int)$home_cfg_id . "' method='post'>";
echo "<input type='hidden' name='home_cfg_id' value='" . (int)$home_cfg_id . "'>";
echo "<input type='hidden' name='section_name' value='{$safeName}'>";
echo "<div class='xml-section-block__head'><div><div class='xml-section-block__title'>{$safeName}</div><div class='xml-section-block__meta'>{$requiredText}</div></div></div>";
echo "<p class='xml-section-block__desc'>{$safeDesc}</p>";
echo "<textarea name='section_xml'>{$safeXml}</textarea>";
echo "<div class='xml-section-actions'>";
echo "<button class='xml-btn' type='submit' name='validate_section' value='1'>Validate Section</button>";
echo "<button class='xml-btn xml-btn--primary' type='submit' name='save_section' value='1'>Save Section</button>";
echo "<button class='xml-btn' type='submit' name='reset_section' value='1'>Reset Section</button>";
if (!$section['required']) {
echo "<button class='xml-btn xml-btn--danger' type='submit' name='remove_section' value='1' onclick=\"return confirm('Remove optional section {$safeName}?');\">Remove Section</button>";
}
echo "</div>";
echo "</form>";
}
echo "</div>";
}
/**
* Save XML from structured form nodes payload.
* Validates against the schema before writing.
@ -613,6 +924,68 @@ function exec_ogp_module() {
print_success(get_lang('configs_updated_ok'));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['home_cfg_id']) &&
(isset($_POST['validate_section']) || isset($_POST['save_section']) || isset($_POST['remove_section']) || isset($_POST['add_optional_section']) || isset($_POST['reset_section']))) {
$edit_id = (int)$_POST['home_cfg_id'];
$sectionName = trim((string)($_POST['section_name'] ?? ''));
$sectionXml = (string)($_POST['section_xml'] ?? '');
if (isset($_POST['reset_section'])) {
print_success('Section reset. No changes were saved.');
} elseif (isset($_POST['validate_section'])) {
list($ok, $errors) = config_games_validate_section_update($db, $edit_id, $sectionName, $sectionXml);
if ($ok) {
print_success('Section XML is valid.');
} else {
echo "<div class='xml-validation-errors'><strong>&#x26A0; Section validation failed:</strong><ul>";
foreach ($errors as $err) {
echo "<li>" . htmlspecialchars($err, ENT_QUOTES, 'UTF-8') . "</li>";
}
echo "</ul></div>";
}
} elseif (isset($_POST['save_section'])) {
list($ok, $errors) = config_games_upsert_top_level_section($db, $edit_id, $sectionName, $sectionXml);
if ($ok) {
print_success(get_lang('configs_updated_ok'));
} else {
echo "<div class='xml-validation-errors'><strong>&#x26A0; Section save failed:</strong><ul>";
foreach ($errors as $err) {
echo "<li>" . htmlspecialchars($err, ENT_QUOTES, 'UTF-8') . "</li>";
}
echo "</ul></div>";
}
} elseif (isset($_POST['remove_section'])) {
list($ok, $errors) = config_games_remove_optional_section($db, $edit_id, $sectionName);
if ($ok) {
print_success('Optional section removed.');
} else {
echo "<div class='xml-validation-errors'><strong>&#x26A0; Could not remove section:</strong><ul>";
foreach ($errors as $err) {
echo "<li>" . htmlspecialchars($err, ENT_QUOTES, 'UTF-8') . "</li>";
}
echo "</ul></div>";
}
} elseif (isset($_POST['add_optional_section'])) {
$schema = config_games_schema_order();
if (($schema[$sectionName] ?? null) !== false) {
print_failure('Only schema-defined optional sections can be added from this menu.');
} else {
$newXml = "<{$sectionName}></{$sectionName}>";
list($ok, $errors) = config_games_upsert_top_level_section($db, $edit_id, $sectionName, $newXml);
if ($ok) {
print_success('Optional section added.');
} else {
echo "<div class='xml-validation-errors'><strong>&#x26A0; Could not add section:</strong><ul>";
foreach ($errors as $err) {
echo "<li>" . htmlspecialchars($err, ENT_QUOTES, 'UTF-8') . "</li>";
}
echo "</ul></div>";
}
}
}
$_GET['home_cfg_id'] = $edit_id;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_xml']) && isset($_POST['home_cfg_id'])) {
$edit_id = (int)$_POST['home_cfg_id'];
@ -755,6 +1128,9 @@ function exec_ogp_module() {
} else {
$raw_xml_content = htmlspecialchars(file_get_contents($config_file), ENT_QUOTES, 'UTF-8');
echo "<div id='xml-editor-section'>";
config_games_render_top_level_editor($home_cfg_id, $config_file);
echo "<details style='margin:18px 0'><summary style='cursor:pointer;color:#9dc7ff'>Open legacy detailed node editor</summary>";
echo "<form action='?m=config_games&amp;home_cfg_id=".$home_cfg_id."' method='post'>";
echo "<input type='hidden' name='home_cfg_id' value='".(int)$home_cfg_id."'>";
echo "<button type='submit' name='save_xml' value='1' class='xml-global-save xml-global-save--top'>".get_lang('save')."</button>";
@ -762,16 +1138,21 @@ function exec_ogp_module() {
echo config_games_render_editor($xml);
echo "<div class='xml-actions'><button type='submit' name='save_xml' value='1' class='xml-global-save'>".get_lang('save')."</button></div>";
echo "<p class='note'>&#x2605; = required field. Use the action dropdown to remove entire sections. Attribute values left blank will be removed. Script sections such as post_install are fully editable. Changes are validated against the schema before saving.</p>";
echo "</form>";
echo "</details>";
// Raw XML editor
echo "<hr style='margin:24px 0;border-color:#333'>";
echo "<h3 style='margin-bottom:8px'>Raw XML Editor</h3>";
echo "<h3 style='margin-bottom:8px'>Full Raw XML Editor</h3>";
echo "<div class='xml-raw-warning'>&#x26A0; <strong>Warning:</strong> Saving raw XML bypasses the guided editor. The file will be validated against the schema before saving. Invalid XML will be rejected.</div>";
echo "<button type='button' class='xml-raw-toggle' onclick=\"var s=document.getElementById('raw_xml_section');s.style.display=s.style.display==='none'?'block':'none'\">Toggle Raw XML Editor</button>";
echo "<div id='raw_xml_section' class='xml-raw-section'>";
echo "<form action='?m=config_games&amp;home_cfg_id=".$home_cfg_id."' method='post'>";
echo "<input type='hidden' name='home_cfg_id' value='".(int)$home_cfg_id."'>";
echo "<textarea name='raw_xml_content'>{$raw_xml_content}</textarea>";
echo "<div class='xml-actions' style='margin-top:8px'><button type='submit' name='save_xml' value='1' class='xml-global-save'>Save Raw XML</button></div>";
echo "</div>";
echo "</form>";
echo "</div>";
echo "</div>"; // #xml-editor-section
}
}

View file

@ -2,18 +2,9 @@
/*
* GSP Steam Workshop: Admin profile management
* Copyright (C) 2025 WDS / GameServerPanel
*
* Accessible via: home.php?m=steam_workshop&p=admin
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*/
require_once __DIR__ . '/includes/functions.php';
// Load the XML config parser so sw_sync_profiles() can read game configs.
if (!defined('SERVER_CONFIG_LOCATION')) {
require_once __DIR__ . '/../../config_games/server_config_parser.php';
}
@ -22,23 +13,65 @@ function exec_ogp_module()
{
global $db;
echo '<h2>Steam Workshop Admin</h2>';
echo '<h2>Steam Workshop &ndash; Admin</h2>';
sw_admin_print_styles();
$action = isset($_GET['action']) ? $_GET['action'] : '';
$action = $_GET['action'] ?? '';
// ── POST: save a profile edit ─────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_profile'])) {
sw_admin_save_profile($db);
return;
}
// ── POST: sync profiles from XML configs ──────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sync_profiles'])) {
$n = sw_sync_profiles($db);
sw_success("Sync complete. $n new profile(s) created.");
}
// ── GET: show edit form for one profile ───────────────────────────
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) {
sw_success("Applied $updated detected default value(s)." . ($overwrite ? ' Existing values were allowed to be overwritten.' : ' Existing non-empty values were kept.'));
} 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) {
@ -50,18 +83,12 @@ function exec_ogp_module()
return;
}
// ── Default: list all profiles ────────────────────────────────────
sw_admin_list($db);
}
// ─────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────
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);
@ -75,38 +102,34 @@ function sw_admin_save_profile($db)
return;
}
// Collect and sanitize fields from POST.
$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'] === '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='),
'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'] ?? ''),
'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'] ?? ''),
);
$set_parts = array();
$setParts = array();
foreach ($fields as $col => $val) {
$safe = $db->realEscapeSingle($val);
$set_parts[] = "`$col` = '$safe'";
$setParts[] = "`$col` = '" . $db->realEscapeSingle($val) . "'";
}
$set_parts[] = "`updated_at` = NOW()";
$set_sql = implode(', ', $set_parts);
$setParts[] = "`updated_at` = NOW()";
$ok = $db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_game_profiles`
SET $set_sql
"UPDATE " . sw_table('steam_workshop_game_profiles') . "
SET " . implode(', ', $setParts) . "
WHERE `id` = $id LIMIT 1"
);
@ -128,297 +151,177 @@ function sw_admin_list($db)
{
$profiles = sw_get_profiles($db);
?>
<p>
Each game config XML gets one Workshop profile.
Use <strong>Sync Profiles</strong> to auto-create rows for new game configs.
Enable and configure each profile to activate Steam Workshop for that game.
</p>
<div class="sw-admin-panel">
<p class="sw-muted">
Profiles map game XML configs to Steam Workshop defaults. Sync to create missing profiles, then edit each profile for game-specific paths and launch templates.
</p>
<form method="post" style="display:inline;">
<button type="submit" name="sync_profiles" value="1"
onclick="return confirm('Sync workshop profiles from all game config XMLs?');"
class="button">Sync Profiles from XML Configs</button>
</form>
<form method="post" style="margin-bottom:12px;">
<button type="submit" name="sync_profiles" value="1" class="button"
onclick="return confirm('Sync workshop profiles from all game config XMLs?');">Sync Profiles from XML Configs</button>
</form>
<hr>
<?php if (empty($profiles)): ?>
<p>No profiles yet. Click <em>Sync Profiles</em> to create them from the installed game configs.</p>
<?php else: ?>
<table class="table" width="100%" style="border-collapse:collapse;">
<thead>
<tr style="background:#f0f0f0;">
<th style="padding:6px 8px;text-align:left;">Config Name</th>
<th style="padding:6px 8px;text-align:left;">Game Name</th>
<th style="padding:6px 8px;text-align:center;">Workshop App ID</th>
<th style="padding:6px 8px;text-align:center;">Enabled</th>
<th style="padding:6px 8px;text-align:center;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($profiles as $p): ?>
<tr style="border-bottom:1px solid #ddd;">
<td style="padding:6px 8px;font-family:monospace;"><?= sw_h($p['config_name']) ?></td>
<td style="padding:6px 8px;"><?= sw_h($p['game_name']) ?></td>
<td style="padding:6px 8px;text-align:center;"><?= sw_h($p['workshop_app_id']) ?></td>
<td style="padding:6px 8px;text-align:center;">
<?= $p['enabled'] ? '<span style="color:green;font-weight:bold;">Yes</span>' : '<span style="color:#999;">No</span>' ?>
</td>
<td style="padding:6px 8px;text-align:center;">
<a href="home.php?m=steam_workshop&p=admin&action=edit&id=<?= (int)$p['id'] ?>"
class="button small">Edit</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif;
<?php if (empty($profiles)): ?>
<p>No profiles yet. Click <em>Sync Profiles</em> to create them from installed game configs.</p>
<?php else: ?>
<table class="sw-admin-table" width="100%">
<thead>
<tr>
<th>Config Name</th>
<th>Game Name</th>
<th style="text-align:center;">Steam App ID</th>
<th style="text-align:center;">Workshop App ID</th>
<th style="text-align:center;">Enabled</th>
<th style="text-align:center;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($profiles as $p): ?>
<tr>
<td><code><?= sw_h($p['config_name']) ?></code></td>
<td><?= sw_h($p['game_name']) ?></td>
<td style="text-align:center;"><?= sw_h($p['steam_app_id']) ?></td>
<td style="text-align:center;"><?= sw_h($p['workshop_app_id']) ?></td>
<td style="text-align:center;"><?= $p['enabled'] ? '<span class="sw-state-on">Yes</span>' : '<span class="sw-state-off">No</span>' ?></td>
<td style="text-align:center;"><a class="button small" href="home.php?m=steam_workshop&p=admin&action=edit&id=<?= (int)$p['id'] ?>">Edit</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}
function sw_admin_edit_form(array $profile)
function sw_admin_edit_form(array $profile, array $detected = array(), $showDetectedBox = false)
{
$id = (int)$profile['id'];
?>
<p><a href="home.php?m=steam_workshop&p=admin">&laquo; Back to profile list</a></p>
<h3>Edit Profile: <?= sw_h($profile['config_name']) ?> &ndash; <?= sw_h($profile['game_name']) ?></h3>
<p style="background:#fff8dc;border:1px solid #e0d090;padding:8px 12px;border-radius:4px;">
<strong>Supported placeholders</strong> (use in path/script templates):<br>
<code>{HOME_ID}</code> &nbsp;
<code>{SERVER_ID}</code> &nbsp;
<code>{REMOTE_SERVER_ID}</code> &nbsp;
<code>{GAME_NAME}</code> &nbsp;
<code>{CONFIG_NAME}</code> &nbsp;
<code>{WORKSHOP_ID}</code> &nbsp;
<code>{MOD_NAME}</code> &nbsp;
<code>{FOLDER_NAME}</code> &nbsp;
<code>{STEAM_APP_ID}</code> &nbsp;
<code>{WORKSHOP_APP_ID}</code> &nbsp;
<code>{STEAMCMD_PATH}</code> &nbsp;
<code>{WORKSHOP_DOWNLOAD_DIR}</code> &nbsp;
<code>{SERVER_ROOT}</code> &nbsp;
<code>{INSTALL_PATH}</code> &nbsp;
<code>{MOD_FOLDER}</code>
</p>
<div class="sw-admin-panel">
<div class="sw-note">
<strong>Placeholder tokens:</strong>
<code>{SERVER_ROOT}</code> <code>{HOME_ID}</code> <code>{STEAM_APP_ID}</code> <code>{WORKSHOP_APP_ID}</code> <code>{MOD_FOLDER}</code>
</div>
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>">
<input type="hidden" name="id" value="<?= $id ?>">
<div class="sw-section">
<h4>XML-Assisted Defaults</h4>
<p class="sw-muted">Use values detected from this game XML. Existing values are not overwritten unless you explicitly allow it.</p>
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>" style="display:inline-block; margin-right:8px;">
<input type="hidden" name="id" value="<?= $id ?>">
<button type="submit" name="detect_defaults" value="1" class="button">Detect from XML</button>
</form>
<table width="100%" style="border-collapse:collapse;">
<?php if ($showDetectedBox && !empty($detected)): ?>
<div class="sw-detected-box">
<strong>Detected values:</strong>
<ul>
<li>Steam App ID: <code><?= sw_h($detected['steam_app_id'] ?? '') ?></code></li>
<li>Workshop App ID: <code><?= sw_h($detected['workshop_app_id'] ?? '') ?></code></li>
<li>SteamCMD path: <code><?= sw_h($detected['steamcmd_path'] ?? '') ?></code></li>
<li>Workshop download dir: <code><?= sw_h($detected['workshop_download_dir_template'] ?? '') ?></code></li>
<li>Server root: <code><?= sw_h($detected['server_root_template'] ?? '') ?></code></li>
<li>Mod install path: <code><?= sw_h($detected['install_path_template'] ?? '') ?></code></li>
</ul>
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>">
<input type="hidden" name="id" value="<?= $id ?>">
<label style="display:block;margin-bottom:8px;">
<input type="checkbox" name="overwrite_existing" value="1">
Allow overwrite of existing non-empty values.
</label>
<button type="submit" name="apply_detected_defaults" value="1" class="button">Refresh defaults from XML</button>
</form>
</div>
<?php endif; ?>
</div>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">General</td>
</tr>
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>">
<input type="hidden" name="id" value="<?= $id ?>">
<tr>
<td style="padding:6px 8px;width:260px;"><label>Enabled</label></td>
<td style="padding:6px 8px;">
<input type="checkbox" name="enabled" value="1"
<?= $profile['enabled'] ? 'checked' : '' ?>>
Enable Steam Workshop for this game config
</td>
</tr>
<div class="sw-section">
<h4>Global Profile Defaults</h4>
<div class="sw-grid">
<label><span>Enabled</span><input type="checkbox" name="enabled" value="1" <?= $profile['enabled'] ? 'checked' : '' ?>></label>
<label><span>Steam App ID</span><input type="text" name="steam_app_id" value="<?= sw_h($profile['steam_app_id']) ?>" placeholder="Detected from XML when available"></label>
<label><span>Workshop App ID</span><input type="text" name="workshop_app_id" value="<?= sw_h($profile['workshop_app_id']) ?>" placeholder="Detected from XML when available"></label>
<label><span>SteamCMD Path</span><input type="text" name="steamcmd_path" value="<?= sw_h($profile['steamcmd_path']) ?>" placeholder="/home/gameserver/steamcmd/steamcmd.sh"></label>
<label><span>Steam Login Required</span><input type="checkbox" name="steam_login_required" value="1" <?= $profile['steam_login_required'] ? 'checked' : '' ?>></label>
<label><span>SteamCMD Login Mode</span>
<select name="steamcmd_login_mode">
<option value="anonymous" <?= $profile['steamcmd_login_mode'] === 'anonymous' ? 'selected' : '' ?>>anonymous</option>
<option value="account" <?= $profile['steamcmd_login_mode'] === 'account' ? 'selected' : '' ?>>account</option>
</select>
</label>
</div>
</div>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Steam / SteamCMD</td>
</tr>
<div class="sw-section">
<h4>Path Templates</h4>
<p class="sw-muted">Use placeholders so paths stay portable between server homes.</p>
<div class="sw-grid">
<label><span>Workshop Download Directory</span><input type="text" name="workshop_download_dir_template" value="<?= sw_h($profile['workshop_download_dir_template']) ?>" placeholder="{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}"></label>
<label><span>Server Root</span><input type="text" name="server_root_template" value="<?= sw_h($profile['server_root_template']) ?>" placeholder="{SERVER_ROOT}"></label>
<label><span>Mod Install Path</span><input type="text" name="install_path_template" value="<?= sw_h($profile['install_path_template']) ?>" placeholder="{SERVER_ROOT}/{MOD_FOLDER}"></label>
</div>
</div>
<tr>
<td style="padding:6px 8px;"><label for="steam_app_id">Steam App ID</label></td>
<td style="padding:6px 8px;">
<input type="text" id="steam_app_id" name="steam_app_id"
value="<?= sw_h($profile['steam_app_id']) ?>"
style="width:200px;">
<span style="color:#666;font-size:0.9em;">(e.g. 223350 for DayZ Dedicated Server)</span>
</td>
</tr>
<div class="sw-section">
<h4>Per-Game Runtime Values</h4>
<div class="sw-grid">
<label><span>Folder Naming Format</span><input type="text" name="folder_naming_format" value="<?= sw_h($profile['folder_naming_format']) ?>" placeholder="@{MOD_NAME}"></label>
<label><span>Client Launch Param</span><input type="text" name="mod_launch_param_template" value="<?= sw_h($profile['mod_launch_param_template']) ?>" placeholder="-mod="></label>
<label><span>Server Launch Param</span><input type="text" name="servermod_launch_param_template" value="<?= sw_h($profile['servermod_launch_param_template']) ?>" placeholder="-serverMod="></label>
<label><span>Copy .bikey files</span><input type="checkbox" name="copy_bikeys_enabled" value="1" <?= $profile['copy_bikeys_enabled'] ? 'checked' : '' ?>></label>
</div>
</div>
<tr>
<td style="padding:6px 8px;"><label for="workshop_app_id">Workshop App ID</label></td>
<td style="padding:6px 8px;">
<input type="text" id="workshop_app_id" name="workshop_app_id"
value="<?= sw_h($profile['workshop_app_id']) ?>"
style="width:200px;">
<span style="color:#666;font-size:0.9em;">(e.g. 221100 for DayZ Workshop content)</span>
</td>
</tr>
<div class="sw-section">
<h4>Optional Script Templates</h4>
<label><span>Install Script Template</span><textarea name="install_script_template" rows="6"><?= sw_h($profile['install_script_template']) ?></textarea></label>
<label><span>Update Script Template</span><textarea name="update_script_template" rows="6"><?= sw_h($profile['update_script_template']) ?></textarea></label>
</div>
<tr>
<td style="padding:6px 8px;"><label for="steamcmd_path">SteamCMD Path</label></td>
<td style="padding:6px 8px;">
<input type="text" id="steamcmd_path" name="steamcmd_path"
value="<?= sw_h($profile['steamcmd_path']) ?>"
style="width:480px;">
</td>
</tr>
<div class="sw-section">
<h4>Notes</h4>
<label><textarea name="notes" rows="4"><?= sw_h($profile['notes']) ?></textarea></label>
</div>
<tr>
<td style="padding:6px 8px;"><label>Steam Login Required</label></td>
<td style="padding:6px 8px;">
<input type="checkbox" name="steam_login_required" value="1"
<?= $profile['steam_login_required'] ? 'checked' : '' ?>>
Requires authenticated Steam login (not anonymous)
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="steamcmd_login_mode">SteamCMD Login Mode</label></td>
<td style="padding:6px 8px;">
<select id="steamcmd_login_mode" name="steamcmd_login_mode">
<option value="anonymous" <?= $profile['steamcmd_login_mode'] === 'anonymous' ? 'selected' : '' ?>>anonymous</option>
<option value="account" <?= $profile['steamcmd_login_mode'] === 'account' ? 'selected' : '' ?>>account (Steam username/password needed)</option>
</select>
</td>
</tr>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Paths</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="workshop_download_dir_template">Workshop Download Dir</label></td>
<td style="padding:6px 8px;">
<input type="text" id="workshop_download_dir_template" name="workshop_download_dir_template"
value="<?= sw_h($profile['workshop_download_dir_template']) ?>"
style="width:480px;">
<br><span style="color:#666;font-size:0.9em;">
Where SteamCMD downloads mods.<br>
Example: <code>{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}</code>
</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="server_root_template">Server Root</label></td>
<td style="padding:6px 8px;">
<input type="text" id="server_root_template" name="server_root_template"
value="<?= sw_h($profile['server_root_template']) ?>"
style="width:480px;">
<br><span style="color:#666;font-size:0.9em;">
Root directory of the game server. Example: <code>/home/gameserver/servers/{HOME_ID}</code>
</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="install_path_template">Mod Install Path</label></td>
<td style="padding:6px 8px;">
<input type="text" id="install_path_template" name="install_path_template"
value="<?= sw_h($profile['install_path_template']) ?>"
style="width:480px;">
<br><span style="color:#666;font-size:0.9em;">
Where the renamed mod folder ends up. Example: <code>{SERVER_ROOT}/{MOD_FOLDER}</code>
</span>
</td>
</tr>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Folder &amp; Launch Params</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="folder_naming_format">Folder Naming Format</label></td>
<td style="padding:6px 8px;">
<input type="text" id="folder_naming_format" name="folder_naming_format"
value="<?= sw_h($profile['folder_naming_format']) ?>"
style="width:300px;">
<br><span style="color:#666;font-size:0.9em;">
Default folder name template. Common values: <code>@{MOD_NAME}</code> or <code>@{WORKSHOP_ID}</code>
</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="mod_launch_param_template">Client Mod Launch Param</label></td>
<td style="padding:6px 8px;">
<input type="text" id="mod_launch_param_template" name="mod_launch_param_template"
value="<?= sw_h($profile['mod_launch_param_template']) ?>"
style="width:200px;">
<span style="color:#666;font-size:0.9em;">Prefix for client-required mods (e.g. <code>-mod=</code>)</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="servermod_launch_param_template">Server-Side Mod Launch Param</label></td>
<td style="padding:6px 8px;">
<input type="text" id="servermod_launch_param_template" name="servermod_launch_param_template"
value="<?= sw_h($profile['servermod_launch_param_template']) ?>"
style="width:200px;">
<span style="color:#666;font-size:0.9em;">Prefix for server-only mods (e.g. <code>-serverMod=</code>)</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label>Copy .bikey Files</label></td>
<td style="padding:6px 8px;">
<input type="checkbox" name="copy_bikeys_enabled" value="1"
<?= $profile['copy_bikeys_enabled'] ? 'checked' : '' ?>>
Copy .bikey files from mod keys/ folder into server keys/ folder
</td>
</tr>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Scripts (optional)</td>
</tr>
<tr>
<td style="padding:6px 8px;vertical-align:top;"><label for="install_script_template">Install Script Template</label></td>
<td style="padding:6px 8px;">
<textarea id="install_script_template" name="install_script_template"
rows="6" style="width:100%;font-family:monospace;"
><?= sw_h($profile['install_script_template']) ?></textarea>
<span style="color:#666;font-size:0.9em;">
Shell commands to run when installing a mod for the first time. Placeholders expanded before execution.
</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;vertical-align:top;"><label for="update_script_template">Update Script Template</label></td>
<td style="padding:6px 8px;">
<textarea id="update_script_template" name="update_script_template"
rows="6" style="width:100%;font-family:monospace;"
><?= sw_h($profile['update_script_template']) ?></textarea>
<span style="color:#666;font-size:0.9em;">
Shell commands to run when updating an already-installed mod.
</span>
</td>
</tr>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Notes</td>
</tr>
<tr>
<td style="padding:6px 8px;vertical-align:top;"><label for="notes">Notes</label></td>
<td style="padding:6px 8px;">
<textarea id="notes" name="notes"
rows="4" style="width:100%;"
><?= sw_h($profile['notes']) ?></textarea>
</td>
</tr>
</table>
<p>
<button type="submit" name="save_profile" value="1" class="button">Save Profile</button>
&nbsp;
<a href="home.php?m=steam_workshop&p=admin" class="button">Cancel</a>
</p>
</form>
<hr>
<h4>DayZ Default Values (for reference)</h4>
<ul>
<li><strong>Steam App ID:</strong> 223350 (DayZ Dedicated Server)</li>
<li><strong>Workshop App ID:</strong> 221100 (DayZ Workshop)</li>
<li><strong>Workshop Download Dir:</strong> <code>{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}</code></li>
<li><strong>Folder Naming Format:</strong> <code>@{MOD_NAME}</code></li>
<li><strong>Client Mod Launch Param:</strong> <code>-mod=</code></li>
<li><strong>Server-Side Mod Launch Param:</strong> <code>-serverMod=</code></li>
<li><strong>Copy .bikey Files:</strong> Yes</li>
</ul>
<p>
<button type="submit" name="save_profile" value="1" class="button">Save Profile</button>
<a href="home.php?m=steam_workshop&p=admin" class="button">Cancel</a>
</p>
</form>
</div>
<?php
}
function sw_admin_print_styles()
{
static $printed = false;
if ($printed) {
return;
}
$printed = true;
echo '<style>
.sw-admin-panel{background:#171717;border:1px solid #2d2d2d;border-radius:6px;padding:14px;margin:10px 0;color:#e7e7e7}
.sw-admin-table{border-collapse:collapse;background:#121212}
.sw-admin-table th,.sw-admin-table td{border:1px solid #2c2c2c;padding:8px}
.sw-admin-table thead th{background:#232323;color:#fff}
.sw-state-on{color:#78d978;font-weight:700}
.sw-state-off{color:#9a9a9a}
.sw-section{margin-top:14px;padding:12px;border:1px solid #2f2f2f;border-radius:4px;background:#111}
.sw-section h4{margin:0 0 8px 0;color:#f6f6f6}
.sw-note{margin-bottom:10px;background:#202020;border-left:3px solid #3f80d0;padding:10px}
.sw-muted{color:#b3b3b3}
.sw-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:10px}
.sw-grid label, .sw-section > label{display:block}
.sw-grid span, .sw-section span{display:block;font-size:12px;color:#bdbdbd;margin-bottom:4px}
.sw-grid input[type=text], .sw-grid select, .sw-section textarea{width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #3a3a3a;color:#eee;padding:7px;border-radius:4px}
.sw-grid input[type=checkbox]{transform:scale(1.1);margin-top:4px}
.sw-detected-box{margin-top:10px;padding:10px;background:#1d2a1d;border:1px solid #335933;border-radius:4px}
.sw-detected-box ul{margin:8px 0 10px 18px}
.sw-detected-box code,.sw-note code{background:#0b0b0b;padding:1px 4px;border-radius:3px;color:#9fd4ff}
</style>';
}

View file

@ -9,6 +9,25 @@
* (at your option) any later version.
*/
function sw_db_prefix()
{
if (defined('DB_PREFIX') && DB_PREFIX !== '') {
return DB_PREFIX;
}
if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') {
return $GLOBALS['db_prefix'];
}
if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') {
return $GLOBALS['table_prefix'];
}
return 'gsp_';
}
function sw_table($tableName)
{
return '`' . sw_db_prefix() . $tableName . '`';
}
// ── Profile helpers ───────────────────────────────────────────────────────
/**
@ -20,7 +39,7 @@
function sw_get_profiles($db)
{
return $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_game_profiles`
"SELECT * FROM " . sw_table('steam_workshop_game_profiles') . "
ORDER BY `game_name` ASC, `config_name` ASC"
);
}
@ -36,7 +55,7 @@ function sw_get_profile_by_id($db, $id)
{
$id = (int)$id;
$rows = $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_game_profiles`
"SELECT * FROM " . sw_table('steam_workshop_game_profiles') . "
WHERE `id` = $id LIMIT 1"
);
return ($rows && isset($rows[0])) ? $rows[0] : false;
@ -53,7 +72,7 @@ function sw_get_profile_by_config_name($db, $config_name)
{
$safe = $db->realEscapeSingle($config_name);
$rows = $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_game_profiles`
"SELECT * FROM " . sw_table('steam_workshop_game_profiles') . "
WHERE `config_name` = '$safe' LIMIT 1"
);
return ($rows && isset($rows[0])) ? $rows[0] : false;
@ -72,11 +91,11 @@ function sw_get_profile_for_home($db, $home_id)
$home_id = (int)$home_id;
$rows = $db->resultQuery(
"SELECT p.*
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`
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"
@ -97,7 +116,7 @@ function sw_get_server_mods($db, $home_id)
{
$home_id = (int)$home_id;
return $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
"SELECT * FROM " . sw_table('steam_workshop_server_mods') . "
WHERE `home_id` = $home_id
ORDER BY `sort_order` ASC, `id` ASC"
);
@ -114,7 +133,7 @@ function sw_get_mod_by_id($db, $id)
{
$id = (int)$id;
$rows = $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
"SELECT * FROM " . sw_table('steam_workshop_server_mods') . "
WHERE `id` = $id LIMIT 1"
);
return ($rows && isset($rows[0])) ? $rows[0] : false;
@ -135,9 +154,9 @@ 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 `OGP_DB_PREFIXserver_homes` s
JOIN `OGP_DB_PREFIXconfig_homes` c ON c.`home_cfg_id` = s.`home_cfg_id`
JOIN `OGP_DB_PREFIXremote_servers` r ON r.`remote_server_id` = s.`remote_server_id`
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;
@ -167,7 +186,7 @@ function sw_user_owns_home($db, $user_id, $home_id)
// Direct owner
$rows = $db->resultQuery(
"SELECT 1 FROM `OGP_DB_PREFIXserver_homes`
"SELECT 1 FROM " . sw_table('server_homes') . "
WHERE `home_id` = $home_id AND `user_id_main` = $user_id LIMIT 1"
);
if ($rows) {
@ -176,7 +195,7 @@ function sw_user_owns_home($db, $user_id, $home_id)
// Assigned via user_homes
$rows = $db->resultQuery(
"SELECT 1 FROM `OGP_DB_PREFIXuser_homes`
"SELECT 1 FROM " . sw_table('user_homes') . "
WHERE `home_id` = $home_id AND `user_id` = $user_id LIMIT 1"
);
if ($rows) {
@ -185,8 +204,8 @@ function sw_user_owns_home($db, $user_id, $home_id)
// Assigned via group
$rows = $db->resultQuery(
"SELECT 1 FROM `OGP_DB_PREFIXuser_group_homes` ugh
JOIN `OGP_DB_PREFIXuser_groups` ug ON ug.`group_id` = ugh.`group_id`
"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;
@ -250,9 +269,9 @@ function sw_sync_profiles($db)
$safe_name = $db->realEscapeSingle($game_name);
$ok = $db->query(
"INSERT IGNORE INTO `OGP_DB_PREFIXsteam_workshop_game_profiles`
"INSERT IGNORE INTO " . sw_table('steam_workshop_game_profiles') . "
(`config_name`, `game_name`, `enabled`)
VALUES ('$safe_config', '$safe_name', 0)"
VALUES ('$safe_config', '$safe_name', 0)"
);
if ($ok) {
$created++;
@ -364,3 +383,92 @@ 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;
}
}
}
$xmlBlob = $matched->asXML();
$workshopAppId = '';
if ($xmlBlob !== false && preg_match('/steamapps\/workshop\/content\/(\d+)/i', $xmlBlob, $m)) {
$workshopAppId = $m[1];
}
if ($workshopAppId === '') {
$workshopAppId = $steamAppId;
}
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' => '{SERVER_ROOT}/{MOD_FOLDER}',
);
}
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;
}

View file

@ -1,7 +1,6 @@
<navigation>
<!-- Admin: manage Steam Workshop game profiles -->
<page key="admin" file="admin.php" access="admin" />
<!-- User: manage per-server mods
<!-- User: manage per-server mods -->
<page key="user" file="user.php" access="admin,user,subuser" />
-->
</navigation>

View file

@ -95,7 +95,7 @@ function sw_user_add_mod($db, $home_id, array $profile)
// Prevent duplicates
$safe_wid = $db->realEscapeSingle($workshop_id);
$exists = $db->resultQuery(
"SELECT id FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
"SELECT id FROM " . sw_table('steam_workshop_server_mods') . "
WHERE `home_id` = $home_id AND `workshop_id` = '$safe_wid' LIMIT 1"
);
if ($exists) {
@ -105,7 +105,7 @@ function sw_user_add_mod($db, $home_id, array $profile)
// Determine next sort_order
$last = $db->resultQuery(
"SELECT MAX(`sort_order`) AS m FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
"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;
@ -127,9 +127,9 @@ function sw_user_add_mod($db, $home_id, array $profile)
$safe_mname = $mod_name; // already escaped above via realEscapeSingle
$ok = $db->query(
"INSERT INTO `OGP_DB_PREFIXsteam_workshop_server_mods`
"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`)
`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())"
);
@ -164,7 +164,7 @@ function sw_user_save_mod($db, $home_id)
}
$ok = $db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
"UPDATE " . sw_table('steam_workshop_server_mods') . "
SET `mod_name` = '$mod_name',
`folder_name` = '$folder_name',
`mod_type` = '$mod_type',
@ -193,7 +193,7 @@ function sw_user_delete_mod($db, $home_id)
}
$db->query(
"DELETE FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
"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.');
@ -214,7 +214,7 @@ function sw_user_toggle_mod($db, $home_id)
$new_state = $mod['enabled'] ? 0 : 1;
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
"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"
);
@ -241,7 +241,7 @@ function sw_user_reorder_mod($db, $home_id, $direction)
$sorted = array_values($mods);
foreach ($sorted as $idx => $m) {
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
"UPDATE " . sw_table('steam_workshop_server_mods') . "
SET `sort_order` = $idx
WHERE `id` = " . (int)$m['id'] . " AND `home_id` = $home_id LIMIT 1"
);
@ -271,12 +271,12 @@ function sw_user_reorder_mod($db, $home_id, $direction)
// Swap sort_order values
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
"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 `OGP_DB_PREFIXsteam_workshop_server_mods`
"UPDATE " . sw_table('steam_workshop_server_mods') . "
SET `sort_order` = $pos
WHERE `id` = $swap_id AND `home_id` = $home_id LIMIT 1"
);
@ -286,7 +286,7 @@ function sw_user_queue_update($db, $home_id)
{
// Mark all enabled mods as 'queued' so the agent picks them up.
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
"UPDATE " . sw_table('steam_workshop_server_mods') . "
SET `install_status` = 'queued', `updated_at` = NOW()
WHERE `home_id` = $home_id AND `enabled` = 1"
);