Steam workshop UPDATE

This commit is contained in:
Frank Harris 2026-01-17 09:12:06 -06:00
parent 49600d1cfd
commit fcc1b18e4c
23 changed files with 1290 additions and 335 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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/<home_id>.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.

View file

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../lib/SteamWorkshopService.php';
class SteamWorkshopController
{
private SteamWorkshopService $service;
private array $lang;
public function __construct(OGPDatabase $db)
{
$this->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 '<link rel="stylesheet" type="text/css" href="modules/steam_workshop/steam_workshop.css" />';
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 [];
}
}

View file

@ -0,0 +1,45 @@
<?php
return [
'heading_overview' => '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: 15360 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',
];

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<adapter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schema.xsd"
key="ark"
name="ARK: Survival Ascended"
supportsHotReload="false">
<steamAppId>346110</steamAppId>
<modsDir>%SERVER_ROOT%/ShooterGame/Content/Mods</modsDir>
<activation type="config_writer">
<template>GameUserSettings.ini:[ServerSettings].ActiveMods={modlist(',')}</template>
</activation>
<postCopyRules>
<copyPattern source="*.mod" destination="%SERVER_ROOT%/ShooterGame/Content/Mods" />
</postCopyRules>
<notes>Copies .mod stubs alongside mod folders and rewrites ActiveMods inside GameUserSettings.ini.</notes>
</adapter>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<adapter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schema.xsd"
key="arma3"
name="Arma 3"
supportsHotReload="false">
<steamAppId>107410</steamAppId>
<modsDir>%SERVER_ROOT%/arma3server/mods</modsDir>
<keysDir>%SERVER_ROOT%/keys</keysDir>
<activation type="launch_parameter">
<template>-mod=@{modlist(';')}</template>
</activation>
<postCopyRules>
<copyPattern source="*.bikey" destination="%SERVER_ROOT%/keys" />
</postCopyRules>
<notes>Matches Bohemia best practices: copy .bikey files and keep @Mod folders under the server root.</notes>
</adapter>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<adapter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schema.xsd"
key="cs2"
name="Counter-Strike 2"
supportsHotReload="true">
<steamAppId>730</steamAppId>
<modsDir>%SERVER_ROOT%/game/csgo/workshop</modsDir>
<activation type="console_command">
<template>host_workshop_map {id}</template>
</activation>
<hotReloadCommand>host_workshop_map {id}</hotReloadCommand>
<notes>Focuses on map workshop IDs and can hot-reload via `host_workshop_map` when the server allows it.</notes>
</adapter>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<adapter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schema.xsd"
key="dayz"
name="DayZ"
supportsHotReload="false">
<steamAppId>221100</steamAppId>
<modsDir>%SERVER_ROOT%/WorkshopMods</modsDir>
<keysDir>%SERVER_ROOT%/keys</keysDir>
<activation type="launch_parameter">
<template>-mod=@{modlist(';')}</template>
</activation>
<postCopyRules>
<copyPattern source="*.bikey" destination="%SERVER_ROOT%/keys" />
</postCopyRules>
<notes>Copies .bikey files into the server keys directory and builds a classic -mod parameter string.</notes>
</adapter>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<adapter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schema.xsd"
key="gmod"
name="Garry's Mod"
supportsHotReload="false">
<steamAppId>4000</steamAppId>
<modsDir>%SERVER_ROOT%/garrysmod/addons</modsDir>
<activation type="launch_parameter">
<template>+host_workshop_collection {collectionId}</template>
</activation>
<notes>Relies on a Steam Workshop collection. Files stay in the Steam library and only the host_workshop_collection parameter changes.</notes>
</adapter>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="adapter">
<xs:complexType>
<xs:sequence>
<xs:element name="steamAppId" type="xs:string" />
<xs:element name="modsDir" type="xs:string" />
<xs:element name="keysDir" type="xs:string" minOccurs="0" />
<xs:element name="activation" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="template" type="xs:string" />
</xs:sequence>
<xs:attribute name="type" type="xs:string" use="optional" />
</xs:complexType>
</xs:element>
<xs:element name="postCopyRules" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="copyPattern" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="source" type="xs:string" use="required" />
<xs:attribute name="destination" type="xs:string" use="optional" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="hotReloadCommand" type="xs:string" minOccurs="0" />
<xs:element name="notes" type="xs:string" minOccurs="0" />
</xs:sequence>
<xs:attribute name="key" type="xs:string" use="required" />
<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="supportsHotReload" type="xs:boolean" use="optional" />
</xs:complexType>
</xs:element>
</xs:schema>

