updated workshop

This commit is contained in:
Frank Harris 2026-01-17 09:59:01 -06:00
parent 0885bfef92
commit 8857f441e7

View file

@ -32,8 +32,6 @@ class SteamWorkshopService
} }
/** /**
* Fetch all homes visible to the given user.
*
* @return array<int,array<string,mixed>> * @return array<int,array<string,mixed>>
*/ */
public function listHomesForUser(int $userId, bool $isAdmin): array public function listHomesForUser(int $userId, bool $isAdmin): array
@ -48,9 +46,6 @@ class SteamWorkshopService
return array_values($homes); return array_values($homes);
} }
/**
* Retrieve a single home, ensuring the user is allowed to see it.
*/
public function getHome(int $homeId, int $userId, bool $isAdmin): ?array public function getHome(int $homeId, int $userId, bool $isAdmin): ?array
{ {
$home = $isAdmin $home = $isAdmin
@ -60,24 +55,9 @@ class SteamWorkshopService
return is_array($home) ? $home : null; return is_array($home) ? $home : null;
} }
/**
* @return array{
* workshop_enabled: bool,
* adapter_key: string,
* update_interval_minutes: int,
* staging_dir: string,
* install_strategy: string,
* on_update_action: string,
* post_install_script: string,
* workshop_items: array<int, array<string, mixed>>,
* raw_definition: string,
* last_saved_at: int|null
* }
*/
public function loadConfig(int $homeId): array public function loadConfig(int $homeId): array
{ {
$path = $this->getConfigPath($homeId); $path = $this->getConfigPath($homeId);
if (!is_file($path)) { if (!is_file($path)) {
return $this->defaultConfig(); return $this->defaultConfig();
} }
@ -96,9 +76,7 @@ class SteamWorkshopService
$config['on_update_action'] = (string)($xml->onUpdateAction ?? $config['on_update_action']); $config['on_update_action'] = (string)($xml->onUpdateAction ?? $config['on_update_action']);
$config['post_install_script'] = trim((string)($xml->postInstallScript ?? '')); $config['post_install_script'] = trim((string)($xml->postInstallScript ?? ''));
$config['raw_definition'] = (string)($xml->rawDefinition ?? ''); $config['raw_definition'] = (string)($xml->rawDefinition ?? '');
$config['last_saved_at'] = isset($xml->timestamps->savedAt) $config['last_saved_at'] = isset($xml->timestamps->savedAt) ? (int)$xml->timestamps->savedAt : null;
? (int)$xml->timestamps->savedAt
: null;
$mods = []; $mods = [];
if (isset($xml->mods)) { if (isset($xml->mods)) {
@ -121,6 +99,7 @@ class SteamWorkshopService
{ {
$path = $this->getConfigPath($homeId); $path = $this->getConfigPath($homeId);
$config = $this->normalizeConfig($config); $config = $this->normalizeConfig($config);
$doc = new DOMDocument('1.0', 'UTF-8'); $doc = new DOMDocument('1.0', 'UTF-8');
$doc->formatOutput = true; $doc->formatOutput = true;
@ -157,62 +136,48 @@ class SteamWorkshopService
$doc->save($path); $doc->save($path);
} }
if ($gameKey === '') {
throw new RuntimeException('Game key is required.'); public function buildConfigFromRequest(array $payload): array
{
$input = $payload['workshop'] ?? [];
$rawMods = trim((string)($input['raw_items'] ?? ''));
$items = $this->parseWorkshopItems($rawMods);
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,
];
} }
$normalized = $this->normalizeAdapterData($gameKey, $data); public function parseWorkshopItems(string $raw): array
if ($normalized['steam_app_id'] === '') { {
throw new RuntimeException('Steam App ID is required.'); if ($raw === '') {
} return [];
if ($normalized['mods_dir'] === '') {
throw new RuntimeException('Mods directory is required.');
} }
$doc = new DOMDocument('1.0', 'UTF-8'); $items = [];
$doc->formatOutput = true;
$root = $doc->createElement('adapter');
$root->setAttribute('key', $gameKey);
$root->setAttribute('name', $normalized['name']);
$doc->appendChild($root);
$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); $lines = preg_split('/\r\n|\r|\n/', $raw);
foreach ($lines as $line) { foreach ($lines as $line) {
$line = trim($line); $line = trim($line);
if ($line === '') { if ($line === '') {
continue; continue;
if ($gameKey === '') {
return false;
} }
$path = $this->getGameAdapterPath($gameKey); $parts = array_map('trim', explode(',', $line, 2));
if (!is_file($path)) { $id = preg_replace('/[^0-9]/', '', $parts[0]);
return false; if ($id === '') {
continue;
} }
$label = $parts[1] ?? '';
return unlink($path); if ($label === '') {
$label = '@' . $id;
} }
$items[] = [ $items[] = [
@ -226,21 +191,13 @@ class SteamWorkshopService
return $items; return $items;
} }
/**
* Build a SteamCMD command array for a single workshop item.
*/
public function buildSteamCmdArgs(array $config, string $workshopId, ?string $login = null): array public function buildSteamCmdArgs(array $config, string $workshopId, ?string $login = null): array
{ {
$loginUser = $login !== null && $login !== '' ? $login : 'anonymous'; $loginUser = $login !== null && $login !== '' ? $login : 'anonymous';
$adapter = $this->getAdapterByKey($config['adapter_key'] ?? ''); $adapter = $this->getAdapterByKey($config['adapter_key'] ?? '');
$appId = $adapter['steam_app_id'] ?? ($config['steam_app_id'] ?? ''); $appId = $adapter['steam_app_id'] ?? ($config['steam_app_id'] ?? '');
return [ return ['+login', $loginUser, '+workshop_download_item', $appId, $workshopId, 'validate'];
'+login', $loginUser,
'+workshop_download_item', $appId,
$workshopId,
'validate',
];
} }
public function getAdapterOptions(): array public function getAdapterOptions(): array
@ -257,17 +214,11 @@ class SteamWorkshopService
return $options; return $options;
} }
/**
* Load adapter metadata for UI and validation.
*
* @return array<int, array<string, mixed>>
*/
public function loadAdapters(): array public function loadAdapters(): array
{ {
$result = []; $result = [];
$schema = $this->adapterDir . '/schema.xsd'; $schema = $this->adapterDir . '/schema.xsd';
$useSchema = is_file($schema); $useSchema = is_file($schema);
$previousLibxml = libxml_use_internal_errors(true);
foreach (glob($this->adapterDir . '/*.xml') as $file) { foreach (glob($this->adapterDir . '/*.xml') as $file) {
if (substr($file, -4) !== '.xml' || basename($file) === 'schema.xsd') { if (substr($file, -4) !== '.xml' || basename($file) === 'schema.xsd') {
@ -294,8 +245,6 @@ class SteamWorkshopService
} }
} }
libxml_use_internal_errors($previousLibxml);
return array_values(array_filter($result, static function (array $adapter): bool { return array_values(array_filter($result, static function (array $adapter): bool {
return $adapter['key'] !== ''; return $adapter['key'] !== '';
})); }));
@ -312,9 +261,6 @@ class SteamWorkshopService
return []; return [];
} }
/**
* Return adapter key chosen for the given game key, or null if unmapped.
*/
public function getAdapterKeyForGame(string $gameKey): ?string public function getAdapterKeyForGame(string $gameKey): ?string
{ {
$gameKey = trim($gameKey); $gameKey = trim($gameKey);
@ -334,9 +280,6 @@ class SteamWorkshopService
return null; return null;
} }
/**
* Persist adapter mappings (game_key => adapter_key).
*/
public function saveAdapterMappings(array $mappings): void public function saveAdapterMappings(array $mappings): void
{ {
$sanitized = []; $sanitized = [];
@ -380,9 +323,6 @@ class SteamWorkshopService
} }
} }
/**
* @return array<string,string>
*/
public function getAdapterMappings(): array public function getAdapterMappings(): array
{ {
if (!is_file($this->adapterMapFile)) { if (!is_file($this->adapterMapFile)) {
@ -406,11 +346,6 @@ class SteamWorkshopService
return $result; return $result;
} }
/**
* Return metadata for every custom adapter stored on disk.
*
* @return array<int,array<string,mixed>>
*/
public function listGameAdapters(): array public function listGameAdapters(): array
{ {
$adapters = []; $adapters = [];
@ -501,11 +436,67 @@ class SteamWorkshopService
return $defaults; return $defaults;
} }
/** public function saveGameAdapter(string $gameKey, array $data): void
* Discover available game keys from server config XMLs. {
* $gameKey = $this->sanitizeGameKey($gameKey);
* @return array<int,string> if ($gameKey === '') {
*/ throw new RuntimeException('Game key is required.');
}
$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.');
}
$doc = new DOMDocument('1.0', 'UTF-8');
$doc->formatOutput = true;
$root = $doc->createElement('adapter');
$root->setAttribute('key', $gameKey);
$root->setAttribute('name', $normalized['name']);
$doc->appendChild($root);
$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);
}
public function deleteGameAdapter(string $gameKey): bool
{
$gameKey = $this->sanitizeGameKey($gameKey);
if ($gameKey === '') {
return false;
}
$path = $this->getGameAdapterPath($gameKey);
if (!is_file($path)) {
return false;
}
return unlink($path);
}
public function listAvailableGameKeys(): array public function listAvailableGameKeys(): array
{ {
$keys = []; $keys = [];
@ -592,7 +583,8 @@ class SteamWorkshopService
private function sanitizeGameKey(string $gameKey): string private function sanitizeGameKey(string $gameKey): string
{ {
$gameKey = strtolower(trim($gameKey)); $gameKey = strtolower(trim($gameKey));
return preg_replace('/[^a-z0-9_\-.]/', '', $gameKey); $sanitized = preg_replace('/[^a-z0-9_\-.]/', '', $gameKey);
return is_string($sanitized) ? $sanitized : '';
} }
private function normalizeAdapterData(string $gameKey, array $data): array private function normalizeAdapterData(string $gameKey, array $data): array
@ -632,6 +624,7 @@ class SteamWorkshopService
$key = $forcedKey ?? (string)($adapter['key'] ?? ''); $key = $forcedKey ?? (string)($adapter['key'] ?? '');
if ($key === '') { if ($key === '') {
libxml_use_internal_errors($previous);
return null; return null;
} }