doc changes and reference folder
This commit is contained in:
parent
11691a5876
commit
82cbc206eb
33 changed files with 1514 additions and 2855 deletions
|
|
@ -223,6 +223,12 @@ function scm_get_workshop_manifest_path(array $home_info)
|
|||
|
||||
function scm_extract_workshop_app_id($server_xml)
|
||||
{
|
||||
if (isset($server_xml->workshop_support->workshop_app_id)) {
|
||||
$value = trim((string)$server_xml->workshop_support->workshop_app_id);
|
||||
if ($value !== '' && preg_match('/^[0-9]+$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
$candidates = array(
|
||||
'workshop_app_id',
|
||||
'workshop_appid',
|
||||
|
|
@ -240,9 +246,53 @@ function scm_extract_workshop_app_id($server_xml)
|
|||
return "";
|
||||
}
|
||||
|
||||
function scm_extract_workshop_steam_app_id($server_xml)
|
||||
{
|
||||
if (isset($server_xml->workshop_support->steam_app_id)) {
|
||||
$value = trim((string)$server_xml->workshop_support->steam_app_id);
|
||||
if ($value !== '' && preg_match('/^[0-9]+$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function scm_extract_workshop_install_path($server_xml)
|
||||
{
|
||||
if (isset($server_xml->workshop_support->install_path)) {
|
||||
$value = trim((string)$server_xml->workshop_support->install_path);
|
||||
if ($value !== '' && preg_match('/^[^\\r\\n\\0]+$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function scm_workshop_xml_bool($value, $default = false)
|
||||
{
|
||||
$value = strtolower(trim((string)$value));
|
||||
if ($value === '') {
|
||||
return (bool)$default;
|
||||
}
|
||||
if (in_array($value, array('1', 'yes', 'true', 'on'), true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($value, array('0', 'no', 'false', 'off'), true)) {
|
||||
return false;
|
||||
}
|
||||
return (bool)$default;
|
||||
}
|
||||
|
||||
function scm_get_workshop_script_path(array $home_info, $server_xml)
|
||||
{
|
||||
$key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux';
|
||||
$nested_key = scm_is_windows_home($home_info) ? 'script_windows' : 'script_linux';
|
||||
if (isset($server_xml->workshop_support->$nested_key)) {
|
||||
$xml_path = trim((string)$server_xml->workshop_support->$nested_key);
|
||||
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return $xml_path;
|
||||
}
|
||||
}
|
||||
if (isset($server_xml->$key)) {
|
||||
$xml_path = trim((string)$server_xml->$key);
|
||||
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
|
|
@ -255,14 +305,20 @@ function scm_get_workshop_script_path(array $home_info, $server_xml)
|
|||
function scm_get_configured_workshop_script_path(array $home_info, $server_xml)
|
||||
{
|
||||
$key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux';
|
||||
if (!isset($server_xml->$key)) {
|
||||
return '';
|
||||
$nested_key = scm_is_windows_home($home_info) ? 'script_windows' : 'script_linux';
|
||||
if (isset($server_xml->workshop_support->$nested_key)) {
|
||||
$xml_path = trim((string)$server_xml->workshop_support->$nested_key);
|
||||
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return $xml_path;
|
||||
}
|
||||
}
|
||||
$xml_path = trim((string)$server_xml->$key);
|
||||
if ($xml_path === '' || !preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return '';
|
||||
if (isset($server_xml->$key)) {
|
||||
$xml_path = trim((string)$server_xml->$key);
|
||||
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return $xml_path;
|
||||
}
|
||||
}
|
||||
return $xml_path;
|
||||
return '';
|
||||
}
|
||||
|
||||
function scm_get_bundled_workshop_script_source(array $home_info)
|
||||
|
|
@ -658,13 +714,16 @@ function scm_build_workshop_runtime_context($db, array $home_info, $server_xml,
|
|||
if ($fallback_workshop_app_id === '') {
|
||||
$fallback_workshop_app_id = scm_extract_workshop_app_id($server_xml);
|
||||
}
|
||||
$steam_app_id = (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '';
|
||||
$steam_app_id = (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : scm_extract_workshop_steam_app_id($server_xml);
|
||||
$folder_name = ($optional_folder_name !== '') ? $optional_folder_name : '@' . $workshop_item_id;
|
||||
$effective_template = $target_path_template;
|
||||
if ($effective_template === '') {
|
||||
$effective_template = (is_array($fallback_profile) && !empty($fallback_profile['install_path_template']))
|
||||
? (string)$fallback_profile['install_path_template']
|
||||
: scm_get_default_workshop_target_template($install_strategy);
|
||||
if (is_array($fallback_profile) && !empty($fallback_profile['install_path_template'])) {
|
||||
$effective_template = (string)$fallback_profile['install_path_template'];
|
||||
} else {
|
||||
$xml_install_path = scm_extract_workshop_install_path($server_xml);
|
||||
$effective_template = $xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy);
|
||||
}
|
||||
}
|
||||
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => isset($server_xml->exe_location) ? (string)$server_xml->exe_location : ''), array(
|
||||
'WORKSHOP_ID' => $workshop_item_id,
|
||||
|
|
@ -695,6 +754,12 @@ function scm_detect_workshop_install_strategy(array $home_info, $server_xml, arr
|
|||
return strtolower($strategy);
|
||||
}
|
||||
}
|
||||
if (isset($server_xml->workshop_support->install_strategy)) {
|
||||
$strategy = trim((string)$server_xml->workshop_support->install_strategy);
|
||||
if ($strategy !== '' && preg_match('/^[a-z0-9_\-]+$/i', $strategy)) {
|
||||
return strtolower($strategy);
|
||||
}
|
||||
}
|
||||
foreach (array('workshop_install_strategy', 'install_strategy') as $tag) {
|
||||
if (isset($server_xml->$tag)) {
|
||||
$strategy = trim((string)$server_xml->$tag);
|
||||
|
|
@ -718,15 +783,33 @@ function scm_detect_workshop_install_strategy(array $home_info, $server_xml, arr
|
|||
|
||||
function scm_workshop_should_copy_keys($server_xml, $install_strategy)
|
||||
{
|
||||
if (isset($server_xml->workshop_support->copy_keys)) {
|
||||
$attrs = $server_xml->workshop_support->copy_keys->attributes();
|
||||
if (isset($attrs['enabled'])) {
|
||||
return scm_workshop_xml_bool((string)$attrs['enabled'], false);
|
||||
}
|
||||
}
|
||||
foreach (array('workshop_copy_keys', 'copy_workshop_keys') as $tag) {
|
||||
if (isset($server_xml->$tag)) {
|
||||
$value = strtolower(trim((string)$server_xml->$tag));
|
||||
return in_array($value, array('1', 'yes', 'true', 'on'), true);
|
||||
return scm_workshop_xml_bool((string)$server_xml->$tag, false);
|
||||
}
|
||||
}
|
||||
return in_array((string)$install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true);
|
||||
}
|
||||
|
||||
function scm_workshop_keys_target_path($server_xml, array $home_info)
|
||||
{
|
||||
$template = '';
|
||||
if (isset($server_xml->workshop_support->copy_keys->target_path)) {
|
||||
$template = trim((string)$server_xml->workshop_support->copy_keys->target_path);
|
||||
}
|
||||
if ($template === '') {
|
||||
$template = '{SERVER_ROOT}/keys';
|
||||
}
|
||||
$map = scm_build_placeholder_map($home_info);
|
||||
return scm_apply_placeholders($template, $map);
|
||||
}
|
||||
|
||||
function scm_build_placeholder_map(array $home_info, array $server_context = array(), array $overrides = array())
|
||||
{
|
||||
$home_id = (int)(isset($home_info['home_id']) ? $home_info['home_id'] : 0);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ scm_workshop_test_assert($script === '/cygdrive/c/OGP_User_Files/11/gsp_server_c
|
|||
scm_workshop_test_assert(isset($windowsRemote->files[$script]), 'writes Windows/Cygwin bundled script to fake agent');
|
||||
scm_workshop_test_assert($error === '', 'default Windows/Cygwin script staging does not report missing script');
|
||||
|
||||
$configuredXml = simplexml_load_string('<game_config><workshop_script_linux>/agent/custom/workshop.sh</workshop_script_linux></game_config>');
|
||||
$configuredXml = simplexml_load_string('<game_config><workshop_support><script_linux>/agent/custom/workshop.sh</script_linux></workshop_support></game_config>');
|
||||
$customRemote = new ScmWorkshopFakeRemote();
|
||||
$customRemote->existing[] = '/agent/custom/workshop.sh';
|
||||
$script = scm_prepare_workshop_script_for_agent($customRemote, $linuxHome, $configuredXml, $error);
|
||||
|
|
@ -110,7 +110,13 @@ $runtime = scm_build_workshop_runtime_context(new stdClass(), $linuxHome, $empty
|
|||
scm_workshop_test_assert($runtime['target_path_template'] === '{SERVER_ROOT}/{MOD_FOLDER}', 'Arma strategy keeps @mod folder at server root');
|
||||
scm_workshop_test_assert($runtime['target_path_resolved'] === '/srv/games/arma3/@450814997', 'Arma target path resolves to root @WorkshopID folder');
|
||||
|
||||
$appXml = simplexml_load_string('<game_config><workshop_app_id>107410</workshop_app_id></game_config>');
|
||||
scm_workshop_test_assert(scm_extract_workshop_app_id($appXml) === '107410', 'extracts explicit Workshop app ID from game XML');
|
||||
$appXml = simplexml_load_string('<game_config><workshop_support><steam_app_id>107410</steam_app_id><workshop_app_id>107410</workshop_app_id><install_strategy>arma_mod_folder</install_strategy><install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path><copy_keys enabled="1"><target_path>{SERVER_ROOT}/keys</target_path></copy_keys></workshop_support></game_config>');
|
||||
scm_workshop_test_assert(scm_extract_workshop_app_id($appXml) === '107410', 'extracts canonical Workshop app ID from game XML');
|
||||
scm_workshop_test_assert(scm_extract_workshop_steam_app_id($appXml) === '107410', 'extracts canonical Steam app ID from game XML');
|
||||
scm_workshop_test_assert(scm_detect_workshop_install_strategy($linuxHome, $appXml) === 'arma_mod_folder', 'extracts canonical Workshop install strategy from game XML');
|
||||
$runtime = scm_build_workshop_runtime_context(new stdClass(), $linuxHome, $appXml, array('workshop_item_id' => '450814997'), $message);
|
||||
scm_workshop_test_assert($runtime['target_path_template'] === '{SERVER_ROOT}/{MOD_FOLDER}', 'canonical Workshop install_path controls target template');
|
||||
scm_workshop_test_assert($runtime['target_path_resolved'] === '/srv/games/arma3/@450814997', 'canonical Workshop install_path resolves under server root');
|
||||
scm_workshop_test_assert(scm_workshop_should_copy_keys($appXml, 'copy_to_mod_folder') === true, 'canonical copy_keys enabled flag is honored');
|
||||
|
||||
echo "All Workshop helper smoke tests passed.\n";
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
|||
{
|
||||
$install_strategy = scm_detect_workshop_install_strategy($home_info, $server_xml, $template);
|
||||
$copy_keys = scm_workshop_should_copy_keys($server_xml, $install_strategy);
|
||||
$xml_install_path = scm_extract_workshop_install_path($server_xml);
|
||||
$keys_target_path = scm_workshop_keys_target_path($server_xml, $home_info);
|
||||
$item_details = array();
|
||||
$resolved_app_id = '';
|
||||
$steam_app_id = '';
|
||||
|
|
@ -127,13 +129,16 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
|||
'target_path_resolved' => isset($runtime['target_path_resolved']) ? (string)$runtime['target_path_resolved'] : '',
|
||||
'install_strategy' => $install_strategy,
|
||||
'copy_keys' => $copy_keys ? 1 : 0,
|
||||
'keys_target_path' => rtrim((string)$home_info['home_path'], '/') . '/keys',
|
||||
'keys_target_path' => $keys_target_path,
|
||||
);
|
||||
}
|
||||
|
||||
if ($resolved_app_id === '') {
|
||||
$resolved_app_id = scm_extract_workshop_app_id($server_xml);
|
||||
}
|
||||
if ($steam_app_id === '') {
|
||||
$steam_app_id = scm_extract_workshop_steam_app_id($server_xml);
|
||||
}
|
||||
|
||||
return array(
|
||||
'workshop_app_id' => $resolved_app_id,
|
||||
|
|
@ -141,7 +146,8 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
|||
'server_root' => rtrim((string)$home_info['home_path'], '/'),
|
||||
'install_strategy' => $install_strategy,
|
||||
'copy_keys' => $copy_keys ? 1 : 0,
|
||||
'target_path_template' => isset($template['target_path_template']) && trim((string)$template['target_path_template']) !== '' ? trim((string)$template['target_path_template']) : scm_get_default_workshop_target_template($install_strategy),
|
||||
'target_path_template' => isset($template['target_path_template']) && trim((string)$template['target_path_template']) !== '' ? trim((string)$template['target_path_template']) : ($xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy)),
|
||||
'keys_target_path' => $keys_target_path,
|
||||
'post_install_script' => isset($template['post_script']) ? trim((string)$template['post_script']) : '',
|
||||
'launch_param_additions' => isset($template['launch_param_additions']) ? trim((string)$template['launch_param_additions']) : '',
|
||||
'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0,
|
||||
|
|
|
|||
|
|
@ -268,6 +268,77 @@
|
|||
<xs:element name="file" type="files_type" maxOccurs="unbounded" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- Canonical Workshop / Server Content capability declaration.
|
||||
The Server Content Manager (addonsmanager) reads this block to build
|
||||
safe Workshop manifests for the agents. Keep customer-supplied Workshop
|
||||
item IDs out of XML; users enter those through the Panel UI. -->
|
||||
<xs:simpleType name="boolean_flag_type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="0" />
|
||||
<xs:enumeration value="1" />
|
||||
<xs:enumeration value="false" />
|
||||
<xs:enumeration value="true" />
|
||||
<xs:enumeration value="no" />
|
||||
<xs:enumeration value="yes" />
|
||||
<xs:enumeration value="off" />
|
||||
<xs:enumeration value="on" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="workshop_provider_type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="steam" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="workshop_download_method_type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="steamcmd" />
|
||||
<xs:enumeration value="game_managed_workshop" />
|
||||
<xs:enumeration value="manual" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="workshop_install_strategy_type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="game_managed_workshop" />
|
||||
<xs:enumeration value="steamcmd_download_only" />
|
||||
<xs:enumeration value="copy_to_game_root" />
|
||||
<xs:enumeration value="copy_to_mod_folder" />
|
||||
<xs:enumeration value="dayz_mod_folder" />
|
||||
<xs:enumeration value="arma_mod_folder" />
|
||||
<xs:enumeration value="config_only" />
|
||||
<xs:enumeration value="custom_scripted_install" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="workshop_copy_keys_type">
|
||||
<xs:sequence>
|
||||
<xs:element name="source_pattern" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="target_path" type="xs:string" minOccurs="0" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="enabled" type="boolean_flag_type" use="optional" />
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="workshop_support_type">
|
||||
<xs:sequence>
|
||||
<xs:element name="enabled" type="boolean_flag_type" minOccurs="0" />
|
||||
<xs:element name="provider" type="workshop_provider_type" minOccurs="0" />
|
||||
<xs:element name="steam_app_id" type="xs:positiveInteger" minOccurs="0" />
|
||||
<xs:element name="workshop_app_id" type="xs:positiveInteger" minOccurs="0" />
|
||||
<xs:element name="download_method" type="workshop_download_method_type" minOccurs="0" />
|
||||
<xs:element name="install_strategy" type="workshop_install_strategy_type" minOccurs="0" />
|
||||
<xs:element name="install_path" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="startup_param_format" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="mod_separator" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="mod_prefix" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="mod_folder_format" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="copy_keys" type="workshop_copy_keys_type" minOccurs="0" />
|
||||
<xs:element name="script_linux" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="script_windows" type="xs:string" minOccurs="0" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- The main of the template -->
|
||||
<xs:complexType name="server_config_type">
|
||||
|
|
@ -278,6 +349,7 @@
|
|||
<xs:element name="gameq_query_name" type="nonEmptyString" minOccurs="0" />
|
||||
<xs:element name="installer" type="nonEmptyString" minOccurs="0" />
|
||||
<xs:element name="game_name" type="nonEmptyString" />
|
||||
<xs:element name="workshop_support" type="workshop_support_type" minOccurs="0" />
|
||||
<xs:element name="server_exec_name" type="nonEmptyString" />
|
||||
<xs:element name="query_port" type="query_port_type" minOccurs="0" />
|
||||
<xs:element name="cli_template" type="nonEmptyString" minOccurs="0" />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,22 @@
|
|||
<game_key>arma3_linux64</game_key>
|
||||
<installer>steamcmd</installer>
|
||||
<game_name>Arma 3</game_name>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<workshop_support>
|
||||
<enabled>1</enabled>
|
||||
<provider>steam</provider>
|
||||
<steam_app_id>107410</steam_app_id>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<download_method>steamcmd</download_method>
|
||||
<install_strategy>arma_mod_folder</install_strategy>
|
||||
<install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path>
|
||||
<startup_param_format>-mod={MOD_LIST}</startup_param_format>
|
||||
<mod_separator>;</mod_separator>
|
||||
<mod_prefix>@</mod_prefix>
|
||||
<copy_keys enabled="1">
|
||||
<source_pattern>{MOD_PATH}/keys/*.bikey</source_pattern>
|
||||
<target_path>{SERVER_ROOT}/keys</target_path>
|
||||
</copy_keys>
|
||||
</workshop_support>
|
||||
<server_exec_name>arma3server_x64</server_exec_name>
|
||||
<cli_template>%CONFIG% %CFG% %PROFILES% %NAME% %IP% %PORT% %PLAYERS% %MODLIST% %SERVERMODLIST% %AUTOINIT%</cli_template>
|
||||
<cli_params>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,22 @@
|
|||
<game_key>arma3_win64</game_key>
|
||||
<installer>steamcmd</installer>
|
||||
<game_name>Arma 3</game_name>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<workshop_support>
|
||||
<enabled>1</enabled>
|
||||
<provider>steam</provider>
|
||||
<steam_app_id>107410</steam_app_id>
|
||||
<workshop_app_id>107410</workshop_app_id>
|
||||
<download_method>steamcmd</download_method>
|
||||
<install_strategy>arma_mod_folder</install_strategy>
|
||||
<install_path>{SERVER_ROOT}/{MOD_FOLDER}</install_path>
|
||||
<startup_param_format>-mod={MOD_LIST}</startup_param_format>
|
||||
<mod_separator>;</mod_separator>
|
||||
<mod_prefix>@</mod_prefix>
|
||||
<copy_keys enabled="1">
|
||||
<source_pattern>{MOD_PATH}/keys/*.bikey</source_pattern>
|
||||
<target_path>{SERVER_ROOT}/keys</target_path>
|
||||
</copy_keys>
|
||||
</workshop_support>
|
||||
<server_exec_name>arma3server.exe</server_exec_name>
|
||||
<cli_template>-profiles=profile -name=player -config=profile\server.cfg -cfg=profile\basic.cfg %PORT% %PLAYERS% %RANKING% %AUTOINIT% %DEBUG% %MODS% %SERVERMODS%</cli_template>
|
||||
<cli_params>
|
||||
|
|
|
|||
60
Panel/modules/config_games/tests/validate_server_configs.php
Normal file
60
Panel/modules/config_games/tests/validate_server_configs.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
/*
|
||||
* Validates all game server XML configuration files against the bundled
|
||||
* schema. This is a developer smoke test; it does not load the full Panel.
|
||||
*/
|
||||
|
||||
$baseDir = dirname(__DIR__);
|
||||
$schema = $baseDir . '/schema_server_config.xml';
|
||||
$configDir = $baseDir . '/server_configs';
|
||||
|
||||
if (!is_file($schema)) {
|
||||
fwrite(STDERR, "Schema not found: {$schema}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$files = glob($configDir . '/*.xml');
|
||||
sort($files);
|
||||
|
||||
if (empty($files)) {
|
||||
fwrite(STDERR, "No XML files found under: {$configDir}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$failed = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
$doc = new DOMDocument();
|
||||
$doc->preserveWhiteSpace = false;
|
||||
$doc->formatOutput = false;
|
||||
|
||||
if (!$doc->load($file)) {
|
||||
$failed++;
|
||||
echo "FAIL " . basename($file) . "\n";
|
||||
foreach (libxml_get_errors() as $error) {
|
||||
echo " XML parse: " . trim($error->message) . "\n";
|
||||
}
|
||||
libxml_clear_errors();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$doc->schemaValidate($schema)) {
|
||||
$failed++;
|
||||
echo "FAIL " . basename($file) . "\n";
|
||||
foreach (libxml_get_errors() as $error) {
|
||||
echo " Schema: " . trim($error->message) . "\n";
|
||||
}
|
||||
libxml_clear_errors();
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "PASS " . basename($file) . "\n";
|
||||
}
|
||||
|
||||
if ($failed > 0) {
|
||||
echo "XML validation failed for {$failed} file(s).\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "All server config XML files validated successfully.\n";
|
||||
|
|
@ -1,364 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: Admin profile management
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
if (!defined('SERVER_CONFIG_LOCATION')) {
|
||||
require_once __DIR__ . '/../../config_games/server_config_parser.php';
|
||||
}
|
||||
|
||||
function exec_ogp_module()
|
||||
{
|
||||
global $db;
|
||||
|
||||
echo '<h2>Steam Workshop – Admin</h2>';
|
||||
sw_admin_print_styles();
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_profile'])) {
|
||||
sw_admin_save_profile($db);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sync_profiles'])) {
|
||||
$n = sw_sync_profiles($db);
|
||||
sw_success("Sync complete. $n new profile(s) created.");
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['detect_defaults'])) {
|
||||
$profile = sw_get_profile_by_id($db, (int)($_POST['id'] ?? 0));
|
||||
if (!$profile) {
|
||||
sw_error('Profile not found.');
|
||||
sw_admin_list($db);
|
||||
return;
|
||||
}
|
||||
$detected = sw_detect_profile_defaults_from_xml($profile['config_name']);
|
||||
if (empty($detected)) {
|
||||
sw_error('No Steam defaults were detected in this game XML. You can still enter values manually.');
|
||||
} else {
|
||||
sw_success('Detected XML defaults. Review and apply when ready.');
|
||||
}
|
||||
sw_admin_edit_form($profile, $detected, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['apply_detected_defaults'])) {
|
||||
$profile = sw_get_profile_by_id($db, (int)($_POST['id'] ?? 0));
|
||||
if (!$profile) {
|
||||
sw_error('Profile not found.');
|
||||
sw_admin_list($db);
|
||||
return;
|
||||
}
|
||||
$detected = sw_detect_profile_defaults_from_xml($profile['config_name']);
|
||||
if (empty($detected)) {
|
||||
sw_error('No Steam defaults were detected in this game XML.');
|
||||
sw_admin_edit_form($profile);
|
||||
return;
|
||||
}
|
||||
|
||||
$overwrite = isset($_POST['overwrite_existing']) && $_POST['overwrite_existing'] === '1';
|
||||
$updated = sw_apply_detected_profile_defaults($db, $profile, $detected, $overwrite);
|
||||
if ($updated > 0) {
|
||||
$overwriteMessage = $overwrite
|
||||
? ' Existing values were allowed to be overwritten.'
|
||||
: ' Existing non-empty values were kept.';
|
||||
sw_success("Applied $updated detected default value(s)." . $overwriteMessage);
|
||||
} else {
|
||||
sw_success('No profile values needed updating based on current overwrite setting.');
|
||||
}
|
||||
|
||||
$profile = sw_get_profile_by_id($db, (int)$profile['id']);
|
||||
sw_admin_edit_form($profile, $detected, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($action === 'edit' && isset($_GET['id'])) {
|
||||
$profile = sw_get_profile_by_id($db, (int)$_GET['id']);
|
||||
if ($profile) {
|
||||
sw_admin_edit_form($profile);
|
||||
} else {
|
||||
sw_error('Profile not found.');
|
||||
sw_admin_list($db);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sw_admin_list($db);
|
||||
}
|
||||
|
||||
function sw_admin_save_profile($db)
|
||||
{
|
||||
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||
if (!$id) {
|
||||
sw_error('Invalid profile ID.');
|
||||
sw_admin_list($db);
|
||||
return;
|
||||
}
|
||||
|
||||
$profile = sw_get_profile_by_id($db, $id);
|
||||
if (!$profile) {
|
||||
sw_error('Profile not found.');
|
||||
sw_admin_list($db);
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = array(
|
||||
'enabled' => isset($_POST['enabled']) ? 1 : 0,
|
||||
'steam_app_id' => trim($_POST['steam_app_id'] ?? ''),
|
||||
'workshop_app_id' => trim($_POST['workshop_app_id'] ?? ''),
|
||||
'steam_login_required' => isset($_POST['steam_login_required']) ? 1 : 0,
|
||||
'steamcmd_login_mode' => (($_POST['steamcmd_login_mode'] ?? 'anonymous') === 'account') ? 'account' : 'anonymous',
|
||||
'steamcmd_path' => trim($_POST['steamcmd_path'] ?? ''),
|
||||
'workshop_download_dir_template' => trim($_POST['workshop_download_dir_template'] ?? ''),
|
||||
'server_root_template' => trim($_POST['server_root_template'] ?? ''),
|
||||
'install_path_template' => trim($_POST['install_path_template'] ?? ''),
|
||||
'folder_naming_format' => trim($_POST['folder_naming_format'] ?? ''),
|
||||
'mod_launch_param_template' => trim($_POST['mod_launch_param_template'] ?? '-mod='),
|
||||
'servermod_launch_param_template' => trim($_POST['servermod_launch_param_template'] ?? '-serverMod='),
|
||||
'install_script_template' => trim($_POST['install_script_template'] ?? ''),
|
||||
'update_script_template' => trim($_POST['update_script_template'] ?? ''),
|
||||
'copy_bikeys_enabled' => isset($_POST['copy_bikeys_enabled']) ? 1 : 0,
|
||||
'notes' => trim($_POST['notes'] ?? ''),
|
||||
);
|
||||
|
||||
// Per-profile default behavior fields
|
||||
$valid_update_modes = array('manual', 'on_restart', 'before_start');
|
||||
$valid_restart_behaviors = array('none', 'if_stopped');
|
||||
$posted_um = $_POST['default_update_mode'] ?? 'manual';
|
||||
$posted_rb = $_POST['default_restart_behavior'] ?? 'none';
|
||||
$fields['default_update_mode'] = in_array($posted_um, $valid_update_modes, true) ? $posted_um : 'manual';
|
||||
$fields['default_restart_behavior'] = in_array($posted_rb, $valid_restart_behaviors, true) ? $posted_rb : 'none';
|
||||
|
||||
$setParts = array();
|
||||
foreach ($fields as $col => $val) {
|
||||
$setParts[] = "`$col` = '" . $db->realEscapeSingle($val) . "'";
|
||||
}
|
||||
$setParts[] = "`updated_at` = NOW()";
|
||||
|
||||
$ok = $db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_game_profiles') . "
|
||||
SET " . implode(', ', $setParts) . "
|
||||
WHERE `id` = $id LIMIT 1"
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
sw_success('Profile saved.');
|
||||
} else {
|
||||
sw_error('Failed to save profile.');
|
||||
}
|
||||
|
||||
$profile = sw_get_profile_by_id($db, $id);
|
||||
if ($profile) {
|
||||
sw_admin_edit_form($profile);
|
||||
} else {
|
||||
sw_admin_list($db);
|
||||
}
|
||||
}
|
||||
|
||||
function sw_admin_list($db)
|
||||
{
|
||||
$profiles = sw_get_profiles($db);
|
||||
?>
|
||||
<div class="sw-admin-panel">
|
||||
<p class="sw-muted">
|
||||
Profiles map game XML configs to Steam Workshop defaults. Sync to create missing profiles, then edit each profile for game-specific paths and launch templates.
|
||||
</p>
|
||||
|
||||
<form method="post" style="margin-bottom:12px;">
|
||||
<button type="submit" name="sync_profiles" value="1" class="button"
|
||||
onclick="return confirm('Sync workshop profiles from all game config XMLs?');">Sync Profiles from XML Configs</button>
|
||||
</form>
|
||||
|
||||
<?php if (empty($profiles)): ?>
|
||||
<p>No profiles yet. Click <em>Sync Profiles</em> to create them from installed game configs.</p>
|
||||
<?php else: ?>
|
||||
<table class="sw-admin-table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Config Name</th>
|
||||
<th>Game Name</th>
|
||||
<th style="text-align:center;">Steam App ID</th>
|
||||
<th style="text-align:center;">Workshop App ID</th>
|
||||
<th style="text-align:center;">Enabled</th>
|
||||
<th style="text-align:center;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($profiles as $p): ?>
|
||||
<tr>
|
||||
<td><code><?= sw_h($p['config_name']) ?></code></td>
|
||||
<td><?= sw_h($p['game_name']) ?></td>
|
||||
<td style="text-align:center;"><?= sw_h($p['steam_app_id']) ?></td>
|
||||
<td style="text-align:center;"><?= sw_h($p['workshop_app_id']) ?></td>
|
||||
<td style="text-align:center;"><?= $p['enabled'] ? '<span class="sw-state-on">Yes</span>' : '<span class="sw-state-off">No</span>' ?></td>
|
||||
<td style="text-align:center;"><a class="button small" href="home.php?m=steam_workshop&p=admin&action=edit&id=<?= (int)$p['id'] ?>">Edit</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
function sw_admin_edit_form(array $profile, array $detected = array(), $showDetectedBox = false)
|
||||
{
|
||||
$id = (int)$profile['id'];
|
||||
?>
|
||||
<p><a href="home.php?m=steam_workshop&p=admin">« Back to profile list</a></p>
|
||||
<h3>Edit Profile: <?= sw_h($profile['config_name']) ?> – <?= sw_h($profile['game_name']) ?></h3>
|
||||
|
||||
<div class="sw-admin-panel">
|
||||
<div class="sw-note">
|
||||
<strong>Placeholder tokens:</strong>
|
||||
<code>{SERVER_ROOT}</code> <code>{HOME_ID}</code> <code>{STEAM_APP_ID}</code> <code>{WORKSHOP_APP_ID}</code> <code>{MOD_FOLDER}</code>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>XML-Assisted Defaults</h4>
|
||||
<p class="sw-muted">Use values detected from this game XML. Existing values are not overwritten unless you explicitly allow it.</p>
|
||||
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>" style="display:inline-block; margin-right:8px;">
|
||||
<input type="hidden" name="id" value="<?= $id ?>">
|
||||
<button type="submit" name="detect_defaults" value="1" class="button">Detect from XML</button>
|
||||
</form>
|
||||
|
||||
<?php if ($showDetectedBox && !empty($detected)): ?>
|
||||
<div class="sw-detected-box">
|
||||
<strong>Detected values:</strong>
|
||||
<ul>
|
||||
<li>Steam App ID: <code><?= sw_h($detected['steam_app_id'] ?? '') ?></code></li>
|
||||
<li>Workshop App ID: <code><?= sw_h($detected['workshop_app_id'] ?? '') ?></code></li>
|
||||
<li>SteamCMD path: <code><?= sw_h($detected['steamcmd_path'] ?? '') ?></code></li>
|
||||
<li>Workshop download dir: <code><?= sw_h($detected['workshop_download_dir_template'] ?? '') ?></code></li>
|
||||
<li>Server root: <code><?= sw_h($detected['server_root_template'] ?? '') ?></code></li>
|
||||
<li>Mod install path: <code><?= sw_h($detected['install_path_template'] ?? '') ?></code></li>
|
||||
</ul>
|
||||
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>">
|
||||
<input type="hidden" name="id" value="<?= $id ?>">
|
||||
<label style="display:block;margin-bottom:8px;">
|
||||
<input type="checkbox" name="overwrite_existing" value="1">
|
||||
Allow overwrite of existing non-empty values.
|
||||
</label>
|
||||
<button type="submit" name="apply_detected_defaults" value="1" class="button">Refresh defaults from XML</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>">
|
||||
<input type="hidden" name="id" value="<?= $id ?>">
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Global Profile Defaults</h4>
|
||||
<div class="sw-grid">
|
||||
<label><span>Enabled</span><input type="checkbox" name="enabled" value="1" <?= $profile['enabled'] ? 'checked' : '' ?>></label>
|
||||
<label><span>Steam App ID</span><input type="text" name="steam_app_id" value="<?= sw_h($profile['steam_app_id']) ?>" placeholder="Detected from XML when available"></label>
|
||||
<label><span>Workshop App ID</span><input type="text" name="workshop_app_id" value="<?= sw_h($profile['workshop_app_id']) ?>" placeholder="Detected from XML when available"></label>
|
||||
<label><span>SteamCMD Path</span><input type="text" name="steamcmd_path" value="<?= sw_h($profile['steamcmd_path']) ?>" placeholder="/home/gameserver/steamcmd/steamcmd.sh"></label>
|
||||
<label><span>Steam Login Required</span><input type="checkbox" name="steam_login_required" value="1" <?= $profile['steam_login_required'] ? 'checked' : '' ?>></label>
|
||||
<label><span>SteamCMD Login Mode</span>
|
||||
<select name="steamcmd_login_mode">
|
||||
<option value="anonymous" <?= $profile['steamcmd_login_mode'] === 'anonymous' ? 'selected' : '' ?>>anonymous</option>
|
||||
<option value="account" <?= $profile['steamcmd_login_mode'] === 'account' ? 'selected' : '' ?>>account</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Path Templates</h4>
|
||||
<p class="sw-muted">Use placeholders so paths stay portable between server homes.</p>
|
||||
<div class="sw-grid">
|
||||
<label><span>Workshop Download Directory</span><input type="text" name="workshop_download_dir_template" value="<?= sw_h($profile['workshop_download_dir_template']) ?>" placeholder="{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}"></label>
|
||||
<label><span>Server Root</span><input type="text" name="server_root_template" value="<?= sw_h($profile['server_root_template']) ?>" placeholder="{SERVER_ROOT}"></label>
|
||||
<label><span>Mod Install Path</span><input type="text" name="install_path_template" value="<?= sw_h($profile['install_path_template']) ?>" placeholder="{SERVER_ROOT}/{MOD_FOLDER}"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Per-Game Runtime Values</h4>
|
||||
<div class="sw-grid">
|
||||
<label><span>Folder Naming Format</span><input type="text" name="folder_naming_format" value="<?= sw_h($profile['folder_naming_format']) ?>" placeholder="@{MOD_NAME}"></label>
|
||||
<label><span>Client Launch Param</span><input type="text" name="mod_launch_param_template" value="<?= sw_h($profile['mod_launch_param_template']) ?>" placeholder="-mod="></label>
|
||||
<label><span>Server Launch Param</span><input type="text" name="servermod_launch_param_template" value="<?= sw_h($profile['servermod_launch_param_template']) ?>" placeholder="-serverMod="></label>
|
||||
<label><span>Copy .bikey files</span><input type="checkbox" name="copy_bikeys_enabled" value="1" <?= $profile['copy_bikeys_enabled'] ? 'checked' : '' ?>></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Optional Script Templates</h4>
|
||||
<label><span>Install Script Template</span><textarea name="install_script_template" rows="6"><?= sw_h($profile['install_script_template']) ?></textarea></label>
|
||||
<label><span>Update Script Template</span><textarea name="update_script_template" rows="6"><?= sw_h($profile['update_script_template']) ?></textarea></label>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Notes</h4>
|
||||
<label><textarea name="notes" rows="4"><?= sw_h($profile['notes']) ?></textarea></label>
|
||||
</div>
|
||||
|
||||
<div class="sw-section">
|
||||
<h4>Default Workshop Behavior for New Servers</h4>
|
||||
<p class="sw-muted">
|
||||
These defaults are applied when a user enables Workshop on a server that has no saved behavior settings yet.
|
||||
Users can always override them on their own server pages.
|
||||
All defaults are intentionally set to the safest option (manual / no automatic restart).
|
||||
</p>
|
||||
<div class="sw-grid">
|
||||
<label>
|
||||
<span>Default Install / Update Mode</span>
|
||||
<select name="default_update_mode">
|
||||
<option value="manual" <?= (($profile['default_update_mode'] ?? 'manual') === 'manual') ? 'selected' : '' ?>>Manual only (safe default)</option>
|
||||
<option value="on_restart" <?= (($profile['default_update_mode'] ?? 'manual') === 'on_restart') ? 'selected' : '' ?>>On next restart</option>
|
||||
<option value="before_start" <?= (($profile['default_update_mode'] ?? 'manual') === 'before_start') ? 'selected' : '' ?>>Before every server start</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Default Restart Behavior</span>
|
||||
<select name="default_restart_behavior">
|
||||
<option value="none" <?= (($profile['default_restart_behavior'] ?? 'none') === 'none') ? 'selected' : '' ?>>Do not restart automatically (safe default)</option>
|
||||
<option value="if_stopped" <?= (($profile['default_restart_behavior'] ?? 'none') === 'if_stopped') ? 'selected' : '' ?>>Restart only if server is stopped</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<button type="submit" name="save_profile" value="1" class="button">Save Profile</button>
|
||||
<a href="home.php?m=steam_workshop&p=admin" class="button">Cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
function sw_admin_print_styles()
|
||||
{
|
||||
static $printed = false;
|
||||
if ($printed) {
|
||||
return;
|
||||
}
|
||||
$printed = true;
|
||||
echo '<style>
|
||||
.sw-admin-panel{background:#171717;border:1px solid #2d2d2d;border-radius:6px;padding:14px;margin:10px 0;color:#e7e7e7}
|
||||
.sw-admin-table{border-collapse:collapse;background:#121212}
|
||||
.sw-admin-table th,.sw-admin-table td{border:1px solid #2c2c2c;padding:8px}
|
||||
.sw-admin-table thead th{background:#232323;color:#fff}
|
||||
.sw-state-on{color:#78d978;font-weight:700}
|
||||
.sw-state-off{color:#9a9a9a}
|
||||
.sw-section{margin-top:14px;padding:12px;border:1px solid #2f2f2f;border-radius:4px;background:#111}
|
||||
.sw-section h4{margin:0 0 8px 0;color:#f6f6f6}
|
||||
.sw-note{margin-bottom:10px;background:#202020;border-left:3px solid #3f80d0;padding:10px}
|
||||
.sw-muted{color:#b3b3b3}
|
||||
.sw-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:10px}
|
||||
.sw-grid label, .sw-section > label{display:block}
|
||||
.sw-grid span, .sw-section span{display:block;font-size:12px;color:#bdbdbd;margin-bottom:4px}
|
||||
.sw-grid input[type=text], .sw-grid select, .sw-section textarea{width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #3a3a3a;color:#eee;padding:7px;border-radius:4px}
|
||||
.sw-grid input[type=checkbox]{transform:scale(1.1);margin-top:4px}
|
||||
.sw-detected-box{margin-top:10px;padding:10px;background:#1d2a1d;border:1px solid #335933;border-radius:4px}
|
||||
.sw-detected-box ul{margin:8px 0 10px 18px}
|
||||
.sw-detected-box code,.sw-note code{background:#0b0b0b;padding:1px 4px;border-radius:3px;color:#9fd4ff}
|
||||
</style>';
|
||||
}
|
||||
|
|
@ -1,569 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: Agent CLI update script
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* This file must only be run from the command line (CLI).
|
||||
* Do NOT expose it through a web server.
|
||||
*
|
||||
* Usage:
|
||||
* php agent_update_workshop.php --home-id=123
|
||||
* php agent_update_workshop.php --all
|
||||
*
|
||||
* The script connects to the panel database, reads the list of enabled mods
|
||||
* for the specified server(s), downloads/updates each mod via SteamCMD,
|
||||
* copies mod folders into the server root, copies .bikey files into the
|
||||
* server keys/ directory, and updates the install status in the database.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
// ── Safety: CLI only ──────────────────────────────────────────────────────
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
exit("This script may only be run from the command line.\n");
|
||||
}
|
||||
|
||||
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
||||
$panel_root = realpath(__DIR__ . '/../../..');
|
||||
if (!$panel_root || !is_dir($panel_root)) {
|
||||
fwrite(STDERR, "ERROR: Cannot locate panel root from " . __DIR__ . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$config_file = $panel_root . '/includes/config.inc.php';
|
||||
if (!is_file($config_file)) {
|
||||
fwrite(STDERR, "ERROR: Panel config not found: $config_file\n");
|
||||
fwrite(STDERR, " Copy includes/config.inc.php.example to includes/config.inc.php and set credentials.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require_once $config_file;
|
||||
require_once $panel_root . '/includes/helpers.php';
|
||||
require_once $panel_root . '/includes/database_mysqli.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
// ── Database connection ───────────────────────────────────────────────────
|
||||
// Variables $db_host, $db_user, $db_pass, $db_name, $table_prefix, $db_type,
|
||||
// $db_port come from config.inc.php (loaded above).
|
||||
$db = createDatabaseConnection(
|
||||
$db_type,
|
||||
$db_host,
|
||||
$db_user,
|
||||
$db_pass,
|
||||
$db_name,
|
||||
$table_prefix,
|
||||
isset($db_port) ? $db_port : null
|
||||
);
|
||||
|
||||
if (!is_object($db)) {
|
||||
$error_text = '';
|
||||
get_db_error_text($db, $error_text);
|
||||
fwrite(STDERR, "ERROR: Database connection failed: $error_text\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ── Argument parsing ──────────────────────────────────────────────────────
|
||||
$opts = getopt('', array('home-id:', 'all', 'dry-run'));
|
||||
|
||||
$do_all = array_key_exists('all', $opts);
|
||||
$dry_run = array_key_exists('dry-run', $opts);
|
||||
$target_id = isset($opts['home-id']) ? (int)$opts['home-id'] : 0;
|
||||
|
||||
if (!$do_all && !$target_id) {
|
||||
fwrite(STDERR, "Usage:\n");
|
||||
fwrite(STDERR, " php agent_update_workshop.php --home-id=123\n");
|
||||
fwrite(STDERR, " php agent_update_workshop.php --all\n");
|
||||
fwrite(STDERR, " Add --dry-run to simulate without running SteamCMD or copying files.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($dry_run) {
|
||||
echo "[DRY RUN] No files will be modified and SteamCMD will not be called.\n";
|
||||
}
|
||||
|
||||
// ── Collect home IDs to process ───────────────────────────────────────────
|
||||
if ($do_all) {
|
||||
// Find all home_ids that have at least one enabled queued mod with an enabled profile.
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT DISTINCT m.home_id
|
||||
FROM " . sw_table('steam_workshop_server_mods') . " m
|
||||
JOIN " . sw_table('steam_workshop_game_profiles') . " p ON p.id = m.profile_id
|
||||
WHERE m.enabled = 1 AND p.enabled = 1 AND m.install_status = 'queued'"
|
||||
);
|
||||
$home_ids = $rows ? array_column($rows, 'home_id') : array();
|
||||
} else {
|
||||
$home_ids = array($target_id);
|
||||
}
|
||||
|
||||
if (empty($home_ids)) {
|
||||
echo "No servers with enabled Workshop mods found.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$overall_success = true;
|
||||
|
||||
foreach ($home_ids as $home_id) {
|
||||
$home_id = (int)$home_id;
|
||||
echo "\n=== Processing home_id=$home_id ===\n";
|
||||
|
||||
$ok = sw_agent_process_home($db, $home_id, $dry_run);
|
||||
if (!$ok) {
|
||||
$overall_success = false;
|
||||
echo " [WARN] One or more errors occurred for home_id=$home_id.\n";
|
||||
}
|
||||
}
|
||||
|
||||
exit($overall_success ? 0 : 1);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Core logic
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Process all enabled mods for one server home.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @param bool $dry_run
|
||||
* @return bool true if all mods processed without errors
|
||||
*/
|
||||
function sw_agent_process_home($db, $home_id, $dry_run)
|
||||
{
|
||||
// Load server info
|
||||
$home = sw_get_home_info($db, $home_id);
|
||||
if (!$home) {
|
||||
echo " [ERROR] Server home $home_id not found in database.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
echo " Server: " . $home['home_name'] . " (game: " . $home['game_name'] . ")\n";
|
||||
echo " Path: " . $home['home_path'] . "\n";
|
||||
|
||||
// Resolve Workshop profile via game config key
|
||||
$profile = sw_get_profile_for_home($db, $home_id);
|
||||
if (!$profile) {
|
||||
echo " [SKIP] No enabled Workshop profile for this game type.\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
echo " Profile: " . $profile['config_name'] . " (workshop_app_id=" . $profile['workshop_app_id'] . ")\n";
|
||||
|
||||
// Build common template variables
|
||||
$server_root = sw_apply_template(
|
||||
$profile['server_root_template'] ?: $home['home_path'],
|
||||
sw_agent_tpl_vars($home, $profile)
|
||||
);
|
||||
$server_root = rtrim($server_root, '/');
|
||||
|
||||
// Load queued+enabled mods, sorted by sort_order
|
||||
$mods = sw_agent_get_queued_mods($db, $home_id);
|
||||
|
||||
if (empty($mods)) {
|
||||
echo " No queued Workshop updates for this server.\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
$keys_dir = $server_root . '/keys';
|
||||
if (!$dry_run && !is_dir($keys_dir)) {
|
||||
@mkdir($keys_dir, 0755, true);
|
||||
}
|
||||
|
||||
$all_ok = true;
|
||||
|
||||
foreach ($mods as $mod) {
|
||||
$mod_id = (int)$mod['id'];
|
||||
$workshop_id = $mod['workshop_id'];
|
||||
echo "\n Mod: " . ($mod['mod_name'] ?: $workshop_id) . " [ID=$workshop_id]\n";
|
||||
|
||||
// Mark as updating
|
||||
if (!$dry_run) {
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'updating', `last_error` = NULL, `updated_at` = NOW()
|
||||
WHERE `id` = $mod_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
// Build template vars for this mod
|
||||
$folder_name = $mod['folder_name'] ?: ('@' . $workshop_id);
|
||||
$tpl_vars = array_merge(
|
||||
sw_agent_tpl_vars($home, $profile),
|
||||
array(
|
||||
'WORKSHOP_ID' => $workshop_id,
|
||||
'MOD_NAME' => $mod['mod_name'] ?: $workshop_id,
|
||||
'FOLDER_NAME' => $folder_name,
|
||||
'MOD_FOLDER' => $folder_name,
|
||||
'SERVER_ROOT' => $server_root,
|
||||
'WORKSHOP_DOWNLOAD_DIR' => sw_apply_template(
|
||||
$profile['workshop_download_dir_template']
|
||||
?: ($server_root . '/steamapps/workshop/content/' . $profile['workshop_app_id']),
|
||||
array(
|
||||
'SERVER_ROOT' => $server_root,
|
||||
'WORKSHOP_APP_ID' => $profile['workshop_app_id'],
|
||||
'HOME_ID' => $home['home_id'],
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$download_dir = $tpl_vars['WORKSHOP_DOWNLOAD_DIR'];
|
||||
$mod_cache = rtrim($download_dir, '/') . '/' . $workshop_id;
|
||||
|
||||
// 1. Download / update via SteamCMD
|
||||
$cmd_result = sw_agent_steamcmd_download($mod, $profile, $tpl_vars, $dry_run);
|
||||
if (!$cmd_result['ok']) {
|
||||
$err = $cmd_result['error'];
|
||||
echo " [ERROR] SteamCMD failed: $err\n";
|
||||
if (!$dry_run) {
|
||||
$safe_err = $db->realEscapeSingle($err);
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'failed', `last_error` = '$safe_err', `updated_at` = NOW()
|
||||
WHERE `id` = $mod_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
$all_ok = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Copy / sync mod folder to server root
|
||||
$install_path = sw_apply_template(
|
||||
$profile['install_path_template'] ?: ($server_root . '/{MOD_FOLDER}'),
|
||||
$tpl_vars
|
||||
);
|
||||
|
||||
$copy_ok = sw_agent_copy_mod($mod_cache, $install_path, $dry_run);
|
||||
if (!$copy_ok) {
|
||||
$err = "Failed to copy mod from $mod_cache to $install_path";
|
||||
echo " [ERROR] $err\n";
|
||||
if (!$dry_run) {
|
||||
$safe_err = $db->realEscapeSingle($err);
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'failed', `last_error` = '$safe_err', `updated_at` = NOW()
|
||||
WHERE `id` = $mod_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
$all_ok = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Copy .bikey files to server keys/ directory
|
||||
if (!empty($profile['copy_bikeys_enabled'])) {
|
||||
sw_agent_copy_bikeys($install_path, $keys_dir, $dry_run);
|
||||
}
|
||||
|
||||
// 4. Mark as installed
|
||||
if (!$dry_run) {
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'installed',
|
||||
`last_installed_at` = NOW(),
|
||||
`last_updated_at` = NOW(),
|
||||
`last_error` = NULL,
|
||||
`updated_at` = NOW()
|
||||
WHERE `id` = $mod_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
echo " [OK] Installed → $install_path\n";
|
||||
}
|
||||
|
||||
return $all_ok;
|
||||
}
|
||||
|
||||
function sw_agent_get_queued_mods($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `home_id` = $home_id
|
||||
AND `enabled` = 1
|
||||
AND `install_status` = 'queued'
|
||||
ORDER BY `sort_order` ASC, `id` ASC"
|
||||
);
|
||||
return $rows ? $rows : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the standard template variable map for a given home + profile.
|
||||
*
|
||||
* @param array $home
|
||||
* @param array $profile
|
||||
* @return array
|
||||
*/
|
||||
function sw_agent_tpl_vars(array $home, array $profile)
|
||||
{
|
||||
return array(
|
||||
'HOME_ID' => $home['home_id'],
|
||||
'SERVER_ID' => $home['home_id'],
|
||||
'REMOTE_SERVER_ID' => $home['remote_server_id'],
|
||||
'GAME_NAME' => $home['game_name'],
|
||||
'CONFIG_NAME' => $home['game_key'],
|
||||
'STEAM_APP_ID' => $profile['steam_app_id'],
|
||||
'WORKSHOP_APP_ID' => $profile['workshop_app_id'],
|
||||
'STEAMCMD_PATH' => $profile['steamcmd_path'],
|
||||
'SERVER_ROOT' => $home['home_path'],
|
||||
'INSTALL_PATH' => $home['home_path'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run SteamCMD to download / update a single Workshop item.
|
||||
*
|
||||
* Uses the profile's update_script_template if set; otherwise falls back to
|
||||
* a standard anonymous or authenticated +workshop_download_item invocation.
|
||||
*
|
||||
* @param array $mod
|
||||
* @param array $profile
|
||||
* @param array $tpl_vars
|
||||
* @param bool $dry_run
|
||||
* @return array ['ok' => bool, 'error' => string]
|
||||
*/
|
||||
function sw_agent_steamcmd_download(array $mod, array $profile, array $tpl_vars, $dry_run)
|
||||
{
|
||||
$steamcmd = $profile['steamcmd_path'] ?: '/home/gameserver/steamcmd/steamcmd.sh';
|
||||
$workshop_id = $mod['workshop_id'];
|
||||
$app_id = $profile['workshop_app_id'];
|
||||
$dl_dir = $tpl_vars['WORKSHOP_DOWNLOAD_DIR'];
|
||||
|
||||
if (!empty($profile['update_script_template'])) {
|
||||
// Admin has provided a custom update script.
|
||||
$script_body = sw_apply_template($profile['update_script_template'], $tpl_vars);
|
||||
return sw_agent_run_script($script_body, $dry_run);
|
||||
}
|
||||
|
||||
// Build default SteamCMD command.
|
||||
// +force_install_dir is set to the parent of the workshop content so that
|
||||
// SteamCMD places files in <dl_dir>/<workshop_id>/.
|
||||
$parent_dir = dirname($dl_dir); // .../steamapps/workshop/content
|
||||
|
||||
if ($profile['steamcmd_login_mode'] === 'account') {
|
||||
// When account login is required the operator must supply credentials
|
||||
// in the update_script_template. We cannot safely store a password here.
|
||||
return array(
|
||||
'ok' => false,
|
||||
'error' => "Account login is required for this profile but no update_script_template is set. "
|
||||
. "Add a custom update_script_template in the admin profile that includes SteamCMD login credentials.",
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that steamcmd exists
|
||||
if (!$dry_run) {
|
||||
if (!is_file($steamcmd)) {
|
||||
return array('ok' => false, 'error' => "SteamCMD not found: $steamcmd");
|
||||
}
|
||||
if (!is_executable($steamcmd)) {
|
||||
return array('ok' => false, 'error' => "SteamCMD is not executable: $steamcmd");
|
||||
}
|
||||
}
|
||||
|
||||
// Build argument list; escape each argument individually.
|
||||
$args = array(
|
||||
escapeshellarg($steamcmd),
|
||||
'+force_install_dir', escapeshellarg($parent_dir),
|
||||
'+login', 'anonymous',
|
||||
'+workshop_download_item', escapeshellarg($app_id), escapeshellarg($workshop_id),
|
||||
'+quit',
|
||||
);
|
||||
$cmd = implode(' ', $args);
|
||||
|
||||
echo " SteamCMD: $cmd\n";
|
||||
|
||||
if ($dry_run) {
|
||||
echo " [DRY RUN] Skipping SteamCMD execution.\n";
|
||||
return array('ok' => true, 'error' => '');
|
||||
}
|
||||
|
||||
$output = array();
|
||||
$return_var = 0;
|
||||
exec($cmd . ' 2>&1', $output, $return_var);
|
||||
|
||||
foreach ($output as $line) {
|
||||
echo " $line\n";
|
||||
}
|
||||
|
||||
if ($return_var !== 0) {
|
||||
return array(
|
||||
'ok' => false,
|
||||
'error' => "SteamCMD exited with code $return_var. " . implode(' ', array_slice($output, -3)),
|
||||
);
|
||||
}
|
||||
|
||||
return array('ok' => true, 'error' => '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a shell script body.
|
||||
* The script is written to a temporary file and executed with /bin/sh.
|
||||
*
|
||||
* @param string $script_body
|
||||
* @param bool $dry_run
|
||||
* @return array ['ok' => bool, 'error' => string]
|
||||
*/
|
||||
function sw_agent_run_script($script_body, $dry_run)
|
||||
{
|
||||
if ($dry_run) {
|
||||
echo " [DRY RUN] Would execute script:\n";
|
||||
foreach (explode("\n", $script_body) as $line) {
|
||||
echo " $line\n";
|
||||
}
|
||||
return array('ok' => true, 'error' => '');
|
||||
}
|
||||
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'sw_agent_');
|
||||
if (!$tmp) {
|
||||
return array('ok' => false, 'error' => 'Could not create temporary file for script.');
|
||||
}
|
||||
|
||||
file_put_contents($tmp, "#!/bin/sh\nset -e\n" . $script_body);
|
||||
chmod($tmp, 0700);
|
||||
|
||||
$output = array();
|
||||
$return_var = 0;
|
||||
exec('/bin/sh ' . escapeshellarg($tmp) . ' 2>&1', $output, $return_var);
|
||||
@unlink($tmp);
|
||||
|
||||
foreach ($output as $line) {
|
||||
echo " $line\n";
|
||||
}
|
||||
|
||||
if ($return_var !== 0) {
|
||||
return array(
|
||||
'ok' => false,
|
||||
'error' => "Script exited with code $return_var. " . implode(' ', array_slice($output, -3)),
|
||||
);
|
||||
}
|
||||
|
||||
return array('ok' => true, 'error' => '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy (rsync-style) the downloaded mod folder into the server root.
|
||||
* Uses rsync when available, falls back to recursive PHP copy.
|
||||
*
|
||||
* @param string $src Downloaded mod folder (e.g. .../content/221100/2863534533)
|
||||
* @param string $dst Target path in server root (e.g. /servers/123/@CF)
|
||||
* @param bool $dry_run
|
||||
* @return bool
|
||||
*/
|
||||
function sw_agent_copy_mod($src, $dst, $dry_run)
|
||||
{
|
||||
echo " Copy: $src → $dst\n";
|
||||
|
||||
if (!$dry_run && !is_dir($src)) {
|
||||
echo " [WARN] Source directory not found: $src\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($dry_run) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_dir($dst)) {
|
||||
if (!@mkdir($dst, 0755, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Try rsync first (preserves permissions, handles deletes cleanly).
|
||||
if (sw_agent_cmd_exists('rsync')) {
|
||||
$cmd = 'rsync -a --delete '
|
||||
. escapeshellarg(rtrim($src, '/') . '/') . ' '
|
||||
. escapeshellarg(rtrim($dst, '/') . '/') . ' 2>&1';
|
||||
exec($cmd, $out, $ret);
|
||||
if ($ret === 0) {
|
||||
return true;
|
||||
}
|
||||
echo " [WARN] rsync failed (exit $ret); falling back to PHP copy.\n";
|
||||
}
|
||||
|
||||
// PHP recursive copy fallback.
|
||||
return sw_agent_recursive_copy($src, $dst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all .bikey files found recursively under $mod_dir/keys/ into $keys_dir.
|
||||
*
|
||||
* @param string $mod_dir Installed mod directory
|
||||
* @param string $keys_dir Server keys directory
|
||||
* @param bool $dry_run
|
||||
* @return void
|
||||
*/
|
||||
function sw_agent_copy_bikeys($mod_dir, $keys_dir, $dry_run)
|
||||
{
|
||||
// Search in common key locations within the mod folder.
|
||||
$search_dirs = array(
|
||||
$mod_dir . '/keys',
|
||||
$mod_dir . '/Keys',
|
||||
$mod_dir . '/key',
|
||||
$mod_dir . '/Key',
|
||||
);
|
||||
|
||||
$found = 0;
|
||||
foreach ($search_dirs as $kdir) {
|
||||
if (!is_dir($kdir)) {
|
||||
continue;
|
||||
}
|
||||
foreach (glob($kdir . '/*.bikey') as $bikey) {
|
||||
$target = $keys_dir . '/' . basename($bikey);
|
||||
echo " .bikey: " . basename($bikey) . " → $keys_dir/\n";
|
||||
if (!$dry_run) {
|
||||
@copy($bikey, $target);
|
||||
}
|
||||
$found++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($found === 0) {
|
||||
echo " (no .bikey files found in mod keys/ folder)\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if a command is available in PATH.
|
||||
*
|
||||
* @param string $cmd
|
||||
* @return bool
|
||||
*/
|
||||
function sw_agent_cmd_exists($cmd)
|
||||
{
|
||||
$which = trim((string)shell_exec('which ' . escapeshellarg($cmd) . ' 2>/dev/null'));
|
||||
return !empty($which);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copy directory $src into $dst (creating $dst if needed).
|
||||
*
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
function sw_agent_recursive_copy($src, $dst)
|
||||
{
|
||||
$dir = @opendir($src);
|
||||
if (!$dir) {
|
||||
return false;
|
||||
}
|
||||
if (!is_dir($dst)) {
|
||||
@mkdir($dst, 0755, true);
|
||||
}
|
||||
while (false !== ($file = readdir($dir))) {
|
||||
if ($file === '.' || $file === '..') {
|
||||
continue;
|
||||
}
|
||||
$s = $src . '/' . $file;
|
||||
$d = $dst . '/' . $file;
|
||||
if (is_dir($s)) {
|
||||
sw_agent_recursive_copy($s, $d);
|
||||
} else {
|
||||
copy($s, $d);
|
||||
}
|
||||
}
|
||||
closedir($dir);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,652 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: shared helper functions
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
function sw_db_prefix()
|
||||
{
|
||||
if (defined('DB_PREFIX') && DB_PREFIX !== '') {
|
||||
return DB_PREFIX;
|
||||
}
|
||||
if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') {
|
||||
return $GLOBALS['db_prefix'];
|
||||
}
|
||||
if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') {
|
||||
return $GLOBALS['table_prefix'];
|
||||
}
|
||||
return 'gsp_';
|
||||
}
|
||||
|
||||
function sw_table($tableName)
|
||||
{
|
||||
return '`' . sw_db_prefix() . $tableName . '`';
|
||||
}
|
||||
|
||||
// ── Profile helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return all rows from steam_workshop_game_profiles ordered by game_name.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_profiles($db)
|
||||
{
|
||||
return $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_game_profiles') . "
|
||||
ORDER BY `game_name` ASC, `config_name` ASC"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single profile row by primary key.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $id
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_profile_by_id($db, $id)
|
||||
{
|
||||
$id = (int)$id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_game_profiles') . "
|
||||
WHERE `id` = $id LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single profile row by config_name (= game_key from XML).
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param string $config_name
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_profile_by_config_name($db, $config_name)
|
||||
{
|
||||
$safe = $db->realEscapeSingle($config_name);
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_game_profiles') . "
|
||||
WHERE `config_name` = '$safe' LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Workshop profile that applies to a server home.
|
||||
* Resolves: home_id → config_homes.game_key → workshop profile.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @return array|false profile row or false when none found / not enabled
|
||||
*/
|
||||
function sw_get_profile_for_home($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT p.*
|
||||
FROM " . sw_table('steam_workshop_game_profiles') . " p
|
||||
JOIN " . sw_table('config_homes') . " c
|
||||
ON c.`game_key` = p.`config_name`
|
||||
JOIN " . sw_table('server_homes') . " s
|
||||
ON s.`home_cfg_id` = c.`home_cfg_id`
|
||||
WHERE s.`home_id` = $home_id
|
||||
AND p.`enabled` = 1
|
||||
LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
// ── Mod helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return all mods for a server, sorted by sort_order ASC.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_server_mods($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
return $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `home_id` = $home_id
|
||||
ORDER BY `sort_order` ASC, `id` ASC"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single mod row by primary key.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $id
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_mod_by_id($db, $id)
|
||||
{
|
||||
$id = (int)$id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `id` = $id LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
// ── Server / ownership helpers ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return server_homes row joined with config_homes and remote_servers
|
||||
* for the given home_id, or false when not found.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @return array|false
|
||||
*/
|
||||
function sw_get_home_info($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT s.*, c.`game_key`, c.`game_name`, r.`agent_ip`, r.`agent_port`
|
||||
FROM " . sw_table('server_homes') . " s
|
||||
JOIN " . sw_table('config_homes') . " c ON c.`home_cfg_id` = s.`home_cfg_id`
|
||||
JOIN " . sw_table('remote_servers') . " r ON r.`remote_server_id` = s.`remote_server_id`
|
||||
WHERE s.`home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
return ($rows && isset($rows[0])) ? $rows[0] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the current session user is allowed to manage this home.
|
||||
* Admins always pass. Regular users/subusers must have an entry in
|
||||
* user_homes (or be the user_id_main).
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $user_id
|
||||
* @param int $home_id
|
||||
* @return bool
|
||||
*/
|
||||
function sw_user_owns_home($db, $user_id, $home_id)
|
||||
{
|
||||
if (!isset($_SESSION['users_group'])) {
|
||||
return false;
|
||||
}
|
||||
if ($_SESSION['users_group'] === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$user_id = (int)$user_id;
|
||||
$home_id = (int)$home_id;
|
||||
|
||||
// Direct owner
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT 1 FROM " . sw_table('server_homes') . "
|
||||
WHERE `home_id` = $home_id AND `user_id_main` = $user_id LIMIT 1"
|
||||
);
|
||||
if ($rows) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Assigned via user_homes
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT 1 FROM " . sw_table('user_homes') . "
|
||||
WHERE `home_id` = $home_id AND `user_id` = $user_id LIMIT 1"
|
||||
);
|
||||
if ($rows) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Assigned via group
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT 1 FROM " . sw_table('user_group_homes') . " ugh
|
||||
JOIN " . sw_table('user_groups') . " ug ON ug.`group_id` = ugh.`group_id`
|
||||
WHERE ugh.`home_id` = $home_id AND ug.`user_id` = $user_id LIMIT 1"
|
||||
);
|
||||
return (bool)$rows;
|
||||
}
|
||||
|
||||
// ── Game-config helpers ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return an array of all game configs from the XML files.
|
||||
* Each element is a SimpleXMLElement (game_config root node).
|
||||
*
|
||||
* @return SimpleXMLElement[]
|
||||
*/
|
||||
function sw_get_all_game_configs()
|
||||
{
|
||||
if (!defined('SERVER_CONFIG_LOCATION')) {
|
||||
// server_config_parser.php defines this; load it if not already done.
|
||||
if (file_exists(__DIR__ . '/../../config_games/server_config_parser.php')) {
|
||||
require_once __DIR__ . '/../../config_games/server_config_parser.php';
|
||||
} else {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
$configs = array();
|
||||
foreach (glob(SERVER_CONFIG_LOCATION . '*.xml') as $file) {
|
||||
$xml = read_server_config($file);
|
||||
if ($xml !== false) {
|
||||
$configs[] = $xml;
|
||||
}
|
||||
}
|
||||
return $configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure every game config has a matching row in steam_workshop_game_profiles.
|
||||
* Only creates rows that are missing; never overwrites existing data.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @return int number of new rows inserted
|
||||
*/
|
||||
function sw_sync_profiles($db)
|
||||
{
|
||||
$configs = sw_get_all_game_configs();
|
||||
$created = 0;
|
||||
|
||||
foreach ($configs as $xml) {
|
||||
$config_name = (string)$xml->game_key;
|
||||
$game_name = (string)$xml->game_name;
|
||||
|
||||
if (empty($config_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = sw_get_profile_by_config_name($db, $config_name);
|
||||
if ($existing) {
|
||||
continue; // already have a profile for this game config
|
||||
}
|
||||
|
||||
$safe_config = $db->realEscapeSingle($config_name);
|
||||
$safe_name = $db->realEscapeSingle($game_name);
|
||||
$ok = $db->query("INSERT IGNORE INTO " . sw_table('steam_workshop_game_profiles') . " (`config_name`, `game_name`, `enabled`) VALUES ('$safe_config', '$safe_name', 0)");
|
||||
if ($ok) {
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
return $created;
|
||||
}
|
||||
|
||||
// ── Template / launch-param helpers ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Replace {PLACEHOLDER} tokens in $template with values from $vars.
|
||||
* Unknown tokens are left intact so admins can spot missing values.
|
||||
*
|
||||
* @param string $template
|
||||
* @param array $vars associative: 'PLACEHOLDER' => 'value'
|
||||
* @return string
|
||||
*/
|
||||
function sw_apply_template($template, array $vars)
|
||||
{
|
||||
$search = array();
|
||||
$replace = array();
|
||||
foreach ($vars as $key => $value) {
|
||||
$search[] = '{' . $key . '}';
|
||||
$replace[] = (string)$value;
|
||||
}
|
||||
return str_replace($search, $replace, $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the -mod= and -serverMod= launch parameter strings from an ordered
|
||||
* list of enabled mods and the game profile.
|
||||
*
|
||||
* Returns an associative array:
|
||||
* 'mod' => '-mod=@Mod1;@Mod2' (client mods)
|
||||
* 'servermod' => '-serverMod=@ServerOnly' (server-side mods)
|
||||
* 'combined' => '-mod=... -serverMod=...' (ready-to-paste)
|
||||
*
|
||||
* @param array $mods rows from steam_workshop_server_mods (must be pre-filtered
|
||||
* for enabled = 1 and sorted by sort_order)
|
||||
* @param array $profile row from steam_workshop_game_profiles
|
||||
* @return array
|
||||
*/
|
||||
function sw_generate_launch_params(array $mods, array $profile)
|
||||
{
|
||||
$mod_param = trim($profile['mod_launch_param_template'] ?? '-mod=');
|
||||
$servermod_param = trim($profile['servermod_launch_param_template'] ?? '-serverMod=');
|
||||
|
||||
$client_folders = array();
|
||||
$server_folders = array();
|
||||
|
||||
foreach ($mods as $mod) {
|
||||
if (empty($mod['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$folder = !empty($mod['folder_name']) ? $mod['folder_name'] : ('@' . $mod['workshop_id']);
|
||||
if ($mod['mod_type'] === 'server') {
|
||||
$server_folders[] = $folder;
|
||||
} else {
|
||||
$client_folders[] = $folder;
|
||||
}
|
||||
}
|
||||
|
||||
$mod_str = $client_folders ? ($mod_param . implode(';', $client_folders)) : '';
|
||||
$servermod_str = $server_folders ? ($servermod_param . implode(';', $server_folders)) : '';
|
||||
$combined = trim($mod_str . ' ' . $servermod_str);
|
||||
|
||||
return array(
|
||||
'mod' => $mod_str,
|
||||
'servermod' => $servermod_str,
|
||||
'combined' => $combined,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Server behavior settings helpers ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return the workshop behavior settings row for a home, or an array of
|
||||
* safe defaults when no row exists yet.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @return array
|
||||
*/
|
||||
function sw_get_server_settings($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM " . sw_table('steam_workshop_server_settings') . "
|
||||
WHERE `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
if ($rows && isset($rows[0]) && is_array($rows[0])) {
|
||||
$settings = $rows[0];
|
||||
|
||||
// Runtime normalization is kept as a fallback for legacy/manual rows that
|
||||
// were not updated via module migrations.
|
||||
$legacyUpdateMap = array(
|
||||
'scheduled' => 'manual',
|
||||
);
|
||||
$legacyRestartMap = array(
|
||||
'if_empty' => 'if_stopped',
|
||||
'next_restart' => 'if_stopped',
|
||||
'immediate' => 'none',
|
||||
);
|
||||
$legacyScheduleMap = array(
|
||||
'hourly' => 'daily',
|
||||
);
|
||||
|
||||
if (isset($legacyUpdateMap[$settings['update_mode'] ?? ''])) {
|
||||
$settings['update_mode'] = $legacyUpdateMap[$settings['update_mode']];
|
||||
}
|
||||
if (isset($legacyRestartMap[$settings['restart_behavior'] ?? ''])) {
|
||||
$settings['restart_behavior'] = $legacyRestartMap[$settings['restart_behavior']];
|
||||
}
|
||||
if (isset($legacyScheduleMap[$settings['schedule_interval'] ?? ''])) {
|
||||
$settings['schedule_interval'] = $legacyScheduleMap[$settings['schedule_interval']];
|
||||
}
|
||||
|
||||
$validUpdateModes = array('manual', 'on_restart', 'before_start');
|
||||
$validRestartBehaviors = array('none', 'if_stopped');
|
||||
$validIntervals = array('disabled', 'daily', 'weekly');
|
||||
|
||||
if (!in_array($settings['update_mode'] ?? '', $validUpdateModes, true)) {
|
||||
$settings['update_mode'] = 'manual';
|
||||
}
|
||||
if (!in_array($settings['restart_behavior'] ?? '', $validRestartBehaviors, true)) {
|
||||
$settings['restart_behavior'] = 'none';
|
||||
}
|
||||
if (!in_array($settings['schedule_interval'] ?? '', $validIntervals, true)) {
|
||||
$settings['schedule_interval'] = 'disabled';
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
// Safe defaults – manual only, no automatic restarts, schedule disabled
|
||||
return array(
|
||||
'home_id' => $home_id,
|
||||
'update_mode' => 'manual',
|
||||
'restart_behavior' => 'none',
|
||||
'schedule_interval' => 'disabled',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert the workshop behavior settings for a server home.
|
||||
*
|
||||
* @param OGPDatabase $db
|
||||
* @param int $home_id
|
||||
* @param array $data keys: update_mode, restart_behavior, schedule_interval
|
||||
* @return bool
|
||||
*/
|
||||
function sw_save_server_settings($db, $home_id, array $data)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
|
||||
$valid_update_modes = array('manual', 'on_restart', 'before_start');
|
||||
$valid_restart_behaviors = array('none', 'if_stopped');
|
||||
$valid_intervals = array('disabled', 'daily', 'weekly');
|
||||
|
||||
$update_mode = in_array($data['update_mode'] ?? '', $valid_update_modes, true) ? $data['update_mode'] : 'manual';
|
||||
$restart_behavior = in_array($data['restart_behavior'] ?? '', $valid_restart_behaviors, true) ? $data['restart_behavior'] : 'none';
|
||||
$schedule_interval = in_array($data['schedule_interval'] ?? '', $valid_intervals, true) ? $data['schedule_interval'] : 'disabled';
|
||||
|
||||
$safe_um = $db->realEscapeSingle($update_mode);
|
||||
$safe_rb = $db->realEscapeSingle($restart_behavior);
|
||||
$safe_si = $db->realEscapeSingle($schedule_interval);
|
||||
|
||||
return (bool)$db->query(
|
||||
"INSERT INTO " . sw_table('steam_workshop_server_settings') . "
|
||||
(`home_id`, `update_mode`, `restart_behavior`, `hot_load`,
|
||||
`warning_minutes`, `schedule_interval`, `created_at`, `updated_at`)
|
||||
VALUES ($home_id, '$safe_um', '$safe_rb', 'disabled',
|
||||
0, '$safe_si', NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`update_mode` = '$safe_um',
|
||||
`restart_behavior` = '$safe_rb',
|
||||
`hot_load` = 'disabled',
|
||||
`warning_minutes` = 0,
|
||||
`schedule_interval` = '$safe_si',
|
||||
`updated_at` = NOW()"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Output helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Render a short inline success banner.
|
||||
*
|
||||
* @param string $msg
|
||||
* @return void
|
||||
*/
|
||||
function sw_success($msg)
|
||||
{
|
||||
echo '<div style="background:#d4edda;border:1px solid #c3e6cb;color:#155724;padding:8px 12px;margin:8px 0;border-radius:4px;">'
|
||||
. htmlspecialchars($msg, ENT_QUOTES, 'UTF-8') . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a short inline error banner.
|
||||
*
|
||||
* @param string $msg
|
||||
* @return void
|
||||
*/
|
||||
function sw_error($msg)
|
||||
{
|
||||
echo '<div style="background:#f8d7da;border:1px solid #f5c6cb;color:#721c24;padding:8px 12px;margin:8px 0;border-radius:4px;">'
|
||||
. htmlspecialchars($msg, ENT_QUOTES, 'UTF-8') . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a value for HTML output.
|
||||
*
|
||||
* @param mixed $v
|
||||
* @return string
|
||||
*/
|
||||
function sw_h($v)
|
||||
{
|
||||
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function sw_detect_profile_defaults_from_xml($configName)
|
||||
{
|
||||
$configName = trim((string)$configName);
|
||||
if ($configName === '') {
|
||||
return array();
|
||||
}
|
||||
|
||||
$matched = null;
|
||||
foreach (sw_get_all_game_configs() as $xml) {
|
||||
if ((string)$xml->game_key === $configName) {
|
||||
$matched = $xml;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$matched) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$steamAppId = '';
|
||||
if (isset($matched->mods->mod)) {
|
||||
foreach ($matched->mods->mod as $mod) {
|
||||
$candidate = trim((string)$mod->installer_name);
|
||||
if ($candidate !== '' && preg_match('/^\d+$/', $candidate)) {
|
||||
$steamAppId = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$workshopAppId = '';
|
||||
foreach (array('workshop_app_id', 'workshop_appid', 'steam_workshop_app_id', 'steam_workshop_appid') as $tag) {
|
||||
if (isset($matched->$tag)) {
|
||||
$candidate = trim((string)$matched->$tag);
|
||||
if ($candidate !== '' && preg_match('/^\d+$/', $candidate)) {
|
||||
$workshopAppId = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$xmlBlob = $matched->asXML();
|
||||
if ($workshopAppId === '' && $xmlBlob !== false && preg_match('/steamapps\/workshop\/content\/(\d+)/i', $xmlBlob, $m)) {
|
||||
$workshopAppId = $m[1];
|
||||
}
|
||||
if ($workshopAppId === '') {
|
||||
$workshopAppId = $steamAppId;
|
||||
}
|
||||
|
||||
$gameKey = strtolower(trim((string)$matched->game_key));
|
||||
$gameName = strtolower(trim((string)$matched->game_name));
|
||||
$installPathTemplate = (strpos($gameKey . ' ' . $gameName, 'arma') !== false || strpos($gameKey . ' ' . $gameName, 'dayz') !== false)
|
||||
? '{SERVER_ROOT}/{MOD_FOLDER}'
|
||||
: '{SERVER_ROOT}/workshop/{MOD_FOLDER}';
|
||||
|
||||
return array(
|
||||
'steam_app_id' => $steamAppId,
|
||||
'workshop_app_id' => $workshopAppId,
|
||||
'steamcmd_path' => '/home/gameserver/steamcmd/steamcmd.sh',
|
||||
'server_root_template' => '{SERVER_ROOT}',
|
||||
'workshop_download_dir_template' => '{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}',
|
||||
'install_path_template' => $installPathTemplate,
|
||||
);
|
||||
}
|
||||
|
||||
function sw_apply_detected_profile_defaults($db, array $profile, array $detected, $overwriteExisting = false)
|
||||
{
|
||||
$columns = array(
|
||||
'steam_app_id',
|
||||
'workshop_app_id',
|
||||
'steamcmd_path',
|
||||
'workshop_download_dir_template',
|
||||
'server_root_template',
|
||||
'install_path_template',
|
||||
);
|
||||
$setParts = array();
|
||||
$updated = 0;
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (!array_key_exists($column, $detected) || $detected[$column] === '') {
|
||||
continue;
|
||||
}
|
||||
$current = trim((string)($profile[$column] ?? ''));
|
||||
if (!$overwriteExisting && $current !== '') {
|
||||
continue;
|
||||
}
|
||||
if ($current === $detected[$column]) {
|
||||
continue;
|
||||
}
|
||||
$setParts[] = "`$column` = '" . $db->realEscapeSingle($detected[$column]) . "'";
|
||||
$updated++;
|
||||
}
|
||||
|
||||
if (empty($setParts)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$setParts[] = "`updated_at` = NOW()";
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_game_profiles') . "
|
||||
SET " . implode(', ', $setParts) . "
|
||||
WHERE `id` = " . (int)$profile['id'] . " LIMIT 1"
|
||||
);
|
||||
return $updated;
|
||||
}
|
||||
|
||||
function steam_workshop_resolve_paths($db, array $home_info, $workshop_id, $target_path_template = '', $optional_folder_name = '', $workshop_app_id_override = '')
|
||||
{
|
||||
$profile = sw_get_profile_for_home($db, (int)$home_info['home_id']);
|
||||
if (!$profile || !is_array($profile)) {
|
||||
return array('ok' => false, 'error' => 'No enabled Steam Workshop profile for this server home.');
|
||||
}
|
||||
$workshop_id = trim((string)$workshop_id);
|
||||
if ($workshop_id === '' || !preg_match('/^[0-9]+$/', $workshop_id)) {
|
||||
return array('ok' => false, 'error' => 'Invalid Workshop ID.');
|
||||
}
|
||||
$workshop_app_id = trim((string)$workshop_app_id_override);
|
||||
if ($workshop_app_id === '') {
|
||||
$workshop_app_id = (string)$profile['workshop_app_id'];
|
||||
}
|
||||
$folder_name = trim((string)$optional_folder_name);
|
||||
if ($folder_name === '') {
|
||||
$folder_name = '@' . $workshop_id;
|
||||
}
|
||||
$vars = array(
|
||||
'HOME_ID' => (int)$home_info['home_id'],
|
||||
'SERVER_ROOT' => rtrim((string)$home_info['home_path'], '/'),
|
||||
'GAME_ROOT' => rtrim((string)$home_info['home_path'], '/'),
|
||||
'WORKSHOP_ID' => $workshop_id,
|
||||
'WORKSHOP_APP_ID' => $workshop_app_id,
|
||||
'STEAM_APP_ID' => (string)$profile['steam_app_id'],
|
||||
'FOLDER_NAME' => $folder_name,
|
||||
'MOD_FOLDER' => $folder_name,
|
||||
);
|
||||
$target_template = trim((string)$target_path_template);
|
||||
if ($target_template === '') {
|
||||
$target_template = !empty($profile['install_path_template']) ? (string)$profile['install_path_template'] : '{SERVER_ROOT}/workshop/{MOD_FOLDER}';
|
||||
}
|
||||
$resolved_target = sw_apply_template($target_template, $vars);
|
||||
return array(
|
||||
'ok' => true,
|
||||
'profile' => $profile,
|
||||
'workshop_id' => $workshop_id,
|
||||
'workshop_app_id' => $workshop_app_id,
|
||||
'steam_app_id' => (string)$profile['steam_app_id'],
|
||||
'folder_name' => $folder_name,
|
||||
'target_path_template' => $target_template,
|
||||
'target_path_resolved' => $resolved_target,
|
||||
'vars' => $vars,
|
||||
);
|
||||
}
|
||||
|
||||
function steam_workshop_download_item($db, array $home_info, $workshop_id, $target_path_template = '', array $options = array())
|
||||
{
|
||||
$optional_folder_name = isset($options['optional_folder_name']) ? $options['optional_folder_name'] : '';
|
||||
$workshop_app_id = isset($options['workshop_app_id']) ? $options['workshop_app_id'] : '';
|
||||
return steam_workshop_resolve_paths($db, $home_info, $workshop_id, $target_path_template, $optional_folder_name, $workshop_app_id);
|
||||
}
|
||||
|
||||
function steam_workshop_install_item_to_home($db, array $home_info, $workshop_id, $target_path_template = '', array $options = array())
|
||||
{
|
||||
return steam_workshop_download_item($db, $home_info, $workshop_id, $target_path_template, $options);
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
-- GSP Steam Workshop – Manual SQL Reference
|
||||
-- =========================================
|
||||
-- Replace PREFIX_ with your actual table prefix (e.g. gsp_).
|
||||
-- Compatible with MySQL 5.7 and MySQL 8.0.
|
||||
-- Do NOT hardcode any database name here.
|
||||
-- Run in the panel database.
|
||||
|
||||
-- ── Drop legacy tables (if upgrading from the old adapter-based implementation) ──
|
||||
DROP TABLE IF EXISTS `PREFIX_workshop_game_profiles`;
|
||||
DROP TABLE IF EXISTS `PREFIX_workshop_cache`;
|
||||
DROP TABLE IF EXISTS `PREFIX_server_workshop_mods`;
|
||||
DROP TABLE IF EXISTS `PREFIX_server_workshop_settings`;
|
||||
|
||||
-- ── Create new tables ─────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PREFIX_steam_workshop_game_profiles` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`config_name` VARCHAR(100) NOT NULL,
|
||||
`game_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`steam_app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`workshop_app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`steam_login_required` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`steamcmd_login_mode` ENUM('anonymous','account') NOT NULL DEFAULT 'anonymous',
|
||||
`steamcmd_path` VARCHAR(512) NOT NULL DEFAULT '/home/gameserver/steamcmd/steamcmd.sh',
|
||||
`workshop_download_dir_template` TEXT NULL,
|
||||
`server_root_template` TEXT NULL,
|
||||
`install_path_template` TEXT NULL,
|
||||
`folder_naming_format` VARCHAR(64) NOT NULL DEFAULT '@{MOD_NAME}',
|
||||
`mod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-mod=',
|
||||
`servermod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-serverMod=',
|
||||
`install_script_template` TEXT NULL,
|
||||
`update_script_template` TEXT NULL,
|
||||
`copy_bikeys_enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`notes` TEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_config_name` (`config_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PREFIX_steam_workshop_server_mods` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`home_id` INT NOT NULL,
|
||||
`profile_id` INT NOT NULL,
|
||||
`workshop_id` VARCHAR(64) NOT NULL,
|
||||
`mod_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`folder_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`mod_type` ENUM('client','server') NOT NULL DEFAULT 'client',
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`install_status` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`last_installed_at` DATETIME NULL,
|
||||
`last_updated_at` DATETIME NULL,
|
||||
`last_error` TEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── Example: DayZ profile ────────────────────────────────────────────────
|
||||
-- After running the above, insert an example DayZ profile.
|
||||
-- Adjust config_name to match your actual DayZ game_key from config_homes.
|
||||
-- (Run `SELECT game_key, game_name FROM PREFIX_config_homes WHERE game_name LIKE '%DayZ%';`
|
||||
-- to find the right config_name.)
|
||||
--
|
||||
-- INSERT INTO `PREFIX_steam_workshop_game_profiles`
|
||||
-- (`config_name`, `game_name`, `enabled`,
|
||||
-- `steam_app_id`, `workshop_app_id`,
|
||||
-- `steamcmd_path`,
|
||||
-- `workshop_download_dir_template`,
|
||||
-- `server_root_template`,
|
||||
-- `install_path_template`,
|
||||
-- `folder_naming_format`,
|
||||
-- `mod_launch_param_template`,
|
||||
-- `servermod_launch_param_template`,
|
||||
-- `copy_bikeys_enabled`)
|
||||
-- VALUES
|
||||
-- ('dayz_win64', 'DayZ', 1,
|
||||
-- '223350', '221100',
|
||||
-- '/home/gameserver/steamcmd/steamcmd.sh',
|
||||
-- '{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}',
|
||||
-- '/home/gameserver/servers/{HOME_ID}',
|
||||
-- '{SERVER_ROOT}/{MOD_FOLDER}',
|
||||
-- '@{MOD_NAME}',
|
||||
-- '-mod=',
|
||||
-- '-serverMod=',
|
||||
-- 1);
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop module
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
$module_title = "Steam Workshop";
|
||||
$module_version = "3.3";
|
||||
$db_version = 5;
|
||||
$module_required = FALSE;
|
||||
// DEPRECATED: The Steam Workshop standalone module has been superseded by
|
||||
// Server Content Manager (addonsmanager). Navigation access is removed so
|
||||
// users are directed to the new unified workshop workflow. The DB tables
|
||||
// and helper functions are preserved for backward compatibility.
|
||||
// See Panel/modules/addonsmanager/ for the replacement implementation.
|
||||
$module_menus = array();
|
||||
|
||||
if (!function_exists('sw_module_db_prefix')) {
|
||||
function sw_module_db_prefix()
|
||||
{
|
||||
if (defined('DB_PREFIX') && DB_PREFIX !== '') {
|
||||
return DB_PREFIX;
|
||||
}
|
||||
if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') {
|
||||
return $GLOBALS['db_prefix'];
|
||||
}
|
||||
if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') {
|
||||
return $GLOBALS['table_prefix'];
|
||||
}
|
||||
return 'gsp_';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sw_module_table_name')) {
|
||||
function sw_module_table_name($table)
|
||||
{
|
||||
$prefix = preg_replace('/[^a-zA-Z0-9_]/', '', (string)sw_module_db_prefix());
|
||||
$name = preg_replace('/[^a-zA-Z0-9_]/', '', (string)$table);
|
||||
if ($prefix === '') {
|
||||
$prefix = 'gsp_';
|
||||
}
|
||||
return $prefix . $name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sw_module_table')) {
|
||||
function sw_module_table($table)
|
||||
{
|
||||
return '`' . sw_module_table_name($table) . '`';
|
||||
}
|
||||
}
|
||||
|
||||
$install_queries = array();
|
||||
|
||||
$legacyDrops = array(
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('workshop_game_profiles'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('workshop_cache'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('server_workshop_mods'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('server_workshop_settings'),
|
||||
);
|
||||
|
||||
$schemaCreate = array(
|
||||
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_game_profiles') . " (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`config_name` VARCHAR(100) NOT NULL,
|
||||
`game_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`steam_app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`workshop_app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`steam_login_required` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`steamcmd_login_mode` ENUM('anonymous','account') NOT NULL DEFAULT 'anonymous',
|
||||
`steamcmd_path` VARCHAR(512) NOT NULL DEFAULT '/home/gameserver/steamcmd/steamcmd.sh',
|
||||
`workshop_download_dir_template` TEXT NULL,
|
||||
`server_root_template` TEXT NULL,
|
||||
`install_path_template` TEXT NULL,
|
||||
`folder_naming_format` VARCHAR(64) NOT NULL DEFAULT '@{MOD_NAME}',
|
||||
`mod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-mod=',
|
||||
`servermod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-serverMod=',
|
||||
`install_script_template` TEXT NULL,
|
||||
`update_script_template` TEXT NULL,
|
||||
`copy_bikeys_enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`notes` TEXT NULL,
|
||||
`default_update_mode` ENUM('manual','on_restart','before_start') NOT NULL DEFAULT 'manual',
|
||||
`default_restart_behavior` ENUM('none','if_stopped') NOT NULL DEFAULT 'none',
|
||||
`default_hot_load` ENUM('disabled','attempt') NOT NULL DEFAULT 'disabled',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_config_name` (`config_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_server_mods') . " (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`home_id` INT NOT NULL,
|
||||
`profile_id` INT NOT NULL,
|
||||
`workshop_id` VARCHAR(64) NOT NULL,
|
||||
`mod_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`folder_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`mod_type` ENUM('client','server') NOT NULL DEFAULT 'client',
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`install_status` VARCHAR(32) NOT NULL DEFAULT '',
|
||||
`last_installed_at` DATETIME NULL,
|
||||
`last_updated_at` DATETIME NULL,
|
||||
`last_error` TEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_server_settings') . " (
|
||||
`home_id` INT NOT NULL,
|
||||
`update_mode` ENUM('manual','on_restart','before_start')
|
||||
NOT NULL DEFAULT 'manual',
|
||||
`restart_behavior` ENUM('none','if_stopped')
|
||||
NOT NULL DEFAULT 'none',
|
||||
`hot_load` ENUM('disabled','attempt')
|
||||
NOT NULL DEFAULT 'disabled',
|
||||
`warning_minutes` INT NOT NULL DEFAULT 0,
|
||||
`schedule_interval` ENUM('disabled','daily','weekly') NOT NULL DEFAULT 'disabled',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`home_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
);
|
||||
|
||||
$install_queries[0] = array_merge($legacyDrops, $schemaCreate);
|
||||
$install_queries[3] = array_merge($legacyDrops, $schemaCreate);
|
||||
$install_queries[4] = array();
|
||||
|
||||
$install_queries[5] = array(
|
||||
function ($db) {
|
||||
$table = sw_module_table_name('steam_workshop_server_settings');
|
||||
$exists = $db->resultQuery(
|
||||
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" . $db->realEscapeSingle($table) . "'"
|
||||
);
|
||||
if (!$exists || (int)($exists[0]['cnt'] ?? 0) === 0) {
|
||||
return (bool)$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS " . sw_module_table('steam_workshop_server_settings') . " (
|
||||
`home_id` INT NOT NULL,
|
||||
`update_mode` ENUM('manual','on_restart','before_start') NOT NULL DEFAULT 'manual',
|
||||
`restart_behavior` ENUM('none','if_stopped') NOT NULL DEFAULT 'none',
|
||||
`hot_load` ENUM('disabled','attempt') NOT NULL DEFAULT 'disabled',
|
||||
`warning_minutes` INT NOT NULL DEFAULT 0,
|
||||
`schedule_interval` ENUM('disabled','daily','weekly') NOT NULL DEFAULT 'disabled',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
PRIMARY KEY (`home_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
);
|
||||
}
|
||||
|
||||
$ok = true;
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
SET `update_mode` = CASE
|
||||
WHEN `update_mode` = 'scheduled' THEN 'manual'
|
||||
ELSE `update_mode`
|
||||
END"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
SET `restart_behavior` = CASE
|
||||
WHEN `restart_behavior` IN ('if_empty','next_restart') THEN 'if_stopped'
|
||||
WHEN `restart_behavior` = 'immediate' THEN 'none'
|
||||
ELSE `restart_behavior`
|
||||
END"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
SET `schedule_interval` = CASE
|
||||
WHEN `schedule_interval` = 'hourly' THEN 'daily'
|
||||
WHEN `schedule_interval` IS NULL OR `schedule_interval` = '' THEN 'disabled'
|
||||
ELSE `schedule_interval`
|
||||
END"
|
||||
);
|
||||
// The simplified workflow intentionally hard-disables these legacy fields
|
||||
// for every row so old unsupported behaviors cannot be re-enabled.
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
SET `hot_load` = 'disabled', `warning_minutes` = 0"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
MODIFY `update_mode` ENUM('manual','on_restart','before_start') NOT NULL DEFAULT 'manual'"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
MODIFY `restart_behavior` ENUM('none','if_stopped') NOT NULL DEFAULT 'none'"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_server_settings') . "
|
||||
MODIFY `schedule_interval` ENUM('disabled','daily','weekly') NOT NULL DEFAULT 'disabled'"
|
||||
);
|
||||
|
||||
return $ok;
|
||||
},
|
||||
function ($db) {
|
||||
$profileTable = sw_module_table_name('steam_workshop_game_profiles');
|
||||
$exists = $db->resultQuery(
|
||||
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" . $db->realEscapeSingle($profileTable) . "'"
|
||||
);
|
||||
if (!$exists || (int)($exists[0]['cnt'] ?? 0) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$ok = true;
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_game_profiles') . "
|
||||
SET `default_update_mode` = CASE
|
||||
WHEN `default_update_mode` = 'scheduled' THEN 'manual'
|
||||
ELSE `default_update_mode`
|
||||
END"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"UPDATE " . sw_module_table('steam_workshop_game_profiles') . "
|
||||
SET `default_restart_behavior` = CASE
|
||||
WHEN `default_restart_behavior` IN ('if_empty','next_restart') THEN 'if_stopped'
|
||||
WHEN `default_restart_behavior` = 'immediate' THEN 'none'
|
||||
ELSE `default_restart_behavior`
|
||||
END"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_game_profiles') . "
|
||||
MODIFY `default_update_mode` ENUM('manual','on_restart','before_start') NOT NULL DEFAULT 'manual'"
|
||||
);
|
||||
$ok = $ok && (bool)$db->query(
|
||||
"ALTER TABLE " . sw_module_table('steam_workshop_game_profiles') . "
|
||||
MODIFY `default_restart_behavior` ENUM('none','if_stopped') NOT NULL DEFAULT 'none'"
|
||||
);
|
||||
|
||||
return $ok;
|
||||
},
|
||||
);
|
||||
|
||||
$uninstall_queries = array(
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('steam_workshop_server_settings'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('steam_workshop_server_mods'),
|
||||
"DROP TABLE IF EXISTS " . sw_module_table('steam_workshop_game_profiles'),
|
||||
);
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: monitor page button
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* Adds a "Steam Workshop" button to the game/server monitor page when:
|
||||
* - the game's Workshop profile is enabled in steam_workshop_game_profiles, AND
|
||||
* - the current user owns the server or is an admin.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
$module_buttons = array();
|
||||
|
||||
if (!function_exists('sw_monitor_db_prefix')) {
|
||||
function sw_monitor_db_prefix()
|
||||
{
|
||||
if (defined('DB_PREFIX') && DB_PREFIX !== '') {
|
||||
return DB_PREFIX;
|
||||
}
|
||||
if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') {
|
||||
return $GLOBALS['db_prefix'];
|
||||
}
|
||||
if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') {
|
||||
return $GLOBALS['table_prefix'];
|
||||
}
|
||||
return 'gsp_';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sw_monitor_table')) {
|
||||
function sw_monitor_table($name)
|
||||
{
|
||||
return '`' . sw_monitor_db_prefix() . $name . '`';
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: the standalone steam_workshop workflow is no longer the primary
|
||||
// user path. Server Content Manager (addonsmanager) now owns Workshop installs
|
||||
// and exposes its own monitor button when content templates exist.
|
||||
?>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<navigation>
|
||||
<!-- Admin: manage Steam Workshop game profiles -->
|
||||
<page key="admin" file="admin.php" access="admin" />
|
||||
<!-- User: manage per-server mods -->
|
||||
<page key="user" file="user.php" access="admin,user,subuser" />
|
||||
</navigation>
|
||||
|
|
@ -1,558 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP – Steam Workshop: User mod management
|
||||
* Copyright (C) 2025 WDS / GameServerPanel
|
||||
*
|
||||
* Accessible via: home.php?m=steam_workshop&p=user&home_id=123
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
function exec_ogp_module()
|
||||
{
|
||||
global $db;
|
||||
|
||||
echo '<h2>Steam Workshop – Mod Manager</h2>';
|
||||
|
||||
$home_id = isset($_REQUEST['home_id']) ? (int)$_REQUEST['home_id'] : 0;
|
||||
|
||||
if (!$home_id) {
|
||||
sw_error('No server selected. Please access this page from your game server manager.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ownership check
|
||||
if (!sw_user_owns_home($db, (int)$_SESSION['user_id'], $home_id)) {
|
||||
sw_error('Access denied. You do not own this server.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load server info
|
||||
$home = sw_get_home_info($db, $home_id);
|
||||
if (!$home) {
|
||||
sw_error('Server not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find matching Workshop profile
|
||||
$profile = sw_get_profile_for_home($db, $home_id);
|
||||
if (!$profile) {
|
||||
echo '<p>Steam Workshop is not enabled for this game.</p>';
|
||||
echo '<p>An administrator must enable Workshop support for this game under '
|
||||
. '<em>Steam Workshop › Admin</em>.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? ($_GET['action'] ?? '');
|
||||
|
||||
// ── POST handlers ─────────────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
switch ($action) {
|
||||
case 'add_mod':
|
||||
sw_user_add_mod($db, $home_id, $profile);
|
||||
break;
|
||||
case 'save_mod':
|
||||
sw_user_save_mod($db, $home_id);
|
||||
break;
|
||||
case 'delete_mod':
|
||||
sw_user_delete_mod($db, $home_id);
|
||||
break;
|
||||
case 'toggle_mod':
|
||||
sw_user_toggle_mod($db, $home_id);
|
||||
break;
|
||||
case 'move_up':
|
||||
case 'move_down':
|
||||
sw_user_reorder_mod($db, $home_id, $action);
|
||||
break;
|
||||
case 'queue_update':
|
||||
sw_user_queue_update($db, $home_id);
|
||||
break;
|
||||
case 'save_settings':
|
||||
sw_user_save_settings($db, $home_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render page ───────────────────────────────────────────────────
|
||||
sw_user_render($db, $home_id, $home, $profile);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST action handlers
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function sw_user_add_mod($db, $home_id, array $profile)
|
||||
{
|
||||
$workshop_id = trim($_POST['workshop_id'] ?? '');
|
||||
if (!preg_match('/^\d{1,20}$/', $workshop_id)) {
|
||||
sw_error('Invalid Workshop ID – must be a numeric Steam Workshop item ID.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplicates
|
||||
$safe_wid = $db->realEscapeSingle($workshop_id);
|
||||
$exists = $db->resultQuery(
|
||||
"SELECT id FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `home_id` = $home_id AND `workshop_id` = '$safe_wid' LIMIT 1"
|
||||
);
|
||||
if ($exists) {
|
||||
sw_error("Workshop ID $workshop_id is already in the list.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine next sort_order
|
||||
$last = $db->resultQuery(
|
||||
"SELECT MAX(`sort_order`) AS m FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `home_id` = $home_id"
|
||||
);
|
||||
$sort = ($last && isset($last[0]['m'])) ? ((int)$last[0]['m'] + 1) : 0;
|
||||
|
||||
$mod_name = $db->realEscapeSingle(trim($_POST['mod_name'] ?? ''));
|
||||
$mod_type = (($_POST['mod_type'] ?? 'client') === 'server') ? 'server' : 'client';
|
||||
$profile_id = (int)$profile['id'];
|
||||
|
||||
// Auto-generate folder name from naming format template
|
||||
$folder_name = sw_apply_template(
|
||||
$profile['folder_naming_format'],
|
||||
array(
|
||||
'MOD_NAME' => !empty($mod_name) ? $mod_name : $workshop_id,
|
||||
'WORKSHOP_ID' => $workshop_id,
|
||||
'WORKSHOP_APP_ID'=> $profile['workshop_app_id'],
|
||||
)
|
||||
);
|
||||
$safe_fname = $db->realEscapeSingle($folder_name);
|
||||
$safe_mname = $mod_name; // already escaped above via realEscapeSingle
|
||||
|
||||
$ok = $db->query(
|
||||
"INSERT INTO " . sw_table('steam_workshop_server_mods') . "
|
||||
(`home_id`, `profile_id`, `workshop_id`, `mod_name`, `folder_name`,
|
||||
`mod_type`, `sort_order`, `enabled`, `install_status`, `created_at`)
|
||||
VALUES ($home_id, $profile_id, '$safe_wid', '$safe_mname', '$safe_fname',
|
||||
'$mod_type', $sort, 1, '', NOW())"
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
sw_success("Workshop mod $workshop_id added.");
|
||||
} else {
|
||||
sw_error('Failed to add mod.');
|
||||
}
|
||||
}
|
||||
|
||||
function sw_user_save_mod($db, $home_id)
|
||||
{
|
||||
$mod_id = (int)($_POST['mod_id'] ?? 0);
|
||||
if (!$mod_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mod = sw_get_mod_by_id($db, $mod_id);
|
||||
if (!$mod || (int)$mod['home_id'] !== $home_id) {
|
||||
sw_error('Mod not found or access denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
$mod_name = $db->realEscapeSingle(trim($_POST['mod_name'] ?? ''));
|
||||
$folder_name = $db->realEscapeSingle(trim($_POST['folder_name'] ?? ''));
|
||||
$mod_type = (($_POST['mod_type'] ?? 'client') === 'server') ? 'server' : 'client';
|
||||
|
||||
if (empty($folder_name)) {
|
||||
sw_error('Folder name cannot be empty.');
|
||||
return;
|
||||
}
|
||||
|
||||
$ok = $db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `mod_name` = '$mod_name',
|
||||
`folder_name` = '$folder_name',
|
||||
`mod_type` = '$mod_type',
|
||||
`updated_at` = NOW()
|
||||
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
sw_success('Mod updated.');
|
||||
} else {
|
||||
sw_error('Failed to update mod.');
|
||||
}
|
||||
}
|
||||
|
||||
function sw_user_delete_mod($db, $home_id)
|
||||
{
|
||||
$mod_id = (int)($_POST['mod_id'] ?? 0);
|
||||
if (!$mod_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mod = sw_get_mod_by_id($db, $mod_id);
|
||||
if (!$mod || (int)$mod['home_id'] !== $home_id) {
|
||||
sw_error('Mod not found or access denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
$db->query(
|
||||
"DELETE FROM " . sw_table('steam_workshop_server_mods') . "
|
||||
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
sw_success('Mod removed from list.');
|
||||
}
|
||||
|
||||
function sw_user_toggle_mod($db, $home_id)
|
||||
{
|
||||
$mod_id = (int)($_POST['mod_id'] ?? 0);
|
||||
if (!$mod_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mod = sw_get_mod_by_id($db, $mod_id);
|
||||
if (!$mod || (int)$mod['home_id'] !== $home_id) {
|
||||
sw_error('Mod not found or access denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
$new_state = $mod['enabled'] ? 0 : 1;
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `enabled` = $new_state, `updated_at` = NOW()
|
||||
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
function sw_user_reorder_mod($db, $home_id, $direction)
|
||||
{
|
||||
$mod_id = (int)($_POST['mod_id'] ?? 0);
|
||||
if (!$mod_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mod = sw_get_mod_by_id($db, $mod_id);
|
||||
if (!$mod || (int)$mod['home_id'] !== $home_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mods = sw_get_server_mods($db, $home_id);
|
||||
if (!$mods) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalise sort_order to 0-based sequential integers
|
||||
$sorted = array_values($mods);
|
||||
foreach ($sorted as $idx => $m) {
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `sort_order` = $idx
|
||||
WHERE `id` = " . (int)$m['id'] . " AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
// Find the position of the target mod
|
||||
$pos = -1;
|
||||
foreach ($sorted as $idx => $m) {
|
||||
if ((int)$m['id'] === $mod_id) {
|
||||
$pos = $idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($pos < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($direction === 'move_up' && $pos > 0) {
|
||||
$swap_pos = $pos - 1;
|
||||
} elseif ($direction === 'move_down' && $pos < (count($sorted) - 1)) {
|
||||
$swap_pos = $pos + 1;
|
||||
} else {
|
||||
return; // already at boundary
|
||||
}
|
||||
|
||||
$swap_id = (int)$sorted[$swap_pos]['id'];
|
||||
|
||||
// Swap sort_order values
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `sort_order` = $swap_pos
|
||||
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `sort_order` = $pos
|
||||
WHERE `id` = $swap_id AND `home_id` = $home_id LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
function sw_user_queue_update($db, $home_id)
|
||||
{
|
||||
// Mark all enabled mods as 'queued' so the agent picks them up.
|
||||
$db->query(
|
||||
"UPDATE " . sw_table('steam_workshop_server_mods') . "
|
||||
SET `install_status` = 'queued', `updated_at` = NOW()
|
||||
WHERE `home_id` = $home_id AND `enabled` = 1"
|
||||
);
|
||||
sw_success('All enabled mods were queued. Updates are processed automatically by the server agent.');
|
||||
}
|
||||
|
||||
function sw_user_save_settings($db, $home_id)
|
||||
{
|
||||
$ok = sw_save_server_settings($db, $home_id, array(
|
||||
'update_mode' => $_POST['update_mode'] ?? 'manual',
|
||||
'restart_behavior' => $_POST['restart_behavior'] ?? 'none',
|
||||
'schedule_interval' => $_POST['schedule_interval'] ?? 'disabled',
|
||||
));
|
||||
|
||||
if ($ok) {
|
||||
sw_success('Workshop behavior settings saved.');
|
||||
} else {
|
||||
sw_error('Failed to save settings.');
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Render
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function sw_user_render($db, $home_id, array $home, array $profile)
|
||||
{
|
||||
$mods = sw_get_server_mods($db, $home_id) ?: array();
|
||||
$settings = sw_get_server_settings($db, $home_id);
|
||||
$queuedCount = 0;
|
||||
$failedCount = 0;
|
||||
$installedCount = 0;
|
||||
$latestUpdateAt = '';
|
||||
$latestError = '';
|
||||
foreach ($mods as $mod) {
|
||||
if (($mod['install_status'] ?? '') === 'queued') {
|
||||
$queuedCount++;
|
||||
} elseif (($mod['install_status'] ?? '') === 'failed') {
|
||||
$failedCount++;
|
||||
} elseif (($mod['install_status'] ?? '') === 'installed') {
|
||||
$installedCount++;
|
||||
}
|
||||
if (!empty($mod['last_updated_at']) && $mod['last_updated_at'] > $latestUpdateAt) {
|
||||
$latestUpdateAt = $mod['last_updated_at'];
|
||||
}
|
||||
if (($mod['install_status'] ?? '') === 'failed' && !empty($mod['last_error']) && $latestError === '') {
|
||||
$latestError = $mod['last_error'];
|
||||
}
|
||||
}
|
||||
|
||||
$base_url = 'home.php?m=steam_workshop&p=user&home_id=' . $home_id;
|
||||
?>
|
||||
<style>
|
||||
.sw-user-panel{background:#161616;border:1px solid #2f2f2f;border-radius:6px;padding:14px;margin:10px 0;color:#ececec}
|
||||
.sw-user-panel h3{margin:0 0 10px 0;color:#fff}
|
||||
.sw-user-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
|
||||
.sw-user-grid label{display:block}
|
||||
.sw-user-grid span{display:block;font-size:12px;color:#bebebe;margin-bottom:4px}
|
||||
.sw-user-grid input[type=text],.sw-user-grid select{width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #3a3a3a;color:#f0f0f0;padding:7px;border-radius:4px}
|
||||
.sw-user-table-wrap{overflow-x:auto}
|
||||
.sw-user-table{width:100%;border-collapse:collapse;min-width:860px}
|
||||
.sw-user-table th,.sw-user-table td{border:1px solid #353535;padding:8px;vertical-align:middle}
|
||||
.sw-user-table th{background:#202020;text-align:left;color:#fff}
|
||||
.sw-status-ok{color:#7cdc7c;font-weight:700}
|
||||
.sw-status-queued{color:#ffca63;font-weight:700}
|
||||
.sw-status-failed{color:#ff8484;font-weight:700}
|
||||
.sw-status-progress{color:#8cc7ff;font-weight:700}
|
||||
.sw-muted{color:#b4b4b4}
|
||||
</style>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<p>
|
||||
<strong>Server:</strong> <?= sw_h($home['home_name']) ?>
|
||||
<strong>Game:</strong> <?= sw_h($home['game_name']) ?>
|
||||
<strong>Workshop Profile:</strong> <?= sw_h($profile['config_name']) ?>
|
||||
</p>
|
||||
<p class="sw-muted" style="margin-bottom:0;">
|
||||
Queue updates from this page. The server agent applies queued updates automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<h3>Add Workshop Mod</h3>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>">
|
||||
<input type="hidden" name="action" value="add_mod">
|
||||
<div class="sw-user-grid">
|
||||
<label>
|
||||
<span>Workshop ID</span>
|
||||
<input type="text" id="workshop_id" name="workshop_id" placeholder="e.g. 2863534533" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Display Name (optional)</span>
|
||||
<input type="text" id="add_mod_name" name="mod_name" placeholder="e.g. CF">
|
||||
</label>
|
||||
<label>
|
||||
<span>Mod Type</span>
|
||||
<select id="add_mod_type" name="mod_type">
|
||||
<option value="client">Client mod</option>
|
||||
<option value="server">Server-side only</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p style="margin:12px 0 0;">
|
||||
<button type="submit" class="button">Add Mod</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<h3>Update Queue & Last Result</h3>
|
||||
<p>
|
||||
<strong>Enabled mods:</strong> <?= count(array_filter($mods, function ($m) { return !empty($m['enabled']); })) ?>
|
||||
<strong>Queued:</strong> <?= $queuedCount ?>
|
||||
<strong>Installed:</strong> <?= $installedCount ?>
|
||||
<strong>Failed:</strong> <?= $failedCount ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Last update time:</strong> <?= $latestUpdateAt ? sw_h($latestUpdateAt) : 'Never' ?>
|
||||
</p>
|
||||
<?php if ($latestError !== ''): ?>
|
||||
<p><strong>Last error:</strong> <?= sw_h($latestError) ?></p>
|
||||
<?php endif; ?>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>">
|
||||
<input type="hidden" name="action" value="queue_update">
|
||||
<button type="submit" class="button" onclick="return confirm('Queue all enabled mods for update?');">Queue Update for All Enabled Mods</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<h3>Workshop Behavior Settings</h3>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>">
|
||||
<input type="hidden" name="action" value="save_settings">
|
||||
<div class="sw-user-grid">
|
||||
<label>
|
||||
<span>Update Mode</span>
|
||||
<select name="update_mode">
|
||||
<option value="manual" <?= ($settings['update_mode'] === 'manual') ? 'selected' : '' ?>>Manual only</option>
|
||||
<option value="on_restart" <?= ($settings['update_mode'] === 'on_restart') ? 'selected' : '' ?>>On next restart</option>
|
||||
<option value="before_start" <?= ($settings['update_mode'] === 'before_start') ? 'selected' : '' ?>>Before server start</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Restart Behavior</span>
|
||||
<select name="restart_behavior">
|
||||
<option value="none" <?= ($settings['restart_behavior'] === 'none') ? 'selected' : '' ?>>Never restart automatically</option>
|
||||
<option value="if_stopped" <?= ($settings['restart_behavior'] === 'if_stopped') ? 'selected' : '' ?>>Restart only if server is stopped</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Scheduled Checks</span>
|
||||
<select name="schedule_interval">
|
||||
<option value="disabled" <?= ($settings['schedule_interval'] === 'disabled') ? 'selected' : '' ?>>Disabled</option>
|
||||
<option value="daily" <?= ($settings['schedule_interval'] === 'daily') ? 'selected' : '' ?>>Daily</option>
|
||||
<option value="weekly" <?= ($settings['schedule_interval'] === 'weekly') ? 'selected' : '' ?>>Weekly</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p style="margin:12px 0 0;">
|
||||
<button type="submit" class="button">Save Behavior Settings</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sw-user-panel">
|
||||
<h3>Installed Mods (<?= count($mods) ?>)</h3>
|
||||
<?php if (empty($mods)): ?>
|
||||
<p>No mods added yet. Use the form above to add Workshop IDs.</p>
|
||||
<?php else: ?>
|
||||
<div class="sw-user-table-wrap">
|
||||
<table class="sw-user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Workshop ID</th>
|
||||
<th>Mod Name</th>
|
||||
<th>Folder Name</th>
|
||||
<th>Type</th>
|
||||
<th>Enabled</th>
|
||||
<th>Status</th>
|
||||
<th>Last Update</th>
|
||||
<th>Last Error</th>
|
||||
<th>Order</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($mods as $idx => $mod): ?>
|
||||
<?php
|
||||
$shortError = '';
|
||||
if (!empty($mod['last_error'])) {
|
||||
$shortError = (strlen($mod['last_error']) > 70)
|
||||
? (substr($mod['last_error'], 0, 67) . '...')
|
||||
: $mod['last_error'];
|
||||
}
|
||||
?>
|
||||
<tr style="<?= !$mod['enabled'] ? 'opacity:0.55;' : '' ?>">
|
||||
<td><?= $idx + 1 ?></td>
|
||||
<td style="font-family:monospace;"><?= sw_h($mod['workshop_id']) ?></td>
|
||||
<td>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
|
||||
<input type="hidden" name="action" value="save_mod">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<input type="text" name="mod_name" value="<?= sw_h($mod['mod_name']) ?>" style="width:120px;">
|
||||
</td>
|
||||
<td><input type="text" name="folder_name" value="<?= sw_h($mod['folder_name']) ?>" style="width:120px;"></td>
|
||||
<td>
|
||||
<select name="mod_type" style="width:120px;">
|
||||
<option value="client" <?= $mod['mod_type'] === 'client' ? 'selected' : '' ?>>Client</option>
|
||||
<option value="server" <?= $mod['mod_type'] === 'server' ? 'selected' : '' ?>>Server</option>
|
||||
</select>
|
||||
<button type="submit" class="button small">Save</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
|
||||
<input type="hidden" name="action" value="toggle_mod">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<button type="submit" class="button small" style="<?= $mod['enabled'] ? 'background:#5cb85c;color:#fff;' : '' ?>"><?= $mod['enabled'] ? 'On' : 'Off' ?></button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$s = $mod['install_status'];
|
||||
if ($s === 'installed') {
|
||||
echo '<span class="sw-status-ok">Installed</span>';
|
||||
} elseif ($s === 'queued') {
|
||||
echo '<span class="sw-status-queued">Queued</span>';
|
||||
} elseif ($s === 'failed') {
|
||||
echo '<span class="sw-status-failed">Failed</span>';
|
||||
} elseif ($s === 'updating') {
|
||||
echo '<span class="sw-status-progress">Updating</span>';
|
||||
} else {
|
||||
echo '<span class="sw-muted">Not installed</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?= !empty($mod['last_updated_at']) ? sw_h($mod['last_updated_at']) : '-' ?></td>
|
||||
<td title="<?= sw_h($mod['last_error'] ?? '') ?>"><?= $shortError !== '' ? sw_h($shortError) : '-' ?></td>
|
||||
<td style="white-space:nowrap;">
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
|
||||
<input type="hidden" name="action" value="move_up">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<button type="submit" class="button small" <?= $idx === 0 ? 'disabled' : '' ?>>▲</button>
|
||||
</form>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
|
||||
<input type="hidden" name="action" value="move_down">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<button type="submit" class="button small" <?= $idx === (count($mods) - 1) ? 'disabled' : '' ?>>▼</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
|
||||
<input type="hidden" name="action" value="delete_mod">
|
||||
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
|
||||
<button type="submit" class="button small danger" onclick="return confirm('Remove this mod from the list?');" style="background:#d9534f;color:#fff;">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue