Updated workshop
This commit is contained in:
parent
0d4cbd66fc
commit
0885bfef92
6 changed files with 531 additions and 75 deletions
4
CHANGELOG.md
Normal file
4
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Changelog
|
||||
|
||||
## 2026-01-17
|
||||
- Added per-game Steam Workshop adapter management with CRUD UI and automatic mapping helpers.
|
||||
1
docs/COPILOT_TODO.md
Normal file
1
docs/COPILOT_TODO.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Auto-detect which server configs actually support Steam Workshop before showing adapter controls.
|
||||
|
|
@ -7,6 +7,8 @@ class AdminWorkshopController
|
|||
{
|
||||
private SteamWorkshopService $service;
|
||||
private array $lang;
|
||||
private ?array $adapterFormOverride = null;
|
||||
private ?string $adapterFormGameKey = null;
|
||||
|
||||
public function __construct(OGPDatabase $db)
|
||||
{
|
||||
|
|
@ -27,13 +29,19 @@ class AdminWorkshopController
|
|||
echo '<link rel="stylesheet" type="text/css" href="modules/steam_workshop/steam_workshop.css" />';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->processSave();
|
||||
$this->processPost();
|
||||
}
|
||||
|
||||
$gameKeys = $this->service->listAvailableGameKeys();
|
||||
$mappings = $this->service->getAdapterMappings();
|
||||
$adapters = $this->service->loadAdapters();
|
||||
$adapterOptions = $this->service->getAdapterOptions();
|
||||
$gameRows = $this->buildGameRows($gameKeys);
|
||||
|
||||
$activeGame = $this->resolveActiveGameKey();
|
||||
$adapterForm = $activeGame !== ''
|
||||
? $this->service->getAdapterFormData($activeGame, $this->adapterFormOverride)
|
||||
: null;
|
||||
|
||||
$this->render('admin/index', [
|
||||
'lang' => $this->lang,
|
||||
|
|
@ -41,10 +49,30 @@ class AdminWorkshopController
|
|||
'mappings' => $mappings,
|
||||
'adapters' => $adapters,
|
||||
'adapterOptions' => $adapterOptions,
|
||||
'gameRows' => $gameRows,
|
||||
'adapterForm' => $adapterForm,
|
||||
'activeGameKey' => $activeGame,
|
||||
]);
|
||||
}
|
||||
|
||||
private function processSave(): void
|
||||
private function processPost(): void
|
||||
{
|
||||
$action = $_POST['admin_action'] ?? 'save_mappings';
|
||||
switch ($action) {
|
||||
case 'save_adapter':
|
||||
$this->processAdapterSave();
|
||||
break;
|
||||
case 'delete_adapter':
|
||||
$this->processAdapterDelete();
|
||||
break;
|
||||
case 'save_mappings':
|
||||
default:
|
||||
$this->processSaveMappings();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function processSaveMappings(): void
|
||||
{
|
||||
$payload = $_POST['mapping'] ?? [];
|
||||
if (!is_array($payload)) {
|
||||
|
|
@ -54,6 +82,88 @@ class AdminWorkshopController
|
|||
print_success($this->lang['message_mappings_saved'] ?? 'Adapter mappings saved.');
|
||||
}
|
||||
|
||||
private function processAdapterSave(): void
|
||||
{
|
||||
$gameKey = $this->sanitizeGameKeyInput($_POST['game_key'] ?? '');
|
||||
if ($gameKey === '') {
|
||||
print_failure($this->lang['error_game_key_required'] ?? 'Game key required.');
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = $_POST['adapter'] ?? [];
|
||||
if (!is_array($payload)) {
|
||||
$payload = [];
|
||||
}
|
||||
|
||||
try {
|
||||
$this->service->saveGameAdapter($gameKey, $payload);
|
||||
$this->service->upsertAdapterMapping($gameKey, $gameKey);
|
||||
print_success($this->lang['message_adapter_saved'] ?? 'Adapter saved.');
|
||||
$this->adapterFormOverride = null;
|
||||
$this->adapterFormGameKey = null;
|
||||
} catch (RuntimeException $e) {
|
||||
$this->adapterFormGameKey = $gameKey;
|
||||
$this->adapterFormOverride = [
|
||||
'name' => trim((string)($payload['name'] ?? '')),
|
||||
'steam_app_id' => trim((string)($payload['steam_app_id'] ?? '')),
|
||||
'mods_dir' => trim((string)($payload['mods_dir'] ?? '')),
|
||||
'keys_dir' => trim((string)($payload['keys_dir'] ?? '')),
|
||||
'supports_hot_reload' => !empty($payload['supports_hot_reload']),
|
||||
'activation_template' => trim((string)($payload['activation_template'] ?? '')),
|
||||
'notes' => trim((string)($payload['notes'] ?? '')),
|
||||
];
|
||||
print_failure($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function processAdapterDelete(): void
|
||||
{
|
||||
$gameKey = $this->sanitizeGameKeyInput($_POST['game_key'] ?? '');
|
||||
if ($gameKey === '') {
|
||||
print_failure($this->lang['error_game_key_required'] ?? 'Game key required.');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->service->deleteGameAdapter($gameKey)) {
|
||||
$this->service->removeAdapterMapping($gameKey, $gameKey);
|
||||
print_success($this->lang['message_adapter_deleted'] ?? 'Adapter deleted.');
|
||||
} else {
|
||||
print_failure($this->lang['error_adapter_delete_failed'] ?? 'Unable to delete adapter.');
|
||||
}
|
||||
}
|
||||
|
||||
private function buildGameRows(array $gameKeys): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach ($gameKeys as $gameKey) {
|
||||
$rows[] = [
|
||||
'game_key' => $gameKey,
|
||||
'exists' => $this->service->gameAdapterExists($gameKey),
|
||||
'adapter' => $this->service->getGameAdapter($gameKey),
|
||||
'updated_at' => $this->service->getGameAdapterUpdatedAt($gameKey),
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private function resolveActiveGameKey(): string
|
||||
{
|
||||
if ($this->adapterFormGameKey !== null) {
|
||||
return $this->adapterFormGameKey;
|
||||
}
|
||||
|
||||
$queryKey = $_GET['adapter_game'] ?? '';
|
||||
return $this->sanitizeGameKeyInput($queryKey);
|
||||
}
|
||||
|
||||
private function sanitizeGameKeyInput($value): string
|
||||
{
|
||||
$gameKey = strtolower(trim((string)$value));
|
||||
$sanitized = preg_replace('/[^a-z0-9_\-.]/', '', $gameKey);
|
||||
return is_string($sanitized) ? $sanitized : '';
|
||||
}
|
||||
|
||||
private function render(string $view, array $data = []): void
|
||||
{
|
||||
extract($data);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ return [
|
|||
'admin_col_key' => 'Key',
|
||||
'admin_col_mods_dir' => 'Mods directory',
|
||||
'admin_col_notes' => 'Notes',
|
||||
'admin_heading_per_game' => 'Per-game adapters',
|
||||
'admin_subheading_per_game' => 'Each game should have its own adapter XML for Steam Workshop automation.',
|
||||
'admin_col_status' => 'Status',
|
||||
'admin_col_updated' => 'Last updated',
|
||||
'admin_col_actions' => 'Actions',
|
||||
'admin_heading_edit_adapter' => 'Editing adapter for %s',
|
||||
'admin_hint_select_game' => 'Select a game in the table above to edit or create its adapter.',
|
||||
'status_no_adapter' => 'No adapter defined',
|
||||
'status_enabled' => 'Enabled',
|
||||
'status_disabled' => 'Disabled',
|
||||
'status_hot_reload' => 'Hot reload ready',
|
||||
|
|
@ -53,5 +61,22 @@ return [
|
|||
'summary_hot_reload' => 'Hot reload',
|
||||
'raw_definition_label' => 'Raw Workshop list',
|
||||
'message_mappings_saved' => 'Adapter mappings saved.',
|
||||
'message_adapter_saved' => 'Adapter saved.',
|
||||
'message_adapter_deleted' => 'Adapter deleted.',
|
||||
'error_admin_only' => 'Administrator access required.',
|
||||
'error_game_key_required' => 'Select a valid game key before editing the adapter.',
|
||||
'error_adapter_delete_failed' => 'Adapter could not be deleted.',
|
||||
'button_edit_adapter' => 'Edit',
|
||||
'button_create_adapter' => 'Create',
|
||||
'button_delete_adapter' => 'Delete',
|
||||
'button_save_adapter' => 'Save adapter',
|
||||
'confirm_delete_adapter' => 'Delete this adapter? Servers mapped to it will fall back to defaults.',
|
||||
'label_game_key' => 'Game key',
|
||||
'label_adapter_name' => 'Adapter display name',
|
||||
'label_adapter_app_id' => 'Steam App ID',
|
||||
'label_adapter_mods_dir' => 'Mods directory',
|
||||
'label_adapter_keys_dir' => 'Keys directory (optional)',
|
||||
'label_adapter_hot_reload' => 'Supports hot reload',
|
||||
'label_adapter_activation' => 'Activation template',
|
||||
'label_adapter_notes' => 'Notes',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class SteamWorkshopService
|
|||
private string $configDir;
|
||||
private string $adapterDir;
|
||||
private string $adapterMapFile;
|
||||
private string $gameAdapterDir;
|
||||
|
||||
public function __construct(OGPDatabase $db)
|
||||
{
|
||||
|
|
@ -17,11 +18,16 @@ class SteamWorkshopService
|
|||
$this->configDir = __DIR__ . '/../data/configs';
|
||||
$this->adapterDir = __DIR__ . '/GameAdapters';
|
||||
$this->adapterMapFile = __DIR__ . '/../data/game_adapter_map.json';
|
||||
$this->gameAdapterDir = __DIR__ . '/../data/game_adapters';
|
||||
|
||||
if (!is_dir($this->configDir)) {
|
||||
mkdir($this->configDir, 0775, true);
|
||||
}
|
||||
|
||||
if (!is_dir($this->gameAdapterDir)) {
|
||||
mkdir($this->gameAdapterDir, 0775, true);
|
||||
}
|
||||
|
||||
$this->ensureDataFiles();
|
||||
}
|
||||
|
||||
|
|
@ -151,56 +157,62 @@ class SteamWorkshopService
|
|||
|
||||
$doc->save($path);
|
||||
}
|
||||
if ($gameKey === '') {
|
||||
throw new RuntimeException('Game key is required.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert POST payload into a config array and merge defaults.
|
||||
*/
|
||||
public function buildConfigFromRequest(array $payload): array
|
||||
{
|
||||
$input = $payload['workshop'] ?? [];
|
||||
$rawMods = trim((string)($input['raw_items'] ?? ''));
|
||||
$items = $this->parseWorkshopItems($rawMods);
|
||||
$normalized = $this->normalizeAdapterData($gameKey, $data);
|
||||
if ($normalized['steam_app_id'] === '') {
|
||||
throw new RuntimeException('Steam App ID is required.');
|
||||
}
|
||||
if ($normalized['mods_dir'] === '') {
|
||||
throw new RuntimeException('Mods directory is required.');
|
||||
}
|
||||
|
||||
return [
|
||||
'workshop_enabled' => isset($input['workshop_enabled']) ? (bool)$input['workshop_enabled'] : false,
|
||||
'adapter_key' => $this->sanitizeAdapterKey((string)($input['adapter_key'] ?? 'dayz')),
|
||||
'update_interval_minutes' => $this->sanitizeInterval(isset($input['update_interval_minutes']) ? (int)$input['update_interval_minutes'] : null),
|
||||
'staging_dir' => trim((string)($input['staging_dir'] ?? '')),
|
||||
'install_strategy' => $this->sanitizeInstallStrategy((string)($input['install_strategy'] ?? 'copy')),
|
||||
'on_update_action' => $this->sanitizeUpdateAction((string)($input['on_update_action'] ?? 'queue_for_restart')),
|
||||
'post_install_script' => trim((string)($input['post_install_script'] ?? '')),
|
||||
'workshop_items' => $items,
|
||||
'raw_definition' => $rawMods,
|
||||
];
|
||||
}
|
||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
||||
$doc->formatOutput = true;
|
||||
|
||||
/**
|
||||
* Accepts imports such as "123456,@My Mod" per line.
|
||||
*
|
||||
* @return array<int, array{id:string,label:string,enabled:bool,source:string}>
|
||||
*/
|
||||
public function parseWorkshopItems(string $raw): array
|
||||
{
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
$root = $doc->createElement('adapter');
|
||||
$root->setAttribute('key', $gameKey);
|
||||
$root->setAttribute('name', $normalized['name']);
|
||||
$doc->appendChild($root);
|
||||
|
||||
$items = [];
|
||||
$root->appendChild($doc->createElement('steamAppId', $normalized['steam_app_id']));
|
||||
$root->appendChild($doc->createElement('modsDir', $normalized['mods_dir']));
|
||||
if ($normalized['keys_dir'] !== '') {
|
||||
$root->appendChild($doc->createElement('keysDir', $normalized['keys_dir']));
|
||||
}
|
||||
$root->appendChild($doc->createElement('supportsHotReload', $normalized['supports_hot_reload'] ? 'true' : 'false'));
|
||||
|
||||
$activationNode = $doc->createElement('activation');
|
||||
$templateNode = $doc->createElement('template');
|
||||
if ($normalized['activation_template'] !== '') {
|
||||
$templateNode->appendChild($doc->createCDATASection($normalized['activation_template']));
|
||||
}
|
||||
$activationNode->appendChild($templateNode);
|
||||
$root->appendChild($activationNode);
|
||||
|
||||
if ($normalized['notes'] !== '') {
|
||||
$root->appendChild($doc->createElement('notes', $normalized['notes']));
|
||||
}
|
||||
|
||||
$path = $this->getGameAdapterPath($gameKey);
|
||||
$doc->save($path);
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
if ($gameKey === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts = array_map('trim', explode(',', $line, 2));
|
||||
$id = preg_replace('/[^0-9]/', '', $parts[0]);
|
||||
if ($id === '') {
|
||||
continue;
|
||||
$path = $this->getGameAdapterPath($gameKey);
|
||||
if (!is_file($path)) {
|
||||
return false;
|
||||
}
|
||||
$label = $parts[1] ?? '';
|
||||
if ($label === '') {
|
||||
$label = '@' . $id;
|
||||
|
||||
return unlink($path);
|
||||
}
|
||||
|
||||
$items[] = [
|
||||
|
|
@ -252,53 +264,41 @@ class SteamWorkshopService
|
|||
*/
|
||||
public function loadAdapters(): array
|
||||
{
|
||||
$adapters = [];
|
||||
$result = [];
|
||||
$schema = $this->adapterDir . '/schema.xsd';
|
||||
$useSchema = is_file($schema);
|
||||
$previousLibxml = libxml_use_internal_errors(true);
|
||||
|
||||
foreach (glob($this->adapterDir . '/*.xml') as $file) {
|
||||
if (substr($file, -4) !== '.xml' || basename($file) === 'schema.xsd') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parsed = $this->parseAdapterFile($file, $schema, $useSchema);
|
||||
if ($parsed !== null) {
|
||||
$parsed['origin'] = 'shared';
|
||||
$result[] = $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (glob($this->gameAdapterDir . '/*.xml') as $file) {
|
||||
if (substr($file, -4) !== '.xml') {
|
||||
continue;
|
||||
}
|
||||
if (basename($file) === 'schema.xsd') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
if (!$doc->load($file)) {
|
||||
continue;
|
||||
$gameKey = basename($file, '.xml');
|
||||
$parsed = $this->parseAdapterFile($file, $schema, $useSchema, $gameKey);
|
||||
if ($parsed !== null) {
|
||||
$parsed['origin'] = 'custom';
|
||||
$result[] = $parsed;
|
||||
}
|
||||
|
||||
if ($useSchema && !$doc->schemaValidate($schema)) {
|
||||
libxml_clear_errors();
|
||||
continue;
|
||||
}
|
||||
|
||||
$adapter = simplexml_import_dom($doc);
|
||||
if ($adapter === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$adapters[] = [
|
||||
'key' => (string)($adapter['key'] ?? ''),
|
||||
'name' => (string)($adapter['name'] ?? ''),
|
||||
'steam_app_id' => (string)($adapter->steamAppId ?? ''),
|
||||
'mods_dir' => (string)($adapter->modsDir ?? ''),
|
||||
'keys_dir' => isset($adapter->keysDir) ? (string)$adapter->keysDir : null,
|
||||
'supports_hot_reload' => filter_var((string)($adapter->supportsHotReload ?? 'false'), FILTER_VALIDATE_BOOLEAN),
|
||||
'activation_template' => (string)($adapter->activation->template ?? ''),
|
||||
'notes' => (string)($adapter->notes ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
$result = array_values(array_filter($adapters, static function (array $adapter): bool {
|
||||
return $adapter['key'] !== '';
|
||||
}));
|
||||
|
||||
libxml_use_internal_errors($previousLibxml);
|
||||
|
||||
return $result;
|
||||
return array_values(array_filter($result, static function (array $adapter): bool {
|
||||
return $adapter['key'] !== '';
|
||||
}));
|
||||
}
|
||||
|
||||
public function getAdapterByKey(string $key): array
|
||||
|
|
@ -323,7 +323,15 @@ class SteamWorkshopService
|
|||
}
|
||||
|
||||
$map = $this->getAdapterMappings();
|
||||
return $map[$gameKey] ?? null;
|
||||
if (isset($map[$gameKey])) {
|
||||
return $map[$gameKey];
|
||||
}
|
||||
|
||||
if ($this->gameAdapterExists($gameKey)) {
|
||||
return $gameKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -345,6 +353,33 @@ class SteamWorkshopService
|
|||
file_put_contents($this->adapterMapFile, json_encode($sanitized, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
public function upsertAdapterMapping(string $gameKey, string $adapterKey): void
|
||||
{
|
||||
$gameKey = $this->sanitizeGameKey($gameKey);
|
||||
$adapterKey = $this->sanitizeAdapterKey($adapterKey);
|
||||
if ($gameKey === '' || $adapterKey === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$map = $this->getAdapterMappings();
|
||||
$map[$gameKey] = $adapterKey;
|
||||
file_put_contents($this->adapterMapFile, json_encode($map, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
public function removeAdapterMapping(string $gameKey, ?string $adapterKey = null): void
|
||||
{
|
||||
$gameKey = $this->sanitizeGameKey($gameKey);
|
||||
if ($gameKey === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$map = $this->getAdapterMappings();
|
||||
if ($adapterKey === null || (isset($map[$gameKey]) && $map[$gameKey] === $adapterKey)) {
|
||||
unset($map[$gameKey]);
|
||||
file_put_contents($this->adapterMapFile, json_encode($map, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
|
|
@ -371,6 +406,101 @@ class SteamWorkshopService
|
|||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return metadata for every custom adapter stored on disk.
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
public function listGameAdapters(): array
|
||||
{
|
||||
$adapters = [];
|
||||
$schema = $this->adapterDir . '/schema.xsd';
|
||||
$useSchema = is_file($schema);
|
||||
foreach (glob($this->gameAdapterDir . '/*.xml') as $file) {
|
||||
$gameKey = basename($file, '.xml');
|
||||
$parsed = $this->parseAdapterFile($file, $schema, $useSchema, $gameKey);
|
||||
if ($parsed !== null) {
|
||||
$parsed['origin'] = 'custom';
|
||||
$parsed['game_key'] = $gameKey;
|
||||
$adapters[] = $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return $adapters;
|
||||
}
|
||||
|
||||
public function gameAdapterExists(string $gameKey): bool
|
||||
{
|
||||
$gameKey = $this->sanitizeGameKey($gameKey);
|
||||
if ($gameKey === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_file($this->getGameAdapterPath($gameKey));
|
||||
}
|
||||
|
||||
public function getGameAdapter(string $gameKey): ?array
|
||||
{
|
||||
$gameKey = $this->sanitizeGameKey($gameKey);
|
||||
if ($gameKey === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = $this->getGameAdapterPath($gameKey);
|
||||
if (!is_file($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->parseAdapterFile($path, $this->adapterDir . '/schema.xsd', is_file($this->adapterDir . '/schema.xsd'), $gameKey);
|
||||
}
|
||||
|
||||
public function getGameAdapterUpdatedAt(string $gameKey): ?int
|
||||
{
|
||||
$path = $this->getGameAdapterPath($gameKey);
|
||||
if (!is_file($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mtime = filemtime($path);
|
||||
return $mtime === false ? null : $mtime;
|
||||
}
|
||||
|
||||
public function getAdapterFormData(string $gameKey, ?array $overrides = null): array
|
||||
{
|
||||
$gameKey = $this->sanitizeGameKey($gameKey);
|
||||
$defaults = [
|
||||
'game_key' => $gameKey,
|
||||
'name' => $gameKey,
|
||||
'steam_app_id' => '',
|
||||
'mods_dir' => '',
|
||||
'keys_dir' => '',
|
||||
'supports_hot_reload' => false,
|
||||
'activation_template' => '',
|
||||
'notes' => '',
|
||||
'exists' => false,
|
||||
];
|
||||
|
||||
$current = $this->getGameAdapter($gameKey);
|
||||
if ($current !== null) {
|
||||
$defaults = array_merge($defaults, [
|
||||
'name' => $current['name'] ?? $gameKey,
|
||||
'steam_app_id' => $current['steam_app_id'] ?? '',
|
||||
'mods_dir' => $current['mods_dir'] ?? '',
|
||||
'keys_dir' => $current['keys_dir'] ?? '',
|
||||
'supports_hot_reload' => !empty($current['supports_hot_reload']),
|
||||
'activation_template' => $current['activation_template'] ?? '',
|
||||
'notes' => $current['notes'] ?? '',
|
||||
'exists' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($overrides !== null) {
|
||||
$defaults = array_merge($defaults, $overrides);
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover available game keys from server config XMLs.
|
||||
*
|
||||
|
|
@ -454,6 +584,73 @@ class SteamWorkshopService
|
|||
return $config;
|
||||
}
|
||||
|
||||
private function getGameAdapterPath(string $gameKey): string
|
||||
{
|
||||
return sprintf('%s/%s.xml', $this->gameAdapterDir, $gameKey);
|
||||
}
|
||||
|
||||
private function sanitizeGameKey(string $gameKey): string
|
||||
{
|
||||
$gameKey = strtolower(trim($gameKey));
|
||||
return preg_replace('/[^a-z0-9_\-.]/', '', $gameKey);
|
||||
}
|
||||
|
||||
private function normalizeAdapterData(string $gameKey, array $data): array
|
||||
{
|
||||
$name = trim((string)($data['name'] ?? ''));
|
||||
return [
|
||||
'name' => $name !== '' ? $name : $gameKey,
|
||||
'steam_app_id' => trim((string)($data['steam_app_id'] ?? '')),
|
||||
'mods_dir' => trim((string)($data['mods_dir'] ?? '')),
|
||||
'keys_dir' => trim((string)($data['keys_dir'] ?? '')),
|
||||
'supports_hot_reload' => !empty($data['supports_hot_reload']),
|
||||
'activation_template' => trim((string)($data['activation_template'] ?? '')),
|
||||
'notes' => trim((string)($data['notes'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
private function parseAdapterFile(string $file, string $schemaPath, bool $useSchema, ?string $forcedKey = null): ?array
|
||||
{
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
if (!$doc->load($file)) {
|
||||
libxml_use_internal_errors($previous);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($useSchema && is_file($schemaPath) && !$doc->schemaValidate($schemaPath)) {
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
return null;
|
||||
}
|
||||
|
||||
$adapter = simplexml_import_dom($doc);
|
||||
if ($adapter === false) {
|
||||
libxml_use_internal_errors($previous);
|
||||
return null;
|
||||
}
|
||||
|
||||
$key = $forcedKey ?? (string)($adapter['key'] ?? '');
|
||||
if ($key === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = [
|
||||
'key' => $key,
|
||||
'name' => (string)($adapter['name'] ?? $key),
|
||||
'steam_app_id' => (string)($adapter->steamAppId ?? ''),
|
||||
'mods_dir' => (string)($adapter->modsDir ?? ''),
|
||||
'keys_dir' => isset($adapter->keysDir) ? (string)$adapter->keysDir : '',
|
||||
'supports_hot_reload' => filter_var((string)($adapter->supportsHotReload ?? 'false'), FILTER_VALIDATE_BOOLEAN),
|
||||
'activation_template' => (string)($adapter->activation->template ?? ''),
|
||||
'notes' => (string)($adapter->notes ?? ''),
|
||||
];
|
||||
|
||||
libxml_use_internal_errors($previous);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getConfigPath(int $homeId): string
|
||||
{
|
||||
return sprintf('%s/%d.xml', $this->configDir, $homeId);
|
||||
|
|
|
|||
|
|
@ -5,16 +5,20 @@ declare(strict_types=1);
|
|||
/** @var array $mappings */
|
||||
/** @var array $adapterOptions */
|
||||
/** @var array $adapters */
|
||||
/** @var array $gameRows */
|
||||
/** @var array|null $adapterForm */
|
||||
/** @var string $activeGameKey */
|
||||
?>
|
||||
<div class="sw-admin">
|
||||
<h3><?php echo htmlspecialchars($lang['admin_heading_game_mapping'] ?? 'Game type adapter mapping'); ?></h3>
|
||||
<p><?php echo htmlspecialchars($lang['admin_subheading_game_mapping'] ?? 'Select which adapter will manage Steam Workshop installs for each supported game.'); ?></p>
|
||||
|
||||
<form method="post" class="sw-form">
|
||||
<input type="hidden" name="admin_action" value="save_mappings">
|
||||
<table class="table sw-mods__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php echo htmlspecialchars($lang['admin_col_game_key'] ?? 'Game Key'); ?></th>
|
||||
<th><?php echo htmlspecialchars($lang['admin_col_game_key'] ?? 'Game key'); ?></th>
|
||||
<th><?php echo htmlspecialchars($lang['admin_col_adapter'] ?? 'Adapter'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -47,6 +51,121 @@ declare(strict_types=1);
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<h3><?php echo htmlspecialchars($lang['admin_heading_per_game'] ?? 'Per-game adapters'); ?></h3>
|
||||
<p><?php echo htmlspecialchars($lang['admin_subheading_per_game'] ?? 'Each game key gets its own adapter XML. Create, edit, or delete them below.'); ?></p>
|
||||
|
||||
<table class="table sw-mods__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php echo htmlspecialchars($lang['admin_col_game_key'] ?? 'Game key'); ?></th>
|
||||
<th><?php echo htmlspecialchars($lang['admin_col_status'] ?? 'Status'); ?></th>
|
||||
<th><?php echo htmlspecialchars($lang['admin_col_updated'] ?? 'Last updated'); ?></th>
|
||||
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($gameRows)): ?>
|
||||
<tr>
|
||||
<td colspan="4"><?php echo htmlspecialchars($lang['admin_no_game_keys'] ?? 'No game definitions were found in modules/config_games/server_configs.'); ?></td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($gameRows as $row): ?>
|
||||
<?php $exists = !empty($row['exists']); ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($row['game_key']); ?></td>
|
||||
<td>
|
||||
<?php if ($exists): ?>
|
||||
<?php echo htmlspecialchars($row['adapter']['name'] ?? $row['game_key']); ?>
|
||||
<?php else: ?>
|
||||
<?php echo htmlspecialchars($lang['status_no_adapter'] ?? 'No adapter'); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($exists && !empty($row['updated_at'])): ?>
|
||||
<?php echo htmlspecialchars(date('Y-m-d H:i', (int)$row['updated_at'])); ?>
|
||||
<?php else: ?>
|
||||
—
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="sw-actions">
|
||||
<a class="btn secondary" href="?m=steam_workshop&p=workshop_admin&adapter_game=<?php echo urlencode($row['game_key']); ?>#adapter-form">
|
||||
<?php echo htmlspecialchars($exists ? ($lang['button_edit_adapter'] ?? 'Edit') : ($lang['button_create_adapter'] ?? 'Create')); ?>
|
||||
</a>
|
||||
<?php if ($exists): ?>
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="admin_action" value="delete_adapter">
|
||||
<input type="hidden" name="game_key" value="<?php echo htmlspecialchars($row['game_key']); ?>">
|
||||
<button type="submit" class="btn danger" onclick="return confirm('<?php echo htmlspecialchars($lang['confirm_delete_adapter'] ?? 'Delete this adapter?'); ?>');">
|
||||
<?php echo htmlspecialchars($lang['button_delete_adapter'] ?? 'Delete'); ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="adapter-form" class="sw-adapter-form">
|
||||
<?php if ($adapterForm): ?>
|
||||
<h3><?php echo htmlspecialchars(sprintf($lang['admin_heading_edit_adapter'] ?? 'Editing adapter for %s', $adapterForm['game_key'])); ?></h3>
|
||||
<form method="post" class="sw-form">
|
||||
<input type="hidden" name="admin_action" value="save_adapter">
|
||||
<input type="hidden" name="game_key" value="<?php echo htmlspecialchars($adapterForm['game_key']); ?>">
|
||||
|
||||
<div class="sw-form__row">
|
||||
<label><?php echo htmlspecialchars($lang['label_game_key'] ?? 'Game key'); ?></label>
|
||||
<input type="text" value="<?php echo htmlspecialchars($adapterForm['game_key']); ?>" readonly>
|
||||
</div>
|
||||
|
||||
<div class="sw-form__row">
|
||||
<label><?php echo htmlspecialchars($lang['label_adapter_name'] ?? 'Adapter display name'); ?></label>
|
||||
<input type="text" name="adapter[name]" value="<?php echo htmlspecialchars($adapterForm['name']); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="sw-form__row">
|
||||
<label><?php echo htmlspecialchars($lang['label_adapter_app_id'] ?? 'Steam App ID'); ?></label>
|
||||
<input type="text" name="adapter[steam_app_id]" value="<?php echo htmlspecialchars($adapterForm['steam_app_id']); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="sw-form__row">
|
||||
<label><?php echo htmlspecialchars($lang['label_adapter_mods_dir'] ?? 'Mods directory'); ?></label>
|
||||
<input type="text" name="adapter[mods_dir]" value="<?php echo htmlspecialchars($adapterForm['mods_dir']); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="sw-form__row">
|
||||
<label><?php echo htmlspecialchars($lang['label_adapter_keys_dir'] ?? 'Keys directory (optional)'); ?></label>
|
||||
<input type="text" name="adapter[keys_dir]" value="<?php echo htmlspecialchars($adapterForm['keys_dir']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="sw-form__row">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="adapter[supports_hot_reload]" value="1" <?php echo !empty($adapterForm['supports_hot_reload']) ? 'checked' : ''; ?> >
|
||||
<span><?php echo htmlspecialchars($lang['label_adapter_hot_reload'] ?? 'Supports hot reload'); ?></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="sw-form__row">
|
||||
<label><?php echo htmlspecialchars($lang['label_adapter_activation'] ?? 'Activation template'); ?></label>
|
||||
<textarea name="adapter[activation_template]" rows="4"><?php echo htmlspecialchars($adapterForm['activation_template']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="sw-form__row">
|
||||
<label><?php echo htmlspecialchars($lang['label_adapter_notes'] ?? 'Notes'); ?></label>
|
||||
<textarea name="adapter[notes]" rows="3"><?php echo htmlspecialchars($adapterForm['notes']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="sw-form__actions">
|
||||
<button class="btn primary" type="submit"><?php echo htmlspecialchars($lang['button_save_adapter'] ?? 'Save adapter'); ?></button>
|
||||
<a class="btn" href="?m=steam_workshop&p=workshop_admin"><?php echo htmlspecialchars($lang['button_cancel'] ?? 'Cancel'); ?></a>
|
||||
</div>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<p><?php echo htmlspecialchars($lang['admin_hint_select_game'] ?? 'Select a game above to start editing its adapter.'); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<h3><?php echo htmlspecialchars($lang['admin_heading_adapters'] ?? 'Available adapters'); ?></h3>
|
||||
<table class="table sw-mods__table">
|
||||
<thead>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue