feat: add database-driven Steam Workshop system

- Create 3 new DB tables: workshop_game_profiles, workshop_cache, server_workshop_mods
- Add WorkshopRepository (DB access layer for all 3 tables)
- Add WorkshopInstaller (rsync/robocopy/custom_script copy logic, SteamCMD download via agent exec)
- Add WorkshopUpdater (scheduled cache update functions grouped by agent)
- Add WorkshopPreStart (pre-start mod sync helper)
- Add WorkshopProfileController (admin CRUD for profiles)
- Add WorkshopModController (user install/remove/toggle/load_order/sync)
- Add admin views: profiles list + profile_form
- Add user views: user_workshop_index + user_workshop_mods
- Add cron_update.php CLI entry point (--all/--agent-id/--home-id/--profile-id/--workshop-id)
- Add prestart_sync.php CLI helper for XML pre_start hook
- Update workshop_admin.php to route to profile management
- Update main.php to route to new mod management (legacy fallback preserved)
- Update module.php with DB migration SQL and version bump to 2.1
- Update lang/en_US.php with all new strings

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/dbeebd0e-e7a5-469d-8a8c-e63193d1ebb0

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-30 18:01:33 +00:00 committed by GitHub
parent 4ad46c4332
commit 8eff063a93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 3007 additions and 8 deletions

View file

@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array|null $profile existing row when editing, null when creating */
/** @var int $profileId */
$isEdit = $profileId > 0 && $profile !== null;
$heading = $isEdit
? sprintf($lang['profile_heading_edit'] ?? 'Edit Workshop Profile: %s', htmlspecialchars($profile['game_name'] ?? ''))
: ($lang['profile_heading_create'] ?? 'Create Workshop Profile');
$v = static function (string $key, array $profile, string $default = ''): string {
return htmlspecialchars((string)($profile[$key] ?? $default), ENT_QUOTES);
};
$osList = ['linux' => 'Linux', 'windows' => 'Windows'];
$currentOs = array_filter(explode(',', (string)($profile['supported_os'] ?? 'linux')));
$methodList = ['rsync' => 'rsync (Linux)', 'robocopy' => 'robocopy (Windows)', 'custom_script' => 'custom_script'];
$curMethod = (string)($profile['copy_method'] ?? 'rsync');
$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}';
?>
<div class="sw-admin sw-profile-form">
<h3><?php echo $heading; ?></h3>
<p><a href="?m=steam_workshop&p=workshop_admin&sw_action=profiles">&larr; <?php echo htmlspecialchars($lang['profile_back_list'] ?? 'Back to profiles'); ?></a></p>
<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="profile_id" value="<?php echo $profileId; ?>">
<!-- Basic info -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_basic'] ?? 'Basic info'); ?></legend>
<div class="sw-form__grid">
<label>
<?php echo htmlspecialchars($lang['label_game_key'] ?? 'Game key'); ?> <em>*</em>
<input type="text" name="game_key" value="<?php echo $v('game_key', $profile ?? []); ?>"
pattern="[A-Za-z0-9_\-.]+" required maxlength="100"
<?php echo $isEdit ? 'readonly' : ''; ?>>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_game_name'] ?? 'Game name'); ?> <em>*</em>
<input type="text" name="game_name" value="<?php echo $v('game_name', $profile ?? []); ?>"
required maxlength="255">
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_app_id'] ?? 'Workshop App ID'); ?> <em>*</em>
<input type="text" name="workshop_app_id"
value="<?php echo $v('workshop_app_id', $profile ?? []); ?>"
pattern="[0-9]+" required maxlength="32">
</label>
</div>
<fieldset class="sw-form__os-group">
<legend><?php echo htmlspecialchars($lang['profile_label_os'] ?? 'Supported OS'); ?></legend>
<?php foreach ($osList as $osVal => $osLabel): ?>
<label class="sw-checkbox">
<input type="checkbox" name="supported_os[]" value="<?php echo $osVal; ?>"
<?php echo in_array($osVal, $currentOs, true) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($osLabel); ?></span>
</label>
<?php endforeach; ?>
</fieldset>
</fieldset>
<!-- Paths / templates -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_paths'] ?? 'Paths &amp; templates'); ?></legend>
<small class="sw-hint"><?php echo htmlspecialchars($tplVarNote); ?></small>
<label>
<?php echo htmlspecialchars($lang['profile_label_cache_path'] ?? 'Cache path template'); ?> <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>
<input type="text" name="cache_path_template"
value="<?php echo $v('cache_path_template', $profile ?? []); ?>" required>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_install_path'] ?? 'Install path template'); ?> <em>*</em>
<small><?php echo htmlspecialchars($lang['profile_hint_install_path'] ?? 'Server-side mod directory. E.g. {server_path}/mods/{mod_folder}'); ?></small>
<input type="text" name="install_path_template"
value="<?php echo $v('install_path_template', $profile ?? []); ?>" required>
</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. Default: @{mod_id}'); ?></small>
<input type="text" name="folder_name_template"
value="<?php echo $v('folder_name_template', $profile ?? [], '@{mod_id}'); ?>">
</label>
</fieldset>
<!-- Copy method -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_copy'] ?? 'Copy / sync method'); ?></legend>
<label>
<?php echo htmlspecialchars($lang['profile_label_copy_method'] ?? 'Copy method'); ?>
<select name="copy_method">
<?php foreach ($methodList as $mVal => $mLabel): ?>
<option value="<?php echo $mVal; ?>" <?php echo $curMethod === $mVal ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($mLabel); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_install_script'] ?? 'Custom install script (admin-defined only, optional)'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_install_script'] ?? 'Only used when copy method is custom_script. Template variables are replaced before execution.'); ?></small>
<textarea name="install_script" rows="4"><?php echo $v('install_script', $profile ?? []); ?></textarea>
</label>
</fieldset>
<!-- Config / launch params -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_config'] ?? 'Config &amp; launch parameters'); ?></legend>
<label>
<?php echo htmlspecialchars($lang['profile_label_config_tpl'] ?? 'Config file template (optional)'); ?>
<textarea name="config_file_template" rows="4"><?php echo $v('config_file_template', $profile ?? []); ?></textarea>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_launch_tpl'] ?? 'Launch parameter template (optional)'); ?>
<input type="text" name="launch_param_template"
value="<?php echo $v('launch_param_template', $profile ?? []); ?>">
</label>
</fieldset>
<!-- Flags -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_flags'] ?? 'Flags'); ?></legend>
<label class="sw-checkbox">
<input type="checkbox" name="requires_restart" value="1"
<?php echo !empty($profile['requires_restart']) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($lang['profile_label_requires_restart'] ?? 'Restart required after mod install/update'); ?></span>
</label>
<label class="sw-checkbox">
<input type="checkbox" name="enabled" value="1"
<?php echo (!isset($profile['enabled']) || !empty($profile['enabled'])) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($lang['profile_label_enabled'] ?? 'Profile enabled'); ?></span>
</label>
</fieldset>
<div class="sw-form__actions">
<button class="btn primary" type="submit">
<?php echo htmlspecialchars($lang['button_save'] ?? 'Save'); ?>
</button>
<a class="btn" href="?m=steam_workshop&p=workshop_admin&sw_action=profiles">
<?php echo htmlspecialchars($lang['button_cancel'] ?? 'Cancel'); ?>
</a>
</div>
</form>
</div>

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array[] $profiles */
?>
<div class="sw-admin sw-profiles">
<div class="sw-admin__intro">
<h3><?php echo htmlspecialchars($lang['profile_heading_list'] ?? 'Workshop Game Profiles'); ?></h3>
<p><?php echo htmlspecialchars($lang['profile_intro'] ?? 'One profile per supported game. Each profile drives mod install and caching behaviour.'); ?></p>
<a class="btn primary" href="?m=steam_workshop&p=workshop_admin&sw_action=profile_form">
<?php echo htmlspecialchars($lang['profile_btn_create'] ?? 'Create Profile'); ?>
</a>
</div>
<?php if (empty($profiles)): ?>
<p class="sw-empty"><?php echo htmlspecialchars($lang['profile_list_empty'] ?? 'No Workshop profiles defined yet.'); ?></p>
<?php else: ?>
<table class="table sw-profiles__table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['profile_col_game'] ?? 'Game'); ?></th>
<th><?php echo htmlspecialchars($lang['profile_col_key'] ?? 'Game Key'); ?></th>
<th>App ID</th>
<th>OS</th>
<th><?php echo htmlspecialchars($lang['profile_col_method'] ?? 'Copy Method'); ?></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['admin_col_actions'] ?? 'Actions'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$profiles as $profile): ?>
<tr>
<td><?php echo htmlspecialchars($profile['game_name']); ?></td>
<td><code><?php echo htmlspecialchars($profile['game_key']); ?></code></td>
<td><?php echo htmlspecialchars($profile['workshop_app_id']); ?></td>
<td><?php echo htmlspecialchars($profile['supported_os']); ?></td>
<td><?php echo htmlspecialchars($profile['copy_method']); ?></td>
<td><?php echo $profile['requires_restart'] ? '✔' : '✘'; ?></td>
<td>
<?php if ($profile['enabled']): ?>
<span class="sw-badge sw-badge--enabled"><?php echo htmlspecialchars($lang['status_enabled'] ?? 'Enabled'); ?></span>
<?php else: ?>
<span class="sw-badge sw-badge--disabled"><?php echo htmlspecialchars($lang['status_disabled'] ?? 'Disabled'); ?></span>
<?php endif; ?>
</td>
<td class="sw-actions">
<a class="btn secondary"
href="?m=steam_workshop&p=workshop_admin&sw_action=profile_form&profile_id=<?php echo (int)$profile['id']; ?>">
<?php echo htmlspecialchars($lang['button_edit'] ?? 'Edit'); ?>
</a>
<form method="post" action="?m=steam_workshop&p=workshop_admin" class="sw-inline-delete">
<input type="hidden" name="sw_action" value="profile_delete">
<input type="hidden" name="profile_id" value="<?php echo (int)$profile['id']; ?>">
<button type="submit" class="btn danger"
onclick="return confirm('<?php echo htmlspecialchars($lang['profile_confirm_delete'] ?? 'Delete this Workshop profile?'); ?>')">
<?php echo htmlspecialchars($lang['button_delete_adapter'] ?? 'Delete'); ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<hr>
<p>
<a href="?m=steam_workshop&p=workshop_admin">&larr;
<?php echo htmlspecialchars($lang['profile_back_adapters'] ?? 'Back to adapter management'); ?>
</a>
</p>
</div>