View file

@ -0,0 +1,390 @@
<?php
declare(strict_types=1);
class SteamWorkshopService
{
private const MIN_INTERVAL = 15;
private const MAX_INTERVAL = 360;
private OGPDatabase $db;
private string $configDir;
private string $adapterDir;
public function __construct(OGPDatabase $db)
{
$this->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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>,
* 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<int, array{id:string,label:string,enabled:bool,source:string}>
*/
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<int, array<string, mixed>>
*/
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,
];
}
}

View file

@ -1,332 +1,17 @@
<?php
declare(strict_types=1);
/*
*
* OGP - Open Game Panel
* Copyright (C) 2008 - 2018 The OGP Development Team
*
* http://www.opengamepanel.org/
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* Steam Workshop module entrypoint.
*/
require_once("includes/lib_remote.php");
require_once("modules/config_games/server_config_parser.php");
require_once("modules/steam_workshop/functions.php");
require_once('includes/form_table_class.php');
echo '<link rel="stylesheet" type="text/css" href="css/xbbcode/xbbcode.css">'."\n".
'<script type="text/javascript" src="js/xbbcode/xbbcode.js"></script>'."\n".
'<script type="text/javascript" src="js/modules/steam_workshop.js"></script>';
require_once __DIR__ . '/controllers/SteamWorkshopController.php';
function exec_ogp_module()
function exec_ogp_module(): void
{
Global $db,$view,$settings;
echo '<h2>Steam Workshop</h2>';
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 "<ul>".
"<li><a href='?m=steam_workshop&p=uninstall&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."'>".get_lang('uninstall_mods')."</a></li>".
"<li><a href='?m=gamemanager&p=game_monitor&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."'>".get_lang('back')."</a></li>".
"</ul>";
}
$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 "<p class='note'>". get_lang("update_in_progress") ."</p>\n";
echo "<pre>".$log_txt."</pre>\n</script>\n<div id='dialog' ></div>\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 "<p><a href=\"?m=steam_workshop&p=main&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."&show_log\">";
echo get_lang("refresh_steam_workshop_status") ."</a></p>";
$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 "<pre>".$log_txt."</pre>\n";
echo "<table class='center'><tr><td><a href='?m=steam_workshop&p=main&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."'><< ". get_lang("back") ."</a></td></tr></table>";
}
}
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/>');
$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 "<table>";
foreach($xml->mods->mod as $mod)
{
if(in_array($mod['id'],$mod_id_array))
{
echo "<tr><td><h4>".$mod->name."</h4>".
"<div><img width='240px' style='float:left;' src='".$mod->image_url."'>".
"<div class='bbcode_container' style='padding-left:245px;'>".htmlentities(base64_decode($mod->description))."</div></div><td><tr>";
}
}
echo "</table><a href='?m=steam_workshop&p=main&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."'>".get_lang('back')."</a>";
}
}
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 '<tr><td colspan=2><div id="uninstall_scrolling_checkbox">';
foreach($xml->mods->mod as $mod)
echo "<input type='checkbox' id='select_mod_$mod[id]' name='workshop_mod_id[]' value='$mod[id]'><label for='select_mod_$mod[id]'>".$mod->name."</label><br>";
echo '</div></td></tr>';
}
$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 '<h2>' . get_lang('steam_workshop') . '</h2>';
$controller = new SteamWorkshopController($db);
$controller->handle();
}
?>

View file

@ -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'
)
);
?>

View file

@ -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(
"<a class='monitorbutton' href='?m=steam_workshop&p=main&home_id-mod_id-ip-port=".$server_home['home_id']."-".$server_home['mod_id']."-".$server_home['ip']."-".$server_home['port']."'>
<img src='" . check_theme_image("images/steam_workshop.png") . "' title='Steam Workshop'>
<span>Steam Workshop</span>
"<a class='monitorbutton' href='" . $href . "'>
<img src='" . check_theme_image("images/steam_workshop.png") . "' title='" . $label . "'>
<span>" . $label . "</span>
</a>"
);
}
else
{
$module_buttons = array();
}
}
else
{
$module_buttons = array();
}
?>

View file

@ -1,5 +1,5 @@
<navigation>
<page key="main" file="main.php" access="user,admin" />
<page key="uninstall" file="uninstall.php" access="user,admin" />
<page key="workshop_admin" file="workshop_admin.php" access="admin" />
<page key="workshop_admin" file="main.php" access="admin" />
</navigation>

View file

@ -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;
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/** @var array $home */
/** @var array $config */
/** @var array $lang */
/** @var array $adapterOptions */
$homeName = htmlspecialchars($home['home_name'] ?? ('#' . $home['home_id']));
$homeId = (int)$home['home_id'];
?>
<div class="sw-admin sw-edit">
<p><a href="?m=steam_workshop&amp;p=main">&larr; <?php echo htmlspecialchars($lang['button_cancel']); ?></a></p>
<h3><?php echo htmlspecialchars(sprintf($lang['heading_edit_home'], $homeName)); ?></h3>
<form method="post" action="?m=steam_workshop&amp;p=main&amp;action=save" class="sw-form">
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>" />
<?php $formConfig = $config; include __DIR__ . '/partials/form_fields.php'; ?>
<div class="sw-form__actions">
<button class="btn primary" type="submit"><?php echo htmlspecialchars($lang['button_save']); ?></button>
<a class="btn" href="?m=steam_workshop&amp;p=main"><?php echo htmlspecialchars($lang['button_cancel']); ?></a>
</div>
</form>
<?php include __DIR__ . '/partials/mod_table.php'; ?>
</div>

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/** @var array $records */
/** @var array $lang */
/** @var bool $isAdmin */
/** @var array $adapterOptions */
?>
<div class="sw-admin sw-index">
<?php if (empty($records)): ?>
<div class="sw-empty">
<p><?php echo $isAdmin ? htmlspecialchars($lang['empty_state_admin']) : htmlspecialchars($lang['empty_state_user']); ?></p>
</div>
<?php else: ?>
<div class="sw-grid">
<?php foreach ($records as $record): ?>
<?php $currentRecord = $record; include __DIR__ . '/partials/server_card.php'; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>

