Merge pull request #116 from GameServerPanel/copilot/add-admin-configuration-page
This commit is contained in:
commit
e338eafb36
10 changed files with 1345 additions and 319 deletions
|
|
@ -72,6 +72,12 @@ class WorkshopModController
|
||||||
case 'sync':
|
case 'sync':
|
||||||
$this->handleSync($userId, $isAdmin);
|
$this->handleSync($userId, $isAdmin);
|
||||||
return;
|
return;
|
||||||
|
case 'save_settings':
|
||||||
|
$this->handleSaveSettings($userId, $isAdmin);
|
||||||
|
return;
|
||||||
|
case 'queue_update':
|
||||||
|
$this->handleQueueUpdate($userId, $isAdmin);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,23 +137,40 @@ class WorkshopModController
|
||||||
}
|
}
|
||||||
|
|
||||||
$agentId = (int)($home['remote_server_id'] ?? 0);
|
$agentId = (int)($home['remote_server_id'] ?? 0);
|
||||||
$appId = $this->searchService->getSteamAppIdForGameKey((string)($home['game_key'] ?? ''));
|
|
||||||
$profile = $appId !== null ? $this->repo->getProfileByAppId($appId) : null;
|
|
||||||
|
|
||||||
$installedMods = $this->repo->listModsForHome($homeId);
|
// Load server-level settings
|
||||||
$availableMods = ($profile !== null && $appId !== null)
|
$serverSettings = $this->repo->getServerSettings($homeId);
|
||||||
|
|
||||||
|
// Determine active profile: from server settings, or fall back to app-id lookup
|
||||||
|
$profile = null;
|
||||||
|
if ($serverSettings !== null && !empty($serverSettings['profile_id'])) {
|
||||||
|
$profile = $this->repo->getProfileById((int)$serverSettings['profile_id']);
|
||||||
|
}
|
||||||
|
if ($profile === null) {
|
||||||
|
$appId = $this->searchService->getSteamAppIdForGameKey((string)($home['game_key'] ?? ''));
|
||||||
|
$profile = $appId !== null ? $this->repo->getProfileByAppId($appId) : null;
|
||||||
|
}
|
||||||
|
$appId = $profile !== null ? (string)($profile['workshop_app_id'] ?? '') : null;
|
||||||
|
|
||||||
|
// All enabled profiles for the profile selector
|
||||||
|
$allProfiles = $this->repo->listProfiles(true);
|
||||||
|
|
||||||
|
$installedMods = $this->repo->listModsForHome($homeId);
|
||||||
|
$availableMods = ($profile !== null && $appId !== null)
|
||||||
? $this->repo->listCacheForAgent($agentId, $appId)
|
? $this->repo->listCacheForAgent($agentId, $appId)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
$this->render('user_workshop_mods', [
|
$this->render('user_workshop_mods', [
|
||||||
'lang' => $this->lang,
|
'lang' => $this->lang,
|
||||||
'home' => $home,
|
'home' => $home,
|
||||||
'homeId' => $homeId,
|
'homeId' => $homeId,
|
||||||
'profile' => $profile,
|
'profile' => $profile,
|
||||||
'appId' => $appId,
|
'appId' => $appId,
|
||||||
'installedMods' => $installedMods,
|
'installedMods' => $installedMods,
|
||||||
'availableMods' => $availableMods,
|
'availableMods' => $availableMods,
|
||||||
'isAdmin' => $isAdmin,
|
'serverSettings' => $serverSettings ?? [],
|
||||||
|
'allProfiles' => $allProfiles,
|
||||||
|
'isAdmin' => $isAdmin,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,6 +371,56 @@ class WorkshopModController
|
||||||
echo json_encode(['ok' => true, 'results' => $payload['results'], 'pagination' => $payload['pagination']]);
|
echo json_encode(['ok' => true, 'results' => $payload['results'], 'pagination' => $payload['pagination']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function handleSaveSettings(int $userId, bool $isAdmin): void
|
||||||
|
{
|
||||||
|
$homeId = (int)($_POST['home_id'] ?? 0);
|
||||||
|
if ($homeId <= 0) {
|
||||||
|
print_failure($this->lang['error_missing_home'] ?? 'Select a server first.');
|
||||||
|
$this->handleIndex($userId, $isAdmin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$home = $this->getHome($homeId, $userId, $isAdmin);
|
||||||
|
if ($home === null) {
|
||||||
|
print_failure($this->lang['error_home_not_found'] ?? 'Server not found.');
|
||||||
|
$this->handleIndex($userId, $isAdmin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->repo->saveServerSettings($homeId, [
|
||||||
|
'workshop_enabled' => !empty($_POST['workshop_enabled']) ? 1 : 0,
|
||||||
|
'profile_id' => (int)($_POST['profile_id'] ?? 0),
|
||||||
|
'update_mode' => $_POST['update_mode'] ?? 'manual',
|
||||||
|
'restart_behavior' => $_POST['restart_behavior'] ?? 'none',
|
||||||
|
]);
|
||||||
|
|
||||||
|
print_success($this->lang['settings_saved'] ?? 'Workshop settings saved.');
|
||||||
|
$_GET['home_id'] = $homeId;
|
||||||
|
$this->handleModsPage($userId, $isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleQueueUpdate(int $userId, bool $isAdmin): void
|
||||||
|
{
|
||||||
|
$homeId = (int)($_POST['home_id'] ?? 0);
|
||||||
|
if ($homeId <= 0) {
|
||||||
|
print_failure($this->lang['error_missing_home'] ?? 'Select a server first.');
|
||||||
|
$this->handleIndex($userId, $isAdmin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$home = $this->getHome($homeId, $userId, $isAdmin);
|
||||||
|
if ($home === null) {
|
||||||
|
print_failure($this->lang['error_home_not_found'] ?? 'Server not found.');
|
||||||
|
$this->handleIndex($userId, $isAdmin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->repo->setUpdateQueued($homeId, true);
|
||||||
|
print_success($this->lang['update_queued'] ?? 'Manual update queued. It will run on the next scheduler cycle.');
|
||||||
|
$_GET['home_id'] = $homeId;
|
||||||
|
$this->handleModsPage($userId, $isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -157,24 +157,58 @@ class WorkshopProfileController
|
||||||
$osValues = array_values(array_intersect($osRaw, $allowedOs));
|
$osValues = array_values(array_intersect($osRaw, $allowedOs));
|
||||||
$supportedOs = implode(',', $osValues !== [] ? $osValues : ['linux']);
|
$supportedOs = implode(',', $osValues !== [] ? $osValues : ['linux']);
|
||||||
|
|
||||||
$allowedMethods = ['rsync', 'robocopy', 'custom_script'];
|
$allowedCopyMethods = ['copy', 'rsync', 'symlink'];
|
||||||
$copyMethod = in_array($post['copy_method'] ?? '', $allowedMethods, true)
|
$copyMethod = in_array($post['copy_method'] ?? '', $allowedCopyMethods, true)
|
||||||
? (string)$post['copy_method']
|
? (string)$post['copy_method']
|
||||||
: 'rsync';
|
: 'rsync';
|
||||||
|
|
||||||
|
$allowedLoginModes = ['anonymous', 'account'];
|
||||||
|
$steamcmdLoginMode = in_array($post['steamcmd_login_mode'] ?? '', $allowedLoginModes, true)
|
||||||
|
? (string)$post['steamcmd_login_mode']
|
||||||
|
: 'anonymous';
|
||||||
|
|
||||||
|
$allowedFolderFormats = ['@%mod_name%', '@%workshop_id%', 'custom'];
|
||||||
|
$folderNamingFormat = in_array($post['folder_naming_format'] ?? '', $allowedFolderFormats, true)
|
||||||
|
? (string)$post['folder_naming_format']
|
||||||
|
: '@%workshop_id%';
|
||||||
|
|
||||||
|
$allowedSeparators = ['semicolon', 'comma', 'space'];
|
||||||
|
$modSeparator = in_array($post['mod_separator'] ?? '', $allowedSeparators, true)
|
||||||
|
? (string)$post['mod_separator']
|
||||||
|
: 'semicolon';
|
||||||
|
|
||||||
|
// When folder naming is preset (@%mod_name% or @%workshop_id%), derive template from format.
|
||||||
|
// When 'custom', use the admin-supplied value.
|
||||||
|
$folderNameTemplate = $folderNamingFormat !== 'custom'
|
||||||
|
? $folderNamingFormat
|
||||||
|
: trim((string)($post['folder_name_template'] ?? '@%workshop_id%'));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'game_key' => trim((string)($post['game_key'] ?? '')),
|
'game_key' => trim((string)($post['game_key'] ?? '')),
|
||||||
'game_name' => trim((string)($post['game_name'] ?? '')),
|
'game_name' => trim((string)($post['game_name'] ?? '')),
|
||||||
|
'steam_app_id' => preg_replace('/[^0-9]/', '', (string)($post['steam_app_id'] ?? '')) ?? '',
|
||||||
'workshop_app_id' => preg_replace('/[^0-9]/', '', (string)($post['workshop_app_id'] ?? '')) ?? '',
|
'workshop_app_id' => preg_replace('/[^0-9]/', '', (string)($post['workshop_app_id'] ?? '')) ?? '',
|
||||||
|
'steam_login_required' => !empty($post['steam_login_required']) ? 1 : 0,
|
||||||
|
'steamcmd_login_mode' => $steamcmdLoginMode,
|
||||||
|
'steamcmd_path' => trim((string)($post['steamcmd_path'] ?? '')),
|
||||||
'supported_os' => $supportedOs,
|
'supported_os' => $supportedOs,
|
||||||
'cache_path_template' => trim((string)($post['cache_path_template'] ?? '')),
|
'cache_path_template' => trim((string)($post['cache_path_template'] ?? '')),
|
||||||
'install_path_template' => trim((string)($post['install_path_template'] ?? '')),
|
'install_path_template' => trim((string)($post['install_path_template'] ?? '')),
|
||||||
'folder_name_template' => trim((string)($post['folder_name_template'] ?? '@{mod_id}')),
|
'folder_naming_format' => $folderNamingFormat,
|
||||||
|
'folder_name_template' => $folderNameTemplate,
|
||||||
|
'mod_launch_param' => trim((string)($post['mod_launch_param'] ?? '')),
|
||||||
|
'mod_separator' => $modSeparator,
|
||||||
'copy_method' => $copyMethod,
|
'copy_method' => $copyMethod,
|
||||||
|
'copy_keys' => !empty($post['copy_keys']) ? 1 : 0,
|
||||||
|
'key_source_path' => trim((string)($post['key_source_path'] ?? '')),
|
||||||
|
'key_dest_path' => trim((string)($post['key_dest_path'] ?? '')),
|
||||||
|
'pre_update_script' => trim((string)($post['pre_update_script'] ?? '')),
|
||||||
'install_script' => trim((string)($post['install_script'] ?? '')),
|
'install_script' => trim((string)($post['install_script'] ?? '')),
|
||||||
|
'post_update_script' => trim((string)($post['post_update_script'] ?? '')),
|
||||||
'config_file_template' => trim((string)($post['config_file_template'] ?? '')),
|
'config_file_template' => trim((string)($post['config_file_template'] ?? '')),
|
||||||
'launch_param_template' => trim((string)($post['launch_param_template'] ?? '')),
|
'launch_param_template' => trim((string)($post['launch_param_template'] ?? '')),
|
||||||
'requires_restart' => !empty($post['requires_restart']) ? 1 : 0,
|
'requires_restart' => !empty($post['requires_restart']) ? 1 : 0,
|
||||||
|
'validation_notes' => trim((string)($post['validation_notes'] ?? '')),
|
||||||
'enabled' => !empty($post['enabled']) ? 1 : 0,
|
'enabled' => !empty($post['enabled']) ? 1 : 0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -198,10 +232,13 @@ class WorkshopProfileController
|
||||||
$errors[] = $this->lang['error_app_id_required'] ?? 'Workshop App ID is required.';
|
$errors[] = $this->lang['error_app_id_required'] ?? 'Workshop App ID is required.';
|
||||||
}
|
}
|
||||||
if (($data['cache_path_template'] ?? '') === '') {
|
if (($data['cache_path_template'] ?? '') === '') {
|
||||||
$errors[] = $this->lang['error_cache_path_required'] ?? 'Cache path template is required.';
|
$errors[] = $this->lang['error_cache_path_required'] ?? 'SteamCMD cache path template is required.';
|
||||||
}
|
}
|
||||||
if (($data['install_path_template'] ?? '') === '') {
|
if (($data['install_path_template'] ?? '') === '') {
|
||||||
$errors[] = $this->lang['error_install_path_required'] ?? 'Install path template is required.';
|
$errors[] = $this->lang['error_install_path_required'] ?? 'Server install path template is required.';
|
||||||
|
}
|
||||||
|
if (($data['folder_naming_format'] ?? '') === 'custom' && ($data['folder_name_template'] ?? '') === '') {
|
||||||
|
$errors[] = $this->lang['error_folder_template_required'] ?? 'Custom folder name template is required when format is set to custom.';
|
||||||
}
|
}
|
||||||
return $errors;
|
return $errors;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,5 +199,102 @@ return [
|
||||||
'error_mod_not_found' => 'Mod or profile not found.',
|
'error_mod_not_found' => 'Mod or profile not found.',
|
||||||
'error_toggle_failed' => 'Failed to update mod status.',
|
'error_toggle_failed' => 'Failed to update mod status.',
|
||||||
'error_order_failed' => 'Failed to update load order.',
|
'error_order_failed' => 'Failed to update load order.',
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// New v2 labels
|
||||||
|
// -------------------------------------------------------
|
||||||
|
|
||||||
|
// Admin profile form – new fields
|
||||||
|
'config_steamcmd_note' => 'Workshop mods are downloaded using SteamCMD: +workshop_download_item <App ID> <Mod ID>. Configure the paths and scripts below to control how mods are installed for servers of this game type.',
|
||||||
|
'config_label_enabled' => 'Profile enabled',
|
||||||
|
'config_hint_game_key' => 'Short identifier matching the game XML key, e.g. dayz_linux',
|
||||||
|
'config_hint_app_id' => 'The App ID used with +workshop_download_item, e.g. 221100 for DayZ',
|
||||||
|
'config_hint_launch_tpl' => 'Complete launch parameter string appended to server start. Each mod folder is joined with the separator above.',
|
||||||
|
'profile_section_basic' => 'Basic identification',
|
||||||
|
'profile_section_steam' => 'Steam & SteamCMD settings',
|
||||||
|
'profile_section_paths' => 'Download & install paths',
|
||||||
|
'profile_section_folder' => 'Mod folder naming',
|
||||||
|
'profile_section_launch' => 'Launch parameters',
|
||||||
|
'profile_section_scripts' => 'Bash scripts',
|
||||||
|
'profile_section_flags' => 'Options & validation',
|
||||||
|
'profile_label_game_name' => 'Game display name',
|
||||||
|
'profile_label_steam_app_id' => 'Steam App ID',
|
||||||
|
'profile_hint_steam_app_id' => 'The Steam game App ID (e.g. 221100 for DayZ). Used when Steam login is required.',
|
||||||
|
'config_label_app_id' => 'Workshop App ID',
|
||||||
|
'config_hint_app_id' => 'The App ID used with +workshop_download_item, e.g. 221100 for DayZ',
|
||||||
|
'profile_label_steamcmd_path' => 'SteamCMD path on agent',
|
||||||
|
'profile_hint_steamcmd_path' => 'Full path to steamcmd.sh on the remote agent. Leave blank to use the agent default (/home/gameserver/steamcmd/steamcmd.sh).',
|
||||||
|
'profile_label_steam_login_required'=> 'Steam login required (game is not free / requires ownership)',
|
||||||
|
'profile_label_steamcmd_login_mode' => 'SteamCMD login mode',
|
||||||
|
'profile_hint_steamcmd_login_mode' => 'Use anonymous for free Workshop mods. Configured account mode stores the intent; full credential injection requires panel-level Steam account configuration (see admin docs).',
|
||||||
|
'profile_label_os' => 'Supported OS',
|
||||||
|
'profile_label_cache_path' => 'Workshop download/cache path',
|
||||||
|
'profile_hint_cache_path' => 'Where SteamCMD stores downloaded mod content on the agent. E.g. /home/gameserver/steamcmd/steamapps/workshop/content/%workshop_app_id%/%workshop_id%',
|
||||||
|
'profile_label_install_path' => 'Server mod install root',
|
||||||
|
'profile_hint_install_path' => 'Base directory inside the server where mods are installed. E.g. %server_path%/mods/%install_name%',
|
||||||
|
'profile_label_folder_format' => 'Folder naming format',
|
||||||
|
'profile_hint_folder_format' => 'How each mod folder is named inside the install root.',
|
||||||
|
'profile_label_folder_name' => 'Custom folder name template',
|
||||||
|
'profile_hint_folder_name' => 'Use %workshop_id% or %mod_name%. E.g. @%workshop_id%',
|
||||||
|
'profile_label_mod_launch_param' => 'Mod launch parameter format',
|
||||||
|
'profile_hint_mod_launch_param' => 'How the full mod list is passed to the server start command. E.g. -mod=%mods%',
|
||||||
|
'profile_label_mod_separator' => 'Mod separator',
|
||||||
|
'profile_hint_mod_separator' => 'Character used to join multiple mod folder names in the launch parameter.',
|
||||||
|
'profile_label_launch_tpl' => 'Full launch parameter template (optional)',
|
||||||
|
'profile_label_copy_method' => 'Copy method',
|
||||||
|
'profile_label_copy_keys' => 'Copy mod keys (*.bikey) to server keys directory',
|
||||||
|
'profile_label_key_source' => 'Key source path',
|
||||||
|
'profile_hint_key_source' => 'Path inside the mod cache where key files live. E.g. %source_path%/keys',
|
||||||
|
'profile_label_key_dest' => 'Key destination path',
|
||||||
|
'profile_hint_key_dest' => 'Where keys are copied on the server. E.g. %server_path%/keys',
|
||||||
|
'profile_label_pre_script' => 'Pre-update bash script',
|
||||||
|
'profile_hint_pre_script' => 'Runs once before any mod is downloaded/installed. Variables: %home_id% %server_path% %workshop_app_id%',
|
||||||
|
'profile_label_install_script' => 'Per-mod install bash script',
|
||||||
|
'profile_hint_install_script' => 'Runs once for each mod. All template variables listed above are available.',
|
||||||
|
'profile_label_post_script' => 'Post-update bash script',
|
||||||
|
'profile_hint_post_script' => 'Runs once after all mods have been installed. Variables: %home_id% %server_path% %workshop_app_id%',
|
||||||
|
'profile_label_requires_restart' => 'Server restart required after mod install or update',
|
||||||
|
'profile_label_validation_notes' => 'Validation notes / help text (shown to server owners)',
|
||||||
|
'profile_label_config_tpl' => 'Config file template (optional)',
|
||||||
|
'profile_template_vars' => 'Variables: %home_id% %server_path% %steam_app_id% %workshop_app_id% %workshop_id% %mod_name% %install_name% %download_path% %source_path% %target_path% %keys_source_path% %keys_target_path% %steamcmd_path%',
|
||||||
|
'profile_template_vars_heading' => 'Template variables:',
|
||||||
|
'profile_scripts_order' => 'Execution order:',
|
||||||
|
'profile_scripts_per_mod' => 'repeated for each mod',
|
||||||
|
'profile_script_example_toggle' => 'Show DayZ-style example',
|
||||||
|
'profile_col_app_ids' => 'App IDs',
|
||||||
|
'profile_col_login' => 'Login',
|
||||||
|
'profile_col_steam' => 'Steam',
|
||||||
|
'profile_col_workshop' => 'Workshop',
|
||||||
|
'profile_badge_login_required' => 'Login req.',
|
||||||
|
'profile_col_game' => 'Game',
|
||||||
|
'profile_col_key' => 'Game Key',
|
||||||
|
'profile_col_method' => 'Install Method',
|
||||||
|
'profile_col_restart' => 'Restart?',
|
||||||
|
'profile_col_status' => 'Status',
|
||||||
|
'error_folder_template_required' => 'Custom folder name template is required when format is set to custom.',
|
||||||
|
|
||||||
|
// Server-level settings (user/server page)
|
||||||
|
'heading_server_settings' => 'Workshop Settings for this server',
|
||||||
|
'label_workshop_enabled' => 'Enable Workshop for this server',
|
||||||
|
'label_select_profile' => 'Workshop game profile',
|
||||||
|
'label_auto_detect' => 'Auto-detect from game type',
|
||||||
|
'label_update_mode' => 'Update mode',
|
||||||
|
'label_restart_behavior' => 'Restart behavior',
|
||||||
|
'update_mode_manual' => 'Manual only',
|
||||||
|
'update_mode_scheduled' => 'Scheduled',
|
||||||
|
'update_mode_on_restart' => 'Before server restart',
|
||||||
|
'restart_behavior_none' => 'No restart',
|
||||||
|
'restart_behavior_queue' => 'Queue restart',
|
||||||
|
'restart_behavior_stop' => 'Stop / Update / Start',
|
||||||
|
'btn_queue_update' => 'Queue manual update',
|
||||||
|
'label_last_update_status' => 'Last update status',
|
||||||
|
'label_last_update_time' => 'Last update time',
|
||||||
|
'label_last_success_time' => 'Last successful update',
|
||||||
|
'label_last_update_error' => 'Last error',
|
||||||
|
'update_queued_notice' => 'A manual update is queued and will run on the next scheduler cycle.',
|
||||||
|
'settings_saved' => 'Workshop settings saved.',
|
||||||
|
'update_queued' => 'Manual update queued. It will run on the next scheduler cycle.',
|
||||||
|
'col_mod_folder' => 'Install folder',
|
||||||
|
'label_admin_notes' => 'Admin notes:',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,22 @@ declare(strict_types=1);
|
||||||
* WorkshopInstaller: handles mod download (via agent SteamCMD) and
|
* WorkshopInstaller: handles mod download (via agent SteamCMD) and
|
||||||
* copy/sync from agent cache to server install path.
|
* copy/sync from agent cache to server install path.
|
||||||
*
|
*
|
||||||
* Template variables supported in all paths/scripts:
|
* Template variables supported in all paths/scripts (%var% style):
|
||||||
* {home_id} numeric home id
|
* %home_id% numeric home id
|
||||||
* {agent_id} numeric remote_server_id
|
* %server_path% game server home_path
|
||||||
* {workshop_app_id} Steam app id (e.g. 221100)
|
* %steam_app_id% Steam game App ID (e.g. 221100 for DayZ)
|
||||||
* {mod_id} Workshop mod id (numeric string)
|
* %workshop_app_id% Workshop App ID used for +workshop_download_item
|
||||||
* {mod_title} mod title (sanitised)
|
* %workshop_id% Workshop mod item id (numeric)
|
||||||
* {steamcmd_path} path to steamcmd.sh / steamcmd.exe on the agent
|
* %mod_name% mod title sanitised for use as a folder name
|
||||||
* {server_path} game server home_path
|
* %install_name% resolved mod folder name (from folder_naming_format)
|
||||||
* {install_path} resolved install path for this mod
|
* %download_path% alias for %source_path% (SteamCMD cache dir for this mod)
|
||||||
* {cache_path} resolved cache path for this mod
|
* %source_path% SteamCMD cache directory for this mod
|
||||||
|
* %target_path% resolved install directory for this mod
|
||||||
|
* %keys_source_path% key source path (resolved from profile key_source_path)
|
||||||
|
* %keys_target_path% key destination path (resolved from profile key_dest_path)
|
||||||
|
* %steamcmd_path% path to steamcmd.sh on the agent
|
||||||
|
*
|
||||||
|
* Legacy {var} style placeholders are also resolved for backward compat.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/WorkshopRepository.php';
|
require_once __DIR__ . '/WorkshopRepository.php';
|
||||||
|
|
@ -43,91 +49,82 @@ class WorkshopInstaller
|
||||||
* @param array $home Row from getGameHome/getUserGameHome
|
* @param array $home Row from getGameHome/getUserGameHome
|
||||||
* @param array $profile Row from gsp_workshop_game_profiles
|
* @param array $profile Row from gsp_workshop_game_profiles
|
||||||
* @param string $workshopId Numeric workshop item id
|
* @param string $workshopId Numeric workshop item id
|
||||||
* @param string $steamCmdPath Path to steamcmd binary on the agent
|
|
||||||
* @return array{success:bool, message:string, restart_required:bool, log:list<string>}
|
* @return array{success:bool, message:string, restart_required:bool, log:list<string>}
|
||||||
*/
|
*/
|
||||||
public function install(
|
public function install(
|
||||||
array $home,
|
array $home,
|
||||||
array $profile,
|
array $profile,
|
||||||
string $workshopId,
|
string $workshopId
|
||||||
string $steamCmdPath = ''
|
|
||||||
): array {
|
): array {
|
||||||
$log = [];
|
$log = [];
|
||||||
|
|
||||||
// Validate workshop id
|
|
||||||
$workshopId = preg_replace('/[^0-9]/', '', $workshopId) ?? '';
|
$workshopId = preg_replace('/[^0-9]/', '', $workshopId) ?? '';
|
||||||
if ($workshopId === '') {
|
if ($workshopId === '') {
|
||||||
return $this->fail('Workshop ID must be numeric.', $log);
|
return $this->fail('Workshop ID must be numeric.', $log);
|
||||||
}
|
}
|
||||||
|
|
||||||
$homeId = (int)($home['home_id'] ?? 0);
|
$homeId = (int)($home['home_id'] ?? 0);
|
||||||
$agentId = (int)($home['remote_server_id'] ?? 0);
|
$agentId = (int)($home['remote_server_id'] ?? 0);
|
||||||
$appId = (string)($profile['workshop_app_id'] ?? '');
|
$appId = (string)($profile['workshop_app_id'] ?? '');
|
||||||
$osType = $this->detectOsType($home);
|
$osType = $this->detectOsType($home);
|
||||||
|
|
||||||
if ($homeId <= 0 || $agentId <= 0 || $appId === '') {
|
if ($homeId <= 0 || $agentId <= 0 || $appId === '') {
|
||||||
return $this->fail('Invalid home, agent, or app ID.', $log);
|
return $this->fail('Invalid home, agent, or app ID.', $log);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build template vars
|
|
||||||
$vars = $this->buildTemplateVars($home, $profile, $workshopId, '', $steamCmdPath);
|
|
||||||
$cachePath = $this->resolveTemplate((string)($profile['cache_path_template'] ?? ''), $vars);
|
|
||||||
$installPath = $this->resolveTemplate((string)($profile['install_path_template'] ?? ''), $vars);
|
|
||||||
$vars['{cache_path}'] = $cachePath;
|
|
||||||
$vars['{install_path}'] = $installPath;
|
|
||||||
|
|
||||||
// Build remote library
|
|
||||||
$remote = $this->buildRemote($home);
|
$remote = $this->buildRemote($home);
|
||||||
if ($remote === null) {
|
if ($remote === null) {
|
||||||
return $this->fail('Unable to connect to agent.', $log);
|
return $this->fail('Unable to connect to agent.', $log);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check agent connectivity
|
|
||||||
if ($remote->status_chk() !== 1) {
|
if ($remote->status_chk() !== 1) {
|
||||||
return $this->fail('Agent is offline.', $log);
|
return $this->fail('Agent is offline.', $log);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cache
|
// Build template vars (source/target paths filled after resolution below)
|
||||||
$cacheEntry = $this->repo->getCacheEntry($agentId, $appId, $workshopId);
|
$vars = $this->buildTemplateVars($home, $profile, $workshopId);
|
||||||
$log[] = "Cache check: agent={$agentId} app={$appId} mod={$workshopId}";
|
|
||||||
|
|
||||||
if ($cacheEntry === null || ($cacheEntry['status'] ?? '') !== 'cached') {
|
// Run pre-update script once (before mods)
|
||||||
$log[] = 'Cache MISS – triggering SteamCMD download on agent.';
|
$preScript = trim((string)($profile['pre_update_script'] ?? ''));
|
||||||
$downloadResult = $this->triggerSteamCmdDownload(
|
if ($preScript !== '') {
|
||||||
$remote, $agentId, $appId, $workshopId, $steamCmdPath, $cachePath, $log
|
$log[] = 'Running pre-update script.';
|
||||||
);
|
$this->runScript($remote, $preScript, $vars, $log);
|
||||||
|
|
||||||
if (!$downloadResult) {
|
|
||||||
// Update cache status to 'missing' so the cron can retry
|
|
||||||
$this->repo->upsertCacheEntry($agentId, $osType, $appId, $workshopId, $cachePath, 'missing');
|
|
||||||
return $this->fail(
|
|
||||||
'SteamCMD download failed. The mod will be retried on the next scheduled update.',
|
|
||||||
$log
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$log[] = 'SteamCMD download success.';
|
|
||||||
$this->repo->upsertCacheEntry($agentId, $osType, $appId, $workshopId, $cachePath, 'cached');
|
|
||||||
} else {
|
|
||||||
$log[] = 'Cache HIT – using existing cached copy.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy / sync from cache to server install path
|
// Download
|
||||||
|
$cacheResult = $this->ensureCached($remote, $agentId, $osType, $appId, $workshopId, $profile, $vars, $log);
|
||||||
|
if (!$cacheResult) {
|
||||||
|
return $this->fail('SteamCMD download failed.', $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy/sync to server
|
||||||
$syncResult = $this->syncToServer($remote, $profile, $vars, $log);
|
$syncResult = $this->syncToServer($remote, $profile, $vars, $log);
|
||||||
if (!$syncResult) {
|
if (!$syncResult) {
|
||||||
return $this->fail('Sync from cache to server failed. Check agent logs.', $log);
|
return $this->fail('Sync from cache to server failed. Check agent logs.', $log);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional install script (admin-defined only)
|
// Per-mod install script
|
||||||
$installScript = trim((string)($profile['install_script'] ?? ''));
|
$installScript = trim((string)($profile['install_script'] ?? ''));
|
||||||
if ($installScript !== '') {
|
if ($installScript !== '') {
|
||||||
$this->runInstallScript($remote, $installScript, $vars, $log);
|
$log[] = 'Running per-mod install script.';
|
||||||
|
$this->runScript($remote, $installScript, $vars, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy keys if configured
|
||||||
|
if (!empty($profile['copy_keys'])) {
|
||||||
|
$this->copyKeys($remote, $profile, $vars, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-update script
|
||||||
|
$postScript = trim((string)($profile['post_update_script'] ?? ''));
|
||||||
|
if ($postScript !== '') {
|
||||||
|
$log[] = 'Running post-update script.';
|
||||||
|
$this->runScript($remote, $postScript, $vars, $log);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record in database
|
// Record in database
|
||||||
$this->repo->insertOrUpdateMod(
|
$this->repo->insertOrUpdateMod(
|
||||||
$homeId, $agentId, (int)$profile['id'], $appId, $workshopId,
|
$homeId, $agentId, (int)$profile['id'], $appId, $workshopId,
|
||||||
$installPath, '', 0
|
$vars['%target_path%'] ?? '', '', 0
|
||||||
);
|
);
|
||||||
|
|
||||||
$restartRequired = !empty($profile['requires_restart']);
|
$restartRequired = !empty($profile['requires_restart']);
|
||||||
|
|
@ -152,14 +149,13 @@ class WorkshopInstaller
|
||||||
*/
|
*/
|
||||||
public function syncMod(array $home, array $modRow, array $profile): array
|
public function syncMod(array $home, array $modRow, array $profile): array
|
||||||
{
|
{
|
||||||
$log = [];
|
$log = [];
|
||||||
$workshopId = (string)($modRow['workshop_id'] ?? '');
|
$workshopId = (string)($modRow['workshop_id'] ?? '');
|
||||||
$agentId = (int)($modRow['agent_id'] ?? 0);
|
$agentId = (int)($modRow['agent_id'] ?? 0);
|
||||||
$appId = (string)($modRow['workshop_app_id'] ?? '');
|
$appId = (string)($modRow['workshop_app_id'] ?? '');
|
||||||
|
|
||||||
$cacheEntry = $this->repo->getCacheEntry($agentId, $appId, $workshopId);
|
$cacheEntry = $this->repo->getCacheEntry($agentId, $appId, $workshopId);
|
||||||
if ($cacheEntry === null || ($cacheEntry['status'] ?? '') !== 'cached') {
|
if ($cacheEntry === null || ($cacheEntry['status'] ?? '') !== 'cached') {
|
||||||
$log[] = "Cache entry not available for mod {$workshopId} – skipping sync.";
|
|
||||||
return ['success' => false, 'changed' => false, 'message' => 'Mod not cached yet.', 'log' => $log];
|
return ['success' => false, 'changed' => false, 'message' => 'Mod not cached yet.', 'log' => $log];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,10 +165,8 @@ class WorkshopInstaller
|
||||||
}
|
}
|
||||||
|
|
||||||
$vars = $this->buildTemplateVars($home, $profile, $workshopId, $modRow['title'] ?? '');
|
$vars = $this->buildTemplateVars($home, $profile, $workshopId, $modRow['title'] ?? '');
|
||||||
$vars['{cache_path}'] = $this->resolveTemplate((string)($profile['cache_path_template'] ?? ''), $vars);
|
|
||||||
$vars['{install_path}'] = (string)($modRow['install_path'] ?? $this->resolveTemplate((string)($profile['install_path_template'] ?? ''), $vars));
|
|
||||||
|
|
||||||
$changed = $this->checkNeedsSync($remote, $vars['{cache_path}'], $vars['{install_path}'], $profile, $log);
|
$changed = $this->checkNeedsSync($remote, $vars['%source_path%'], $vars['%target_path%'], $profile, $log);
|
||||||
if (!$changed) {
|
if (!$changed) {
|
||||||
$log[] = 'No changes detected – skipping sync.';
|
$log[] = 'No changes detected – skipping sync.';
|
||||||
return ['success' => true, 'changed' => false, 'message' => 'Already up to date.', 'log' => $log];
|
return ['success' => true, 'changed' => false, 'message' => 'Already up to date.', 'log' => $log];
|
||||||
|
|
@ -181,6 +175,16 @@ class WorkshopInstaller
|
||||||
$log[] = 'Changes detected – syncing.';
|
$log[] = 'Changes detected – syncing.';
|
||||||
$ok = $this->syncToServer($remote, $profile, $vars, $log);
|
$ok = $this->syncToServer($remote, $profile, $vars, $log);
|
||||||
|
|
||||||
|
if ($ok) {
|
||||||
|
$installScript = trim((string)($profile['install_script'] ?? ''));
|
||||||
|
if ($installScript !== '') {
|
||||||
|
$this->runScript($remote, $installScript, $vars, $log);
|
||||||
|
}
|
||||||
|
if (!empty($profile['copy_keys'])) {
|
||||||
|
$this->copyKeys($remote, $profile, $vars, $log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => $ok,
|
'success' => $ok,
|
||||||
'changed' => true,
|
'changed' => true,
|
||||||
|
|
@ -190,17 +194,34 @@ class WorkshopInstaller
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Template resolution
|
// Template resolution (public – used by WorkshopUpdater)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace template placeholders in a string.
|
* Replace template placeholders in a string.
|
||||||
|
* Supports both %var% (canonical) and {var} (legacy) style.
|
||||||
*
|
*
|
||||||
* @param array<string,string> $vars
|
* @param array<string,string> $vars
|
||||||
*/
|
*/
|
||||||
public function resolveTemplate(string $template, array $vars): string
|
public function resolveTemplate(string $template, array $vars): string
|
||||||
{
|
{
|
||||||
return str_replace(array_keys($vars), array_values($vars), $template);
|
// %var% style (canonical)
|
||||||
|
$result = str_replace(array_keys($vars), array_values($vars), $template);
|
||||||
|
|
||||||
|
// Legacy {var} style aliases – map old keys to same values
|
||||||
|
$legacy = [];
|
||||||
|
foreach ($vars as $k => $v) {
|
||||||
|
$legacyKey = '{' . trim($k, '%') . '}';
|
||||||
|
$legacy[$legacyKey] = $v;
|
||||||
|
}
|
||||||
|
// Extra legacy aliases
|
||||||
|
$legacy['{mod_id}'] = $vars['%workshop_id%'] ?? '';
|
||||||
|
$legacy['{mod_title}'] = $vars['%mod_name%'] ?? '';
|
||||||
|
$legacy['{mod_folder}'] = $vars['%install_name%'] ?? '';
|
||||||
|
$legacy['{install_path}'] = $vars['%target_path%'] ?? '';
|
||||||
|
$legacy['{cache_path}'] = $vars['%source_path%'] ?? '';
|
||||||
|
|
||||||
|
return str_replace(array_keys($legacy), array_values($legacy), $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -212,30 +233,67 @@ class WorkshopInstaller
|
||||||
array $home,
|
array $home,
|
||||||
array $profile,
|
array $profile,
|
||||||
string $workshopId,
|
string $workshopId,
|
||||||
string $modTitle = '',
|
string $modTitle = ''
|
||||||
string $steamCmdPath = ''
|
|
||||||
): array {
|
): array {
|
||||||
$serverPath = rtrim((string)($home['home_path'] ?? ''), '/');
|
$serverPath = rtrim((string)($home['home_path'] ?? ''), '/');
|
||||||
$safeName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $modTitle) ?? '';
|
$steamcmdPath = trim((string)($profile['steamcmd_path'] ?? ''));
|
||||||
|
if ($steamcmdPath === '') {
|
||||||
|
$steamcmdPath = '/home/gameserver/steamcmd/steamcmd.sh';
|
||||||
|
}
|
||||||
|
|
||||||
$folderNameTpl = (string)($profile['folder_name_template'] ?? '@{mod_id}');
|
$safeName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $modTitle) ?? '';
|
||||||
$folderNameVars = [
|
|
||||||
'{mod_id}' => $workshopId,
|
// Resolve folder name from format
|
||||||
'{mod_title}' => $safeName,
|
$folderFormat = (string)($profile['folder_naming_format'] ?? '@%workshop_id%');
|
||||||
];
|
if ($folderFormat === '@%mod_name%') {
|
||||||
$folderName = str_replace(array_keys($folderNameVars), array_values($folderNameVars), $folderNameTpl);
|
$installName = '@' . $safeName;
|
||||||
|
} elseif ($folderFormat === '@%workshop_id%') {
|
||||||
|
$installName = '@' . $workshopId;
|
||||||
|
} else {
|
||||||
|
// custom – use folder_name_template as-is, resolve %workshop_id%/%mod_name% inline
|
||||||
|
$tpl = (string)($profile['folder_name_template'] ?? '@%workshop_id%');
|
||||||
|
$installName = str_replace(['%workshop_id%', '%mod_name%'], [$workshopId, $safeName], $tpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
$steamAppId = (string)($profile['steam_app_id'] ?? '');
|
||||||
|
$workshopAppId = (string)($profile['workshop_app_id'] ?? '');
|
||||||
|
|
||||||
|
// Resolve cache/source path template
|
||||||
|
$cachePathTpl = (string)($profile['cache_path_template'] ?? '');
|
||||||
|
$sourcePath = str_replace(
|
||||||
|
['%workshop_app_id%', '%workshop_id%', '%mod_name%', '%install_name%', '%steam_app_id%', '%steamcmd_path%'],
|
||||||
|
[$workshopAppId, $workshopId, $safeName, $installName, $steamAppId, dirname($steamcmdPath)],
|
||||||
|
$cachePathTpl
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resolve target/install path template
|
||||||
|
$installPathTpl = (string)($profile['install_path_template'] ?? '');
|
||||||
|
$targetPath = str_replace(
|
||||||
|
['%server_path%', '%workshop_app_id%', '%workshop_id%', '%mod_name%', '%install_name%', '%steam_app_id%'],
|
||||||
|
[$serverPath, $workshopAppId, $workshopId, $safeName, $installName, $steamAppId],
|
||||||
|
$installPathTpl
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resolve key paths
|
||||||
|
$keySourceRaw = (string)($profile['key_source_path'] ?? '');
|
||||||
|
$keyDestRaw = (string)($profile['key_dest_path'] ?? '');
|
||||||
|
$keySource = str_replace(['%source_path%', '%server_path%'], [$sourcePath, $serverPath], $keySourceRaw);
|
||||||
|
$keyDest = str_replace(['%target_path%', '%server_path%'], [$targetPath, $serverPath], $keyDestRaw);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'{home_id}' => (string)($home['home_id'] ?? ''),
|
'%home_id%' => (string)($home['home_id'] ?? ''),
|
||||||
'{agent_id}' => (string)($home['remote_server_id'] ?? ''),
|
'%server_path%' => $serverPath,
|
||||||
'{workshop_app_id}' => (string)($profile['workshop_app_id'] ?? ''),
|
'%steam_app_id%' => $steamAppId,
|
||||||
'{mod_id}' => $workshopId,
|
'%workshop_app_id%' => $workshopAppId,
|
||||||
'{mod_title}' => $safeName,
|
'%workshop_id%' => $workshopId,
|
||||||
'{mod_folder}' => $folderName,
|
'%mod_name%' => $safeName,
|
||||||
'{steamcmd_path}' => $steamCmdPath !== '' ? $steamCmdPath : '/home/gameserver/steamcmd',
|
'%install_name%' => $installName,
|
||||||
'{server_path}' => $serverPath,
|
'%download_path%' => $sourcePath,
|
||||||
'{install_path}' => '', // filled by caller after resolution
|
'%source_path%' => $sourcePath,
|
||||||
'{cache_path}' => '', // filled by caller after resolution
|
'%target_path%' => $targetPath,
|
||||||
|
'%keys_source_path%' => $keySource,
|
||||||
|
'%keys_target_path%' => $keyDest,
|
||||||
|
'%steamcmd_path%' => $steamcmdPath,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,6 +301,44 @@ class WorkshopInstaller
|
||||||
// Private helpers
|
// Private helpers
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a mod is downloaded/cached on the agent.
|
||||||
|
* Returns true if cached and available.
|
||||||
|
*
|
||||||
|
* @param list<string> $log
|
||||||
|
*/
|
||||||
|
private function ensureCached(
|
||||||
|
object $remote,
|
||||||
|
int $agentId,
|
||||||
|
string $osType,
|
||||||
|
string $appId,
|
||||||
|
string $workshopId,
|
||||||
|
array $profile,
|
||||||
|
array &$vars,
|
||||||
|
array &$log
|
||||||
|
): bool {
|
||||||
|
$sourcePath = $vars['%source_path%'];
|
||||||
|
|
||||||
|
$cacheEntry = $this->repo->getCacheEntry($agentId, $appId, $workshopId);
|
||||||
|
$log[] = "Cache check: agent={$agentId} app={$appId} mod={$workshopId}";
|
||||||
|
|
||||||
|
if ($cacheEntry !== null && ($cacheEntry['status'] ?? '') === 'cached') {
|
||||||
|
$log[] = 'Cache HIT – using existing cached copy.';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$log[] = 'Cache MISS – triggering SteamCMD download on agent.';
|
||||||
|
$ok = $this->triggerSteamCmdDownload($remote, $agentId, $appId, $workshopId, $profile, $sourcePath, $log);
|
||||||
|
|
||||||
|
$status = $ok ? 'cached' : 'missing';
|
||||||
|
$this->repo->upsertCacheEntry($agentId, $osType, $appId, $workshopId, $sourcePath, $status);
|
||||||
|
|
||||||
|
if ($ok) {
|
||||||
|
$log[] = 'SteamCMD download success.';
|
||||||
|
}
|
||||||
|
return $ok;
|
||||||
|
}
|
||||||
|
|
||||||
/** Build an OGPRemoteLibrary instance from a home row. */
|
/** Build an OGPRemoteLibrary instance from a home row. */
|
||||||
private function buildRemote(array $home): ?object
|
private function buildRemote(array $home): ?object
|
||||||
{
|
{
|
||||||
|
|
@ -253,9 +349,9 @@ class WorkshopInstaller
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ip = (string)($home['agent_ip'] ?? '');
|
$ip = (string)($home['agent_ip'] ?? '');
|
||||||
$port = (string)($home['agent_port'] ?? '');
|
$port = (string)($home['agent_port'] ?? '');
|
||||||
$key = (string)($home['encryption_key'] ?? '');
|
$key = (string)($home['encryption_key'] ?? '');
|
||||||
$timeout = isset($home['timeout']) ? (int)$home['timeout'] : 30;
|
$timeout = isset($home['timeout']) ? (int)$home['timeout'] : 30;
|
||||||
|
|
||||||
if ($ip === '' || $port === '') {
|
if ($ip === '' || $port === '') {
|
||||||
|
|
@ -266,7 +362,7 @@ class WorkshopInstaller
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a SteamCMD workshop_download_item on the agent via exec().
|
* Trigger a SteamCMD workshop_download_item on the agent.
|
||||||
* Returns true on success.
|
* Returns true on success.
|
||||||
*
|
*
|
||||||
* @param list<string> $log
|
* @param list<string> $log
|
||||||
|
|
@ -276,36 +372,43 @@ class WorkshopInstaller
|
||||||
int $agentId,
|
int $agentId,
|
||||||
string $appId,
|
string $appId,
|
||||||
string $workshopId,
|
string $workshopId,
|
||||||
string $steamCmdPath,
|
array $profile,
|
||||||
string $cachePath,
|
string $cachePath,
|
||||||
array &$log
|
array &$log
|
||||||
): bool {
|
): bool {
|
||||||
if ($steamCmdPath === '') {
|
$steamcmdPath = trim((string)($profile['steamcmd_path'] ?? ''));
|
||||||
$steamCmdPath = '/home/gameserver/steamcmd/steamcmd.sh';
|
if ($steamcmdPath === '') {
|
||||||
|
$steamcmdPath = '/home/gameserver/steamcmd/steamcmd.sh';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$loginMode = (string)($profile['steamcmd_login_mode'] ?? 'anonymous');
|
||||||
|
// TODO: When login_mode is 'account', replace 'anonymous' with the
|
||||||
|
// configured SteamCMD credentials (username + password) loaded from
|
||||||
|
// a secure panel-side credential store. Until that feature is
|
||||||
|
// implemented, 'account' mode logs in anonymously (which works for
|
||||||
|
// free/publicly-accessible Workshop items).
|
||||||
|
$loginArg = 'anonymous';
|
||||||
|
|
||||||
$cmd = implode(' ', [
|
$cmd = implode(' ', [
|
||||||
escapeshellarg($steamCmdPath),
|
escapeshellarg($steamcmdPath),
|
||||||
'+login', 'anonymous',
|
'+login', escapeshellarg($loginArg),
|
||||||
'+workshop_download_item', escapeshellarg($appId), escapeshellarg($workshopId),
|
'+workshop_download_item', escapeshellarg($appId), escapeshellarg($workshopId),
|
||||||
'validate',
|
'validate',
|
||||||
'+quit',
|
'+quit',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$log[] = "SteamCMD start: {$cmd}";
|
$log[] = "SteamCMD start: agent={$agentId} app={$appId} mod={$workshopId}";
|
||||||
$this->writeLog("STEAMCMD START agent={$agentId} app={$appId} mod={$workshopId}");
|
$this->writeLog("STEAMCMD START agent={$agentId} app={$appId} mod={$workshopId}");
|
||||||
|
|
||||||
$output = $remote->exec($cmd);
|
$output = $remote->exec($cmd);
|
||||||
|
|
||||||
if ($output === null) {
|
if ($output === null) {
|
||||||
$log[] = 'SteamCMD: no response from agent (command may still be running).';
|
$log[] = 'SteamCMD: no response from agent (command may still be running).';
|
||||||
$this->writeLog("STEAMCMD NO_RESPONSE agent={$agentId} app={$appId} mod={$workshopId}");
|
|
||||||
// Treat as unknown – check file existence
|
|
||||||
} else {
|
} else {
|
||||||
$log[] = 'SteamCMD output: ' . substr((string)$output, 0, 500);
|
$log[] = 'SteamCMD output: ' . substr((string)$output, 0, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the download succeeded by checking for the cache path on the agent
|
// Verify by checking whether the cache path now exists
|
||||||
$exists = $remote->rfile_exists($cachePath);
|
$exists = $remote->rfile_exists($cachePath);
|
||||||
if ($exists === 1) {
|
if ($exists === 1) {
|
||||||
$this->writeLog("STEAMCMD SUCCESS agent={$agentId} app={$appId} mod={$workshopId} path={$cachePath}");
|
$this->writeLog("STEAMCMD SUCCESS agent={$agentId} app={$appId} mod={$workshopId} path={$cachePath}");
|
||||||
|
|
@ -317,52 +420,33 @@ class WorkshopInstaller
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if cache path differs from install path using a dry-run compare.
|
* Check if cache path differs from install path (dry-run compare).
|
||||||
* Returns true if sync is needed.
|
* Returns true if sync is needed.
|
||||||
*
|
*
|
||||||
* @param list<string> $log
|
* @param list<string> $log
|
||||||
*/
|
*/
|
||||||
private function checkNeedsSync(
|
private function checkNeedsSync(
|
||||||
object $remote,
|
object $remote,
|
||||||
string $cachePath,
|
string $sourcePath,
|
||||||
string $installPath,
|
string $targetPath,
|
||||||
array $profile,
|
array $profile,
|
||||||
array &$log
|
array &$log
|
||||||
): bool {
|
): bool {
|
||||||
$copyMethod = (string)($profile['copy_method'] ?? 'rsync');
|
$copyMethod = (string)($profile['copy_method'] ?? 'rsync');
|
||||||
$log[] = "Pre-start compare: cache={$cachePath} dest={$installPath} method={$copyMethod}";
|
$log[] = "Pre-start compare: source={$sourcePath} target={$targetPath} method={$copyMethod}";
|
||||||
|
|
||||||
if ($copyMethod === 'rsync') {
|
if ($copyMethod === 'rsync') {
|
||||||
// Dry-run: any output lines (beyond the exit sentinel) mean changes exist
|
$cmd = sprintf(
|
||||||
$cmd = sprintf(
|
|
||||||
'rsync -rcn --delete %s %s 2>/dev/null; echo "RSYNC_EXIT:$?"',
|
'rsync -rcn --delete %s %s 2>/dev/null; echo "RSYNC_EXIT:$?"',
|
||||||
escapeshellarg(rtrim($cachePath, '/') . '/'),
|
escapeshellarg(rtrim($sourcePath, '/') . '/'),
|
||||||
escapeshellarg(rtrim($installPath, '/') . '/')
|
escapeshellarg(rtrim($targetPath, '/') . '/')
|
||||||
);
|
);
|
||||||
$out = (string)$remote->exec($cmd);
|
$out = (string)$remote->exec($cmd);
|
||||||
// Strip the exit line, then check for any non-whitespace output
|
|
||||||
$body = preg_replace('/RSYNC_EXIT:\d+\s*$/', '', $out) ?? '';
|
$body = preg_replace('/RSYNC_EXIT:\d+\s*$/', '', $out) ?? '';
|
||||||
return preg_match('/\S/', $body) === 1;
|
return preg_match('/\S/', $body) === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($copyMethod === 'robocopy') {
|
// copy / symlink: always sync
|
||||||
// List-only mode: robocopy exit code 0 = no differences, 1+ = changes or errors.
|
|
||||||
// Embed the exit code in output so we can read it back via exec().
|
|
||||||
$cmd = sprintf(
|
|
||||||
'robocopy /L /MIR /NJH /NJS %s %s; echo "ROBOCOPY_EXIT:$LASTEXITCODE"',
|
|
||||||
escapeshellarg($cachePath),
|
|
||||||
escapeshellarg($installPath)
|
|
||||||
);
|
|
||||||
$out = (string)$remote->exec($cmd);
|
|
||||||
if (preg_match('/ROBOCOPY_EXIT:(\d+)/', $out, $m)) {
|
|
||||||
// 0 = no change; 1–7 = informational (changes found); 8+ = error
|
|
||||||
return (int)$m[1] !== 0;
|
|
||||||
}
|
|
||||||
// If we cannot determine, assume sync is needed
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// custom_script: always sync
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,98 +459,110 @@ class WorkshopInstaller
|
||||||
private function syncToServer(
|
private function syncToServer(
|
||||||
object $remote,
|
object $remote,
|
||||||
array $profile,
|
array $profile,
|
||||||
array $vars,
|
array &$vars,
|
||||||
array &$log
|
array &$log
|
||||||
): bool {
|
): bool {
|
||||||
$copyMethod = (string)($profile['copy_method'] ?? 'rsync');
|
$copyMethod = (string)($profile['copy_method'] ?? 'rsync');
|
||||||
$cachePath = $vars['{cache_path}'] ?? '';
|
$sourcePath = $vars['%source_path%'];
|
||||||
$installPath = $vars['{install_path}'] ?? '';
|
$targetPath = $vars['%target_path%'];
|
||||||
|
|
||||||
if ($cachePath === '' || $installPath === '') {
|
if ($sourcePath === '' || $targetPath === '') {
|
||||||
$log[] = 'Sync skipped: empty cache or install path.';
|
$log[] = 'Sync skipped: empty source or target path.';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$log[] = "Sync start: method={$copyMethod} cache={$cachePath} dest={$installPath}";
|
$log[] = "Sync start: method={$copyMethod} source={$sourcePath} target={$targetPath}";
|
||||||
$this->writeLog("COPY START method={$copyMethod} cache={$cachePath} dest={$installPath}");
|
$this->writeLog("COPY START method={$copyMethod} source={$sourcePath} target={$targetPath}");
|
||||||
|
|
||||||
if ($copyMethod === 'rsync') {
|
if ($copyMethod === 'rsync') {
|
||||||
$cmd = sprintf(
|
$cmd = sprintf(
|
||||||
'mkdir -p %s && rsync -a --delete %s %s 2>&1; echo "EXIT:$?"',
|
'mkdir -p %s && rsync -a --delete %s %s 2>&1; echo "EXIT:$?"',
|
||||||
escapeshellarg($installPath),
|
escapeshellarg($targetPath),
|
||||||
escapeshellarg(rtrim($cachePath, '/') . '/'),
|
escapeshellarg(rtrim($sourcePath, '/') . '/'),
|
||||||
escapeshellarg(rtrim($installPath, '/') . '/')
|
escapeshellarg(rtrim($targetPath, '/') . '/')
|
||||||
);
|
);
|
||||||
} elseif ($copyMethod === 'robocopy') {
|
} elseif ($copyMethod === 'symlink') {
|
||||||
$cmd = sprintf(
|
$cmd = sprintf(
|
||||||
'robocopy /MIR /NJH /NJS %s %s; echo "ROBOCOPY_EXIT:$LASTEXITCODE"',
|
'mkdir -p %s && ln -sfn %s %s 2>&1; echo "EXIT:$?"',
|
||||||
escapeshellarg($cachePath),
|
escapeshellarg(dirname($targetPath)),
|
||||||
escapeshellarg($installPath)
|
escapeshellarg($sourcePath),
|
||||||
|
escapeshellarg($targetPath)
|
||||||
);
|
);
|
||||||
} elseif ($copyMethod === 'custom_script') {
|
|
||||||
$script = trim((string)($profile['install_script'] ?? ''));
|
|
||||||
if ($script === '') {
|
|
||||||
$log[] = 'custom_script requested but install_script is empty – falling back to rsync.';
|
|
||||||
$cmd = sprintf(
|
|
||||||
'mkdir -p %s && rsync -a --delete %s %s 2>&1; echo "EXIT:$?"',
|
|
||||||
escapeshellarg($installPath),
|
|
||||||
escapeshellarg(rtrim($cachePath, '/') . '/'),
|
|
||||||
escapeshellarg(rtrim($installPath, '/') . '/')
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// The admin-defined script is templated; execute it via the agent exec()
|
|
||||||
$resolvedScript = $this->resolveTemplate($script, $vars);
|
|
||||||
$cmd = $resolvedScript . ' 2>&1; echo "EXIT:$?"';
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$log[] = "Unknown copy method '{$copyMethod}'.";
|
// 'copy' – basic cp
|
||||||
return false;
|
$cmd = sprintf(
|
||||||
|
'mkdir -p %s && cp -r %s %s 2>&1; echo "EXIT:$?"',
|
||||||
|
escapeshellarg($targetPath),
|
||||||
|
escapeshellarg(rtrim($sourcePath, '/') . '/.'),
|
||||||
|
escapeshellarg($targetPath)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$out = (string)$remote->exec($cmd);
|
$out = (string)$remote->exec($cmd);
|
||||||
$log[] = 'Sync output: ' . substr($out, 0, 500);
|
$log[] = 'Sync output: ' . substr($out, 0, 500);
|
||||||
|
|
||||||
// Determine success from embedded exit code sentinel
|
if (preg_match('/EXIT:(\d+)/', $out, $m)) {
|
||||||
if ($copyMethod === 'robocopy') {
|
$ok = (int)$m[1] === 0;
|
||||||
if (preg_match('/ROBOCOPY_EXIT:(\d+)/', $out, $m)) {
|
|
||||||
// 0–7 = success/informational; 8+ = error
|
|
||||||
$ok = (int)$m[1] < 8;
|
|
||||||
} else {
|
|
||||||
$ok = true; // assume success if no code extracted
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (preg_match('/EXIT:(\d+)/', $out, $m)) {
|
$ok = true;
|
||||||
$ok = (int)$m[1] === 0;
|
|
||||||
} else {
|
|
||||||
$ok = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($ok) {
|
if ($ok) {
|
||||||
$log[] = 'Sync success.';
|
$log[] = 'Sync success.';
|
||||||
$this->writeLog("COPY SUCCESS cache={$cachePath} dest={$installPath}");
|
$this->writeLog("COPY SUCCESS source={$sourcePath} target={$targetPath}");
|
||||||
} else {
|
} else {
|
||||||
$log[] = 'Sync failed (non-zero exit).';
|
$log[] = 'Sync failed (non-zero exit).';
|
||||||
$this->writeLog("COPY FAILURE cache={$cachePath} dest={$installPath}");
|
$this->writeLog("COPY FAILURE source={$sourcePath} target={$targetPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $ok;
|
return $ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the admin-defined install script on the agent.
|
* Copy key files from the mod's keys directory to the server keys directory.
|
||||||
*
|
*
|
||||||
* @param array<string,string> $vars
|
* @param array<string,string> $vars
|
||||||
* @param list<string> $log
|
* @param list<string> $log
|
||||||
*/
|
*/
|
||||||
private function runInstallScript(
|
private function copyKeys(
|
||||||
|
object $remote,
|
||||||
|
array $profile,
|
||||||
|
array $vars,
|
||||||
|
array &$log
|
||||||
|
): void {
|
||||||
|
$keySrc = $vars['%keys_source_path%'];
|
||||||
|
$keyDest = $vars['%keys_target_path%'];
|
||||||
|
|
||||||
|
if ($keySrc === '' || $keyDest === '') {
|
||||||
|
$log[] = 'Key copy skipped: key paths not configured.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$log[] = "Copying keys: {$keySrc} → {$keyDest}";
|
||||||
|
$cmd = sprintf(
|
||||||
|
'if [ -d %s ]; then mkdir -p %s && cp -f %s/*.bikey %s/ 2>/dev/null; fi; echo "EXIT:$?"',
|
||||||
|
escapeshellarg($keySrc),
|
||||||
|
escapeshellarg($keyDest),
|
||||||
|
escapeshellarg($keySrc),
|
||||||
|
escapeshellarg($keyDest)
|
||||||
|
);
|
||||||
|
$out = (string)$remote->exec($cmd);
|
||||||
|
$log[] = 'Key copy output: ' . substr($out, 0, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run an admin-defined bash script on the agent after resolving template vars.
|
||||||
|
*
|
||||||
|
* @param array<string,string> $vars
|
||||||
|
* @param list<string> $log
|
||||||
|
*/
|
||||||
|
private function runScript(
|
||||||
object $remote,
|
object $remote,
|
||||||
string $script,
|
string $script,
|
||||||
array $vars,
|
array $vars,
|
||||||
array &$log
|
array &$log
|
||||||
): void {
|
): void {
|
||||||
$resolved = $this->resolveTemplate($script, $vars);
|
$resolved = $this->resolveTemplate($script, $vars);
|
||||||
$log[] = 'Running install script.';
|
|
||||||
$out = (string)$remote->exec($resolved . ' 2>&1');
|
$out = (string)$remote->exec($resolved . ' 2>&1');
|
||||||
$log[] = 'Script output: ' . substr($out, 0, 500);
|
$log[] = 'Script output: ' . substr($out, 0, 500);
|
||||||
$this->writeLog('SCRIPT OUTPUT: ' . substr($out, 0, 1000));
|
$this->writeLog('SCRIPT OUTPUT: ' . substr($out, 0, 1000));
|
||||||
|
|
@ -475,10 +571,7 @@ class WorkshopInstaller
|
||||||
private function detectOsType(array $home): string
|
private function detectOsType(array $home): string
|
||||||
{
|
{
|
||||||
$gameKey = strtolower((string)($home['game_key'] ?? ''));
|
$gameKey = strtolower((string)($home['game_key'] ?? ''));
|
||||||
if (preg_match('/win/', $gameKey)) {
|
return preg_match('/win/', $gameKey) ? 'windows' : 'linux';
|
||||||
return 'windows';
|
|
||||||
}
|
|
||||||
return 'linux';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function writeLog(string $message): void
|
private function writeLog(string $message): void
|
||||||
|
|
|
||||||
|
|
@ -97,35 +97,69 @@ class WorkshopRepository
|
||||||
|
|
||||||
$gameKey = $this->esc($data['game_key'] ?? '');
|
$gameKey = $this->esc($data['game_key'] ?? '');
|
||||||
$gameName = $this->esc($data['game_name'] ?? '');
|
$gameName = $this->esc($data['game_name'] ?? '');
|
||||||
|
$steamAppId = $this->esc($data['steam_app_id'] ?? '');
|
||||||
$workshopAppId = $this->esc($data['workshop_app_id'] ?? '');
|
$workshopAppId = $this->esc($data['workshop_app_id'] ?? '');
|
||||||
|
$steamLoginRequired = empty($data['steam_login_required']) ? 0 : 1;
|
||||||
|
$steamcmdLoginMode = in_array($data['steamcmd_login_mode'] ?? '', ['anonymous', 'account'], true)
|
||||||
|
? $this->esc($data['steamcmd_login_mode'])
|
||||||
|
: 'anonymous';
|
||||||
|
$steamcmdPath = $this->esc($data['steamcmd_path'] ?? '');
|
||||||
$supportedOs = $this->esc($data['supported_os'] ?? 'linux');
|
$supportedOs = $this->esc($data['supported_os'] ?? 'linux');
|
||||||
$cachePathTpl = $this->esc($data['cache_path_template'] ?? '');
|
$cachePathTpl = $this->esc($data['cache_path_template'] ?? '');
|
||||||
$installPathTpl = $this->esc($data['install_path_template'] ?? '');
|
$installPathTpl = $this->esc($data['install_path_template'] ?? '');
|
||||||
$folderNameTpl = $this->esc($data['folder_name_template'] ?? '@{mod_id}');
|
$folderNamingFormat = in_array($data['folder_naming_format'] ?? '', ['@%mod_name%', '@%workshop_id%', 'custom'], true)
|
||||||
$copyMethod = $this->esc($data['copy_method'] ?? 'rsync');
|
? $this->esc($data['folder_naming_format'])
|
||||||
$installScript = isset($data['install_script']) && $data['install_script'] !== '' ? "'" . $this->esc($data['install_script']) . "'" : 'NULL';
|
: '@%workshop_id%';
|
||||||
$configFileTpl = isset($data['config_file_template']) && $data['config_file_template'] !== '' ? "'" . $this->esc($data['config_file_template']) . "'" : 'NULL';
|
$folderNameTpl = $this->esc($data['folder_name_template'] ?? '@%workshop_id%');
|
||||||
$launchParamTpl = isset($data['launch_param_template']) && $data['launch_param_template'] !== '' ? "'" . $this->esc($data['launch_param_template']) . "'" : 'NULL';
|
$modLaunchParam = $this->esc($data['mod_launch_param'] ?? '');
|
||||||
|
$modSeparator = in_array($data['mod_separator'] ?? '', ['semicolon', 'comma', 'space'], true)
|
||||||
|
? $this->esc($data['mod_separator'])
|
||||||
|
: 'semicolon';
|
||||||
|
$copyMethod = in_array($data['copy_method'] ?? '', ['copy', 'rsync', 'symlink'], true)
|
||||||
|
? $this->esc($data['copy_method'])
|
||||||
|
: 'rsync';
|
||||||
|
$copyKeys = empty($data['copy_keys']) ? 0 : 1;
|
||||||
|
$keySourcePath = $this->nullOrStr($data['key_source_path'] ?? '');
|
||||||
|
$keyDestPath = $this->nullOrStr($data['key_dest_path'] ?? '');
|
||||||
|
$preUpdateScript = $this->nullOrStr($data['pre_update_script'] ?? '');
|
||||||
|
$installScript = $this->nullOrStr($data['install_script'] ?? '');
|
||||||
|
$postUpdateScript = $this->nullOrStr($data['post_update_script'] ?? '');
|
||||||
|
$configFileTpl = $this->nullOrStr($data['config_file_template'] ?? '');
|
||||||
|
$launchParamTpl = $this->nullOrStr($data['launch_param_template'] ?? '');
|
||||||
$requiresRestart = empty($data['requires_restart']) ? 0 : 1;
|
$requiresRestart = empty($data['requires_restart']) ? 0 : 1;
|
||||||
|
$validationNotes = $this->nullOrStr($data['validation_notes'] ?? '');
|
||||||
$enabled = isset($data['enabled']) && !$data['enabled'] ? 0 : 1;
|
$enabled = isset($data['enabled']) && !$data['enabled'] ? 0 : 1;
|
||||||
|
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
$this->exec(
|
$this->exec(
|
||||||
"UPDATE `{$this->prefix}workshop_game_profiles` SET
|
"UPDATE `{$this->prefix}workshop_game_profiles` SET
|
||||||
game_key = '{$gameKey}',
|
game_key = '{$gameKey}',
|
||||||
game_name = '{$gameName}',
|
game_name = '{$gameName}',
|
||||||
workshop_app_id = '{$workshopAppId}',
|
steam_app_id = '{$steamAppId}',
|
||||||
supported_os = '{$supportedOs}',
|
workshop_app_id = '{$workshopAppId}',
|
||||||
cache_path_template = '{$cachePathTpl}',
|
steam_login_required = {$steamLoginRequired},
|
||||||
|
steamcmd_login_mode = '{$steamcmdLoginMode}',
|
||||||
|
steamcmd_path = '{$steamcmdPath}',
|
||||||
|
supported_os = '{$supportedOs}',
|
||||||
|
cache_path_template = '{$cachePathTpl}',
|
||||||
install_path_template = '{$installPathTpl}',
|
install_path_template = '{$installPathTpl}',
|
||||||
folder_name_template = '{$folderNameTpl}',
|
folder_naming_format = '{$folderNamingFormat}',
|
||||||
copy_method = '{$copyMethod}',
|
folder_name_template = '{$folderNameTpl}',
|
||||||
install_script = {$installScript},
|
mod_launch_param = '{$modLaunchParam}',
|
||||||
config_file_template = {$configFileTpl},
|
mod_separator = '{$modSeparator}',
|
||||||
|
copy_method = '{$copyMethod}',
|
||||||
|
copy_keys = {$copyKeys},
|
||||||
|
key_source_path = {$keySourcePath},
|
||||||
|
key_dest_path = {$keyDestPath},
|
||||||
|
pre_update_script = {$preUpdateScript},
|
||||||
|
install_script = {$installScript},
|
||||||
|
post_update_script = {$postUpdateScript},
|
||||||
|
config_file_template = {$configFileTpl},
|
||||||
launch_param_template = {$launchParamTpl},
|
launch_param_template = {$launchParamTpl},
|
||||||
requires_restart = {$requiresRestart},
|
requires_restart = {$requiresRestart},
|
||||||
enabled = {$enabled},
|
validation_notes = {$validationNotes},
|
||||||
updated_at = NOW()
|
enabled = {$enabled},
|
||||||
|
updated_at = NOW()
|
||||||
WHERE id = {$id}"
|
WHERE id = {$id}"
|
||||||
);
|
);
|
||||||
return $id;
|
return $id;
|
||||||
|
|
@ -133,17 +167,31 @@ class WorkshopRepository
|
||||||
|
|
||||||
$this->exec(
|
$this->exec(
|
||||||
"INSERT INTO `{$this->prefix}workshop_game_profiles`
|
"INSERT INTO `{$this->prefix}workshop_game_profiles`
|
||||||
(game_key, game_name, workshop_app_id, supported_os, cache_path_template,
|
(game_key, game_name, steam_app_id, workshop_app_id, steam_login_required,
|
||||||
install_path_template, folder_name_template, copy_method, install_script,
|
steamcmd_login_mode, steamcmd_path, supported_os, cache_path_template,
|
||||||
config_file_template, launch_param_template, requires_restart, enabled, created_at)
|
install_path_template, folder_naming_format, folder_name_template,
|
||||||
|
mod_launch_param, mod_separator, copy_method, copy_keys,
|
||||||
|
key_source_path, key_dest_path, pre_update_script, install_script,
|
||||||
|
post_update_script, config_file_template, launch_param_template,
|
||||||
|
requires_restart, validation_notes, enabled, created_at)
|
||||||
VALUES
|
VALUES
|
||||||
('{$gameKey}', '{$gameName}', '{$workshopAppId}', '{$supportedOs}', '{$cachePathTpl}',
|
('{$gameKey}', '{$gameName}', '{$steamAppId}', '{$workshopAppId}', {$steamLoginRequired},
|
||||||
'{$installPathTpl}', '{$folderNameTpl}', '{$copyMethod}', {$installScript},
|
'{$steamcmdLoginMode}', '{$steamcmdPath}', '{$supportedOs}', '{$cachePathTpl}',
|
||||||
{$configFileTpl}, {$launchParamTpl}, {$requiresRestart}, {$enabled}, NOW())"
|
'{$installPathTpl}', '{$folderNamingFormat}', '{$folderNameTpl}',
|
||||||
|
'{$modLaunchParam}', '{$modSeparator}', '{$copyMethod}', {$copyKeys},
|
||||||
|
{$keySourcePath}, {$keyDestPath}, {$preUpdateScript}, {$installScript},
|
||||||
|
{$postUpdateScript}, {$configFileTpl}, {$launchParamTpl},
|
||||||
|
{$requiresRestart}, {$validationNotes}, {$enabled}, NOW())"
|
||||||
);
|
);
|
||||||
return $this->lastInsertId();
|
return $this->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Return NULL or an escaped quoted string, for optional TEXT columns. */
|
||||||
|
private function nullOrStr(string $value): string
|
||||||
|
{
|
||||||
|
return $value !== '' ? "'" . $this->esc($value) . "'" : 'NULL';
|
||||||
|
}
|
||||||
|
|
||||||
public function deleteProfile(int $id): bool
|
public function deleteProfile(int $id): bool
|
||||||
{
|
{
|
||||||
return $this->exec(
|
return $this->exec(
|
||||||
|
|
@ -267,7 +315,7 @@ class WorkshopRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a new mod row or update the existing one (upsert by home_id + workshop_id).
|
* Insert (id = 0) or update (id > 0) a Workshop mod entry for a game home.
|
||||||
* Returns the row id.
|
* Returns the row id.
|
||||||
*/
|
*/
|
||||||
public function insertOrUpdateMod(
|
public function insertOrUpdateMod(
|
||||||
|
|
@ -278,12 +326,14 @@ class WorkshopRepository
|
||||||
string $workshopId,
|
string $workshopId,
|
||||||
string $installPath,
|
string $installPath,
|
||||||
string $title = '',
|
string $title = '',
|
||||||
int $loadOrder = 0
|
int $loadOrder = 0,
|
||||||
|
string $customFolder = ''
|
||||||
): int {
|
): int {
|
||||||
$appId = $this->esc($appId);
|
$appId = $this->esc($appId);
|
||||||
$workshopId = $this->esc($workshopId);
|
$workshopId = $this->esc($workshopId);
|
||||||
$installPath = $this->esc($installPath);
|
$installPath = $this->esc($installPath);
|
||||||
$title = $this->esc($title);
|
$title = $this->esc($title);
|
||||||
|
$customFolder = $this->esc($customFolder);
|
||||||
|
|
||||||
$existing = $this->getServerMod($homeId, $workshopId);
|
$existing = $this->getServerMod($homeId, $workshopId);
|
||||||
|
|
||||||
|
|
@ -294,6 +344,7 @@ class WorkshopRepository
|
||||||
profile_id = {$profileId},
|
profile_id = {$profileId},
|
||||||
workshop_app_id = '{$appId}',
|
workshop_app_id = '{$appId}',
|
||||||
title = '{$title}',
|
title = '{$title}',
|
||||||
|
custom_folder = '{$customFolder}',
|
||||||
install_path = '{$installPath}',
|
install_path = '{$installPath}',
|
||||||
load_order = {$loadOrder},
|
load_order = {$loadOrder},
|
||||||
enabled = 1,
|
enabled = 1,
|
||||||
|
|
@ -305,9 +356,9 @@ class WorkshopRepository
|
||||||
|
|
||||||
$this->exec(
|
$this->exec(
|
||||||
"INSERT INTO `{$this->prefix}server_workshop_mods`
|
"INSERT INTO `{$this->prefix}server_workshop_mods`
|
||||||
(home_id, agent_id, profile_id, workshop_app_id, workshop_id, title, enabled, install_path, load_order, installed_at)
|
(home_id, agent_id, profile_id, workshop_app_id, workshop_id, title, custom_folder, enabled, install_path, load_order, installed_at)
|
||||||
VALUES
|
VALUES
|
||||||
({$homeId}, {$agentId}, {$profileId}, '{$appId}', '{$workshopId}', '{$title}', 1, '{$installPath}', {$loadOrder}, NOW())"
|
({$homeId}, {$agentId}, {$profileId}, '{$appId}', '{$workshopId}', '{$title}', '{$customFolder}', 1, '{$installPath}', {$loadOrder}, NOW())"
|
||||||
);
|
);
|
||||||
return $this->lastInsertId();
|
return $this->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
@ -349,9 +400,13 @@ class WorkshopRepository
|
||||||
{
|
{
|
||||||
return $this->select(
|
return $this->select(
|
||||||
"SELECT m.*,
|
"SELECT m.*,
|
||||||
p.cache_path_template, p.install_path_template, p.folder_name_template,
|
p.steam_app_id, p.cache_path_template, p.install_path_template,
|
||||||
p.copy_method, p.install_script, p.config_file_template, p.launch_param_template,
|
p.folder_naming_format, p.folder_name_template,
|
||||||
p.requires_restart
|
p.copy_method, p.copy_keys, p.key_source_path, p.key_dest_path,
|
||||||
|
p.pre_update_script, p.install_script, p.post_update_script,
|
||||||
|
p.steamcmd_path, p.steamcmd_login_mode,
|
||||||
|
p.config_file_template, p.launch_param_template,
|
||||||
|
p.requires_restart
|
||||||
FROM `{$this->prefix}server_workshop_mods` m
|
FROM `{$this->prefix}server_workshop_mods` m
|
||||||
JOIN `{$this->prefix}workshop_game_profiles` p ON m.profile_id = p.id
|
JOIN `{$this->prefix}workshop_game_profiles` p ON m.profile_id = p.id
|
||||||
WHERE m.enabled = 1 AND p.enabled = 1
|
WHERE m.enabled = 1 AND p.enabled = 1
|
||||||
|
|
@ -435,4 +490,100 @@ class WorkshopRepository
|
||||||
WHERE m.enabled = 1 AND m.profile_id = {$profileId}"
|
WHERE m.enabled = 1 AND m.profile_id = {$profileId}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// SERVER WORKSHOP SETTINGS (per-server/home configuration)
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the workshop settings row for a game home, or null if not set.
|
||||||
|
*/
|
||||||
|
public function getServerSettings(int $homeId): ?array
|
||||||
|
{
|
||||||
|
return $this->selectOne(
|
||||||
|
"SELECT * FROM `{$this->prefix}server_workshop_settings`
|
||||||
|
WHERE home_id = {$homeId} LIMIT 1"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert server-level workshop settings.
|
||||||
|
*/
|
||||||
|
public function saveServerSettings(int $homeId, array $data): void
|
||||||
|
{
|
||||||
|
$workshopEnabled = empty($data['workshop_enabled']) ? 0 : 1;
|
||||||
|
$profileId = isset($data['profile_id']) && (int)$data['profile_id'] > 0
|
||||||
|
? (int)$data['profile_id']
|
||||||
|
: 'NULL';
|
||||||
|
$updateMode = in_array($data['update_mode'] ?? '', ['manual', 'scheduled', 'on_restart'], true)
|
||||||
|
? "'" . $this->esc($data['update_mode']) . "'"
|
||||||
|
: "'manual'";
|
||||||
|
$restartBehavior = in_array($data['restart_behavior'] ?? '', ['none', 'queue', 'stop_update_start'], true)
|
||||||
|
? "'" . $this->esc($data['restart_behavior']) . "'"
|
||||||
|
: "'none'";
|
||||||
|
$updateQueued = empty($data['update_queued']) ? 0 : 1;
|
||||||
|
|
||||||
|
$this->exec(
|
||||||
|
"INSERT INTO `{$this->prefix}server_workshop_settings`
|
||||||
|
(home_id, workshop_enabled, profile_id, update_mode, restart_behavior, update_queued, updated_at)
|
||||||
|
VALUES
|
||||||
|
({$homeId}, {$workshopEnabled}, {$profileId}, {$updateMode}, {$restartBehavior}, {$updateQueued}, NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
workshop_enabled = {$workshopEnabled},
|
||||||
|
profile_id = {$profileId},
|
||||||
|
update_mode = {$updateMode},
|
||||||
|
restart_behavior = {$restartBehavior},
|
||||||
|
update_queued = {$updateQueued},
|
||||||
|
updated_at = NOW()"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record the result of an update run for a home.
|
||||||
|
*/
|
||||||
|
public function recordUpdateResult(int $homeId, string $status, string $error = ''): void
|
||||||
|
{
|
||||||
|
$status = $this->esc($status);
|
||||||
|
$errorSql = $error !== '' ? "'" . $this->esc($error) . "'" : 'NULL';
|
||||||
|
$successSql = $status === 'success' ? 'NOW()' : 'last_success_time';
|
||||||
|
|
||||||
|
$this->exec(
|
||||||
|
"INSERT INTO `{$this->prefix}server_workshop_settings`
|
||||||
|
(home_id, last_update_status, last_update_error, last_update_time, last_success_time)
|
||||||
|
VALUES
|
||||||
|
({$homeId}, '{$status}', {$errorSql}, NOW(), " . ($status === 'success' ? 'NOW()' : 'NULL') . ")
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
last_update_status = '{$status}',
|
||||||
|
last_update_error = {$errorSql},
|
||||||
|
last_update_time = NOW(),
|
||||||
|
last_success_time = {$successSql}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a manual update as queued (or clear the queue flag).
|
||||||
|
*/
|
||||||
|
public function setUpdateQueued(int $homeId, bool $queued): void
|
||||||
|
{
|
||||||
|
$val = $queued ? 1 : 0;
|
||||||
|
$this->exec(
|
||||||
|
"INSERT INTO `{$this->prefix}server_workshop_settings` (home_id, update_queued)
|
||||||
|
VALUES ({$homeId}, {$val})
|
||||||
|
ON DUPLICATE KEY UPDATE update_queued = {$val}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all home IDs that have a queued manual update.
|
||||||
|
*
|
||||||
|
* @return array<int,int>
|
||||||
|
*/
|
||||||
|
public function listQueuedUpdateHomes(): array
|
||||||
|
{
|
||||||
|
$rows = $this->select(
|
||||||
|
"SELECT home_id FROM `{$this->prefix}server_workshop_settings`
|
||||||
|
WHERE update_queued = 1 AND workshop_enabled = 1"
|
||||||
|
);
|
||||||
|
return array_column($rows, 'home_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,33 +23,52 @@
|
||||||
*/
|
*/
|
||||||
// Module general information
|
// Module general information
|
||||||
$module_title = "Steam Workshop";
|
$module_title = "Steam Workshop";
|
||||||
$module_version = "2.2";
|
$module_version = "2.3";
|
||||||
$db_version = 1;
|
$db_version = 2;
|
||||||
$module_required = TRUE;
|
$module_required = TRUE;
|
||||||
$module_menus = array();
|
$module_menus = array();
|
||||||
|
|
||||||
// Database schema: create the three Workshop tables when the module is installed.
|
// -----------------------------------------------------------------------
|
||||||
// $install_queries[0] is run during initial install (from db_version 0 to 1).
|
// $install_queries[0] – executed for FRESH installs (all keys run).
|
||||||
// Each table uses IF NOT EXISTS so re-running is safe.
|
// Contains the full v2 schema with every column.
|
||||||
|
// $install_queries[2] – executed when upgrading an existing v1 install
|
||||||
|
// to v2 (ALTER TABLE + new settings table).
|
||||||
|
// $db_version = 2 (v1 = original release; v2 = this rewrite).
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
$install_queries = array();
|
$install_queries = array();
|
||||||
|
|
||||||
|
// Full schema for fresh installs (includes every column from all versions).
|
||||||
$install_queries[0] = array(
|
$install_queries[0] = array(
|
||||||
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."workshop_game_profiles` (
|
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."workshop_game_profiles` (
|
||||||
`id` INT NOT NULL AUTO_INCREMENT,
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
`game_key` VARCHAR(100) NOT NULL,
|
`game_key` VARCHAR(100) NOT NULL,
|
||||||
`game_name` VARCHAR(255) NOT NULL,
|
`game_name` VARCHAR(255) NOT NULL,
|
||||||
`workshop_app_id` VARCHAR(32) NOT NULL,
|
`steam_app_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`workshop_app_id` VARCHAR(32) NOT NULL,
|
||||||
|
`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 '',
|
||||||
`supported_os` SET('linux','windows') NOT NULL DEFAULT 'linux',
|
`supported_os` SET('linux','windows') NOT NULL DEFAULT 'linux',
|
||||||
`cache_path_template` TEXT NOT NULL,
|
`cache_path_template` TEXT NOT NULL,
|
||||||
`install_path_template` TEXT NOT NULL,
|
`install_path_template` TEXT NOT NULL,
|
||||||
`folder_name_template` VARCHAR(255) NOT NULL DEFAULT '@{mod_id}',
|
`folder_naming_format` ENUM('@%mod_name%','@%workshop_id%','custom') NOT NULL DEFAULT '@%workshop_id%',
|
||||||
`copy_method` ENUM('rsync','robocopy','custom_script') NOT NULL DEFAULT 'rsync',
|
`folder_name_template` VARCHAR(255) NOT NULL DEFAULT '@%workshop_id%',
|
||||||
`install_script` TEXT NULL,
|
`mod_launch_param` VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
`config_file_template` TEXT NULL,
|
`mod_separator` ENUM('semicolon','comma','space') NOT NULL DEFAULT 'semicolon',
|
||||||
`launch_param_template` TEXT NULL,
|
`copy_method` ENUM('copy','rsync','symlink') NOT NULL DEFAULT 'rsync',
|
||||||
`requires_restart` TINYINT(1) NOT NULL DEFAULT 1,
|
`copy_keys` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
`key_source_path` TEXT NULL,
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`key_dest_path` TEXT NULL,
|
||||||
`updated_at` DATETIME NULL,
|
`pre_update_script` TEXT NULL,
|
||||||
|
`install_script` TEXT NULL,
|
||||||
|
`post_update_script` TEXT NULL,
|
||||||
|
`config_file_template` TEXT NULL,
|
||||||
|
`launch_param_template` TEXT NULL,
|
||||||
|
`requires_restart` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`validation_notes` TEXT NULL,
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uniq_game_key` (`game_key`)
|
UNIQUE KEY `uniq_game_key` (`game_key`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||||
|
|
@ -78,6 +97,7 @@ $install_queries[0] = array(
|
||||||
`workshop_app_id` VARCHAR(32) NOT NULL,
|
`workshop_app_id` VARCHAR(32) NOT NULL,
|
||||||
`workshop_id` VARCHAR(64) NOT NULL,
|
`workshop_id` VARCHAR(64) NOT NULL,
|
||||||
`title` VARCHAR(255) NULL,
|
`title` VARCHAR(255) NULL,
|
||||||
|
`custom_folder` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
`install_path` TEXT NOT NULL,
|
`install_path` TEXT NOT NULL,
|
||||||
`load_order` INT NOT NULL DEFAULT 0,
|
`load_order` INT NOT NULL DEFAULT 0,
|
||||||
|
|
@ -85,5 +105,62 @@ $install_queries[0] = array(
|
||||||
`updated_at` DATETIME NULL,
|
`updated_at` DATETIME NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
|
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||||
|
|
||||||
|
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_workshop_settings` (
|
||||||
|
`home_id` INT NOT NULL,
|
||||||
|
`workshop_enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`profile_id` INT NULL,
|
||||||
|
`update_mode` ENUM('manual','scheduled','on_restart') NOT NULL DEFAULT 'manual',
|
||||||
|
`restart_behavior` ENUM('none','queue','stop_update_start') NOT NULL DEFAULT 'none',
|
||||||
|
`update_queued` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`last_update_status` VARCHAR(20) NOT NULL DEFAULT '',
|
||||||
|
`last_update_error` TEXT NULL,
|
||||||
|
`last_update_time` DATETIME NULL,
|
||||||
|
`last_success_time` DATETIME NULL,
|
||||||
|
`updated_at` DATETIME NULL,
|
||||||
|
PRIMARY KEY (`home_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Migration: upgrade existing v1 installs to v2 schema.
|
||||||
|
$install_queries[2] = array(
|
||||||
|
// New columns on workshop_game_profiles
|
||||||
|
"ALTER TABLE `".OGP_DB_PREFIX."workshop_game_profiles`
|
||||||
|
ADD COLUMN IF NOT EXISTS `steam_app_id` VARCHAR(32) NOT NULL DEFAULT '' AFTER `game_name`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `steam_login_required` TINYINT(1) NOT NULL DEFAULT 0 AFTER `workshop_app_id`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `steamcmd_login_mode` ENUM('anonymous','account') NOT NULL DEFAULT 'anonymous' AFTER `steam_login_required`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `steamcmd_path` VARCHAR(512) NOT NULL DEFAULT '' AFTER `steamcmd_login_mode`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `folder_naming_format` ENUM('@%mod_name%','@%workshop_id%','custom') NOT NULL DEFAULT '@%workshop_id%' AFTER `install_path_template`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `mod_launch_param` VARCHAR(512) NOT NULL DEFAULT '' AFTER `folder_name_template`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `mod_separator` ENUM('semicolon','comma','space') NOT NULL DEFAULT 'semicolon' AFTER `mod_launch_param`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `copy_keys` TINYINT(1) NOT NULL DEFAULT 0 AFTER `copy_method`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `key_source_path` TEXT NULL AFTER `copy_keys`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `key_dest_path` TEXT NULL AFTER `key_source_path`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `pre_update_script` TEXT NULL AFTER `key_dest_path`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `post_update_script` TEXT NULL AFTER `install_script`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `validation_notes` TEXT NULL AFTER `requires_restart`",
|
||||||
|
|
||||||
|
// Rename copy_method enum values to match new options (copy/rsync/symlink)
|
||||||
|
// (existing 'rsync' stays valid; 'robocopy'/'custom_script' are legacy)
|
||||||
|
|
||||||
|
// New column on server_workshop_mods
|
||||||
|
"ALTER TABLE `".OGP_DB_PREFIX."server_workshop_mods`
|
||||||
|
ADD COLUMN IF NOT EXISTS `custom_folder` VARCHAR(255) NOT NULL DEFAULT '' AFTER `title`",
|
||||||
|
|
||||||
|
// New server-level settings table
|
||||||
|
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_workshop_settings` (
|
||||||
|
`home_id` INT NOT NULL,
|
||||||
|
`workshop_enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`profile_id` INT NULL,
|
||||||
|
`update_mode` ENUM('manual','scheduled','on_restart') NOT NULL DEFAULT 'manual',
|
||||||
|
`restart_behavior` ENUM('none','queue','stop_update_start') NOT NULL DEFAULT 'none',
|
||||||
|
`update_queued` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`last_update_status` VARCHAR(20) NOT NULL DEFAULT '',
|
||||||
|
`last_update_error` TEXT NULL,
|
||||||
|
`last_update_time` DATETIME NULL,
|
||||||
|
`last_success_time` DATETIME NULL,
|
||||||
|
`updated_at` DATETIME NULL,
|
||||||
|
PRIMARY KEY (`home_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -605,3 +605,186 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #2c5f8a;
|
color: #2c5f8a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── v2 form additions ── */
|
||||||
|
|
||||||
|
.sw-form__grid--3col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-form__grid--2col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-form__row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-form__row label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-form fieldset {
|
||||||
|
border: 1px solid #dde;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-form fieldset legend {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-form__os-group {
|
||||||
|
border: none !important;
|
||||||
|
padding: 0.5rem 0 0 !important;
|
||||||
|
margin: 0.5rem 0 0 !important;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-script-textarea {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-info-box--compact {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin: 0.5rem 0 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-code-pre {
|
||||||
|
background: #f4f4f8;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.6rem 0.9rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-example-block summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #0b5ed7;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-inline {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server settings section */
|
||||||
|
.sw-server-settings {
|
||||||
|
background: #f8f8fc;
|
||||||
|
border: 1px solid #e0e0ee;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-server-settings h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-update-status {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
border-top: 1px solid #e0e0ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-status-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
gap: 0.25rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-status-grid dt {
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-status-grid dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-error-text code {
|
||||||
|
color: #c0392b;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-notice {
|
||||||
|
background: #fff8e1;
|
||||||
|
border: 1px solid #ffe082;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.6rem 0.9rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-notice--info {
|
||||||
|
background: #e8f4fd;
|
||||||
|
border-color: #b3d7f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-badge--danger {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-badge--info {
|
||||||
|
background: #cce5ff;
|
||||||
|
color: #004085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-badge--warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-badge--app, .sw-badge--custom {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-order-input {
|
||||||
|
width: 4.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-profiles__table code {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 0 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-settings-form .sw-checkbox {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,28 @@ declare(strict_types=1);
|
||||||
/** @var array|null $profile existing row when editing, null when creating */
|
/** @var array|null $profile existing row when editing, null when creating */
|
||||||
/** @var int $profileId */
|
/** @var int $profileId */
|
||||||
|
|
||||||
$isEdit = $profileId > 0 && $profile !== null;
|
$isEdit = $profileId > 0 && $profile !== null;
|
||||||
$heading = $isEdit
|
$heading = $isEdit
|
||||||
? sprintf($lang['config_heading_edit'] ?? 'Edit Workshop Configuration: %s', htmlspecialchars($profile['game_name'] ?? ''))
|
? sprintf($lang['config_heading_edit'] ?? 'Edit Workshop Configuration: %s', htmlspecialchars($profile['game_name'] ?? ''))
|
||||||
: ($lang['config_heading_create'] ?? 'Add Workshop Game Configuration');
|
: ($lang['config_heading_create'] ?? 'Add Workshop Game Configuration');
|
||||||
|
|
||||||
$v = static function (string $key, array $profile, string $default = ''): string {
|
/** Helper: return html-safe value from profile array (or default). */
|
||||||
return htmlspecialchars((string)($profile[$key] ?? $default), ENT_QUOTES);
|
$v = static function (string $key, array $p, string $default = ''): string {
|
||||||
|
return htmlspecialchars((string)($p[$key] ?? $default), ENT_QUOTES);
|
||||||
};
|
};
|
||||||
|
|
||||||
$osList = ['linux' => 'Linux', 'windows' => 'Windows'];
|
$osList = ['linux' => 'Linux', 'windows' => 'Windows'];
|
||||||
$currentOs = array_filter(explode(',', (string)($profile['supported_os'] ?? 'linux')));
|
$currentOs = array_filter(explode(',', (string)($profile['supported_os'] ?? 'linux')));
|
||||||
$methodList = ['rsync' => 'rsync (Linux)', 'robocopy' => 'robocopy (Windows)', 'custom_script' => 'Custom script'];
|
$folderFormats = ['@%mod_name%' => '@%mod_name% (mod title)', '@%workshop_id%' => '@%workshop_id% (numeric ID)', 'custom' => 'Custom template'];
|
||||||
$curMethod = (string)($profile['copy_method'] ?? 'rsync');
|
$curFolderFormat = (string)($profile['folder_naming_format'] ?? '@%workshop_id%');
|
||||||
|
$separatorList = ['semicolon' => 'Semicolon ( ; )', 'comma' => 'Comma ( , )', 'space' => 'Space ( )'];
|
||||||
|
$curSeparator = (string)($profile['mod_separator'] ?? 'semicolon');
|
||||||
|
$copyMethods = ['rsync' => 'rsync (Linux/Unix)', 'copy' => 'cp / basic copy', 'symlink' => 'Symlink (requires persistent cache path)'];
|
||||||
|
$curCopyMethod = (string)($profile['copy_method'] ?? 'rsync');
|
||||||
|
$loginModes = ['anonymous' => 'Anonymous (recommended for free mods)', 'account' => 'Configured account (paid games)'];
|
||||||
|
$curLoginMode = (string)($profile['steamcmd_login_mode'] ?? 'anonymous');
|
||||||
|
|
||||||
$tplVarNote = $lang['profile_template_vars'] ?? 'Available: {home_id} {agent_id} {workshop_app_id} {mod_id} {mod_title} {mod_folder} {steamcmd_path} {server_path} {install_path} {cache_path}';
|
$tplVarNote = $lang['profile_template_vars'] ?? 'Variables: %home_id% %server_path% %steam_app_id% %workshop_app_id% %workshop_id% %mod_name% %install_name% %download_path% %source_path% %target_path% %keys_source_path% %keys_target_path% %steamcmd_path%';
|
||||||
?>
|
?>
|
||||||
<div class="sw-admin sw-profile-form">
|
<div class="sw-admin sw-profile-form">
|
||||||
<h3><?php echo $heading; ?></h3>
|
<h3><?php echo $heading; ?></h3>
|
||||||
|
|
@ -26,36 +33,84 @@ $tplVarNote = $lang['profile_template_vars'] ?? 'Available: {home_id} {agent_id
|
||||||
|
|
||||||
<div class="sw-info-box">
|
<div class="sw-info-box">
|
||||||
<strong><?php echo htmlspecialchars($lang['config_steamcmd_heading'] ?? 'How mods are downloaded'); ?></strong>
|
<strong><?php echo htmlspecialchars($lang['config_steamcmd_heading'] ?? 'How mods are downloaded'); ?></strong>
|
||||||
<p><?php echo htmlspecialchars($lang['config_steamcmd_note'] ?? 'Workshop mods are downloaded using SteamCMD: +workshop_download_item <App ID> <Mod ID>. The cache path below is where SteamCMD stores downloaded content on the agent. The install path is where the mod files are copied into the game server directory.'); ?></p>
|
<p><?php echo htmlspecialchars($lang['config_steamcmd_note'] ?? 'Workshop mods are downloaded using SteamCMD: +workshop_download_item <App ID> <Mod ID>. Configure the paths and scripts below to control how mods are installed for servers of this game type.'); ?></p>
|
||||||
|
<p><strong><?php echo htmlspecialchars($lang['profile_template_vars_heading'] ?? 'Template variables:'); ?></strong><br>
|
||||||
|
<code><?php echo htmlspecialchars($tplVarNote); ?></code></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="?m=steam_workshop&p=workshop_admin" class="sw-form">
|
<form method="post" action="?m=steam_workshop&p=workshop_admin" class="sw-form">
|
||||||
<input type="hidden" name="sw_action" value="profile_save">
|
<input type="hidden" name="sw_action" value="profile_save">
|
||||||
<input type="hidden" name="profile_id" value="<?php echo $profileId; ?>">
|
<input type="hidden" name="profile_id" value="<?php echo $profileId; ?>">
|
||||||
|
|
||||||
<!-- Basic info -->
|
<!-- Basic identification -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><?php echo htmlspecialchars($lang['profile_section_basic'] ?? 'Basic info'); ?></legend>
|
<legend><?php echo htmlspecialchars($lang['profile_section_basic'] ?? 'Basic identification'); ?></legend>
|
||||||
<div class="sw-form__grid">
|
<div class="sw-form__grid sw-form__grid--3col">
|
||||||
<label>
|
<label>
|
||||||
<?php echo htmlspecialchars($lang['label_game_key'] ?? 'Game key'); ?> <em>*</em>
|
<?php echo htmlspecialchars($lang['label_game_key'] ?? 'Game key'); ?> <em>*</em>
|
||||||
<small><?php echo htmlspecialchars($lang['config_hint_game_key'] ?? 'Short identifier matching the game XML key, e.g. arma3_linux'); ?></small>
|
<small><?php echo htmlspecialchars($lang['config_hint_game_key'] ?? 'Short identifier matching the game XML key, e.g. dayz_linux'); ?></small>
|
||||||
<input type="text" name="game_key" value="<?php echo $v('game_key', $profile ?? []); ?>"
|
<input type="text" name="game_key" value="<?php echo $v('game_key', $profile ?? []); ?>"
|
||||||
pattern="[A-Za-z0-9_\-.]+" required maxlength="100"
|
pattern="[A-Za-z0-9_\-.]+" required maxlength="100"
|
||||||
<?php echo $isEdit ? 'readonly' : ''; ?>>
|
<?php echo $isEdit ? 'readonly' : ''; ?>>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<?php echo htmlspecialchars($lang['profile_label_game_name'] ?? 'Game name'); ?> <em>*</em>
|
<?php echo htmlspecialchars($lang['profile_label_game_name'] ?? 'Game display name'); ?> <em>*</em>
|
||||||
<input type="text" name="game_name" value="<?php echo $v('game_name', $profile ?? []); ?>"
|
<input type="text" name="game_name" value="<?php echo $v('game_name', $profile ?? []); ?>"
|
||||||
required maxlength="255">
|
required maxlength="255">
|
||||||
</label>
|
</label>
|
||||||
|
<label class="sw-checkbox" style="align-self:end;padding-bottom:0.75rem;">
|
||||||
|
<input type="checkbox" name="enabled" value="1"
|
||||||
|
<?php echo ($profile['enabled'] ?? 1) ? 'checked' : ''; ?>>
|
||||||
|
<span><?php echo htmlspecialchars($lang['config_label_enabled'] ?? 'Profile enabled'); ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Steam / SteamCMD settings -->
|
||||||
|
<fieldset>
|
||||||
|
<legend><?php echo htmlspecialchars($lang['profile_section_steam'] ?? 'Steam & SteamCMD settings'); ?></legend>
|
||||||
|
<div class="sw-form__grid sw-form__grid--3col">
|
||||||
<label>
|
<label>
|
||||||
<?php echo htmlspecialchars($lang['config_label_app_id'] ?? 'Steam App ID'); ?> <em>*</em>
|
<?php echo htmlspecialchars($lang['profile_label_steam_app_id'] ?? 'Steam App ID'); ?>
|
||||||
<small><?php echo htmlspecialchars($lang['config_hint_app_id'] ?? 'The Steam App ID used with +workshop_download_item, e.g. 107410 for Arma 3'); ?></small>
|
<small><?php echo htmlspecialchars($lang['profile_hint_steam_app_id'] ?? 'The Steam game App ID (e.g. 221100 for DayZ). Used when Steam login is required.'); ?></small>
|
||||||
|
<input type="text" name="steam_app_id"
|
||||||
|
value="<?php echo $v('steam_app_id', $profile ?? []); ?>"
|
||||||
|
pattern="[0-9]*" maxlength="32">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['config_label_app_id'] ?? 'Workshop App ID'); ?> <em>*</em>
|
||||||
|
<small><?php echo htmlspecialchars($lang['config_hint_app_id'] ?? 'The App ID used with +workshop_download_item, e.g. 221100 for DayZ'); ?></small>
|
||||||
<input type="text" name="workshop_app_id"
|
<input type="text" name="workshop_app_id"
|
||||||
value="<?php echo $v('workshop_app_id', $profile ?? []); ?>"
|
value="<?php echo $v('workshop_app_id', $profile ?? []); ?>"
|
||||||
pattern="[0-9]+" required maxlength="32">
|
pattern="[0-9]+" required maxlength="32">
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_steamcmd_path'] ?? 'SteamCMD path on agent'); ?>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_hint_steamcmd_path'] ?? 'Full path to steamcmd.sh on the remote agent. Leave blank to use the agent default (/home/gameserver/steamcmd/steamcmd.sh).'); ?></small>
|
||||||
|
<input type="text" name="steamcmd_path"
|
||||||
|
value="<?php echo $v('steamcmd_path', $profile ?? []); ?>"
|
||||||
|
placeholder="/home/gameserver/steamcmd/steamcmd.sh" maxlength="512">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sw-form__grid sw-form__grid--2col">
|
||||||
|
<label class="sw-checkbox">
|
||||||
|
<input type="checkbox" name="steam_login_required" value="1"
|
||||||
|
id="sw-login-required"
|
||||||
|
<?php echo !empty($profile['steam_login_required']) ? 'checked' : ''; ?>>
|
||||||
|
<span><?php echo htmlspecialchars($lang['profile_label_steam_login_required'] ?? 'Steam login required (game is not free / requires ownership)'); ?></span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_steamcmd_login_mode'] ?? 'SteamCMD login mode'); ?>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_hint_steamcmd_login_mode'] ?? 'Use anonymous for free Workshop mods. Use configured account for games requiring ownership.'); ?></small>
|
||||||
|
<select name="steamcmd_login_mode">
|
||||||
|
<?php foreach ($loginModes as $mVal => $mLabel): ?>
|
||||||
|
<option value="<?php echo $mVal; ?>" <?php echo $curLoginMode === $mVal ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($mLabel); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset class="sw-form__os-group">
|
<fieldset class="sw-form__os-group">
|
||||||
|
|
@ -70,81 +125,180 @@ $tplVarNote = $lang['profile_template_vars'] ?? 'Available: {home_id} {agent_id
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Paths / templates -->
|
<!-- Download & install paths -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><?php echo htmlspecialchars($lang['profile_section_paths'] ?? 'Paths & templates'); ?></legend>
|
<legend><?php echo htmlspecialchars($lang['profile_section_paths'] ?? 'Download & install paths'); ?></legend>
|
||||||
<small class="sw-hint"><?php echo htmlspecialchars($tplVarNote); ?></small>
|
<small class="sw-hint"><?php echo htmlspecialchars($tplVarNote); ?></small>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<?php echo htmlspecialchars($lang['profile_label_cache_path'] ?? 'SteamCMD cache path template'); ?> <em>*</em>
|
<?php echo htmlspecialchars($lang['profile_label_cache_path'] ?? 'Workshop download/cache path'); ?> <em>*</em>
|
||||||
<small><?php echo htmlspecialchars($lang['profile_hint_cache_path'] ?? 'Where SteamCMD downloads mods on the agent. E.g. {steamcmd_path}/steamapps/workshop/content/{workshop_app_id}/{mod_id}'); ?></small>
|
<small><?php echo htmlspecialchars($lang['profile_hint_cache_path'] ?? 'Where SteamCMD stores downloaded mod content on the agent. E.g. /home/gameserver/steamcmd/steamapps/workshop/content/%workshop_app_id%/%workshop_id%'); ?></small>
|
||||||
<input type="text" name="cache_path_template"
|
<input type="text" name="cache_path_template"
|
||||||
value="<?php echo $v('cache_path_template', $profile ?? []); ?>" required>
|
value="<?php echo $v('cache_path_template', $profile ?? []); ?>" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<?php echo htmlspecialchars($lang['profile_label_install_path'] ?? 'Server install path template'); ?> <em>*</em>
|
<?php echo htmlspecialchars($lang['profile_label_install_path'] ?? 'Server mod install root'); ?> <em>*</em>
|
||||||
<small><?php echo htmlspecialchars($lang['profile_hint_install_path'] ?? 'Where mod files are placed inside the game server directory. E.g. {server_path}/mods/{mod_folder}'); ?></small>
|
<small><?php echo htmlspecialchars($lang['profile_hint_install_path'] ?? 'Base directory inside the server where mods are installed. E.g. %server_path%/mods/%install_name%'); ?></small>
|
||||||
<input type="text" name="install_path_template"
|
<input type="text" name="install_path_template"
|
||||||
value="<?php echo $v('install_path_template', $profile ?? []); ?>" required>
|
value="<?php echo $v('install_path_template', $profile ?? []); ?>" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
|
||||||
<?php echo htmlspecialchars($lang['profile_label_folder_name'] ?? 'Mod folder name template'); ?>
|
|
||||||
<small><?php echo htmlspecialchars($lang['profile_hint_folder_name'] ?? 'Folder name for each mod inside the install path. Default: @{mod_id}'); ?></small>
|
|
||||||
<input type="text" name="folder_name_template"
|
|
||||||
value="<?php echo $v('folder_name_template', $profile ?? [], '@{mod_id}'); ?>">
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Copy method -->
|
<!-- Mod folder naming -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><?php echo htmlspecialchars($lang['config_section_copy'] ?? 'Copy / sync method'); ?></legend>
|
<legend><?php echo htmlspecialchars($lang['profile_section_folder'] ?? 'Mod folder naming'); ?></legend>
|
||||||
<label>
|
<label>
|
||||||
<?php echo htmlspecialchars($lang['profile_label_copy_method'] ?? 'Method used to copy mod files from SteamCMD cache to the server'); ?>
|
<?php echo htmlspecialchars($lang['profile_label_folder_format'] ?? 'Folder naming format'); ?>
|
||||||
<select name="copy_method">
|
<small><?php echo htmlspecialchars($lang['profile_hint_folder_format'] ?? 'How each mod folder is named inside the install root.'); ?></small>
|
||||||
<?php foreach ($methodList as $mVal => $mLabel): ?>
|
<select name="folder_naming_format" id="sw-folder-format">
|
||||||
<option value="<?php echo $mVal; ?>" <?php echo $curMethod === $mVal ? 'selected' : ''; ?>>
|
<?php foreach ($folderFormats as $fVal => $fLabel): ?>
|
||||||
<?php echo htmlspecialchars($mLabel); ?>
|
<option value="<?php echo $fVal; ?>" <?php echo $curFolderFormat === $fVal ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($fLabel); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<div id="sw-custom-folder-wrap" <?php echo $curFolderFormat !== 'custom' ? 'style="display:none"' : ''; ?>>
|
||||||
<?php echo htmlspecialchars($lang['profile_label_install_script'] ?? 'Custom install script (optional, admin-defined)'); ?>
|
<label>
|
||||||
<small><?php echo htmlspecialchars($lang['profile_hint_install_script'] ?? 'Only used when copy method is custom_script. Template variables are replaced before execution.'); ?></small>
|
<?php echo htmlspecialchars($lang['profile_label_folder_name'] ?? 'Custom folder name template'); ?>
|
||||||
<textarea name="install_script" rows="4"><?php echo $v('install_script', $profile ?? []); ?></textarea>
|
<small><?php echo htmlspecialchars($lang['profile_hint_folder_name'] ?? 'Use %workshop_id% or %mod_name%. E.g. @%workshop_id%'); ?></small>
|
||||||
</label>
|
<input type="text" name="folder_name_template"
|
||||||
|
value="<?php echo $v('folder_name_template', $profile ?? [], '@%workshop_id%'); ?>">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Config / launch params -->
|
<!-- Launch parameters -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><?php echo htmlspecialchars($lang['profile_section_config'] ?? 'Config & launch parameters'); ?></legend>
|
<legend><?php echo htmlspecialchars($lang['profile_section_launch'] ?? 'Launch parameters'); ?></legend>
|
||||||
|
<div class="sw-form__grid sw-form__grid--2col">
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_mod_launch_param'] ?? 'Mod launch parameter format'); ?>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_hint_mod_launch_param'] ?? 'How the full mod list is passed to the server start command. E.g. -mod=%mods%'); ?></small>
|
||||||
|
<input type="text" name="mod_launch_param"
|
||||||
|
value="<?php echo $v('mod_launch_param', $profile ?? []); ?>"
|
||||||
|
placeholder="-mod=%mods%" maxlength="512">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_mod_separator'] ?? 'Mod separator'); ?>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_hint_mod_separator'] ?? 'Character used to join multiple mod folder names in the launch parameter.'); ?></small>
|
||||||
|
<select name="mod_separator">
|
||||||
|
<?php foreach ($separatorList as $sVal => $sLabel): ?>
|
||||||
|
<option value="<?php echo $sVal; ?>" <?php echo $curSeparator === $sVal ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($sLabel); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<?php echo htmlspecialchars($lang['profile_label_config_tpl'] ?? 'Config file template (optional)'); ?>
|
<?php echo htmlspecialchars($lang['profile_label_launch_tpl'] ?? 'Full launch parameter template (optional)'); ?>
|
||||||
<textarea name="config_file_template" rows="4"><?php echo $v('config_file_template', $profile ?? []); ?></textarea>
|
<small><?php echo htmlspecialchars($lang['config_hint_launch_tpl'] ?? 'Complete launch parameter string appended to server start. Each mod folder name is joined with the separator above.'); ?></small>
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<?php echo htmlspecialchars($lang['profile_label_launch_tpl'] ?? 'Launch parameter template (optional)'); ?>
|
|
||||||
<small><?php echo htmlspecialchars($lang['config_hint_launch_tpl'] ?? 'Extra launch parameters added when this game has Workshop mods enabled. E.g. -mod=@{mod_id}'); ?></small>
|
|
||||||
<input type="text" name="launch_param_template"
|
<input type="text" name="launch_param_template"
|
||||||
value="<?php echo $v('launch_param_template', $profile ?? []); ?>">
|
value="<?php echo $v('launch_param_template', $profile ?? []); ?>">
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Flags -->
|
<!-- Copy / sync method -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><?php echo htmlspecialchars($lang['profile_section_flags'] ?? 'Flags'); ?></legend>
|
<legend><?php echo htmlspecialchars($lang['config_section_copy'] ?? 'Copy / sync method'); ?></legend>
|
||||||
|
<div class="sw-form__grid sw-form__grid--2col">
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_copy_method'] ?? 'Copy method'); ?>
|
||||||
|
<select name="copy_method">
|
||||||
|
<?php foreach ($copyMethods as $mVal => $mLabel): ?>
|
||||||
|
<option value="<?php echo $mVal; ?>" <?php echo $curCopyMethod === $mVal ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($mLabel); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="sw-checkbox" style="align-self:end;padding-bottom:0.5rem;">
|
||||||
|
<input type="checkbox" name="copy_keys" value="1"
|
||||||
|
id="sw-copy-keys"
|
||||||
|
<?php echo !empty($profile['copy_keys']) ? 'checked' : ''; ?>>
|
||||||
|
<span><?php echo htmlspecialchars($lang['profile_label_copy_keys'] ?? 'Copy mod keys (*.bikey) to server keys directory'); ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sw-key-paths-wrap" <?php echo empty($profile['copy_keys']) ? 'style="display:none"' : ''; ?>>
|
||||||
|
<div class="sw-form__grid sw-form__grid--2col">
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_key_source'] ?? 'Key source path'); ?>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_hint_key_source'] ?? 'Path inside the mod cache where key files live. E.g. %source_path%/keys'); ?></small>
|
||||||
|
<input type="text" name="key_source_path"
|
||||||
|
value="<?php echo $v('key_source_path', $profile ?? []); ?>"
|
||||||
|
placeholder="%source_path%/keys">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_key_dest'] ?? 'Key destination path'); ?>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_hint_key_dest'] ?? 'Where keys are copied on the server. E.g. %server_path%/keys'); ?></small>
|
||||||
|
<input type="text" name="key_dest_path"
|
||||||
|
value="<?php echo $v('key_dest_path', $profile ?? []); ?>"
|
||||||
|
placeholder="%server_path%/keys">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Bash scripts -->
|
||||||
|
<fieldset>
|
||||||
|
<legend><?php echo htmlspecialchars($lang['profile_section_scripts'] ?? 'Bash scripts'); ?></legend>
|
||||||
|
<div class="sw-info-box sw-info-box--compact">
|
||||||
|
<strong><?php echo htmlspecialchars($lang['profile_scripts_order'] ?? 'Execution order:'); ?></strong>
|
||||||
|
1. <?php echo htmlspecialchars($lang['profile_label_pre_script'] ?? 'Pre-update script'); ?> →
|
||||||
|
2. <?php echo htmlspecialchars($lang['profile_label_install_script'] ?? 'Per-mod install script'); ?> (<?php echo htmlspecialchars($lang['profile_scripts_per_mod'] ?? 'repeated for each mod'); ?>) →
|
||||||
|
3. <?php echo htmlspecialchars($lang['profile_label_post_script'] ?? 'Post-update script'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_pre_script'] ?? 'Pre-update bash script'); ?>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_hint_pre_script'] ?? 'Runs once before any mod is downloaded/installed. Variables: %home_id% %server_path% %workshop_app_id%'); ?></small>
|
||||||
|
<textarea name="pre_update_script" rows="4" class="sw-script-textarea"><?php echo $v('pre_update_script', $profile ?? []); ?></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_install_script'] ?? 'Per-mod install bash script'); ?>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_hint_install_script'] ?? 'Runs once for each mod. All template variables listed above are available.'); ?></small>
|
||||||
|
<details class="sw-example-block">
|
||||||
|
<summary><?php echo htmlspecialchars($lang['profile_script_example_toggle'] ?? 'Show DayZ-style example'); ?></summary>
|
||||||
|
<pre class="sw-code-pre">mkdir -p "%target_path%"
|
||||||
|
rsync -a --delete "%source_path%/" "%target_path%/"
|
||||||
|
if [ -d "%source_path%/keys" ]; then
|
||||||
|
mkdir -p "%keys_target_path%"
|
||||||
|
cp -f "%source_path%/keys/"*.bikey "%keys_target_path%/" 2>/dev/null || true
|
||||||
|
fi</pre>
|
||||||
|
</details>
|
||||||
|
<textarea name="install_script" rows="8" class="sw-script-textarea"><?php echo $v('install_script', $profile ?? []); ?></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_post_script'] ?? 'Post-update bash script'); ?>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_hint_post_script'] ?? 'Runs once after all mods have been installed. Variables: %home_id% %server_path% %workshop_app_id%'); ?></small>
|
||||||
|
<textarea name="post_update_script" rows="4" class="sw-script-textarea"><?php echo $v('post_update_script', $profile ?? []); ?></textarea>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Options & validation -->
|
||||||
|
<fieldset>
|
||||||
|
<legend><?php echo htmlspecialchars($lang['profile_section_flags'] ?? 'Options & validation'); ?></legend>
|
||||||
<label class="sw-checkbox">
|
<label class="sw-checkbox">
|
||||||
<input type="checkbox" name="requires_restart" value="1"
|
<input type="checkbox" name="requires_restart" value="1"
|
||||||
<?php echo !empty($profile['requires_restart']) ? 'checked' : ''; ?>>
|
<?php echo !empty($profile['requires_restart']) ? 'checked' : ''; ?>>
|
||||||
<span><?php echo htmlspecialchars($lang['profile_label_requires_restart'] ?? 'Server restart required after mod install or update'); ?></span>
|
<span><?php echo htmlspecialchars($lang['profile_label_requires_restart'] ?? 'Server restart required after mod install or update'); ?></span>
|
||||||
</label>
|
</label>
|
||||||
<label class="sw-checkbox">
|
|
||||||
<input type="checkbox" name="enabled" value="1"
|
<label>
|
||||||
<?php echo ($profile['enabled'] ?? 1) ? 'checked' : ''; ?>>
|
<?php echo htmlspecialchars($lang['profile_label_validation_notes'] ?? 'Validation notes / help text (shown to server owners)'); ?>
|
||||||
<span><?php echo htmlspecialchars($lang['config_label_enabled'] ?? 'Configuration enabled (allows servers to use Workshop mods for this game)'); ?></span>
|
<textarea name="validation_notes" rows="3"><?php echo $v('validation_notes', $profile ?? []); ?></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['profile_label_config_tpl'] ?? 'Config file template (optional)'); ?>
|
||||||
|
<textarea name="config_file_template" rows="3"><?php echo $v('config_file_template', $profile ?? []); ?></textarea>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|
@ -158,3 +312,25 @@ $tplVarNote = $lang['profile_template_vars'] ?? 'Available: {home_id} {agent_id
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Show/hide custom folder template field
|
||||||
|
const formatSel = document.getElementById('sw-folder-format');
|
||||||
|
const customWrap = document.getElementById('sw-custom-folder-wrap');
|
||||||
|
if (formatSel && customWrap) {
|
||||||
|
formatSel.addEventListener('change', function () {
|
||||||
|
customWrap.style.display = this.value === 'custom' ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide key path fields
|
||||||
|
const copyKeysChk = document.getElementById('sw-copy-keys');
|
||||||
|
const keyPathsWrap = document.getElementById('sw-key-paths-wrap');
|
||||||
|
if (copyKeysChk && keyPathsWrap) {
|
||||||
|
copyKeysChk.addEventListener('change', function () {
|
||||||
|
keyPathsWrap.style.display = this.checked ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ declare(strict_types=1);
|
||||||
<tr>
|
<tr>
|
||||||
<th><?php echo htmlspecialchars($lang['profile_col_game'] ?? 'Game'); ?></th>
|
<th><?php echo htmlspecialchars($lang['profile_col_game'] ?? 'Game'); ?></th>
|
||||||
<th><?php echo htmlspecialchars($lang['profile_col_key'] ?? 'Game Key'); ?></th>
|
<th><?php echo htmlspecialchars($lang['profile_col_key'] ?? 'Game Key'); ?></th>
|
||||||
<th>App ID</th>
|
<th><?php echo htmlspecialchars($lang['profile_col_app_ids'] ?? 'App IDs'); ?></th>
|
||||||
<th>OS</th>
|
<th><?php echo htmlspecialchars($lang['profile_col_login'] ?? 'Login'); ?></th>
|
||||||
<th><?php echo htmlspecialchars($lang['profile_col_method'] ?? 'Install Method'); ?></th>
|
<th><?php echo htmlspecialchars($lang['profile_col_method'] ?? 'Install Method'); ?></th>
|
||||||
<th><?php echo htmlspecialchars($lang['profile_col_restart'] ?? 'Restart?'); ?></th>
|
<th><?php echo htmlspecialchars($lang['profile_col_restart'] ?? 'Restart?'); ?></th>
|
||||||
<th><?php echo htmlspecialchars($lang['profile_col_status'] ?? 'Status'); ?></th>
|
<th><?php echo htmlspecialchars($lang['profile_col_status'] ?? 'Status'); ?></th>
|
||||||
|
|
@ -33,8 +33,16 @@ declare(strict_types=1);
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php echo htmlspecialchars($profile['game_name']); ?></td>
|
<td><?php echo htmlspecialchars($profile['game_name']); ?></td>
|
||||||
<td><code><?php echo htmlspecialchars($profile['game_key']); ?></code></td>
|
<td><code><?php echo htmlspecialchars($profile['game_key']); ?></code></td>
|
||||||
<td><?php echo htmlspecialchars($profile['workshop_app_id']); ?></td>
|
<td>
|
||||||
<td><?php echo htmlspecialchars($profile['supported_os']); ?></td>
|
<small><?php echo htmlspecialchars($lang['profile_col_steam'] ?? 'Steam'); ?>:</small> <?php echo htmlspecialchars($profile['steam_app_id'] !== '' ? $profile['steam_app_id'] : '—'); ?><br>
|
||||||
|
<small><?php echo htmlspecialchars($lang['profile_col_workshop'] ?? 'Workshop'); ?>:</small> <?php echo htmlspecialchars($profile['workshop_app_id']); ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo htmlspecialchars($profile['steamcmd_login_mode'] ?? 'anonymous'); ?>
|
||||||
|
<?php if (!empty($profile['steam_login_required'])): ?>
|
||||||
|
<span class="sw-badge sw-badge--warning"><?php echo htmlspecialchars($lang['profile_badge_login_required'] ?? 'Login req.'); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td><?php echo htmlspecialchars($profile['copy_method']); ?></td>
|
<td><?php echo htmlspecialchars($profile['copy_method']); ?></td>
|
||||||
<td><?php echo $profile['requires_restart'] ? '✔' : '✘'; ?></td>
|
<td><?php echo $profile['requires_restart'] ? '✔' : '✘'; ?></td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,156 @@ declare(strict_types=1);
|
||||||
/** @var string|null $appId */
|
/** @var string|null $appId */
|
||||||
/** @var array[] $installedMods */
|
/** @var array[] $installedMods */
|
||||||
/** @var array[] $availableMods */
|
/** @var array[] $availableMods */
|
||||||
|
/** @var array $serverSettings */
|
||||||
|
/** @var array[] $allProfiles */
|
||||||
/** @var bool $isAdmin */
|
/** @var bool $isAdmin */
|
||||||
|
|
||||||
$homeName = htmlspecialchars($home['home_name'] ?? ('#' . $homeId));
|
$homeName = htmlspecialchars($home['home_name'] ?? ('#' . $homeId));
|
||||||
$baseAction = '?m=steam_workshop&p=main';
|
$baseAction = '?m=steam_workshop&p=main';
|
||||||
|
|
||||||
|
$wsEnabled = !empty($serverSettings['workshop_enabled']);
|
||||||
|
$curProfileId = (int)($serverSettings['profile_id'] ?? 0);
|
||||||
|
$updateMode = (string)($serverSettings['update_mode'] ?? 'manual');
|
||||||
|
$restartBehav = (string)($serverSettings['restart_behavior'] ?? 'none');
|
||||||
|
$lastStatus = (string)($serverSettings['last_update_status'] ?? '');
|
||||||
|
$lastError = (string)($serverSettings['last_update_error'] ?? '');
|
||||||
|
$lastUpdateTime = (string)($serverSettings['last_update_time'] ?? '');
|
||||||
|
$lastSuccess = (string)($serverSettings['last_success_time'] ?? '');
|
||||||
|
$updateQueued = !empty($serverSettings['update_queued']);
|
||||||
|
|
||||||
|
$updateModes = [
|
||||||
|
'manual' => $lang['update_mode_manual'] ?? 'Manual only',
|
||||||
|
'scheduled' => $lang['update_mode_scheduled'] ?? 'Scheduled',
|
||||||
|
'on_restart' => $lang['update_mode_on_restart'] ?? 'Before server restart',
|
||||||
|
];
|
||||||
|
$restartBehaviors = [
|
||||||
|
'none' => $lang['restart_behavior_none'] ?? 'No restart',
|
||||||
|
'queue' => $lang['restart_behavior_queue'] ?? 'Queue restart',
|
||||||
|
'stop_update_start'=> $lang['restart_behavior_stop'] ?? 'Stop / Update / Start',
|
||||||
|
];
|
||||||
|
|
||||||
|
$statusClass = match($lastStatus) {
|
||||||
|
'success' => 'sw-badge--enabled',
|
||||||
|
'failed' => 'sw-badge--danger',
|
||||||
|
'running' => 'sw-badge--info',
|
||||||
|
'pending' => 'sw-badge--warning',
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
?>
|
?>
|
||||||
<div class="sw-user sw-ws-mods">
|
<div class="sw-user sw-ws-mods">
|
||||||
<p><a href="<?php echo $baseAction; ?>">← <?php echo htmlspecialchars($lang['button_cancel'] ?? 'Back'); ?></a></p>
|
<p><a href="<?php echo $baseAction; ?>">← <?php echo htmlspecialchars($lang['button_cancel'] ?? 'Back'); ?></a></p>
|
||||||
<h3><?php echo sprintf(htmlspecialchars($lang['user_workshop_server_heading'] ?? 'Workshop Mods – %s'), $homeName); ?></h3>
|
<h3><?php echo sprintf(htmlspecialchars($lang['user_workshop_server_heading'] ?? 'Workshop Mods – %s'), $homeName); ?></h3>
|
||||||
|
|
||||||
|
<!-- ── Workshop server settings ── -->
|
||||||
|
<section class="sw-server-settings">
|
||||||
|
<h4><?php echo htmlspecialchars($lang['heading_server_settings'] ?? 'Workshop Settings for this server'); ?></h4>
|
||||||
|
<form method="post" action="<?php echo $baseAction; ?>" class="sw-form sw-settings-form">
|
||||||
|
<input type="hidden" name="ws_action" value="save_settings">
|
||||||
|
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
|
||||||
|
|
||||||
|
<div class="sw-form__grid sw-form__grid--2col">
|
||||||
|
<label class="sw-checkbox">
|
||||||
|
<input type="checkbox" name="workshop_enabled" value="1" id="sw-ws-enabled"
|
||||||
|
<?php echo $wsEnabled ? 'checked' : ''; ?>>
|
||||||
|
<span><?php echo htmlspecialchars($lang['label_workshop_enabled'] ?? 'Enable Workshop for this server'); ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['label_select_profile'] ?? 'Workshop game profile'); ?>
|
||||||
|
<select name="profile_id">
|
||||||
|
<option value="0">-- <?php echo htmlspecialchars($lang['label_auto_detect'] ?? 'Auto-detect from game type'); ?> --</option>
|
||||||
|
<?php foreach ((array)$allProfiles as $p): ?>
|
||||||
|
<option value="<?php echo (int)$p['id']; ?>"
|
||||||
|
<?php echo $curProfileId === (int)$p['id'] ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($p['game_name'] . ' (' . $p['workshop_app_id'] . ')'); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['label_update_mode'] ?? 'Update mode'); ?>
|
||||||
|
<select name="update_mode">
|
||||||
|
<?php foreach ($updateModes as $mVal => $mLabel): ?>
|
||||||
|
<option value="<?php echo $mVal; ?>" <?php echo $updateMode === $mVal ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($mLabel); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php echo htmlspecialchars($lang['label_restart_behavior'] ?? 'Restart behavior'); ?>
|
||||||
|
<select name="restart_behavior">
|
||||||
|
<?php foreach ($restartBehaviors as $rVal => $rLabel): ?>
|
||||||
|
<option value="<?php echo $rVal; ?>" <?php echo $restartBehav === $rVal ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($rLabel); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sw-form__actions">
|
||||||
|
<button type="submit" class="btn primary">
|
||||||
|
<?php echo htmlspecialchars($lang['button_save'] ?? 'Save'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Update status summary -->
|
||||||
|
<div class="sw-update-status">
|
||||||
|
<dl class="sw-status-grid">
|
||||||
|
<?php if ($lastStatus !== ''): ?>
|
||||||
|
<dt><?php echo htmlspecialchars($lang['label_last_update_status'] ?? 'Last update status'); ?></dt>
|
||||||
|
<dd><span class="sw-badge <?php echo $statusClass; ?>"><?php echo htmlspecialchars($lastStatus); ?></span></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($lastUpdateTime !== ''): ?>
|
||||||
|
<dt><?php echo htmlspecialchars($lang['label_last_update_time'] ?? 'Last update time'); ?></dt>
|
||||||
|
<dd><?php echo htmlspecialchars($lastUpdateTime); ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($lastSuccess !== ''): ?>
|
||||||
|
<dt><?php echo htmlspecialchars($lang['label_last_success_time'] ?? 'Last successful update'); ?></dt>
|
||||||
|
<dd><?php echo htmlspecialchars($lastSuccess); ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($lastError !== ''): ?>
|
||||||
|
<dt><?php echo htmlspecialchars($lang['label_last_update_error'] ?? 'Last error'); ?></dt>
|
||||||
|
<dd class="sw-error-text"><code><?php echo htmlspecialchars($lastError); ?></code></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<?php if ($updateQueued): ?>
|
||||||
|
<p class="sw-notice sw-notice--info">
|
||||||
|
<?php echo htmlspecialchars($lang['update_queued_notice'] ?? 'A manual update is queued and will run on the next scheduler cycle.'); ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Queue manual update -->
|
||||||
|
<form method="post" action="<?php echo $baseAction; ?>" class="sw-inline">
|
||||||
|
<input type="hidden" name="ws_action" value="queue_update">
|
||||||
|
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
|
||||||
|
<button type="submit" class="btn secondary"
|
||||||
|
<?php echo !$wsEnabled ? 'disabled title="Enable Workshop for this server first."' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($lang['btn_queue_update'] ?? 'Queue manual update'); ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<?php if ($profile === null): ?>
|
<?php if ($profile === null): ?>
|
||||||
<div class="sw-notice">
|
<div class="sw-notice">
|
||||||
<p><?php echo htmlspecialchars($lang['no_profile_notice'] ?? 'No Workshop profile is configured for this game. An administrator needs to create one first.'); ?></p>
|
<p><?php echo htmlspecialchars($lang['no_profile_notice'] ?? 'No Workshop profile is configured for this game. An administrator needs to create one first.'); ?></p>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
|
||||||
<!-- Installed mods table -->
|
<?php if (!empty($profile['validation_notes'])): ?>
|
||||||
|
<div class="sw-notice sw-notice--info">
|
||||||
|
<strong><?php echo htmlspecialchars($lang['label_admin_notes'] ?? 'Admin notes:'); ?></strong>
|
||||||
|
<?php echo htmlspecialchars($profile['validation_notes']); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- ── Installed mods table ── -->
|
||||||
<h4><?php echo htmlspecialchars($lang['heading_installed_mods'] ?? 'Installed Mods'); ?></h4>
|
<h4><?php echo htmlspecialchars($lang['heading_installed_mods'] ?? 'Installed Mods'); ?></h4>
|
||||||
<?php if (empty($installedMods)): ?>
|
<?php if (empty($installedMods)): ?>
|
||||||
<p class="sw-empty"><?php echo htmlspecialchars($lang['no_installed_mods'] ?? 'No mods installed yet.'); ?></p>
|
<p class="sw-empty"><?php echo htmlspecialchars($lang['no_installed_mods'] ?? 'No mods installed yet.'); ?></p>
|
||||||
|
|
@ -32,6 +166,7 @@ $baseAction = '?m=steam_workshop&p=main';
|
||||||
<tr>
|
<tr>
|
||||||
<th><?php echo htmlspecialchars($lang['col_mod_id'] ?? 'Workshop ID'); ?></th>
|
<th><?php echo htmlspecialchars($lang['col_mod_id'] ?? 'Workshop ID'); ?></th>
|
||||||
<th><?php echo htmlspecialchars($lang['col_mod_title'] ?? 'Title'); ?></th>
|
<th><?php echo htmlspecialchars($lang['col_mod_title'] ?? 'Title'); ?></th>
|
||||||
|
<th><?php echo htmlspecialchars($lang['col_mod_folder'] ?? 'Install folder'); ?></th>
|
||||||
<th><?php echo htmlspecialchars($lang['mods_header_enabled'] ?? 'Enabled'); ?></th>
|
<th><?php echo htmlspecialchars($lang['mods_header_enabled'] ?? 'Enabled'); ?></th>
|
||||||
<th><?php echo htmlspecialchars($lang['col_load_order'] ?? 'Load order'); ?></th>
|
<th><?php echo htmlspecialchars($lang['col_load_order'] ?? 'Load order'); ?></th>
|
||||||
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
|
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
|
||||||
|
|
@ -46,6 +181,7 @@ $baseAction = '?m=steam_workshop&p=main';
|
||||||
target="_blank" rel="noopener"><?php echo $wid; ?></a>
|
target="_blank" rel="noopener"><?php echo $wid; ?></a>
|
||||||
</td>
|
</td>
|
||||||
<td><?php echo htmlspecialchars($mod['title'] ?? $mod['workshop_id']); ?></td>
|
<td><?php echo htmlspecialchars($mod['title'] ?? $mod['workshop_id']); ?></td>
|
||||||
|
<td><code><?php echo htmlspecialchars($mod['custom_folder'] !== '' ? $mod['custom_folder'] : ($mod['install_path'] ?? '')); ?></code></td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" action="<?php echo $baseAction; ?>" class="sw-toggle-form">
|
<form method="post" action="<?php echo $baseAction; ?>" class="sw-toggle-form">
|
||||||
<input type="hidden" name="ws_action" value="toggle">
|
<input type="hidden" name="ws_action" value="toggle">
|
||||||
|
|
@ -97,7 +233,7 @@ $baseAction = '?m=steam_workshop&p=main';
|
||||||
</table>
|
</table>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Install from cache -->
|
<!-- ── Available cached mods ── -->
|
||||||
<?php if (!empty($availableMods)): ?>
|
<?php if (!empty($availableMods)): ?>
|
||||||
<h4><?php echo htmlspecialchars($lang['heading_cached_mods'] ?? 'Available Cached Mods (this agent)'); ?></h4>
|
<h4><?php echo htmlspecialchars($lang['heading_cached_mods'] ?? 'Available Cached Mods (this agent)'); ?></h4>
|
||||||
<table class="table sw-ws-mods__cache-table">
|
<table class="table sw-ws-mods__cache-table">
|
||||||
|
|
@ -132,7 +268,7 @@ $baseAction = '?m=steam_workshop&p=main';
|
||||||
</table>
|
</table>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Search + install by Workshop ID -->
|
<!-- ── Install by Workshop ID ── -->
|
||||||
<h4><?php echo htmlspecialchars($lang['heading_install_mod'] ?? 'Install Mod by Workshop ID'); ?></h4>
|
<h4><?php echo htmlspecialchars($lang['heading_install_mod'] ?? 'Install Mod by Workshop ID'); ?></h4>
|
||||||
<form method="post" action="<?php echo $baseAction; ?>" class="sw-form sw-install-form">
|
<form method="post" action="<?php echo $baseAction; ?>" class="sw-form sw-install-form">
|
||||||
<input type="hidden" name="ws_action" value="install">
|
<input type="hidden" name="ws_action" value="install">
|
||||||
|
|
@ -149,10 +285,10 @@ $baseAction = '?m=steam_workshop&p=main';
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Steam Workshop search widget (reuse existing JS picker) -->
|
<!-- ── Steam Workshop search widget ── -->
|
||||||
<?php
|
<?php
|
||||||
$scriptPath = (string)($_SERVER['PHP_SELF'] ?? '/index.php');
|
$requestPath = (string)($_SERVER['PHP_SELF'] ?? '/index.php');
|
||||||
$searchEndpoint = sprintf('%s?m=steam_workshop&p=main&action=search&home_id=%d', $scriptPath, $homeId);
|
$searchEndpoint = sprintf('%s?m=steam_workshop&p=main&action=search&home_id=%d', $requestPath, $homeId);
|
||||||
$langAttrs = [
|
$langAttrs = [
|
||||||
'add' => $lang['mod_picker_action_add'] ?? 'Add',
|
'add' => $lang['mod_picker_action_add'] ?? 'Add',
|
||||||
'remove' => $lang['mod_picker_action_remove'] ?? 'Remove',
|
'remove' => $lang['mod_picker_action_remove'] ?? 'Remove',
|
||||||
|
|
@ -201,20 +337,15 @@ $baseAction = '?m=steam_workshop&p=main';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* Simple toggle / order auto-submit for the mods table */
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Toggle enable/disable: submit the parent form immediately on change
|
// Toggle enable/disable: submit the parent form immediately on change
|
||||||
document.querySelectorAll('.js-ws-toggle').forEach(function (cb) {
|
document.querySelectorAll('.js-ws-toggle').forEach(function (cb) {
|
||||||
cb.addEventListener('change', function () {
|
cb.addEventListener('change', function () { cb.closest('form').submit(); });
|
||||||
cb.closest('form').submit();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load order: submit on change (blur triggers faster than enter on number inputs)
|
// Load order: submit on change
|
||||||
document.querySelectorAll('.js-ws-order').forEach(function (inp) {
|
document.querySelectorAll('.js-ws-order').forEach(function (inp) {
|
||||||
inp.addEventListener('change', function () {
|
inp.addEventListener('change', function () { inp.closest('form').submit(); });
|
||||||
inp.closest('form').submit();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue