From fcc1b18e4c65d5f0573cf0b31783bdbc1ec9b1ce Mon Sep 17 00:00:00 2001 From: Frank Harris Date: Sat, 17 Jan 2026 09:12:06 -0600 Subject: [PATCH] Steam workshop UPDATE --- .../DayZ Workshop Mod Auto Update/Modlist.txt | 13 + .../ServerStartup - OlivUpdate V2 | 114 +++++ .../ServerStartup - OlivUpdate.bat | 100 +++++ modules/steam_workshop/README.md | 26 ++ .../controllers/SteamWorkshopController.php | 135 ++++++ modules/steam_workshop/lang/en_US.php | 45 ++ .../steam_workshop/lib/GameAdapters/ark.xml | 16 + .../steam_workshop/lib/GameAdapters/arma3.xml | 17 + .../steam_workshop/lib/GameAdapters/cs2.xml | 14 + .../steam_workshop/lib/GameAdapters/dayz.xml | 17 + .../steam_workshop/lib/GameAdapters/gmod.xml | 13 + .../lib/GameAdapters/schema.xsd | 37 ++ .../lib/SteamWorkshopService.php | 390 ++++++++++++++++++ modules/steam_workshop/main.php | 335 +-------------- modules/steam_workshop/module.php | 10 +- modules/steam_workshop/monitor_buttons.php | 23 +- modules/steam_workshop/navigation.xml | 2 +- modules/steam_workshop/steam_workshop.css | 108 +++++ modules/steam_workshop/views/edit.php | 25 ++ modules/steam_workshop/views/index.php | 20 + .../views/partials/form_fields.php | 69 ++++ .../views/partials/mod_table.php | 33 ++ .../views/partials/server_card.php | 63 +++ 23 files changed, 1290 insertions(+), 335 deletions(-) create mode 100644 modules/steam_workshop/DayZ Workshop Mod Auto Update/Modlist.txt create mode 100644 modules/steam_workshop/DayZ Workshop Mod Auto Update/ServerStartup - OlivUpdate V2 create mode 100644 modules/steam_workshop/DayZ Workshop Mod Auto Update/ServerStartup - OlivUpdate.bat create mode 100644 modules/steam_workshop/README.md create mode 100644 modules/steam_workshop/controllers/SteamWorkshopController.php create mode 100644 modules/steam_workshop/lang/en_US.php create mode 100644 modules/steam_workshop/lib/GameAdapters/ark.xml create mode 100644 modules/steam_workshop/lib/GameAdapters/arma3.xml create mode 100644 modules/steam_workshop/lib/GameAdapters/cs2.xml create mode 100644 modules/steam_workshop/lib/GameAdapters/dayz.xml create mode 100644 modules/steam_workshop/lib/GameAdapters/gmod.xml create mode 100644 modules/steam_workshop/lib/GameAdapters/schema.xsd create mode 100644 modules/steam_workshop/lib/SteamWorkshopService.php create mode 100644 modules/steam_workshop/views/edit.php create mode 100644 modules/steam_workshop/views/index.php create mode 100644 modules/steam_workshop/views/partials/form_fields.php create mode 100644 modules/steam_workshop/views/partials/mod_table.php create mode 100644 modules/steam_workshop/views/partials/server_card.php diff --git a/modules/steam_workshop/DayZ Workshop Mod Auto Update/Modlist.txt b/modules/steam_workshop/DayZ Workshop Mod Auto Update/Modlist.txt new file mode 100644 index 00000000..97c20426 --- /dev/null +++ b/modules/steam_workshop/DayZ Workshop Mod Auto Update/Modlist.txt @@ -0,0 +1,13 @@ +1565508334,@SepticFalconMerch +1559317235,@Weapon Redux Pack +1567720365,@DayZ Plus +1572541337,@InventoryPlus +1560819773,@[MOV] Unlimited Stamina +1599353287,@bT_Map +1589849870,@bT_Items +1617874376,@OP_BaseItems +1574054508,@BuildAnywhere +1590841260,@Trader +1559212036,@RPCFramework +1578227776,@Permissions-Framework +1564026768,@Community-Online-Tools \ No newline at end of file diff --git a/modules/steam_workshop/DayZ Workshop Mod Auto Update/ServerStartup - OlivUpdate V2 b/modules/steam_workshop/DayZ Workshop Mod Auto Update/ServerStartup - OlivUpdate V2 new file mode 100644 index 00000000..137a4328 --- /dev/null +++ b/modules/steam_workshop/DayZ Workshop Mod Auto Update/ServerStartup - OlivUpdate V2 @@ -0,0 +1,114 @@ +@echo off +TITLE DayZ Server 1 - Normal Modded Server +COLOR 0B +:: Parameters:: +::DayZ Parameters +set DAYZ-SA_SERVER_LOCATION="C:\FILEPATH\Server" +set DAYZ-SAL_NAME=DZSALModServer.exe +set LOG_LOCATION=C:\FILEPATH\ServerLogs\Server +set PORT_NUM=2302 +:: +::Battleye Parameters +set BE_FOLDER="C:\FILEPATH\Server\Battleye" +set BEC_LOCATION="C:\FILEPATH\Server\Battleye\Bec" +:: +::ModCheck Parameters +set MOD_LIST=(C:\FILEPATH\Modlist.txt) +set STEAM_WORKSHOP=C:\FILEPATH\steamcmd\steamapps\workshop\content\221100 +set STEAMCMD_LOCATION=C:\FILEPATH\steamcmd +set STEAM_USER=your steam username +set STEAMCMD_DEL=5 +setlocal EnableDelayedExpansion +:::::::::::::: + +echo Agusanz +goto checksv +pause + +:checksv +tasklist /FI "IMAGENAME eq %DAYZ-SAL_NAME%" 2>NUL | find /I /N "%DAYZ-SAL_NAME%">NUL +if "%ERRORLEVEL%"=="0" goto checkbec +cls +echo Server is not running, taking care of it.. +goto killsv + +:checkbec +tasklist /FI "IMAGENAME eq Bec.exe" 2>NUL | find /I /N "Bec.exe">NUL +if "%ERRORLEVEL%"=="0" goto loopsv +cls +echo Bec is not running, taking care of it.. +goto startbec + +:loopsv +FOR /L %%s IN (30,-1,0) DO ( + cls + echo Server is running. Checking again in %%s seconds.. + timeout 1 >nul +) +goto checksv + +:killsv +taskkill /f /im %DAYZ-SAL_NAME% +goto checkmods + +:startsv +cls +echo Starting DayZ SA Server. +timeout 1 >nul +cls +echo Starting DayZ SA Server.. +timeout 1 >nul +cls +echo Starting DayZ SA Server... +cd "%DAYZ-SA_SERVER_LOCATION%" +start %DAYZ-SAL_NAME% -config=serverDZ.cfg -port=%PORT_NUM% -dologs -adminlog -netlog -freezecheck -BEpath=%BE_FOLDER% -profiles=%LOG_LOCATION% "-mod=!MODS_TO_LOAD!%" "-scrAllowFileWrite" +FOR /L %%s IN (30,-1,0) DO ( + cls + echo Initializing server, wait %%s seconds to initialize Bec.. + timeout 1 >nul +) +goto startbec + +:startbec +cls +echo Starting Bec. +timeout 1 >nul +cls +echo Starting Bec.. +timeout 1 >nul +cls +echo Starting Bec... +timeout 1 >nul +cd "%BEC_LOCATION%" +start Bec.exe -f Config.cfg +goto checksv + +:checkmods +cls +FOR /L %%s IN (%STEAMCMD_DEL%,-1,0) DO ( + cls + echo Checking for mod updates in %%s seconds.. + timeout 1 >nul +) +echo Reading in configurations/variables set in this batch and MOD_LIST. Updating Steam Workbench mods... +@ timeout 1 >nul +cd %STEAMCMD_LOCATION% +for /f "tokens=1,2 delims=," %%g in %MOD_LIST% do steamcmd.exe +login %STEAM_USER% +workshop_download_item 221100 "%%g" +quit +cls +echo Steam Workshop files up to date! Syncing Workbench source with server destination... +@ timeout 2 >nul +cls +@ for /f "tokens=1,2 delims=," %%g in %MOD_LIST% do robocopy "%STEAM_WORKSHOP%\%%g" "%DAYZ-SA_SERVER_LOCATION%\%%h" *.* /mir +@ for /f "tokens=1,2 delims=," %%g in %MOD_LIST% do forfiles /p "%DAYZ-SA_SERVER_LOCATION%\%%h" /m *.bikey /s /c "cmd /c copy @path %DAYZ-SA_SERVER_LOCATION%\keys" +cls +echo Sync complete! If sync not completed correctly, verify configuration file. +@ timeout 3 >nul +cls +set "MODS_TO_LOAD=" +for /f "tokens=1,2 delims=," %%g in %MOD_LIST% do ( +set "MODS_TO_LOAD=!MODS_TO_LOAD!%%h;" +) +set "MODS_TO_LOAD=!MODS_TO_LOAD:~0,-1!" +ECHO Will start DayZ with the following mods: !MODS_TO_LOAD!% +@ timeout 3 >nul +goto startsv \ No newline at end of file diff --git a/modules/steam_workshop/DayZ Workshop Mod Auto Update/ServerStartup - OlivUpdate.bat b/modules/steam_workshop/DayZ Workshop Mod Auto Update/ServerStartup - OlivUpdate.bat new file mode 100644 index 00000000..9b927ff4 --- /dev/null +++ b/modules/steam_workshop/DayZ Workshop Mod Auto Update/ServerStartup - OlivUpdate.bat @@ -0,0 +1,100 @@ +@echo off +TITLE DayZ Server +COLOR 0A +:: Variables:: +::DZSALModServer.exe path +set DAYZ-SA_SERVER_LOCATION="C:\SERVERFILEPATH\server" +::Bec.exe path +set BEC_LOCATION="C:\SERVERFILEPATH\server\Battleye\Bec" +:: +::ModCheck ; Enter location of Mod List and where your steam workshop files download to (set for default) +set MOD_LIST=(C:\FILEPATH\Modlist.txt) +set STEAM_WORKSHOP=C:\FILEPATH\steamcmd\steamapps\workshop\content\221100 +set STEAMCMD_LOCATION=C:\FILEPATH\steamcmd +set STEAM_USER=USERNAME +set STEAMCMD_DEL=10 +:::::::::::::: + +echo Agusanz +goto checksv +pause + +:checksv +tasklist /FI "IMAGENAME eq DZSALModserver.exe" 2>NUL | find /I /N "DZSALModserver.exe">NUL +if "%ERRORLEVEL%"=="0" goto checkbec +cls +echo Server is not running, taking care of it.. +goto killsv + +:checkbec +tasklist /FI "IMAGENAME eq Bec.exe" 2>NUL | find /I /N "Bec.exe">NUL +if "%ERRORLEVEL%"=="0" goto loopsv +cls +echo Bec is not running, taking care of it.. +goto startbec + +:loopsv +FOR /L %%s IN (30,-1,0) DO ( + cls + echo Server is running. Checking again in %%s seconds.. + timeout 1 >nul +) +goto checksv + +:killsv +taskkill /f /im Bec.exe +taskkill /f /im DZSALModserver.exe +goto checkmods + +:startsv +cls +echo Starting DayZ SA Server. +timeout 1 >nul +cls +echo Starting DayZ SA Server.. +timeout 1 >nul +cls +echo Starting DayZ SA Server... +cd "%DAYZ-SA_SERVER_LOCATION%" +start DZSALModserver.exe -config=serverDZ.cfg -port=2302 -dologs -adminlog -netlog -freezecheck -BEpath=C:\SERVERFILEPATH\server\battleye -profiles=C:\FILEPATH\ServerLogs\server "-mod=@yourmods;@gohere" +FOR /L %%s IN (30,-1,0) DO ( + cls + echo Initializing server, wait %%s seconds to initialize Bec.. + timeout 1 >nul +) +goto startbec + +:startbec +cls +echo Starting Bec. +timeout 1 >nul +cls +echo Starting Bec.. +timeout 1 >nul +cls +echo Starting Bec... +timeout 1 >nul +cd "%BEC_LOCATION%" +start Bec.exe -f Config.cfg +goto checksv + +:checkmods +cls +FOR /L %%s IN (90,-1,0) DO ( + cls + echo Checking for mod updates in %%s seconds.. + timeout 1 >nul +) +echo Reading in configurations/variables set in this batch and MOD_LIST. Updating Steam Workbench mods... +@ timeout 1 >nul +cd %STEAMCMD_LOCATION% +for /f "tokens=1,2 delims=," %%g in %MOD_LIST% do steamcmd.exe +login %STEAM_USER% +workshop_download_item 221100 "%%g" +quit +cls +cls +echo Steam Workshop files up to date! Syncing Workbench source with server destination... +@ timeout 2 >nul +@ for /f "tokens=1,2 delims=," %%g in %MOD_LIST% do robocopy "%STEAM_WORKSHOP%\%%g" "%DAYZ-SA_SERVER_LOCATION%\%%h" *.* /mir +cls +echo Sync complete! If sync not completed correctly, verify configuration file. +@ timeout 3 >nul +cls +goto startsv \ No newline at end of file diff --git a/modules/steam_workshop/README.md b/modules/steam_workshop/README.md new file mode 100644 index 00000000..721ad4bc --- /dev/null +++ b/modules/steam_workshop/README.md @@ -0,0 +1,26 @@ +# Steam Workshop Automation (WIP) + +This folder now hosts the rewritten Steam Workshop tooling for the GSP panel. The previous DayZ-only batch scripts are left untouched under `DayZ Workshop Mod Auto Update/` for historical reference, but the new MVC layer introduces adapters, XML-backed configuration, and an eventual agent scheduler. + +## Milestone 1 summary + +- **Controllers** – `controllers/SteamWorkshopController.php` routes the module entrypoint through a thin MVC wrapper. +- **Service layer** – `lib/SteamWorkshopService.php` loads/saves per-home XML configs under `data/configs/.xml`, parses Modlist-style imports, and exposes adapter metadata. +- **Adapters** – `lib/GameAdapters/*.xml` define canonical behaviors for DayZ, Arma 3, ARK, Garry's Mod, and CS2. They are validated against `schema.xsd`. +- **Views** – `views/*` render the server list, edit form, and parsed mod table using localized strings from `lang/en_US.php`. +- **Data directory** – `data/configs/` stores the serialized workshop configuration for each game home. + +## Editing workflow + +1. Visit `home.php?m=steam_workshop&p=main` to see the list of homes you can access. Click **Configure** on any home to edit its Workshop setup. +2. Paste a Modlist.txt style payload (e.g., `1565508334,@MyMod`) into the Workshop IDs textarea. +3. Choose the adapter, interval, install strategy, and on-update action, then click **Save settings**. The controller serializes this into XML so the agent can consume it later. +4. Config files live under `modules/steam_workshop/data/configs/`. Delete a file to reset a home to defaults. + +## Roadmap + +- **Milestone 2** will flesh out the adapter runtime helpers and validation against the schema. +- **Milestone 3** wires the Linux/Windows agents via a new `workshop_update` RPC and scheduler, using the serialized XML from this module. +- Later milestones add dry-run/apply actions, activation writers, and safe apply hooks. + +> GSP is a heavily customized fork of OGP maintained by WDS. Keep all Steam Workshop code inside this module tree so storefront, agents, and future docs stay decoupled. diff --git a/modules/steam_workshop/controllers/SteamWorkshopController.php b/modules/steam_workshop/controllers/SteamWorkshopController.php new file mode 100644 index 00000000..9d27216f --- /dev/null +++ b/modules/steam_workshop/controllers/SteamWorkshopController.php @@ -0,0 +1,135 @@ +service = new SteamWorkshopService($db); + $this->lang = $this->loadLang(); + } + + public function handle(): void + { + global $db; + + $userId = (int)($_SESSION['user_id'] ?? 0); + $isAdmin = $db->isAdmin($userId); + $action = $_GET['action'] ?? 'index'; + + echo ''; + + if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') { + $this->handleSave($userId, $isAdmin); + return; + } + + if ($action === 'edit') { + $this->handleEdit($userId, $isAdmin); + return; + } + + $this->renderIndex($userId, $isAdmin); + } + + private function handleSave(int $userId, bool $isAdmin): void + { + $homeId = isset($_POST['home_id']) ? (int)$_POST['home_id'] : 0; + if ($homeId <= 0) { + print_failure($this->lang['error_missing_home'] ?? 'Home ID missing.'); + $this->renderIndex($userId, $isAdmin); + return; + } + + $home = $this->service->getHome($homeId, $userId, $isAdmin); + if ($home === null) { + print_failure($this->lang['error_home_not_found'] ?? 'Home not found.'); + $this->renderIndex($userId, $isAdmin); + return; + } + + $config = $this->service->buildConfigFromRequest($_POST); + $this->service->saveConfig($homeId, $config); + print_success($this->lang['message_config_saved'] ?? 'Workshop configuration saved.'); + + $this->renderEdit($home, $config, $isAdmin); + } + + private function handleEdit(int $userId, bool $isAdmin): void + { + $homeId = isset($_GET['home_id']) ? (int)$_GET['home_id'] : 0; + if ($homeId <= 0) { + print_failure($this->lang['error_missing_home'] ?? 'Home ID missing.'); + $this->renderIndex($userId, $isAdmin); + return; + } + + $home = $this->service->getHome($homeId, $userId, $isAdmin); + if ($home === null) { + print_failure($this->lang['error_home_not_found'] ?? 'Home not found.'); + $this->renderIndex($userId, $isAdmin); + return; + } + + $config = $this->service->loadConfig($homeId); + $this->renderEdit($home, $config, $isAdmin); + } + + private function renderIndex(int $userId, bool $isAdmin): void + { + $records = []; + $homes = $this->service->listHomesForUser($userId, $isAdmin); + foreach ($homes as $home) { + $config = $this->service->loadConfig((int)$home['home_id']); + $adapter = $this->service->getAdapterByKey($config['adapter_key']); + $records[] = [ + 'home' => $home, + 'config' => $config, + 'adapter' => $adapter, + ]; + } + + $this->render('index', [ + 'lang' => $this->lang, + 'records' => $records, + 'isAdmin' => $isAdmin, + 'adapterOptions' => $this->service->getAdapterOptions(), + ]); + } + + private function renderEdit(array $home, array $config, bool $isAdmin): void + { + $this->render('edit', [ + 'lang' => $this->lang, + 'home' => $home, + 'config' => $config, + 'isAdmin' => $isAdmin, + 'adapterOptions' => $this->service->getAdapterOptions(), + ]); + } + + private function render(string $view, array $data = []): void + { + extract($data); + $lang = $this->lang; + require __DIR__ . '/../views/' . $view . '.php'; + } + + private function loadLang(): array + { + $langFile = __DIR__ . '/../lang/en_US.php'; + if (is_file($langFile)) { + $strings = require $langFile; + if (is_array($strings)) { + return $strings; + } + } + + return []; + } +} diff --git a/modules/steam_workshop/lang/en_US.php b/modules/steam_workshop/lang/en_US.php new file mode 100644 index 00000000..0de88db5 --- /dev/null +++ b/modules/steam_workshop/lang/en_US.php @@ -0,0 +1,45 @@ + 'Steam Workshop automation (preview)', + 'heading_edit_home' => 'Edit Workshop settings for %s', + 'button_edit' => 'Configure', + 'button_create_config' => 'Create config', + 'button_save' => 'Save settings', + 'button_cancel' => 'Back to list', + 'label_feature_flag' => 'Enable scheduled Workshop updates for this server', + 'label_adapter' => 'Game adapter', + 'label_interval' => 'Update interval (minutes)', + 'label_interval_hint' => 'Runs on the agent scheduler. Allowed range: 15–360 minutes.', + 'label_staging_dir' => 'Staging directory (optional)', + 'label_install_strategy' => 'Install strategy', + 'label_on_update_action' => 'On update action', + 'label_post_install_script' => 'Post-install script (absolute path)', + 'label_mod_import' => 'Workshop IDs list (one "id,@ModName" per line)', + 'hint_mod_import' => 'Paste from Modlist.txt or import from a collection. IDs are sanitized automatically.', + 'status_enabled' => 'Enabled', + 'status_disabled' => 'Disabled', + 'status_hot_reload' => 'Hot reload ready', + 'status_restart_required' => 'Restart required', + 'empty_state_admin' => 'No game homes are assigned yet. Use "Assign Game Homes" to continue.', + 'empty_state_user' => 'No servers available. Ask an administrator to assign a game home.', + 'message_config_saved' => 'Workshop settings saved.', + 'error_missing_home' => 'Select a server before editing Workshop settings.', + 'error_home_not_found' => 'Unable to locate that server or you do not have permission.', + 'mods_table_heading' => 'Parsed Workshop entries', + 'mods_table_empty' => 'No Workshop IDs have been added yet.', + 'mods_header_id' => 'Workshop ID', + 'mods_header_label' => 'Label', + 'mods_header_source' => 'Source', + 'mods_header_enabled' => 'Enabled', + 'install_copy' => 'Copy files into the server directory', + 'install_symlink' => 'Symlink from Steam workshop content', + 'install_staging' => 'Download to staging only (manual apply)', + 'action_queue_for_restart' => 'Queue for restart', + 'action_hot_reload_if_supported' => 'Hot reload if the adapter allows it', + 'summary_adapter' => 'Adapter', + 'summary_interval' => 'Interval', + 'summary_mods' => 'Mods', + 'summary_last_saved' => 'Last saved', + 'summary_hot_reload' => 'Hot reload', + 'raw_definition_label' => 'Raw Workshop list', +]; diff --git a/modules/steam_workshop/lib/GameAdapters/ark.xml b/modules/steam_workshop/lib/GameAdapters/ark.xml new file mode 100644 index 00000000..e3cccf4e --- /dev/null +++ b/modules/steam_workshop/lib/GameAdapters/ark.xml @@ -0,0 +1,16 @@ + + + 346110 + %SERVER_ROOT%/ShooterGame/Content/Mods + + + + + + + Copies .mod stubs alongside mod folders and rewrites ActiveMods inside GameUserSettings.ini. + diff --git a/modules/steam_workshop/lib/GameAdapters/arma3.xml b/modules/steam_workshop/lib/GameAdapters/arma3.xml new file mode 100644 index 00000000..c00740b0 --- /dev/null +++ b/modules/steam_workshop/lib/GameAdapters/arma3.xml @@ -0,0 +1,17 @@ + + + 107410 + %SERVER_ROOT%/arma3server/mods + %SERVER_ROOT%/keys + + + + + + + Matches Bohemia best practices: copy .bikey files and keep @Mod folders under the server root. + diff --git a/modules/steam_workshop/lib/GameAdapters/cs2.xml b/modules/steam_workshop/lib/GameAdapters/cs2.xml new file mode 100644 index 00000000..45e72e4e --- /dev/null +++ b/modules/steam_workshop/lib/GameAdapters/cs2.xml @@ -0,0 +1,14 @@ + + + 730 + %SERVER_ROOT%/game/csgo/workshop + + + + host_workshop_map {id} + Focuses on map workshop IDs and can hot-reload via `host_workshop_map` when the server allows it. + diff --git a/modules/steam_workshop/lib/GameAdapters/dayz.xml b/modules/steam_workshop/lib/GameAdapters/dayz.xml new file mode 100644 index 00000000..6097833e --- /dev/null +++ b/modules/steam_workshop/lib/GameAdapters/dayz.xml @@ -0,0 +1,17 @@ + + + 221100 + %SERVER_ROOT%/WorkshopMods + %SERVER_ROOT%/keys + + + + + + + Copies .bikey files into the server keys directory and builds a classic -mod parameter string. + diff --git a/modules/steam_workshop/lib/GameAdapters/gmod.xml b/modules/steam_workshop/lib/GameAdapters/gmod.xml new file mode 100644 index 00000000..04af37af --- /dev/null +++ b/modules/steam_workshop/lib/GameAdapters/gmod.xml @@ -0,0 +1,13 @@ + + + 4000 + %SERVER_ROOT%/garrysmod/addons + + + + Relies on a Steam Workshop collection. Files stay in the Steam library and only the host_workshop_collection parameter changes. + diff --git a/modules/steam_workshop/lib/GameAdapters/schema.xsd b/modules/steam_workshop/lib/GameAdapters/schema.xsd new file mode 100644 index 00000000..d1b3fcd1 --- /dev/null +++ b/modules/steam_workshop/lib/GameAdapters/schema.xsd @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/steam_workshop/lib/SteamWorkshopService.php b/modules/steam_workshop/lib/SteamWorkshopService.php new file mode 100644 index 00000000..166e4d72 --- /dev/null +++ b/modules/steam_workshop/lib/SteamWorkshopService.php @@ -0,0 +1,390 @@ +db = $db; + $this->configDir = __DIR__ . '/../data/configs'; + $this->adapterDir = __DIR__ . '/GameAdapters'; + + if (!is_dir($this->configDir)) { + mkdir($this->configDir, 0775, true); + } + } + + /** + * Fetch all homes visible to the given user. + * + * @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); + } + + /** + * Retrieve a single home, ensuring the user is allowed to see it. + */ + 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; + } + + /** + * @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>, + * raw_definition: string, + * last_saved_at: int|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); + } + + /** + * 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); + + 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, + ]; + } + + /** + * Accepts imports such as "123456,@My Mod" per line. + * + * @return array + */ + 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; + } + + /** + * Build a SteamCMD command array for a single workshop item. + */ + 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; + } + + /** + * Load adapter metadata for UI and validation. + * + * @return array> + */ + public function loadAdapters(): array + { + $adapters = []; + $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') { + continue; + } + if (basename($file) === 'schema.xsd') { + continue; + } + + $doc = new DOMDocument(); + if (!$doc->load($file)) { + continue; + } + + 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; + } + + public function getAdapterByKey(string $key): array + { + foreach ($this->loadAdapters() as $adapter) { + if ($adapter['key'] === $key) { + return $adapter; + } + } + + return []; + } + + 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'; + 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 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, + ]; + } +} diff --git a/modules/steam_workshop/main.php b/modules/steam_workshop/main.php index 87d9f7c2..c26198dd 100644 --- a/modules/steam_workshop/main.php +++ b/modules/steam_workshop/main.php @@ -1,332 +1,17 @@ '."\n". - ''."\n". - ''; +require_once __DIR__ . '/controllers/SteamWorkshopController.php'; -function exec_ogp_module() +function exec_ogp_module(): void { - - Global $db,$view,$settings; - echo '

Steam Workshop

'; - define('CONFIGS', "modules/steam_workshop/game_configs/"); - - if(isset($_GET['home_id-mod_id-ip-port']) && $_GET['home_id-mod_id-ip-port'] != "") - list($home_id, $mod_id, $ip, $port) = explode("-", $_GET['home_id-mod_id-ip-port']); - else - { - print_failure(get_lang('no_game_servers_assigned')); - return; - } - - if(!isset($_POST['workshop_mod_id']) and !isset($_GET['show_log']) and !isset($_POST['manual_workshop_mod_id'])) - { - echo ""; - } - - $isAdmin = $db->isAdmin( $_SESSION['user_id'] ); - - if($isAdmin) - $home_cfg = $db->getGameHome($home_id); - else - $home_cfg = $db->getUserGameHome($_SESSION['user_id'],$home_id); - - if($home_cfg) - { - $server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_cfg['home_cfg_file']); - if($server_xml === FALSE) - { - print_failure(get_lang_f('failed_reading_xml_file', SERVER_CONFIG_LOCATION."/".$home_cfg['home_cfg_file'])); - return; - } - - if(!isset($home_cfg['mods'][$mod_id]['mod_key'])) - { - print_failure(get_lang_f('mod_id_does_not_exists_in_home', $mod_id, $home_id)); - return; - } - - $modkey = $home_cfg['mods'][$mod_id]['mod_key']; - $mod_xml = xml_get_mod($server_xml, $modkey); - - if (!$mod_xml) - { - print_failure(get_lang_f('mod_key_not_found_from_xml', $modkey)); - return; - } - - if(preg_match('/(linux|win)(32|64)?/i', $home_cfg['game_key'], $matches)) - { - $os = ""; - if(strtolower($matches[1]) == 'linux') - $os = "Linux"; - elseif(strtolower($matches[1]) == 'win') - $os = "Windows"; - } - else - { - print_failure(get_lang_f('unable_to_get_os_from_game_key', $home_cfg['game_key'])); - return; - } - - $xml_file = CONFIGS.$mod_xml->installer_name."_".$os.".xml"; - - if(!file_exists($xml_file)) - { - print_failure(get_lang('workshop_configuration_not_found')); - return; - } - - $dom = new DOMDocument(); - - if ( @$dom->load($xml_file) === FALSE ) - { - print_failure(get_lang('workshop_configuration_file_has_bad_format')); - return; - } - - $xml = simplexml_load_file($xml_file); - - if($xml !== false) - { - $remote = new OGPRemoteLibrary($home_cfg['agent_ip'],$home_cfg['agent_port'],$home_cfg['encryption_key'], $home_cfg['timeout']); - - if($remote->status_chk() !== 1) - { - print_failure(get_lang('remote_server_offline')); - } - - if(isset($_GET['show_log'])) - { - $update_active = $remote->get_log(OGP_SCREEN_TYPE_UPDATE,$home_id,clean_path($home_cfg['home_path']),$log_txt); - if ( $update_active == 1 ) - { - if(isset($_POST['sgc'])) - { - $remote->send_steam_guard_code($home_id, $_POST['sgc']); - return; - } - echo "

". get_lang("update_in_progress") ."

\n"; - echo "
".$log_txt."
\n\n
\n"; - if(preg_match('/Two-factor code:$/m', $log_txt) and !isset($_GET['get_sgc'])) - { - $view->refresh("?m=steam_workshop&p=main&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."&get_sgc=show&show_log",0); - return; - } - if(isset($_GET['get_sgc']) && $_GET['get_sgc'] == 'show') - return; - - echo "

"; - echo get_lang("refresh_steam_workshop_status") ."

"; - $view->refresh("?m=steam_workshop&p=main&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."&show_log",5); - } - else - { - print_success( get_lang("update_completed") ); - echo "
".$log_txt."
\n"; - echo "
<< ". get_lang("back") ."
"; - } - } - else - { - if(isset($_POST['workshop_mod_id']) OR isset($_POST['manual_workshop_mod_id'])) - { - $failure = false; - if(isset($_POST['manual_workshop_mod_id']) and $_POST['manual_workshop_mod_id'] != "" and preg_match('/^([0-9]+,?)+$/', $_POST['manual_workshop_mod_id'])) - { - $mods_list = $_POST['manual_workshop_mod_id']; - $mod_id_array = explode(',', $mods_list); - foreach($mod_id_array as $workshop_mod_id) - { - $exist = false; - foreach($xml->mods->mod as $mod) - { - if($mod['id'] == $workshop_mod_id) - { - $exist = true; - break; - } - } - - if(belongs_to_workshop($workshop_mod_id, $xml->workshop_id)) - { - if(!$exist) - { - list($mod_title, $mod_description, $mod_image_url, $download_url, $filename, $file_size) = get_mod_info($workshop_mod_id); - //add mods to the xml - $mod = new SimpleXMLElement(''); - $mod->addAttribute('id', $workshop_mod_id); - $mod->addChild('name', $mod_title); - $mod->addChild('description', base64_encode($mod_description)); - $mod->addChild('image_url', $mod_image_url); - $mod->addChild('download_url', $download_url); - $mod->addChild('filename', $filename); - $mod->addChild('file_size', $file_size); - $moddom = dom_import_simplexml($mod)->ownerDocument; - $moddom->formatOutput = true; - $mod_string = $moddom->saveXML($moddom->documentElement); - - $dom = dom_import_simplexml($xml)->ownerDocument; - $dom->formatOutput = true; - - $mods = $dom->getElementsByTagName('mods')->item(0); - - $f = $dom->createDocumentFragment(); - $f->appendXML($mod_string."\n"); - $mods->appendChild($f); - - - file_put_contents($xml_file, $dom->saveXML()); - $xml = simplexml_load_file($xml_file); - } - } - else - { - print_failure(get_lang_f('mod_does_not_belong_to_workshop', $workshop_mod_id)); - $failure = true; - } - } - } - elseif(isset($_POST['workshop_mod_id'])) - { - $mods_list = implode(',',$_POST['workshop_mod_id']); - } - - if(isset($_POST['install']) and !$failure and isset($mods_list) and preg_match('/^([0-9]+,?)+$/', $mods_list)) - { - $config = $xml->config; - $anonymous_login = $xml->anonymous_login; - $download_method = $xml->download_method; - $user = $settings['steam_user']; - $pass = $settings['steam_pass']; - $regex = $config->regex; - $mods_backreference_index = (int)$config->mods_backreference_index; - $variable = $config->variable; - $place_after = $config->place_after; - $mod_string = $config->mod_string; - $string_separator = $config->string_separator; - $config_file_path = clean_path($home_cfg['home_path']."/".$config->filepath); - $post_install = $xml->post_install; - $mod_names_list = get_mod_names_list($mods_list, $xml->mods->mod); - $mods_full_path = clean_path($home_cfg['home_path'].'/'.$xml->mods_path); - $workshop_id = $xml->workshop_id; - - $url_list = ""; - $filename_list = ""; - if($download_method == "steamapi") - { - foreach(explode(',', $mods_list) as $workshop_mod_id) - { - foreach($xml->mods->mod as $mod) - { - if($mod['id'] == $workshop_mod_id) - { - $separator = $url_list == ""?"":","; - $url_list .= $separator.$mod->download_url; - $filename_list .= $separator.$mod->filename; - } - } - } - } - - $steam_out = $remote->steam_workshop($home_id, $mods_full_path, - $workshop_id, $mods_list, - $regex, $mods_backreference_index, - $variable, $place_after, $mod_string, - $string_separator, $config_file_path, - $post_install, $mod_names_list, - $anonymous_login, $user, $pass, - $download_method, $url_list, $filename_list); - if ( $steam_out === 1 ) - { - print_success( get_lang("mod_installation_started") ); - $view->refresh("?m=steam_workshop&p=main&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."&show_log", 2); - } - elseif( $steam_out === 0 ) - { - print_failure( get_lang("failed_to_start_steam_workshop") ); - return; - } - elseif ( $steam_out === -1 ) - { - print_failure( get_lang("connection_error") ); - } - } - - if(isset($_POST['show_info']) and !$failure and isset($mods_list) and preg_match('/^([0-9]+,?)+$/', $mods_list)) - { - $mod_id_array = explode(',', $mods_list); - echo ""; - foreach($xml->mods->mod as $mod) - { - if(in_array($mod['id'],$mod_id_array)) - { - echo ""; - } - } - echo "

".$mod->name."

". - "
". - "
".htmlentities(base64_decode($mod->description))."
".get_lang('back').""; - } - } - else - { - $ft = new FormTable(); - $ft->start_form("?m=steam_workshop&p=main&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port'], "post", "onsubmit='return isValidForm(this)' data-form-error='".get_lang('select_at_least_one_mod_or_enter_mod_id')."'"); - $ft->start_table(); - if(count($xml->mods->mod) > 0) - { - echo '
'; - foreach($xml->mods->mod as $mod) - echo "
"; - echo '
'; - } - $ft->add_field('string', 'manual_workshop_mod_id',''); - $ft->end_table(); - $ft->add_button("submit", "install", get_lang('install_mod')); - $ft->add_button("submit", "show_info", get_lang('show_mod_info')); - $ft->end_form(); - } - } - } - else - { - print_failure(get_lang('workshop_configuration_file_has_bad_format')); - return; - } - } - else - { - print_failure(get_lang('game_home_not_found')); - return; - } + global $db; + + echo '

' . get_lang('steam_workshop') . '

'; + + $controller = new SteamWorkshopController($db); + $controller->handle(); } -?> diff --git a/modules/steam_workshop/module.php b/modules/steam_workshop/module.php index b8faf67e..2b79a365 100644 --- a/modules/steam_workshop/module.php +++ b/modules/steam_workshop/module.php @@ -23,8 +23,14 @@ */ // Module general information $module_title = "Steam Workshop"; -$module_version = "1.1"; +$module_version = "2.0"; $db_version = 0; $module_required = TRUE; -$module_menus = array(array( 'subpage' => 'workshop_admin', 'name'=>'Steam Workshop', 'group'=>'admin' )); +$module_menus = array( + array( + 'subpage' => 'main', + 'name' => 'Steam Workshop', + 'group' => 'admin' + ) +); ?> \ No newline at end of file diff --git a/modules/steam_workshop/monitor_buttons.php b/modules/steam_workshop/monitor_buttons.php index 4da0e471..7c11ec40 100644 --- a/modules/steam_workshop/monitor_buttons.php +++ b/modules/steam_workshop/monitor_buttons.php @@ -22,22 +22,31 @@ * */ -if(isset($server_xml->installer) and $server_xml->installer == "steamcmd") +if (isset($server_xml->installer) && $server_xml->installer === "steamcmd") { - $mod_xml = xml_get_mod($server_xml, $server_home['mod_key']); - require_once("modules/steam_workshop/functions.php"); - if(isset($mod_xml->installer_name) and !in_array((string)$mod_xml->installer_name, get_blacklist())) + $homeId = isset($server_home['home_id']) ? (int)$server_home['home_id'] : 0; + if ($homeId > 0) { + $label = get_lang('steam_workshop'); + if ($label === 'steam_workshop') + { + $label = 'Steam Workshop'; + } + $href = "?m=steam_workshop&p=main&action=edit&home_id=" . $homeId; $module_buttons = array( - " - - Steam Workshop + " + + " . $label . " " ); } else + { $module_buttons = array(); + } } else +{ $module_buttons = array(); +} ?> \ No newline at end of file diff --git a/modules/steam_workshop/navigation.xml b/modules/steam_workshop/navigation.xml index a82623fb..b5017232 100644 --- a/modules/steam_workshop/navigation.xml +++ b/modules/steam_workshop/navigation.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/modules/steam_workshop/steam_workshop.css b/modules/steam_workshop/steam_workshop.css index 25394a4b..c8167c2e 100644 --- a/modules/steam_workshop/steam_workshop.css +++ b/modules/steam_workshop/steam_workshop.css @@ -15,3 +15,111 @@ overflow-y: scroll; text-align:left; } + +.sw-admin { + margin-top: 1rem; +} + +.sw-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1rem; +} + +.sw-card { + border: 1px solid #dcdcdc; + border-radius: 6px; + padding: 1rem; + background: #fafafa; +} + +.sw-card__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.sw-card__meta { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 0.75rem; + margin-top: 0.75rem; +} + +.sw-card__meta dt { + font-weight: 600; + margin: 0; +} + +.sw-card__meta dd { + margin: 0; +} + +.sw-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.sw-form__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1rem; +} + +.sw-form__actions { + display: flex; + gap: 0.5rem; +} + +.sw-form label { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-weight: 600; +} + +.sw-form input, +.sw-form select, +.sw-form textarea { + padding: 0.4rem; + border-radius: 4px; + border: 1px solid #c7c7c7; +} + +.sw-mods__table { + width: 100%; + border-collapse: collapse; +} + +.sw-mods__table th, +.sw-mods__table td { + border: 1px solid #ddd; + padding: 0.4rem; +} + +.btn { + display: inline-block; + padding: 0.4rem 0.8rem; + border-radius: 4px; + border: 1px solid #888; + text-decoration: none; + color: #222; + background: #fff; +} + +.btn.primary { + background: #0b5ed7; + border-color: #0a58ca; + color: #fff; +} + +.sw-toggle { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.sw-toggle input { + width: auto; +} diff --git a/modules/steam_workshop/views/edit.php b/modules/steam_workshop/views/edit.php new file mode 100644 index 00000000..91126941 --- /dev/null +++ b/modules/steam_workshop/views/edit.php @@ -0,0 +1,25 @@ + +
+

+ +

+ +
+ + +
+ + +
+
+ + +
diff --git a/modules/steam_workshop/views/index.php b/modules/steam_workshop/views/index.php new file mode 100644 index 00000000..2057dcad --- /dev/null +++ b/modules/steam_workshop/views/index.php @@ -0,0 +1,20 @@ + +
+ +
+

+
+ +
+ + + +
+ +
diff --git a/modules/steam_workshop/views/partials/form_fields.php b/modules/steam_workshop/views/partials/form_fields.php new file mode 100644 index 00000000..5c9c1c76 --- /dev/null +++ b/modules/steam_workshop/views/partials/form_fields.php @@ -0,0 +1,69 @@ + +
+ + + + + + + + + + + + + +
+ + diff --git a/modules/steam_workshop/views/partials/mod_table.php b/modules/steam_workshop/views/partials/mod_table.php new file mode 100644 index 00000000..bffadf7f --- /dev/null +++ b/modules/steam_workshop/views/partials/mod_table.php @@ -0,0 +1,33 @@ + +
+

+ +

+ + + + + + + + + + + + + + + + + + + + +
+ +
diff --git a/modules/steam_workshop/views/partials/server_card.php b/modules/steam_workshop/views/partials/server_card.php new file mode 100644 index 00000000..c35e6d1c --- /dev/null +++ b/modules/steam_workshop/views/partials/server_card.php @@ -0,0 +1,63 @@ + +
+
+
+

+ +

+ +
+
+ + + +
+
+
+
+
+
+
+
+
+
min
+
+
+
+
+
+
+
+
+
+
+
Status
+
+
+
+
+
+
+
+