View file

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/** @var array $formConfig */
/** @var array $adapterOptions */
/** @var array $lang */
$enabled = !empty($formConfig['workshop_enabled']);
$interval = (int)$formConfig['update_interval_minutes'];
$stagingDir = htmlspecialchars($formConfig['staging_dir']);
$postInstall = htmlspecialchars($formConfig['post_install_script']);
$rawDefinition = htmlspecialchars($formConfig['raw_definition']);
$installStrategy = $formConfig['install_strategy'];
$onUpdateAction = $formConfig['on_update_action'];
?>
<div class="sw-form__grid">
<label class="sw-toggle">
<input type="checkbox" name="workshop[workshop_enabled]" value="1" <?php echo $enabled ? 'checked' : ''; ?> />
<span><?php echo htmlspecialchars($lang['label_feature_flag']); ?></span>
</label>
<label>
<span><?php echo htmlspecialchars($lang['label_adapter']); ?></span>
<select name="workshop[adapter_key]">
<?php foreach ($adapterOptions as $key => $label): ?>
<option value="<?php echo htmlspecialchars($key); ?>" <?php echo $formConfig['adapter_key'] === $key ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($label); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label>
<span><?php echo htmlspecialchars($lang['label_interval']); ?></span>
<input type="number" min="15" max="360" step="5" name="workshop[update_interval_minutes]" value="<?php echo $interval; ?>" />
<small><?php echo htmlspecialchars($lang['label_interval_hint']); ?></small>
</label>
<label>
<span><?php echo htmlspecialchars($lang['label_staging_dir']); ?></span>
<input type="text" name="workshop[staging_dir]" value="<?php echo $stagingDir; ?>" placeholder="/home/ogp_agent/workshop-staging" />
</label>
<label>
<span><?php echo htmlspecialchars($lang['label_install_strategy']); ?></span>
<select name="workshop[install_strategy]">
<option value="copy" <?php echo $installStrategy === 'copy' ? 'selected' : ''; ?>><?php echo htmlspecialchars($lang['install_copy']); ?></option>
<option value="symlink" <?php echo $installStrategy === 'symlink' ? 'selected' : ''; ?>><?php echo htmlspecialchars($lang['install_symlink']); ?></option>
<option value="staging" <?php echo $installStrategy === 'staging' ? 'selected' : ''; ?>><?php echo htmlspecialchars($lang['install_staging']); ?></option>
</select>
</label>
<label>
<span><?php echo htmlspecialchars($lang['label_on_update_action']); ?></span>
<select name="workshop[on_update_action]">
<option value="queue_for_restart" <?php echo $onUpdateAction === 'queue_for_restart' ? 'selected' : ''; ?>><?php echo htmlspecialchars($lang['action_queue_for_restart']); ?></option>
<option value="hot_reload_if_supported" <?php echo $onUpdateAction === 'hot_reload_if_supported' ? 'selected' : ''; ?>><?php echo htmlspecialchars($lang['action_hot_reload_if_supported']); ?></option>
</select>
</label>
<label>
<span><?php echo htmlspecialchars($lang['label_post_install_script']); ?></span>
<input type="text" name="workshop[post_install_script]" value="<?php echo $postInstall; ?>" placeholder="/home/ogp_agent/scripts/workshop-hook.sh" />
</label>
</div>
<label>
<span><?php echo htmlspecialchars($lang['label_mod_import']); ?></span>
<textarea name="workshop[raw_items]" rows="8" placeholder="123456789,@Example Mod&#10;987654321,@QoL Pack"><?php echo $rawDefinition; ?></textarea>
<small><?php echo htmlspecialchars($lang['hint_mod_import']); ?></small>
</label>

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/** @var array $config */
/** @var array $lang */
$mods = $config['workshop_items'] ?? [];
?>
<div class="sw-mods">
<h4><?php echo htmlspecialchars($lang['mods_table_heading']); ?></h4>
<?php if (empty($mods)): ?>
<p><?php echo htmlspecialchars($lang['mods_table_empty']); ?></p>
<?php else: ?>
<table class="table sw-mods__table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['mods_header_id']); ?></th>
<th><?php echo htmlspecialchars($lang['mods_header_label']); ?></th>
<th><?php echo htmlspecialchars($lang['mods_header_source']); ?></th>
<th><?php echo htmlspecialchars($lang['mods_header_enabled']); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($mods as $mod): ?>
<tr>
<td><?php echo htmlspecialchars($mod['id']); ?></td>
<td><?php echo htmlspecialchars($mod['label']); ?></td>
<td><?php echo htmlspecialchars($mod['source']); ?></td>
<td><?php echo !empty($mod['enabled']) ? htmlspecialchars($lang['status_enabled']) : htmlspecialchars($lang['status_disabled']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/** @var array $currentRecord */
/** @var array $lang */
$home = $currentRecord['home'];
$config = $currentRecord['config'];
$adapter = $currentRecord['adapter'];
$homeName = htmlspecialchars($home['home_name'] ?? ('#' . $home['home_id']));
$homeId = (int)($home['home_id'] ?? 0);
$modCount = count($config['workshop_items']);
$interval = (int)$config['update_interval_minutes'];
$enabled = !empty($config['workshop_enabled']);
$lastSaved = $config['last_saved_at'] ? date('Y-m-d H:i', (int)$config['last_saved_at']) : '—';
$adapterName = htmlspecialchars($adapter['name'] ?? strtoupper($config['adapter_key']));
$hotReload = !empty($adapter['supports_hot_reload']);
$ip = htmlspecialchars($home['ip'] ?? '');
$port = $home['port'] ?? '';
$address = $ip;
if ($ip !== '' && $port !== '') {
$address .= ':' . htmlspecialchars((string)$port);
}
?>
<div class="sw-card">
<div class="sw-card__header">
<div>
<h3><?php echo $homeName; ?></h3>
<?php if ($address !== ''): ?>
<p><?php echo $address; ?></p>
<?php endif; ?>
</div>
<div>
<a class="btn" href="?m=steam_workshop&amp;p=main&amp;action=edit&amp;home_id=<?php echo $homeId; ?>">
<?php echo htmlspecialchars($lang['button_edit']); ?>
</a>
</div>
</div>
<dl class="sw-card__meta">
<div>
<dt><?php echo htmlspecialchars($lang['summary_adapter']); ?></dt>
<dd><?php echo $adapterName; ?></dd>
</div>
<div>
<dt><?php echo htmlspecialchars($lang['summary_interval']); ?></dt>
<dd><?php echo $interval; ?> min</dd>
</div>
<div>
<dt><?php echo htmlspecialchars($lang['summary_mods']); ?></dt>
<dd><?php echo $modCount; ?></dd>
</div>
<div>
<dt><?php echo htmlspecialchars($lang['summary_last_saved']); ?></dt>
<dd><?php echo htmlspecialchars($lastSaved); ?></dd>
</div>
<div>
<dt>Status</dt>
<dd><?php echo $enabled ? htmlspecialchars($lang['status_enabled']) : htmlspecialchars($lang['status_disabled']); ?></dd>
</div>
<div>
<dt><?php echo htmlspecialchars($lang['summary_hot_reload']); ?></dt>
<dd><?php echo $hotReload ? htmlspecialchars($lang['status_hot_reload']) : htmlspecialchars($lang['status_restart_required']); ?></dd>
</div>
</dl>
</div>