db = $db; $this->configDir = __DIR__ . '/../data/configs'; $this->adapterDir = __DIR__ . '/GameAdapters'; $this->adapterMapFile = __DIR__ . '/../data/game_adapter_map.json'; $this->gameAdapterDir = __DIR__ . '/../data/game_adapters'; $this->serverConfigDir = defined('SERVER_CONFIG_LOCATION') ? SERVER_CONFIG_LOCATION : __DIR__ . '/../../config_games/server_configs'; if (!is_dir($this->configDir)) { mkdir($this->configDir, 0775, true); } if (!is_dir($this->gameAdapterDir)) { mkdir($this->gameAdapterDir, 0775, true); } $this->ensureDataFiles(); } /** * @return array> */ public function listHomesForUser(int $userId, bool $isAdmin): array { $accessType = $isAdmin ? 'admin' : 'user_and_group'; $homes = $this->db->getHomesFor($accessType, $userId); if ($homes === false || $homes === null) { return []; } return array_values($homes); } public function getHome(int $homeId, int $userId, bool $isAdmin): ?array { $home = $isAdmin ? $this->db->getGameHome($homeId) : $this->db->getUserGameHome($userId, $homeId); return is_array($home) ? $home : null; } public function loadConfig(int $homeId): array { $path = $this->getConfigPath($homeId); if (!is_file($path)) { return $this->defaultConfig(); } $xml = @simplexml_load_file($path); if ($xml === false) { return $this->defaultConfig(); } $config = $this->defaultConfig(); $config['workshop_enabled'] = ((string)($xml->enabled ?? 'false')) === 'true'; $config['adapter_key'] = (string)($xml->adapter['key'] ?? $config['adapter_key']); $config['update_interval_minutes'] = $this->sanitizeInterval((int)($xml->updateInterval ?? $config['update_interval_minutes'])); $config['staging_dir'] = trim((string)($xml->stagingDir ?? '')); $config['install_strategy'] = (string)($xml->installStrategy ?? $config['install_strategy']); $config['on_update_action'] = (string)($xml->onUpdateAction ?? $config['on_update_action']); $config['post_install_script'] = trim((string)($xml->postInstallScript ?? '')); $config['raw_definition'] = (string)($xml->rawDefinition ?? ''); $config['last_saved_at'] = isset($xml->timestamps->savedAt) ? (int)$xml->timestamps->savedAt : null; $mods = []; if (isset($xml->mods)) { foreach ($xml->mods->mod as $mod) { $mods[] = [ 'id' => (string)$mod['id'], 'label' => (string)$mod['label'], 'enabled' => ((string)$mod['enabled']) !== 'false', 'source' => (string)($mod['source'] ?? 'manual'), ]; } } $config['workshop_items'] = $mods; return $config; } public function saveConfig(int $homeId, array $config): void { $path = $this->getConfigPath($homeId); $config = $this->normalizeConfig($config); $doc = new DOMDocument('1.0', 'UTF-8'); $doc->formatOutput = true; $root = $doc->createElement('workshop'); $doc->appendChild($root); $root->appendChild($doc->createElement('enabled', $config['workshop_enabled'] ? 'true' : 'false')); $adapterNode = $doc->createElement('adapter'); $adapterNode->setAttribute('key', $config['adapter_key']); $root->appendChild($adapterNode); $root->appendChild($doc->createElement('updateInterval', (string)$config['update_interval_minutes'])); $root->appendChild($doc->createElement('stagingDir', $config['staging_dir'])); $root->appendChild($doc->createElement('installStrategy', $config['install_strategy'])); $root->appendChild($doc->createElement('onUpdateAction', $config['on_update_action'])); $root->appendChild($doc->createElement('postInstallScript', $config['post_install_script'])); $root->appendChild($doc->createElement('rawDefinition', $config['raw_definition'])); $modsNode = $doc->createElement('mods'); foreach ($config['workshop_items'] as $item) { $mod = $doc->createElement('mod'); $mod->setAttribute('id', (string)$item['id']); $mod->setAttribute('label', (string)$item['label']); $mod->setAttribute('enabled', !empty($item['enabled']) ? 'true' : 'false'); $mod->setAttribute('source', (string)($item['source'] ?? 'manual')); $modsNode->appendChild($mod); } $root->appendChild($modsNode); $timestampsNode = $doc->createElement('timestamps'); $timestampsNode->appendChild($doc->createElement('savedAt', (string)time())); $root->appendChild($timestampsNode); $doc->save($path); } public function buildConfigFromRequest(array $payload): array { $input = $payload['workshop'] ?? []; $rawMods = trim((string)($input['raw_items'] ?? '')); $selectedItems = $this->parseSelectedItemsJson((string)($input['selected_items'] ?? '')); if (!empty($selectedItems)) { $items = $selectedItems; $rawMods = $this->serializeWorkshopItems($selectedItems); } else { $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, ]; } public function parseWorkshopItems(string $raw): array { if ($raw === '') { return []; } $items = []; $lines = preg_split('/\r\n|\r|\n/', $raw); foreach ($lines as $line) { $line = trim($line); if ($line === '') { continue; } $parts = array_map('trim', explode(',', $line, 2)); $id = preg_replace('/[^0-9]/', '', $parts[0]); if ($id === '') { continue; } $label = $parts[1] ?? ''; if ($label === '') { $label = '@' . $id; } $items[] = [ 'id' => $id, 'label' => $label, 'enabled' => true, 'source' => 'manual', ]; } return $items; } public function buildSteamCmdArgs(array $config, string $workshopId, ?string $login = null): array { $loginUser = $login !== null && $login !== '' ? $login : 'anonymous'; $adapter = $this->getAdapterByKey($config['adapter_key'] ?? ''); $appId = $adapter['steam_app_id'] ?? ($config['steam_app_id'] ?? ''); return ['+login', $loginUser, '+workshop_download_item', $appId, $workshopId, 'validate']; } public function getAdapterOptions(): array { $options = []; foreach ($this->loadAdapters() as $adapter) { $options[$adapter['key']] = $adapter['name']; } if (empty($options)) { $options['dayz'] = 'DayZ (fallback)'; } return $options; } public function loadAdapters(): array { $result = []; $schema = $this->adapterDir . '/schema.xsd'; $useSchema = is_file($schema); 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; } $gameKey = basename($file, '.xml'); $parsed = $this->parseAdapterFile($file, $schema, $useSchema, $gameKey); if ($parsed !== null) { $parsed['origin'] = 'custom'; $result[] = $parsed; } } return array_values(array_filter($result, static function (array $adapter): bool { return $adapter['key'] !== ''; })); } public function getAdapterByKey(string $key): array { foreach ($this->loadAdapters() as $adapter) { if ($adapter['key'] === $key) { return $adapter; } } return []; } public function getAdapterKeyForGame(string $gameKey): ?string { $gameKey = trim($gameKey); if ($gameKey === '') { return null; } $map = $this->getAdapterMappings(); if (isset($map[$gameKey])) { return $map[$gameKey]; } if ($this->gameAdapterExists($gameKey)) { return $gameKey; } return null; } public function saveAdapterMappings(array $mappings): void { $sanitized = []; $options = $this->getAdapterOptions(); foreach ($mappings as $gameKey => $adapterKey) { $gameKey = trim((string)$gameKey); $adapterKey = $this->sanitizeAdapterKey((string)$adapterKey); if ($gameKey === '' || !isset($options[$adapterKey])) { continue; } $sanitized[$gameKey] = $adapterKey; } 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)); } } public function getAdapterMappings(): array { if (!is_file($this->adapterMapFile)) { return []; } $raw = file_get_contents($this->adapterMapFile); $decoded = json_decode((string)$raw, true); if (!is_array($decoded)) { return []; } $result = []; foreach ($decoded as $gameKey => $adapterKey) { if (!is_string($gameKey) || !is_string($adapterKey)) { continue; } $result[$gameKey] = $adapterKey; } return $result; } 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; } public function saveGameAdapter(string $gameKey, array $data): void { $gameKey = $this->sanitizeGameKey($gameKey); 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 listWorkshopGameGroups(): array { $configDir = $this->serverConfigDir; if (!is_dir($configDir)) { return []; } $groups = []; foreach (glob($configDir . '/*.xml') as $file) { $xml = @simplexml_load_file($file); if ($xml === false) { continue; } $installer = isset($xml->installer) ? trim((string)$xml->installer) : ''; if ($installer !== 'steamcmd') { continue; } $gameKey = isset($xml->game_key) ? trim((string)$xml->game_key) : ''; if ($gameKey === '') { continue; } $appId = $this->parseSteamAppIdFromConfig($xml); if ($appId === null) { continue; } $groupKey = $this->buildWorkshopGroupKey($appId); if (!isset($groups[$groupKey])) { $gameName = isset($xml->game_name) ? trim((string)$xml->game_name) : ''; $groups[$groupKey] = [ 'group_key' => $groupKey, 'app_id' => $appId, 'game_name' => $gameName !== '' ? $gameName : $gameKey, 'game_keys' => [], ]; } $groups[$groupKey]['game_keys'][] = $gameKey; } foreach ($groups as &$group) { $group['game_keys'] = array_values(array_unique($group['game_keys'])); sort($group['game_keys']); $group['primary_game_key'] = $group['game_keys'][0]; } unset($group); usort($groups, static function (array $a, array $b): int { return strcmp($a['game_name'], $b['game_name']); }); return array_values($groups); } public function listAvailableGameKeys(): array { $keys = []; foreach ($this->listWorkshopGameGroups() as $group) { $keys = array_merge($keys, $group['game_keys']); } return array_values(array_unique($keys)); } public function getSteamAppIdForGameKey(string $gameKey): ?string { $xml = $this->loadServerConfigXml($gameKey); if ($xml === null) { return null; } return $this->parseSteamAppIdFromConfig($xml); } public function searchWorkshopItems(string $gameKey, string $query, int $limit = 12): array { $query = trim($query); if ($query === '') { return []; } $appId = $this->getSteamAppIdForGameKey($gameKey); if ($appId === null) { return []; } if (ctype_digit($query)) { $item = $this->fetchWorkshopItemById($query); return $item === null ? [] : [$item]; } $html = $this->fetchWorkshopSearchHtml($appId, $query, $limit * 2); if ($html === null) { return []; } return $this->extractWorkshopItemsFromHtml($html, $limit); } private function sanitizeInterval(?int $minutes): int { if ($minutes === null || $minutes <= 0) { $minutes = 60; } return max(self::MIN_INTERVAL, min(self::MAX_INTERVAL, $minutes)); } private function sanitizeAdapterKey(string $key): string { $key = strtolower(trim($key)); if ($key === '') { return 'dayz'; } $adapters = $this->getAdapterOptions(); if (array_key_exists($key, $adapters)) { return $key; } $adapterKeys = array_keys($adapters); return $adapterKeys[0] ?? 'dayz'; } private function sanitizeInstallStrategy(string $strategy): string { $valid = ['copy', 'symlink', 'staging']; return in_array($strategy, $valid, true) ? $strategy : 'copy'; } private function sanitizeUpdateAction(string $action): string { $valid = ['queue_for_restart', 'hot_reload_if_supported']; return in_array($action, $valid, true) ? $action : 'queue_for_restart'; } private function normalizeConfig(array $config): array { $config = array_merge($this->defaultConfig(), $config); $config['update_interval_minutes'] = $this->sanitizeInterval((int)$config['update_interval_minutes']); $config['adapter_key'] = $this->sanitizeAdapterKey((string)$config['adapter_key']); $config['install_strategy'] = $this->sanitizeInstallStrategy((string)$config['install_strategy']); $config['on_update_action'] = $this->sanitizeUpdateAction((string)$config['on_update_action']); $config['workshop_items'] = array_map(static function (array $item): array { $item['id'] = preg_replace('/[^0-9]/', '', (string)($item['id'] ?? '')); $item['label'] = trim((string)($item['label'] ?? '')); $item['enabled'] = !empty($item['enabled']); $item['source'] = $item['source'] ?? 'manual'; $item['author'] = trim((string)($item['author'] ?? '')); $item['preview_url'] = trim((string)($item['preview_url'] ?? '')); return $item; }, $config['workshop_items']); $config['workshop_items'] = array_values(array_filter($config['workshop_items'], static function (array $item): bool { return $item['id'] !== ''; })); 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)); $sanitized = preg_replace('/[^a-z0-9_\-.]/', '', $gameKey); return is_string($sanitized) ? $sanitized : ''; } 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 === '') { libxml_use_internal_errors($previous); 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); } private function defaultConfig(): array { return [ 'workshop_enabled' => false, 'adapter_key' => 'dayz', 'update_interval_minutes' => 60, 'staging_dir' => '', 'install_strategy' => 'copy', 'on_update_action' => 'queue_for_restart', 'post_install_script' => '', 'workshop_items' => [], 'raw_definition' => '', 'last_saved_at' => null, ]; } private function ensureDataFiles(): void { $directories = [ $this->configDir, $this->gameAdapterDir, dirname($this->adapterMapFile), ]; foreach ($directories as $dir) { if (!is_dir($dir)) { mkdir($dir, 0775, true); } } if (!is_file($this->adapterMapFile)) { file_put_contents($this->adapterMapFile, json_encode([], JSON_PRETTY_PRINT)); } } public function gameSupportsWorkshop($serverXml): bool { if (!($serverXml instanceof SimpleXMLElement)) { return false; } $installer = trim((string)($serverXml->installer ?? '')); if ($installer !== 'steamcmd') { return false; } $appId = $this->parseSteamAppIdFromConfig($serverXml); return $appId !== null; } private function parseSteamAppIdFromConfig($xml): ?string { if (!isset($xml->mods) || !isset($xml->mods->mod)) { return null; } $candidate = null; foreach ($xml->mods->mod as $mod) { $installerName = trim((string)($mod->installer_name ?? '')); if ($installerName === '' || preg_match('/\D/', $installerName)) { continue; } $modName = strtolower(trim((string)($mod->name ?? ''))); $modKey = strtolower(trim((string)($mod['key'] ?? ''))); if ($modKey === 'default' || $modName === 'none' || $modName === '') { return $installerName; } if ($candidate === null) { $candidate = $installerName; } } return $candidate; } private function loadServerConfigXml(string $gameKey): ?SimpleXMLElement { $gameKey = $this->sanitizeGameKey($gameKey); if ($gameKey === '') { return null; } $directPath = sprintf('%s/%s.xml', $this->serverConfigDir, $gameKey); if (is_file($directPath)) { $xml = @simplexml_load_file($directPath); if ($xml !== false) { return $xml; } } foreach (glob($this->serverConfigDir . '/*.xml') as $file) { $xml = @simplexml_load_file($file); if ($xml === false) { continue; } $configuredKey = isset($xml->game_key) ? $this->sanitizeGameKey((string)$xml->game_key) : ''; if ($configuredKey === $gameKey) { return $xml; } } return null; } private function parseSelectedItemsJson(string $json): array { if ($json === '') { return []; } $decoded = json_decode($json, true); if (!is_array($decoded)) { return []; } $result = []; foreach ($decoded as $item) { if (!is_array($item)) { continue; } $id = preg_replace('/[^0-9]/', '', (string)($item['id'] ?? '')); if ($id === '') { continue; } $label = trim((string)($item['label'] ?? '')); if ($label === '') { $label = '@' . $id; } $result[$id] = [ 'id' => $id, 'label' => $label, 'author' => trim((string)($item['author'] ?? '')), 'preview_url' => trim((string)($item['preview_url'] ?? '')), 'enabled' => isset($item['enabled']) ? (bool)$item['enabled'] : true, 'source' => trim((string)($item['source'] ?? 'search')), ]; } return array_values($result); } private function serializeWorkshopItems(array $items): string { $lines = []; foreach ($items as $item) { $id = preg_replace('/[^0-9]/', '', (string)($item['id'] ?? '')); if ($id === '') { continue; } $label = trim((string)($item['label'] ?? '')); if ($label === '') { $label = '@' . $id; } $lines[] = $id . ',' . $label; } return implode(PHP_EOL, $lines); } private function fetchWorkshopSearchHtml(string $appId, string $query, int $pageSize): ?string { $params = http_build_query([ 'appid' => $appId, 'searchtext' => $query, 'numperpage' => max(5, $pageSize), 'format' => 'json', 'browsesort' => 'textsearch', ]); $url = 'https://steamcommunity.com/workshop/browse/?' . $params; $response = $this->httpRequest($url); if ($response === null) { return null; } $data = json_decode($response, true); if (is_array($data) && isset($data['html'])) { return (string)$data['html']; } return $response; } private function extractWorkshopItemsFromHtml(string $html, int $limit): array { libxml_use_internal_errors(true); $doc = new DOMDocument(); $doc->loadHTML('' . $html); libxml_clear_errors(); $xpath = new DOMXPath($doc); $nodes = $xpath->query('//*[@data-publishedfileid]'); $results = []; foreach ($nodes as $node) { if (!($node instanceof DOMElement)) { continue; } $id = $node->getAttribute('data-publishedfileid'); if ($id === '') { continue; } $titleNode = $xpath->query('.//*[contains(@class,"workshopItemTitle")]', $node)->item(0); $authorNode = $xpath->query('.//*[contains(@class,"workshopItemAuthorName")]', $node)->item(0); $imgNode = $xpath->query('.//img[contains(@class,"workshopItemPreviewImage") or contains(@class,"workshopItemPreviewImageMain")]', $node)->item(0); $results[$id] = [ 'id' => $id, 'label' => trim($titleNode instanceof DOMNode ? $titleNode->textContent : ('@' . $id)), 'author' => trim($authorNode instanceof DOMNode ? $authorNode->textContent : ''), 'preview_url' => $imgNode instanceof DOMElement ? (string)$imgNode->getAttribute('src') : '', 'enabled' => true, 'source' => 'search', ]; if (count($results) >= $limit) { break; } } return array_values($results); } private function fetchWorkshopItemById(string $id): ?array { $postData = http_build_query([ 'itemcount' => 1, 'publishedfileids[0]' => $id, ]); $response = $this->httpRequest('https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/', $postData); if ($response === null) { return null; } $data = json_decode($response, true); $details = $data['response']['publishedfiledetails'][0] ?? null; if (!is_array($details) || (int)($details['result'] ?? 0) !== 1) { return null; } $title = trim((string)($details['title'] ?? '')); if ($title === '') { $title = '@' . $id; } return [ 'id' => $id, 'label' => $title, 'author' => (string)($details['creator'] ?? ''), 'preview_url' => (string)($details['preview_url'] ?? ''), 'enabled' => true, 'source' => 'search', ]; } private function httpRequest(string $url, ?string $postFields = null): ?string { if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_USERAGENT, 'GSP-Workshop/1.0 (+https://github.com/GameServerPanel/GSP)'); if ($postFields !== null) { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); } $response = curl_exec($ch); curl_close($ch); return is_string($response) ? $response : null; } $contextOptions = [ 'http' => [ 'method' => $postFields !== null ? 'POST' : 'GET', 'timeout' => 10, 'header' => "User-Agent: GSP-Workshop/1.0 (+https://github.com/GameServerPanel/GSP)\r\n" . ($postFields !== null ? "Content-Type: application/x-www-form-urlencoded\r\n" : ''), ], ]; if ($postFields !== null) { $contextOptions['http']['content'] = $postFields; } $context = stream_context_create($contextOptions); $result = @file_get_contents($url, false, $context); return $result === false ? null : $result; } private function buildWorkshopGroupKey(string $appId): string { return 'steamapp_' . $appId; } }