Merge pull request #134 from GameServerPanel/copilot/fix-lang-defaults-panel-settings
Fix panel settings language fallbacks, redesign XML config editing, and clean up Steam Workshop admin/user flows
This commit is contained in:
commit
7ff7adca9d
10 changed files with 851 additions and 357 deletions
1
.github/module-map.md
vendored
1
.github/module-map.md
vendored
|
|
@ -31,6 +31,7 @@ This file captures how the control panel, storefront, agents, and helper scripts
|
|||
| `dashboard` | `modules/dashboard/dashboard.php` | Landing page once authenticated. Pulls stats from homes, invoices, and support modules. Shows "Last updated" footer based on `modules/billing/timestamp.txt`. | Reads `billing_orders`, `game_homes`, `tickets`. |
|
||||
| `gamemanager` | `modules/gamemanager/server_monitor.php`, `modules/gamemanager/game_monitor.php` | Shows owned homes, start/stop, update, reinstall, port usage. Uses XML to know command lines. | Relies on `lib_remote`, `config_games`, `user_games` assignments. |
|
||||
| `config_games` | `modules/config_games/add_mod.php`, `server_config_parser.php`, XML files under `server_configs/` | Admin UI for XML definitions. Controls what appears in storefront/service catalog. | Feeds `gamemanager`, billing catalog, cron installers. |
|
||||
| `steam_workshop` | `modules/steam_workshop/admin.php`, `user.php`, `includes/functions.php`, `navigation.xml` | Admin profile defaults + per-home mod management. Profile defaults can now be refreshed from game XML and the user route is explicitly exposed via `p=user`. | Uses `config_games` XML metadata + `server_homes`/assignment tables; feeds workshop agent updater. |
|
||||
| `user_games` | `modules/user_games/add_home.php`, `assign_home.php`, `edit_home.php` | Admin workflow to add homes manually or edit assignments. Shares DB tables with billing provisioner. | Uses `game_homes`, `remote_servers`, `billing_orders`. |
|
||||
| `administration` / `user_admin` | CRUD around users, groups, permissions, expire dates. | Sets roles consumed by storefront admin guard and provisioning ACLs. |
|
||||
| `server` | `modules/server/*` | Remote server management (agents, IPs, ports, reinstall keys). Billing uses these tables for available nodes/locations. |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## 2026-05-06
|
||||
- **Panel settings language defaults:** Added missing English labels/help text for `login_ban_time`, `allow_setting_cpu_affinity`, `regex_invalid_file_name_chars`, `discord_invite_url`, `discord_webhook_main`, and `discord_webhook_admin`; language lookup now loads English fallback strings when the active locale is missing a key so settings pages stop rendering raw `_key_` tokens.
|
||||
- **Config XML section editor redesign:** Added a top-level section-based XML editor in `config_games` with per-section Validate/Save/Reset actions, optional-section add/remove controls, required-section removal protection, and schema validation for section updates; kept the existing detailed node editor and full raw XML editor for advanced edits.
|
||||
- **Steam Workshop admin and routing cleanup:** Reworked `steam_workshop` admin profile editing into clearer grouped dark-theme panels, added XML-based default detection/refresh flow with explicit overwrite confirmation, switched touched SQL to active DB prefix helpers, and re-enabled the `p=user` route in module navigation so valid user links no longer fail with “Invalid subpage given.”
|
||||
- **Billing/admin provisioning hardening:** Styled the panel Migrate action like the other server action buttons, switched admin-created billing rows to the canonical monthly/31-day default, and made paid checkout fulfillment sync `billing_orders.home_id`, `billing_invoices.home_id`, and `billing_transactions.home_id` after provisioning so paid orders no longer stay at `home_id = 0`.
|
||||
- **Billing cart data correctness:** `add_to_cart.php` now calculates invoice amounts from the selected slot count and duration, stores `subtotal`/`total_due` metadata, and replaces `ChangeMe` placeholders with securely generated passwords before anything is written to billing tables.
|
||||
- **PayPal/coupon idempotency:** Cart checkout now stamps PayPal `custom_id` with the exact invoice IDs being purchased, capture/free/webhook handlers normalize month=31-day renewals, avoid duplicate transaction logs, and queue provisioning only for orders that still lack a home.
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@
|
|||
- Add an admin-facing toggle that makes it clear when the HTML scraper fallback is in use and lets staff force API-only mode if Valve ever objects.
|
||||
- Add Workshop result preview thumbnails and author links in the picker for easier browsing.
|
||||
- Add a lightweight admin UI report that flags remaining PHP files still relying on legacy PHP 7 constructs not covered by the automated compatibility pass.
|
||||
- Add a side-by-side before/after diff preview panel to the config_games top-level XML section editor before section saves.
|
||||
|
|
|
|||
|
|
@ -85,6 +85,63 @@ function ogpLang()
|
|||
}
|
||||
}
|
||||
|
||||
function ogp_load_english_fallbacks()
|
||||
{
|
||||
static $coreLoaded = false;
|
||||
static $loadedModules = array();
|
||||
global $lang_modules;
|
||||
|
||||
$englishDir = "lang/English";
|
||||
if (!is_dir($englishDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($coreLoaded === false) {
|
||||
$coreFiles = glob($englishDir . "/*.php");
|
||||
if (is_array($coreFiles)) {
|
||||
foreach ($coreFiles as $coreFile) {
|
||||
ogp_include_lang_file_safely($coreFile);
|
||||
}
|
||||
}
|
||||
$coreLoaded = true;
|
||||
}
|
||||
|
||||
$modulesToLoad = array();
|
||||
if (isset($_REQUEST['m']) && $_REQUEST['m'] !== '') {
|
||||
$modulesToLoad[] = $_REQUEST['m'];
|
||||
}
|
||||
foreach ((array)$lang_modules as $moduleName) {
|
||||
$modulesToLoad[] = $moduleName;
|
||||
}
|
||||
|
||||
foreach (array_unique($modulesToLoad) as $moduleName) {
|
||||
if (!preg_match('/^([a-z]|[0-9]|_|-)+$/i', (string)$moduleName)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($loadedModules[$moduleName])) {
|
||||
continue;
|
||||
}
|
||||
$moduleFile = $englishDir . "/modules/" . $moduleName . ".php";
|
||||
if (is_file($moduleFile)) {
|
||||
ogp_include_lang_file_safely($moduleFile);
|
||||
}
|
||||
$loadedModules[$moduleName] = true;
|
||||
}
|
||||
}
|
||||
|
||||
function ogp_include_lang_file_safely($filePath)
|
||||
{
|
||||
set_error_handler(function ($severity, $message) {
|
||||
$isConstantRedefinition = (bool)preg_match('/^Constant\\s+.+\\s+already\\s+defined$/i', trim((string)$message));
|
||||
if ($severity === E_WARNING && $isConstantRedefinition) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
include_once($filePath);
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
function get_lang($lang_index)
|
||||
{
|
||||
global $OGPLangPre;
|
||||
|
|
@ -94,13 +151,26 @@ function get_lang($lang_index)
|
|||
return constant($lang_index);
|
||||
}
|
||||
|
||||
if(!startsWith($lang_index, $OGPLangPre)){
|
||||
if(!startsWith($lang_index, $OGPLangPre)){
|
||||
$newLangIndex = $OGPLangPre . $lang_index;
|
||||
if (defined($newLangIndex))
|
||||
{
|
||||
return constant($newLangIndex);
|
||||
}
|
||||
ogp_load_english_fallbacks();
|
||||
if (defined($newLangIndex))
|
||||
{
|
||||
return constant($newLangIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ogp_load_english_fallbacks();
|
||||
if (defined($lang_index))
|
||||
{
|
||||
return constant($lang_index);
|
||||
}
|
||||
}
|
||||
|
||||
// Any other case is error.
|
||||
return "_".$lang_index."_";
|
||||
|
|
@ -117,13 +187,26 @@ function get_lang_f()
|
|||
return vsprintf(constant($lang_index),$args);
|
||||
}
|
||||
|
||||
if(!startsWith($lang_index, $OGPLangPre)){
|
||||
if(!startsWith($lang_index, $OGPLangPre)){
|
||||
$newLangIndex = $OGPLangPre . $lang_index;
|
||||
if (defined($newLangIndex))
|
||||
{
|
||||
return vsprintf(constant($newLangIndex),$args);
|
||||
}
|
||||
ogp_load_english_fallbacks();
|
||||
if (defined($newLangIndex))
|
||||
{
|
||||
return vsprintf(constant($newLangIndex),$args);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ogp_load_english_fallbacks();
|
||||
if (defined($lang_index))
|
||||
{
|
||||
return vsprintf(constant($lang_index),$args);
|
||||
}
|
||||
}
|
||||
|
||||
return "_".$lang_index."_".implode("_",$args)."_";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ define('LANG_recaptcha_use_login', "Use Recaptcha on Login");
|
|||
define('LANG_recaptcha_use_login_info', "If enabled, users will have to solve the Not a Robot Recaptcha when attempting to login.");
|
||||
define('LANG_login_attempts_before_banned', "Number of failed login attempts before user is banned");
|
||||
define('LANG_login_attempts_before_banned_info', "If a user tries to login with invalid credentials more than this many times, the user will be banned temporarily by the panel.");
|
||||
define('LANG_login_ban_time', "Login ban duration (seconds)");
|
||||
define('LANG_login_ban_time_info', "How long a user stays temporarily banned after reaching the failed login attempt limit.");
|
||||
define('LANG_custom_github_update_username', "GitHub update username");
|
||||
define('LANG_custom_github_update_username_info', "Enter your GitHub username ONLY to use your own forked repositories to update OGP. This should only be changed by developers who wish to use their own repos for development rather than checking in possibly buggy code into the main branch.");
|
||||
define('LANG_remote_query', "Remote query");
|
||||
|
|
@ -129,6 +131,16 @@ define('LANG_default_game_server_home_path_prefix', "Default game server home di
|
|||
define('LANG_default_game_server_home_path_prefix_info', "Enter a path prefix for where you want game server homes to be created by default. You can use \"{USERNAME}\" in the path which will be replaced with the OGP username the game server is being assigned to. You can use \"{GAMEKEY}\" in the path which will be replaced with a friendly lowercase name. You can use \"{SKIPID}\" anywhere in the path to skip appending the home ID to the path. Example: /ogp/games/{USERNAME}/{GAMEKEY}{SKIPID} will become /ogp/games/username/arkse/. Example 2: /ogp/games will become /ogp/games/1 where 1 is the game servers ID.");
|
||||
define('LANG_use_authorized_hosts', "Limit API to Defined Authorized Hosts");
|
||||
define('LANG_use_authorized_hosts_info', "Enable this setting to only allow API calls from pre-defined and approved IP addresses. Approved addresses can be set on this page once the setting has been enabled. If this setting is disabled, a user using a valid key will have access to the API from any IP address. Users using a valid key will be able to use the API to manage any game server they have permissions to administrate.");
|
||||
define('LANG_allow_setting_cpu_affinity', "Allow CPU affinity editing");
|
||||
define('LANG_allow_setting_cpu_affinity_info', "Allow users to set CPU affinity values for their game servers when supported by the host.");
|
||||
define('LANG_regex_invalid_file_name_chars', "Invalid filename characters regex");
|
||||
define('LANG_regex_invalid_file_name_chars_info', "Regular expression used by the file manager to block unsafe filename characters.");
|
||||
define('LANG_discord_invite_url', "Discord invite URL");
|
||||
define('LANG_discord_invite_url_info', "Invite URL used by panel links that send users to your Discord server.");
|
||||
define('LANG_discord_webhook_main', "Discord webhook (main)");
|
||||
define('LANG_discord_webhook_main_info', "Webhook URL used for general panel notifications.");
|
||||
define('LANG_discord_webhook_admin', "Discord webhook (admin)");
|
||||
define('LANG_discord_webhook_admin_info', "Webhook URL used for administrator-specific notifications.");
|
||||
define('LANG_setup_api_authorized_hosts', "Setup API authorized hosts");
|
||||
define('LANG_autohorized_hosts', "Authorized hosts");
|
||||
define('LANG_add', "Add");
|
||||
|
|
@ -143,6 +155,7 @@ define('LANG_reset_game_server_order_info', "Resets game server ordering back to
|
|||
|
||||
// Debug level
|
||||
define('LANG_debug_level', "Panel Debug Level");
|
||||
define('LANG_debug_level_info', "Controls how much PHP error output is shown in the panel.");
|
||||
define('LANG_debug_off', "Off (production)");
|
||||
define('LANG_debug_fatal_only', "Fatal errors only (page-breaking)");
|
||||
define('LANG_debug_errors_warnings', "Errors & Warnings");
|
||||
|
|
|
|||
|
|
@ -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,305 @@ 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 = 'Section XML is not well-formed.';
|
||||
if (!empty($errors)) {
|
||||
$msg = trim($errors[0]->message) . ' (line ' . $errors[0]->line . ')';
|
||||
}
|
||||
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>";
|
||||
$sectionEditorNote = "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.";
|
||||
echo "<p class='note'>{$sectionEditorNote}</p>";
|
||||
|
||||
if (!empty($optionalMissing)) {
|
||||
echo "<form class='xml-add-section' action='?m=config_games&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&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 +928,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>⚠ 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>⚠ 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>⚠ 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>⚠ 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 +1132,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 (previous default editor)</summary>";
|
||||
echo "<form action='?m=config_games&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 +1142,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'>★ = 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'>⚠ <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&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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,68 @@ function exec_ogp_module()
|
|||
{
|
||||
global $db;
|
||||
|
||||
echo '<h2>Steam Workshop – Admin</h2>';
|
||||
echo '<h2>Steam Workshop – 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) {
|
||||
$overwriteMessage = $overwrite
|
||||
? ' Existing values were allowed to be overwritten.'
|
||||
: ' Existing non-empty values were kept.';
|
||||
sw_success("Applied $updated detected default value(s)." . $overwriteMessage);
|
||||
} else {
|
||||
sw_success('No profile values needed updating based on current overwrite setting.');
|
||||
}
|
||||
|
||||
$profile = sw_get_profile_by_id($db, (int)$profile['id']);
|
||||
sw_admin_edit_form($profile, $detected, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($action === 'edit' && isset($_GET['id'])) {
|
||||
$profile = sw_get_profile_by_id($db, (int)$_GET['id']);
|
||||
if ($profile) {
|
||||
|
|
@ -50,18 +86,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 +105,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 +154,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">« Back to profile list</a></p>
|
||||
|
||||
<h3>Edit Profile: <?= sw_h($profile['config_name']) ?> – <?= 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>
|
||||
<code>{SERVER_ID}</code>
|
||||
<code>{REMOTE_SERVER_ID}</code>
|
||||
<code>{GAME_NAME}</code>
|
||||
<code>{CONFIG_NAME}</code>
|
||||
<code>{WORKSHOP_ID}</code>
|
||||
<code>{MOD_NAME}</code>
|
||||
<code>{FOLDER_NAME}</code>
|
||||
<code>{STEAM_APP_ID}</code>
|
||||
<code>{WORKSHOP_APP_ID}</code>
|
||||
<code>{STEAMCMD_PATH}</code>
|
||||
<code>{WORKSHOP_DOWNLOAD_DIR}</code>
|
||||
<code>{SERVER_ROOT}</code>
|
||||
<code>{INSTALL_PATH}</code>
|
||||
<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 & 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>
|
||||
|
||||
<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>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -248,12 +267,7 @@ function sw_sync_profiles($db)
|
|||
|
||||
$safe_config = $db->realEscapeSingle($config_name);
|
||||
$safe_name = $db->realEscapeSingle($game_name);
|
||||
|
||||
$ok = $db->query(
|
||||
"INSERT IGNORE INTO `OGP_DB_PREFIXsteam_workshop_game_profiles`
|
||||
(`config_name`, `game_name`, `enabled`)
|
||||
VALUES ('$safe_config', '$safe_name', 0)"
|
||||
);
|
||||
$ok = $db->query("INSERT IGNORE INTO " . sw_table('steam_workshop_game_profiles') . " (`config_name`, `game_name`, `enabled`) VALUES ('$safe_config', '$safe_name', 0)");
|
||||
if ($ok) {
|
||||
$created++;
|
||||
}
|
||||
|
|
@ -364,3 +378,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue