Merge pull request #120 from GameServerPanel/copilot/delete-old-steam-workshop-implementation

This commit is contained in:
Frank Harris 2026-05-05 07:21:39 -05:00 committed by GitHub
commit 8c7c63bb72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 2068 additions and 14237 deletions

View file

@ -1,13 +0,0 @@
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

@ -1,114 +0,0 @@
@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

@ -1,100 +0,0 @@
@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

@ -1,26 +0,0 @@
# 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,424 @@
<?php
/*
* GSP Steam Workshop: Admin profile management
* Copyright (C) 2025 WDS / GameServerPanel
*
* Accessible via: home.php?m=steam_workshop&p=admin
*
* 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
* (at your option) any later version.
*/
require_once __DIR__ . '/includes/functions.php';
// Load the XML config parser so sw_sync_profiles() can read game configs.
if (!defined('SERVER_CONFIG_LOCATION')) {
require_once __DIR__ . '/../../config_games/server_config_parser.php';
}
function exec_ogp_module()
{
global $db;
echo '<h2>Steam Workshop Admin</h2>';
$action = isset($_GET['action']) ? $_GET['action'] : '';
// ── POST: save a profile edit ─────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_profile'])) {
sw_admin_save_profile($db);
return;
}
// ── POST: sync profiles from XML configs ──────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sync_profiles'])) {
$n = sw_sync_profiles($db);
sw_success("Sync complete. $n new profile(s) created.");
}
// ── GET: show edit form for one profile ───────────────────────────
if ($action === 'edit' && isset($_GET['id'])) {
$profile = sw_get_profile_by_id($db, (int)$_GET['id']);
if ($profile) {
sw_admin_edit_form($profile);
} else {
sw_error('Profile not found.');
sw_admin_list($db);
}
return;
}
// ── Default: list all profiles ────────────────────────────────────
sw_admin_list($db);
}
// ─────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────
function sw_admin_save_profile($db)
{
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
if (!$id) {
sw_error('Invalid profile ID.');
sw_admin_list($db);
return;
}
$profile = sw_get_profile_by_id($db, $id);
if (!$profile) {
sw_error('Profile not found.');
sw_admin_list($db);
return;
}
// Collect and sanitize fields from POST.
$fields = array(
'enabled' => isset($_POST['enabled']) ? 1 : 0,
'steam_app_id' => trim($_POST['steam_app_id'] ?? ''),
'workshop_app_id' => trim($_POST['workshop_app_id'] ?? ''),
'steam_login_required' => isset($_POST['steam_login_required']) ? 1 : 0,
'steamcmd_login_mode' => $_POST['steamcmd_login_mode'] === 'account' ? 'account' : 'anonymous',
'steamcmd_path' => trim($_POST['steamcmd_path'] ?? ''),
'workshop_download_dir_template' => trim($_POST['workshop_download_dir_template'] ?? ''),
'server_root_template' => trim($_POST['server_root_template'] ?? ''),
'install_path_template' => trim($_POST['install_path_template'] ?? ''),
'folder_naming_format' => trim($_POST['folder_naming_format'] ?? ''),
'mod_launch_param_template' => trim($_POST['mod_launch_param_template'] ?? '-mod='),
'servermod_launch_param_template' => trim($_POST['servermod_launch_param_template'] ?? '-serverMod='),
'install_script_template' => trim($_POST['install_script_template'] ?? ''),
'update_script_template' => trim($_POST['update_script_template'] ?? ''),
'copy_bikeys_enabled' => isset($_POST['copy_bikeys_enabled']) ? 1 : 0,
'notes' => trim($_POST['notes'] ?? ''),
);
$set_parts = array();
foreach ($fields as $col => $val) {
$safe = $db->realEscapeSingle($val);
$set_parts[] = "`$col` = '$safe'";
}
$set_parts[] = "`updated_at` = NOW()";
$set_sql = implode(', ', $set_parts);
$ok = $db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_game_profiles`
SET $set_sql
WHERE `id` = $id LIMIT 1"
);
if ($ok) {
sw_success('Profile saved.');
} else {
sw_error('Failed to save profile.');
}
$profile = sw_get_profile_by_id($db, $id);
if ($profile) {
sw_admin_edit_form($profile);
} else {
sw_admin_list($db);
}
}
function sw_admin_list($db)
{
$profiles = sw_get_profiles($db);
?>
<p>
Each game config XML gets one Workshop profile.
Use <strong>Sync Profiles</strong> to auto-create rows for new game configs.
Enable and configure each profile to activate Steam Workshop for that game.
</p>
<form method="post" style="display:inline;">
<button type="submit" name="sync_profiles" value="1"
onclick="return confirm('Sync workshop profiles from all game config XMLs?');"
class="button">Sync Profiles from XML Configs</button>
</form>
<hr>
<?php if (empty($profiles)): ?>
<p>No profiles yet. Click <em>Sync Profiles</em> to create them from the installed game configs.</p>
<?php else: ?>
<table class="table" width="100%" style="border-collapse:collapse;">
<thead>
<tr style="background:#f0f0f0;">
<th style="padding:6px 8px;text-align:left;">Config Name</th>
<th style="padding:6px 8px;text-align:left;">Game Name</th>
<th style="padding:6px 8px;text-align:center;">Workshop App ID</th>
<th style="padding:6px 8px;text-align:center;">Enabled</th>
<th style="padding:6px 8px;text-align:center;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($profiles as $p): ?>
<tr style="border-bottom:1px solid #ddd;">
<td style="padding:6px 8px;font-family:monospace;"><?= sw_h($p['config_name']) ?></td>
<td style="padding:6px 8px;"><?= sw_h($p['game_name']) ?></td>
<td style="padding:6px 8px;text-align:center;"><?= sw_h($p['workshop_app_id']) ?></td>
<td style="padding:6px 8px;text-align:center;">
<?= $p['enabled'] ? '<span style="color:green;font-weight:bold;">Yes</span>' : '<span style="color:#999;">No</span>' ?>
</td>
<td style="padding:6px 8px;text-align:center;">
<a href="home.php?m=steam_workshop&p=admin&action=edit&id=<?= (int)$p['id'] ?>"
class="button small">Edit</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif;
}
function sw_admin_edit_form(array $profile)
{
$id = (int)$profile['id'];
?>
<p><a href="home.php?m=steam_workshop&p=admin">&laquo; Back to profile list</a></p>
<h3>Edit Profile: <?= sw_h($profile['config_name']) ?> &ndash; <?= sw_h($profile['game_name']) ?></h3>
<p style="background:#fff8dc;border:1px solid #e0d090;padding:8px 12px;border-radius:4px;">
<strong>Supported placeholders</strong> (use in path/script templates):<br>
<code>{HOME_ID}</code> &nbsp;
<code>{SERVER_ID}</code> &nbsp;
<code>{REMOTE_SERVER_ID}</code> &nbsp;
<code>{GAME_NAME}</code> &nbsp;
<code>{CONFIG_NAME}</code> &nbsp;
<code>{WORKSHOP_ID}</code> &nbsp;
<code>{MOD_NAME}</code> &nbsp;
<code>{FOLDER_NAME}</code> &nbsp;
<code>{STEAM_APP_ID}</code> &nbsp;
<code>{WORKSHOP_APP_ID}</code> &nbsp;
<code>{STEAMCMD_PATH}</code> &nbsp;
<code>{WORKSHOP_DOWNLOAD_DIR}</code> &nbsp;
<code>{SERVER_ROOT}</code> &nbsp;
<code>{INSTALL_PATH}</code> &nbsp;
<code>{MOD_FOLDER}</code>
</p>
<form method="post" action="home.php?m=steam_workshop&p=admin&action=edit&id=<?= $id ?>">
<input type="hidden" name="id" value="<?= $id ?>">
<table width="100%" style="border-collapse:collapse;">
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">General</td>
</tr>
<tr>
<td style="padding:6px 8px;width:260px;"><label>Enabled</label></td>
<td style="padding:6px 8px;">
<input type="checkbox" name="enabled" value="1"
<?= $profile['enabled'] ? 'checked' : '' ?>>
Enable Steam Workshop for this game config
</td>
</tr>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Steam / SteamCMD</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="steam_app_id">Steam App ID</label></td>
<td style="padding:6px 8px;">
<input type="text" id="steam_app_id" name="steam_app_id"
value="<?= sw_h($profile['steam_app_id']) ?>"
style="width:200px;">
<span style="color:#666;font-size:0.9em;">(e.g. 223350 for DayZ Dedicated Server)</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="workshop_app_id">Workshop App ID</label></td>
<td style="padding:6px 8px;">
<input type="text" id="workshop_app_id" name="workshop_app_id"
value="<?= sw_h($profile['workshop_app_id']) ?>"
style="width:200px;">
<span style="color:#666;font-size:0.9em;">(e.g. 221100 for DayZ Workshop content)</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="steamcmd_path">SteamCMD Path</label></td>
<td style="padding:6px 8px;">
<input type="text" id="steamcmd_path" name="steamcmd_path"
value="<?= sw_h($profile['steamcmd_path']) ?>"
style="width:480px;">
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label>Steam Login Required</label></td>
<td style="padding:6px 8px;">
<input type="checkbox" name="steam_login_required" value="1"
<?= $profile['steam_login_required'] ? 'checked' : '' ?>>
Requires authenticated Steam login (not anonymous)
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="steamcmd_login_mode">SteamCMD Login Mode</label></td>
<td style="padding:6px 8px;">
<select id="steamcmd_login_mode" name="steamcmd_login_mode">
<option value="anonymous" <?= $profile['steamcmd_login_mode'] === 'anonymous' ? 'selected' : '' ?>>anonymous</option>
<option value="account" <?= $profile['steamcmd_login_mode'] === 'account' ? 'selected' : '' ?>>account (Steam username/password needed)</option>
</select>
</td>
</tr>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Paths</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="workshop_download_dir_template">Workshop Download Dir</label></td>
<td style="padding:6px 8px;">
<input type="text" id="workshop_download_dir_template" name="workshop_download_dir_template"
value="<?= sw_h($profile['workshop_download_dir_template']) ?>"
style="width:480px;">
<br><span style="color:#666;font-size:0.9em;">
Where SteamCMD downloads mods.<br>
Example: <code>{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}</code>
</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="server_root_template">Server Root</label></td>
<td style="padding:6px 8px;">
<input type="text" id="server_root_template" name="server_root_template"
value="<?= sw_h($profile['server_root_template']) ?>"
style="width:480px;">
<br><span style="color:#666;font-size:0.9em;">
Root directory of the game server. Example: <code>/home/gameserver/servers/{HOME_ID}</code>
</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="install_path_template">Mod Install Path</label></td>
<td style="padding:6px 8px;">
<input type="text" id="install_path_template" name="install_path_template"
value="<?= sw_h($profile['install_path_template']) ?>"
style="width:480px;">
<br><span style="color:#666;font-size:0.9em;">
Where the renamed mod folder ends up. Example: <code>{SERVER_ROOT}/{MOD_FOLDER}</code>
</span>
</td>
</tr>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Folder &amp; Launch Params</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="folder_naming_format">Folder Naming Format</label></td>
<td style="padding:6px 8px;">
<input type="text" id="folder_naming_format" name="folder_naming_format"
value="<?= sw_h($profile['folder_naming_format']) ?>"
style="width:300px;">
<br><span style="color:#666;font-size:0.9em;">
Default folder name template. Common values: <code>@{MOD_NAME}</code> or <code>@{WORKSHOP_ID}</code>
</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="mod_launch_param_template">Client Mod Launch Param</label></td>
<td style="padding:6px 8px;">
<input type="text" id="mod_launch_param_template" name="mod_launch_param_template"
value="<?= sw_h($profile['mod_launch_param_template']) ?>"
style="width:200px;">
<span style="color:#666;font-size:0.9em;">Prefix for client-required mods (e.g. <code>-mod=</code>)</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label for="servermod_launch_param_template">Server-Side Mod Launch Param</label></td>
<td style="padding:6px 8px;">
<input type="text" id="servermod_launch_param_template" name="servermod_launch_param_template"
value="<?= sw_h($profile['servermod_launch_param_template']) ?>"
style="width:200px;">
<span style="color:#666;font-size:0.9em;">Prefix for server-only mods (e.g. <code>-serverMod=</code>)</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;"><label>Copy .bikey Files</label></td>
<td style="padding:6px 8px;">
<input type="checkbox" name="copy_bikeys_enabled" value="1"
<?= $profile['copy_bikeys_enabled'] ? 'checked' : '' ?>>
Copy .bikey files from mod keys/ folder into server keys/ folder
</td>
</tr>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Scripts (optional)</td>
</tr>
<tr>
<td style="padding:6px 8px;vertical-align:top;"><label for="install_script_template">Install Script Template</label></td>
<td style="padding:6px 8px;">
<textarea id="install_script_template" name="install_script_template"
rows="6" style="width:100%;font-family:monospace;"
><?= sw_h($profile['install_script_template']) ?></textarea>
<span style="color:#666;font-size:0.9em;">
Shell commands to run when installing a mod for the first time. Placeholders expanded before execution.
</span>
</td>
</tr>
<tr>
<td style="padding:6px 8px;vertical-align:top;"><label for="update_script_template">Update Script Template</label></td>
<td style="padding:6px 8px;">
<textarea id="update_script_template" name="update_script_template"
rows="6" style="width:100%;font-family:monospace;"
><?= sw_h($profile['update_script_template']) ?></textarea>
<span style="color:#666;font-size:0.9em;">
Shell commands to run when updating an already-installed mod.
</span>
</td>
</tr>
<tr>
<td colspan="2" style="background:#eee;padding:6px 8px;font-weight:bold;">Notes</td>
</tr>
<tr>
<td style="padding:6px 8px;vertical-align:top;"><label for="notes">Notes</label></td>
<td style="padding:6px 8px;">
<textarea id="notes" name="notes"
rows="4" style="width:100%;"
><?= sw_h($profile['notes']) ?></textarea>
</td>
</tr>
</table>
<p>
<button type="submit" name="save_profile" value="1" class="button">Save Profile</button>
&nbsp;
<a href="home.php?m=steam_workshop&p=admin" class="button">Cancel</a>
</p>
</form>
<hr>
<h4>DayZ Default Values (for reference)</h4>
<ul>
<li><strong>Steam App ID:</strong> 223350 (DayZ Dedicated Server)</li>
<li><strong>Workshop App ID:</strong> 221100 (DayZ Workshop)</li>
<li><strong>Workshop Download Dir:</strong> <code>{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}</code></li>
<li><strong>Folder Naming Format:</strong> <code>@{MOD_NAME}</code></li>
<li><strong>Client Mod Launch Param:</strong> <code>-mod=</code></li>
<li><strong>Server-Side Mod Launch Param:</strong> <code>-serverMod=</code></li>
<li><strong>Copy .bikey Files:</strong> Yes</li>
</ul>
<?php
}

View file

@ -0,0 +1,572 @@
<?php
/*
* GSP Steam Workshop: Agent CLI update script
* Copyright (C) 2025 WDS / GameServerPanel
*
* This file must only be run from the command line (CLI).
* Do NOT expose it through a web server.
*
* Usage:
* php agent_update_workshop.php --home-id=123
* php agent_update_workshop.php --all
*
* The script connects to the panel database, reads the list of enabled mods
* for the specified server(s), downloads/updates each mod via SteamCMD,
* copies mod folders into the server root, copies .bikey files into the
* server keys/ directory, and updates the install status in the database.
*
* 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
* (at your option) any later version.
*/
// ── Safety: CLI only ──────────────────────────────────────────────────────
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit("This script may only be run from the command line.\n");
}
// ── Bootstrap ─────────────────────────────────────────────────────────────
$panel_root = realpath(__DIR__ . '/../../..');
if (!$panel_root || !is_dir($panel_root)) {
fwrite(STDERR, "ERROR: Cannot locate panel root from " . __DIR__ . "\n");
exit(1);
}
$config_file = $panel_root . '/includes/config.inc.php';
if (!is_file($config_file)) {
fwrite(STDERR, "ERROR: Panel config not found: $config_file\n");
fwrite(STDERR, " Copy includes/config.inc.php.example to includes/config.inc.php and set credentials.\n");
exit(1);
}
require_once $config_file;
require_once $panel_root . '/includes/helpers.php';
require_once $panel_root . '/includes/database_mysqli.php';
require_once __DIR__ . '/includes/functions.php';
// ── Database connection ───────────────────────────────────────────────────
// Variables $db_host, $db_user, $db_pass, $db_name, $table_prefix, $db_type,
// $db_port come from config.inc.php (loaded above).
$db = createDatabaseConnection(
$db_type,
$db_host,
$db_user,
$db_pass,
$db_name,
$table_prefix,
isset($db_port) ? $db_port : null
);
if (!is_object($db)) {
$error_text = '';
get_db_error_text($db, $error_text);
fwrite(STDERR, "ERROR: Database connection failed: $error_text\n");
exit(1);
}
// ── Argument parsing ──────────────────────────────────────────────────────
$opts = getopt('', array('home-id:', 'all', 'dry-run'));
$do_all = array_key_exists('all', $opts);
$dry_run = array_key_exists('dry-run', $opts);
$target_id = isset($opts['home-id']) ? (int)$opts['home-id'] : 0;
if (!$do_all && !$target_id) {
fwrite(STDERR, "Usage:\n");
fwrite(STDERR, " php agent_update_workshop.php --home-id=123\n");
fwrite(STDERR, " php agent_update_workshop.php --all\n");
fwrite(STDERR, " Add --dry-run to simulate without running SteamCMD or copying files.\n");
exit(1);
}
if ($dry_run) {
echo "[DRY RUN] No files will be modified and SteamCMD will not be called.\n";
}
// ── Collect home IDs to process ───────────────────────────────────────────
if ($do_all) {
// Find all home_ids that have at least one enabled mod with a valid enabled profile.
$rows = $db->resultQuery(
"SELECT DISTINCT m.home_id
FROM `OGP_DB_PREFIXsteam_workshop_server_mods` m
JOIN `OGP_DB_PREFIXsteam_workshop_game_profiles` p ON p.id = m.profile_id
WHERE m.enabled = 1 AND p.enabled = 1"
);
$home_ids = $rows ? array_column($rows, 'home_id') : array();
} else {
$home_ids = array($target_id);
}
if (empty($home_ids)) {
echo "No servers with enabled Workshop mods found.\n";
exit(0);
}
$overall_success = true;
foreach ($home_ids as $home_id) {
$home_id = (int)$home_id;
echo "\n=== Processing home_id=$home_id ===\n";
$ok = sw_agent_process_home($db, $home_id, $dry_run);
if (!$ok) {
$overall_success = false;
echo " [WARN] One or more errors occurred for home_id=$home_id.\n";
}
}
exit($overall_success ? 0 : 1);
// ─────────────────────────────────────────────────────────────────────────
// Core logic
// ─────────────────────────────────────────────────────────────────────────
/**
* Process all enabled mods for one server home.
*
* @param OGPDatabase $db
* @param int $home_id
* @param bool $dry_run
* @return bool true if all mods processed without errors
*/
function sw_agent_process_home($db, $home_id, $dry_run)
{
// Load server info
$home = sw_get_home_info($db, $home_id);
if (!$home) {
echo " [ERROR] Server home $home_id not found in database.\n";
return false;
}
echo " Server: " . $home['home_name'] . " (game: " . $home['game_name'] . ")\n";
echo " Path: " . $home['home_path'] . "\n";
// Resolve Workshop profile via game config key
$profile = sw_get_profile_for_home($db, $home_id);
if (!$profile) {
echo " [SKIP] No enabled Workshop profile for this game type.\n";
return true;
}
echo " Profile: " . $profile['config_name'] . " (workshop_app_id=" . $profile['workshop_app_id'] . ")\n";
// Build common template variables
$server_root = sw_apply_template(
$profile['server_root_template'] ?: $home['home_path'],
sw_agent_tpl_vars($home, $profile)
);
$server_root = rtrim($server_root, '/');
// Load enabled mods, sorted by sort_order
$mods = sw_get_server_mods($db, $home_id) ?: array();
$mods = array_filter($mods, function ($m) {
return !empty($m['enabled']);
});
if (empty($mods)) {
echo " No enabled mods.\n";
return true;
}
$keys_dir = $server_root . '/keys';
if (!$dry_run && !is_dir($keys_dir)) {
@mkdir($keys_dir, 0755, true);
}
$all_ok = true;
foreach ($mods as $mod) {
$mod_id = (int)$mod['id'];
$workshop_id = $mod['workshop_id'];
echo "\n Mod: " . ($mod['mod_name'] ?: $workshop_id) . " [ID=$workshop_id]\n";
// Mark as updating
if (!$dry_run) {
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `install_status` = 'updating', `last_error` = NULL, `updated_at` = NOW()
WHERE `id` = $mod_id LIMIT 1"
);
}
// Build template vars for this mod
$folder_name = $mod['folder_name'] ?: ('@' . $workshop_id);
$tpl_vars = array_merge(
sw_agent_tpl_vars($home, $profile),
array(
'WORKSHOP_ID' => $workshop_id,
'MOD_NAME' => $mod['mod_name'] ?: $workshop_id,
'FOLDER_NAME' => $folder_name,
'MOD_FOLDER' => $folder_name,
'SERVER_ROOT' => $server_root,
'WORKSHOP_DOWNLOAD_DIR' => sw_apply_template(
$profile['workshop_download_dir_template']
?: ($server_root . '/steamapps/workshop/content/' . $profile['workshop_app_id']),
array(
'SERVER_ROOT' => $server_root,
'WORKSHOP_APP_ID' => $profile['workshop_app_id'],
'HOME_ID' => $home['home_id'],
)
),
)
);
$download_dir = $tpl_vars['WORKSHOP_DOWNLOAD_DIR'];
$mod_cache = rtrim($download_dir, '/') . '/' . $workshop_id;
// 1. Download / update via SteamCMD
$cmd_result = sw_agent_steamcmd_download($mod, $profile, $tpl_vars, $dry_run);
if (!$cmd_result['ok']) {
$err = $cmd_result['error'];
echo " [ERROR] SteamCMD failed: $err\n";
if (!$dry_run) {
$safe_err = $db->realEscapeSingle($err);
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `install_status` = 'failed', `last_error` = '$safe_err', `updated_at` = NOW()
WHERE `id` = $mod_id LIMIT 1"
);
}
$all_ok = false;
continue;
}
// 2. Copy / sync mod folder to server root
$install_path = sw_apply_template(
$profile['install_path_template'] ?: ($server_root . '/{MOD_FOLDER}'),
$tpl_vars
);
$copy_ok = sw_agent_copy_mod($mod_cache, $install_path, $dry_run);
if (!$copy_ok) {
$err = "Failed to copy mod from $mod_cache to $install_path";
echo " [ERROR] $err\n";
if (!$dry_run) {
$safe_err = $db->realEscapeSingle($err);
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `install_status` = 'failed', `last_error` = '$safe_err', `updated_at` = NOW()
WHERE `id` = $mod_id LIMIT 1"
);
}
$all_ok = false;
continue;
}
// 3. Copy .bikey files to server keys/ directory
if (!empty($profile['copy_bikeys_enabled'])) {
sw_agent_copy_bikeys($install_path, $keys_dir, $dry_run);
}
// 4. Mark as installed
if (!$dry_run) {
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `install_status` = 'installed',
`last_installed_at` = NOW(),
`last_updated_at` = NOW(),
`last_error` = NULL,
`updated_at` = NOW()
WHERE `id` = $mod_id LIMIT 1"
);
}
echo " [OK] Installed → $install_path\n";
}
// 5. Print launch parameters
$enabled_mods = array_values(array_filter(
sw_get_server_mods($db, $home_id) ?: array(),
function ($m) { return !empty($m['enabled']); }
));
$params = sw_generate_launch_params($enabled_mods, $profile);
echo "\n Generated launch parameters:\n";
if ($params['mod']) {
echo " " . $params['mod'] . "\n";
}
if ($params['servermod']) {
echo " " . $params['servermod'] . "\n";
}
if (!$params['mod'] && !$params['servermod']) {
echo " (none)\n";
}
return $all_ok;
}
/**
* Build the standard template variable map for a given home + profile.
*
* @param array $home
* @param array $profile
* @return array
*/
function sw_agent_tpl_vars(array $home, array $profile)
{
return array(
'HOME_ID' => $home['home_id'],
'SERVER_ID' => $home['home_id'],
'REMOTE_SERVER_ID' => $home['remote_server_id'],
'GAME_NAME' => $home['game_name'],
'CONFIG_NAME' => $home['game_key'],
'STEAM_APP_ID' => $profile['steam_app_id'],
'WORKSHOP_APP_ID' => $profile['workshop_app_id'],
'STEAMCMD_PATH' => $profile['steamcmd_path'],
'SERVER_ROOT' => $home['home_path'],
'INSTALL_PATH' => $home['home_path'],
);
}
/**
* Run SteamCMD to download / update a single Workshop item.
*
* Uses the profile's update_script_template if set; otherwise falls back to
* a standard anonymous or authenticated +workshop_download_item invocation.
*
* @param array $mod
* @param array $profile
* @param array $tpl_vars
* @param bool $dry_run
* @return array ['ok' => bool, 'error' => string]
*/
function sw_agent_steamcmd_download(array $mod, array $profile, array $tpl_vars, $dry_run)
{
$steamcmd = $profile['steamcmd_path'] ?: '/home/gameserver/steamcmd/steamcmd.sh';
$workshop_id = $mod['workshop_id'];
$app_id = $profile['workshop_app_id'];
$dl_dir = $tpl_vars['WORKSHOP_DOWNLOAD_DIR'];
if (!empty($profile['update_script_template'])) {
// Admin has provided a custom update script.
$script_body = sw_apply_template($profile['update_script_template'], $tpl_vars);
return sw_agent_run_script($script_body, $dry_run);
}
// Build default SteamCMD command.
// +force_install_dir is set to the parent of the workshop content so that
// SteamCMD places files in <dl_dir>/<workshop_id>/.
$parent_dir = dirname($dl_dir); // .../steamapps/workshop/content
if ($profile['steamcmd_login_mode'] === 'account') {
// When account login is required the operator must supply credentials
// in the update_script_template. We cannot safely store a password here.
return array(
'ok' => false,
'error' => "Account login is required for this profile but no update_script_template is set. "
. "Add a custom update_script_template in the admin profile that includes SteamCMD login credentials.",
);
}
// Validate that steamcmd exists
if (!$dry_run && !is_file($steamcmd) && !is_executable($steamcmd)) {
return array('ok' => false, 'error' => "SteamCMD not found or not executable: $steamcmd");
}
// Build argument list; escape each argument individually.
$args = array(
escapeshellarg($steamcmd),
'+force_install_dir', escapeshellarg($parent_dir),
'+login', 'anonymous',
'+workshop_download_item', escapeshellarg($app_id), escapeshellarg($workshop_id),
'+quit',
);
$cmd = implode(' ', $args);
echo " SteamCMD: $cmd\n";
if ($dry_run) {
echo " [DRY RUN] Skipping SteamCMD execution.\n";
return array('ok' => true, 'error' => '');
}
$output = array();
$return_var = 0;
exec($cmd . ' 2>&1', $output, $return_var);
foreach ($output as $line) {
echo " $line\n";
}
if ($return_var !== 0) {
return array(
'ok' => false,
'error' => "SteamCMD exited with code $return_var. " . implode(' ', array_slice($output, -3)),
);
}
return array('ok' => true, 'error' => '');
}
/**
* Execute a shell script body.
* The script is written to a temporary file and executed with /bin/sh.
*
* @param string $script_body
* @param bool $dry_run
* @return array ['ok' => bool, 'error' => string]
*/
function sw_agent_run_script($script_body, $dry_run)
{
if ($dry_run) {
echo " [DRY RUN] Would execute script:\n";
foreach (explode("\n", $script_body) as $line) {
echo " $line\n";
}
return array('ok' => true, 'error' => '');
}
$tmp = tempnam(sys_get_temp_dir(), 'sw_agent_');
if (!$tmp) {
return array('ok' => false, 'error' => 'Could not create temporary file for script.');
}
file_put_contents($tmp, "#!/bin/sh\nset -e\n" . $script_body);
chmod($tmp, 0700);
$output = array();
$return_var = 0;
exec('/bin/sh ' . escapeshellarg($tmp) . ' 2>&1', $output, $return_var);
@unlink($tmp);
foreach ($output as $line) {
echo " $line\n";
}
if ($return_var !== 0) {
return array(
'ok' => false,
'error' => "Script exited with code $return_var. " . implode(' ', array_slice($output, -3)),
);
}
return array('ok' => true, 'error' => '');
}
/**
* Copy (rsync-style) the downloaded mod folder into the server root.
* Uses rsync when available, falls back to recursive PHP copy.
*
* @param string $src Downloaded mod folder (e.g. .../content/221100/2863534533)
* @param string $dst Target path in server root (e.g. /servers/123/@CF)
* @param bool $dry_run
* @return bool
*/
function sw_agent_copy_mod($src, $dst, $dry_run)
{
echo " Copy: $src$dst\n";
if (!$dry_run && !is_dir($src)) {
echo " [WARN] Source directory not found: $src\n";
return false;
}
if ($dry_run) {
return true;
}
if (!is_dir($dst)) {
if (!@mkdir($dst, 0755, true)) {
return false;
}
}
// Try rsync first (preserves permissions, handles deletes cleanly).
if (sw_agent_cmd_exists('rsync')) {
$cmd = 'rsync -a --delete '
. escapeshellarg(rtrim($src, '/') . '/') . ' '
. escapeshellarg(rtrim($dst, '/') . '/') . ' 2>&1';
exec($cmd, $out, $ret);
if ($ret === 0) {
return true;
}
echo " [WARN] rsync failed (exit $ret); falling back to PHP copy.\n";
}
// PHP recursive copy fallback.
return sw_agent_recursive_copy($src, $dst);
}
/**
* Copy all .bikey files found recursively under $mod_dir/keys/ into $keys_dir.
*
* @param string $mod_dir Installed mod directory
* @param string $keys_dir Server keys directory
* @param bool $dry_run
* @return void
*/
function sw_agent_copy_bikeys($mod_dir, $keys_dir, $dry_run)
{
// Search in common key locations within the mod folder.
$search_dirs = array(
$mod_dir . '/keys',
$mod_dir . '/Keys',
$mod_dir . '/key',
$mod_dir . '/Key',
);
$found = 0;
foreach ($search_dirs as $kdir) {
if (!is_dir($kdir)) {
continue;
}
foreach (glob($kdir . '/*.bikey') as $bikey) {
$target = $keys_dir . '/' . basename($bikey);
echo " .bikey: " . basename($bikey) . "$keys_dir/\n";
if (!$dry_run) {
@copy($bikey, $target);
}
$found++;
}
}
if ($found === 0) {
echo " (no .bikey files found in mod keys/ folder)\n";
}
}
/**
* Return true if a command is available in PATH.
*
* @param string $cmd
* @return bool
*/
function sw_agent_cmd_exists($cmd)
{
$which = trim((string)shell_exec('which ' . escapeshellarg($cmd) . ' 2>/dev/null'));
return !empty($which);
}
/**
* Recursively copy directory $src into $dst (creating $dst if needed).
*
* @param string $src
* @param string $dst
* @return bool
*/
function sw_agent_recursive_copy($src, $dst)
{
$dir = @opendir($src);
if (!$dir) {
return false;
}
if (!is_dir($dst)) {
@mkdir($dst, 0755, true);
}
while (false !== ($file = readdir($dir))) {
if ($file === '.' || $file === '..') {
continue;
}
$s = $src . '/' . $file;
$d = $dst . '/' . $file;
if (is_dir($s)) {
sw_agent_recursive_copy($s, $d);
} else {
copy($s, $d);
}
}
closedir($dir);
return true;
}

View file

@ -1,57 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: $0 <appid> <query> [page] [limit]" >&2
exit 2
fi
appid="$1"
query="$2"
page="${3:-1}"
limit="${4:-0}"
if ! [[ "$appid" =~ ^[0-9]+$ ]]; then
echo "AppID must be numeric." >&2
exit 3
fi
if ! [[ "$page" =~ ^[0-9]+$ ]]; then
page=1
fi
if ! [[ "$limit" =~ ^[0-9]+$ ]]; then
limit=0
fi
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'
curl -s --compressed -A "$user_agent" \
--get "https://steamcommunity.com/workshop/browse/" \
--data-urlencode "appid=${appid}" \
--data-urlencode "browsesort=textsearch" \
--data-urlencode "section=readytouseitems" \
--data-urlencode "searchtext=${query}" \
--data-urlencode "p=${page}" \
| grep -oE 'sharedfiles/filedetails/\?id=[0-9]+' \
| sed -E 's/.*id=//' \
| sort -u \
| {
count=0
while IFS= read -r id; do
[[ -z "$id" ]] && continue
((count++))
if [[ "$limit" -gt 0 && "$count" -gt "$limit" ]]; then
break
fi
title=$(curl -s --compressed -A "$user_agent" "https://steamcommunity.com/sharedfiles/filedetails/?id=${id}" \
| tr '\n' ' ' \
| sed -nE 's/.*<title>([^<]+)<\/title>.*/\1/p' \
| sed -E 's/ - Steam (Community|Workshop).*//' \
| sed -E 's/^\s+|\s+$//g')
if [[ -z "$title" ]]; then
title="(title parse failed)"
fi
printf '%s\t%s\n' "$id" "$title"
done
}

View file

@ -1,247 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../lib/SteamWorkshopService.php';
class AdminWorkshopController
{
private SteamWorkshopService $service;
private array $lang;
private ?array $adapterFormOverride = null;
private ?string $adapterFormGameKey = null;
private array $gameGroups = [];
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);
if (!$db->isAdmin($userId)) {
print_failure($this->lang['error_admin_only'] ?? 'Admin access required.');
return;
}
echo '<link rel="stylesheet" type="text/css" href="modules/steam_workshop/steam_workshop.css" />';
$this->gameGroups = $this->service->listWorkshopGameGroups();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->processPost();
}
$mappings = $this->service->getAdapterMappings();
$adapters = $this->service->loadAdapters();
$adapterOptions = $this->service->getAdapterOptions();
$gameRows = $this->buildGameRows($mappings);
$requestedGame = $this->sanitizeGameKeyInput($_GET['adapter_game'] ?? '');
$activeGame = $this->adapterFormGameKey !== null ? $this->adapterFormGameKey : $requestedGame;
$this->render('admin/index', [
'lang' => $this->lang,
'mappings' => $mappings,
'adapters' => $adapters,
'adapterOptions' => $adapterOptions,
'gameRows' => $gameRows,
'activeGameKey' => $activeGame,
]);
}
private function processPost(): void
{
$action = $_POST['admin_action'] ?? 'save_mappings';
switch ($action) {
case 'save_adapter':
$this->processAdapterSave();
break;
case 'delete_adapter':
$this->processAdapterDelete();
break;
case 'save_mappings':
default:
$this->processSaveMappings();
break;
}
}
private function processSaveMappings(): void
{
$payload = $_POST['mapping'] ?? [];
if (!is_array($payload)) {
$payload = [];
}
$fanOut = [];
$groupIndex = $this->indexGameGroups();
foreach ((array)$payload as $groupKey => $adapterKey) {
$groupKey = (string)$groupKey;
$adapterKey = (string)$adapterKey;
if (!isset($groupIndex[$groupKey])) {
continue;
}
foreach ((array)$groupIndex[$groupKey] as $gameKey) {
$fanOut[$gameKey] = $adapterKey;
}
}
$this->service->saveAdapterMappings($fanOut);
print_success($this->lang['message_mappings_saved'] ?? 'Adapter mappings saved.');
}
private function processAdapterSave(): void
{
$gameKey = $this->sanitizeGameKeyInput($_POST['game_key'] ?? '');
if ($gameKey === '') {
print_failure($this->lang['error_game_key_required'] ?? 'Game key required.');
return;
}
$payload = $_POST['adapter'] ?? [];
if (!is_array($payload)) {
$payload = [];
}
try {
$this->service->saveGameAdapter($gameKey, $payload);
$this->propagateAdapterMapping($gameKey);
print_success($this->lang['message_adapter_saved'] ?? 'Adapter saved.');
$this->adapterFormOverride = null;
$this->adapterFormGameKey = null;
} catch (RuntimeException $e) {
$this->adapterFormGameKey = $gameKey;
$this->adapterFormOverride = [
'name' => trim((string)($payload['name'] ?? '')),
'steam_app_id' => trim((string)($payload['steam_app_id'] ?? '')),
'mods_dir' => trim((string)($payload['mods_dir'] ?? '')),
'keys_dir' => trim((string)($payload['keys_dir'] ?? '')),
'supports_hot_reload' => !empty($payload['supports_hot_reload']),
'activation_template' => trim((string)($payload['activation_template'] ?? '')),
'notes' => trim((string)($payload['notes'] ?? '')),
];
print_failure($e->getMessage());
}
}
private function processAdapterDelete(): void
{
$gameKey = $this->sanitizeGameKeyInput($_POST['game_key'] ?? '');
if ($gameKey === '') {
print_failure($this->lang['error_game_key_required'] ?? 'Game key required.');
return;
}
if ($this->service->deleteGameAdapter($gameKey)) {
$this->clearGroupMappings($gameKey);
print_success($this->lang['message_adapter_deleted'] ?? 'Adapter deleted.');
} else {
print_failure($this->lang['error_adapter_delete_failed'] ?? 'Unable to delete adapter.');
}
}
private function buildGameRows(array $mappings): array
{
$rows = [];
foreach ($this->gameGroups as $group) {
$primaryKey = isset($group['primary_game_key']) ? (string)$group['primary_game_key'] : '';
$override = ($primaryKey !== '' && $this->adapterFormGameKey === $primaryKey) ? $this->adapterFormOverride : null;
$mappingValues = [];
foreach ((array)$group['game_keys'] as $gameKey) {
if (isset($mappings[$gameKey]) && $mappings[$gameKey] !== '') {
$mappingValues[$mappings[$gameKey]] = true;
}
}
$rows[] = [
'group_key' => $group['group_key'] ?? '',
'app_id' => $group['app_id'] ?? '',
'game_name' => $group['game_name'] ?? '',
'game_keys' => $group['game_keys'] ?? [],
'primary_game_key' => $primaryKey,
'mixed_mapping' => count((array)$mappingValues) > 1,
'selected_adapter' => count((array)$mappingValues) === 1 ? array_key_first($mappingValues) : '',
'exists' => $primaryKey !== '' && $this->service->gameAdapterExists($primaryKey),
'adapter' => $primaryKey !== '' ? $this->service->getGameAdapter($primaryKey) : null,
'updated_at' => $primaryKey !== '' ? $this->service->getGameAdapterUpdatedAt($primaryKey) : null,
'form' => $this->service->getAdapterFormData($primaryKey, $override),
];
}
return $rows;
}
private function indexGameGroups(): array
{
$index = [];
foreach ($this->gameGroups as $group) {
$index[$group['group_key']] = $group['game_keys'];
}
return $index;
}
private function propagateAdapterMapping(string $primaryGameKey): void
{
foreach ($this->gameGroups as $group) {
if (!in_array($primaryGameKey, $group['game_keys'], true)) {
continue;
}
foreach ((array)$group['game_keys'] as $gameKey) {
$this->service->upsertAdapterMapping($gameKey, $primaryGameKey);
}
return;
}
$this->service->upsertAdapterMapping($primaryGameKey, $primaryGameKey);
}
private function clearGroupMappings(string $primaryGameKey): void
{
foreach ($this->gameGroups as $group) {
if (!in_array($primaryGameKey, $group['game_keys'], true)) {
continue;
}
foreach ((array)$group['game_keys'] as $gameKey) {
$this->service->removeAdapterMapping($gameKey, $primaryGameKey);
}
return;
}
$this->service->removeAdapterMapping($primaryGameKey, $primaryGameKey);
}
private function sanitizeGameKeyInput($value): string
{
$gameKey = strtolower(trim((string)$value));
$sanitized = preg_replace('/[^a-z0-9_\-.]/', '', $gameKey);
return is_string($sanitized) ? $sanitized : '';
}
private function render(string $view, array $data = []): void
{
extract($data);
$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

@ -1,276 +0,0 @@
<?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';
if ($action === 'search') {
$this->handleSearch($userId, $isAdmin);
return;
}
if ($action === 'monitor_search') {
$this->handleMonitorSearch($userId, $isAdmin);
return;
}
echo '<link rel="stylesheet" type="text/css" href="modules/steam_workshop/steam_workshop.css" />';
echo '<script src="modules/steam_workshop/steam_workshop.js" defer></script>';
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);
$adapterLocked = $this->applyGameAdapterOverride($home, $config);
$this->service->saveConfig($homeId, $config);
print_success($this->lang['message_config_saved'] ?? 'Workshop configuration saved.');
$this->renderEdit($home, $config, $isAdmin, $adapterLocked);
}
private function handleMonitorSearch(int $userId, bool $isAdmin): void
{
$homeId = isset($_GET['home_id']) ? (int)$_GET['home_id'] : 0;
$query = trim((string)($_GET['q'] ?? ''));
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$perPage = isset($_GET['per_page']) ? max(1, min(100, (int)$_GET['per_page'])) : 25;
$error = null;
$results = [];
$request = null;
$requestSummary = null;
if ($homeId <= 0) {
print_failure($this->lang['error_missing_home'] ?? 'Home ID missing.');
return;
}
$home = $this->service->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
print_failure($this->lang['error_home_not_found'] ?? 'Home not found.');
return;
}
$gameKey = (string)($home['game_key'] ?? '');
$appId = $gameKey !== '' ? $this->service->getSteamAppIdForGameKey($gameKey) : null;
if ($query !== '' && $appId !== null) {
$payload = $this->service->searchWorkshopItems($gameKey, $query, $perPage, $page);
$results = $payload['results'];
$error = $payload['error'];
$request = $payload['request'];
$requestSummary = $payload['request']['summary'] ?? null;
} elseif ($query !== '' && $appId === null) {
$error = $this->lang['error_home_not_found'] ?? 'Workshop search is unavailable for this server.';
}
$this->render('monitor_search', [
'lang' => $this->lang,
'home' => $home,
'homeId' => $homeId,
'query' => $query,
'page' => $page,
'perPage' => $perPage,
'results' => $results,
'error' => $error,
'request' => $request,
'requestSummary' => $requestSummary,
'appId' => $appId,
]);
}
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);
$adapterLocked = $this->applyGameAdapterOverride($home, $config);
$this->renderEdit($home, $config, $isAdmin, $adapterLocked);
}
private function renderIndex(int $userId, bool $isAdmin): void
{
$records = [];
$homes = $this->service->listHomesForUser($userId, $isAdmin);
foreach ((array)$homes as $home) {
$config = $this->service->loadConfig((int)$home['home_id']);
$this->applyGameAdapterOverride($home, $config);
$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, bool $adapterLocked): void
{
$gameKey = (string)($home['game_key'] ?? '');
$appId = $gameKey !== '' ? $this->service->getSteamAppIdForGameKey($gameKey) : null;
$this->render('edit', [
'lang' => $this->lang,
'home' => $home,
'config' => $config,
'isAdmin' => $isAdmin,
'adapterOptions' => $this->service->getAdapterOptions(),
'adapterLocked' => $adapterLocked,
'appId' => $appId,
]);
}
private function handleSearch(int $userId, bool $isAdmin): void
{
header('Content-Type: application/json');
$homeId = isset($_GET['home_id']) ? (int)$_GET['home_id'] : 0;
$query = trim((string)($_GET['q'] ?? ''));
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$perPage = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 12;
if ($homeId <= 0) {
echo json_encode(['ok' => false, 'error' => $this->lang['error_missing_home'] ?? 'Home ID missing.']);
return;
}
if ($query === '') {
echo json_encode(['ok' => false, 'error' => $this->lang['error_missing_query'] ?? 'Enter a search term.']);
return;
}
$home = $this->service->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
echo json_encode(['ok' => false, 'error' => $this->lang['error_home_not_found'] ?? 'Home not found.']);
return;
}
$gameKey = (string)($home['game_key'] ?? '');
if ($gameKey === '') {
echo json_encode(['ok' => false, 'error' => $this->lang['error_home_not_found'] ?? 'Home not found.']);
return;
}
$payload = $this->service->searchWorkshopItems($gameKey, $query, $perPage, $page);
$requestSummary = $payload['request']['summary'] ?? sprintf('REQUEST => %s | PARAMS => %s | HTTP => %s | TRANSPORT => %s',
(string)($payload['request']['url'] ?? ''),
http_build_query($payload['request']['params'] ?? [], '', '&'),
(string)($payload['request']['http_code'] ?? ''),
(string)($payload['request']['transport_error'] ?? 'none')
);
if ($payload['error'] !== null) {
echo json_encode([
'ok' => false,
'error' => $payload['error'],
'request' => $payload['request'],
'status' => $requestSummary,
]);
return;
}
$response = [
'ok' => true,
'results' => $payload['results'],
'pagination' => $payload['pagination'],
'request' => $payload['request'],
'status' => $requestSummary,
];
if (empty($payload['results'])) {
$response['empty'] = true;
}
echo json_encode($response);
}
private function applyGameAdapterOverride(array $home, array &$config): bool
{
$gameKey = isset($home['game_key']) ? (string)$home['game_key'] : '';
$mapped = $this->service->getAdapterKeyForGame($gameKey);
if ($mapped !== null && $mapped !== '') {
$config['adapter_key'] = $mapped;
return true;
}
return false;
}
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

@ -1,461 +0,0 @@
<?php
declare(strict_types=1);
/*
* OGP / GSP Steam Workshop
* WorkshopModController: user-facing mod management per game server.
*
* Actions (via ?action=...):
* mods show installed mods + available cached mods for a server
* install install a mod (POST: home_id, workshop_id)
* remove remove a mod (POST: home_id, workshop_id)
* toggle enable/disable (POST: home_id, workshop_id, enabled)
* load_order update load order (POST: home_id, workshop_id, load_order)
* sync sync now (POST: home_id, workshop_id)
* search JSON search (GET: home_id, q) reuses SteamWorkshopService
*/
require_once __DIR__ . '/../lib/WorkshopRepository.php';
require_once __DIR__ . '/../lib/WorkshopInstaller.php';
require_once __DIR__ . '/../lib/SteamWorkshopService.php';
class WorkshopModController
{
private WorkshopRepository $repo;
private WorkshopInstaller $installer;
private SteamWorkshopService $searchService;
private array $lang;
public function __construct(OGPDatabase $db)
{
$this->repo = new WorkshopRepository($db);
$this->installer = new WorkshopInstaller($this->repo);
$this->searchService = new SteamWorkshopService($db);
$this->lang = $this->loadLang();
}
// ------------------------------------------------------------------
// Dispatch
// ------------------------------------------------------------------
public function handle(): void
{
global $db;
$userId = (int)($_SESSION['user_id'] ?? 0);
$isAdmin = $db->isAdmin($userId);
$action = $_GET['action'] ?? 'index';
// JSON endpoint no HTML output
if ($action === 'search') {
$this->handleSearch($userId, $isAdmin);
return;
}
echo '<link rel="stylesheet" type="text/css" href="modules/steam_workshop/steam_workshop.css" />';
echo '<script src="modules/steam_workshop/steam_workshop.js" defer></script>';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$postAction = $_POST['ws_action'] ?? $action;
switch ($postAction) {
case 'install':
$this->handleInstall($userId, $isAdmin);
return;
case 'remove':
$this->handleRemove($userId, $isAdmin);
return;
case 'toggle':
$this->handleToggle($userId, $isAdmin);
return;
case 'load_order':
$this->handleLoadOrder($userId, $isAdmin);
return;
case 'sync':
$this->handleSync($userId, $isAdmin);
return;
case 'save_settings':
$this->handleSaveSettings($userId, $isAdmin);
return;
case 'queue_update':
$this->handleQueueUpdate($userId, $isAdmin);
return;
}
}
switch ($action) {
case 'mods':
$this->handleModsPage($userId, $isAdmin);
break;
default:
$this->handleIndex($userId, $isAdmin);
break;
}
}
// ------------------------------------------------------------------
// Pages
// ------------------------------------------------------------------
private function handleIndex(int $userId, bool $isAdmin): void
{
$homes = $this->getHomesForUser($userId, $isAdmin);
$records = [];
foreach ((array)$homes as $home) {
$homeId = (int)($home['home_id'] ?? 0);
$appId = $this->searchService->getSteamAppIdForGameKey((string)($home['game_key'] ?? ''));
$profile = $appId !== null ? $this->repo->getProfileByAppId($appId) : null;
$mods = $profile !== null ? $this->repo->listModsForHome($homeId) : [];
$records[] = [
'home' => $home,
'profile' => $profile,
'mods' => $mods,
];
}
$this->render('user_workshop_index', [
'lang' => $this->lang,
'records' => $records,
'isAdmin' => $isAdmin,
]);
}
private function handleModsPage(int $userId, bool $isAdmin): void
{
$homeId = (int)($_GET['home_id'] ?? 0);
if ($homeId <= 0) {
print_failure($this->lang['error_missing_home'] ?? 'Select a server first.');
$this->handleIndex($userId, $isAdmin);
return;
}
$home = $this->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
print_failure($this->lang['error_home_not_found'] ?? 'Server not found.');
$this->handleIndex($userId, $isAdmin);
return;
}
$agentId = (int)($home['remote_server_id'] ?? 0);
// Load server-level settings
$serverSettings = $this->repo->getServerSettings($homeId);
// Determine active profile: from server settings, or fall back to app-id lookup
$profile = null;
if ($serverSettings !== null && !empty($serverSettings['profile_id'])) {
$profile = $this->repo->getProfileById((int)$serverSettings['profile_id']);
}
if ($profile === null) {
$appId = $this->searchService->getSteamAppIdForGameKey((string)($home['game_key'] ?? ''));
$profile = $appId !== null ? $this->repo->getProfileByAppId($appId) : null;
}
$appId = $profile !== null ? (string)($profile['workshop_app_id'] ?? '') : null;
// All enabled profiles for the profile selector
$allProfiles = $this->repo->listProfiles(true);
$installedMods = $this->repo->listModsForHome($homeId);
$availableMods = ($profile !== null && $appId !== null)
? $this->repo->listCacheForAgent($agentId, $appId)
: [];
$this->render('user_workshop_mods', [
'lang' => $this->lang,
'home' => $home,
'homeId' => $homeId,
'profile' => $profile,
'appId' => $appId,
'installedMods' => $installedMods,
'availableMods' => $availableMods,
'serverSettings' => $serverSettings ?? [],
'allProfiles' => $allProfiles,
'isAdmin' => $isAdmin,
]);
}
// ------------------------------------------------------------------
// AJAX / POST actions
// ------------------------------------------------------------------
private function handleInstall(int $userId, bool $isAdmin): void
{
$homeId = (int)($_POST['home_id'] ?? 0);
$workshopId = preg_replace('/[^0-9]/', '', (string)($_POST['workshop_id'] ?? '')) ?? '';
if ($homeId <= 0 || $workshopId === '') {
print_failure($this->lang['error_missing_params'] ?? 'Missing home or workshop ID.');
$this->handleIndex($userId, $isAdmin);
return;
}
$home = $this->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
print_failure($this->lang['error_home_not_found'] ?? 'Server not found or access denied.');
$this->handleIndex($userId, $isAdmin);
return;
}
$appId = $this->searchService->getSteamAppIdForGameKey((string)($home['game_key'] ?? ''));
$profile = $appId !== null ? $this->repo->getProfileByAppId($appId) : null;
if ($profile === null) {
print_failure($this->lang['error_no_profile'] ?? 'No Workshop profile configured for this game.');
$this->handleModsPage($userId, $isAdmin);
return;
}
$result = $this->installer->install($home, $profile, $workshopId);
if ($result['success']) {
$msg = $this->lang['mod_installed'] ?? 'Mod installed successfully.';
if (!empty($result['restart_required'])) {
$msg .= ' ' . ($this->lang['restart_required'] ?? 'A server restart is required to activate this mod.');
}
print_success($msg);
} else {
print_failure(($this->lang['mod_install_error'] ?? 'Install failed: ') . $result['message']);
}
$_GET['home_id'] = $homeId;
$this->handleModsPage($userId, $isAdmin);
}
private function handleRemove(int $userId, bool $isAdmin): void
{
$homeId = (int)($_POST['home_id'] ?? 0);
$workshopId = preg_replace('/[^0-9]/', '', (string)($_POST['workshop_id'] ?? '')) ?? '';
if ($homeId <= 0 || $workshopId === '') {
print_failure($this->lang['error_missing_params'] ?? 'Missing parameters.');
$this->handleIndex($userId, $isAdmin);
return;
}
$home = $this->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
print_failure($this->lang['error_home_not_found'] ?? 'Server not found.');
$this->handleIndex($userId, $isAdmin);
return;
}
if ($this->repo->removeMod($homeId, $workshopId)) {
print_success($this->lang['mod_removed'] ?? 'Mod removed.');
} else {
print_failure($this->lang['mod_remove_error'] ?? 'Failed to remove mod.');
}
$_GET['home_id'] = $homeId;
$this->handleModsPage($userId, $isAdmin);
}
private function handleToggle(int $userId, bool $isAdmin): void
{
$homeId = (int)($_POST['home_id'] ?? 0);
$workshopId = preg_replace('/[^0-9]/', '', (string)($_POST['workshop_id'] ?? '')) ?? '';
$enabled = !empty($_POST['enabled']);
if ($homeId <= 0 || $workshopId === '') {
print_failure($this->lang['error_missing_params'] ?? 'Missing parameters.');
$this->handleIndex($userId, $isAdmin);
return;
}
$home = $this->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
print_failure($this->lang['error_home_not_found'] ?? 'Server not found.');
$this->handleIndex($userId, $isAdmin);
return;
}
$ok = $this->repo->toggleMod($homeId, $workshopId, $enabled);
if (!$ok) {
print_failure($this->lang['error_toggle_failed'] ?? 'Failed to update mod status.');
}
$_GET['home_id'] = $homeId;
$this->handleModsPage($userId, $isAdmin);
}
private function handleLoadOrder(int $userId, bool $isAdmin): void
{
$homeId = (int)($_POST['home_id'] ?? 0);
$workshopId = preg_replace('/[^0-9]/', '', (string)($_POST['workshop_id'] ?? '')) ?? '';
$order = (int)($_POST['load_order'] ?? 0);
if ($homeId <= 0 || $workshopId === '') {
print_failure($this->lang['error_missing_params'] ?? 'Missing parameters.');
$this->handleIndex($userId, $isAdmin);
return;
}
$home = $this->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
print_failure($this->lang['error_home_not_found'] ?? 'Server not found.');
$this->handleIndex($userId, $isAdmin);
return;
}
$ok = $this->repo->updateLoadOrder($homeId, $workshopId, $order);
if (!$ok) {
print_failure($this->lang['error_order_failed'] ?? 'Failed to update load order.');
}
$_GET['home_id'] = $homeId;
$this->handleModsPage($userId, $isAdmin);
}
private function handleSync(int $userId, bool $isAdmin): void
{
$homeId = (int)($_POST['home_id'] ?? 0);
$workshopId = preg_replace('/[^0-9]/', '', (string)($_POST['workshop_id'] ?? '')) ?? '';
if ($homeId <= 0 || $workshopId === '') {
print_failure($this->lang['error_missing_params'] ?? 'Missing parameters.');
$this->handleIndex($userId, $isAdmin);
return;
}
$home = $this->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
print_failure($this->lang['error_home_not_found'] ?? 'Server not found.');
$this->handleIndex($userId, $isAdmin);
return;
}
$modRow = $this->repo->getServerMod($homeId, $workshopId);
$profile = $modRow !== null ? $this->repo->getProfileById((int)$modRow['profile_id']) : null;
if ($modRow === null || $profile === null) {
print_failure($this->lang['error_mod_not_found'] ?? 'Mod or profile not found.');
} else {
$result = $this->installer->syncMod($home, $modRow, $profile);
if ($result['success']) {
print_success($result['changed']
? ($this->lang['sync_success'] ?? 'Mod synced successfully.')
: ($this->lang['sync_no_change'] ?? 'Mod is already up to date.'));
} else {
print_failure(($this->lang['sync_error'] ?? 'Sync failed: ') . $result['message']);
}
}
$_GET['home_id'] = $homeId;
$this->handleModsPage($userId, $isAdmin);
}
private function handleSearch(int $userId, bool $isAdmin): void
{
header('Content-Type: application/json');
$homeId = (int)($_GET['home_id'] ?? 0);
$query = trim((string)($_GET['q'] ?? ''));
if ($homeId <= 0 || $query === '') {
echo json_encode(['ok' => false, 'error' => 'Missing parameters.']);
return;
}
$home = $this->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
echo json_encode(['ok' => false, 'error' => 'Server not found.']);
return;
}
$gameKey = (string)($home['game_key'] ?? '');
$payload = $this->searchService->searchWorkshopItems($gameKey, $query, 12, 1);
if ($payload['error'] !== null) {
echo json_encode(['ok' => false, 'error' => $payload['error']]);
return;
}
echo json_encode(['ok' => true, 'results' => $payload['results'], 'pagination' => $payload['pagination']]);
}
private function handleSaveSettings(int $userId, bool $isAdmin): void
{
$homeId = (int)($_POST['home_id'] ?? 0);
if ($homeId <= 0) {
print_failure($this->lang['error_missing_home'] ?? 'Select a server first.');
$this->handleIndex($userId, $isAdmin);
return;
}
$home = $this->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
print_failure($this->lang['error_home_not_found'] ?? 'Server not found.');
$this->handleIndex($userId, $isAdmin);
return;
}
$this->repo->saveServerSettings($homeId, [
'workshop_enabled' => !empty($_POST['workshop_enabled']) ? 1 : 0,
'profile_id' => (int)($_POST['profile_id'] ?? 0),
'update_mode' => $_POST['update_mode'] ?? 'manual',
'restart_behavior' => $_POST['restart_behavior'] ?? 'none',
]);
print_success($this->lang['settings_saved'] ?? 'Workshop settings saved.');
$_GET['home_id'] = $homeId;
$this->handleModsPage($userId, $isAdmin);
}
private function handleQueueUpdate(int $userId, bool $isAdmin): void
{
$homeId = (int)($_POST['home_id'] ?? 0);
if ($homeId <= 0) {
print_failure($this->lang['error_missing_home'] ?? 'Select a server first.');
$this->handleIndex($userId, $isAdmin);
return;
}
$home = $this->getHome($homeId, $userId, $isAdmin);
if ($home === null) {
print_failure($this->lang['error_home_not_found'] ?? 'Server not found.');
$this->handleIndex($userId, $isAdmin);
return;
}
$this->repo->setUpdateQueued($homeId, true);
print_success($this->lang['update_queued'] ?? 'Manual update queued. It will run on the next scheduler cycle.');
$_GET['home_id'] = $homeId;
$this->handleModsPage($userId, $isAdmin);
}
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
/** @return array<int,array<string,mixed>> */
private function getHomesForUser(int $userId, bool $isAdmin): array
{
global $db;
$accessType = $isAdmin ? 'admin' : 'user_and_group';
$homes = $db->getHomesFor($accessType, $userId);
return is_array($homes) ? array_values($homes) : [];
}
private function getHome(int $homeId, int $userId, bool $isAdmin): ?array
{
global $db;
$row = $isAdmin ? $db->getGameHome($homeId) : $db->getUserGameHome($userId, $homeId);
return is_array($row) ? $row : null;
}
private function render(string $view, array $data = []): void
{
extract($data);
require __DIR__ . '/../views/' . $view . '.php';
}
private function loadLang(): array
{
$file = __DIR__ . '/../lang/en_US.php';
if (is_file($file)) {
$strings = require $file;
if (is_array($strings)) {
return $strings;
}
}
return [];
}
}

View file

@ -1,267 +0,0 @@
<?php
declare(strict_types=1);
/*
* OGP / GSP Steam Workshop
* WorkshopProfileController: admin CRUD for Workshop game profiles
* (gsp_workshop_game_profiles table).
*
* Routed via workshop_admin.php:
* ?m=steam_workshop&p=workshop_admin&sw_action=profiles list
* ?m=steam_workshop&p=workshop_admin&sw_action=profile_form create/edit
* POST sw_action=profile_save save
* POST sw_action=profile_delete delete
*/
require_once __DIR__ . '/../lib/WorkshopRepository.php';
class WorkshopProfileController
{
private WorkshopRepository $repo;
private array $lang;
public function __construct(OGPDatabase $db)
{
$this->repo = new WorkshopRepository($db);
$this->lang = $this->loadLang();
}
// ------------------------------------------------------------------
// Dispatch
// ------------------------------------------------------------------
public function handle(): void
{
global $db;
$userId = (int)($_SESSION['user_id'] ?? 0);
if (!$db->isAdmin($userId)) {
print_failure($this->lang['error_admin_only'] ?? 'Administrator access required.');
return;
}
echo '<link rel="stylesheet" type="text/css" href="modules/steam_workshop/steam_workshop.css" />';
$action = $_GET['sw_action'] ?? 'list';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$postAction = $_POST['sw_action'] ?? '';
switch ($postAction) {
case 'profile_save':
$this->handleSave();
return;
case 'profile_delete':
$this->handleDelete();
return;
}
}
switch ($action) {
case 'config_form':
case 'profile_form':
$this->handleForm((int)($_GET['profile_id'] ?? 0));
break;
default:
$this->handleList();
break;
}
}
// ------------------------------------------------------------------
// Actions
// ------------------------------------------------------------------
private function handleList(): void
{
$profiles = $this->repo->listProfiles();
$this->render('admin/profiles', [
'lang' => $this->lang,
'profiles' => $profiles,
]);
}
private function handleForm(int $profileId): void
{
$profile = $profileId > 0 ? $this->repo->getProfileById($profileId) : null;
$this->render('admin/profile_form', [
'lang' => $this->lang,
'profile' => $profile,
'profileId' => $profileId,
]);
}
private function handleSave(): void
{
$id = (int)($_POST['profile_id'] ?? 0);
$data = $this->extractProfileData($_POST);
$errors = $this->validateProfileData($data);
if (!empty($errors)) {
foreach ($errors as $err) {
print_failure($err);
}
$profile = $id > 0 ? $this->repo->getProfileById($id) : null;
$this->render('admin/profile_form', [
'lang' => $this->lang,
'profile' => array_merge($profile ?? [], $data, ['id' => $id]),
'profileId' => $id,
]);
return;
}
$data['id'] = $id;
$savedId = $this->repo->saveProfile($data);
if ($savedId > 0) {
print_success($this->lang['profile_saved'] ?? 'Workshop profile saved.');
} else {
print_failure($this->lang['profile_save_error'] ?? 'Failed to save Workshop profile.');
}
$this->handleList();
}
private function handleDelete(): void
{
$id = (int)($_POST['profile_id'] ?? 0);
if ($id <= 0) {
print_failure($this->lang['profile_not_found'] ?? 'Profile not found.');
$this->handleList();
return;
}
if ($this->repo->deleteProfile($id)) {
print_success($this->lang['profile_deleted'] ?? 'Workshop profile deleted.');
} else {
print_failure($this->lang['profile_delete_error'] ?? 'Failed to delete Workshop profile.');
}
$this->handleList();
}
// ------------------------------------------------------------------
// Input helpers
// ------------------------------------------------------------------
/**
* @param array<string,mixed> $post
* @return array<string,mixed>
*/
private function extractProfileData(array $post): array
{
// supported_os can be multiple values (SET type)
$osRaw = $post['supported_os'] ?? [];
if (!is_array($osRaw)) {
$osRaw = [$osRaw];
}
$allowedOs = ['linux', 'windows'];
$osValues = array_values(array_intersect($osRaw, $allowedOs));
$supportedOs = implode(',', $osValues !== [] ? $osValues : ['linux']);
$allowedCopyMethods = ['copy', 'rsync', 'symlink'];
$copyMethod = in_array($post['copy_method'] ?? '', $allowedCopyMethods, true)
? (string)$post['copy_method']
: 'rsync';
$allowedLoginModes = ['anonymous', 'account'];
$steamcmdLoginMode = in_array($post['steamcmd_login_mode'] ?? '', $allowedLoginModes, true)
? (string)$post['steamcmd_login_mode']
: 'anonymous';
$allowedFolderFormats = ['@%mod_name%', '@%workshop_id%', 'custom'];
$folderNamingFormat = in_array($post['folder_naming_format'] ?? '', $allowedFolderFormats, true)
? (string)$post['folder_naming_format']
: '@%workshop_id%';
$allowedSeparators = ['semicolon', 'comma', 'space'];
$modSeparator = in_array($post['mod_separator'] ?? '', $allowedSeparators, true)
? (string)$post['mod_separator']
: 'semicolon';
// When folder naming is preset (@%mod_name% or @%workshop_id%), derive template from format.
// When 'custom', use the admin-supplied value.
$folderNameTemplate = $folderNamingFormat !== 'custom'
? $folderNamingFormat
: trim((string)($post['folder_name_template'] ?? '@%workshop_id%'));
return [
'game_key' => trim((string)($post['game_key'] ?? '')),
'game_name' => trim((string)($post['game_name'] ?? '')),
'steam_app_id' => preg_replace('/[^0-9]/', '', (string)($post['steam_app_id'] ?? '')) ?? '',
'workshop_app_id' => preg_replace('/[^0-9]/', '', (string)($post['workshop_app_id'] ?? '')) ?? '',
'steam_login_required' => !empty($post['steam_login_required']) ? 1 : 0,
'steamcmd_login_mode' => $steamcmdLoginMode,
'steamcmd_path' => trim((string)($post['steamcmd_path'] ?? '')),
'supported_os' => $supportedOs,
'cache_path_template' => trim((string)($post['cache_path_template'] ?? '')),
'install_path_template' => trim((string)($post['install_path_template'] ?? '')),
'folder_naming_format' => $folderNamingFormat,
'folder_name_template' => $folderNameTemplate,
'mod_launch_param' => trim((string)($post['mod_launch_param'] ?? '')),
'mod_separator' => $modSeparator,
'copy_method' => $copyMethod,
'copy_keys' => !empty($post['copy_keys']) ? 1 : 0,
'key_source_path' => trim((string)($post['key_source_path'] ?? '')),
'key_dest_path' => trim((string)($post['key_dest_path'] ?? '')),
'pre_update_script' => trim((string)($post['pre_update_script'] ?? '')),
'install_script' => trim((string)($post['install_script'] ?? '')),
'post_update_script' => trim((string)($post['post_update_script'] ?? '')),
'config_file_template' => trim((string)($post['config_file_template'] ?? '')),
'launch_param_template' => trim((string)($post['launch_param_template'] ?? '')),
'requires_restart' => !empty($post['requires_restart']) ? 1 : 0,
'validation_notes' => trim((string)($post['validation_notes'] ?? '')),
'enabled' => !empty($post['enabled']) ? 1 : 0,
];
}
/**
* @param array<string,mixed> $data
* @return list<string>
*/
private function validateProfileData(array $data): array
{
$errors = [];
if (($data['game_key'] ?? '') === '') {
$errors[] = $this->lang['error_game_key_required'] ?? 'Game key is required.';
} elseif (!preg_match('/^[a-z0-9_\-.]+$/i', (string)$data['game_key'])) {
$errors[] = $this->lang['error_game_key_invalid'] ?? 'Game key may only contain letters, digits, underscores, dots, and hyphens.';
}
if (($data['game_name'] ?? '') === '') {
$errors[] = $this->lang['error_game_name_required'] ?? 'Game name is required.';
}
if (($data['workshop_app_id'] ?? '') === '') {
$errors[] = $this->lang['error_app_id_required'] ?? 'Workshop App ID is required.';
}
if (($data['cache_path_template'] ?? '') === '') {
$errors[] = $this->lang['error_cache_path_required'] ?? 'SteamCMD cache path template is required.';
}
if (($data['install_path_template'] ?? '') === '') {
$errors[] = $this->lang['error_install_path_required'] ?? 'Server install path template is required.';
}
if (($data['folder_naming_format'] ?? '') === 'custom' && ($data['folder_name_template'] ?? '') === '') {
$errors[] = $this->lang['error_folder_template_required'] ?? 'Custom folder name template is required when format is set to custom.';
}
return $errors;
}
// ------------------------------------------------------------------
// Rendering
// ------------------------------------------------------------------
private function render(string $view, array $data = []): void
{
extract($data);
require __DIR__ . '/../views/' . $view . '.php';
}
private function loadLang(): array
{
$file = __DIR__ . '/../lang/en_US.php';
if (is_file($file)) {
$strings = require $file;
if (is_array($strings)) {
return $strings;
}
}
return [];
}
}

View file

@ -1,178 +0,0 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/*
* OGP / GSP Steam Workshop cron update script
*
* Usage:
* php modules/steam_workshop/cron_update.php --all
* php modules/steam_workshop/cron_update.php --agent-id=<ID>
* php modules/steam_workshop/cron_update.php --home-id=<ID>
* php modules/steam_workshop/cron_update.php --profile-id=<ID>
* php modules/steam_workshop/cron_update.php --workshop-id=<WID> --agent-id=<AID> --app-id=<APPID>
*
* This script:
* 1. Finds enabled installed mods from gsp_server_workshop_mods.
* 2. Groups them by agent_id and workshop_app_id.
* 3. For each unique (agent, appid, workshop_id), runs SteamCMD
* workshop_download_item validate on the agent.
* 4. Updates gsp_workshop_cache (status, last_checked, last_updated, last_error).
* 5. Does NOT copy into running servers.
* 6. Does NOT restart servers.
* 7. Logs all update attempts.
*
* Run from the panel root directory:
* cd /var/www/html && php modules/steam_workshop/cron_update.php --all
*/
// -----------------------------------------------------------------------
// Bootstrap: load panel includes
// -----------------------------------------------------------------------
// Determine panel root
$panelRoot = defined('PANEL_ROOT') ? PANEL_ROOT : realpath(__DIR__ . '/../../..');
if ($panelRoot === false) {
$panelRoot = __DIR__ . '/../../..';
}
chdir($panelRoot);
// Load configuration
if (!is_file('includes/config.inc.php')) {
fwrite(STDERR, "[ERROR] Cannot locate includes/config.inc.php. Run this script from the panel root.\n");
exit(1);
}
require_once 'includes/config.inc.php';
require_once 'includes/database.php';
require_once 'includes/database_mysqli.php';
require_once 'includes/lib_remote.php';
// Connect to database
if (!isset($db_host, $db_user, $db_pass, $db_name)) {
fwrite(STDERR, "[ERROR] Database configuration variables not set.\n");
exit(1);
}
$db = new OGPDatabaseMySQL();
/** @var int|true $connResult */
$connResult = $db->connect(
$db_host,
$db_user,
$db_pass,
$db_name,
$table_prefix ?? 'gsp_',
$db_port ?? null
);
if ($connResult !== true) {
fwrite(STDERR, "[ERROR] Database connection failed (code: {$connResult}).\n");
exit(1);
}
require_once __DIR__ . '/lib/WorkshopRepository.php';
require_once __DIR__ . '/lib/WorkshopInstaller.php';
require_once __DIR__ . '/lib/WorkshopUpdater.php';
$repo = new WorkshopRepository($db);
$installer = new WorkshopInstaller($repo);
$updater = new WorkshopUpdater($repo, $installer);
// -----------------------------------------------------------------------
// Parse CLI arguments
// -----------------------------------------------------------------------
$opts = getopt('', [
'all',
'agent-id:',
'home-id:',
'profile-id:',
'workshop-id:',
'app-id:',
'help',
]);
if (isset($opts['help']) || $opts === false || empty($opts)) {
echo <<<HELP
GSP Steam Workshop cron cache updater
Usage:
php cron_update.php --all
php cron_update.php --agent-id=<ID>
php cron_update.php --home-id=<ID>
php cron_update.php --profile-id=<ID>
php cron_update.php --workshop-id=<WID> --agent-id=<AID> --app-id=<APPID>
HELP;
exit(0);
}
// -----------------------------------------------------------------------
// Execute the requested update
// -----------------------------------------------------------------------
function printResults(array $results): void
{
$ok = 0;
$fail = 0;
foreach ($results as $r) {
$status = $r['success'] ? 'OK ' : 'FAIL ';
if ($r['success']) {
$ok++;
} else {
$fail++;
}
$msg = $r['message'] ?? '';
echo "[{$status}] agent={$r['agent_id']} app={$r['workshop_app_id']} mod={$r['workshop_id']} {$msg}\n";
}
echo "Done: {$ok} succeeded, {$fail} failed.\n";
}
if (isset($opts['all'])) {
echo "[INFO] Updating all enabled Workshop mods…\n";
$results = $updater->updateAll();
printResults($results);
exit(0);
}
if (isset($opts['agent-id']) && !isset($opts['workshop-id'])) {
$agentId = (int)$opts['agent-id'];
echo "[INFO] Updating Workshop mods for agent {$agentId}\n";
$results = $updater->updateWorkshopCacheForAgent($agentId);
printResults($results);
exit(0);
}
if (isset($opts['home-id'])) {
$homeId = (int)$opts['home-id'];
echo "[INFO] Updating Workshop mods for home {$homeId}\n";
$results = $updater->updateWorkshopCacheForHome($homeId);
printResults($results);
exit(0);
}
if (isset($opts['profile-id'])) {
$profileId = (int)$opts['profile-id'];
echo "[INFO] Updating Workshop mods for profile {$profileId}\n";
$results = $updater->updateWorkshopCacheForProfile($profileId);
printResults($results);
exit(0);
}
if (isset($opts['workshop-id'], $opts['agent-id'], $opts['app-id'])) {
$workshopId = preg_replace('/[^0-9]/', '', (string)$opts['workshop-id']) ?? '';
$agentId = (int)$opts['agent-id'];
$appId = preg_replace('/[^0-9]/', '', (string)$opts['app-id']) ?? '';
if ($workshopId === '' || $appId === '') {
fwrite(STDERR, "[ERROR] --workshop-id and --app-id must be numeric.\n");
exit(1);
}
echo "[INFO] Updating single mod: agent={$agentId} app={$appId} mod={$workshopId}\n";
$result = $updater->updateSingleWorkshopMod($agentId, $appId, $workshopId);
printResults([$result]);
exit(0);
}
fwrite(STDERR, "[ERROR] No valid option provided. Use --help for usage.\n");
exit(1);

View file

@ -1,219 +0,0 @@
<?php
/*
*
* 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.
*
*/
function create_drop_box_from_array_onchange($input_array,$listname,$current_value = "")
{
$only_one = count((array)$input_array) == 1;
$disabled = $only_one? "disabled=disabled":"";
$retval = "<select id=\"$listname\" name=\"$listname\" style=\"max-width:330px;\" onchange=\"this.form.submit()\" $disabled>\n";
foreach ((array)$input_array as $key => $value)
{
// Make sure we don't allow HTML or script
$key = trim(strip_tags($key));
$value = trim(strip_tags($value));
// We want to print lines with zeros, but not empty lines.
if ( empty($value) and $value !="0" )
{
continue;
}
$sel = "";
if ( $key == $current_value )
{
$sel .= "selected='selected'";
}
$retval .= "<option value='$key' $sel>$value</option>\n";
}
$retval .= "</select>\n";
return $retval;
}
function get_mod_names_list($mods_list, $xml_mods)
{
$mod_names = "";
foreach(explode(',', $mods_list) as $workshop_mod_id)
{
foreach ((array)$xml_mods as $mod)
{
if($mod['id'] == $workshop_mod_id)
{
if($mod_names != "")
$mod_names .= ",";
$mod_names .= $mod->name;
}
}
}
return $mod_names;
}
function get_mod_info($workshop_mod_id)
{
$request = http_build_query(array('itemcount' => '1', 'publishedfileids[0]' => "$workshop_mod_id"));
$context = stream_context_create
(array('http' => array
(
'method' => "POST",
'header' => "Content-type: application/x-www-form-urlencoded",
'content' => $request,
'timeout' => 5
)));
$json = @file_get_contents('http://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/', false, $context);
$response_array = json_decode($json, true);
$app_info = $response_array['response']['publishedfiledetails'][0];
return array($app_info['title'], $app_info['description'], $app_info['preview_url'], $app_info['file_url'], basename($app_info['filename']), $app_info['file_size']);
}
function belongs_to_workshop($workshop_mod_id, $workshop_id)
{
$request = http_build_query(array('itemcount' => '1', 'publishedfileids[0]' => "$workshop_mod_id"));
$context = stream_context_create
(array('http' => array
(
'method' => "POST",
'header' => "Content-type: application/x-www-form-urlencoded",
'content' => $request,
'timeout' => 5
)));
$json = @file_get_contents('http://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/', false, $context);
$response_array = json_decode($json, true);
$app_info = $response_array['response']['publishedfiledetails'][0];
if($app_info['creator_app_id'] == $workshop_id)
return true;
else
return false;
}
function get_installed_mods($home_cfg, $remote, $xml)
{
$workshop_id = $xml->workshop_id;
$config = $xml->config;
$regex = $config->regex;
$mods_backreference_index = (int)$config->mods_backreference_index;
$string_separator = stripcslashes($config->string_separator);
$filepath = $config->filepath;
$mods = $xml->mods->mod;
$full_filepath = clean_path($home_cfg['home_path']."/$filepath");
if($remote->rfile_exists($full_filepath) === 0)
return False;
if($remote->remote_readfile($full_filepath, $file_content) !== 1)
return False;
if(preg_match("/$regex/m", $file_content, $matches))
{
$full_regex_string = trim($matches[0]);
$current_mods_string = trim($matches[$mods_backreference_index]);
if($current_mods_string != '')
{
$retval = $remote->get_workshop_mods_info($mod_info_array);
$current = explode($string_separator, $current_mods_string);
$installed_mods = array();
foreach ((array)$current as $c)
{
if($c != "")
{
$mod_string = trim($c);
if($retval == "1")
$installed_mods["$mod_string"] = isset($mod_info_array["$mod_string"])?$mod_info_array["$mod_string"]:$mod_string;
else
$installed_mods["$mod_string"] = $mod_string;
}
}
return $installed_mods;
}
else
return False;
}
else
return False;
}
function remove_mod($home_cfg, $remote, $xml, $mod_string)
{
$config = $xml->config;
$regex = $config->regex;
$mods_backreference_index = (int)$config->mods_backreference_index;
$variable = $config->variable;
$string_separator = stripcslashes($config->string_separator);
$filepath = $config->filepath;
$full_filepath = $home_cfg['home_path']."/$filepath";
$mods_full_path = clean_path($home_cfg['home_path'].'/'.$xml->mods_path);
if($remote->rfile_exists($full_filepath) === 0)
return False;
$remote->remote_readfile($full_filepath, $file_content);
if(preg_match("/$regex/m", $file_content, $matches))
{
$full_regex_string = trim($matches[0]);
$current_mods_string = trim($matches[$mods_backreference_index]);
if($current_mods_string != '')
{
$current = explode($string_separator, $current_mods_string);
foreach ((array)$current as $index => $c)
{
if(trim($c) == $mod_string)
unset($current[$index]);
}
$current = array_filter($current);
$new_mods_string = implode($string_separator, $current);
$replacement = $variable.$new_mods_string;
$file_content = str_replace($full_regex_string, $replacement, $file_content);
}
else
return False;
}
else
return False;
$remote->remote_writefile($full_filepath, $file_content);
$uninstall_filepath = clean_path($mods_full_path.'/postuninstall.sh');
$uninstallcmd = str_replace('%mods_full_path%', $mods_full_path, $xml->uninstall);
$uninstallcmd = str_replace('%mod_string%', $mod_string, $uninstallcmd);
$uninstallcmd .= "\nrm -f $uninstall_filepath";
$output = "";
if($remote->remote_writefile($uninstall_filepath, $uninstallcmd) === 1)
$output .= $remote->exec("bash $uninstall_filepath");
return $output;
}
function get_blacklist()
{
return array( "232330","90","294420","251570","17515","34120","302550","42750","489650","748090","232290",
"17585","739590","17555","232370","55280","17705","261140","222840","320850","317670","824360","381690",
"41005","208050","105600","556450","402370");
}
?>

View file

@ -1,70 +0,0 @@
<workshop_settings>
<workshop_id>221100</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>.</mods_path>
<mods>
<mod id='1899391480'>
<name>Cabin_Mod</name>
<description>W2gxXUNhYmluIE1vZFsvaDFdDQoNCltiXURlc2NyaXB0aW9uOlsvYl0NCkNyYWZ0LWFibGUgTG9nIENhYmluIG1hZGUgb2YgV29vZGVuIFBsYW5rcywgTG9ncywgTmFpbHMgYW5kIFJvY2tzLg0KW2gxXVN0aWxsIFdvcmsgSW4gUHJvZ3Jlc3MgLSBzb21lIHRoaW5ncyBkbyBub3Qgd29yayBwZXJmZWN0bHkhWy9oMV0NCg0KVGhlIENhYmluIGhhcyBkZXBlbmRlbmNpZXMsIHlvdSBNVVNUIGZpcnN0IGJ1aWxkIHRoZSBmb3VuZGF0aW9uIG1hZGUgb2Ygcm9ja3MuDQpUaGVuIHRoZSBmbG9vciwgdGhlIHdhbGxzIG9uZSBhZnRlciBhbm90aGVyIChsZWZ0IHdhbGwgaXMgYWxzbyBjb25zdW1pbmcgcm9ja3MgZm9yIHRoZSBjaGltbmV5KSBhbmQgdGhlbiB0aGUgcm9vZiBhbmQgdGhlIGRvb3IgKENvZGVMb2NrIHRlc3RlZCwgQ29tYmluYXRpb25Mb2NrcyBhbHNvIHdvcmspDQoNCltiXUNyZWRpdHM6Wy9iXQ0KLSBUaGUgQ2FiaW4gTW9kIHdhcyBpbnNwaXJlZCBieSBodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PVdtWUNVbGpzckRnDQotIEhlbm5lc3N5L0Nob3BwZXIgLSBiZXN0IGJveXMgYXJvdW5kIQ0KLSAzRCBNb2RlbCBwdXJjaGFzZWQgZnJvbSBodHRwczovL3d3dy50dXJib3NxdWlkLmNvbS9GdWxsUHJldmlldy9JbmRleC5jZm0vSUQvMTE4NTUzOA0KLSBVc2luZyB0aGUgVHVyYm9TcXVpZCBSb3lhbHR5IEZyZWUgTGljZW5zZQ0KICAoaHR0cHM6Ly9ibG9nLnR1cmJvc3F1aWQuY29tL3JveWFsdHktZnJlZS1saWNlbnNlLyNnYW1lLW1vZHMpDQotIFBpY3R1cmUgbWFkZSBieSBNaW11cyA6KQ0KDQpbYl1UbyBhZGQgdGhlIG1vZCB0byB5b3VyIHNlcnZlcjpbL2JdDQpDb3B5IHRoZSBAQ2FiaW5fbW9kIGZvbGRlciB0byB5b3VyIHNlcnZlciBtb2QgZm9sZGVyDQpDb3B5IHRoZSBjb250ZW50IG9mIHRoZSBrZXlzIGZvbGRlciB0byB5b3VyIHNlcnZlciBrZXlzIGZvbGRlcg0KQ29weSBjb250ZW50IGZyb20gdGhlIHR5cGVzLnhtbCB0byB5b3VyIHNlcnZlciB0eXBlcy54bWwNCg0KW2JdTWF0ZXJpYWxzIG5lZWRlZDpbL2JdDQpDYWJpbiBLaXQgICAgICAtIDggUm9ja3MgYW5kIDUgV29vZGVuIFBsYW5rcw0KRm91bmRhdGlvbiAgLSA0MCBSb2NrcyAoc3RhY2thYmxlKQ0KRmxvb3IgICAgICAgICAgICAtIDMwIE5haWxzICYgNTAgUGxhbmtzDQpSZWFyIHdhbGwgICAgIC0gMzAgTG9ncyAmIDMwIE5haWxzDQpGcm9udCB3YWxsICAgIC0gMzAgTG9ncyAmIDMwIE5haWxzDQpSaWdodCB3YWxsICAgIC0gMzAgTG9ncyAmIDMwIE5haWxzDQpMZWZ0IHdhbGwgICAgICAtIDMwIExvZ3MgJiAzMCBOYWlscyAmIDIwIFJvY2tzDQpEb29yICAgICAgICAgICAtIDEwIE5haWxzICYgMTAgUGxhbmtzDQpSb29mICAgICAgICAgICAtIDQwIE5haWxzICYgNDAgUGxhbmtzDQoNCltiXVRvb2xzIG5lZWRlZDpbL2JdDQpTaG92ZWwgKG9ubHkgZm9yIHRoZSBmb3VuZGF0aW9uKSwgSGFtbWVyIG9yIEhhdGNoZXQgKHVubGVzcyBzZXJ2ZXIgYWRtaW5zIGRpc2FibGVkIEhhdGNoZWQgYmVpbmcgdXNlZCBhcyBIYW1tZXIpDQoNCltiXVNlcnZlci1BZG1pbnM6Wy9iXQ0KVGhpcyBtb2QgYWxsb3dzIHNlcnZlci1hZG1pbnMgdG8gYWRqdXN0IHRoZSBhbW91bnQgb2YgbWF0ZXJpYWxzIGJlaW5nIHVzZWQgZm9yIGJ1aWxkaW5nIHRoZSBjYWJpbi4NCk1vZCBicmluZ3MgYSBjYWJpbmV0IHdpdGggMTUwIHNsb3RzIGFuZCBhdHRhY2hhYmxlIGl0ZW1zIGZvciBhZGRpdGlvbmFsIHNwYWNlLg0KVGhlIENhYmluIG1vZCBtdXN0IGJlIHN0YXJ0ZWQgYWZ0ZXIgdGhlIENvZGVMb2NrIG1vZC4NCg0KW2JdUGxheWVyLWhpbnQ6Wy9iXQ0KT25jZSBjcmFmdGVkIHRoZSBUb29sYm94LCB0aGUgcGxhY2Ugd2hlcmUgeW91IHNwYXduIGl0LCBpcyB0aGUgcGxhY2Ugd2hlcmUgeW91IGhhdmUgdG8gYWRkIG1hdGVyaWFscy4NCllvdSBoYXZlIHRvIHN0YXJ0IHdpdGggdGhlIHJvY2tzIGZvciB0aGUgZm91bmRhdGlvbiENCk9uY2UgeW91IHJlYWNoZWQgdGhlIGFtb3VudCBvZiBtYXRlcmlhbHMsIHlvdSBhcmUgcmVhZHkgdG8gZ28uIENsaWNrIE5leHQgaWYgeW91IHdhbnQgdG8gc3dpdGNoIG9yZGVyIG9mIGJ1aWxkaW5nIHRoZSB3YWxscy4NCldpbmRvd3MgYXJlIG5vdCBidWxsZXQtcHJvb2YhIA0KT24gc2VydmVycyB3aXRoIGJ1aWxkLWFueXdoZXJlIHlvdSBjYW4gcGxhY2UgYSBmZW5jZSBiZWZvcmUgdGhlIHdpbmRvdy4gU2VydmVycyB3aXRob3V0Li4ud29ya2luZyBvbiBpdC4NCg0KW2JdUGVybWlzc2lvbjpbL2JdDQouLi4gaXMgZ2l2ZW4gdG8gcmVwYWNrLg0KLi4uIGlzIE5PVCBnaXZlbiBmb3IgYW55IGtpbmQgb2YgbW9uZXRhcml6YXRpb24hDQoNCltiXUtub3duIElzc3VlczogSWYgeW91IHBsYWNlIGEgc3RvcmFnZSBvYmplY3QgdG9vIGNsb3NlIHRvIHRoZSBsZWZ0L3JlYXIvZnJvbnQvcmlnaHQgd2FsbCwgeW91IGNhbiBhY2Nlc3MgdGhlIHN0b3JhZ2Ugb2JqZWN0IGZyb20gdGhlIG91dHNpZGUuIFdvcmthcm91bmQ6IEEgZmVuY2UgaW5zaWRlIHRoZSBjYWJpbiAod29ya3Mgb25seSB3aXRoIGJ1aWxkLWFueXdoZXJlIG9yIHBsYWNlIHRoZSBzdG9yYWdlIG9iamVjdCBtb3JlIGludG8gdGhlIG1pZGRsZSB1bnRpbCBpdCBpcyBub3QgdmlzaWJsZSBhbnltb3JlIGZyb20gdGhlIG91dHNpZGUuIFsvYl0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/755968752090355253/7D17BEA9A3F27C5E99936A681985E22834802AFD/</image_url>
<download_url />
<filename />
<file_size>30299992</file_size>
</mod>
<mod id='1559212036'>
<name>CF</name>
<description>VGhpcyBpcyBhIENvbW11bml0eSBmcmFtZXdvcmsgZm9yIERheVogU0EuDQoNCk9uZSBub3RhYmxlIGZlYXR1cmUgaXMgaXQgYWltcyB0byByZXNvbHZlIHRoZSBpc3N1ZSBvZiBjb25mbGljdGluZyBSUEMgdHlwZSBJRCdzIGFuZCBtb2RzLg0KDQpGb3IgaGVscCBvbiB1c2luZyB0aGlzIG1vZCBpbiB5b3VyIG93biBwcm9qZWN0cywgZm9sbG93IHRoaXMgUkVBRE1FIG9uIGdpdGh1YiB0aGUgQ29tbXVuaXR5LUZyYW1ld29yayBnaXRodWIgW3VybD1odHRwczovL2dpdGh1Yi5jb20vSmFjb2ItTWFuZ28vRGF5Wi1Db21tdW5pdHktRnJhbWV3b3JrL2Jsb2IvbWFzdGVyL1JFQURNRS5tZF1oZXJlWy91cmxdLg0KDQoNCltoMV1Nb25ldGl6YXRpb246Wy9oMV0NCg0KTW9uZXRpemF0aW9uIGlzIGFsbG93ZWQuIElmIHlvdSBkbyBtYWtlIG1vbmV5IHdoaWxlIHRoaXMgbW9kIGlzIGluc3RhbGxlZCBwbGVhc2UgZG8gY29uc2lkZXIgc2VuZGluZyBhIGRvbmF0aW9uLg0KDQpbaDFdUmVwYWNraW5nOlsvaDFdDQoNClVuZGVyIGFueSBjaXJjdW1zdGFuY2UgYXJlIHlvdSBub3QgYWxsb3dlZCB0byByZXBhY2sgdGhpcyBtb2QuIE5vIG9uZSB3aWxsIGV2ZXIgYmUgZ2l2ZW4gcGVybWlzc2lvbiB0byB1cGxvYWQgdGhpcyBtb2Qu</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/786364427281894860/A2C5EACFCB4CF0CC843F537E11A4BE1250E32C45/</image_url>
<download_url />
<filename />
<file_size>90188</file_size>
</mod>
<mod id='2048694610'>
<name>JunkYardDog</name>
<description>QWRkcyB0aGUgYWJpbGl0eSB0byBzYWx2YWdlIHBhcnRzICh1c2luZyBhIHdyZW5jaCkgYW5kIGZ1ZWwgZnJvbSB3cmVja3Mgb24gdGhlIG1hcCwgY2FuIGFsc28gc2lwaG9uIGZ1ZWwgZnJvbSBhY3RpdmUgY2Fycy4gDQpTb21lIGZ1ZWwgaXMgbG9zdCB3aGVuIHNpcGhvbmluZy4NCiBBZGRzIHRoZSBhYmlsaXR5IHRvIHJlZmlsbCBjb250YWluZXJzIGF0IHRoZSBNZWRpdW0gZnVlbCB0YW5rcyB3aXRoIGxhZGRlcnMsIGFuZCBhcyBhIHNpZGUgbm90LCB0YWtlIGNhdXRpb24gYXMgdGhleSBhbHNvIGV4cGxvZGUgd2l0aCBtb3JlIGZvcmNlIHRoYW4gdGhlIGZ1ZWwgcHVtcHMuIA0KW2JdRklYRURbL2JdOiBubyBsb25nZXIgYWJsZSB0byByZWZpbGwgY29udGFpbmVycyBhdCBydWluZWQgcHVtcHMuIA0KDQpbYl1QUk8gVElQWy9iXTogSXRzIGlzIGFkdmlzYWJsZSB0byBicmluZyB0d28gY29udGFpbmVycyB3aXRoIHlvdSB3aGVuIHNpcGhvbmluZyBmdWVsLCB5b3UgY2FuIG5vdCBzaXBob24gaW50byBhIGNvbnRhaW5lciB0aGF0IGFscmVhZHkgaGFzIGxpcXVpZCBpbiBpdC4gWW91IHdpbGwgbmVlZCB0byBlbXB0eSB0aGUgY29udGFpbmVyIG9yIHBvdXIgbGlxdWlkIGludG8gYSBsYXJnZXIgY29udGFpbmVyIHRvIHNhdmUgaXQuIEJlY2FyZWZ1bCBub3QgdG8gc3dvbGxvdyBhbnkgZnVlbCBhbmQgbWFrZSBzdXJlIHRvIHdlYXIgZ2xvdmVzIHdoZW4gc2FsdmFnaW5nIHBhcnRzLCB5b3UgZG9udCB3YW50IHRvIGdldCBidXN0ZWQga251Y2tsZXMuIA0KDQpDdXJyZW50bHkgdGhpcyBpcyBmb3IgQm90aCB2YW5pbGxhIG1hcHMNCg0KUGxlYXNlIHJlcG9ydCBhbnkgaXNzdWVzIHlvdSBmaW5kIQ0KDQpbYl1UT0RPWy9iXToNCi1NYWtlIHVuaXZlcnNhbCBmb3IgYWxsIG1hcHMuDQotQWRkIHNlcnZlciBjb25maWcgc2V0dGluZ3MgZm9yIHBhcnRzIGFycmF5DQoNCltiXU90aGVyIE1vZHM6Wy9iXQ0KDQpbdXJsPWh0dHBzOi8vc3RlYW1jb21tdW5pdHkuY29tL3NoYXJlZGZpbGVzL2ZpbGVkZXRhaWxzLz9pZD0yMDM5NDQ4MDU4XU5vTXVmZmxlIFsvdXJsXS1SZW1vdmVzIG11ZmZsZWQgdm9pY2UgZnJvbSBoZWxlbXRzIGFuZCBnYXMgbWFza3MNCg0KW3VybD1odHRwczovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9MjA0MTkwNDk3N11WZW5kaW5nU2VhcmNoIFsvdXJsXS1BZGRzIGFiaWxpdHkgdG8gc2VhcmNoIHZlbmRpbmcgbWFjaGluZXMgZm9yIGRyaW5rcw0KDQoNCltiXUNSRURJVFNbL2JdOg0KS3VyZG8gLSBncmFwaGljcyBhbmQgaW1hZ2VzIGFuZCB0ZXN0aW5nDQpNb3N0YWNob0dHIC0gaW50ZWxsZWN0dWFsIGlucHV0IGFuZCB0ZXN0aW5nDQpQbGF5ZGFjaGkgLSBpbnRlbGxlY3R1YWwgaW5wdXQgYW5kIHNjcmlwdGluZw0KDQpbYl1VU0FHRSAmIFRFUk1TWy9iXToNCi0gWW91IG1heSBub3QgcmVwYWNrIG9yIHB1Ymxpc2ggdGhpcyBtb2Qgb24gYW55IHBsYXRmb3JtIGluY2x1ZGluZyBTdGVhbS4NCg0KW2JdUEVSTUlTU0lPTiBJUyBOT1QgR1JBTlRFRCBGT1IgVEhJUyBNT0QgVE8gQkUgSU5DTFVERUQgSU4gQSAiU0VSVkVSIFBBQ0siIG9yICJNT0QgUEFDSyIuDQpVc2UgYSBDb2xsZWN0aW9uIGlmIHlvdSB3YW50IHRvIGluY2x1ZGUgdGhpcyBtb2Qgb24geW91ciBzZXJ2ZXIgZm9yIHlvdXIgdXNlcnMuWy9iXQ0KDQpDb3B5cmlnaHQgwqkgMjAyMCBbaV1aZWRtYWdbL2ldDQoNCg0KW2JdUGxlYXNlIGNvbnNpZGVyIGRvbmF0aW5nLCBUaGFuayB5b3UhWy9iXQ0KW3VybD1odHRwczovL3N0cmVhbWxhYnMuY29tL2Nvd2JveW1pbGxlcl1baW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vaUZnMFYwWC5wbmdbL2ltZ11bL3VybF0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1022822776633980236/39F0EC933CC2874160552C5A371CEE055D8792DA/</image_url>
<download_url />
<filename />
<file_size>64773</file_size>
</mod>
<mod id='2058634870'>
<name>Materials for construction</name>
<description>W2gxXU1hdGVyaWFscyBmb3IgY29uc3RydWN0aW9uWy9oMV0NCg0KW2ldTW9kIA0KK2FkZCA0IHNwYXduIHBvaW50cyBQaWxlIE9mIE1ldGFsIFNoZWV0cyAgICAgIA0KY2xhc3MgbmFtZSBmb3Igc2VydmVyIGFkbWluaXN0cmF0b3JzID0gUE9NU19QaWxlT2ZNZXRhbFBsYXRlDQpbaW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vNXczWVMxci5qcGdbL2ltZ10NCltpbWddaHR0cHM6Ly9pLmltZ3VyLmNvbS83eEZDZEtMLmpwZ1svaW1nXQ0KW2ltZ11odHRwczovL2kuaW1ndXIuY29tLzNlS1dxR3UuanBnWy9pbWddDQpbaW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vMXZmNnN4dS5qcGdbL2ltZ10NCg0KK2FkZCA0IHNwYXduIHBvaW50cyBQaWxlIE9mIE1ldGFsIFNoZWV0cyBMaXZvbmlhDQpbaW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vaG8wcWtwMi5qcGdbL2ltZ10NCltpbWddaHR0cHM6Ly9pLmltZ3VyLmNvbS9pa3VnRk9ELmpwZ1svaW1nXQ0KW2ltZ11odHRwczovL2kuaW1ndXIuY29tL0E5RzRvUkEuanBnWy9pbWddDQpbaW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vWGZONHpndy5qcGdbL2ltZ10NCi0tLS0tLS0tLS0tLS0tLS0NCg0KK2FkZHMgdG9vbHMgQ2xpcHBlciBmb3IgY3V0dGluZyBQaWxlIE9mIE1ldGFsIFNoZWV0cyBvbiBNZXRhbCBTaGVldHMgICAgICAgDQpjbGFzcyBuYW1lIGZvciBzZXJ2ZXIgYWRtaW5pc3RyYXRvcnMgPSBQT01TX01ldGFsX0NsaXBwZXINCltpbWddaHR0cHM6Ly9pLmltZ3VyLmNvbS9wY0Nkck9uLmpwZ1svaW1nXQ0KDQp0aGUgdHlwZXMgZm9sZGVyIGNvbnRhaW5zIGV2ZXJ5dGhpbmcgeW91IG5lZWQgZm9yIHNlcnZlcnNbL2ldDQoNCi0tLT09PXw9PT0tLS0NCg0KW2gxXdCc0LDRgtC10YDQuNCw0LvRiyDQtNC70Y8g0YHRgtGA0L7QuNGC0LXQu9GM0YHRgtCy0LBbL2gxXQ0KDQpbaV3QnNC+0LQgDQor0LTQvtCx0LDQstC70Y/QtdGCIDQg0YLQvtGH0LrQuCDRgdC/0LDQstC90LAg0YjRgtCw0LHQtdC70Y8g0LzQtdGC0LDQu9C70LjRh9C10YHQutC40YUg0LvQuNGB0YLQvtCyICAgICAgICAgIA0KY2xhc3MgbmFtZSDQtNC70Y8g0LDQtNC80LjQvdC40YHRgtGA0LDRgtC+0YDQvtCyINGB0LXRgNCy0LXRgNC+0LIgPSBQT01TX1BpbGVPZk1ldGFsUGxhdGUNCivQtNC+0LHQsNCy0LvRj9C10YIgNCDRgtC+0YfQutC4INGB0L/QsNCy0L3QsCDRiNGC0LDQsdC10LvRjyDQvNC10YLQsNC70LvQuNGH0LXRgdC60LjRhSDQu9C40YHRgtC+0LIgIExpdm9uaWENCi0tLS0tLS0tLS0tLS0tLS0NCg0KK9C00L7QsdCw0LLQu9GP0LXRgiDQuNC90YHRgtGA0YPQvNC10L3RgiDQmtGD0YHQsNGH0LrQuCDQtNC70Y8g0YDQtdC30LrQuCDRiNGC0LDQsdC10LvRjyDQvNC10YLQsNC70LvQuNGH0LXRgdC60LjRhSDQu9C40YHRgtC+0LIg0L3QsCDQvtCx0YvRh9C90YvQtSDQvNC10YLQsNC70Lsg0LvQuNGB0YLRiyAgICAgICAgICANCmNsYXNzIG5hbWUg0LTQu9GPINCw0LTQvNC40L3QuNGB0YLRgNCw0YLQvtGA0L7QsiDRgdC10YDQstC10YDQvtCyID0gUE9NU19NZXRhbF9DbGlwcGVyDQoNCtCyINC/0LDQv9C60LUgdHlwZXMg0L3QsNGF0L7QtNC40YLRgdGPINCy0YHQtSDQvdC10L7QsdGF0L7QtNC40LzQvtC1INC00LvRjyDRgdC10YDQstC10YDQvtCyWy9pXQ0KDQpbaV15b3UgY2FuIGFkZCB5b3VyIG93biBzcGF3biBwb2ludHMsIGFkZGluZyBjb29yZGluYXRlcyB0byB0aGUgZmlsZSBjZmdldmVudHNwYXducy54bWxbL2ldDQoNCi0tLT09PT09PS0tLQ0KDQpbaV3QstGLINC80L7QttC10YLQtSDQtNC+0LHQsNCy0LjRgtGMINGB0LLQvtC4INGB0L7QsdGB0YLQstC10L3QvdGL0LUg0YLQvtGH0LrQuCDRgdC/0LDQstC90LAsINC00L7QsdCw0LLQuNCyINC60L7QvtGA0LTQuNC90LDRgtGLINCyINGE0LDQudC7IGNmZ2V2ZW50c3Bhd25zLnhtbFsvaV0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/999179297231231643/075074238C5691636E42F5F9B26B8070C362E4E1/</image_url>
<download_url />
<filename />
<file_size>9218416</file_size>
</mod>
<mod id='1896108455'>
<name>Sector 9 Weapons</name>
<description>VGhpcyBpcyBhIG1vZCB0byBhZGQgaW4gbW9yZSB3ZWFwb25zIHRvIERheVogZm9yIHRoZSBTZWN0b3IgOSBTZXJ2ZXJzLg0KDQpXZWFwb25zIGN1cnJlbnRseSBhZGRlZDoNCk1HNDIgKEdlcm1hbiBMTUcpIHdpdGggNTBSbmQgTWFnDQpCcm93bmluZyAxOTI4IHZlcnNpb24gd2l0aCAyMFJuZCBNYWcNCg0KDQpKb2luIG91ciBkaXNjb3JkISBodHRwOi8vZGlzY29yZC5nZy9VZGdYMlVFDQoNCkpvaW4gdGhlIHNlcnZlciB2aWEgRFpTQSBMYXVuY2hlciEgSVA6W2JdMTA4LjE3OC43LjEyNjoyMzAyWy9iXQ0KDQpJIERPIE5PVCBBTExPVyBUSElTIE1PRCBUTyBCRSBVTlBBQ0tFRCBPUiBSRS1VUExPQURFRCBOT1IgVVNFRCBPTiBBTlkgT1RIRVIgU0VSVkVSIQ0KDQpQbGVhc2UgRE0gbWUgb24gZGlzY29yZCBpZiB5b3UgYXJlIHdhbnRpbmcgdG8gYXNrIGZvciBwZXJtaXNzaW9uIHRvIHVzZSB0aGVzZSB3ZWFwb25zLg==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/764976585413131037/7CC60E9F69FDCAD612FDA6FCF62AB91C0EDC432A/</image_url>
<download_url />
<filename />
<file_size>33807531</file_size>
</mod>
<mod id='1582756848'>
<name>ZomBerry Admin Tools</name>
<description>RGF5WiAxLjA3IENvbXBhdGlibGUhDQpTaW1wbGUgYW5kIGN1c3RvbWlzYWJsZSBDbGllbnQvU2VydmVyIGFkbWluIHRvb2xzIHdpdGggR1VJICh3b3JrcyBpbiBib3RoIFNQIGFuZCBNUCkNCg0KTmVlZCBoZWxwPyBEaXNjb3JkOiBodHRwczovL2Rpc2NvcmQuZ2cvQmZNWnhSaA0KDQpDaGFuZ2Vsb2c6DQp2MC41LjktcHJlcCAtIGEgdHJhbnNpdGlvbmFsIDAuNS45LzEuMCB1cGRhdGUgKHlvdSBndXlzIGFyZSBwcm9iYWJseSBib3JlZCBvZiAwLjUuOSwgaHVoPykNCg0KdjAuNS45eCAtIERheVogMS4wMiBjb21wYXRpYmlsaXR5IHVwZGF0ZQ0KDQp2MC41LjlzKyAtIFVwZGF0ZWQgUmVwYWlyIGFuZCByZWZ1ZWwgZnVuYyBmb3IgMS4wMiwgdW5iYW5uZWQgc2NvdXQgc2NvcGUsIGZpeGVkIGNvbmZpZyBsb2FkaW5nIHNlcXVlbmNlIGEgYml0DQoNCnYwLjUuOCAtIFN0ZWFtNjQgc3VwcG9ydCwgRnJlZUNhbSBhbmQgY2hhdCBmaXhlcywgc2VwYXJhdGUgbG9nIGZpbGVzLCBKU09OaXplZCBtYWluIGNvbmZpZyBmaWxlLCBjdXN0b20gZmlsdGVycyBmb3Igc3Bhd24gbWVudSAoZG9uJ3QgZm9yZ2V0IHRvIHVwZGF0ZSB5b3VyIHNlcnZlciEpDQoNCnYwLjUuNyAtIEFkZGVkIGdvZCBtb2RlLCBmaXhlZCB1cGRhdGUgbm90aWZpY2F0aW9ucw0KDQp2MC41LjYgLSBGaXhlZCBzZWFyY2ggaW5wdXQgaW4gIlNwYXduIG1lbnUiIHRhYiwgbGl0dGxlIFVJIGFkanVzdG1lbnRzDQoNCnYwLjUuNSAtIEZpeGVkIHBvc3NpYmxlIGVycm9ycyB3aGVuIGZ1bmN0aW9uIGlzIGJlaW5nIGV4ZWN1dGVkIG9uIGRpc2Nvbm5lY3RlZCBwbGF5ZXIsIHNvbWUgc2VydmVyLXNpZGUgb3B0aW1pemF0aW9ucywgYWRqdXN0ZWQgbWFwIGxvb2tzIGJhc2VkIG9uIGZlZWRiYWNrDQoNCnYwLjUuNCAtIENoYW5nZWQgZGVmYXVsdCBpdGVtIHNwYXduIHR5cGUgdG8gT25DdXJzb3IsIG1pbm9yIGZpeGVzLCAxLjEgbWFwIGZpeCAoeWVwLCBtYXAgSVMgd29ya2luZyEpDQoNCnYwLjUuMyAtIFVzZXIgZGVmaW5lZCBLZXlCaW5kcyBhZGRlZA0KDQp2MC41IC0gTWFqb3IgdW5kZXItdGhlLWhvb2QgY2hhbmdlcywgbGl0dGxlIFVJIHVwZ3JhZGVzLCBhZGRlZCAiSW5zdGFsbGF0aW9uIG1vZGUiIGZvciBpbml0aWFsIHNlcnZlciBjb25maWd1cmF0aW9uICgtemJyeUluc3RhbGxNb2RlPXRydWUgbGF1bmNoIG9wdGlvbikNCkRvbid0IGZvcmdldCB0byB1cGRhdGUgYm90aCBzZXJ2ZXItIGFuZCBjbGllbnQtIHNpZGUsIGFzIHZlcnNpb24gbWlzbWF0Y2ggbWlnaHQgY2F1c2UgcHJvYmxlbXMuDQoNCkRvY3VtZW50YXRpb24gb24gR2l0SHViOg0KW3VybD1odHRwczovL2dpdGh1Yi5jb20vTW9vbmRhcmtlci9ab21CZXJyeS1EYXlaQWRtaW5Ub29sc11HaXRIdWIgbGlua1svdXJsXQ0KDQpSZXVwbG9hZGluZyAmIHJlcGFja2luZyB0aGlzIG1vZCB3aXRob3V0IHBlcm1pc3Npb24gcmVxdWVzdCBpcyBub3QgYWxsb3dlZCBeXg0KDQpGQVEgJiBIb3cgdG8gaW5zdGFsbCBpbiBkaXNjdXNzaW9ucw==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/955224589383459935/28DDCC9D0645BCC795A7FE5B24971AE10175CC41/</image_url>
<download_url />
<filename />
<file_size>2981868</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable />
<place_after />
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>\n</string_separator>
<filepath>workshop_installed.txt</filepath>
</config>
<post_install>cp -Rf &quot;%mods_full_path%/steamapps/workshop/content/221100/%workshop_mod_id%&quot; &quot;%mods_full_path%/%workshop_mod_id%&quot;
rm -Rf &quot;%mods_full_path%/steamapps/workshop/content/221100/%workshop_mod_id%&quot;
</post_install>
<uninstall>printf &quot;\nUninstalling...\n&quot;
rm -Rf &quot;%mods_full_path%/%workshop_mod_id%&quot;</uninstall>
</workshop_settings>

File diff suppressed because one or more lines are too long

View file

@ -1,58 +0,0 @@
<workshop_settings>
<workshop_id>107410</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>.</mods_path>
<mods>
<mod id='1199493544'>
<name>ArmA 2 Anims To ArmA 3 | A2ATA3</name>
<description>W3F1b3RlXVtoMV1Bcm1BMiBBbmltcyBUbyBBcm1BMyAoQTJBVEEzKSAtIG1vZCBmb3IgQXJtQTMgdGhhdCBjaGFuZ2VzIHRoZSBtb3N0IHBhcnQgb2YgbW92ZW1lbnQgYW5pbWF0aW9ucyBpbiBBMyB0byBBMiBhbmltYXRpb25zLlsvaDFdWy9xdW90ZV0NCltjb2RlXVdhcm5pbmc6IFRoaXMgbW9kIGlzIGluIGRldmVsb3BtZW50LCBzbyBpZiB5b3Ugbm90aWNlIGFueSBwcm9ibGVtcyBqdXN0IGxldCBtZSBrbm93IDopWy9jb2RlXQ0KDQpbcXVvdGVdW2JdW2gxXUZlYXR1cmVzOlsvaDFdW2xpc3RdDQpbKl1Nb3ZlbWVudCBhbmltYXRpb25zIGluIEEzIGNoYW5nZWQgdG8gYW5pbWF0aW9ucyBmcm9tIEEyDQpbKl1Db21wbGV0ZWx5IHdvcmtpbmcsIGZ1bGx5IGZpeGVkIGFuaW1hdGlvbnNbL2xpc3RdWy9iXVsvcXVvdGVdDQoNCltjb2RlXQ0KW2NvZGVdW2JdW3VdQ1VSUkVOVCBWRVJTSU9OOiAwLjkuMi4xWy91XVsvYl1bL2NvZGVdDQpbY29kZV1bYl1bdV1MQVNUIFBBVENIIENIQU5HRUxPRzpbL3VdWy9iXQ0KW2ldI3ZlcnNpb249MC45LjIuMVsvaV0NCltFRElUXSBOZXcgbG9nby4NCltpXSN2ZXJzaW9uPTAuOS4yQVsvaV0NCi0gdXBkYXRlZCB2ZXJzaW9uIGluICRQQk9QUkVGSVgkLg0KW2ldI3ZlcnNpb249MC45LjJbL2ldDQpbTkVXXSAuYmlzaWduIHVwZGF0ZWQgdG8gdjMNCltGSVhdIEZpeGVkIGFuaW1hdGlvbiBmcmVlemUgd2hlbiBwbGF5ZXIgdHJ5aW5nIHRvIHNwcmludCB3aGlsZSBhaW1pbmcgaW4gW1BsYXllciA+IE1haW4gV2VhcG9uID4gUmFpc2VkIChBaW1pbmcpID4gU3RhbmQgPiBTcHJpbnRdLg0KW0VESVRdIEZpbGUgc3RydWN0dXJlIHJld29ya2VkLg0KW1JFTU9WRURdIFJlbW92ZWQgdXNlbGVzcyBjbGFzc2VzIGZyb20gJ0EyQVRBM1xhMmFfZGF0YVxhbmltc19jZmcuaHBwJy4NCltSRU1PVkVEXSBSZW1vdmVkICd0YWN0aWNhbCcgYW5pbWF0aW9ucyBmcm9tICdBMkFUQTNcYTJhX2FuaW1zXEFuaW0nLlsvY29kZV0NCg0KW2NvZGVdW2JdW3VdTElOS1M6Wy91XVsvYl1bbGlzdF0NClsqXVt1cmw9aHR0cHM6Ly9mb3J1bXMuYm9oZW1pYS5uZXQvZm9ydW1zL3RvcGljLzIxMTc3My1hcm1hLTItYW5pbWF0aW9ucy10by1hcm1hLTMtYTJhdGEzL11CSSBGb3J1bXMgVGhyZWFkWy91cmxdDQpbKl1bdXJsPWh0dHA6Ly93d3cuYXJtYWhvbGljLmNvbS9wYWdlLnBocD9pZD0zMzUwNl1Nb2QgcGFnZSBvbiBBcm1haG9saWNbL3VybF0NClsqXVt1cmw9aHR0cHM6Ly9naXRodWIuY29tL21heGltaWxpb251cy9BMkFUQTNdR2l0aHViWy91cmxdDQpbKl1bdXJsPWh0dHBzOi8vZ2l0aHViLmNvbS9tYXhpbWlsaW9udXMvQTJBVEEzL3Byb2plY3RzLzFdR2l0aHViIERldmVsb3BtZW50IFRyYWNrZXJbL3VybF1bL2NvZGVdDQoNCltxdW90ZV1baV1JZiB5b3UgaGF2ZSBhbnkgcXVlc3Rpb25zIGZlZWwgZnJlZSB0byBjb250YWN0IG1lIHZpYTpbL2ldDQpbdXJsPWh0dHBzOi8vd3d3LnJlZGRpdC5jb20vdXNlci9tYXhpbWlsaW9udXMvXVJlZGRpdFsvdXJsXQ0KW3VybD1odHRwczovL3R3aXR0ZXIuY29tL21heGltaWxpb251c11Ud2l0dGVyWy91cmxdDQpbdXJsPWh0dHBzOi8vdmsuY29tL21heGltaWxpb251c2NvbW1dVktbL3VybF0NClt1cmw9aHR0cHM6Ly9mb3J1bXMuYm9oZW1pYS5uZXQvcHJvZmlsZS8xMTM5MDYwLW1heGltaWxpb251cy9dQkkgRm9ydW1zWy91cmxdDQpbdXJsPWh0dHA6Ly93d3cuYXJtYWhvbGljLmNvbS91c2Vycy5waHA/bT1kZXRhaWxzJmlkPTkyNTUyJnU9bWF4aW1pbGlvbnVzXUFybWFob2xpY1svdXJsXQ0KW3VybD1odHRwOi8vc3RlYW1jb21tdW5pdHkuY29tL3Byb2ZpbGVzLzc2NTYxMTk4MDUwOTUyMTU2XVN0ZWFtWy91cmxdWy9xdW90ZV0NCltxdW90ZV1BbmQuLi4gZWhlbS4uLiBpZiB5b3Ugd2FudCB0byBzdXBwb3J0IG1lIGFuZCBteSB3b3Jrcy4uLiB3ZWxsLi4uIGhlcmVzIG15IFt1cmw9cGF5cGFsLm1lL21heGltaWxpb251c21dUGF5cGFsWy91cmxdIDopWy9xdW90ZV0NClsvY29kZV0NCltjb2RlXVt1cmw9aHR0cHM6Ly93d3cuYmlzdHVkaW8uY29tL2NvbW11bml0eS9saWNlbnNlcy9hcm1hLXB1YmxpYy1saWNlbnNlLXNoYXJlLWFsaWtlXVtpbWddaHR0cHM6Ly9pLmltZ3VyLmNvbS9MSndZdlpCLnBuZ1svaW1nXVsvdXJsXVsvY29kZV0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/776228609003078177/39522B183D24DA8D393E0D77978831FD00EA4955/</image_url>
<download_url />
<filename />
<file_size>64638590</file_size>
</mod>
<mod id='583496184'>
<name>CUP Terrains - Core</name>
<description>W2gxXUNVUCBUZXJyYWlucyAtIENvcmVbL2gxXQ0KDQpUaGUgQ29tbXVuaXR5IFVwZ3JhZGUgUHJvamVjdCBpcyBhIGNvb3BlcmF0aXZlIGVmZm9ydCB0byBicmluZyB0aGUgY29udGVudCBvZiBCb2hlbWlhIEludGVyYWN0aXZlJ3MgDQplYXJsaWVyIGdhbWVzIChBcm1hIDIgYW5kIEFybWEgMjogT3BlcmF0aW9uIEFycm93aGVhZCBhbmQgRExDJ3MgaW4gcGFydGljdWxhcikgaW50byBBcm1hIDMsIHVwZGF0ZWQgDQp0byB0aGUgZnVuY3Rpb25hbGl0eSBhbmQgc3RhbmRhcmRzIG9mIHRoZSBuZXh0IGdlbmVyYXRpb24gZ2FtZS4gDQoNCkZvciBtb3JlIGluZm9ybWF0aW9uIG9uIHRoZSBwcm9qZWN0LCBjaGVjayBvdXIgd2VicGFnZSBhdCANCmh0dHA6Ly9jdXAtYXJtYTMub3JnLyANCm9yIHZpc2l0IHVzIG9uIG91dCBkaXNjb3JkIHNlcnZlciBhdA0KaHR0cHM6Ly9kaXNjb3JkLm1lL2N1cC1hcm1hMw0KDQpUaGlzIGlzIHRoZSBURVJSQUlOUyAtIENPUkUgcGFjaywgdGhlIHN1Y2Nlc3NvciBvZiAiQTNNUCIgYW5kICJBbGwgaW4gQXJtQSAtIFRlcnJhaW4gUGFjayAoQWlBIFRQKSIuIEl0IGNvbnRhaW5zIGFsbCB0aGUgY29yZSBkYXRhIGZvciBtYXBzIGZyb20gQXJtYTEsIEFybWEgMiBhbmQgdGhlIGV4cGFuc2lvbiBhbmQgRExDJ3MuDQoNCltiXVRISVMgV09SS1NIT1AgUEFHRSBJUyBOT1QgTU9OSVRPUkVEIEJZIFRIRSBERVZFTE9QRVJTDQpQbGVhc2UgcmVwb3J0IGJ1Z3MgdG8gDQpbaV1odHRwczovL2dvby5nbC9BVXNNbnNbL2ldWy9iXQ0KDQoNClRoaXMgcGFjayBjb250YWluczoNCltsaXN0XQ0KWypdYWxsIHRlcnJhaW5zIGNvcmUgZGF0YSBsaWtlIG1vZGVscyBhbmQgY29uZmlncyBmcm9tIHByZXZpb3VzIGFybWEgdGl0bGVzDQpbKl1jb21tdW5pdHkgbWFkZSBhZGRpdGlvbmFsIGNvbnRlbnQgdGhhdCB3YXMgZG9uYXRlZCBhbmQgZml0J3MgdGhlIHRpbWVmcmFtZVsvbGlzdF0NCg0KDQoNCltxdW90ZV0NCklNUE9SVEFOVCENClRoaXMgaXMgdGhlIENPUkUgREFUQSBwYWNrLCBpdCBbYl1bdV1ET0VTIE5PVFsvdV1bL2JdIGluY2x1ZGUgYW55IG1hcHMhDQpUbyBnZXQgdGhlIG1hcHMgZnJvbSBDVVAgVGVycmFpbnMgUGFjaywgeW91IG5lZWQgdG8gZG93bmxvYWQgdGhlIE1BUFMgUEFDSw0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9NTgzNTQ0OTg3DQpbL3F1b3RlXQ0KDQoNCltxdW90ZV0NCltiXVt1XUFOWSBSRVVQTE9BRFMgKFNUQU5EQUxPTkUgT1IgUEFSVCBPRiBNT0RQQUNLUykgVE8gVEhFIFNURUFNIFdPUktTSE9QIChBUk1BMyAmIERBWVopIEFSRSBQUk9ISUJJVEVEIEFORCBWSU9MQVRJTkcgVEhFIFNURUFNIFdPUktTSE9QIEVVTEEgU0VDVElPTiA2RCwgQVMgV0VMTCBBUyBUSEUgQ1VQIExJQ0VOU0UuIFJFVVBMT0FEUyBXSUxMIEJFIFRBS0VOIERPV04gVklBIERNQ0EgTk9USUNFIFdJVEhPVVQgV0FSTklORyFbL3VdWy9iXQ0KWy9xdW90ZV0gDQo=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/954108744283705578/9057AFA885298D149510454FA270399226B50A9C/</image_url>
<download_url />
<filename />
<file_size>1032741022</file_size>
</mod>
<mod id='868032727'>
<name>DesolationREDUX</name>
<description>RGVzb2xhdGlvblJFRFVYIGlzIHRoZSBzcGlyaXR1YWwgc3VjY2Vzc29yIHRvIERlc29sYXRpb25Nb2QuIFJFRFVYIGlzIGEgbGFyZ2Ugc2NhbGUgc3Vydml2YWwgbW9kIHdoZXJlIHRoZSBwbGF5ZXIgbXVzdCBnYXRoZXIgcmVzb3VyY2VzLCBidWlsZCBhIGhvbWUsIGFuZCBkZWZlbmQgdGhlbXNlbHZlcyBmcm9tIG90aGVycyBsb29raW5nIHRvIHRha2Ugd2hhdCB0aGV5IGhhdmUuIA0KDQpXZWJzaXRlOiBbdXJsPWh0dHA6Ly9kZXNvbGF0aW9ucmVkdXguY29tXWh0dHA6Ly9kZXNvbGF0aW9ucmVkdXguY29tWy91cmxdDQpXSUtJOiBbdXJsPWh0dHA6Ly93aWtpLmRlc29sYXRpb25yZWR1eC5jb21daHR0cDovL3dpa2kuZGVzb2xhdGlvbnJlZHV4LmNvbVsvdXJsXQ0KDQpXZSBoYXZlIGluY2x1ZGVkIFRoZXN1cyBTZXJ2aWNlcyBpbnRvIG91ciBhZGRvbnM6IGh0dHBzOi8vZm9ydW1zLmJpc3R1ZGlvLmNvbS9mb3J1bXMvdG9waWMvMTg5MTY3LXRoZXNldXMtc2VydmljZXMv</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/854970836911038557/25437F8ED3570D240C6D1F5A75B5BA9D2CEDBF5D/</image_url>
<download_url />
<filename />
<file_size>1872893114</file_size>
</mod>
<mod id='612930542'>
<name>RDS Civilian Pack</name>
<description>W2JdQTIgRWFzdGVybiBUaGVtZWQgQ2l2aWxpYW4gUGFjayB3aGljaCBjb250YWlucyBkaWZmZXJlbnQgdmVoaWNsZXMgJiBjaGFyYWN0ZXJzOlsvYl0NCklrYXJ1cyAyNjANClNrb2QgMTIwMw0KU2tvZGEgT2N0YXZpYSBJSSAyLjAgVERJDQpWVyBHb2xmIElWIDEuOSBUREkgKEkga25vdyBpbiBmYWN0IGl0J3MgMS42IEZTSSBidXQgSSBsaWtlIHRoYXQgdmVyc2lvbiA6UCkNClZBWi0yMTAzDQpHQVotMjQNClpldG9yIFRyYWN0b3INCllhbWFoYSBUVC02NTAgDQpKYXdhIDM1Mw0KMiBCaWtlcyAoQTIgb2xkICYgbW91bnRhaW4gYmlrZSkNCg0KQTIgQ2l2aWxpYW5zIHdpdGggd29ya2luZyBpbnZlbnRvcnksIGV0Yy4gaS5lLiBwb2xpY2VtYW4sIHdvcmtlciwgd29vZGxhbmRlciwgZG9jdG9yLCBwcm9maXRlZXIsIGJ1c2luZXNzbWVuLCBwb3AgKHByaWVzdCkuDQoNCkFsc28sIGlmIHlvdSBmZWVsaW5nIHlvdSBoYXZlIHNvbWUgc3BhcmUgYnVja3MsIHlvdSBjYW4gbm93IG1ha2UgZG9uYXRpb24gOykNClt1cmw9aHR0cHM6Ly93d3cucGF5cGFsLmNvbS9jZ2ktYmluL3dlYnNjcj9jbWQ9X3MteGNsaWNrJmhvc3RlZF9idXR0b25faWQ9Q1hEVDI5S0daRkNDTl1baW1nXWh0dHBzOi8vd3d3LnBheXBhbG9iamVjdHMuY29tL2VuX0dCL2kvYnRuL2J0bl9kb25hdGVfTEcuZ2lmWy9pbWddWy91cmxdDQoNCg0KW2JdTGF0ZXN0IGNoYW5nZXMgWzEuMzBdOlsvYl0NCisgYWRkZWQgSmF3YSAzNTMsIFlhbWFoYSBUVC02NTAgbW90b3JjeWNsZSAmIDIgQmlrZXMgKE9sZCAmIG1vdW50YWluIGJpa2UpDQorIGFkZGVkIDIgaGFuZGhlbGQgZmxhc2hsaWdodHMgLSBKYW50YSAmIExUUy0xDQorIGFkZGVkIHNob3J0L2xvbmcgbGlnaHQgdG9nZ2xlIChyIC0gdG9nZ2xlIGxpZ2h0LCB0IC0gdG9nZ2xlIGNhYmluIGxpZ2h0KQ0KKyBhZGRlZCBlbmdpbmUgZGVzdHJ1Y3Rpb24gZWZmZWN0ICYgaW1wcm92ZWQgaGl0cG9pbnRzIG9uIGFsbCB2ZWhpY2xlcw0KKyBhZGRlZCBSb2NrZXIgY2hhcmFjdGVyIGZyb20gQTINCisgYWRkZWQgcmFuZG9tIGNpdmlsaWFucyBjbGFzcw0KKyBhZGRlZCBncm91cHMgb2YgcmFuZG9tIGNpdmlsaWFucw0KKyBhZGRlZCBlZGVuIHByZXZpZXdzIGltYWdlcw0KKyBhZGRlZCBzb21lIG1vcmUgZWRlbiBhdHRyaWJ1dGVzIChvcGVuIGRvb3IvdHJ1bmssIGJsaW5rZXJzIGNvbnRyb2wpDQorIGFkZGVkIHVuaXF1ZSBwaWN0dXJlcyB0byBhbGwgdW5pZm9ybSB2YXJpYW50cw0KKyBhZGRlZCBjYXIgYWxhcm0gZm9yIGxvY2tlZCB2ZWhpY2xlcyAoYXBwbGllcyB0byBHb2xmIElWICYgT2N0YXZpYSkgLSBsYXVuY2hlZCB1cG9uIGhpdCBvciBieSBmaXJlIGZyb20gbGFyZ2UgY2FsaWJlciBndW5zDQpeIGNoYW5nZWQgZW1lcmdlbmN5IGxpZ2h0IGtleWJpbmQgdG8gY3RybCtnIChjeWNsZSBuZXh0IGdyZW5hZGUga2V5KQ0KXiBpbXByb3ZlZCBibGlua2VycyBVSSBoYW5kbGVyIChjaGFuZ2luZyBiZXR3ZWVuIHZlaGljbGUgdGhyb3VnaCBWRyBzaG91bGQgcHJlc2VydmUgYWJpbGl0eSB0byBhY3RpdmF0ZSBibGlua2VycykNCl4gdHdlYWtlZCBtaXJyb3IgcG9zaXRpb24gaW4gU2tvZGEgT2N0YXZpYQ0KXiB1cGRhdGVkIGRlc3RydWN0aW9uIHRleHR1cmVzIGZvciBtb3N0IG9mIHZlaGljbGVzDQpeIHR3ZWFrZWQgcGh5c3ggb2Ygc29tZSB2ZWhpY2xlcw0KXiByZW5hbWVkIHZlaGljbGUgc2tlbGV0b25zIGZvciBiZXR0ZXIgY3Jvc3MgbW9kIGNvbXBhdGliaWxpdHkNCkAgZml4ZWQgd2hlZWwgZHVzdCBwb3NpdGlvbg0KQCBmaXhlZCBzb21lIGl0ZW1zIHdlcmUgbWlzc2luZyBpbiBjZmdQYXRjaGVzDQpAIGZpeGVkIGluanVyeSBzZWxlY3Rpb24gb24gcmlnaHQgbGVnIGZvciBhbGwgY2hhcmFjdGVycyANCkAgbWFkZSB3b3JrYXJvdW5kIGZvciBicm9rZW4gc2VhcmNobGlnaHRzICggaHR0cHM6Ly9mZWVkYmFjay5iaXN0dWRpby5jb20vVDExODMzMCApDQpAIGZpeGVkIE9jdGF2aWEgd2luZG93cyBoaWRkaW5nIG9uIGRlc3RydWN0aW9uDQpAIGZpeGVkIHZlaGljbGUgZGFzaGJvYXJkIGlsbHVtaW5hdGlvbiANCkAgZml4ZWQgQXBleCBlcnJvcnMNCkAgZml4ZWQgcmRzX2Nhcl93YXJuaW5nX3RyaWFuZ2xlX3RvMTEgZmxhc2hsaWdodCAmIHBvaW50ZXIgLnJwdCBlcnJvcnM=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/368535631804911993/549B15695A5A0090B15707FE49BB60E4C9BD9754/</image_url>
<download_url />
<filename />
<file_size>482158623</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable />
<place_after />
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>;</string_separator>
<filepath>workshop_installed.txt</filepath>
</config>
<post_install>modname=$( awk -F &quot;=&quot; </post_install>
<uninstall>a=%mods_full_path%
modid=$(find $a -iname &quot;%mod_string%&quot;)
modfolder=$(dirname $modid)
echo $modfolder
rm -Rf $modfolder
printf &quot;\n %mod_string% automatically uninstall. \n&quot; </uninstall>
</workshop_settings>

File diff suppressed because one or more lines are too long

View file

@ -1,18 +0,0 @@
<workshop_settings>
<workshop_id />
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path />
<mods />
<config>
<regex />
<mods_backreference_index />
<variable />
<place_after />
<mod_string />
<string_separator />
<filepath />
</config>
<post_install />
<uninstall />
</workshop_settings>

View file

@ -1,56 +0,0 @@
<workshop_settings>
<workshop_id>244850</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path />
<mods>
<mod id='2472607330'>
<name>Cepheus LX-50 Cutter</name>
<description>W2JdVmFuaWxsYSB8IFN1cnZpdmFsIHwgTm8gU3ViZ3JpZHMgfCBObyBTY3JpcHRzIHwgTm8gRExDIFsvYl0NCg0KDQpbY29kZV1bYl1BIHNtYWxsIHNoaXAgd2l0aCBhIGZldyB0dXJyZXRzLCBub3RoaW5nIHNwZWNpYWwuWy9iXVsvY29kZV0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIA0KDQpJIGJ1aWx0IGl0IGFzIGEgc29ydCBvZiBjaGFsbGVuZ2UgZm9yIG15c2VsZiB0byBidWlsZCBhbnkgc2hpcCBhcyBmYXN0IGFzIHBvc3NpYmxlLiANClRvb2sgbWUgYWJvdXQgMiBob3VycyBwbHVzIGEgYml0IG9mIHJlZmluaW5nLiBJdCdzIGZ1bGwgaHlkcm8gcG93ZXJlZCwgY2FuIGZseSBvbiBhbGwgcGxhbmV0cw0KYW5kIGhhcyBhbGwgdGhlIGJhc2ljcy4gSXQganVzdCBsYWNrcyB0aGUgbGFyZ2UgcmVmaW5lcnkgYW5kIGp1bXAgZHJpdmUgYmVjYXVzZSBvZiBhIGxhY2sgb2Ygc3BhY2UuDQoNCg0KW2gxXVN0YXRzWy9oMV0NCltsaXN0XQ0KWypdIFdlaWdodDogMzEwIHQNClsqXSBMZW5ndGg6IDQ1IG0NClsqXSBXaWR0aDogMzIuNSBtDQpbKl0gQmxvY2tzOiA0NTINClsqXSBQQ1U6IDUwNTcNClsvbGlzdF0NCg0KW2gxXUVuZ2luZXM6Wy9oMV0NCltsaXN0XQ0KWypdIDIgYmF0dGVyaWVzDQpbKl0gMiBoeWRyb2dlbiBlbmdpbmVzDQoNClsqXSAyOCBzbWFsbCBoeWRybyB0aHJ1c3RlcnMNClsvbGlzdF0NCg0KW2gxXUFybWFtZW50OlsvaDFdDQpbbGlzdF0NClsqXSA0IGdhdGxpbmcgdHVycmV0cw0KWy9saXN0XQ0KDQpbaDFdVXRpbGl0eTpbL2gxXQ0KW2xpc3RdDQpbKl0gYmFzaWMgcmVmaW5lcnkNClsqXSBhc3NlbWJsZXINClsqXSBvcmUgZGV0ZWN0b3INClsqXSBjcnlvIHBvZHMNClsvbGlzdF0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1790721049091201826/43AA92A474DAE9CB99D545CAF54461842BD17409/</image_url>
<download_url />
<filename />
<file_size>1012221</file_size>
</mod>
<mod id='2470607526'>
<name>Mr Bean&apos;s Mini</name>
<description>W2gxXSBCZWFuLiBbL2gxXQ0KTXIgQmVhbi4NCg0KW2gxXSBDb250cm9scyBbL2gxXQ0KMS4gTGVmdCBUdXJuIFNpZ25hbCBPbi9PZmYNCjIuIEhlYWRsaWdodHMgT24vT2ZmDQozLiBSaWdodCBUdXJuIFNpZ25hbCBPbi9PZmYNCg0KW2gxXSBOb3RlcyBbL2gxXQ0KLSBSZXF1aXJlcyBXYXN0ZWxhbmQgRExDIG9ubHkgZm9yIGZyb250IGdyaWxsZSAmIFJlYXIgTGlnaHRzICsgQmxpbmtlcnMgKHdvcmtzIHdpdGhvdXQpDQotIFlvdSBjYW4gc2l0IG9uIHRoZSBtb3VudGVkIGNvdWNoIGJ1dCBJIGRvdWJ0IGl0J3Mgc3RyZWV0LWxlZ2FsDQotIEdldCBpbiB0aHJvdWdoIHRoZSB0aW55IGdhcHMgb24gZWl0aGVyIHNpZGU=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1781713698219456050/8CE4CA03027726515A67B7C254DE8E0010B24BE1/</image_url>
<download_url />
<filename />
<file_size>481088</file_size>
</mod>
<mod id='2473969260'>
<name>Saratoga Class Cruiser ( No Mods) (DLC Req )</name>
<description>W2ltZ10gaHR0cHM6Ly9pLmltZ3VyLmNvbS9wN0Z2MVo2LmdpZiBbL2ltZ10NCg0KU28gSSBXYXMgSGF2aW5nIEEgTGl0dGxlIEJpdCBPZiBBIEhhbG8gaXRjaCBBbmQgRGVjaWRlZCBUbyBCdWlsZCBBIEhhbG8gc3R5bGUgQ3J1aXNlciBUaGF0IFdvdWxkIEZpdCBJbiBUaGUgSGFsbyBVbml2ZXJzZSBXaXRob3V0IEJlaW5nIEEgRGlyZWN0IENvcHkgT2YgVGhlIEhhbGN5b24gT3IgTWFyYXRob24gQ2xhc3MgQ3J1aXNlcnMsIFNvIEhlcmUgV2UgSGF2ZSBUaGUgU2FyYXRvZ2EgQ2xhc3MgQ3J1aXNlci4NCg0KUENVID0gODYxMzggKCBBYm91dCBIYWxmIElzIFdlYXBvbnMgKQ0KQmxvY2tzID0gMTE4MDIgDQoNCg0KU3Vydml2YWwNCj09PT09PT09PT09PT09PT0NCkZ1bmN0aW9uYWwgIDogWWVzDQpCdWlsZGFibGUgICAgOiBUaGUgTWFpbiBIdWxsIEFuZCBJbnRlcmlvciBBcmUgUHJvamVjdG9yIEJ1aWxkYWJsZSwgVGhlIFJlc3QsIE5vdCBTbyBNdWNoDQoNCkZlYXR1cmVzIDogV2VhcG9ucw0KPT09PT09PT09PT09PT09PT09DQoxMSBHYXRsaW5nIFR1cnJldHMNCjEwIE1pc3NpbGUgVHVycmV0cw0KMTIgRm9yd2FyZCBGYWNpbmcgUm9ja2V0IExhdWNoZXJzDQo0IEN1c3RvbSBUdXJyZXRzIFdpdGggMiBMYXJnZSBSb2NrZXQgTGF1bmNoZXJzIEVhY2gNCjQgQ3VzdG9tIEpvbHQgQ2Fubm9ucyBCYXNlZCBPbiBUaGUgT25lIEZvdW5kIEhlcmUgaHR0cHM6Ly9zdGVhbWNvbW11bml0eS5jb20vc2hhcmVkZmlsZXMvZmlsZWRldGFpbHMvP2lkPTI0MDc2NTU2MDcmc2VhcmNodGV4dD1waXN0b24rZ3VuDQo4IEN1c3RvbSBNaXNzaWxlIEJheXMNCg0KUHJvZHVjdGlvbiAvIFBvd2VyIC8gRnVlbCAvIENhcmdvDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KMjkgTzJIMiBHZW5lcmF0b3JzDQo2IExhcmdlIEgyIFRhbmtzDQo2IFNtYWxsIEgyIFRhbmtzDQoxIExhcmdlIFJlYWN0b3INCjE0IEJhdHRlcmllcw0KNCBPMiBUYW5rcw0KMiBMYXJnZSBDYXJnbyBDb250YWluZXJzDQoyMCBTbWFsbCBDYXJnbyBDb250YWluZXJzDQo5IEJhc2ljIEFzc2VtYmxlcnMNCjUgSnVtcCBEcml2ZXMNCg0KUm9vbXMgLyBFeHRyYXMNCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KQnJpZGdlDQpTeXN0ZW1zIENvbnRyb2wgUm9vbQ0KTWVkQmF5DQpDcnlvTGFiDQpGVEwgQ29udHJvbA0KU21hbGwgSGFuZ2VyDQo0IENyZXcgUXVhdGVycywgMiBCZWRzIEVhY2gNCkdhbGx5DQpSZWFjdG9yIENvbnRyb2wNCkZ1ZWwgQ29udHJvbA0KQXJtb3J5DQpHeXJvIENvbnRyb2wNCkdyYXZpdHkvUHJvZHVjdGlvbiBDb250cm9sIFN0YXRpb24NCjggRXNjYXBlIFBvZHMNCg0KDQpOb3Rlcw0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoqIE1heCBSZWNvbW1lbmRlZCBHcmF2aXR5IElzIDAuNTAgKCBZb3UgTWlnaHQgQmUgYWJsZSBUbyBIaXQgMC42NSB3aXRoIG91dCBjYXJnbyApDQoqIFRoZSBNaXNzaWxlcyBVc2UgSW9uIFRocnVzdGVycywgU28gVGhlbiBEb250IFdvcmsgSW4gR3Jhdml0eQ0KKiBTZXZlcmFsIFNjcmlwdHMgQXJlIFVzZWQsIFlvdSBDYW4gRGlzYWJsZSBNb3N0IElGIHlvdSBXYW50IEhvd2V2ZXIgIDMgQXJlIFJlcXVpcmVkDQpXaGlwcyBUdXJyZXQgU2xhdmVyIFNjcmlwdCAsIFdoaXBzIExBTVAgU2NyaXB0LCBXaGlwcyBXSEFNIFNjcmlwdC4NCiogV2hlbiBMYXVuY2hpbmcgTWlzc2lsZXMgU2xvdyB0byA0MC9tcyBPciBMb3dlciBXaXRoIE5vIEVycmF0aWMgTW92ZW1lbnRzDQoqIFRoZSBNYWluIENhbm5vbnMgQ2FuIEJlIEZpcmVkIEF0IEFueSBWYW5pbGxhIFNwZWVkLCBUaG91Z2ggWW91IFdpbGwgSGF2ZSBUbyBEbyBTb21lIExlYWRpbmcgT24gVGhlIFRhcmdldA0KKiBUaGUgU2hpcCBTaG91bGQgQmUgU3RvcHBlZCBPciBBdCBWZXJ5IExvdyBTcGVlZCBUbyBMYXVuY2ggVGhlIEVzY2FwZSBQb2Rz</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1776084465699675544/E3B57B81B8CB75A600B1BB8FE4BDB177F4AACAD5/</image_url>
<download_url />
<filename />
<file_size>13956266</file_size>
</mod>
<mod id='2475399454'>
<name>VANGUARD: The Puddlejumper (Modded Small Grid Space/Atmo Transport)</name>
<description>VGhlIFB1ZGRsZWp1bXBlciBmcm9tIHRoZSBWYW5ndWFyZCBzZXJpZXMgb24gWW91VHViZSBmZWF0dXJpbmcgU3BhY2ViYXIsIFRleGZpcmUsIEZhcnJlbGwsIGFuZCB3NHN0ZWRzcGFjZS4NCg0KVGhlIFB1ZGRsZWp1bXBlciBpcyBhIGp1bXAgY2FwYWJsZSBhaXJ0aWdodCBzbWFsbCBncmlkIHZlc3NlbCBjYXBhYmxlIG9mIGZseWluZyBpbiB1cHRvIDFnIGF0bW8sIHdpdGggYSBtdWx0aS10aHJ1c3Qgc2V0dXAgZmVhdHVyaW5nIGhvdmVyIGVuZ2luZXMsIGF0bW8gdGhydXN0ZXJzLCBhbmQgbW9kdWxhciB0aHJ1c3RlcnMgY2FwYWJsZSBvZiBhbGwgZW52aXJvbm1lbnRzLiBUaGUgc2hpcCBpcyBhbHNvIGNhcGFibGUgb2YganVtcGluZyAyNTAwa20gYW5kIGhhcyBjYXJnbyBjYXBhY2l0eSBmb3IgYSBkZWNlbnQgaGF1bCBhbG9uZ3NpZGUgMyBzZWF0LCBhIHR1cnJldCwgYW5kIGEgc3Vydml2YWwga2l0IQ0KDQpPaGguLiBBbmQgaXQncyBzaGllbGQgYXJlIHByZXR0eSBiZWFzdCAoNjQwaykNCg0KTW9kIExpc3Q6DQpBemltdXRoIE92ZXJjbG9ja2VkIE9yZSBEZXRlY3RvcnN+KERYLTExIFJlYWR5KQ0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9NDY5MzAxNzExLnNibQ0KDQpTbWFsbCBTaGlwIFZhbmlsbGEgTW9kIFBhY2sNCmh0dHA6Ly9zdGVhbWNvbW11bml0eS5jb20vc2hhcmVkZmlsZXMvZmlsZWRldGFpbHMvP2lkPTY3MjkxOTY3NS5zYm0NCg0KRHluYW1pYyBMYXNlciBDb21wcmVzc2lvbiBNb2R1bGFyIFRocnVzdGVycw0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9MjE2MTI0MzMzMy5zYm0NCg0KRGVmZW5zZSBTaGllbGRzIC0gdjIuMCgzKQ0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9MTM2NTYxNjkxOC5zYm0NCg0KSG92ZXJFbmdpbmUNCmh0dHA6Ly9zdGVhbWNvbW11bml0eS5jb20vc2hhcmVkZmlsZXMvZmlsZWRldGFpbHMvP2lkPTEyMjUxMDcwNzAuc2JtDQoNCkF6aW11dGggUGFzc2VuZ2VyIFNlYXQgJiBPcGVuIENvY2twaXR+KERYLTExIFJlYWR5KQ0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9NDY4NTkzOTUxLnNibQ0KDQpNQSBTcG90bGlnaHQgcGFjaw0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9MTg4MTE1ODA2Ni5zYm0NCg0KQWR2YW5jZWQgRG9vcnMgTW9kIFBhY2t+KERYLTExIFJlYWR5KQ0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9NTA2OTY0ODUzLnNibQ0KDQoNCg0KRmluZCB0aGUgVmFuZ3VhcmQgc2VyaWVzIGhlcmU6IGh0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3BsYXlsaXN0P2xpc3Q9UExUbHBPM0llZi04RjZWZ2hibldreWxmY0I1V1ExckJhTQ0KDQpodHRwOi8vd3d3LnlvdXR1YmUuY29tL3c0c3RlZHNwYWNlDQpodHRwOi8vd3d3LnR3aXRjaC50di93NHN0ZWRzcGFjZQ==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1768203314435924846/79D46947DEB2E40FFD96CA9B93FF2BA8318B6D91/</image_url>
<download_url />
<filename />
<file_size>1119104</file_size>
</mod>
</mods>
<config>
<regex />
<mods_backreference_index />
<variable />
<place_after />
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>\n</string_separator>
<filepath>workshop_installed.txt</filepath>
</config>
<post_install>modID=%workshop_mod_id%
echo $modID
sed -i &apos;s/&lt;Mods \/&gt;/&lt;Mods&gt;\n&lt;\/Mods&gt;/&apos; Sandbox_config.sbc
sed -i &quot;s/&lt;Mods&gt;/&lt;Mods&gt;\n&lt;ModItem&gt;\n&lt;Name&gt;$modID.sbm&lt;\/Name&gt;\n&lt;PublishedFileId&gt;$modID&lt;\/PublishedFileId&gt;\n&lt;\/Moditem&gt;\n/&quot; Sandbox_config.sbc</post_install>
<uninstall />
</workshop_settings>

View file

@ -1,23 +0,0 @@
<workshop_settings>
<workshop_id>228380</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>mods</mods_path>
<mods />
<config>
<regex>mods=(([0-z]+,?)*)</regex>
<mods_backreference_index>1</mods_backreference_index>
<variable>mods=</variable>
<place_after />
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>,</string_separator>
<filepath>server_config.cfg</filepath>
</config>
<post_install>printf &quot;\nMoving item %workshop_mod_id% ...&quot;
cp -Rf &quot;%mods_full_path%/steamapps/workshop/content/228380/%workshop_mod_id%&quot; &quot;%mods_full_path%/.&quot;
rm -Rf &quot;%mods_full_path%/steamapps/workshop/content/228380/%workshop_mod_id%&quot;
printf &quot;\nSuccess.&quot;</post_install>
<uninstall>printf &quot;\nUninstalling item %mod_string% ...\n&quot;
rm -Rf &quot;%mods_full_path%/%mod_string%&quot;
printf &quot;\nSuccess.&quot;</uninstall>
</workshop_settings>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,103 +0,0 @@
<workshop_settings>
<workshop_id>4000</workshop_id>
<download_method>steamapi</download_method>
<anonymous_login>1</anonymous_login>
<mods_path>mods</mods_path>
<mods>
<mod id='1443096823'>
<name>gm_goldencity_day</name>
<description>SGVyZSdzIGEgbWFwIHRoYXQncyBoZWF2aWx5IGluc3BpcmVkIGJ5IGdtX2JpZ2NpdHkuIEkndmUgYmVlbiB3YW50aW5nIHRvIG1ha2UgYSBjaXR5IG1hcCBsaWtlIHRoaXMgZm9yIGEgZGVjYWRlLCBhbmQgSSd2ZSBmaW5hbGx5IGdvdHRlbiBhcm91bmQgaW50byBkb2luZyBqdXN0IHRoYXQuIEkgcHJvYmFibHkgY291bGQgaGF2ZSBnb3R0ZW4gdGhpcyBvdXQgb2YgdGhlIHdheSBhIHdoaWxlIGFnbywgYnV0IGJldHRlciBsYXRlIHRoYW4gbmV2ZXIsIEkgc3VwcG9zZS4NCg0KRmVhdHVyZXM6DQoNCi0gQSBkb3dudG93biBhcmVhIHBsdXMgYSBoaWdocmlzZSBhcmVhDQotIEEgc3Vua2VuIGhpZ2h3YXkgd2l0aCBhIGNvdXBsZSBvZiB0dW5uZWxzIGNvbm5lY3RpbmcgdG8gdGhlIG90aGVyIHJvYWRzDQotIDE0IGJ1aWxkaW5ncyB3aXRoIGludGVyaW9ycy4gVGhlcmUgYXJlIHNvbWUgZXh0ZXJpb3Igb3BlbiBkb29ycyB0aGF0IHlvdSBjYW4gd2FsayBpbnRvIHRoYXQgdGVsZXBvcnQgeW91IGludG8gdGhlIGludGVyaW9yLiBUbyBnbyBiYWNrIG91dHNpZGUsIHByZXNzIEUgb24gb25lIG9mIHRoZSBlbGV2YXRvciBkb29ycyBvciB3aGF0ZXZlciBraW5kIG9mIGRvb3IgeW91IHRlbGVwb3J0ZWQgaW4gZnJvbnQgb2YuDQotIEFuIEFJIE5vZGVncmFwaA0KLSBIRFINCi0gQSBzaW5nbGUgc291bmRzY2FwZSB0aHJvdWdob3V0IHRoZSBtYXAuIFRoZSByZWFzb24gaXQncyBqdXN0IG9uZSBpcyBiZWNhdXNlIEknbSBub3QgZ3JlYXQgYXQgc291bmRzY2FwZXMuIEknbGwgcHJvYmFibHkgd29yayBvbiB0aGF0IGxhdGVyIGFzIHdlbGwuDQotIEEgc2luZ2xlIGN1YmVtYXAgdGhhdCBhY3R1YWxseSB3b3JrcyAoYXQgbGVhc3Qgb24gbXkgZW5kLCB0aG91Z2ggSSBtYWRlIHN1cmUgaXQgd2Fzbid0IGp1c3QgdGhlIGxlZnRvdmVyIGJzcCB0aGF0IGhhZCB0aGF0KSB1bmxpa2UgdGhlIG9uZXMgaW4gdGhlIHByZXZpb3VzIG1hcCBJIHVwbG9hZGVkDQotIFR3byBjdXN0b20gbW9kZWxzIChBIHRyZWUgYW5kIGEgYmFza2V0YmFsbCBob29wKSB1c2VkIGluIHRoZSBtYXAgdGhhdCB5b3UgY2FuIHVzZSB5b3Vyc2VsZg0KLSBBIGNvdXBsZSBvZiBzZWNyZXRzDQoNCkNyZWRpdHM6DQoNCkRvY3RvciBGbG91bmRlciBCb3gsIGZvciB0aGUgYmFza2V0YmFsbCBob29wIG1vZGVsDQpCbHVlYmVycnlfUGllLCBmb3IgY29taW5nIHVwIHdpdGggdGhlIGlsbHVtaW5hdGVkIHdpbmRvdyB0ZWNobmlxdWUgKG5pZ2h0IHZlcnNpb24pDQpLaW5nUG9tbWUsIGZvciBleHBhbmRpbmcgb24gdGhhdCB0ZWNobmlxdWUgKG5pZ2h0IHZlcnNpb24pDQpic2hhZG93LCBmb3IgdGhlIGludmlzaWJsZSByYWQgbGlnaHQgdGVjaG5pcXVlIHVzZWQgZm9yIHRoZSBzdHJlZXRsaWdodHMgKG5pZ2h0IHZlcnNpb24pDQpKYWtvYmkgTydCcmllbiwgZm9yIGhlbHBpbmcgbWUgb3V0IHdpdGggYSBiaXQgb2Ygb3B0aW1pemF0aW9uIChUaG91Z2ggSSBkaWRuJ3QgZG8gYSBncmVhdCBqb2Igd2l0aCBpdCBvbiB0aGlzIHVwZGF0ZS4gSSdsbCBwcm9iYWJseSB3b3JrIG9uIGl0IHNvbWUgbW9yZSBvbiB0aGUgbmV4dCB1cGRhdGUpDQpWYWx2ZSBhbmQgQ0dUZXh0dXJlcywgZm9yIHRoZSBzb3VyY2UgbWF0ZXJpYWwgdXNlZCBmb3IgdGhlIGN1c3RvbSB0ZXh0dXJlcw0KRXZlcnlvbmUgd2hvIGdhdmUgZmVlZGJhY2sgb24gdGhlIG1hcCBpbiB0aGUgd29yay1pbi1wcm9ncmVzcyB0aHJlYWQNCg0KWW91IHdvbid0IG5lZWQgQ291bnRlciBTdHJpa2U6IFNvdXJjZSBvciBMZWZ0IDQgRGVhZCBmb3IgdGhlIHRleHR1cmVzIG9uIHRoaXMgbWFwIHRvIHdvcmssIGZvciB0aG9zZSBvZiB5b3Ugd2hvIGRvbid0IGhhdmUgZWl0aGVyIGdhbWUuIFRoZXJlIGlzIGFsc28gYSBuaWdodCB2ZXJzaW9uIG9mIHRoaXMgbWFwIHdoaWNoIGlzIHRoZSBvcmlnaW5hbCwgaWYgeW91IHdhbnQgdG8gY2hlY2sgdGhhdCBvdXQuDQoNCkFsc28sIEkganVzdCBub3RpY2VkIHRoYXQgdGhlcmUncyBhIGJ1ZyBvbiB0aGlzIG1hcCB3aGVyZSBzbWFsbCBmbGlja2VyaW5nIGJsYWNrIHRyaWFuZ2xlcyB3aWxsIGFwcGVhciBpbiB0aGUgY29ybmVycyBvZiB0aGUgc2t5Ym94IGlmIHlvdSBsb29rIGF0IGFueSBvZiB0aG9zZSBjb3JuZXJzLiBJIGhhdmUgbm8gaWRlYSBob3cgdG8gZml4IHRoaXMsIHRob3VnaCBpdCdzIG5vdCB0b28gbm90aWNhYmxlLiBJJ20gdGhpbmtpbmcgaXQgaGFzIHNvbWV0aGluZyB0byBkbyB3aXRoIHRoZSByZW5kZXIgZGlzdGFuY2UsIHdoaWNoIHdvdWxkbid0IHJlYWxseSBtYWtlIGFueSBzZW5zZSBzaW5jZSB0aGUgc2t5Ym94IGlzbid0IHN1cHBvc2VkIHRvIGhhdmUgdGhhdCBpc3N1ZS4=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/943951991654869773/338A0338BA966AF835CEB2B3B5A800487A1BACE1/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/943951991654867861/F167313D927ECA5D4AFAEFD76644F598B9A55AC1/</download_url>
<filename>1531788260_1094686331.gma</filename>
<file_size>25824223</file_size>
</mod>
<mod id='1403476308'>
<name>gm_news_studio</name>
<description>TmV3cyBTdHVkaW8gb2YgR2FycnkncyBNb2QhIFlvdSBjYW4gZmluYWxseSBtYWtlIHlvdXIgb3duIG5ld3Mgb24gdGhpcyBtYXAhDQpNYXAgZG9lcyBub3QgcmVxdWlyZSBhbnkgYWRkaXRpb25hbCBjb250ZW50Lg0KU29tZSBwYXJ0cyBvZiBtYXAgYXJlIGNvbG91cmFibGUuDQoNCklmIHlvdSBsaWtlIHRoaXMgYWRkb24gc28gbXVjaCwgdGhhdCB5b3UgY2FuIGdpZnQgbWUgc29tZXRoaW5nIC0gaGVyZSdzIG15IHRyYWRlLW9mZmVyOg0KDQpodHRwczovL3N0ZWFtY29tbXVuaXR5LmNvbS90cmFkZW9mZmVyL25ldy8/cGFydG5lcj0xMTEzOTA4NDAmdG9rZW49c3c4ekZmWEQNCg0KVGFnczoNCk5ld3MNClRGTg0KQnJvYWRjYXN0DQpTYW5kYm94DQpNb3ZpZQ0KQW5pbWF0aW9uDQpDYW1lcmENClZpZGlv</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/910171257329171075/337E32C3BEE684EFF58A0837FAADE9CC69AE04AC/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/909045760924451577/085F0A63EC79B0509E2C8643DFAFE0D9E4785FCE/</download_url>
<filename>1528544715_223866981.gma</filename>
<file_size>6461914</file_size>
</mod>
<mod id='1437899079'>
<name>Smitty Werbenjagermanjensen - Spongebob Squarepants</name>
<description>Ikl0IHdhcyBoaXMgaGF0IE1yLiBLcmFicyEgSGUgd2FzIG51bWJlciBvbmUhIg0KDQpTbWl0dHkgV2VyYmVuamFnZXJtYW5qZW5zZW4sIGZhaXRoZnVsbHkgcmVjcmVhdGVkIGZyb20gdGhlIGVwaXNvZGUgIk9uZSBLcmFiJ3MgVHJhc2giDQoNCltoMV1JbmNsdWRlcyBbL2gxXQ0KDQpQbGF5ZXIgbW9kZWwgDQpSYWdkb2xsIA0KRmluZ2VyIFBvc2luZyAoT25seSBvbmUgZmluZ2VyKQ0KRlBTIEFybXMNCkZyaWVuZGx5IGFuZCBIb3N0aWxlIE5QQ3MNCg0KW2gxXUNyZWRpdHMgYW5kIEh1Z2UgVGhhbmtzIFRvWy9oMV0NCltiXUdyaWZmYm9bL2JdIFRoZSBib2R5IGZvciBoaW0sIEkgZG91YnQgSSB3b3VsZCd2ZSBiZWVuIGFibGUgdG8gZG8gdGhpcy4NCltiXVdpbm5pbmdSb29rWy9iXSBGb3IgdGhlIHBpY3R1cmUuDQpbYl1TcGlrZVsvYl0gRm9yIHRoZSBoYXRzLCBoZWFkIGFuZCB0ZXh0dXJlcy4=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/955210546397733977/A0293C4D6AD86A37CFCB8EC18071FCB3E773D76B/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/955210546397733770/C0A22FDAC654A2067C761E5C322986CFBCC3A158/</download_url>
<filename>1531283572_229351499.gma</filename>
<file_size>2577446</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable />
<place_after />
<mod_string>%first_file%</mod_string>
<string_separator>\n</string_separator>
<filepath>mods/mods.txt</filepath>
</config>
<post_install>cd &quot;%mods_full_path%/steamapps/workshop/content/4000/%workshop_mod_id%&quot;
cp -f %first_file% myfile.gma
7z x myfile.gma -aoa &gt; /dev/null 2&gt;&amp;1
cp -f myfile content.gma
rm -f myfile.gma myfile
&quot;%mods_full_path%/../bin/gmad_linux&quot; content.gma &gt; /dev/null 2&gt;&amp;1
rm -f &quot;content.gma&quot;
cd content
zip -r &quot;%mods_full_path%/steamapps/workshop/content/4000/%first_file%.zip&quot; * &gt; /dev/null 2&gt;&amp;1
cd ../..
rm -Rf &quot;%mods_full_path%/steamapps/workshop/content/4000/%workshop_mod_id%&quot;
unzip -Z1 &quot;%first_file%.zip&quot; &gt; &quot;%first_file%.list&quot;
tac &quot;%first_file%.list&quot; &gt; &quot;%first_file%.listinv&quot;
cp -f &quot;%first_file%.listinv&quot; &quot;%first_file%.list&quot;
rm &quot;%first_file%.listinv&quot;
unzip -o &quot;%first_file%.zip&quot; -d &quot;%mods_full_path%/../garrysmod&quot; &gt; /dev/null 2&gt;&amp;1
rm -f &quot;%first_file%.zip&quot;
if [ -f &quot;%mods_full_path%/steamapps/workshop/content/4000/%first_file%.list&quot; ];then
cd &quot;%mods_full_path%/../garrysmod&quot;
luaFile=&quot;%mods_full_path%/../garrysmod/lua/autorun/server/resources.lua&quot;
while read p; do
if [ -f &quot;$p&quot; ] &amp;&amp; [ ! -d &quot;$p&quot; ]; then
filename=$(basename -- &quot;$p&quot;)
extension=&quot;${filename##*.}&quot;
if [ &quot;$extension&quot; != &quot;bsp&quot; ] &amp;&amp; [ &quot;$extension&quot; != &quot;png&quot; ]; then
newstring=&quot;resource.AddSingleFile(\&quot;$p\&quot;)&quot;
if ! grep -Fxq &quot;$newstring&quot; &quot;$luaFile&quot;; then
echo &quot;$newstring&quot; &gt;&gt; &quot;$luaFile&quot;
fi
fi
fi
done &lt;&quot;%mods_full_path%/steamapps/workshop/content/4000/%first_file%.list&quot;
printf &quot;\nContents of %first_file% successfully installed!&quot;
else
printf &quot;\nFile listing not found, try it again after reinstalling the mod.&quot;
fi
</post_install>
<uninstall>if [ -f &quot;%mods_full_path%/steamapps/workshop/content/4000/%mod_string%.list&quot; ];then
cd &quot;%mods_full_path%/../garrysmod&quot;
luaFile=&quot;%mods_full_path%/../garrysmod/lua/autorun/server/resources.lua&quot;
while read p; do
if [ -d &quot;$p&quot; ]; then
if [ -z &quot;$(ls -A &quot;$p&quot;)&quot; ]; then
rm -vRf &quot;$p&quot;
fi
else
if [ -f &quot;$p&quot; ]; then
rm -vf &quot;$p&quot;
filestring=&quot;resource.AddSingleFile(\&quot;$p\&quot;)&quot;
if grep -Fxq &quot;$filestring&quot; &quot;$luaFile&quot;; then
escaped_filestring=$(sed -e &apos;s/[]\/$*.^[]/\\&amp;/g&apos; &lt;&lt;&lt; $filestring)
sed -i &quot;/$escaped_filestring/d&quot; &quot;$luaFile&quot;
fi
fi
fi
done &lt;&quot;%mods_full_path%/steamapps/workshop/content/4000/%mod_string%.list&quot;
printf &quot;\nContents of %mod_string% successfully uninstalled!&quot;
else
printf &quot;\nFile listing not found, try it again after reinstalling the mod.&quot;
fi</uninstall>
</workshop_settings>

View file

@ -1,100 +0,0 @@
<workshop_settings>
<workshop_id>440900</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>1</anonymous_login>
<mods_path>ConanSandbox/Mods</mods_path>
<mods>
<mod id='1378596051'>
<name>Banners to the Gods</name>
<description>VGhpcyBNb2QgaXMgYSBtb2QgdGhhdCBnaXZlcyAzIG5ldyBmbGF2b3JzIG9mIGJhbm5lcnMgdG8geW91ciBnYW1lLiBEZXJrZXRvLCBNaXRyYSBhbmQgWW1pciBiYW5uZXJzLiBTaW5jZSB0aGVyZSB3ZXJlIG9ubHkgU2V0LCBhbmQgRGFyZmFyaSBiYW5uZXJzLCBwbHVzIG9mIGNvdXJzZSB0aGUgb3RoZXIgY2xhbnMgaW4gdGhlIEV4aWxlZCBsYW5kcy4uLiBZZXQsIG5vdyB0aGUgTm9yZGhlaW1lcnMsIHRoZSBNaXRyYWVucywgRGVya2V0aWFucywgaGF2ZSBhIGJhbm5lciBhcyB3ZWxsIQ==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/930434406223072719/09C64DD22443CA300AB5C9C148D542385C458BDF/</image_url>
<download_url />
<filename />
<file_size>155457134</file_size>
</mod>
<mod id='1426203926'>
<name>Compass Icon</name>
<description>QSB2ZXJ5IGJhc2ljIGNvbXBhc3MgaWNvbiB0aGF0IG1vdmVzIHRvIGluZGljYXRlIE5vcnRoIGFuZCBibGVuZHMgd2l0aCB0aGUgZXhpc3RpbmcgVUkuCgpJIHdhcyB0cnlpbmcgdG8gZmlndXJlIG91dCBob3cgdG8gZG8gbW9kcyBzbyBJIG1hZGUgYSBzdXBlciBiYXNpYyBjb21wYXNzLCBJIGZpZ3VyZWQgSSBtaWdodCBhcyB3ZWxsIHNoYXJlIGl0LiBJJ20gc3RpbGwgbGVhcm5pbmcgc28gYW55IGZlZWRiYWNrIGlzIHdlbGNvbWUu</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/952957720418478101/595F301CFA480B162796FE56793C1A650722DEDF/</image_url>
<download_url />
<filename />
<file_size>1942869</file_size>
</mod>
<mod id='1384471264'>
<name>Drag thralls in water (May 2018)</name>
<description>RHJhZyB0aHJhbGxzIHRocm91Z2ggd2F0ZXIgd2l0aCByb3BlLgoKU3VnZ2VzdGVkIGJ5IERyZWFndWgu</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/912420738527948826/40A5E9CD53E58008EBDC0EC4519DF55144DD03C5/</image_url>
<download_url />
<filename />
<file_size>777732</file_size>
</mod>
<mod id='1369802940'>
<name>Emberlight</name>
<description>TW9kIElEOiAxMzY5ODAyOTQwDQoNCldlbGNvbWUgdG8gRW1iZXJsaWdodCEgVGhpcyBtb2QgaXMgaW50ZW5kZWQgZm9yIHJvbGVwbGF5ZXJzIGFuZCBvdGhlciBDb25hbiBFeGlsZXMgcGxheWVycyB3aG8gd2FudCBhIHJpY2hlciBhbmQgbW9yZSBpbW1lcnNpdmUgZXhwZXJpZW5jZS4gSXQgZm9jdXNlcyBlbnRpcmVseSBvbiBjb250ZW50IGZvciBwbGF5ZXJzOyB5b3Ugd29uJ3QgbmVlZCB0aGUgYWRtaW4gcGFuZWwgdG8gYWNjZXNzIGFueSBvZiB0aGUgbW9kJ3MgY3VycmVudCBvciBmdXR1cmUgY29udGVudC4gUmVhZCBvbiB0byBzZWUgd2hhdCB0aGUgbW9kIGluY2x1ZGVzIGN1cnJlbnRseSBhbmQgd2hhdCB3ZSBoYXZlIHBsYW5uZWQgZm9yIGZvciB0aGUgd2Vla3MgYW5kIG1vbnRocyBhaGVhZC4NCg0KSm9pbiB0aGUgRW1iZXJsZWdpb24gb24gRGlzY29yZDoNCmh0dHBzOi8vZGlzY29yZC5nZy81TXY3ZWR5DQoNCllvdSBjYW4gYWxzbyBiZWNvbWUgYW4gRW1iZXJsaWdodCBQYXRyb24gaGVyZToNCmh0dHBzOi8vd3d3LnBhdHJlb24uY29tL3N0dWRpb2VtYmVybGlnaHQNCg0KDQoNCltoMV1GRUFUVVJFUyBTVU1NQVJZWy9oMV0NCg0KKFZpc2l0IG91ciBEaXNjdXNzaW9ucyB0YWIgZm9yIGEgbW9yZSBkZXRhaWxlZCBicmVha2Rvd24gb2YgRW1iZXJsaWdodCdzIGZlYXR1cmVzKQ0KDQpbYl1Ib3J0aWN1bHR1cmVbL2JdDQpbbGlzdF1bKl1DcmFmdCwgZmFybSBhbmQgZGVjb3JhdGUgd2l0aCBhIHdpZGUgdmFyaWV0eSBvZiBpdGVtczoNCltsaXN0XVsqXURlY29yYXRpdmUgZmxvd2VyIHBvdHMgYW5kIHRyZWVzDQpbKl1QbGFjZWFibGUgcG90dGVkIHBsYW50ZXJzIGFuZCBtdXNocm9vbSBib3hlcw0KWypdR2FyZGVuIGJveGVzIGFuZCB3ZWRnZXMgd2hpY2ggY2FuIHNuYXAgdG8gYnVpbGRpbmcgcGllY2VzWy9saXN0XVsvbGlzdF0NCg0KW2JdQW5pbWFsIEh1c2JhbmRyeVsvYl0NCltsaXN0XVsqXUJ1aWxkIHBlbnMgYW5kIGtlZXAgZ2FtZSBmb3IgaGlkZXMsIG1lYXQgYW5kIG90aGVyIHJlc291cmNlcy4gVGhlIGZvbGxvd2luZyBhbmltYWxzIGNhbiBiZSBkb21lc3RpY2F0ZWQ6DQpbbGlzdF1bKl1SYWJiaXRzDQpbKl1BbnRlbG9wZXMNClsqXUdhemVsbGUNClsqXU9zdHJpY2hlcw0KWypdR29hdHMNClsqXUJvYXINClsqXURlZXINClsqXUp1bmdsZSBCaXJkc1svbGlzdF1bL2xpc3RdDQoNCltiXUN1aXNpbmUhWy9iXQ0KW2xpc3RdWypdRm9vZCwgc291cHMgYW5kIGRyaW5rcyBhcmUgbm93IHZpc2libGUgd2hlbiBwbGFjZWQgaW4gdGhlIGludmVudG9yeSBvZiBzcGVjaWFsIFNlcnZpbmcgZGlzaGVzIGFuZCBtdWdzLCB0YW5rYXJkcyBhbmQgZmxhZ29ucy4gWW91IGNhbiBvYnRhaW4gdGhlc2UgaXRlbXMgZnJvbSB0aGUgSG9zcGl0YWxpdHkgZmVhdHMgaW4gdGhlIERlY29yYXRpb24gdGFiIG9mIHlvdXIgRmVhdHMgc2NyZWVuLlsvbGlzdF0NCg0KW2JdU3RyYWlnaHQgUmF6b3JbL2JdDQpbbGlzdF1bKl1Vc2UgdGhlIFZhbml0eSBwbGFjZWFibGUgaXRlbSB0byBjdXN0b21pemUgeW91ciBjaGFyYWN0ZXIncyBoZWFkIGFuZCBib2R5IGhhaXIuWy9saXN0XQ0KDQpbYl1BZGRpdGlvbmFsIFdlYXBvbnMgYW5kIEFybW9yWy9iXQ0KW2xpc3RdWypdQ3VsdHVyYWwgV2VhcG9ucyB3aXRoIGZ1bGwgdGllciBwcm9ncmVzc2lvbnMNClsqXUFybW9yIGFuZCBjbG90aGluZyB2YXJpYW50cw0KWypdRW5kZ2FtZSB2YXJpYW50cyBvZiBwb3B1bGFyIGxvd2VyLXRpZXIgd2VhcG9ucw0KWypdRmlzdCBXZWFwb25zIGZvciB0aWVycyAyIHRocm91Z2ggNQ0KWypdV29vZGVuIHdlYXBvbnMgZm9yIHNwYXJyaW5nDQpbKl1SdWdnZWQgV3JhcHMsIHJlaW50cm9kdWNpbmcgdGhlIGxvaW5jbG90aCBhbmQgY2hlc3R3cmFwIG9mIG9sZA0KWypdQ29sZCB3ZWF0aGVyIGNsaW1iaW5nIGJvb3RzIGFuZCBnbG92ZXMsIGxlYXJuZWQgdmlhIHRoZSBNb3VudGFpbmVlciBmZWF0Wy9saXN0XQ0KDQpbYl1BZGRpdGlvbmFsIEl0ZW1zWy9iXQ0KW2xpc3RdWypdQmluZGFibGUgQmVkIFBpbGxvd3MNClsqXUJvb2sgc2hlbHZlcyBhbmQgcGxhY2VhYmxlIHJvd3Mgb2Ygam91cm5hbHMgYW5kIHN0YWNrcyBvZiBzY3JvbGxzDQpbKl1TdG9uZSBidXRjaGVyIHRvb2xzIChsZWFybmVkIHdpdGggdGhlIEFwcHJlbnRpY2UgQnV0Y2hlciBmZWF0KQ0KWypdSXJvbiBTaWNrbGUgKGxlYXJuZWQgd2l0aCB0aGUgSXJvbiBUb29scyBmZWF0KVsvbGlzdF0NCg0KW2JdUXVhbGl0eSBvZiBsaWZlIGltcHJvdmVtZW50c1svYl0NCltsaXN0XVsqXXN0YWNrIHNpemVzIGluY3JlYXNlZCB0byAxMDAgZm9yIG1vc3QgY29uc3VtYWJsZXMgYW5kIG1hdGVyaWFscw0KWypdQmFzaWMgY3JhZnRpbmcgc3RhdGlvbiBpbnZlbnRvcnkgc2l6ZSBpbmNyZWFzZWQgdG8gMzAgc2xvdHMuDQpbKl1QcmVzZXJ2YXRpb24gYm94IGFuZCBJbXByb3ZlZCBQcmVzZXJ2YXRpb24gYm94IGludmVudG9yaWVzIGRvdWJsZWQuDQpbKl1CYXJyZWxzIGFuZCBTbWFsbCBCYXJyZWxzIGNhbiBub3cgYmUgdXNlZCB0byBzdG9yZSBpdGVtcw0KWypdQ29tYmluZSBMZWF0aGVyIHRvIG1ha2UgVGhpY2sgTGVhdGhlciBhdCB0aGUgQXJtb3JlcidzIEJlbmNoDQpbKl1DcmFmdGluZyBzaGFwZWQgd29vZCBwcm9kdWNlcyAxIGJhcmsNClsqXVZhbml0eSBDYW1lcmEgYWRqdXN0ZWQgdG8gYWxsb3cgeW91IHRvIGdldCB5b3VyIEdVSSBiYWNrIGJ5IGNyb3VjaGluZyBvciBlbW90aW5nLlsvbGlzdF0NCg0KDQoNCltoMV1GRUFUVVJFUyAoQ29taW5nIFNvb24hKVsvaDFdDQoNClsqXU1vcmUgY3VsdHVyYWwgd2VhcG9ucyBhbmQgbW9yZSBhcm1vciB2YXJpYW50cw0KWypdQ29uc2NyaXB0cyEgU2VuZCB5b3VyIHRocmFsbHMgb3V0IHRvIGNvbGxlY3QgcmVzb3VyY2VzIGFuZCBjb21wbGV0ZSBvdGhlciB0YXNrcw0KWypdTmV3IGJ1aWxkaW5nIGJsb2NrcyBhbmQgZGVjbyBpdGVtc1svbGlzdF0NCg0KDQoNCltoMV1GRUFUVVJFUyAoQ29taW5nIG5vdCBhcyBzb29uISlbL2gxXQ0KDQpbbGlzdF1bKl1BZHZhbmNlZCBjb21iYXQNClsqXUFkdmFuY2VkIHJlbGlnaW9uLCBteXN0aWNpc20gYW5kIGFsY2hlbXlbL2xpc3RdDQoNCg0KDQpbaDFdS05PV04gSVNTVUVTWy9oMV0NCg0KWypdU29tZSBob3J0aWN1bHR1cmUgaXRlbXMgZG9uJ3QgcGxheSB0aGVpciBwbGFjZW1lbnQgc291bmRzLg0KWypdR2FyZGVuIFdlZGdlcyBjYW4gYmUgcGxhY2VkIGluc2lkZSBvZiBHYXJkZW4gQm94ZXMuIElmIHlvdSBkbyB0aGlzLCB5b3UncmUgYmFkLiBEb24ndCBiZSBiYWQuDQpbKl1Mb290IHByZXZpZXcgZG9lcyBub3Qgc2hvdyB0aGUgb3V0cHV0IG9mIEhvcnRpY3VsdHVyZSBtYWNoaW5lcyAoZ2FyZGVuIGJveGVzIGFuZCB3ZWRnZXMsIHBsYW50ZXJzIGFuZCBtdXNocm9vbSBib3hlcykuIFRoZSBvdXRwdXQgb2YgdGhlc2Ugc3RhdGlvbnMgaXMgY2FsY3VsYXRlZCBvbiBwbGF5ZXIgaW50ZXJhY3Rpb24gdG8gcmVkdWNlIHNlcnZlciBsb2FkLiBXZSdyZSB3b3JraW5nIG9uIGEgc29sdXRpb24gZm9yIHRoaXMuWy9saXN0XQ0KDQoNCg0KU3BlY2lhbCB0aGFua3MgdG8gSm9zaHRlY2ggYW5kIHRoZSBQSVBQSSB0ZWFtIGZvciB0aGVpciBzdXBwb3J0IGFuZCBicmFpbiBwb3dlci4gd2UgPDMgdSBndXl6Lg==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/929308506301543680/CE3C865085C0C5FF2879FB8E9D875098C17F01B5/</image_url>
<download_url />
<filename />
<file_size>1667910631</file_size>
</mod>
<mod id='1403991684'>
<name>Exile Architect</name>
<description>QnVpbGRpbmcgYmxvY2sgc2V0IGZvciBzY2FmZm9sZGluZyBvciBicmlkZ2VzLCBhbmQgbWFzb24gbGluZXMgdG8gaGVscCBsYXlvdXQgZm91bmRhdGlvbnMuIE1hc29uIGxpbmVzIGJlaGF2ZXMgbGlrZSBmZW5jZSBmb3VuZGF0aW9ucywgYnV0IGNhbiBhbHNvIHNuYXAgYXQgYW5nbGVzLgoKVGhlcmUncyBhIDEgcG9pbnQgZmVhdCBpbiB0aGUgYnVpbGRpbmcgY2F0ZWdvcnkuCgpLbm93biBpc3N1ZXM6CgoqU2hvcnQgbWFzb24gbGluZXMgY2FuIG5vdyBzbmFwIGF0IDYwPyBhbmdsZXMgZm9yIGRyYXdpbmcgdHJpYW5nbGVzLiBCdXQgZHVlIHRvIGhvdyBzb2NrZXRzIHdvcmssIHRoZXknbGwgYWxzbyBzbmFwIGF0IDMwPyBhbmdsZXMsIHNvIGJlIGF3YXJlLgoKKkZlbmNlcyBhbmQgd2FsbHMgY2FuIHN0YWNrIG9uIG1hc29uIGxpbmVzLiBUaGV5IGhhdmUgcmF0aGVyIGxvdyBoZWFsdGggdGhvdWdoLCBzbyBpdCdzIHByb2JhYmx5IG5vdCBhIGdvb2QgaWRlYSB0byB1c2UgdGhlbSBhcyBmb3VuZGF0aW9ucyBvbiBhbnl0aGluZyBkZWZlbnNpdmUuCgoqSWYgdXBncmFkaW5nIGZyb20gcHJldmlvdXMgdmVyc2lvbiwgeW91IG1heSBuZWVkIHRvIGRyaW5rIGEgbG90dXMgcG90IChvciBhZG1pbiBzZWxmIGlmIFNQKSB0byByZWxlYXJuIGZlYXQgdG8gZ2V0IG5ldyByZWNpcGVzLgoKTW9kIGNvbXBhdGliaWxpdHkgbm90ZXM6CgpJdGVtIElEcyAxNzc1MDAxIC0gMTc3NTAwOQpSZWNpcGUgSURzIDE3NzUxMDEgLSAxNzc1MTA5CkZlYXQgSUQgMTc3NTEwMA==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/915800878318924163/704FD0BCBB9780EBCD0AB50B81DCEA210EC752C6/</image_url>
<download_url />
<filename />
<file_size>21626043</file_size>
</mod>
<mod id='1382120864'>
<name>LowerMonsterHPSolo</name>
<description>Q2hhbmdlczoNCk1vbnN0ZXJIZWFsdGgNCjgJTlBDX0thcHBhSGF0Y2hsaW5nDQo4CU5QQ19SYWJiaXQNCjExCU5QQ19WdWx0dXJlDQoxMwlOUENfR2F6ZWxsZUZhd24NCjEzCU5QQ19IeWVuYVNwb3R0ZWRDdWINCjEzCU5QQ19IeWVuYVN0cmlwZWRDdWINCjE5CU5QQ19QaXJhbmhhDQoyMAlOUENfVGlnZXJDdWINCjIwCU5QQ19Xb2xmUHVwcHkNCjIyCU5QQ19Pc3RyaWNoQ2hpY2sNCjIyCU5QQ19Xb2xmRGlyZVB1cHB5DQoyMwlOUENfQ3JvY29kaWxlQmFieQ0KMjMJTlBDX1NhYnJldG9vdGhDdWINCjI1CU5QQ19KYWd1YXJDdWINCjI1CU5QQ19QYW50aGVyQ3ViDQoyNQlOUENfUGlnbGV0DQoyNQlOUENfV2lsZEJvYXJQaWdsZXQNCjMwCU5QQ19CZWFyQmxhY2tDdWINCjMwCU5QQ19CZWFyQnJvd25DdWINCjQ2CU5QQ19SaGlub0JhYnkNCjU3CU5QQ19QaWtlZmlzaA0KNTgJTlBDX0ltcA0KNTgJTlBDX0ltcEV4cGxvc2l2ZQ0KNjUJTlBDX0dhemVsbGUNCjY2CU5QQ19Db2JyYQ0KNjgJTlBDX0VsZXBoYW50QmFieQ0KODcJTlBDX1NwaWRlckJyb3duDQo4OQlOUENfT3N0cmljaA0KOTIJTlBDX0FudGVsb3BlU3BpcmFsSG9ybg0KOTYJTlBDX0h5ZW5hU3BvdHRlZA0KMTAwCU5QQ19IdW1hbm9pZA0KMTAzCU5QQ19IeWVuYVN0cmlwZWQNCjEwOQlOUENfU3BpZGVyV2lkb3dZZWxsb3cNCjE0MAkjTi9BDQoxNDIJTlBDX0FudGVsb3BlS2luZw0KMTQ3CU5QQ19TcGlkZXJHcmV5DQoxNDcJTlBDX1NlcnBlbnRwZW9wbGVIb3JkZWxpbmcNCjE0NwlOUENfU2VycGVudHBlb3BsZUlsbHVzaW9uDQoxNTIJTlBDX01vdW50YWluR29hdA0KMTU5CU5QQ19HZW5lcmljDQoxNjQJTlBDX0h5ZW5hVW5kZWFkDQoxNzEJTlBDX1NwaWRlclJlZG1vdXRoDQoxOTUJTlBDX1NwaWRlcldpZG93DQoyMDAJTlBDX1Njb3JwaW9uTWVkaXVtDQoyMDAJTlBDX1NwaWRlckdyZWVuDQoyMTcJTlBDX09vemUNCjIzNQlOUENfS2FwcGENCjIzNQlOUENfU2tlbGV0b25EYXJmYXJpDQoyMzYJTlBDX0RlZXINCjIzNwlOUENfUm9ja25vc2UNCjI2MQlOUENfQ3JvY29kaWxlDQoyNjUJTlBDX0NhbWVsDQoyNzgJTlBDX0Vsaw0KMjkxCU5QQ19Sb2Nrbm9zZU1vbHRlbg0KMjkxCU5QQ19Sb2Nrbm9zZVdoaXRlDQozMDgJTlBDX1NwaWRlcldpZG93Qmx1ZQ0KMzEwCU5QQ19Ta2VsZXRvbkRyZWdzDQozMTEJTlBDX0xvY3VzdEdyZWVuDQozMTEJTlBDX1RpZ2VyDQozMTgJTlBDX1NwaWRlcldpZG93R3JlZW4NCjMyMQlOUENfU2NvcnBpb25MYXJnZQ0KMzU5CU5QQ19TcGlkZXJXaWRvd1JlZA0KMzU5CU5QQ19Xb2xmDQozODUJTlBDX1BhbnRoZXINCjQwMAlOUENfSnVuZ2xlQmlyZA0KNDMwCU5QQ19KYWd1YXINCjQ0MwlOUENfS29tb2RvDQo0NDkJTlBDX0xvY3VzdFllbGxvdw0KNTA0CU5QQ19TYWxhbWFuZGVyDQo1MDcJTlBDX0xvY3VzdFdoaXRlDQo1MzEJTlBDX1NlcnBlbnRwZW9wbGVCb3cNCjUzMQlOUENfU2VycGVudHBlb3BsZVN3b3Jkcw0KNTM1CU5QQ19BcnRpbGxlcnkNCjUzNQlOUENfQmVhc3RtYXN0ZXINCjUzNQlOUENfQnJhd2xlcg0KNTM1CU5QQ19DcnVzaGVyDQo1MzUJTlBDX01vdW50ZWQNCjUzNQlOUENfUmFuZ2VyDQo1MzUJTlBDX1Njb3V0DQo1MzUJTlBDX1VuZGVhZA0KNTM1CU5QQ19XYXJyaW9yDQo1MzUJTlBDX1dlcmVoeWVuYQ0KNTM1CU5QQ19XaWxkQm9hcg0KNjEwCU5QQ19Hb3JpbGxhDQo2MzcJTlBDX1NhYnJldG9vdGgNCjY3MwlOUENfUmVwdGlsZUJlYXN0DQo2ODUJTlBDX0NoaWxkcmVuT2ZKaGlsDQo2ODUJTlBDX0dyZXlBcGUNCjY4NQlOUENfU2tlbGV0b25TZXJwZW50TWFuDQo3NTAJTlBDX1JvY2tub3NlS2luZ01vbHRlbg0KNzUwCU5QQ19CYXREZW1vbg0KODE4CU5QQ19Hb3JpbGxhU2lsdmVyYmFjaw0KODE4CU5QQ19LYXBwYVVuZGVhZA0KODM1CU5QQ19Ta2VsZXRvbkFybW9yDQo4NTUJTlBDX0JlYXJCcm93bg0KODgwCU5QQ19EZWF0aEtuaWdodE1pbmlvbg0KOTA5CU5QQ19Xb2xmRGlyZQ0KOTEwCU5QQ19ZZXRpDQoxMTUyCU5QQ19CZWFyDQoxMjc0CU5QQ19XaWdodA0KMTI5OAlOUENfUmhpbm9HcmV5DQoxMjk4CU5QQ19SaGlub1doaXRlDQoxMzY1CU5QQ19JbXBLaW5nDQoxNDY3CU5QQ19FbGtLaW5nDQoxNDY3CU5QQ19FbGVwaGFudA0KMTU5MAlOUENfQ3JvY29kaWxlR2lhbnRUb21iDQoxNjI2CU5QQ19Gcm9zdEdpYW50DQoxNjI2CU5QQ19Gcm9zdEdpYW50VHV0b3JpYWwNCjE3MzEJTlBDX1JvY2tub3NlS2luZw0KMTk3MwlOUENfTWFtbW90aA0KMjA3OAlOUENfUm9ja25vc2VLaW5nSWNlDQoyMTA4CU5QQ19TZXJwZW50cGVvcGxlQnJ1dGUNCjIxNDIJTlBDX1N0b3J5Ym9zcw0KMjE3NQlOUENfU2VycGVudHBlb3BsZUJvd0tpbmcNCjIxNzUJTlBDX1NlcnBlbnRwZW9wbGVTd29yZHNLaW5nDQoyMjQwCU5QQ19HaWFudEtpbmdHaG9zdA0KMjQ5MglOUENfV2l0Y2hRdWVlbkd1YXJkaWFuDQoyNTQwCU5QQ19TYW5kc3Rvcm1CZWFzdA0KMjcyMAlOUENfV2lsZEJvYXJCb3NzDQozMjAwCU5QQ19LYXBwYUtpbmcNCjM2NTAJTlBDX0JhdERlbW9uV2hpdGUNCjM4MzIJTlBDX0xhdmFXb3JtDQo0MDU0CU5QQ19TZXdlckFib21pbmF0aW9uDQo0MjEzCU5QQ19HaWFudEtpbmdCb3NzDQo0ODkwCU5QQ19SaGlub0JsYWNrDQo1NTY1CU5QQ19EcmFnb25IYXRjaGxpbmcNCjU4MjcJTlBDX0Zyb3N0R2lhbnRCb3NzDQo1ODI3CU5QQ19Gcm9zdEdpYW50U21pdGgNCjYxODMJTlBDX0RlYXRoS25pZ2h0Qm9zcw0KNjE4MwlOUENfTG9jdXN0UXVlZW5Td2FtcFRvbWINCjkwOTUJTlBDX1JvY2tub3NlS2luZ0Jvc3NNb3NzDQo5MTAwCU5QQ19EcmFnb24NCjkxMDAJTlBDX0RyYWdvbkdyZWVuDQo5MTAwCU5QQ19VbmRlYWREcmFnb24NCjkxMDAJTlBDX0RyYWdvbldoaXRlDQoxMDA1MwlOUENfQWxwaGFlbGVwaGFudA0KMTAwNTMJTlBDX0FscGhhc25ha2UNCjEwMDUzCU5QQ19TbmFrZUdpYW50DQoxMDI2NAlOUENfTG9jdXN0UXVlZW5EZXNlcnQNCjEwMjY0CU5QQ19Mb2N1c3RRdWVlblN3YW1wDQoxMDI2NAlOUENfVGlnZXJXaGl0ZQ0KMTA3NDQJTlBDX0RlbW9uU3BpZGVyDQoxMDc0NAlOUENfU3BpZGVyR2lhbnQNCjExMDQ0CU5QQ19Dcm9jb2RpbGVHaWFudA0KMTE0NzkJTlBDX1Njb3JwaW9uS2luZw0KMTE2NTEJTlBDX1JlcHRpbGVCZWFzdEJvc3MNCjEyOTAwCU5QQ19SaGlub0tpbmcNCjEzMjAwCU5QQ19Td2FtcEtpbmcNCg==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/929309228564296120/59CC87CB7CC2019162FFD950A15765B0420D4431/</image_url>
<download_url />
<filename />
<file_size>724791</file_size>
</mod>
<mod id='864199675'>
<name>Pickup+</name>
<description>V2l0aCB0aGlzIG1vZCB5b3UgYXJlIGFibGUgdG8gcGljayB1cCBhbGwgdGhlIHRoaW5ncyB5b3UndmUgcGxhY2VkIC0gc2ltcGxlIGFzIHRoYXQhIDotKQoKKioqIEFkZGVkIHBpY2t1cCBzdXBwb3J0IGZvciB0aHJhbGxzISAqKioKCi0gV29ya3Mgb24gc2luZ2xlcGxheWVyIGFuZCBkZWRpY2F0ZWQgc2VydmVycyEKLSBBZG1pbnMgaGF2ZSB0aGUgb3B0aW9uIHRvIHJlbW92ZSB0aGUgcGlja3VwIG9wdGlvbiBmcm9tIGl0ZW1zIChPbmx5IGluIE1QKQotIEFkbWlucyBoYXZlIHRoZSBvcHRpb24gdG8gZW5hYmxlL2Rpc2FibGUgdGhlIHBpY2t1cCBvcHRpb24gZnJvbSBhbGwgdGhyYWxscyBvdmVyIHRoZSBvcHRpb25zd2hlZWwgKE9ubHkgaW4gTVApCgoqKiogWW91IGNhbiBvbmx5IHBpY2t1cCB0aHJhbGxzIHRoYXQgYXJlIG5vdCB3ZWFyaW5nIGFueSBhcm1vciEgKioqCgoKWW91IHdhbnQgdG8gcmVwb3J0IGEgYnVnPyBQbGVhc2UgdXNlIHRoaXMgdGVtcGxhdGUgYW5kIGp1c3QgcG9zdCBpdCBpbiB0aGUgY29tbWVudHMhCmh0dHBzOi8vc3RlYW1jb21tdW5pdHkuY29tL3dvcmtzaG9wL2ZpbGVkZXRhaWxzL2Rpc2N1c3Npb24vODY0MTk5Njc1LzE3Mjg3MDE4Nzc0ODE5NTQ0NTkvIAoKCkhhdmUgRnVuIQoKCk1PRCBJRDogODY0MTk5Njc1CgpJZiB5b3UgaGF2ZSBhbnkgcHJvYmxlbXMgb3Igc3VnZ2VzdGlvbnMgZmVlbCBmcmVlIHRvIHdyaXRlIGl0IGluIHRoZSBjb21tZW50cyBvciBzdGFydCBhIGRpc2N1c3Npb24hCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKVGhpcyBtb2QvY29kZS93b3JrIGlzIHByb3RlY3RlZCBieSB0aGUgW1VSTD1odHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy1uZC80LjAvbGVnYWxjb2RlXUF0dHJpYnV0aW9uLU5vbkNvbW1lcmNpYWwtTm9EZXJpdmF0aXZlcyA0LjAgSW50ZXJuYXRpb25hbCBDcmVhdGl2ZSBDb21tb25zIExpY2Vuc2UuCltJTUddaHR0cHM6Ly9pLmNyZWF0aXZlY29tbW9ucy5vcmcvbC9ieS1uYy1uZC80LjAvODh4MzEucG5nWy9JTUddWy9VUkxd</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/96102470863175828/E2FD19AEC364F48C7B0D0FB7231D937A097A0EEB/</image_url>
<download_url />
<filename />
<file_size>691137</file_size>
</mod>
<mod id='1367404881'>
<name>Savage Steel</name>
<description>VGhpcyBtb2QgaGFzIGEgd2lkZSB2YXJpZXR5IG9mIHJlYWxpc3RpYyBwbGFjZWFibGUgb3IgUlAgaXRlbXMuIFNvbWUgb2YgdGhlIGZlYXR1cmVzIHRoYXQgd2UgaGF2ZSBhcmUgaW52ZW50b3JpZXMgaW4gc2Fja3MsIGNyYXRlcywgYmFycmVscyBhbmQgbWFueSBvdGhlciBpdGVtcy4gV2UgYWxzbyBoYXZlIGV4dHJhIHN0b3JhZ2Ugc3BhY2UgaW4gb3VyIFN0cm9uZ2JveCBjaGVzdC4gV2Ugbm93IGhhdmUgIlBpY2sgVXAiIG9uIHRoZSBTYXZhZ2UgU3RlZWwgcGxhY2VhYmxlcy4gV2UgaGF2ZSBkZXNpZ25lZCBvdXIgcGxhY2VhYmxlcyB0byBiZSBwbGFjZWQgY2xvc2VseSB0b2dldGhlciBvciBzdGFja2VkLCBpZiBkZXNpcmVkLiBXZSB3aWxsIGNvbnRpbnVlIHRvIGFkZCB0byB0aGlzIG1vZCBvbiBhbiBvbmdvaW5nIGJhc2lzLiBUaGlzIG1vZCBpcyBkZXNpZ25lZCBmb3IgdGhlIFNhdmFnZSBTdGVlbCBzZXJ2ZXIsIGJ1dCBhbnlvbmUgaXMgd2VsY29tZSB0byB1c2UgaXQuIElmIHlvdSBsaWtlIG91ciBtb2QgcGxlYXNlIGJlIHN1cmUgdG8gZ2l2ZSB1cyBhICJUaHVtYnMgVXAiISEgDQoNCltoMV1MaXN0IG9mIFBsYWNlYWJsZXNbL2gxXSANCg0KW2gxXVN0b3JhZ2UgSXRlbXNbL2gxXSANCg0KQnVja2V0ICANClRhbGwgQnVja2V0IA0KMyBkaWZmZXJlbnQgQ2xvdGggQmFsZXMgDQozIGRpZmZlcmVudCBCYWdzIA0KV29vZGVuIFR1YiANCkJhc2tldCANClRhbGwgQmFza2V0IA0KMyBCYXJyZWxzIA0KNCBkaWZmZXJlbnQgQ3JhdGVzIA0KMiBkaWZmZXJlbnQgU3Ryb25nYm94IENoZXN0cyANCg0KW2gxXUZ1cm5pc2hpbmdzWy9oMV0gDQoNCkxhcmdlIENhc2sgDQozIGJhciBwaWVjZXMgDQpGdWxseSBhc3NlbWJsZWQgYmFyIA0KQ2xvdGggQmFyIGNvdmVyIA0KMiBkaWZmZXJlbnQgZW1wdHkgYm93bHMgDQoyIGRpZmZlcmVudCBmdWxsIGJvd2xzIA0KQ2FiaW5ldCANCjIgZGlmZmVyZW50IFNoZWx2ZXMgDQpDYWJpbmV0IHdpdGggc2hlbGYgDQo1IGRpZmZlcmVudCBoZXJiIGJpbnMgDQpEcmVzc2VyIA0KV2FsbCBTaGVsZiANCkJhdGggDQpTdHlnaWFuIEJhdGggDQpSdXN0aWMgQmF0aCAod29vZGVuKSANCkN1cnRhaW5zIA0KQ2hhbWJlciBQb3QNClNhdWNlcGFuDQpTb3VwIExhZGxlDQo0IENhbmlzdGVycyAoU2FsdCwgUGVwcGVyLCBDaW5uYW1vbiAmIFBhcHJpa2EpIHdpdGggNSBzdG9yYWdlIHNsb3RzIGVhY2gNCkNhbmlzdGVyIFNldCBvbiBhIHNoZWxmIHdpdGggMjAgc3RvcmFnZSBzbG90cw0KMiBXZWFwb24gRHJvcHMgLSBOb3J0aGVybiBhbmQgU291dGhlcm4NCkxhdW5kcnkgQnVja2V0DQpTY3JvbGwgU3RhbXANClNlYWxlZCBTY3JvbGwNClNjcm9sbCB3YXgNCjMgZGlmZmVyZW50IHJ1Z3MNCiANCltoMV1Gb29kWy9oMV0gDQogDQpDaGlja2VuIExlZ3Mgb24gYSBQbGF0ZSANCldoaXRlIEJyZWFkIG9uIGEgcGxhdGUgDQpDYWtlcyBvbiBhIHBsYXRlIA0KQ2hlZXNlIHdoZWVsIG9uIGEgcGxhdGUgDQpCb3dsIG9mIEVnZ3MgDQpGcmllZCBlZ2dzIG9uIGEgcGxhdGUgDQpIYW0gb24gYSBwbGF0ZSANCkJyZWFkIG9uIGEgcGxhdGUgDQogDQpbaDFdQWxjaGVteS9BcG90aGVjYXJ5Wy9oMV0gDQoNCkFsY2hlbXkgRGVzayANCkFsY2hlbXkgRGVzayBDaGFpciANCkluayBXZWxsIG9wZW4gDQpJbmsgV2VsbCBDbG9zZWQgDQpJbmsgd2VsbCBjYXAgDQo0IGRpZmZlcmVudCBQb3Rpb25zIA0KMyBkaWZmZXJlbnQgQXBvdGhlY2FyeSBOb3RlcyANCjYgZGlmZmVyZW50IEZlYXRoZXJzIA0KTW9ydGFyICYgUGVzdGxlIA0KMiBkaWZmZXJlbnQgSGFuZ2luZyBIZXJiIHJhY2tzIChvbmUgd29vZCBhbmQgb25lIG1ldGFsKSANCjMgZGlmZmVyZW50IEFsY2hlbXkgc2V0cyANCjUgZGlmZmVyZW50IGhlcmJzIHRvIHBsYWNlIG9uIGEgdGFibGUgb3IgY291bnRlcg0KDQpbaDFdT3V0ZG9vciBEZWNvclsvaDFdIA0KDQpXYXRlciBDYW4gDQpXYWdvbiANCldhZ29uIFdoZWVsIA0KQnJ1c2h3b29kIA0KOCBkaWZmZXJlbnQgcGllY2VzIG9mIGZpcmV3b29kICg0IHN0YW5kaW5nIHVwLCA0IGxheWluZyBkb3duKSANCjIgTG9ncyANCjUgZGlmZmVyZW50IFBsYW5rcyANCkdhbGxvd3MNCkV4ZWN1dGlvbmVyJ3MgQmxvY2ssIEF4ZSBhbmQgY29tYmluYXRpb24NCkd1aWxsb3RpbmUNClBpbGxhcnkNCkhhbmdpbmcgQ2FnZQ0KDQogW2gxXX5+ICBNb2QgSUQgMTM2NzQwNDg4MVsvaDFdDQoNCltoMV1XZSBub3cgaGF2ZSBhIERpc2NvcmQgc2VydmVyOlsvaDFdDQpodHRwczovL2Rpc2NvcmQuZ2cvcUVoTTNXdA0KDQpUbyBnZXQgdGhlIEdVSSBib3ggb24gYm90aCBiYXRocywgaG92ZXIgb3ZlciB0aGUgbGFkZGVyIGFyZWEuIFRoaXMgc2hvdWxkIGdpdmUgeW91IHRoZSBvcHRpb24gdG8gcGlja3VwIG9yIGRlc3Ryb3kuIA0KDQoNClRoYW5rcyB0byBTaGFkb3dDTUQgZm9yIGFsbCB5b3VyIGhlbHAgYW5kIHBhdGllbmNlIGFuZCB0byBSZWQgTWFyY2ggZm9yIGhlbHBpbmcgd2l0aCB0aGUgaWNvbnMgYW5kICB0aGUgYXJ0d29yayBmb3IgdGhlIG1vZCBjb3ZlciEh</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/929311364899517529/96C8FED0D19E78C69A2D571BE083FC7FC26A3F32/</image_url>
<download_url />
<filename />
<file_size>2644257234</file_size>
</mod>
<mod id='1113901982'>
<name>The Age of Calamitous</name>
<description>W2gxXVdlbGNvbWUgdG8gVGhlIEFnZSBvZiBDYWxhbWl0b3VzIVsvaDFdCgpUaGlzIG1vZCBzZXJ2ZXMgYXMgYSB0b3RhbCBjb252ZXJzaW9uIG1vZCwgaW50cm9kdWNpbmcgbmV3IHN5c3RlbXMsIGNvbnRlbnQsIGZlYXRzLCBhbmQgbXVjaCBtb3JlISAKCkhlcmUgaXMgYSBsaXN0IG9mIHNvbWUgYWRkaXRpb25zIHRvIHRoZSBnYW1lOgpbbGlzdF0KWypdIEFkZGl0aW9uYWwgQ2hhcmFjdGVyIENyZWF0aW9uIE9wdGlvbnMKWypdIE5ldyBTdGFja3MgJiBXZWlnaHQKWypdIFVJIC8gSFVEIG1vZGlmaWNhdGlvbnMKWypdIEh1bmRyZWRzIG9mIE5ldyBEZWNvcmF0aW9ucywgUHJvcHMsIEl0ZW1zLCBXZWFwb25zLCBldGMuClsqXSBNYW55IE5ldyBDcmFmdGluZyBTdGF0aW9ucywgRmVhdHMgJiBSZWNpcGVzClsqXSBOZXcgTGV2ZWwgQ2FwIDEwMCAoQXNjZW5zaW9uIDEwMS0xMjApClsqXSBTcGVjaWFsIGNvbnRlbnQgZnJvbSBUaGUgQWdlIG9mIENhbGFtaXRvdXMKWy9saXN0XQpBbmQgbXVjaCBtb3JlIQoKW2gxXVdBUk5JTkdbL2gxXQoKVGhpcyBtb2QgaXMgaW4gYWN0aXZlIGRldmVsb3BtZW50LCBhbmQgdGhlcmVmb3JlIHRoZXJlIHdpbGwgYmUgZnJlcXVlbnQgcGF0Y2hlcyBjb21pbmcgb3V0LiBTbWFsbCAmIGxhcmdlIG9uZXMgY29udGFpbmluZyBhZGp1c3RtZW50cywgYmFsYW5jaW5nLCBjb250ZW50ICYgZml4ZXMuCklmIHlvdSBkbyBub3Qgd2FudCB0byBrZWVwIHVwIHdpdGggZnJlcXVlbnQgdXBkYXRlcywgYXZvaWQgdXNpbmcgdGhlIG1vZCB1bnRpbCBpdCdzIGluIGEgY29tcGxldGVkIHN0YXRlLgpUaGlzIG1vZCBpcyBpbnRlbmRlZCB0byBiZSBzdGFuZGFsb25lIGFuZCBpcyBub3QgbWFkZSB0byB3b3JrIHdpdGggYWRkaXRpb25hbCBtb2RzLgpSZWFkIG1vcmUgYXQgdGhlIEltcG9ydGFudCBJbmZvcm1hdGlvbiB0b3BpYyBpbiB0aGUgZGlzY3Vzc2lvbnMuCgpbaDFdSW5mb3JtYXRpb25bL2gxXQoKVGhlIGludGVudGlvbiBvZiB0aGlzIG1vZCBpcyB0byBleHBhbmQgdXBvbiBDb25hbiBFeGlsZXMgd2l0aCBuZXcgY29udGVudCBmcm9tIFRoZSBBZ2Ugb2YgQ2FsYW1pdG91cyB1bml2ZXJzZSwgaW50cm9kdWNpbmcgYSBmZXcgYXNwZWN0cyBvZiB0aGUgZmFudGFzeSBtZWRpZXZhbCBnZW5yZS4KSm9pbiB1cyBvbiBEaXNjb3JkIGZvciBtb2QgdXBkYXRlcyBhbmQgc2VydmVycyBydW5uaW5nIHRoZSBtb2Q6Ci0gW3VybD1odHRwczovL2Rpc2NvcmQuZ2cvODJoZ3ZHaF0gRGlzY29yZFsvdXJsXQoKRmVlbCBmcmVlIHRvIHJlZ2lzdGVyIG9uIHRoZSB3ZWJzaXRlIHRvIGtlZXAgeW91cnNlbGYgdXAgdG8gZGF0ZSB3aXRoIHRoZSBsYXRlc3QgbmV3cyEKLSBbdXJsPWh0dHA6Ly93d3cudGhlLWFnZS1vZi1jYWxhbWl0b3VzLmNvbS9dIFRoZSBBZ2Ugb2YgQ2FsYW1pdG91cyBXZWJzaXRlWy91cmxdCgpNT0QgSUQ6IDExMTM5MDE5ODIKCltoMV1UaGUgT2ZmaWNpYWwgQWdlIG9mIENhbGFtaXRvdXMgUHJvamVjdFsvaDFdCgpJZiB5b3UgYXJlIGludGVyZXN0ZWQgaW4gbGVhcm5pbmcgbW9yZSBhYm91dCB3aGF0IFRoZSBBZ2Ugb2YgQ2FsYW1pdG91cyBwcm9qZWN0IGlzLCB5b3UgY2FuIGRyb3AgYnkgb3VyIG9mZmljaWFsIEZhY2Vib29rIHBhZ2UhIEJlIGF3YXJlIHRoYXQgdGhpcyBpcyB0aGUgb2ZmaWNpYWwgcHJvamVjdCBhbmQgbm90IHRoZSBtb2QuIFRoZSBtb2QgaXMgYSBwZXJzb25hbCBzaWRlIHByb2plY3QgYW5kIGlzIG5vdCB0aGUgb2ZmaWNpYWwgcHJvamVjdC4KLSBbdXJsPWh0dHBzOi8vd3d3LmZhY2Vib29rLmNvbS9BbmFyaW91c1Byb2R1Y3Rpb25zXSBUaGUgQWdlIG9mIENhbGFtaXRvdXMgRmFjZWJvb2tbL3VybF0KCkFkZGl0aW9uYWxseSwgeW91IGNhbiBmb2xsb3cgbWUgb24gVHdpdHRlciBmb3IgYW55IG5ld3MgdXBkYXRlcyByZWdhcmRpbmcgdGhlIG1vZCBhbmQgdGhlIG92ZXJhbGwgcHJvamVjdC4gCi0gW3VybD1odHRwczovL3R3aXR0ZXIuY29tL0VzcGVuR0pvaGFuc2VuXSBUd2l0dGVyWy91cmxdCgpbaDFdV2FudCB0byBzdXBwb3J0IHRoZSBwcm9qZWN0P1svaDFdCgpBbnkgZm9ybSBvZiBzdXBwb3J0IGlzIGdyZWF0bHkgYXBwcmVjaWF0ZWQhCkFsbCB0cmlidXRlcyBtYWRlIHRocm91Z2ggRG9uYXRpb24gYW5kL29yIFBhdHJlb24gd2lsbCBnbyB0b3dhcmRzIGV4cGFuZGluZyBUaGUgQWdlIG9mIENhbGFtaXRvdXMgcHJvamVjdC4KLSBbdXJsPWh0dHA6Ly93d3cudGhlLWFnZS1vZi1jYWxhbWl0b3VzLmNvbS9dIERvbmF0aW9uWy91cmxdCi0gW3VybD1odHRwczovL3d3dy5wYXRyZW9uLmNvbS9lc3Blbmdqb2hhbnNlbl0gUGF0cmVvblsvdXJsXQoKCkFsbCBjb250ZW50IG93bmVkIGFuZC9vciBwcm92aWRlZCBmb3IgVGhlIEFnZSBvZiBDYWxhbWl0b3VzIGlzIGNvcHlyaWdodGVkLgooYylDb3B5cmlnaHQgMjAxMS0yMDE4IEFuYXJpb3VzIFByb2R1Y3Rpb25zLCBBbGwgUmlnaHRzIFJlc2VydmVkCihjKUNvcHlyaWdodCAyMDExLTIwMTggRXNwZW4gR3JhdmRhaGwgSm9oYW5zZW4sIEFsbCBSaWdodHMgUmVzZXJ2ZWQKCkNvbmFuIEV4aWxlcyBjb250ZW50IGFuZCBtYXRlcmlhbHMgYXJlIHRyYWRlbWFya3MgYW5kIGNvcHlyaWdodHMgb2YgRnVuY29tLiA=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/861731135277865883/BCA986F592ABA8A6B95A687E0E1A3BE8749CDD3F/</image_url>
<download_url />
<filename />
<file_size>1291252425</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable />
<place_after />
<mod_string>%first_file%</mod_string>
<string_separator>\n</string_separator>
<filepath>ConanSandbox/Mods/modlist.txt</filepath>
</config>
<post_install>printf &quot;\nRunning post installation for mod %workshop_mod_id%&quot;
printf &quot;\nMovin Folders&quot;
mv %mods_full_path%/steamapps/workshop/content/440900/%workshop_mod_id%/%first_file% %mods_full_path%/%first_file%
printf &quot;\nCleaning up&quot;
rm -Rf %mods_full_path%/steamapps/workshop/content/440900/%workshop_mod_id%
printf &quot;\nInstallation for mod %workshop_mod_id% completed!&quot;
</post_install>
<uninstall>printf &quot;\nUninstalling...\n&quot;
rm -vf %mods_full_path%/%mod_string%
printf &quot;\nDone!&quot;
</uninstall>
</workshop_settings>

View file

@ -1,23 +0,0 @@
<workshop_settings>
<workshop_id>211820</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>mods</mods_path>
<mods />
<config>
<regex>mods=(([0-9]+,?)*)</regex>
<mods_backreference_index>1</mods_backreference_index>
<variable>mods=</variable>
<place_after />
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>,</string_separator>
<filepath>steam_workshop.cfg</filepath>
</config>
<post_install>printf &quot;\nMoving item %workshop_mod_id% ...&quot;
mv -f &quot;%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%/contents.pak&quot; &quot;%mods_full_path%/%workshop_mod_id%.pak&quot;
rm -Rf &quot;%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%&quot;
printf &quot;\nSuccess.&quot;</post_install>
<uninstall>printf &quot;\nUninstalling item %mod_string% ...\n&quot;
rm -Rf &quot;%mods_full_path%/%mod_string%.pak&quot;
printf &quot;\nSuccess.&quot;</uninstall>
</workshop_settings>

View file

@ -1,23 +0,0 @@
<workshop_settings>
<workshop_id>211820</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>mods</mods_path>
<mods />
<config>
<regex>mods=(([0-9]+,?)*)</regex>
<mods_backreference_index>1</mods_backreference_index>
<variable>mods=</variable>
<place_after />
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>,</string_separator>
<filepath>steam_workshop.cfg</filepath>
</config>
<post_install>printf &quot;\nMoving item %workshop_mod_id% ...&quot;
mv -f &quot;%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%/contents.pak&quot; &quot;%mods_full_path%/%workshop_mod_id%.pak&quot;
rm -Rf &quot;%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%&quot;
printf &quot;\nSuccess.&quot;</post_install>
<uninstall>printf &quot;\nUninstalling item %mod_string% ...\n&quot;
rm -Rf &quot;%mods_full_path%/%mod_string%.pak&quot;
printf &quot;\nSuccess.&quot;</uninstall>
</workshop_settings>

View file

@ -1,66 +0,0 @@
<workshop_settings>
<workshop_id>730</workshop_id>
<download_method>steamapi</download_method>
<anonymous_login>1</anonymous_login>
<mods_path>mods</mods_path>
<mods>
<mod id='1440818854'>
<name>cs_noffice [office in nuke-style]</name>
<description>YSBzbWFsbCBmdW5tYXANCg0KZW5qb3kgYW5kIGhhdmUgZnVuIDotKQ==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/964217986228487212/CF7FB6AFE894AF59908CDA64AD5E8F852D39AE1E/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/964217986228482922/A3EB0675317A2395DC96870AFE3EDB9608616787/</download_url>
<filename>cs_noffice.bsp</filename>
<file_size>72639068</file_size>
</mod>
<mod id='1414531578'>
<name>de_cornerwork</name>
<description>RGVfY29ybmVyd29yayBmcm9tIENTTzIsIG1hZGUgYnkgTmV4b24=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/938320142839248719/DE42CB9345A53EC8B4BBE5381D8AD55407FD88D1/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/938321006101014631/2F2EF3472A0FC4B10D1AD559FC516B742AF43C15/</download_url>
<filename>de_cornerwork.bsp</filename>
<file_size>87872150</file_size>
</mod>
<mod id='1433404064'>
<name>Mirage [Compatibility Version 1.36.3.8]</name>
<description>QW4gb2xkZXIgdmVyc2lvbiBvZiBvZmZpY2lhbCBtYXAgYnkgVmFsdmUgZm9yIGRlbW8gcGxheWJhY2sgY29tcGF0aWJpbGl0eS4gVGhpcyBtYXAgd2FzIHByZXZpb3VzbHkgdXNlZCBpbiBPZmZpY2lhbCBNYXRjaG1ha2luZyBpbiBDUzpHTy4gSXQgY291bGQgYmUgcGxheWVkIGluIERlYXRobWF0Y2gsIENsYXNzaWMgQ2FzdWFsLCBhbmQgQ2xhc3NpYyBDb21wZXRpdGl2ZS4=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/387665671598503104/9BC8E9D876916173C915233460D559231FF4E4E3/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/945077059916661709/A20ADA8668F0BB0EE12F61314137BE71EFDFF6C3/</download_url>
<filename>de_mirage.bsp</filename>
<file_size>17429043</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable />
<place_after />
<mod_string>%first_file%</mod_string>
<string_separator>\n</string_separator>
<filepath>mods/modlist.txt</filepath>
</config>
<post_install>printf &quot;\nRunning post installation for mod %workshop_mod_id%&quot;
printf &quot;\nInstalling Map %first_file%\n&quot;
unzip -o &quot;%mods_full_path%/steamapps/workshop/content/730/%workshop_mod_id%/%first_file%&quot; -d &quot;%mods_full_path%/../csgo/maps&quot;
printf &quot;\nCleaning up&quot;
rm -Rf &quot;%mods_full_path%/steamapps/workshop/content/730/%workshop_mod_id%&quot;
map=%first_file%
map=${map%.bsp}
maplist_file=&quot;%mods_full_path%/../csgo/maplist.txt&quot;
maplist_content=$(cat &quot;$maplist_file&quot;)
if [ ! -z &quot;${maplist_content##*$map*}&quot; ];then
printf &quot;\nAdding Map to maplist.txt&quot;
echo $map &gt;&gt; &quot;$maplist_file&quot;
else
printf &quot;\nMap already in maplist.txt&quot;
fi
printf &quot;\nInstallation for map %first_file% completed!&quot;
</post_install>
<uninstall>map=%mod_string%
if [ -f &quot;%mods_full_path%/../csgo/maps/$map&quot; ];then
rm -f $map
fi
map=${map%.bsp}
maplist_file=&quot;%mods_full_path%/../csgo/maplist.txt&quot;
sed -i &quot;/^$map$/d&quot; $maplist_file
</uninstall>
</workshop_settings>

View file

@ -1,71 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id>221100</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>.</mods_path>
<mods>
<mod id="1559212036">
<name>CF</name>
<description>VGhpcyBpcyBhIENvbW11bml0eSBmcmFtZXdvcmsgZm9yIERheVogU0EuDQoNCk9uZSBub3RhYmxlIGZlYXR1cmUgaXMgaXQgYWltcyB0byByZXNvbHZlIHRoZSBpc3N1ZSBvZiBjb25mbGljdGluZyBSUEMgdHlwZSBJRCdzIGFuZCBtb2RzLg0KDQpGb3IgaGVscCBvbiB1c2luZyB0aGlzIG1vZCBpbiB5b3VyIG93biBwcm9qZWN0cywgZm9sbG93IHRoaXMgUkVBRE1FIG9uIGdpdGh1YiB0aGUgQ29tbXVuaXR5LUZyYW1ld29yayBnaXRodWIgW3VybD1odHRwczovL2dpdGh1Yi5jb20vSmFjb2ItTWFuZ28vRGF5Wi1Db21tdW5pdHktRnJhbWV3b3JrL2Jsb2IvbWFzdGVyL1JFQURNRS5tZF1oZXJlWy91cmxdLg0KDQoNCltoMV1Nb25ldGl6YXRpb246Wy9oMV0NCg0KTW9uZXRpemF0aW9uIGlzIGFsbG93ZWQuIElmIHlvdSBkbyBtYWtlIG1vbmV5IHdoaWxlIHRoaXMgbW9kIGlzIGluc3RhbGxlZCBwbGVhc2UgZG8gY29uc2lkZXIgc2VuZGluZyBhIGRvbmF0aW9uLg0KDQpbaDFdUmVwYWNraW5nOlsvaDFdDQoNClVuZGVyIGFueSBjaXJjdW1zdGFuY2UgYXJlIHlvdSBub3QgYWxsb3dlZCB0byByZXBhY2sgdGhpcyBtb2QuIE5vIG9uZSB3aWxsIGV2ZXIgYmUgZ2l2ZW4gcGVybWlzc2lvbiB0byB1cGxvYWQgdGhpcyBtb2Qu</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/786364427281894860/A2C5EACFCB4CF0CC843F537E11A4BE1250E32C45/</image_url>
<download_url/>
<filename/>
<file_size>90188</file_size>
</mod>
<mod id="1899391480">
<name>Cabin_Mod</name>
<description>W2gxXUNhYmluIE1vZFsvaDFdDQoNCltiXURlc2NyaXB0aW9uOlsvYl0NCkNyYWZ0LWFibGUgTG9nIENhYmluIG1hZGUgb2YgV29vZGVuIFBsYW5rcywgTG9ncywgTmFpbHMgYW5kIFJvY2tzLg0KW2gxXVN0aWxsIFdvcmsgSW4gUHJvZ3Jlc3MgLSBzb21lIHRoaW5ncyBkbyBub3Qgd29yayBwZXJmZWN0bHkhWy9oMV0NCg0KVGhlIENhYmluIGhhcyBkZXBlbmRlbmNpZXMsIHlvdSBNVVNUIGZpcnN0IGJ1aWxkIHRoZSBmb3VuZGF0aW9uIG1hZGUgb2Ygcm9ja3MuDQpUaGVuIHRoZSBmbG9vciwgdGhlIHdhbGxzIG9uZSBhZnRlciBhbm90aGVyIChsZWZ0IHdhbGwgaXMgYWxzbyBjb25zdW1pbmcgcm9ja3MgZm9yIHRoZSBjaGltbmV5KSBhbmQgdGhlbiB0aGUgcm9vZiBhbmQgdGhlIGRvb3IgKENvZGVMb2NrIHRlc3RlZCwgQ29tYmluYXRpb25Mb2NrcyBhbHNvIHdvcmspDQoNCltiXUNyZWRpdHM6Wy9iXQ0KLSBUaGUgQ2FiaW4gTW9kIHdhcyBpbnNwaXJlZCBieSBodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PVdtWUNVbGpzckRnDQotIEhlbm5lc3N5L0Nob3BwZXIgLSBiZXN0IGJveXMgYXJvdW5kIQ0KLSAzRCBNb2RlbCBwdXJjaGFzZWQgZnJvbSBodHRwczovL3d3dy50dXJib3NxdWlkLmNvbS9GdWxsUHJldmlldy9JbmRleC5jZm0vSUQvMTE4NTUzOA0KLSBVc2luZyB0aGUgVHVyYm9TcXVpZCBSb3lhbHR5IEZyZWUgTGljZW5zZQ0KICAoaHR0cHM6Ly9ibG9nLnR1cmJvc3F1aWQuY29tL3JveWFsdHktZnJlZS1saWNlbnNlLyNnYW1lLW1vZHMpDQotIFBpY3R1cmUgbWFkZSBieSBNaW11cyA6KQ0KDQpbYl1UbyBhZGQgdGhlIG1vZCB0byB5b3VyIHNlcnZlcjpbL2JdDQpDb3B5IHRoZSBAQ2FiaW5fbW9kIGZvbGRlciB0byB5b3VyIHNlcnZlciBtb2QgZm9sZGVyDQpDb3B5IHRoZSBjb250ZW50IG9mIHRoZSBrZXlzIGZvbGRlciB0byB5b3VyIHNlcnZlciBrZXlzIGZvbGRlcg0KQ29weSBjb250ZW50IGZyb20gdGhlIHR5cGVzLnhtbCB0byB5b3VyIHNlcnZlciB0eXBlcy54bWwNCg0KW2JdTWF0ZXJpYWxzIG5lZWRlZDpbL2JdDQpDYWJpbiBLaXQgICAgICAtIDggUm9ja3MgYW5kIDUgV29vZGVuIFBsYW5rcw0KRm91bmRhdGlvbiAgLSA0MCBSb2NrcyAoc3RhY2thYmxlKQ0KRmxvb3IgICAgICAgICAgICAtIDMwIE5haWxzICYgNTAgUGxhbmtzDQpSZWFyIHdhbGwgICAgIC0gMzAgTG9ncyAmIDMwIE5haWxzDQpGcm9udCB3YWxsICAgIC0gMzAgTG9ncyAmIDMwIE5haWxzDQpSaWdodCB3YWxsICAgIC0gMzAgTG9ncyAmIDMwIE5haWxzDQpMZWZ0IHdhbGwgICAgICAtIDMwIExvZ3MgJiAzMCBOYWlscyAmIDIwIFJvY2tzDQpEb29yICAgICAgICAgICAtIDEwIE5haWxzICYgMTAgUGxhbmtzDQpSb29mICAgICAgICAgICAtIDQwIE5haWxzICYgNDAgUGxhbmtzDQoNCltiXVRvb2xzIG5lZWRlZDpbL2JdDQpTaG92ZWwgKG9ubHkgZm9yIHRoZSBmb3VuZGF0aW9uKSwgSGFtbWVyIG9yIEhhdGNoZXQgKHVubGVzcyBzZXJ2ZXIgYWRtaW5zIGRpc2FibGVkIEhhdGNoZWQgYmVpbmcgdXNlZCBhcyBIYW1tZXIpDQoNCltiXVNlcnZlci1BZG1pbnM6Wy9iXQ0KVGhpcyBtb2QgYWxsb3dzIHNlcnZlci1hZG1pbnMgdG8gYWRqdXN0IHRoZSBhbW91bnQgb2YgbWF0ZXJpYWxzIGJlaW5nIHVzZWQgZm9yIGJ1aWxkaW5nIHRoZSBjYWJpbi4NCk1vZCBicmluZ3MgYSBjYWJpbmV0IHdpdGggMTUwIHNsb3RzIGFuZCBhdHRhY2hhYmxlIGl0ZW1zIGZvciBhZGRpdGlvbmFsIHNwYWNlLg0KVGhlIENhYmluIG1vZCBtdXN0IGJlIHN0YXJ0ZWQgYWZ0ZXIgdGhlIENvZGVMb2NrIG1vZC4NCg0KW2JdUGxheWVyLWhpbnQ6Wy9iXQ0KT25jZSBjcmFmdGVkIHRoZSBUb29sYm94LCB0aGUgcGxhY2Ugd2hlcmUgeW91IHNwYXduIGl0LCBpcyB0aGUgcGxhY2Ugd2hlcmUgeW91IGhhdmUgdG8gYWRkIG1hdGVyaWFscy4NCllvdSBoYXZlIHRvIHN0YXJ0IHdpdGggdGhlIHJvY2tzIGZvciB0aGUgZm91bmRhdGlvbiENCk9uY2UgeW91IHJlYWNoZWQgdGhlIGFtb3VudCBvZiBtYXRlcmlhbHMsIHlvdSBhcmUgcmVhZHkgdG8gZ28uIENsaWNrIE5leHQgaWYgeW91IHdhbnQgdG8gc3dpdGNoIG9yZGVyIG9mIGJ1aWxkaW5nIHRoZSB3YWxscy4NCldpbmRvd3MgYXJlIG5vdCBidWxsZXQtcHJvb2YhIA0KT24gc2VydmVycyB3aXRoIGJ1aWxkLWFueXdoZXJlIHlvdSBjYW4gcGxhY2UgYSBmZW5jZSBiZWZvcmUgdGhlIHdpbmRvdy4gU2VydmVycyB3aXRob3V0Li4ud29ya2luZyBvbiBpdC4NCg0KW2JdUGVybWlzc2lvbjpbL2JdDQouLi4gaXMgZ2l2ZW4gdG8gcmVwYWNrLg0KLi4uIGlzIE5PVCBnaXZlbiBmb3IgYW55IGtpbmQgb2YgbW9uZXRhcml6YXRpb24hDQoNCltiXUtub3duIElzc3VlczogSWYgeW91IHBsYWNlIGEgc3RvcmFnZSBvYmplY3QgdG9vIGNsb3NlIHRvIHRoZSBsZWZ0L3JlYXIvZnJvbnQvcmlnaHQgd2FsbCwgeW91IGNhbiBhY2Nlc3MgdGhlIHN0b3JhZ2Ugb2JqZWN0IGZyb20gdGhlIG91dHNpZGUuIFdvcmthcm91bmQ6IEEgZmVuY2UgaW5zaWRlIHRoZSBjYWJpbiAod29ya3Mgb25seSB3aXRoIGJ1aWxkLWFueXdoZXJlIG9yIHBsYWNlIHRoZSBzdG9yYWdlIG9iamVjdCBtb3JlIGludG8gdGhlIG1pZGRsZSB1bnRpbCBpdCBpcyBub3QgdmlzaWJsZSBhbnltb3JlIGZyb20gdGhlIG91dHNpZGUuIFsvYl0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/755968752090355253/7D17BEA9A3F27C5E99936A681985E22834802AFD/</image_url>
<download_url/>
<filename/>
<file_size>30299992</file_size>
</mod>
<mod id="1896108455">
<name>Sector 9 Weapons</name>
<description>VGhpcyBpcyBhIG1vZCB0byBhZGQgaW4gbW9yZSB3ZWFwb25zIHRvIERheVogZm9yIHRoZSBTZWN0b3IgOSBTZXJ2ZXJzLg0KDQpXZWFwb25zIGN1cnJlbnRseSBhZGRlZDoNCk1HNDIgKEdlcm1hbiBMTUcpIHdpdGggNTBSbmQgTWFnDQpCcm93bmluZyAxOTI4IHZlcnNpb24gd2l0aCAyMFJuZCBNYWcNCg0KDQpKb2luIG91ciBkaXNjb3JkISBodHRwOi8vZGlzY29yZC5nZy9VZGdYMlVFDQoNCkpvaW4gdGhlIHNlcnZlciB2aWEgRFpTQSBMYXVuY2hlciEgSVA6W2JdMTA4LjE3OC43LjEyNjoyMzAyWy9iXQ0KDQpJIERPIE5PVCBBTExPVyBUSElTIE1PRCBUTyBCRSBVTlBBQ0tFRCBPUiBSRS1VUExPQURFRCBOT1IgVVNFRCBPTiBBTlkgT1RIRVIgU0VSVkVSIQ0KDQpQbGVhc2UgRE0gbWUgb24gZGlzY29yZCBpZiB5b3UgYXJlIHdhbnRpbmcgdG8gYXNrIGZvciBwZXJtaXNzaW9uIHRvIHVzZSB0aGVzZSB3ZWFwb25zLg==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/764976585413131037/7CC60E9F69FDCAD612FDA6FCF62AB91C0EDC432A/</image_url>
<download_url/>
<filename/>
<file_size>33807531</file_size>
</mod>
<mod id="1582756848">
<name>ZomBerry Admin Tools</name>
<description>RGF5WiAxLjA3IENvbXBhdGlibGUhDQpTaW1wbGUgYW5kIGN1c3RvbWlzYWJsZSBDbGllbnQvU2VydmVyIGFkbWluIHRvb2xzIHdpdGggR1VJICh3b3JrcyBpbiBib3RoIFNQIGFuZCBNUCkNCg0KTmVlZCBoZWxwPyBEaXNjb3JkOiBodHRwczovL2Rpc2NvcmQuZ2cvQmZNWnhSaA0KDQpDaGFuZ2Vsb2c6DQp2MC41LjktcHJlcCAtIGEgdHJhbnNpdGlvbmFsIDAuNS45LzEuMCB1cGRhdGUgKHlvdSBndXlzIGFyZSBwcm9iYWJseSBib3JlZCBvZiAwLjUuOSwgaHVoPykNCg0KdjAuNS45eCAtIERheVogMS4wMiBjb21wYXRpYmlsaXR5IHVwZGF0ZQ0KDQp2MC41LjlzKyAtIFVwZGF0ZWQgUmVwYWlyIGFuZCByZWZ1ZWwgZnVuYyBmb3IgMS4wMiwgdW5iYW5uZWQgc2NvdXQgc2NvcGUsIGZpeGVkIGNvbmZpZyBsb2FkaW5nIHNlcXVlbmNlIGEgYml0DQoNCnYwLjUuOCAtIFN0ZWFtNjQgc3VwcG9ydCwgRnJlZUNhbSBhbmQgY2hhdCBmaXhlcywgc2VwYXJhdGUgbG9nIGZpbGVzLCBKU09OaXplZCBtYWluIGNvbmZpZyBmaWxlLCBjdXN0b20gZmlsdGVycyBmb3Igc3Bhd24gbWVudSAoZG9uJ3QgZm9yZ2V0IHRvIHVwZGF0ZSB5b3VyIHNlcnZlciEpDQoNCnYwLjUuNyAtIEFkZGVkIGdvZCBtb2RlLCBmaXhlZCB1cGRhdGUgbm90aWZpY2F0aW9ucw0KDQp2MC41LjYgLSBGaXhlZCBzZWFyY2ggaW5wdXQgaW4gIlNwYXduIG1lbnUiIHRhYiwgbGl0dGxlIFVJIGFkanVzdG1lbnRzDQoNCnYwLjUuNSAtIEZpeGVkIHBvc3NpYmxlIGVycm9ycyB3aGVuIGZ1bmN0aW9uIGlzIGJlaW5nIGV4ZWN1dGVkIG9uIGRpc2Nvbm5lY3RlZCBwbGF5ZXIsIHNvbWUgc2VydmVyLXNpZGUgb3B0aW1pemF0aW9ucywgYWRqdXN0ZWQgbWFwIGxvb2tzIGJhc2VkIG9uIGZlZWRiYWNrDQoNCnYwLjUuNCAtIENoYW5nZWQgZGVmYXVsdCBpdGVtIHNwYXduIHR5cGUgdG8gT25DdXJzb3IsIG1pbm9yIGZpeGVzLCAxLjEgbWFwIGZpeCAoeWVwLCBtYXAgSVMgd29ya2luZyEpDQoNCnYwLjUuMyAtIFVzZXIgZGVmaW5lZCBLZXlCaW5kcyBhZGRlZA0KDQp2MC41IC0gTWFqb3IgdW5kZXItdGhlLWhvb2QgY2hhbmdlcywgbGl0dGxlIFVJIHVwZ3JhZGVzLCBhZGRlZCAiSW5zdGFsbGF0aW9uIG1vZGUiIGZvciBpbml0aWFsIHNlcnZlciBjb25maWd1cmF0aW9uICgtemJyeUluc3RhbGxNb2RlPXRydWUgbGF1bmNoIG9wdGlvbikNCkRvbid0IGZvcmdldCB0byB1cGRhdGUgYm90aCBzZXJ2ZXItIGFuZCBjbGllbnQtIHNpZGUsIGFzIHZlcnNpb24gbWlzbWF0Y2ggbWlnaHQgY2F1c2UgcHJvYmxlbXMuDQoNCkRvY3VtZW50YXRpb24gb24gR2l0SHViOg0KW3VybD1odHRwczovL2dpdGh1Yi5jb20vTW9vbmRhcmtlci9ab21CZXJyeS1EYXlaQWRtaW5Ub29sc11HaXRIdWIgbGlua1svdXJsXQ0KDQpSZXVwbG9hZGluZyAmIHJlcGFja2luZyB0aGlzIG1vZCB3aXRob3V0IHBlcm1pc3Npb24gcmVxdWVzdCBpcyBub3QgYWxsb3dlZCBeXg0KDQpGQVEgJiBIb3cgdG8gaW5zdGFsbCBpbiBkaXNjdXNzaW9ucw==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/955224589383459935/28DDCC9D0645BCC795A7FE5B24971AE10175CC41/</image_url>
<download_url/>
<filename/>
<file_size>2981868</file_size>
</mod>
<mod id="2048694610">
<name>JunkYardDog</name>
<description>QWRkcyB0aGUgYWJpbGl0eSB0byBzYWx2YWdlIHBhcnRzICh1c2luZyBhIHdyZW5jaCkgYW5kIGZ1ZWwgZnJvbSB3cmVja3Mgb24gdGhlIG1hcCwgY2FuIGFsc28gc2lwaG9uIGZ1ZWwgZnJvbSBhY3RpdmUgY2Fycy4gDQpTb21lIGZ1ZWwgaXMgbG9zdCB3aGVuIHNpcGhvbmluZy4NCiBBZGRzIHRoZSBhYmlsaXR5IHRvIHJlZmlsbCBjb250YWluZXJzIGF0IHRoZSBNZWRpdW0gZnVlbCB0YW5rcyB3aXRoIGxhZGRlcnMsIGFuZCBhcyBhIHNpZGUgbm90LCB0YWtlIGNhdXRpb24gYXMgdGhleSBhbHNvIGV4cGxvZGUgd2l0aCBtb3JlIGZvcmNlIHRoYW4gdGhlIGZ1ZWwgcHVtcHMuIA0KW2JdRklYRURbL2JdOiBubyBsb25nZXIgYWJsZSB0byByZWZpbGwgY29udGFpbmVycyBhdCBydWluZWQgcHVtcHMuIA0KDQpbYl1QUk8gVElQWy9iXTogSXRzIGlzIGFkdmlzYWJsZSB0byBicmluZyB0d28gY29udGFpbmVycyB3aXRoIHlvdSB3aGVuIHNpcGhvbmluZyBmdWVsLCB5b3UgY2FuIG5vdCBzaXBob24gaW50byBhIGNvbnRhaW5lciB0aGF0IGFscmVhZHkgaGFzIGxpcXVpZCBpbiBpdC4gWW91IHdpbGwgbmVlZCB0byBlbXB0eSB0aGUgY29udGFpbmVyIG9yIHBvdXIgbGlxdWlkIGludG8gYSBsYXJnZXIgY29udGFpbmVyIHRvIHNhdmUgaXQuIEJlY2FyZWZ1bCBub3QgdG8gc3dvbGxvdyBhbnkgZnVlbCBhbmQgbWFrZSBzdXJlIHRvIHdlYXIgZ2xvdmVzIHdoZW4gc2FsdmFnaW5nIHBhcnRzLCB5b3UgZG9udCB3YW50IHRvIGdldCBidXN0ZWQga251Y2tsZXMuIA0KDQpDdXJyZW50bHkgdGhpcyBpcyBmb3IgQm90aCB2YW5pbGxhIG1hcHMNCg0KUGxlYXNlIHJlcG9ydCBhbnkgaXNzdWVzIHlvdSBmaW5kIQ0KDQpbYl1UT0RPWy9iXToNCi1NYWtlIHVuaXZlcnNhbCBmb3IgYWxsIG1hcHMuDQotQWRkIHNlcnZlciBjb25maWcgc2V0dGluZ3MgZm9yIHBhcnRzIGFycmF5DQoNCltiXU90aGVyIE1vZHM6Wy9iXQ0KDQpbdXJsPWh0dHBzOi8vc3RlYW1jb21tdW5pdHkuY29tL3NoYXJlZGZpbGVzL2ZpbGVkZXRhaWxzLz9pZD0yMDM5NDQ4MDU4XU5vTXVmZmxlIFsvdXJsXS1SZW1vdmVzIG11ZmZsZWQgdm9pY2UgZnJvbSBoZWxlbXRzIGFuZCBnYXMgbWFza3MNCg0KW3VybD1odHRwczovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9MjA0MTkwNDk3N11WZW5kaW5nU2VhcmNoIFsvdXJsXS1BZGRzIGFiaWxpdHkgdG8gc2VhcmNoIHZlbmRpbmcgbWFjaGluZXMgZm9yIGRyaW5rcw0KDQoNCltiXUNSRURJVFNbL2JdOg0KS3VyZG8gLSBncmFwaGljcyBhbmQgaW1hZ2VzIGFuZCB0ZXN0aW5nDQpNb3N0YWNob0dHIC0gaW50ZWxsZWN0dWFsIGlucHV0IGFuZCB0ZXN0aW5nDQpQbGF5ZGFjaGkgLSBpbnRlbGxlY3R1YWwgaW5wdXQgYW5kIHNjcmlwdGluZw0KDQpbYl1VU0FHRSAmIFRFUk1TWy9iXToNCi0gWW91IG1heSBub3QgcmVwYWNrIG9yIHB1Ymxpc2ggdGhpcyBtb2Qgb24gYW55IHBsYXRmb3JtIGluY2x1ZGluZyBTdGVhbS4NCg0KW2JdUEVSTUlTU0lPTiBJUyBOT1QgR1JBTlRFRCBGT1IgVEhJUyBNT0QgVE8gQkUgSU5DTFVERUQgSU4gQSAiU0VSVkVSIFBBQ0siIG9yICJNT0QgUEFDSyIuDQpVc2UgYSBDb2xsZWN0aW9uIGlmIHlvdSB3YW50IHRvIGluY2x1ZGUgdGhpcyBtb2Qgb24geW91ciBzZXJ2ZXIgZm9yIHlvdXIgdXNlcnMuWy9iXQ0KDQpDb3B5cmlnaHQgwqkgMjAyMCBbaV1aZWRtYWdbL2ldDQoNCg0KW2JdUGxlYXNlIGNvbnNpZGVyIGRvbmF0aW5nLCBUaGFuayB5b3UhWy9iXQ0KW3VybD1odHRwczovL3N0cmVhbWxhYnMuY29tL2Nvd2JveW1pbGxlcl1baW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vaUZnMFYwWC5wbmdbL2ltZ11bL3VybF0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1022822776633980236/39F0EC933CC2874160552C5A371CEE055D8792DA/</image_url>
<download_url/>
<filename/>
<file_size>64773</file_size>
</mod>
<mod id="2058634870">
<name>Materials for construction</name>
<description>W2gxXU1hdGVyaWFscyBmb3IgY29uc3RydWN0aW9uWy9oMV0NCg0KW2ldTW9kIA0KK2FkZCA0IHNwYXduIHBvaW50cyBQaWxlIE9mIE1ldGFsIFNoZWV0cyAgICAgIA0KY2xhc3MgbmFtZSBmb3Igc2VydmVyIGFkbWluaXN0cmF0b3JzID0gUE9NU19QaWxlT2ZNZXRhbFBsYXRlDQpbaW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vNXczWVMxci5qcGdbL2ltZ10NCltpbWddaHR0cHM6Ly9pLmltZ3VyLmNvbS83eEZDZEtMLmpwZ1svaW1nXQ0KW2ltZ11odHRwczovL2kuaW1ndXIuY29tLzNlS1dxR3UuanBnWy9pbWddDQpbaW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vMXZmNnN4dS5qcGdbL2ltZ10NCg0KK2FkZCA0IHNwYXduIHBvaW50cyBQaWxlIE9mIE1ldGFsIFNoZWV0cyBMaXZvbmlhDQpbaW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vaG8wcWtwMi5qcGdbL2ltZ10NCltpbWddaHR0cHM6Ly9pLmltZ3VyLmNvbS9pa3VnRk9ELmpwZ1svaW1nXQ0KW2ltZ11odHRwczovL2kuaW1ndXIuY29tL0E5RzRvUkEuanBnWy9pbWddDQpbaW1nXWh0dHBzOi8vaS5pbWd1ci5jb20vWGZONHpndy5qcGdbL2ltZ10NCi0tLS0tLS0tLS0tLS0tLS0NCg0KK2FkZHMgdG9vbHMgQ2xpcHBlciBmb3IgY3V0dGluZyBQaWxlIE9mIE1ldGFsIFNoZWV0cyBvbiBNZXRhbCBTaGVldHMgICAgICAgDQpjbGFzcyBuYW1lIGZvciBzZXJ2ZXIgYWRtaW5pc3RyYXRvcnMgPSBQT01TX01ldGFsX0NsaXBwZXINCltpbWddaHR0cHM6Ly9pLmltZ3VyLmNvbS9wY0Nkck9uLmpwZ1svaW1nXQ0KDQp0aGUgdHlwZXMgZm9sZGVyIGNvbnRhaW5zIGV2ZXJ5dGhpbmcgeW91IG5lZWQgZm9yIHNlcnZlcnNbL2ldDQoNCi0tLT09PXw9PT0tLS0NCg0KW2gxXdCc0LDRgtC10YDQuNCw0LvRiyDQtNC70Y8g0YHRgtGA0L7QuNGC0LXQu9GM0YHRgtCy0LBbL2gxXQ0KDQpbaV3QnNC+0LQgDQor0LTQvtCx0LDQstC70Y/QtdGCIDQg0YLQvtGH0LrQuCDRgdC/0LDQstC90LAg0YjRgtCw0LHQtdC70Y8g0LzQtdGC0LDQu9C70LjRh9C10YHQutC40YUg0LvQuNGB0YLQvtCyICAgICAgICAgIA0KY2xhc3MgbmFtZSDQtNC70Y8g0LDQtNC80LjQvdC40YHRgtGA0LDRgtC+0YDQvtCyINGB0LXRgNCy0LXRgNC+0LIgPSBQT01TX1BpbGVPZk1ldGFsUGxhdGUNCivQtNC+0LHQsNCy0LvRj9C10YIgNCDRgtC+0YfQutC4INGB0L/QsNCy0L3QsCDRiNGC0LDQsdC10LvRjyDQvNC10YLQsNC70LvQuNGH0LXRgdC60LjRhSDQu9C40YHRgtC+0LIgIExpdm9uaWENCi0tLS0tLS0tLS0tLS0tLS0NCg0KK9C00L7QsdCw0LLQu9GP0LXRgiDQuNC90YHRgtGA0YPQvNC10L3RgiDQmtGD0YHQsNGH0LrQuCDQtNC70Y8g0YDQtdC30LrQuCDRiNGC0LDQsdC10LvRjyDQvNC10YLQsNC70LvQuNGH0LXRgdC60LjRhSDQu9C40YHRgtC+0LIg0L3QsCDQvtCx0YvRh9C90YvQtSDQvNC10YLQsNC70Lsg0LvQuNGB0YLRiyAgICAgICAgICANCmNsYXNzIG5hbWUg0LTQu9GPINCw0LTQvNC40L3QuNGB0YLRgNCw0YLQvtGA0L7QsiDRgdC10YDQstC10YDQvtCyID0gUE9NU19NZXRhbF9DbGlwcGVyDQoNCtCyINC/0LDQv9C60LUgdHlwZXMg0L3QsNGF0L7QtNC40YLRgdGPINCy0YHQtSDQvdC10L7QsdGF0L7QtNC40LzQvtC1INC00LvRjyDRgdC10YDQstC10YDQvtCyWy9pXQ0KDQpbaV15b3UgY2FuIGFkZCB5b3VyIG93biBzcGF3biBwb2ludHMsIGFkZGluZyBjb29yZGluYXRlcyB0byB0aGUgZmlsZSBjZmdldmVudHNwYXducy54bWxbL2ldDQoNCi0tLT09PT09PS0tLQ0KDQpbaV3QstGLINC80L7QttC10YLQtSDQtNC+0LHQsNCy0LjRgtGMINGB0LLQvtC4INGB0L7QsdGB0YLQstC10L3QvdGL0LUg0YLQvtGH0LrQuCDRgdC/0LDQstC90LAsINC00L7QsdCw0LLQuNCyINC60L7QvtGA0LTQuNC90LDRgtGLINCyINGE0LDQudC7IGNmZ2V2ZW50c3Bhd25zLnhtbFsvaV0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/999179297231231643/075074238C5691636E42F5F9B26B8070C362E4E1/</image_url>
<download_url/>
<filename/>
<file_size>9218416</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable/>
<place_after/>
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>\n</string_separator>
<filepath>workshop_installed.txt</filepath>
</config>
<post_install>cp -Rf "%mods_full_path%/steamapps/workshop/content/221100/%workshop_mod_id%" "%mods_full_path%/%workshop_mod_id%"&#xD;
rm -Rf "%mods_full_path%/steamapps/workshop/content/221100/%workshop_mod_id%"&#xD;
</post_install>
<uninstall>printf "\nUninstalling...\n"&#xD;
rm -Rf "%mods_full_path%/%workshop_mod_id%"</uninstall>
</workshop_settings>

File diff suppressed because one or more lines are too long

View file

@ -1,59 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id>107410</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>.</mods_path>
<mods>
<mod id="1199493544">
<name>ArmA 2 Anims To ArmA 3 | A2ATA3</name>
<description>W3F1b3RlXVtoMV1Bcm1BMiBBbmltcyBUbyBBcm1BMyAoQTJBVEEzKSAtIG1vZCBmb3IgQXJtQTMgdGhhdCBjaGFuZ2VzIHRoZSBtb3N0IHBhcnQgb2YgbW92ZW1lbnQgYW5pbWF0aW9ucyBpbiBBMyB0byBBMiBhbmltYXRpb25zLlsvaDFdWy9xdW90ZV0NCltjb2RlXVdhcm5pbmc6IFRoaXMgbW9kIGlzIGluIGRldmVsb3BtZW50LCBzbyBpZiB5b3Ugbm90aWNlIGFueSBwcm9ibGVtcyBqdXN0IGxldCBtZSBrbm93IDopWy9jb2RlXQ0KDQpbcXVvdGVdW2JdW2gxXUZlYXR1cmVzOlsvaDFdW2xpc3RdDQpbKl1Nb3ZlbWVudCBhbmltYXRpb25zIGluIEEzIGNoYW5nZWQgdG8gYW5pbWF0aW9ucyBmcm9tIEEyDQpbKl1Db21wbGV0ZWx5IHdvcmtpbmcsIGZ1bGx5IGZpeGVkIGFuaW1hdGlvbnNbL2xpc3RdWy9iXVsvcXVvdGVdDQoNCltjb2RlXQ0KW2NvZGVdW2JdW3VdQ1VSUkVOVCBWRVJTSU9OOiAwLjkuMi4xWy91XVsvYl1bL2NvZGVdDQpbY29kZV1bYl1bdV1MQVNUIFBBVENIIENIQU5HRUxPRzpbL3VdWy9iXQ0KW2ldI3ZlcnNpb249MC45LjIuMVsvaV0NCltFRElUXSBOZXcgbG9nby4NCltpXSN2ZXJzaW9uPTAuOS4yQVsvaV0NCi0gdXBkYXRlZCB2ZXJzaW9uIGluICRQQk9QUkVGSVgkLg0KW2ldI3ZlcnNpb249MC45LjJbL2ldDQpbTkVXXSAuYmlzaWduIHVwZGF0ZWQgdG8gdjMNCltGSVhdIEZpeGVkIGFuaW1hdGlvbiBmcmVlemUgd2hlbiBwbGF5ZXIgdHJ5aW5nIHRvIHNwcmludCB3aGlsZSBhaW1pbmcgaW4gW1BsYXllciA+IE1haW4gV2VhcG9uID4gUmFpc2VkIChBaW1pbmcpID4gU3RhbmQgPiBTcHJpbnRdLg0KW0VESVRdIEZpbGUgc3RydWN0dXJlIHJld29ya2VkLg0KW1JFTU9WRURdIFJlbW92ZWQgdXNlbGVzcyBjbGFzc2VzIGZyb20gJ0EyQVRBM1xhMmFfZGF0YVxhbmltc19jZmcuaHBwJy4NCltSRU1PVkVEXSBSZW1vdmVkICd0YWN0aWNhbCcgYW5pbWF0aW9ucyBmcm9tICdBMkFUQTNcYTJhX2FuaW1zXEFuaW0nLlsvY29kZV0NCg0KW2NvZGVdW2JdW3VdTElOS1M6Wy91XVsvYl1bbGlzdF0NClsqXVt1cmw9aHR0cHM6Ly9mb3J1bXMuYm9oZW1pYS5uZXQvZm9ydW1zL3RvcGljLzIxMTc3My1hcm1hLTItYW5pbWF0aW9ucy10by1hcm1hLTMtYTJhdGEzL11CSSBGb3J1bXMgVGhyZWFkWy91cmxdDQpbKl1bdXJsPWh0dHA6Ly93d3cuYXJtYWhvbGljLmNvbS9wYWdlLnBocD9pZD0zMzUwNl1Nb2QgcGFnZSBvbiBBcm1haG9saWNbL3VybF0NClsqXVt1cmw9aHR0cHM6Ly9naXRodWIuY29tL21heGltaWxpb251cy9BMkFUQTNdR2l0aHViWy91cmxdDQpbKl1bdXJsPWh0dHBzOi8vZ2l0aHViLmNvbS9tYXhpbWlsaW9udXMvQTJBVEEzL3Byb2plY3RzLzFdR2l0aHViIERldmVsb3BtZW50IFRyYWNrZXJbL3VybF1bL2NvZGVdDQoNCltxdW90ZV1baV1JZiB5b3UgaGF2ZSBhbnkgcXVlc3Rpb25zIGZlZWwgZnJlZSB0byBjb250YWN0IG1lIHZpYTpbL2ldDQpbdXJsPWh0dHBzOi8vd3d3LnJlZGRpdC5jb20vdXNlci9tYXhpbWlsaW9udXMvXVJlZGRpdFsvdXJsXQ0KW3VybD1odHRwczovL3R3aXR0ZXIuY29tL21heGltaWxpb251c11Ud2l0dGVyWy91cmxdDQpbdXJsPWh0dHBzOi8vdmsuY29tL21heGltaWxpb251c2NvbW1dVktbL3VybF0NClt1cmw9aHR0cHM6Ly9mb3J1bXMuYm9oZW1pYS5uZXQvcHJvZmlsZS8xMTM5MDYwLW1heGltaWxpb251cy9dQkkgRm9ydW1zWy91cmxdDQpbdXJsPWh0dHA6Ly93d3cuYXJtYWhvbGljLmNvbS91c2Vycy5waHA/bT1kZXRhaWxzJmlkPTkyNTUyJnU9bWF4aW1pbGlvbnVzXUFybWFob2xpY1svdXJsXQ0KW3VybD1odHRwOi8vc3RlYW1jb21tdW5pdHkuY29tL3Byb2ZpbGVzLzc2NTYxMTk4MDUwOTUyMTU2XVN0ZWFtWy91cmxdWy9xdW90ZV0NCltxdW90ZV1BbmQuLi4gZWhlbS4uLiBpZiB5b3Ugd2FudCB0byBzdXBwb3J0IG1lIGFuZCBteSB3b3Jrcy4uLiB3ZWxsLi4uIGhlcmVzIG15IFt1cmw9cGF5cGFsLm1lL21heGltaWxpb251c21dUGF5cGFsWy91cmxdIDopWy9xdW90ZV0NClsvY29kZV0NCltjb2RlXVt1cmw9aHR0cHM6Ly93d3cuYmlzdHVkaW8uY29tL2NvbW11bml0eS9saWNlbnNlcy9hcm1hLXB1YmxpYy1saWNlbnNlLXNoYXJlLWFsaWtlXVtpbWddaHR0cHM6Ly9pLmltZ3VyLmNvbS9MSndZdlpCLnBuZ1svaW1nXVsvdXJsXVsvY29kZV0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/776228609003078177/39522B183D24DA8D393E0D77978831FD00EA4955/</image_url>
<download_url/>
<filename/>
<file_size>64638590</file_size>
</mod>
<mod id="612930542">
<name>RDS Civilian Pack</name>
<description>W2JdQTIgRWFzdGVybiBUaGVtZWQgQ2l2aWxpYW4gUGFjayB3aGljaCBjb250YWlucyBkaWZmZXJlbnQgdmVoaWNsZXMgJiBjaGFyYWN0ZXJzOlsvYl0NCklrYXJ1cyAyNjANClNrb2QgMTIwMw0KU2tvZGEgT2N0YXZpYSBJSSAyLjAgVERJDQpWVyBHb2xmIElWIDEuOSBUREkgKEkga25vdyBpbiBmYWN0IGl0J3MgMS42IEZTSSBidXQgSSBsaWtlIHRoYXQgdmVyc2lvbiA6UCkNClZBWi0yMTAzDQpHQVotMjQNClpldG9yIFRyYWN0b3INCllhbWFoYSBUVC02NTAgDQpKYXdhIDM1Mw0KMiBCaWtlcyAoQTIgb2xkICYgbW91bnRhaW4gYmlrZSkNCg0KQTIgQ2l2aWxpYW5zIHdpdGggd29ya2luZyBpbnZlbnRvcnksIGV0Yy4gaS5lLiBwb2xpY2VtYW4sIHdvcmtlciwgd29vZGxhbmRlciwgZG9jdG9yLCBwcm9maXRlZXIsIGJ1c2luZXNzbWVuLCBwb3AgKHByaWVzdCkuDQoNCkFsc28sIGlmIHlvdSBmZWVsaW5nIHlvdSBoYXZlIHNvbWUgc3BhcmUgYnVja3MsIHlvdSBjYW4gbm93IG1ha2UgZG9uYXRpb24gOykNClt1cmw9aHR0cHM6Ly93d3cucGF5cGFsLmNvbS9jZ2ktYmluL3dlYnNjcj9jbWQ9X3MteGNsaWNrJmhvc3RlZF9idXR0b25faWQ9Q1hEVDI5S0daRkNDTl1baW1nXWh0dHBzOi8vd3d3LnBheXBhbG9iamVjdHMuY29tL2VuX0dCL2kvYnRuL2J0bl9kb25hdGVfTEcuZ2lmWy9pbWddWy91cmxdDQoNCg0KW2JdTGF0ZXN0IGNoYW5nZXMgWzEuMzBdOlsvYl0NCisgYWRkZWQgSmF3YSAzNTMsIFlhbWFoYSBUVC02NTAgbW90b3JjeWNsZSAmIDIgQmlrZXMgKE9sZCAmIG1vdW50YWluIGJpa2UpDQorIGFkZGVkIDIgaGFuZGhlbGQgZmxhc2hsaWdodHMgLSBKYW50YSAmIExUUy0xDQorIGFkZGVkIHNob3J0L2xvbmcgbGlnaHQgdG9nZ2xlIChyIC0gdG9nZ2xlIGxpZ2h0LCB0IC0gdG9nZ2xlIGNhYmluIGxpZ2h0KQ0KKyBhZGRlZCBlbmdpbmUgZGVzdHJ1Y3Rpb24gZWZmZWN0ICYgaW1wcm92ZWQgaGl0cG9pbnRzIG9uIGFsbCB2ZWhpY2xlcw0KKyBhZGRlZCBSb2NrZXIgY2hhcmFjdGVyIGZyb20gQTINCisgYWRkZWQgcmFuZG9tIGNpdmlsaWFucyBjbGFzcw0KKyBhZGRlZCBncm91cHMgb2YgcmFuZG9tIGNpdmlsaWFucw0KKyBhZGRlZCBlZGVuIHByZXZpZXdzIGltYWdlcw0KKyBhZGRlZCBzb21lIG1vcmUgZWRlbiBhdHRyaWJ1dGVzIChvcGVuIGRvb3IvdHJ1bmssIGJsaW5rZXJzIGNvbnRyb2wpDQorIGFkZGVkIHVuaXF1ZSBwaWN0dXJlcyB0byBhbGwgdW5pZm9ybSB2YXJpYW50cw0KKyBhZGRlZCBjYXIgYWxhcm0gZm9yIGxvY2tlZCB2ZWhpY2xlcyAoYXBwbGllcyB0byBHb2xmIElWICYgT2N0YXZpYSkgLSBsYXVuY2hlZCB1cG9uIGhpdCBvciBieSBmaXJlIGZyb20gbGFyZ2UgY2FsaWJlciBndW5zDQpeIGNoYW5nZWQgZW1lcmdlbmN5IGxpZ2h0IGtleWJpbmQgdG8gY3RybCtnIChjeWNsZSBuZXh0IGdyZW5hZGUga2V5KQ0KXiBpbXByb3ZlZCBibGlua2VycyBVSSBoYW5kbGVyIChjaGFuZ2luZyBiZXR3ZWVuIHZlaGljbGUgdGhyb3VnaCBWRyBzaG91bGQgcHJlc2VydmUgYWJpbGl0eSB0byBhY3RpdmF0ZSBibGlua2VycykNCl4gdHdlYWtlZCBtaXJyb3IgcG9zaXRpb24gaW4gU2tvZGEgT2N0YXZpYQ0KXiB1cGRhdGVkIGRlc3RydWN0aW9uIHRleHR1cmVzIGZvciBtb3N0IG9mIHZlaGljbGVzDQpeIHR3ZWFrZWQgcGh5c3ggb2Ygc29tZSB2ZWhpY2xlcw0KXiByZW5hbWVkIHZlaGljbGUgc2tlbGV0b25zIGZvciBiZXR0ZXIgY3Jvc3MgbW9kIGNvbXBhdGliaWxpdHkNCkAgZml4ZWQgd2hlZWwgZHVzdCBwb3NpdGlvbg0KQCBmaXhlZCBzb21lIGl0ZW1zIHdlcmUgbWlzc2luZyBpbiBjZmdQYXRjaGVzDQpAIGZpeGVkIGluanVyeSBzZWxlY3Rpb24gb24gcmlnaHQgbGVnIGZvciBhbGwgY2hhcmFjdGVycyANCkAgbWFkZSB3b3JrYXJvdW5kIGZvciBicm9rZW4gc2VhcmNobGlnaHRzICggaHR0cHM6Ly9mZWVkYmFjay5iaXN0dWRpby5jb20vVDExODMzMCApDQpAIGZpeGVkIE9jdGF2aWEgd2luZG93cyBoaWRkaW5nIG9uIGRlc3RydWN0aW9uDQpAIGZpeGVkIHZlaGljbGUgZGFzaGJvYXJkIGlsbHVtaW5hdGlvbiANCkAgZml4ZWQgQXBleCBlcnJvcnMNCkAgZml4ZWQgcmRzX2Nhcl93YXJuaW5nX3RyaWFuZ2xlX3RvMTEgZmxhc2hsaWdodCAmIHBvaW50ZXIgLnJwdCBlcnJvcnM=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/368535631804911993/549B15695A5A0090B15707FE49BB60E4C9BD9754/</image_url>
<download_url/>
<filename/>
<file_size>482158623</file_size>
</mod>
<mod id="583496184">
<name>CUP Terrains - Core</name>
<description>W2gxXUNVUCBUZXJyYWlucyAtIENvcmVbL2gxXQ0KDQpUaGUgQ29tbXVuaXR5IFVwZ3JhZGUgUHJvamVjdCBpcyBhIGNvb3BlcmF0aXZlIGVmZm9ydCB0byBicmluZyB0aGUgY29udGVudCBvZiBCb2hlbWlhIEludGVyYWN0aXZlJ3MgDQplYXJsaWVyIGdhbWVzIChBcm1hIDIgYW5kIEFybWEgMjogT3BlcmF0aW9uIEFycm93aGVhZCBhbmQgRExDJ3MgaW4gcGFydGljdWxhcikgaW50byBBcm1hIDMsIHVwZGF0ZWQgDQp0byB0aGUgZnVuY3Rpb25hbGl0eSBhbmQgc3RhbmRhcmRzIG9mIHRoZSBuZXh0IGdlbmVyYXRpb24gZ2FtZS4gDQoNCkZvciBtb3JlIGluZm9ybWF0aW9uIG9uIHRoZSBwcm9qZWN0LCBjaGVjayBvdXIgd2VicGFnZSBhdCANCmh0dHA6Ly9jdXAtYXJtYTMub3JnLyANCm9yIHZpc2l0IHVzIG9uIG91dCBkaXNjb3JkIHNlcnZlciBhdA0KaHR0cHM6Ly9kaXNjb3JkLm1lL2N1cC1hcm1hMw0KDQpUaGlzIGlzIHRoZSBURVJSQUlOUyAtIENPUkUgcGFjaywgdGhlIHN1Y2Nlc3NvciBvZiAiQTNNUCIgYW5kICJBbGwgaW4gQXJtQSAtIFRlcnJhaW4gUGFjayAoQWlBIFRQKSIuIEl0IGNvbnRhaW5zIGFsbCB0aGUgY29yZSBkYXRhIGZvciBtYXBzIGZyb20gQXJtYTEsIEFybWEgMiBhbmQgdGhlIGV4cGFuc2lvbiBhbmQgRExDJ3MuDQoNCltiXVRISVMgV09SS1NIT1AgUEFHRSBJUyBOT1QgTU9OSVRPUkVEIEJZIFRIRSBERVZFTE9QRVJTDQpQbGVhc2UgcmVwb3J0IGJ1Z3MgdG8gDQpbaV1odHRwczovL2dvby5nbC9BVXNNbnNbL2ldWy9iXQ0KDQoNClRoaXMgcGFjayBjb250YWluczoNCltsaXN0XQ0KWypdYWxsIHRlcnJhaW5zIGNvcmUgZGF0YSBsaWtlIG1vZGVscyBhbmQgY29uZmlncyBmcm9tIHByZXZpb3VzIGFybWEgdGl0bGVzDQpbKl1jb21tdW5pdHkgbWFkZSBhZGRpdGlvbmFsIGNvbnRlbnQgdGhhdCB3YXMgZG9uYXRlZCBhbmQgZml0J3MgdGhlIHRpbWVmcmFtZVsvbGlzdF0NCg0KDQoNCltxdW90ZV0NCklNUE9SVEFOVCENClRoaXMgaXMgdGhlIENPUkUgREFUQSBwYWNrLCBpdCBbYl1bdV1ET0VTIE5PVFsvdV1bL2JdIGluY2x1ZGUgYW55IG1hcHMhDQpUbyBnZXQgdGhlIG1hcHMgZnJvbSBDVVAgVGVycmFpbnMgUGFjaywgeW91IG5lZWQgdG8gZG93bmxvYWQgdGhlIE1BUFMgUEFDSw0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9NTgzNTQ0OTg3DQpbL3F1b3RlXQ0KDQoNCltxdW90ZV0NCltiXVt1XUFOWSBSRVVQTE9BRFMgKFNUQU5EQUxPTkUgT1IgUEFSVCBPRiBNT0RQQUNLUykgVE8gVEhFIFNURUFNIFdPUktTSE9QIChBUk1BMyAmIERBWVopIEFSRSBQUk9ISUJJVEVEIEFORCBWSU9MQVRJTkcgVEhFIFNURUFNIFdPUktTSE9QIEVVTEEgU0VDVElPTiA2RCwgQVMgV0VMTCBBUyBUSEUgQ1VQIExJQ0VOU0UuIFJFVVBMT0FEUyBXSUxMIEJFIFRBS0VOIERPV04gVklBIERNQ0EgTk9USUNFIFdJVEhPVVQgV0FSTklORyFbL3VdWy9iXQ0KWy9xdW90ZV0gDQo=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/954108744283705578/9057AFA885298D149510454FA270399226B50A9C/</image_url>
<download_url/>
<filename/>
<file_size>1032741022</file_size>
</mod>
<mod id="868032727">
<name>DesolationREDUX</name>
<description>RGVzb2xhdGlvblJFRFVYIGlzIHRoZSBzcGlyaXR1YWwgc3VjY2Vzc29yIHRvIERlc29sYXRpb25Nb2QuIFJFRFVYIGlzIGEgbGFyZ2Ugc2NhbGUgc3Vydml2YWwgbW9kIHdoZXJlIHRoZSBwbGF5ZXIgbXVzdCBnYXRoZXIgcmVzb3VyY2VzLCBidWlsZCBhIGhvbWUsIGFuZCBkZWZlbmQgdGhlbXNlbHZlcyBmcm9tIG90aGVycyBsb29raW5nIHRvIHRha2Ugd2hhdCB0aGV5IGhhdmUuIA0KDQpXZWJzaXRlOiBbdXJsPWh0dHA6Ly9kZXNvbGF0aW9ucmVkdXguY29tXWh0dHA6Ly9kZXNvbGF0aW9ucmVkdXguY29tWy91cmxdDQpXSUtJOiBbdXJsPWh0dHA6Ly93aWtpLmRlc29sYXRpb25yZWR1eC5jb21daHR0cDovL3dpa2kuZGVzb2xhdGlvbnJlZHV4LmNvbVsvdXJsXQ0KDQpXZSBoYXZlIGluY2x1ZGVkIFRoZXN1cyBTZXJ2aWNlcyBpbnRvIG91ciBhZGRvbnM6IGh0dHBzOi8vZm9ydW1zLmJpc3R1ZGlvLmNvbS9mb3J1bXMvdG9waWMvMTg5MTY3LXRoZXNldXMtc2VydmljZXMv</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/854970836911038557/25437F8ED3570D240C6D1F5A75B5BA9D2CEDBF5D/</image_url>
<download_url/>
<filename/>
<file_size>1872893114</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable/>
<place_after/>
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>;</string_separator>
<filepath>workshop_installed.txt</filepath>
</config>
<post_install>modname=$( awk -F "=" </post_install>
<uninstall>a=%mods_full_path%&#xD;
&#xD;
modid=$(find $a -iname "%mod_string%")&#xD;
&#xD;
modfolder=$(dirname $modid)&#xD;
echo $modfolder &#xD;
rm -Rf $modfolder&#xD;
printf "\n %mod_string% automatically uninstall. \n" </uninstall>
</workshop_settings>

File diff suppressed because one or more lines are too long

View file

@ -1,19 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id/>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path/>
<mods/>
<config>
<regex/>
<mods_backreference_index/>
<variable/>
<place_after/>
<mod_string/>
<string_separator/>
<filepath/>
</config>
<post_install/>
<uninstall/>
</workshop_settings>

View file

@ -1,57 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id>244850</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path/>
<mods>
<mod id="2475399454">
<name>VANGUARD: The Puddlejumper (Modded Small Grid Space/Atmo Transport)</name>
<description>VGhlIFB1ZGRsZWp1bXBlciBmcm9tIHRoZSBWYW5ndWFyZCBzZXJpZXMgb24gWW91VHViZSBmZWF0dXJpbmcgU3BhY2ViYXIsIFRleGZpcmUsIEZhcnJlbGwsIGFuZCB3NHN0ZWRzcGFjZS4NCg0KVGhlIFB1ZGRsZWp1bXBlciBpcyBhIGp1bXAgY2FwYWJsZSBhaXJ0aWdodCBzbWFsbCBncmlkIHZlc3NlbCBjYXBhYmxlIG9mIGZseWluZyBpbiB1cHRvIDFnIGF0bW8sIHdpdGggYSBtdWx0aS10aHJ1c3Qgc2V0dXAgZmVhdHVyaW5nIGhvdmVyIGVuZ2luZXMsIGF0bW8gdGhydXN0ZXJzLCBhbmQgbW9kdWxhciB0aHJ1c3RlcnMgY2FwYWJsZSBvZiBhbGwgZW52aXJvbm1lbnRzLiBUaGUgc2hpcCBpcyBhbHNvIGNhcGFibGUgb2YganVtcGluZyAyNTAwa20gYW5kIGhhcyBjYXJnbyBjYXBhY2l0eSBmb3IgYSBkZWNlbnQgaGF1bCBhbG9uZ3NpZGUgMyBzZWF0LCBhIHR1cnJldCwgYW5kIGEgc3Vydml2YWwga2l0IQ0KDQpPaGguLiBBbmQgaXQncyBzaGllbGQgYXJlIHByZXR0eSBiZWFzdCAoNjQwaykNCg0KTW9kIExpc3Q6DQpBemltdXRoIE92ZXJjbG9ja2VkIE9yZSBEZXRlY3RvcnN+KERYLTExIFJlYWR5KQ0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9NDY5MzAxNzExLnNibQ0KDQpTbWFsbCBTaGlwIFZhbmlsbGEgTW9kIFBhY2sNCmh0dHA6Ly9zdGVhbWNvbW11bml0eS5jb20vc2hhcmVkZmlsZXMvZmlsZWRldGFpbHMvP2lkPTY3MjkxOTY3NS5zYm0NCg0KRHluYW1pYyBMYXNlciBDb21wcmVzc2lvbiBNb2R1bGFyIFRocnVzdGVycw0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9MjE2MTI0MzMzMy5zYm0NCg0KRGVmZW5zZSBTaGllbGRzIC0gdjIuMCgzKQ0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9MTM2NTYxNjkxOC5zYm0NCg0KSG92ZXJFbmdpbmUNCmh0dHA6Ly9zdGVhbWNvbW11bml0eS5jb20vc2hhcmVkZmlsZXMvZmlsZWRldGFpbHMvP2lkPTEyMjUxMDcwNzAuc2JtDQoNCkF6aW11dGggUGFzc2VuZ2VyIFNlYXQgJiBPcGVuIENvY2twaXR+KERYLTExIFJlYWR5KQ0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9NDY4NTkzOTUxLnNibQ0KDQpNQSBTcG90bGlnaHQgcGFjaw0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9MTg4MTE1ODA2Ni5zYm0NCg0KQWR2YW5jZWQgRG9vcnMgTW9kIFBhY2t+KERYLTExIFJlYWR5KQ0KaHR0cDovL3N0ZWFtY29tbXVuaXR5LmNvbS9zaGFyZWRmaWxlcy9maWxlZGV0YWlscy8/aWQ9NTA2OTY0ODUzLnNibQ0KDQoNCg0KRmluZCB0aGUgVmFuZ3VhcmQgc2VyaWVzIGhlcmU6IGh0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3BsYXlsaXN0P2xpc3Q9UExUbHBPM0llZi04RjZWZ2hibldreWxmY0I1V1ExckJhTQ0KDQpodHRwOi8vd3d3LnlvdXR1YmUuY29tL3c0c3RlZHNwYWNlDQpodHRwOi8vd3d3LnR3aXRjaC50di93NHN0ZWRzcGFjZQ==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1768203314435924846/79D46947DEB2E40FFD96CA9B93FF2BA8318B6D91/</image_url>
<download_url/>
<filename/>
<file_size>1119104</file_size>
</mod>
<mod id="2470607526">
<name>Mr Bean's Mini</name>
<description>W2gxXSBCZWFuLiBbL2gxXQ0KTXIgQmVhbi4NCg0KW2gxXSBDb250cm9scyBbL2gxXQ0KMS4gTGVmdCBUdXJuIFNpZ25hbCBPbi9PZmYNCjIuIEhlYWRsaWdodHMgT24vT2ZmDQozLiBSaWdodCBUdXJuIFNpZ25hbCBPbi9PZmYNCg0KW2gxXSBOb3RlcyBbL2gxXQ0KLSBSZXF1aXJlcyBXYXN0ZWxhbmQgRExDIG9ubHkgZm9yIGZyb250IGdyaWxsZSAmIFJlYXIgTGlnaHRzICsgQmxpbmtlcnMgKHdvcmtzIHdpdGhvdXQpDQotIFlvdSBjYW4gc2l0IG9uIHRoZSBtb3VudGVkIGNvdWNoIGJ1dCBJIGRvdWJ0IGl0J3Mgc3RyZWV0LWxlZ2FsDQotIEdldCBpbiB0aHJvdWdoIHRoZSB0aW55IGdhcHMgb24gZWl0aGVyIHNpZGU=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1781713698219456050/8CE4CA03027726515A67B7C254DE8E0010B24BE1/</image_url>
<download_url/>
<filename/>
<file_size>481088</file_size>
</mod>
<mod id="2473969260">
<name>Saratoga Class Cruiser ( No Mods) (DLC Req )</name>
<description>W2ltZ10gaHR0cHM6Ly9pLmltZ3VyLmNvbS9wN0Z2MVo2LmdpZiBbL2ltZ10NCg0KU28gSSBXYXMgSGF2aW5nIEEgTGl0dGxlIEJpdCBPZiBBIEhhbG8gaXRjaCBBbmQgRGVjaWRlZCBUbyBCdWlsZCBBIEhhbG8gc3R5bGUgQ3J1aXNlciBUaGF0IFdvdWxkIEZpdCBJbiBUaGUgSGFsbyBVbml2ZXJzZSBXaXRob3V0IEJlaW5nIEEgRGlyZWN0IENvcHkgT2YgVGhlIEhhbGN5b24gT3IgTWFyYXRob24gQ2xhc3MgQ3J1aXNlcnMsIFNvIEhlcmUgV2UgSGF2ZSBUaGUgU2FyYXRvZ2EgQ2xhc3MgQ3J1aXNlci4NCg0KUENVID0gODYxMzggKCBBYm91dCBIYWxmIElzIFdlYXBvbnMgKQ0KQmxvY2tzID0gMTE4MDIgDQoNCg0KU3Vydml2YWwNCj09PT09PT09PT09PT09PT0NCkZ1bmN0aW9uYWwgIDogWWVzDQpCdWlsZGFibGUgICAgOiBUaGUgTWFpbiBIdWxsIEFuZCBJbnRlcmlvciBBcmUgUHJvamVjdG9yIEJ1aWxkYWJsZSwgVGhlIFJlc3QsIE5vdCBTbyBNdWNoDQoNCkZlYXR1cmVzIDogV2VhcG9ucw0KPT09PT09PT09PT09PT09PT09DQoxMSBHYXRsaW5nIFR1cnJldHMNCjEwIE1pc3NpbGUgVHVycmV0cw0KMTIgRm9yd2FyZCBGYWNpbmcgUm9ja2V0IExhdWNoZXJzDQo0IEN1c3RvbSBUdXJyZXRzIFdpdGggMiBMYXJnZSBSb2NrZXQgTGF1bmNoZXJzIEVhY2gNCjQgQ3VzdG9tIEpvbHQgQ2Fubm9ucyBCYXNlZCBPbiBUaGUgT25lIEZvdW5kIEhlcmUgaHR0cHM6Ly9zdGVhbWNvbW11bml0eS5jb20vc2hhcmVkZmlsZXMvZmlsZWRldGFpbHMvP2lkPTI0MDc2NTU2MDcmc2VhcmNodGV4dD1waXN0b24rZ3VuDQo4IEN1c3RvbSBNaXNzaWxlIEJheXMNCg0KUHJvZHVjdGlvbiAvIFBvd2VyIC8gRnVlbCAvIENhcmdvDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KMjkgTzJIMiBHZW5lcmF0b3JzDQo2IExhcmdlIEgyIFRhbmtzDQo2IFNtYWxsIEgyIFRhbmtzDQoxIExhcmdlIFJlYWN0b3INCjE0IEJhdHRlcmllcw0KNCBPMiBUYW5rcw0KMiBMYXJnZSBDYXJnbyBDb250YWluZXJzDQoyMCBTbWFsbCBDYXJnbyBDb250YWluZXJzDQo5IEJhc2ljIEFzc2VtYmxlcnMNCjUgSnVtcCBEcml2ZXMNCg0KUm9vbXMgLyBFeHRyYXMNCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KQnJpZGdlDQpTeXN0ZW1zIENvbnRyb2wgUm9vbQ0KTWVkQmF5DQpDcnlvTGFiDQpGVEwgQ29udHJvbA0KU21hbGwgSGFuZ2VyDQo0IENyZXcgUXVhdGVycywgMiBCZWRzIEVhY2gNCkdhbGx5DQpSZWFjdG9yIENvbnRyb2wNCkZ1ZWwgQ29udHJvbA0KQXJtb3J5DQpHeXJvIENvbnRyb2wNCkdyYXZpdHkvUHJvZHVjdGlvbiBDb250cm9sIFN0YXRpb24NCjggRXNjYXBlIFBvZHMNCg0KDQpOb3Rlcw0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoqIE1heCBSZWNvbW1lbmRlZCBHcmF2aXR5IElzIDAuNTAgKCBZb3UgTWlnaHQgQmUgYWJsZSBUbyBIaXQgMC42NSB3aXRoIG91dCBjYXJnbyApDQoqIFRoZSBNaXNzaWxlcyBVc2UgSW9uIFRocnVzdGVycywgU28gVGhlbiBEb250IFdvcmsgSW4gR3Jhdml0eQ0KKiBTZXZlcmFsIFNjcmlwdHMgQXJlIFVzZWQsIFlvdSBDYW4gRGlzYWJsZSBNb3N0IElGIHlvdSBXYW50IEhvd2V2ZXIgIDMgQXJlIFJlcXVpcmVkDQpXaGlwcyBUdXJyZXQgU2xhdmVyIFNjcmlwdCAsIFdoaXBzIExBTVAgU2NyaXB0LCBXaGlwcyBXSEFNIFNjcmlwdC4NCiogV2hlbiBMYXVuY2hpbmcgTWlzc2lsZXMgU2xvdyB0byA0MC9tcyBPciBMb3dlciBXaXRoIE5vIEVycmF0aWMgTW92ZW1lbnRzDQoqIFRoZSBNYWluIENhbm5vbnMgQ2FuIEJlIEZpcmVkIEF0IEFueSBWYW5pbGxhIFNwZWVkLCBUaG91Z2ggWW91IFdpbGwgSGF2ZSBUbyBEbyBTb21lIExlYWRpbmcgT24gVGhlIFRhcmdldA0KKiBUaGUgU2hpcCBTaG91bGQgQmUgU3RvcHBlZCBPciBBdCBWZXJ5IExvdyBTcGVlZCBUbyBMYXVuY2ggVGhlIEVzY2FwZSBQb2Rz</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1776084465699675544/E3B57B81B8CB75A600B1BB8FE4BDB177F4AACAD5/</image_url>
<download_url/>
<filename/>
<file_size>13956266</file_size>
</mod>
<mod id="2472607330">
<name>Cepheus LX-50 Cutter</name>
<description>W2JdVmFuaWxsYSB8IFN1cnZpdmFsIHwgTm8gU3ViZ3JpZHMgfCBObyBTY3JpcHRzIHwgTm8gRExDIFsvYl0NCg0KDQpbY29kZV1bYl1BIHNtYWxsIHNoaXAgd2l0aCBhIGZldyB0dXJyZXRzLCBub3RoaW5nIHNwZWNpYWwuWy9iXVsvY29kZV0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIA0KDQpJIGJ1aWx0IGl0IGFzIGEgc29ydCBvZiBjaGFsbGVuZ2UgZm9yIG15c2VsZiB0byBidWlsZCBhbnkgc2hpcCBhcyBmYXN0IGFzIHBvc3NpYmxlLiANClRvb2sgbWUgYWJvdXQgMiBob3VycyBwbHVzIGEgYml0IG9mIHJlZmluaW5nLiBJdCdzIGZ1bGwgaHlkcm8gcG93ZXJlZCwgY2FuIGZseSBvbiBhbGwgcGxhbmV0cw0KYW5kIGhhcyBhbGwgdGhlIGJhc2ljcy4gSXQganVzdCBsYWNrcyB0aGUgbGFyZ2UgcmVmaW5lcnkgYW5kIGp1bXAgZHJpdmUgYmVjYXVzZSBvZiBhIGxhY2sgb2Ygc3BhY2UuDQoNCg0KW2gxXVN0YXRzWy9oMV0NCltsaXN0XQ0KWypdIFdlaWdodDogMzEwIHQNClsqXSBMZW5ndGg6IDQ1IG0NClsqXSBXaWR0aDogMzIuNSBtDQpbKl0gQmxvY2tzOiA0NTINClsqXSBQQ1U6IDUwNTcNClsvbGlzdF0NCg0KW2gxXUVuZ2luZXM6Wy9oMV0NCltsaXN0XQ0KWypdIDIgYmF0dGVyaWVzDQpbKl0gMiBoeWRyb2dlbiBlbmdpbmVzDQoNClsqXSAyOCBzbWFsbCBoeWRybyB0aHJ1c3RlcnMNClsvbGlzdF0NCg0KW2gxXUFybWFtZW50OlsvaDFdDQpbbGlzdF0NClsqXSA0IGdhdGxpbmcgdHVycmV0cw0KWy9saXN0XQ0KDQpbaDFdVXRpbGl0eTpbL2gxXQ0KW2xpc3RdDQpbKl0gYmFzaWMgcmVmaW5lcnkNClsqXSBhc3NlbWJsZXINClsqXSBvcmUgZGV0ZWN0b3INClsqXSBjcnlvIHBvZHMNClsvbGlzdF0=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/1790721049091201826/43AA92A474DAE9CB99D545CAF54461842BD17409/</image_url>
<download_url/>
<filename/>
<file_size>1012221</file_size>
</mod>
</mods>
<config>
<regex/>
<mods_backreference_index/>
<variable/>
<place_after/>
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>\n</string_separator>
<filepath>workshop_installed.txt</filepath>
</config>
<post_install>modID=%workshop_mod_id%&#xD;
echo $modID&#xD;
&#xD;
sed -i 's/&lt;Mods \/&gt;/&lt;Mods&gt;\n&lt;\/Mods&gt;/' Sandbox_config.sbc&#xD;
&#xD;
sed -i "s/&lt;Mods&gt;/&lt;Mods&gt;\n&lt;ModItem&gt;\n&lt;Name&gt;$modID.sbm&lt;\/Name&gt;\n&lt;PublishedFileId&gt;$modID&lt;\/PublishedFileId&gt;\n&lt;\/Moditem&gt;\n/" Sandbox_config.sbc</post_install>
<uninstall/>
</workshop_settings>

View file

@ -1,24 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id>228380</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>mods</mods_path>
<mods/>
<config>
<regex>mods=(([0-z]+,?)*)</regex>
<mods_backreference_index>1</mods_backreference_index>
<variable>mods=</variable>
<place_after/>
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>,</string_separator>
<filepath>server_config.cfg</filepath>
</config>
<post_install>printf "\nMoving item %workshop_mod_id% ..."&#xD;
cp -Rf "%mods_full_path%/steamapps/workshop/content/228380/%workshop_mod_id%" "%mods_full_path%/."&#xD;
rm -Rf "%mods_full_path%/steamapps/workshop/content/228380/%workshop_mod_id%"&#xD;
printf "\nSuccess."</post_install>
<uninstall>printf "\nUninstalling item %mod_string% ...\n"&#xD;
rm -Rf "%mods_full_path%/%mod_string%"&#xD;
printf "\nSuccess."</uninstall>
</workshop_settings>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,104 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id>4000</workshop_id>
<download_method>steamapi</download_method>
<anonymous_login>1</anonymous_login>
<mods_path>mods</mods_path>
<mods>
<mod id="1403476308">
<name>gm_news_studio</name>
<description>TmV3cyBTdHVkaW8gb2YgR2FycnkncyBNb2QhIFlvdSBjYW4gZmluYWxseSBtYWtlIHlvdXIgb3duIG5ld3Mgb24gdGhpcyBtYXAhDQpNYXAgZG9lcyBub3QgcmVxdWlyZSBhbnkgYWRkaXRpb25hbCBjb250ZW50Lg0KU29tZSBwYXJ0cyBvZiBtYXAgYXJlIGNvbG91cmFibGUuDQoNCklmIHlvdSBsaWtlIHRoaXMgYWRkb24gc28gbXVjaCwgdGhhdCB5b3UgY2FuIGdpZnQgbWUgc29tZXRoaW5nIC0gaGVyZSdzIG15IHRyYWRlLW9mZmVyOg0KDQpodHRwczovL3N0ZWFtY29tbXVuaXR5LmNvbS90cmFkZW9mZmVyL25ldy8/cGFydG5lcj0xMTEzOTA4NDAmdG9rZW49c3c4ekZmWEQNCg0KVGFnczoNCk5ld3MNClRGTg0KQnJvYWRjYXN0DQpTYW5kYm94DQpNb3ZpZQ0KQW5pbWF0aW9uDQpDYW1lcmENClZpZGlv</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/910171257329171075/337E32C3BEE684EFF58A0837FAADE9CC69AE04AC/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/909045760924451577/085F0A63EC79B0509E2C8643DFAFE0D9E4785FCE/</download_url>
<filename>1528544715_223866981.gma</filename>
<file_size>6461914</file_size>
</mod>
<mod id="1437899079">
<name>Smitty Werbenjagermanjensen - Spongebob Squarepants</name>
<description>Ikl0IHdhcyBoaXMgaGF0IE1yLiBLcmFicyEgSGUgd2FzIG51bWJlciBvbmUhIg0KDQpTbWl0dHkgV2VyYmVuamFnZXJtYW5qZW5zZW4sIGZhaXRoZnVsbHkgcmVjcmVhdGVkIGZyb20gdGhlIGVwaXNvZGUgIk9uZSBLcmFiJ3MgVHJhc2giDQoNCltoMV1JbmNsdWRlcyBbL2gxXQ0KDQpQbGF5ZXIgbW9kZWwgDQpSYWdkb2xsIA0KRmluZ2VyIFBvc2luZyAoT25seSBvbmUgZmluZ2VyKQ0KRlBTIEFybXMNCkZyaWVuZGx5IGFuZCBIb3N0aWxlIE5QQ3MNCg0KW2gxXUNyZWRpdHMgYW5kIEh1Z2UgVGhhbmtzIFRvWy9oMV0NCltiXUdyaWZmYm9bL2JdIFRoZSBib2R5IGZvciBoaW0sIEkgZG91YnQgSSB3b3VsZCd2ZSBiZWVuIGFibGUgdG8gZG8gdGhpcy4NCltiXVdpbm5pbmdSb29rWy9iXSBGb3IgdGhlIHBpY3R1cmUuDQpbYl1TcGlrZVsvYl0gRm9yIHRoZSBoYXRzLCBoZWFkIGFuZCB0ZXh0dXJlcy4=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/955210546397733977/A0293C4D6AD86A37CFCB8EC18071FCB3E773D76B/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/955210546397733770/C0A22FDAC654A2067C761E5C322986CFBCC3A158/</download_url>
<filename>1531283572_229351499.gma</filename>
<file_size>2577446</file_size>
</mod>
<mod id="1443096823">
<name>gm_goldencity_day</name>
<description>SGVyZSdzIGEgbWFwIHRoYXQncyBoZWF2aWx5IGluc3BpcmVkIGJ5IGdtX2JpZ2NpdHkuIEkndmUgYmVlbiB3YW50aW5nIHRvIG1ha2UgYSBjaXR5IG1hcCBsaWtlIHRoaXMgZm9yIGEgZGVjYWRlLCBhbmQgSSd2ZSBmaW5hbGx5IGdvdHRlbiBhcm91bmQgaW50byBkb2luZyBqdXN0IHRoYXQuIEkgcHJvYmFibHkgY291bGQgaGF2ZSBnb3R0ZW4gdGhpcyBvdXQgb2YgdGhlIHdheSBhIHdoaWxlIGFnbywgYnV0IGJldHRlciBsYXRlIHRoYW4gbmV2ZXIsIEkgc3VwcG9zZS4NCg0KRmVhdHVyZXM6DQoNCi0gQSBkb3dudG93biBhcmVhIHBsdXMgYSBoaWdocmlzZSBhcmVhDQotIEEgc3Vua2VuIGhpZ2h3YXkgd2l0aCBhIGNvdXBsZSBvZiB0dW5uZWxzIGNvbm5lY3RpbmcgdG8gdGhlIG90aGVyIHJvYWRzDQotIDE0IGJ1aWxkaW5ncyB3aXRoIGludGVyaW9ycy4gVGhlcmUgYXJlIHNvbWUgZXh0ZXJpb3Igb3BlbiBkb29ycyB0aGF0IHlvdSBjYW4gd2FsayBpbnRvIHRoYXQgdGVsZXBvcnQgeW91IGludG8gdGhlIGludGVyaW9yLiBUbyBnbyBiYWNrIG91dHNpZGUsIHByZXNzIEUgb24gb25lIG9mIHRoZSBlbGV2YXRvciBkb29ycyBvciB3aGF0ZXZlciBraW5kIG9mIGRvb3IgeW91IHRlbGVwb3J0ZWQgaW4gZnJvbnQgb2YuDQotIEFuIEFJIE5vZGVncmFwaA0KLSBIRFINCi0gQSBzaW5nbGUgc291bmRzY2FwZSB0aHJvdWdob3V0IHRoZSBtYXAuIFRoZSByZWFzb24gaXQncyBqdXN0IG9uZSBpcyBiZWNhdXNlIEknbSBub3QgZ3JlYXQgYXQgc291bmRzY2FwZXMuIEknbGwgcHJvYmFibHkgd29yayBvbiB0aGF0IGxhdGVyIGFzIHdlbGwuDQotIEEgc2luZ2xlIGN1YmVtYXAgdGhhdCBhY3R1YWxseSB3b3JrcyAoYXQgbGVhc3Qgb24gbXkgZW5kLCB0aG91Z2ggSSBtYWRlIHN1cmUgaXQgd2Fzbid0IGp1c3QgdGhlIGxlZnRvdmVyIGJzcCB0aGF0IGhhZCB0aGF0KSB1bmxpa2UgdGhlIG9uZXMgaW4gdGhlIHByZXZpb3VzIG1hcCBJIHVwbG9hZGVkDQotIFR3byBjdXN0b20gbW9kZWxzIChBIHRyZWUgYW5kIGEgYmFza2V0YmFsbCBob29wKSB1c2VkIGluIHRoZSBtYXAgdGhhdCB5b3UgY2FuIHVzZSB5b3Vyc2VsZg0KLSBBIGNvdXBsZSBvZiBzZWNyZXRzDQoNCkNyZWRpdHM6DQoNCkRvY3RvciBGbG91bmRlciBCb3gsIGZvciB0aGUgYmFza2V0YmFsbCBob29wIG1vZGVsDQpCbHVlYmVycnlfUGllLCBmb3IgY29taW5nIHVwIHdpdGggdGhlIGlsbHVtaW5hdGVkIHdpbmRvdyB0ZWNobmlxdWUgKG5pZ2h0IHZlcnNpb24pDQpLaW5nUG9tbWUsIGZvciBleHBhbmRpbmcgb24gdGhhdCB0ZWNobmlxdWUgKG5pZ2h0IHZlcnNpb24pDQpic2hhZG93LCBmb3IgdGhlIGludmlzaWJsZSByYWQgbGlnaHQgdGVjaG5pcXVlIHVzZWQgZm9yIHRoZSBzdHJlZXRsaWdodHMgKG5pZ2h0IHZlcnNpb24pDQpKYWtvYmkgTydCcmllbiwgZm9yIGhlbHBpbmcgbWUgb3V0IHdpdGggYSBiaXQgb2Ygb3B0aW1pemF0aW9uIChUaG91Z2ggSSBkaWRuJ3QgZG8gYSBncmVhdCBqb2Igd2l0aCBpdCBvbiB0aGlzIHVwZGF0ZS4gSSdsbCBwcm9iYWJseSB3b3JrIG9uIGl0IHNvbWUgbW9yZSBvbiB0aGUgbmV4dCB1cGRhdGUpDQpWYWx2ZSBhbmQgQ0dUZXh0dXJlcywgZm9yIHRoZSBzb3VyY2UgbWF0ZXJpYWwgdXNlZCBmb3IgdGhlIGN1c3RvbSB0ZXh0dXJlcw0KRXZlcnlvbmUgd2hvIGdhdmUgZmVlZGJhY2sgb24gdGhlIG1hcCBpbiB0aGUgd29yay1pbi1wcm9ncmVzcyB0aHJlYWQNCg0KWW91IHdvbid0IG5lZWQgQ291bnRlciBTdHJpa2U6IFNvdXJjZSBvciBMZWZ0IDQgRGVhZCBmb3IgdGhlIHRleHR1cmVzIG9uIHRoaXMgbWFwIHRvIHdvcmssIGZvciB0aG9zZSBvZiB5b3Ugd2hvIGRvbid0IGhhdmUgZWl0aGVyIGdhbWUuIFRoZXJlIGlzIGFsc28gYSBuaWdodCB2ZXJzaW9uIG9mIHRoaXMgbWFwIHdoaWNoIGlzIHRoZSBvcmlnaW5hbCwgaWYgeW91IHdhbnQgdG8gY2hlY2sgdGhhdCBvdXQuDQoNCkFsc28sIEkganVzdCBub3RpY2VkIHRoYXQgdGhlcmUncyBhIGJ1ZyBvbiB0aGlzIG1hcCB3aGVyZSBzbWFsbCBmbGlja2VyaW5nIGJsYWNrIHRyaWFuZ2xlcyB3aWxsIGFwcGVhciBpbiB0aGUgY29ybmVycyBvZiB0aGUgc2t5Ym94IGlmIHlvdSBsb29rIGF0IGFueSBvZiB0aG9zZSBjb3JuZXJzLiBJIGhhdmUgbm8gaWRlYSBob3cgdG8gZml4IHRoaXMsIHRob3VnaCBpdCdzIG5vdCB0b28gbm90aWNhYmxlLiBJJ20gdGhpbmtpbmcgaXQgaGFzIHNvbWV0aGluZyB0byBkbyB3aXRoIHRoZSByZW5kZXIgZGlzdGFuY2UsIHdoaWNoIHdvdWxkbid0IHJlYWxseSBtYWtlIGFueSBzZW5zZSBzaW5jZSB0aGUgc2t5Ym94IGlzbid0IHN1cHBvc2VkIHRvIGhhdmUgdGhhdCBpc3N1ZS4=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/943951991654869773/338A0338BA966AF835CEB2B3B5A800487A1BACE1/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/943951991654867861/F167313D927ECA5D4AFAEFD76644F598B9A55AC1/</download_url>
<filename>1531788260_1094686331.gma</filename>
<file_size>25824223</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable/>
<place_after/>
<mod_string>%first_file%</mod_string>
<string_separator>\n</string_separator>
<filepath>mods/mods.txt</filepath>
</config>
<post_install>cd "%mods_full_path%/steamapps/workshop/content/4000/%workshop_mod_id%"&#xD;
cp -f %first_file% myfile.gma&#xD;
7z x myfile.gma -aoa &gt; /dev/null 2&gt;&amp;1&#xD;
cp -f myfile content.gma&#xD;
rm -f myfile.gma myfile&#xD;
"%mods_full_path%/../bin/gmad_linux" content.gma &gt; /dev/null 2&gt;&amp;1&#xD;
rm -f "content.gma"&#xD;
cd content&#xD;
zip -r "%mods_full_path%/steamapps/workshop/content/4000/%first_file%.zip" * &gt; /dev/null 2&gt;&amp;1&#xD;
cd ../..&#xD;
rm -Rf "%mods_full_path%/steamapps/workshop/content/4000/%workshop_mod_id%"&#xD;
unzip -Z1 "%first_file%.zip" &gt; "%first_file%.list"&#xD;
tac "%first_file%.list" &gt; "%first_file%.listinv"&#xD;
cp -f "%first_file%.listinv" "%first_file%.list"&#xD;
rm "%first_file%.listinv"&#xD;
unzip -o "%first_file%.zip" -d "%mods_full_path%/../garrysmod" &gt; /dev/null 2&gt;&amp;1&#xD;
rm -f "%first_file%.zip"&#xD;
&#xD;
if [ -f "%mods_full_path%/steamapps/workshop/content/4000/%first_file%.list" ];then&#xD;
cd "%mods_full_path%/../garrysmod"&#xD;
luaFile="%mods_full_path%/../garrysmod/lua/autorun/server/resources.lua"&#xD;
while read p; do&#xD;
if [ -f "$p" ] &amp;&amp; [ ! -d "$p" ]; then&#xD;
filename=$(basename -- "$p")&#xD;
extension="${filename##*.}"&#xD;
if [ "$extension" != "bsp" ] &amp;&amp; [ "$extension" != "png" ]; then&#xD;
newstring="resource.AddSingleFile(\"$p\")"&#xD;
if ! grep -Fxq "$newstring" "$luaFile"; then&#xD;
echo "$newstring" &gt;&gt; "$luaFile"&#xD;
fi &#xD;
fi&#xD;
fi&#xD;
done &lt;"%mods_full_path%/steamapps/workshop/content/4000/%first_file%.list"&#xD;
printf "\nContents of %first_file% successfully installed!"&#xD;
else&#xD;
printf "\nFile listing not found, try it again after reinstalling the mod."&#xD;
fi&#xD;
&#xD;
</post_install>
<uninstall>if [ -f "%mods_full_path%/steamapps/workshop/content/4000/%mod_string%.list" ];then&#xD;
cd "%mods_full_path%/../garrysmod"&#xD;
luaFile="%mods_full_path%/../garrysmod/lua/autorun/server/resources.lua"&#xD;
while read p; do&#xD;
if [ -d "$p" ]; then&#xD;
if [ -z "$(ls -A "$p")" ]; then&#xD;
rm -vRf "$p"&#xD;
fi&#xD;
else&#xD;
if [ -f "$p" ]; then&#xD;
rm -vf "$p"&#xD;
filestring="resource.AddSingleFile(\"$p\")"&#xD;
if grep -Fxq "$filestring" "$luaFile"; then&#xD;
escaped_filestring=$(sed -e 's/[]\/$*.^[]/\\&amp;/g' &lt;&lt;&lt; $filestring)&#xD;
sed -i "/$escaped_filestring/d" "$luaFile"&#xD;
fi &#xD;
fi&#xD;
fi&#xD;
done &lt;"%mods_full_path%/steamapps/workshop/content/4000/%mod_string%.list"&#xD;
printf "\nContents of %mod_string% successfully uninstalled!"&#xD;
else&#xD;
printf "\nFile listing not found, try it again after reinstalling the mod."&#xD;
fi</uninstall>
</workshop_settings>

View file

@ -1,101 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id>440900</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>1</anonymous_login>
<mods_path>ConanSandbox/Mods</mods_path>
<mods>
<mod id="864199675">
<name>Pickup+</name>
<description>V2l0aCB0aGlzIG1vZCB5b3UgYXJlIGFibGUgdG8gcGljayB1cCBhbGwgdGhlIHRoaW5ncyB5b3UndmUgcGxhY2VkIC0gc2ltcGxlIGFzIHRoYXQhIDotKQoKKioqIEFkZGVkIHBpY2t1cCBzdXBwb3J0IGZvciB0aHJhbGxzISAqKioKCi0gV29ya3Mgb24gc2luZ2xlcGxheWVyIGFuZCBkZWRpY2F0ZWQgc2VydmVycyEKLSBBZG1pbnMgaGF2ZSB0aGUgb3B0aW9uIHRvIHJlbW92ZSB0aGUgcGlja3VwIG9wdGlvbiBmcm9tIGl0ZW1zIChPbmx5IGluIE1QKQotIEFkbWlucyBoYXZlIHRoZSBvcHRpb24gdG8gZW5hYmxlL2Rpc2FibGUgdGhlIHBpY2t1cCBvcHRpb24gZnJvbSBhbGwgdGhyYWxscyBvdmVyIHRoZSBvcHRpb25zd2hlZWwgKE9ubHkgaW4gTVApCgoqKiogWW91IGNhbiBvbmx5IHBpY2t1cCB0aHJhbGxzIHRoYXQgYXJlIG5vdCB3ZWFyaW5nIGFueSBhcm1vciEgKioqCgoKWW91IHdhbnQgdG8gcmVwb3J0IGEgYnVnPyBQbGVhc2UgdXNlIHRoaXMgdGVtcGxhdGUgYW5kIGp1c3QgcG9zdCBpdCBpbiB0aGUgY29tbWVudHMhCmh0dHBzOi8vc3RlYW1jb21tdW5pdHkuY29tL3dvcmtzaG9wL2ZpbGVkZXRhaWxzL2Rpc2N1c3Npb24vODY0MTk5Njc1LzE3Mjg3MDE4Nzc0ODE5NTQ0NTkvIAoKCkhhdmUgRnVuIQoKCk1PRCBJRDogODY0MTk5Njc1CgpJZiB5b3UgaGF2ZSBhbnkgcHJvYmxlbXMgb3Igc3VnZ2VzdGlvbnMgZmVlbCBmcmVlIHRvIHdyaXRlIGl0IGluIHRoZSBjb21tZW50cyBvciBzdGFydCBhIGRpc2N1c3Npb24hCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKVGhpcyBtb2QvY29kZS93b3JrIGlzIHByb3RlY3RlZCBieSB0aGUgW1VSTD1odHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy1uZC80LjAvbGVnYWxjb2RlXUF0dHJpYnV0aW9uLU5vbkNvbW1lcmNpYWwtTm9EZXJpdmF0aXZlcyA0LjAgSW50ZXJuYXRpb25hbCBDcmVhdGl2ZSBDb21tb25zIExpY2Vuc2UuCltJTUddaHR0cHM6Ly9pLmNyZWF0aXZlY29tbW9ucy5vcmcvbC9ieS1uYy1uZC80LjAvODh4MzEucG5nWy9JTUddWy9VUkxd</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/96102470863175828/E2FD19AEC364F48C7B0D0FB7231D937A097A0EEB/</image_url>
<download_url/>
<filename/>
<file_size>691137</file_size>
</mod>
<mod id="1367404881">
<name>Savage Steel</name>
<description>VGhpcyBtb2QgaGFzIGEgd2lkZSB2YXJpZXR5IG9mIHJlYWxpc3RpYyBwbGFjZWFibGUgb3IgUlAgaXRlbXMuIFNvbWUgb2YgdGhlIGZlYXR1cmVzIHRoYXQgd2UgaGF2ZSBhcmUgaW52ZW50b3JpZXMgaW4gc2Fja3MsIGNyYXRlcywgYmFycmVscyBhbmQgbWFueSBvdGhlciBpdGVtcy4gV2UgYWxzbyBoYXZlIGV4dHJhIHN0b3JhZ2Ugc3BhY2UgaW4gb3VyIFN0cm9uZ2JveCBjaGVzdC4gV2Ugbm93IGhhdmUgIlBpY2sgVXAiIG9uIHRoZSBTYXZhZ2UgU3RlZWwgcGxhY2VhYmxlcy4gV2UgaGF2ZSBkZXNpZ25lZCBvdXIgcGxhY2VhYmxlcyB0byBiZSBwbGFjZWQgY2xvc2VseSB0b2dldGhlciBvciBzdGFja2VkLCBpZiBkZXNpcmVkLiBXZSB3aWxsIGNvbnRpbnVlIHRvIGFkZCB0byB0aGlzIG1vZCBvbiBhbiBvbmdvaW5nIGJhc2lzLiBUaGlzIG1vZCBpcyBkZXNpZ25lZCBmb3IgdGhlIFNhdmFnZSBTdGVlbCBzZXJ2ZXIsIGJ1dCBhbnlvbmUgaXMgd2VsY29tZSB0byB1c2UgaXQuIElmIHlvdSBsaWtlIG91ciBtb2QgcGxlYXNlIGJlIHN1cmUgdG8gZ2l2ZSB1cyBhICJUaHVtYnMgVXAiISEgDQoNCltoMV1MaXN0IG9mIFBsYWNlYWJsZXNbL2gxXSANCg0KW2gxXVN0b3JhZ2UgSXRlbXNbL2gxXSANCg0KQnVja2V0ICANClRhbGwgQnVja2V0IA0KMyBkaWZmZXJlbnQgQ2xvdGggQmFsZXMgDQozIGRpZmZlcmVudCBCYWdzIA0KV29vZGVuIFR1YiANCkJhc2tldCANClRhbGwgQmFza2V0IA0KMyBCYXJyZWxzIA0KNCBkaWZmZXJlbnQgQ3JhdGVzIA0KMiBkaWZmZXJlbnQgU3Ryb25nYm94IENoZXN0cyANCg0KW2gxXUZ1cm5pc2hpbmdzWy9oMV0gDQoNCkxhcmdlIENhc2sgDQozIGJhciBwaWVjZXMgDQpGdWxseSBhc3NlbWJsZWQgYmFyIA0KQ2xvdGggQmFyIGNvdmVyIA0KMiBkaWZmZXJlbnQgZW1wdHkgYm93bHMgDQoyIGRpZmZlcmVudCBmdWxsIGJvd2xzIA0KQ2FiaW5ldCANCjIgZGlmZmVyZW50IFNoZWx2ZXMgDQpDYWJpbmV0IHdpdGggc2hlbGYgDQo1IGRpZmZlcmVudCBoZXJiIGJpbnMgDQpEcmVzc2VyIA0KV2FsbCBTaGVsZiANCkJhdGggDQpTdHlnaWFuIEJhdGggDQpSdXN0aWMgQmF0aCAod29vZGVuKSANCkN1cnRhaW5zIA0KQ2hhbWJlciBQb3QNClNhdWNlcGFuDQpTb3VwIExhZGxlDQo0IENhbmlzdGVycyAoU2FsdCwgUGVwcGVyLCBDaW5uYW1vbiAmIFBhcHJpa2EpIHdpdGggNSBzdG9yYWdlIHNsb3RzIGVhY2gNCkNhbmlzdGVyIFNldCBvbiBhIHNoZWxmIHdpdGggMjAgc3RvcmFnZSBzbG90cw0KMiBXZWFwb24gRHJvcHMgLSBOb3J0aGVybiBhbmQgU291dGhlcm4NCkxhdW5kcnkgQnVja2V0DQpTY3JvbGwgU3RhbXANClNlYWxlZCBTY3JvbGwNClNjcm9sbCB3YXgNCjMgZGlmZmVyZW50IHJ1Z3MNCiANCltoMV1Gb29kWy9oMV0gDQogDQpDaGlja2VuIExlZ3Mgb24gYSBQbGF0ZSANCldoaXRlIEJyZWFkIG9uIGEgcGxhdGUgDQpDYWtlcyBvbiBhIHBsYXRlIA0KQ2hlZXNlIHdoZWVsIG9uIGEgcGxhdGUgDQpCb3dsIG9mIEVnZ3MgDQpGcmllZCBlZ2dzIG9uIGEgcGxhdGUgDQpIYW0gb24gYSBwbGF0ZSANCkJyZWFkIG9uIGEgcGxhdGUgDQogDQpbaDFdQWxjaGVteS9BcG90aGVjYXJ5Wy9oMV0gDQoNCkFsY2hlbXkgRGVzayANCkFsY2hlbXkgRGVzayBDaGFpciANCkluayBXZWxsIG9wZW4gDQpJbmsgV2VsbCBDbG9zZWQgDQpJbmsgd2VsbCBjYXAgDQo0IGRpZmZlcmVudCBQb3Rpb25zIA0KMyBkaWZmZXJlbnQgQXBvdGhlY2FyeSBOb3RlcyANCjYgZGlmZmVyZW50IEZlYXRoZXJzIA0KTW9ydGFyICYgUGVzdGxlIA0KMiBkaWZmZXJlbnQgSGFuZ2luZyBIZXJiIHJhY2tzIChvbmUgd29vZCBhbmQgb25lIG1ldGFsKSANCjMgZGlmZmVyZW50IEFsY2hlbXkgc2V0cyANCjUgZGlmZmVyZW50IGhlcmJzIHRvIHBsYWNlIG9uIGEgdGFibGUgb3IgY291bnRlcg0KDQpbaDFdT3V0ZG9vciBEZWNvclsvaDFdIA0KDQpXYXRlciBDYW4gDQpXYWdvbiANCldhZ29uIFdoZWVsIA0KQnJ1c2h3b29kIA0KOCBkaWZmZXJlbnQgcGllY2VzIG9mIGZpcmV3b29kICg0IHN0YW5kaW5nIHVwLCA0IGxheWluZyBkb3duKSANCjIgTG9ncyANCjUgZGlmZmVyZW50IFBsYW5rcyANCkdhbGxvd3MNCkV4ZWN1dGlvbmVyJ3MgQmxvY2ssIEF4ZSBhbmQgY29tYmluYXRpb24NCkd1aWxsb3RpbmUNClBpbGxhcnkNCkhhbmdpbmcgQ2FnZQ0KDQogW2gxXX5+ICBNb2QgSUQgMTM2NzQwNDg4MVsvaDFdDQoNCltoMV1XZSBub3cgaGF2ZSBhIERpc2NvcmQgc2VydmVyOlsvaDFdDQpodHRwczovL2Rpc2NvcmQuZ2cvcUVoTTNXdA0KDQpUbyBnZXQgdGhlIEdVSSBib3ggb24gYm90aCBiYXRocywgaG92ZXIgb3ZlciB0aGUgbGFkZGVyIGFyZWEuIFRoaXMgc2hvdWxkIGdpdmUgeW91IHRoZSBvcHRpb24gdG8gcGlja3VwIG9yIGRlc3Ryb3kuIA0KDQoNClRoYW5rcyB0byBTaGFkb3dDTUQgZm9yIGFsbCB5b3VyIGhlbHAgYW5kIHBhdGllbmNlIGFuZCB0byBSZWQgTWFyY2ggZm9yIGhlbHBpbmcgd2l0aCB0aGUgaWNvbnMgYW5kICB0aGUgYXJ0d29yayBmb3IgdGhlIG1vZCBjb3ZlciEh</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/929311364899517529/96C8FED0D19E78C69A2D571BE083FC7FC26A3F32/</image_url>
<download_url/>
<filename/>
<file_size>2644257234</file_size>
</mod>
<mod id="1384471264">
<name>Drag thralls in water (May 2018)</name>
<description>RHJhZyB0aHJhbGxzIHRocm91Z2ggd2F0ZXIgd2l0aCByb3BlLgoKU3VnZ2VzdGVkIGJ5IERyZWFndWgu</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/912420738527948826/40A5E9CD53E58008EBDC0EC4519DF55144DD03C5/</image_url>
<download_url/>
<filename/>
<file_size>777732</file_size>
</mod>
<mod id="1378596051">
<name>Banners to the Gods</name>
<description>VGhpcyBNb2QgaXMgYSBtb2QgdGhhdCBnaXZlcyAzIG5ldyBmbGF2b3JzIG9mIGJhbm5lcnMgdG8geW91ciBnYW1lLiBEZXJrZXRvLCBNaXRyYSBhbmQgWW1pciBiYW5uZXJzLiBTaW5jZSB0aGVyZSB3ZXJlIG9ubHkgU2V0LCBhbmQgRGFyZmFyaSBiYW5uZXJzLCBwbHVzIG9mIGNvdXJzZSB0aGUgb3RoZXIgY2xhbnMgaW4gdGhlIEV4aWxlZCBsYW5kcy4uLiBZZXQsIG5vdyB0aGUgTm9yZGhlaW1lcnMsIHRoZSBNaXRyYWVucywgRGVya2V0aWFucywgaGF2ZSBhIGJhbm5lciBhcyB3ZWxsIQ==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/930434406223072719/09C64DD22443CA300AB5C9C148D542385C458BDF/</image_url>
<download_url/>
<filename/>
<file_size>155457134</file_size>
</mod>
<mod id="1426203926">
<name>Compass Icon</name>
<description>QSB2ZXJ5IGJhc2ljIGNvbXBhc3MgaWNvbiB0aGF0IG1vdmVzIHRvIGluZGljYXRlIE5vcnRoIGFuZCBibGVuZHMgd2l0aCB0aGUgZXhpc3RpbmcgVUkuCgpJIHdhcyB0cnlpbmcgdG8gZmlndXJlIG91dCBob3cgdG8gZG8gbW9kcyBzbyBJIG1hZGUgYSBzdXBlciBiYXNpYyBjb21wYXNzLCBJIGZpZ3VyZWQgSSBtaWdodCBhcyB3ZWxsIHNoYXJlIGl0LiBJJ20gc3RpbGwgbGVhcm5pbmcgc28gYW55IGZlZWRiYWNrIGlzIHdlbGNvbWUu</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/952957720418478101/595F301CFA480B162796FE56793C1A650722DEDF/</image_url>
<download_url/>
<filename/>
<file_size>1942869</file_size>
</mod>
<mod id="1113901982">
<name>The Age of Calamitous</name>
<description>W2gxXVdlbGNvbWUgdG8gVGhlIEFnZSBvZiBDYWxhbWl0b3VzIVsvaDFdCgpUaGlzIG1vZCBzZXJ2ZXMgYXMgYSB0b3RhbCBjb252ZXJzaW9uIG1vZCwgaW50cm9kdWNpbmcgbmV3IHN5c3RlbXMsIGNvbnRlbnQsIGZlYXRzLCBhbmQgbXVjaCBtb3JlISAKCkhlcmUgaXMgYSBsaXN0IG9mIHNvbWUgYWRkaXRpb25zIHRvIHRoZSBnYW1lOgpbbGlzdF0KWypdIEFkZGl0aW9uYWwgQ2hhcmFjdGVyIENyZWF0aW9uIE9wdGlvbnMKWypdIE5ldyBTdGFja3MgJiBXZWlnaHQKWypdIFVJIC8gSFVEIG1vZGlmaWNhdGlvbnMKWypdIEh1bmRyZWRzIG9mIE5ldyBEZWNvcmF0aW9ucywgUHJvcHMsIEl0ZW1zLCBXZWFwb25zLCBldGMuClsqXSBNYW55IE5ldyBDcmFmdGluZyBTdGF0aW9ucywgRmVhdHMgJiBSZWNpcGVzClsqXSBOZXcgTGV2ZWwgQ2FwIDEwMCAoQXNjZW5zaW9uIDEwMS0xMjApClsqXSBTcGVjaWFsIGNvbnRlbnQgZnJvbSBUaGUgQWdlIG9mIENhbGFtaXRvdXMKWy9saXN0XQpBbmQgbXVjaCBtb3JlIQoKW2gxXVdBUk5JTkdbL2gxXQoKVGhpcyBtb2QgaXMgaW4gYWN0aXZlIGRldmVsb3BtZW50LCBhbmQgdGhlcmVmb3JlIHRoZXJlIHdpbGwgYmUgZnJlcXVlbnQgcGF0Y2hlcyBjb21pbmcgb3V0LiBTbWFsbCAmIGxhcmdlIG9uZXMgY29udGFpbmluZyBhZGp1c3RtZW50cywgYmFsYW5jaW5nLCBjb250ZW50ICYgZml4ZXMuCklmIHlvdSBkbyBub3Qgd2FudCB0byBrZWVwIHVwIHdpdGggZnJlcXVlbnQgdXBkYXRlcywgYXZvaWQgdXNpbmcgdGhlIG1vZCB1bnRpbCBpdCdzIGluIGEgY29tcGxldGVkIHN0YXRlLgpUaGlzIG1vZCBpcyBpbnRlbmRlZCB0byBiZSBzdGFuZGFsb25lIGFuZCBpcyBub3QgbWFkZSB0byB3b3JrIHdpdGggYWRkaXRpb25hbCBtb2RzLgpSZWFkIG1vcmUgYXQgdGhlIEltcG9ydGFudCBJbmZvcm1hdGlvbiB0b3BpYyBpbiB0aGUgZGlzY3Vzc2lvbnMuCgpbaDFdSW5mb3JtYXRpb25bL2gxXQoKVGhlIGludGVudGlvbiBvZiB0aGlzIG1vZCBpcyB0byBleHBhbmQgdXBvbiBDb25hbiBFeGlsZXMgd2l0aCBuZXcgY29udGVudCBmcm9tIFRoZSBBZ2Ugb2YgQ2FsYW1pdG91cyB1bml2ZXJzZSwgaW50cm9kdWNpbmcgYSBmZXcgYXNwZWN0cyBvZiB0aGUgZmFudGFzeSBtZWRpZXZhbCBnZW5yZS4KSm9pbiB1cyBvbiBEaXNjb3JkIGZvciBtb2QgdXBkYXRlcyBhbmQgc2VydmVycyBydW5uaW5nIHRoZSBtb2Q6Ci0gW3VybD1odHRwczovL2Rpc2NvcmQuZ2cvODJoZ3ZHaF0gRGlzY29yZFsvdXJsXQoKRmVlbCBmcmVlIHRvIHJlZ2lzdGVyIG9uIHRoZSB3ZWJzaXRlIHRvIGtlZXAgeW91cnNlbGYgdXAgdG8gZGF0ZSB3aXRoIHRoZSBsYXRlc3QgbmV3cyEKLSBbdXJsPWh0dHA6Ly93d3cudGhlLWFnZS1vZi1jYWxhbWl0b3VzLmNvbS9dIFRoZSBBZ2Ugb2YgQ2FsYW1pdG91cyBXZWJzaXRlWy91cmxdCgpNT0QgSUQ6IDExMTM5MDE5ODIKCltoMV1UaGUgT2ZmaWNpYWwgQWdlIG9mIENhbGFtaXRvdXMgUHJvamVjdFsvaDFdCgpJZiB5b3UgYXJlIGludGVyZXN0ZWQgaW4gbGVhcm5pbmcgbW9yZSBhYm91dCB3aGF0IFRoZSBBZ2Ugb2YgQ2FsYW1pdG91cyBwcm9qZWN0IGlzLCB5b3UgY2FuIGRyb3AgYnkgb3VyIG9mZmljaWFsIEZhY2Vib29rIHBhZ2UhIEJlIGF3YXJlIHRoYXQgdGhpcyBpcyB0aGUgb2ZmaWNpYWwgcHJvamVjdCBhbmQgbm90IHRoZSBtb2QuIFRoZSBtb2QgaXMgYSBwZXJzb25hbCBzaWRlIHByb2plY3QgYW5kIGlzIG5vdCB0aGUgb2ZmaWNpYWwgcHJvamVjdC4KLSBbdXJsPWh0dHBzOi8vd3d3LmZhY2Vib29rLmNvbS9BbmFyaW91c1Byb2R1Y3Rpb25zXSBUaGUgQWdlIG9mIENhbGFtaXRvdXMgRmFjZWJvb2tbL3VybF0KCkFkZGl0aW9uYWxseSwgeW91IGNhbiBmb2xsb3cgbWUgb24gVHdpdHRlciBmb3IgYW55IG5ld3MgdXBkYXRlcyByZWdhcmRpbmcgdGhlIG1vZCBhbmQgdGhlIG92ZXJhbGwgcHJvamVjdC4gCi0gW3VybD1odHRwczovL3R3aXR0ZXIuY29tL0VzcGVuR0pvaGFuc2VuXSBUd2l0dGVyWy91cmxdCgpbaDFdV2FudCB0byBzdXBwb3J0IHRoZSBwcm9qZWN0P1svaDFdCgpBbnkgZm9ybSBvZiBzdXBwb3J0IGlzIGdyZWF0bHkgYXBwcmVjaWF0ZWQhCkFsbCB0cmlidXRlcyBtYWRlIHRocm91Z2ggRG9uYXRpb24gYW5kL29yIFBhdHJlb24gd2lsbCBnbyB0b3dhcmRzIGV4cGFuZGluZyBUaGUgQWdlIG9mIENhbGFtaXRvdXMgcHJvamVjdC4KLSBbdXJsPWh0dHA6Ly93d3cudGhlLWFnZS1vZi1jYWxhbWl0b3VzLmNvbS9dIERvbmF0aW9uWy91cmxdCi0gW3VybD1odHRwczovL3d3dy5wYXRyZW9uLmNvbS9lc3Blbmdqb2hhbnNlbl0gUGF0cmVvblsvdXJsXQoKCkFsbCBjb250ZW50IG93bmVkIGFuZC9vciBwcm92aWRlZCBmb3IgVGhlIEFnZSBvZiBDYWxhbWl0b3VzIGlzIGNvcHlyaWdodGVkLgooYylDb3B5cmlnaHQgMjAxMS0yMDE4IEFuYXJpb3VzIFByb2R1Y3Rpb25zLCBBbGwgUmlnaHRzIFJlc2VydmVkCihjKUNvcHlyaWdodCAyMDExLTIwMTggRXNwZW4gR3JhdmRhaGwgSm9oYW5zZW4sIEFsbCBSaWdodHMgUmVzZXJ2ZWQKCkNvbmFuIEV4aWxlcyBjb250ZW50IGFuZCBtYXRlcmlhbHMgYXJlIHRyYWRlbWFya3MgYW5kIGNvcHlyaWdodHMgb2YgRnVuY29tLiA=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/861731135277865883/BCA986F592ABA8A6B95A687E0E1A3BE8749CDD3F/</image_url>
<download_url/>
<filename/>
<file_size>1291252425</file_size>
</mod>
<mod id="1403991684">
<name>Exile Architect</name>
<description>QnVpbGRpbmcgYmxvY2sgc2V0IGZvciBzY2FmZm9sZGluZyBvciBicmlkZ2VzLCBhbmQgbWFzb24gbGluZXMgdG8gaGVscCBsYXlvdXQgZm91bmRhdGlvbnMuIE1hc29uIGxpbmVzIGJlaGF2ZXMgbGlrZSBmZW5jZSBmb3VuZGF0aW9ucywgYnV0IGNhbiBhbHNvIHNuYXAgYXQgYW5nbGVzLgoKVGhlcmUncyBhIDEgcG9pbnQgZmVhdCBpbiB0aGUgYnVpbGRpbmcgY2F0ZWdvcnkuCgpLbm93biBpc3N1ZXM6CgoqU2hvcnQgbWFzb24gbGluZXMgY2FuIG5vdyBzbmFwIGF0IDYwPyBhbmdsZXMgZm9yIGRyYXdpbmcgdHJpYW5nbGVzLiBCdXQgZHVlIHRvIGhvdyBzb2NrZXRzIHdvcmssIHRoZXknbGwgYWxzbyBzbmFwIGF0IDMwPyBhbmdsZXMsIHNvIGJlIGF3YXJlLgoKKkZlbmNlcyBhbmQgd2FsbHMgY2FuIHN0YWNrIG9uIG1hc29uIGxpbmVzLiBUaGV5IGhhdmUgcmF0aGVyIGxvdyBoZWFsdGggdGhvdWdoLCBzbyBpdCdzIHByb2JhYmx5IG5vdCBhIGdvb2QgaWRlYSB0byB1c2UgdGhlbSBhcyBmb3VuZGF0aW9ucyBvbiBhbnl0aGluZyBkZWZlbnNpdmUuCgoqSWYgdXBncmFkaW5nIGZyb20gcHJldmlvdXMgdmVyc2lvbiwgeW91IG1heSBuZWVkIHRvIGRyaW5rIGEgbG90dXMgcG90IChvciBhZG1pbiBzZWxmIGlmIFNQKSB0byByZWxlYXJuIGZlYXQgdG8gZ2V0IG5ldyByZWNpcGVzLgoKTW9kIGNvbXBhdGliaWxpdHkgbm90ZXM6CgpJdGVtIElEcyAxNzc1MDAxIC0gMTc3NTAwOQpSZWNpcGUgSURzIDE3NzUxMDEgLSAxNzc1MTA5CkZlYXQgSUQgMTc3NTEwMA==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/915800878318924163/704FD0BCBB9780EBCD0AB50B81DCEA210EC752C6/</image_url>
<download_url/>
<filename/>
<file_size>21626043</file_size>
</mod>
<mod id="1382120864">
<name>LowerMonsterHPSolo</name>
<description>Q2hhbmdlczoNCk1vbnN0ZXJIZWFsdGgNCjgJTlBDX0thcHBhSGF0Y2hsaW5nDQo4CU5QQ19SYWJiaXQNCjExCU5QQ19WdWx0dXJlDQoxMwlOUENfR2F6ZWxsZUZhd24NCjEzCU5QQ19IeWVuYVNwb3R0ZWRDdWINCjEzCU5QQ19IeWVuYVN0cmlwZWRDdWINCjE5CU5QQ19QaXJhbmhhDQoyMAlOUENfVGlnZXJDdWINCjIwCU5QQ19Xb2xmUHVwcHkNCjIyCU5QQ19Pc3RyaWNoQ2hpY2sNCjIyCU5QQ19Xb2xmRGlyZVB1cHB5DQoyMwlOUENfQ3JvY29kaWxlQmFieQ0KMjMJTlBDX1NhYnJldG9vdGhDdWINCjI1CU5QQ19KYWd1YXJDdWINCjI1CU5QQ19QYW50aGVyQ3ViDQoyNQlOUENfUGlnbGV0DQoyNQlOUENfV2lsZEJvYXJQaWdsZXQNCjMwCU5QQ19CZWFyQmxhY2tDdWINCjMwCU5QQ19CZWFyQnJvd25DdWINCjQ2CU5QQ19SaGlub0JhYnkNCjU3CU5QQ19QaWtlZmlzaA0KNTgJTlBDX0ltcA0KNTgJTlBDX0ltcEV4cGxvc2l2ZQ0KNjUJTlBDX0dhemVsbGUNCjY2CU5QQ19Db2JyYQ0KNjgJTlBDX0VsZXBoYW50QmFieQ0KODcJTlBDX1NwaWRlckJyb3duDQo4OQlOUENfT3N0cmljaA0KOTIJTlBDX0FudGVsb3BlU3BpcmFsSG9ybg0KOTYJTlBDX0h5ZW5hU3BvdHRlZA0KMTAwCU5QQ19IdW1hbm9pZA0KMTAzCU5QQ19IeWVuYVN0cmlwZWQNCjEwOQlOUENfU3BpZGVyV2lkb3dZZWxsb3cNCjE0MAkjTi9BDQoxNDIJTlBDX0FudGVsb3BlS2luZw0KMTQ3CU5QQ19TcGlkZXJHcmV5DQoxNDcJTlBDX1NlcnBlbnRwZW9wbGVIb3JkZWxpbmcNCjE0NwlOUENfU2VycGVudHBlb3BsZUlsbHVzaW9uDQoxNTIJTlBDX01vdW50YWluR29hdA0KMTU5CU5QQ19HZW5lcmljDQoxNjQJTlBDX0h5ZW5hVW5kZWFkDQoxNzEJTlBDX1NwaWRlclJlZG1vdXRoDQoxOTUJTlBDX1NwaWRlcldpZG93DQoyMDAJTlBDX1Njb3JwaW9uTWVkaXVtDQoyMDAJTlBDX1NwaWRlckdyZWVuDQoyMTcJTlBDX09vemUNCjIzNQlOUENfS2FwcGENCjIzNQlOUENfU2tlbGV0b25EYXJmYXJpDQoyMzYJTlBDX0RlZXINCjIzNwlOUENfUm9ja25vc2UNCjI2MQlOUENfQ3JvY29kaWxlDQoyNjUJTlBDX0NhbWVsDQoyNzgJTlBDX0Vsaw0KMjkxCU5QQ19Sb2Nrbm9zZU1vbHRlbg0KMjkxCU5QQ19Sb2Nrbm9zZVdoaXRlDQozMDgJTlBDX1NwaWRlcldpZG93Qmx1ZQ0KMzEwCU5QQ19Ta2VsZXRvbkRyZWdzDQozMTEJTlBDX0xvY3VzdEdyZWVuDQozMTEJTlBDX1RpZ2VyDQozMTgJTlBDX1NwaWRlcldpZG93R3JlZW4NCjMyMQlOUENfU2NvcnBpb25MYXJnZQ0KMzU5CU5QQ19TcGlkZXJXaWRvd1JlZA0KMzU5CU5QQ19Xb2xmDQozODUJTlBDX1BhbnRoZXINCjQwMAlOUENfSnVuZ2xlQmlyZA0KNDMwCU5QQ19KYWd1YXINCjQ0MwlOUENfS29tb2RvDQo0NDkJTlBDX0xvY3VzdFllbGxvdw0KNTA0CU5QQ19TYWxhbWFuZGVyDQo1MDcJTlBDX0xvY3VzdFdoaXRlDQo1MzEJTlBDX1NlcnBlbnRwZW9wbGVCb3cNCjUzMQlOUENfU2VycGVudHBlb3BsZVN3b3Jkcw0KNTM1CU5QQ19BcnRpbGxlcnkNCjUzNQlOUENfQmVhc3RtYXN0ZXINCjUzNQlOUENfQnJhd2xlcg0KNTM1CU5QQ19DcnVzaGVyDQo1MzUJTlBDX01vdW50ZWQNCjUzNQlOUENfUmFuZ2VyDQo1MzUJTlBDX1Njb3V0DQo1MzUJTlBDX1VuZGVhZA0KNTM1CU5QQ19XYXJyaW9yDQo1MzUJTlBDX1dlcmVoeWVuYQ0KNTM1CU5QQ19XaWxkQm9hcg0KNjEwCU5QQ19Hb3JpbGxhDQo2MzcJTlBDX1NhYnJldG9vdGgNCjY3MwlOUENfUmVwdGlsZUJlYXN0DQo2ODUJTlBDX0NoaWxkcmVuT2ZKaGlsDQo2ODUJTlBDX0dyZXlBcGUNCjY4NQlOUENfU2tlbGV0b25TZXJwZW50TWFuDQo3NTAJTlBDX1JvY2tub3NlS2luZ01vbHRlbg0KNzUwCU5QQ19CYXREZW1vbg0KODE4CU5QQ19Hb3JpbGxhU2lsdmVyYmFjaw0KODE4CU5QQ19LYXBwYVVuZGVhZA0KODM1CU5QQ19Ta2VsZXRvbkFybW9yDQo4NTUJTlBDX0JlYXJCcm93bg0KODgwCU5QQ19EZWF0aEtuaWdodE1pbmlvbg0KOTA5CU5QQ19Xb2xmRGlyZQ0KOTEwCU5QQ19ZZXRpDQoxMTUyCU5QQ19CZWFyDQoxMjc0CU5QQ19XaWdodA0KMTI5OAlOUENfUmhpbm9HcmV5DQoxMjk4CU5QQ19SaGlub1doaXRlDQoxMzY1CU5QQ19JbXBLaW5nDQoxNDY3CU5QQ19FbGtLaW5nDQoxNDY3CU5QQ19FbGVwaGFudA0KMTU5MAlOUENfQ3JvY29kaWxlR2lhbnRUb21iDQoxNjI2CU5QQ19Gcm9zdEdpYW50DQoxNjI2CU5QQ19Gcm9zdEdpYW50VHV0b3JpYWwNCjE3MzEJTlBDX1JvY2tub3NlS2luZw0KMTk3MwlOUENfTWFtbW90aA0KMjA3OAlOUENfUm9ja25vc2VLaW5nSWNlDQoyMTA4CU5QQ19TZXJwZW50cGVvcGxlQnJ1dGUNCjIxNDIJTlBDX1N0b3J5Ym9zcw0KMjE3NQlOUENfU2VycGVudHBlb3BsZUJvd0tpbmcNCjIxNzUJTlBDX1NlcnBlbnRwZW9wbGVTd29yZHNLaW5nDQoyMjQwCU5QQ19HaWFudEtpbmdHaG9zdA0KMjQ5MglOUENfV2l0Y2hRdWVlbkd1YXJkaWFuDQoyNTQwCU5QQ19TYW5kc3Rvcm1CZWFzdA0KMjcyMAlOUENfV2lsZEJvYXJCb3NzDQozMjAwCU5QQ19LYXBwYUtpbmcNCjM2NTAJTlBDX0JhdERlbW9uV2hpdGUNCjM4MzIJTlBDX0xhdmFXb3JtDQo0MDU0CU5QQ19TZXdlckFib21pbmF0aW9uDQo0MjEzCU5QQ19HaWFudEtpbmdCb3NzDQo0ODkwCU5QQ19SaGlub0JsYWNrDQo1NTY1CU5QQ19EcmFnb25IYXRjaGxpbmcNCjU4MjcJTlBDX0Zyb3N0R2lhbnRCb3NzDQo1ODI3CU5QQ19Gcm9zdEdpYW50U21pdGgNCjYxODMJTlBDX0RlYXRoS25pZ2h0Qm9zcw0KNjE4MwlOUENfTG9jdXN0UXVlZW5Td2FtcFRvbWINCjkwOTUJTlBDX1JvY2tub3NlS2luZ0Jvc3NNb3NzDQo5MTAwCU5QQ19EcmFnb24NCjkxMDAJTlBDX0RyYWdvbkdyZWVuDQo5MTAwCU5QQ19VbmRlYWREcmFnb24NCjkxMDAJTlBDX0RyYWdvbldoaXRlDQoxMDA1MwlOUENfQWxwaGFlbGVwaGFudA0KMTAwNTMJTlBDX0FscGhhc25ha2UNCjEwMDUzCU5QQ19TbmFrZUdpYW50DQoxMDI2NAlOUENfTG9jdXN0UXVlZW5EZXNlcnQNCjEwMjY0CU5QQ19Mb2N1c3RRdWVlblN3YW1wDQoxMDI2NAlOUENfVGlnZXJXaGl0ZQ0KMTA3NDQJTlBDX0RlbW9uU3BpZGVyDQoxMDc0NAlOUENfU3BpZGVyR2lhbnQNCjExMDQ0CU5QQ19Dcm9jb2RpbGVHaWFudA0KMTE0NzkJTlBDX1Njb3JwaW9uS2luZw0KMTE2NTEJTlBDX1JlcHRpbGVCZWFzdEJvc3MNCjEyOTAwCU5QQ19SaGlub0tpbmcNCjEzMjAwCU5QQ19Td2FtcEtpbmcNCg==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/929309228564296120/59CC87CB7CC2019162FFD950A15765B0420D4431/</image_url>
<download_url/>
<filename/>
<file_size>724791</file_size>
</mod>
<mod id="1369802940">
<name>Emberlight</name>
<description>TW9kIElEOiAxMzY5ODAyOTQwDQoNCldlbGNvbWUgdG8gRW1iZXJsaWdodCEgVGhpcyBtb2QgaXMgaW50ZW5kZWQgZm9yIHJvbGVwbGF5ZXJzIGFuZCBvdGhlciBDb25hbiBFeGlsZXMgcGxheWVycyB3aG8gd2FudCBhIHJpY2hlciBhbmQgbW9yZSBpbW1lcnNpdmUgZXhwZXJpZW5jZS4gSXQgZm9jdXNlcyBlbnRpcmVseSBvbiBjb250ZW50IGZvciBwbGF5ZXJzOyB5b3Ugd29uJ3QgbmVlZCB0aGUgYWRtaW4gcGFuZWwgdG8gYWNjZXNzIGFueSBvZiB0aGUgbW9kJ3MgY3VycmVudCBvciBmdXR1cmUgY29udGVudC4gUmVhZCBvbiB0byBzZWUgd2hhdCB0aGUgbW9kIGluY2x1ZGVzIGN1cnJlbnRseSBhbmQgd2hhdCB3ZSBoYXZlIHBsYW5uZWQgZm9yIGZvciB0aGUgd2Vla3MgYW5kIG1vbnRocyBhaGVhZC4NCg0KSm9pbiB0aGUgRW1iZXJsZWdpb24gb24gRGlzY29yZDoNCmh0dHBzOi8vZGlzY29yZC5nZy81TXY3ZWR5DQoNCllvdSBjYW4gYWxzbyBiZWNvbWUgYW4gRW1iZXJsaWdodCBQYXRyb24gaGVyZToNCmh0dHBzOi8vd3d3LnBhdHJlb24uY29tL3N0dWRpb2VtYmVybGlnaHQNCg0KDQoNCltoMV1GRUFUVVJFUyBTVU1NQVJZWy9oMV0NCg0KKFZpc2l0IG91ciBEaXNjdXNzaW9ucyB0YWIgZm9yIGEgbW9yZSBkZXRhaWxlZCBicmVha2Rvd24gb2YgRW1iZXJsaWdodCdzIGZlYXR1cmVzKQ0KDQpbYl1Ib3J0aWN1bHR1cmVbL2JdDQpbbGlzdF1bKl1DcmFmdCwgZmFybSBhbmQgZGVjb3JhdGUgd2l0aCBhIHdpZGUgdmFyaWV0eSBvZiBpdGVtczoNCltsaXN0XVsqXURlY29yYXRpdmUgZmxvd2VyIHBvdHMgYW5kIHRyZWVzDQpbKl1QbGFjZWFibGUgcG90dGVkIHBsYW50ZXJzIGFuZCBtdXNocm9vbSBib3hlcw0KWypdR2FyZGVuIGJveGVzIGFuZCB3ZWRnZXMgd2hpY2ggY2FuIHNuYXAgdG8gYnVpbGRpbmcgcGllY2VzWy9saXN0XVsvbGlzdF0NCg0KW2JdQW5pbWFsIEh1c2JhbmRyeVsvYl0NCltsaXN0XVsqXUJ1aWxkIHBlbnMgYW5kIGtlZXAgZ2FtZSBmb3IgaGlkZXMsIG1lYXQgYW5kIG90aGVyIHJlc291cmNlcy4gVGhlIGZvbGxvd2luZyBhbmltYWxzIGNhbiBiZSBkb21lc3RpY2F0ZWQ6DQpbbGlzdF1bKl1SYWJiaXRzDQpbKl1BbnRlbG9wZXMNClsqXUdhemVsbGUNClsqXU9zdHJpY2hlcw0KWypdR29hdHMNClsqXUJvYXINClsqXURlZXINClsqXUp1bmdsZSBCaXJkc1svbGlzdF1bL2xpc3RdDQoNCltiXUN1aXNpbmUhWy9iXQ0KW2xpc3RdWypdRm9vZCwgc291cHMgYW5kIGRyaW5rcyBhcmUgbm93IHZpc2libGUgd2hlbiBwbGFjZWQgaW4gdGhlIGludmVudG9yeSBvZiBzcGVjaWFsIFNlcnZpbmcgZGlzaGVzIGFuZCBtdWdzLCB0YW5rYXJkcyBhbmQgZmxhZ29ucy4gWW91IGNhbiBvYnRhaW4gdGhlc2UgaXRlbXMgZnJvbSB0aGUgSG9zcGl0YWxpdHkgZmVhdHMgaW4gdGhlIERlY29yYXRpb24gdGFiIG9mIHlvdXIgRmVhdHMgc2NyZWVuLlsvbGlzdF0NCg0KW2JdU3RyYWlnaHQgUmF6b3JbL2JdDQpbbGlzdF1bKl1Vc2UgdGhlIFZhbml0eSBwbGFjZWFibGUgaXRlbSB0byBjdXN0b21pemUgeW91ciBjaGFyYWN0ZXIncyBoZWFkIGFuZCBib2R5IGhhaXIuWy9saXN0XQ0KDQpbYl1BZGRpdGlvbmFsIFdlYXBvbnMgYW5kIEFybW9yWy9iXQ0KW2xpc3RdWypdQ3VsdHVyYWwgV2VhcG9ucyB3aXRoIGZ1bGwgdGllciBwcm9ncmVzc2lvbnMNClsqXUFybW9yIGFuZCBjbG90aGluZyB2YXJpYW50cw0KWypdRW5kZ2FtZSB2YXJpYW50cyBvZiBwb3B1bGFyIGxvd2VyLXRpZXIgd2VhcG9ucw0KWypdRmlzdCBXZWFwb25zIGZvciB0aWVycyAyIHRocm91Z2ggNQ0KWypdV29vZGVuIHdlYXBvbnMgZm9yIHNwYXJyaW5nDQpbKl1SdWdnZWQgV3JhcHMsIHJlaW50cm9kdWNpbmcgdGhlIGxvaW5jbG90aCBhbmQgY2hlc3R3cmFwIG9mIG9sZA0KWypdQ29sZCB3ZWF0aGVyIGNsaW1iaW5nIGJvb3RzIGFuZCBnbG92ZXMsIGxlYXJuZWQgdmlhIHRoZSBNb3VudGFpbmVlciBmZWF0Wy9saXN0XQ0KDQpbYl1BZGRpdGlvbmFsIEl0ZW1zWy9iXQ0KW2xpc3RdWypdQmluZGFibGUgQmVkIFBpbGxvd3MNClsqXUJvb2sgc2hlbHZlcyBhbmQgcGxhY2VhYmxlIHJvd3Mgb2Ygam91cm5hbHMgYW5kIHN0YWNrcyBvZiBzY3JvbGxzDQpbKl1TdG9uZSBidXRjaGVyIHRvb2xzIChsZWFybmVkIHdpdGggdGhlIEFwcHJlbnRpY2UgQnV0Y2hlciBmZWF0KQ0KWypdSXJvbiBTaWNrbGUgKGxlYXJuZWQgd2l0aCB0aGUgSXJvbiBUb29scyBmZWF0KVsvbGlzdF0NCg0KW2JdUXVhbGl0eSBvZiBsaWZlIGltcHJvdmVtZW50c1svYl0NCltsaXN0XVsqXXN0YWNrIHNpemVzIGluY3JlYXNlZCB0byAxMDAgZm9yIG1vc3QgY29uc3VtYWJsZXMgYW5kIG1hdGVyaWFscw0KWypdQmFzaWMgY3JhZnRpbmcgc3RhdGlvbiBpbnZlbnRvcnkgc2l6ZSBpbmNyZWFzZWQgdG8gMzAgc2xvdHMuDQpbKl1QcmVzZXJ2YXRpb24gYm94IGFuZCBJbXByb3ZlZCBQcmVzZXJ2YXRpb24gYm94IGludmVudG9yaWVzIGRvdWJsZWQuDQpbKl1CYXJyZWxzIGFuZCBTbWFsbCBCYXJyZWxzIGNhbiBub3cgYmUgdXNlZCB0byBzdG9yZSBpdGVtcw0KWypdQ29tYmluZSBMZWF0aGVyIHRvIG1ha2UgVGhpY2sgTGVhdGhlciBhdCB0aGUgQXJtb3JlcidzIEJlbmNoDQpbKl1DcmFmdGluZyBzaGFwZWQgd29vZCBwcm9kdWNlcyAxIGJhcmsNClsqXVZhbml0eSBDYW1lcmEgYWRqdXN0ZWQgdG8gYWxsb3cgeW91IHRvIGdldCB5b3VyIEdVSSBiYWNrIGJ5IGNyb3VjaGluZyBvciBlbW90aW5nLlsvbGlzdF0NCg0KDQoNCltoMV1GRUFUVVJFUyAoQ29taW5nIFNvb24hKVsvaDFdDQoNClsqXU1vcmUgY3VsdHVyYWwgd2VhcG9ucyBhbmQgbW9yZSBhcm1vciB2YXJpYW50cw0KWypdQ29uc2NyaXB0cyEgU2VuZCB5b3VyIHRocmFsbHMgb3V0IHRvIGNvbGxlY3QgcmVzb3VyY2VzIGFuZCBjb21wbGV0ZSBvdGhlciB0YXNrcw0KWypdTmV3IGJ1aWxkaW5nIGJsb2NrcyBhbmQgZGVjbyBpdGVtc1svbGlzdF0NCg0KDQoNCltoMV1GRUFUVVJFUyAoQ29taW5nIG5vdCBhcyBzb29uISlbL2gxXQ0KDQpbbGlzdF1bKl1BZHZhbmNlZCBjb21iYXQNClsqXUFkdmFuY2VkIHJlbGlnaW9uLCBteXN0aWNpc20gYW5kIGFsY2hlbXlbL2xpc3RdDQoNCg0KDQpbaDFdS05PV04gSVNTVUVTWy9oMV0NCg0KWypdU29tZSBob3J0aWN1bHR1cmUgaXRlbXMgZG9uJ3QgcGxheSB0aGVpciBwbGFjZW1lbnQgc291bmRzLg0KWypdR2FyZGVuIFdlZGdlcyBjYW4gYmUgcGxhY2VkIGluc2lkZSBvZiBHYXJkZW4gQm94ZXMuIElmIHlvdSBkbyB0aGlzLCB5b3UncmUgYmFkLiBEb24ndCBiZSBiYWQuDQpbKl1Mb290IHByZXZpZXcgZG9lcyBub3Qgc2hvdyB0aGUgb3V0cHV0IG9mIEhvcnRpY3VsdHVyZSBtYWNoaW5lcyAoZ2FyZGVuIGJveGVzIGFuZCB3ZWRnZXMsIHBsYW50ZXJzIGFuZCBtdXNocm9vbSBib3hlcykuIFRoZSBvdXRwdXQgb2YgdGhlc2Ugc3RhdGlvbnMgaXMgY2FsY3VsYXRlZCBvbiBwbGF5ZXIgaW50ZXJhY3Rpb24gdG8gcmVkdWNlIHNlcnZlciBsb2FkLiBXZSdyZSB3b3JraW5nIG9uIGEgc29sdXRpb24gZm9yIHRoaXMuWy9saXN0XQ0KDQoNCg0KU3BlY2lhbCB0aGFua3MgdG8gSm9zaHRlY2ggYW5kIHRoZSBQSVBQSSB0ZWFtIGZvciB0aGVpciBzdXBwb3J0IGFuZCBicmFpbiBwb3dlci4gd2UgPDMgdSBndXl6Lg==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/929308506301543680/CE3C865085C0C5FF2879FB8E9D875098C17F01B5/</image_url>
<download_url/>
<filename/>
<file_size>1667910631</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable/>
<place_after/>
<mod_string>%first_file%</mod_string>
<string_separator>\n</string_separator>
<filepath>ConanSandbox/Mods/modlist.txt</filepath>
</config>
<post_install>printf "\nRunning post installation for mod %workshop_mod_id%"&#xD;
printf "\nMovin Folders"&#xD;
mv %mods_full_path%/steamapps/workshop/content/440900/%workshop_mod_id%/%first_file% %mods_full_path%/%first_file%&#xD;
printf "\nCleaning up"&#xD;
rm -Rf %mods_full_path%/steamapps/workshop/content/440900/%workshop_mod_id%&#xD;
printf "\nInstallation for mod %workshop_mod_id% completed!"&#xD;
</post_install>
<uninstall>printf "\nUninstalling...\n"&#xD;
rm -vf %mods_full_path%/%mod_string%&#xD;
printf "\nDone!"&#xD;
</uninstall>
</workshop_settings>

View file

@ -1,24 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id>211820</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>mods</mods_path>
<mods/>
<config>
<regex>mods=(([0-9]+,?)*)</regex>
<mods_backreference_index>1</mods_backreference_index>
<variable>mods=</variable>
<place_after/>
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>,</string_separator>
<filepath>steam_workshop.cfg</filepath>
</config>
<post_install>printf "\nMoving item %workshop_mod_id% ..."&#xD;
mv -f "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%/contents.pak" "%mods_full_path%/%workshop_mod_id%.pak"&#xD;
rm -Rf "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%"&#xD;
printf "\nSuccess."</post_install>
<uninstall>printf "\nUninstalling item %mod_string% ...\n"&#xD;
rm -Rf "%mods_full_path%/%mod_string%.pak"&#xD;
printf "\nSuccess."</uninstall>
</workshop_settings>

View file

@ -1,24 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id>211820</workshop_id>
<download_method>steamcmd</download_method>
<anonymous_login>0</anonymous_login>
<mods_path>mods</mods_path>
<mods/>
<config>
<regex>mods=(([0-9]+,?)*)</regex>
<mods_backreference_index>1</mods_backreference_index>
<variable>mods=</variable>
<place_after/>
<mod_string>%workshop_mod_id%</mod_string>
<string_separator>,</string_separator>
<filepath>steam_workshop.cfg</filepath>
</config>
<post_install>printf "\nMoving item %workshop_mod_id% ..."&#xD;
mv -f "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%/contents.pak" "%mods_full_path%/%workshop_mod_id%.pak"&#xD;
rm -Rf "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%"&#xD;
printf "\nSuccess."</post_install>
<uninstall>printf "\nUninstalling item %mod_string% ...\n"&#xD;
rm -Rf "%mods_full_path%/%mod_string%.pak"&#xD;
printf "\nSuccess."</uninstall>
</workshop_settings>

View file

@ -1,67 +0,0 @@
<?xml version="1.0"?>
<workshop_settings>
<workshop_id>730</workshop_id>
<download_method>steamapi</download_method>
<anonymous_login>1</anonymous_login>
<mods_path>mods</mods_path>
<mods>
<mod id="1433404064">
<name>Mirage [Compatibility Version 1.36.3.8]</name>
<description>QW4gb2xkZXIgdmVyc2lvbiBvZiBvZmZpY2lhbCBtYXAgYnkgVmFsdmUgZm9yIGRlbW8gcGxheWJhY2sgY29tcGF0aWJpbGl0eS4gVGhpcyBtYXAgd2FzIHByZXZpb3VzbHkgdXNlZCBpbiBPZmZpY2lhbCBNYXRjaG1ha2luZyBpbiBDUzpHTy4gSXQgY291bGQgYmUgcGxheWVkIGluIERlYXRobWF0Y2gsIENsYXNzaWMgQ2FzdWFsLCBhbmQgQ2xhc3NpYyBDb21wZXRpdGl2ZS4=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/387665671598503104/9BC8E9D876916173C915233460D559231FF4E4E3/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/945077059916661709/A20ADA8668F0BB0EE12F61314137BE71EFDFF6C3/</download_url>
<filename>de_mirage.bsp</filename>
<file_size>17429043</file_size>
</mod>
<mod id="1440818854">
<name>cs_noffice [office in nuke-style]</name>
<description>YSBzbWFsbCBmdW5tYXANCg0KZW5qb3kgYW5kIGhhdmUgZnVuIDotKQ==</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/964217986228487212/CF7FB6AFE894AF59908CDA64AD5E8F852D39AE1E/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/964217986228482922/A3EB0675317A2395DC96870AFE3EDB9608616787/</download_url>
<filename>cs_noffice.bsp</filename>
<file_size>72639068</file_size>
</mod>
<mod id="1414531578">
<name>de_cornerwork</name>
<description>RGVfY29ybmVyd29yayBmcm9tIENTTzIsIG1hZGUgYnkgTmV4b24=</description>
<image_url>https://steamuserimages-a.akamaihd.net/ugc/938320142839248719/DE42CB9345A53EC8B4BBE5381D8AD55407FD88D1/</image_url>
<download_url>https://steamusercontent-a.akamaihd.net/ugc/938321006101014631/2F2EF3472A0FC4B10D1AD559FC516B742AF43C15/</download_url>
<filename>de_cornerwork.bsp</filename>
<file_size>87872150</file_size>
</mod>
</mods>
<config>
<regex>(.*\n?)*</regex>
<mods_backreference_index>0</mods_backreference_index>
<variable/>
<place_after/>
<mod_string>%first_file%</mod_string>
<string_separator>\n</string_separator>
<filepath>mods/modlist.txt</filepath>
</config>
<post_install>printf "\nRunning post installation for mod %workshop_mod_id%"&#xD;
printf "\nInstalling Map %first_file%\n"&#xD;
unzip -o "%mods_full_path%/steamapps/workshop/content/730/%workshop_mod_id%/%first_file%" -d "%mods_full_path%/../csgo/maps"&#xD;
printf "\nCleaning up"&#xD;
rm -Rf "%mods_full_path%/steamapps/workshop/content/730/%workshop_mod_id%"&#xD;
map=%first_file%&#xD;
map=${map%.bsp}&#xD;
maplist_file="%mods_full_path%/../csgo/maplist.txt"&#xD;
maplist_content=$(cat "$maplist_file")&#xD;
if [ ! -z "${maplist_content##*$map*}" ];then&#xD;
printf "\nAdding Map to maplist.txt"&#xD;
echo $map &gt;&gt; "$maplist_file"&#xD;
else&#xD;
printf "\nMap already in maplist.txt"&#xD;
fi&#xD;
printf "\nInstallation for map %first_file% completed!"&#xD;
</post_install>
<uninstall>map=%mod_string%&#xD;
if [ -f "%mods_full_path%/../csgo/maps/$map" ];then&#xD;
rm -f $map&#xD;
fi&#xD;
map=${map%.bsp}&#xD;
maplist_file="%mods_full_path%/../csgo/maplist.txt"&#xD;
sed -i "/^$map$/d" $maplist_file&#xD;
</uninstall>
</workshop_settings>

View file

@ -1,8 +0,0 @@
#!/bin/bash
for f in *.xml; do
bakUp=${f%.*}.bak.xml
mv $f $bakUp
xmlsort -r 'mods/mod' -k 'name' -i -s $bakUp > $f
rm $bakUp
done

View file

@ -1,22 +0,0 @@
#!/bin/bash
if [ -z "$1" ]; then
for f in *.xml; do
bakUp=${f%.*}.bak.xml
echo "Working on.... $f"
mv $f $bakUp
xmlsort -r 'mods/mod' -k 'name' -i -s $bakUp > $f
rm $bakUp
done
else
f="$1"
echo "Working on.... $f"
bakUp=${f%.*}.bak.xml
mv $f $bakUp
xmlsort -r 'mods/mod' -k 'name' -i -s $bakUp > $f
rm $bakUp
fi
chown www-data:www-data *.xml

View file

@ -0,0 +1,366 @@
<?php
/*
* GSP Steam Workshop: shared helper functions
* Copyright (C) 2025 WDS / GameServerPanel
*
* 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
* (at your option) any later version.
*/
// ── Profile helpers ───────────────────────────────────────────────────────
/**
* Return all rows from steam_workshop_game_profiles ordered by game_name.
*
* @param OGPDatabase $db
* @return array|false
*/
function sw_get_profiles($db)
{
return $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_game_profiles`
ORDER BY `game_name` ASC, `config_name` ASC"
);
}
/**
* Return a single profile row by primary key.
*
* @param OGPDatabase $db
* @param int $id
* @return array|false
*/
function sw_get_profile_by_id($db, $id)
{
$id = (int)$id;
$rows = $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_game_profiles`
WHERE `id` = $id LIMIT 1"
);
return ($rows && isset($rows[0])) ? $rows[0] : false;
}
/**
* Return a single profile row by config_name (= game_key from XML).
*
* @param OGPDatabase $db
* @param string $config_name
* @return array|false
*/
function sw_get_profile_by_config_name($db, $config_name)
{
$safe = $db->realEscapeSingle($config_name);
$rows = $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_game_profiles`
WHERE `config_name` = '$safe' LIMIT 1"
);
return ($rows && isset($rows[0])) ? $rows[0] : false;
}
/**
* Return the Workshop profile that applies to a server home.
* Resolves: home_id config_homes.game_key workshop profile.
*
* @param OGPDatabase $db
* @param int $home_id
* @return array|false profile row or false when none found / not enabled
*/
function sw_get_profile_for_home($db, $home_id)
{
$home_id = (int)$home_id;
$rows = $db->resultQuery(
"SELECT p.*
FROM `OGP_DB_PREFIXsteam_workshop_game_profiles` p
JOIN `OGP_DB_PREFIXconfig_homes` c
ON c.`game_key` = p.`config_name`
JOIN `OGP_DB_PREFIXserver_homes` s
ON s.`home_cfg_id` = c.`home_cfg_id`
WHERE s.`home_id` = $home_id
AND p.`enabled` = 1
LIMIT 1"
);
return ($rows && isset($rows[0])) ? $rows[0] : false;
}
// ── Mod helpers ───────────────────────────────────────────────────────────
/**
* Return all mods for a server, sorted by sort_order ASC.
*
* @param OGPDatabase $db
* @param int $home_id
* @return array|false
*/
function sw_get_server_mods($db, $home_id)
{
$home_id = (int)$home_id;
return $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
WHERE `home_id` = $home_id
ORDER BY `sort_order` ASC, `id` ASC"
);
}
/**
* Return a single mod row by primary key.
*
* @param OGPDatabase $db
* @param int $id
* @return array|false
*/
function sw_get_mod_by_id($db, $id)
{
$id = (int)$id;
$rows = $db->resultQuery(
"SELECT * FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
WHERE `id` = $id LIMIT 1"
);
return ($rows && isset($rows[0])) ? $rows[0] : false;
}
// ── Server / ownership helpers ────────────────────────────────────────────
/**
* Return server_homes row joined with config_homes and remote_servers
* for the given home_id, or false when not found.
*
* @param OGPDatabase $db
* @param int $home_id
* @return array|false
*/
function sw_get_home_info($db, $home_id)
{
$home_id = (int)$home_id;
$rows = $db->resultQuery(
"SELECT s.*, c.`game_key`, c.`game_name`, r.`agent_ip`, r.`agent_port`
FROM `OGP_DB_PREFIXserver_homes` s
JOIN `OGP_DB_PREFIXconfig_homes` c ON c.`home_cfg_id` = s.`home_cfg_id`
JOIN `OGP_DB_PREFIXremote_servers` r ON r.`remote_server_id` = s.`remote_server_id`
WHERE s.`home_id` = $home_id LIMIT 1"
);
return ($rows && isset($rows[0])) ? $rows[0] : false;
}
/**
* Verify that the current session user is allowed to manage this home.
* Admins always pass. Regular users/subusers must have an entry in
* user_homes (or be the user_id_main).
*
* @param OGPDatabase $db
* @param int $user_id
* @param int $home_id
* @return bool
*/
function sw_user_owns_home($db, $user_id, $home_id)
{
if (!isset($_SESSION['users_group'])) {
return false;
}
if ($_SESSION['users_group'] === 'admin') {
return true;
}
$user_id = (int)$user_id;
$home_id = (int)$home_id;
// Direct owner
$rows = $db->resultQuery(
"SELECT 1 FROM `OGP_DB_PREFIXserver_homes`
WHERE `home_id` = $home_id AND `user_id_main` = $user_id LIMIT 1"
);
if ($rows) {
return true;
}
// Assigned via user_homes
$rows = $db->resultQuery(
"SELECT 1 FROM `OGP_DB_PREFIXuser_homes`
WHERE `home_id` = $home_id AND `user_id` = $user_id LIMIT 1"
);
if ($rows) {
return true;
}
// Assigned via group
$rows = $db->resultQuery(
"SELECT 1 FROM `OGP_DB_PREFIXuser_group_homes` ugh
JOIN `OGP_DB_PREFIXuser_groups` ug ON ug.`group_id` = ugh.`group_id`
WHERE ugh.`home_id` = $home_id AND ug.`user_id` = $user_id LIMIT 1"
);
return (bool)$rows;
}
// ── Game-config helpers ───────────────────────────────────────────────────
/**
* Return an array of all game configs from the XML files.
* Each element is a SimpleXMLElement (game_config root node).
*
* @return SimpleXMLElement[]
*/
function sw_get_all_game_configs()
{
if (!defined('SERVER_CONFIG_LOCATION')) {
// server_config_parser.php defines this; load it if not already done.
if (file_exists(__DIR__ . '/../../config_games/server_config_parser.php')) {
require_once __DIR__ . '/../../config_games/server_config_parser.php';
} else {
return array();
}
}
$configs = array();
foreach (glob(SERVER_CONFIG_LOCATION . '*.xml') as $file) {
$xml = read_server_config($file);
if ($xml !== false) {
$configs[] = $xml;
}
}
return $configs;
}
/**
* Ensure every game config has a matching row in steam_workshop_game_profiles.
* Only creates rows that are missing; never overwrites existing data.
*
* @param OGPDatabase $db
* @return int number of new rows inserted
*/
function sw_sync_profiles($db)
{
$configs = sw_get_all_game_configs();
$created = 0;
foreach ($configs as $xml) {
$config_name = (string)$xml->game_key;
$game_name = (string)$xml->game_name;
if (empty($config_name)) {
continue;
}
$existing = sw_get_profile_by_config_name($db, $config_name);
if ($existing) {
continue; // already have a profile for this game config
}
$safe_config = $db->realEscapeSingle($config_name);
$safe_name = $db->realEscapeSingle($game_name);
$ok = $db->query(
"INSERT IGNORE INTO `OGP_DB_PREFIXsteam_workshop_game_profiles`
(`config_name`, `game_name`, `enabled`)
VALUES ('$safe_config', '$safe_name', 0)"
);
if ($ok) {
$created++;
}
}
return $created;
}
// ── Template / launch-param helpers ─────────────────────────────────────
/**
* Replace {PLACEHOLDER} tokens in $template with values from $vars.
* Unknown tokens are left intact so admins can spot missing values.
*
* @param string $template
* @param array $vars associative: 'PLACEHOLDER' => 'value'
* @return string
*/
function sw_apply_template($template, array $vars)
{
$search = array();
$replace = array();
foreach ($vars as $key => $value) {
$search[] = '{' . $key . '}';
$replace[] = (string)$value;
}
return str_replace($search, $replace, $template);
}
/**
* Build the -mod= and -serverMod= launch parameter strings from an ordered
* list of enabled mods and the game profile.
*
* Returns an associative array:
* 'mod' => '-mod=@Mod1;@Mod2' (client mods)
* 'servermod' => '-serverMod=@ServerOnly' (server-side mods)
* 'combined' => '-mod=... -serverMod=...' (ready-to-paste)
*
* @param array $mods rows from steam_workshop_server_mods (must be pre-filtered
* for enabled = 1 and sorted by sort_order)
* @param array $profile row from steam_workshop_game_profiles
* @return array
*/
function sw_generate_launch_params(array $mods, array $profile)
{
$mod_param = trim($profile['mod_launch_param_template'] ?? '-mod=');
$servermod_param = trim($profile['servermod_launch_param_template'] ?? '-serverMod=');
$client_folders = array();
$server_folders = array();
foreach ($mods as $mod) {
if (empty($mod['enabled'])) {
continue;
}
$folder = !empty($mod['folder_name']) ? $mod['folder_name'] : ('@' . $mod['workshop_id']);
if ($mod['mod_type'] === 'server') {
$server_folders[] = $folder;
} else {
$client_folders[] = $folder;
}
}
$mod_str = $client_folders ? ($mod_param . implode(';', $client_folders)) : '';
$servermod_str = $server_folders ? ($servermod_param . implode(';', $server_folders)) : '';
$combined = trim($mod_str . ' ' . $servermod_str);
return array(
'mod' => $mod_str,
'servermod' => $servermod_str,
'combined' => $combined,
);
}
// ── Output helpers ────────────────────────────────────────────────────────
/**
* Render a short inline success banner.
*
* @param string $msg
* @return void
*/
function sw_success($msg)
{
echo '<div style="background:#d4edda;border:1px solid #c3e6cb;color:#155724;padding:8px 12px;margin:8px 0;border-radius:4px;">'
. htmlspecialchars($msg, ENT_QUOTES, 'UTF-8') . '</div>';
}
/**
* Render a short inline error banner.
*
* @param string $msg
* @return void
*/
function sw_error($msg)
{
echo '<div style="background:#f8d7da;border:1px solid #f5c6cb;color:#721c24;padding:8px 12px;margin:8px 0;border-radius:4px;">'
. htmlspecialchars($msg, ENT_QUOTES, 'UTF-8') . '</div>';
}
/**
* Escape a value for HTML output.
*
* @param mixed $v
* @return string
*/
function sw_h($v)
{
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
}

View file

@ -0,0 +1,89 @@
-- GSP Steam Workshop Manual SQL Reference
-- =========================================
-- Replace PREFIX_ with your actual table prefix (e.g. gsp_).
-- Compatible with MySQL 5.7 and MySQL 8.0.
-- Do NOT hardcode any database name here.
-- Run in the panel database.
-- ── Drop legacy tables (if upgrading from the old adapter-based implementation) ──
DROP TABLE IF EXISTS `PREFIX_workshop_game_profiles`;
DROP TABLE IF EXISTS `PREFIX_workshop_cache`;
DROP TABLE IF EXISTS `PREFIX_server_workshop_mods`;
DROP TABLE IF EXISTS `PREFIX_server_workshop_settings`;
-- ── Create new tables ─────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS `PREFIX_steam_workshop_game_profiles` (
`id` INT NOT NULL AUTO_INCREMENT,
`config_name` VARCHAR(100) NOT NULL,
`game_name` VARCHAR(255) NOT NULL DEFAULT '',
`enabled` TINYINT(1) NOT NULL DEFAULT 0,
`steam_app_id` VARCHAR(32) NOT NULL DEFAULT '',
`workshop_app_id` VARCHAR(32) NOT NULL DEFAULT '',
`steam_login_required` TINYINT(1) NOT NULL DEFAULT 0,
`steamcmd_login_mode` ENUM('anonymous','account') NOT NULL DEFAULT 'anonymous',
`steamcmd_path` VARCHAR(512) NOT NULL DEFAULT '/home/gameserver/steamcmd/steamcmd.sh',
`workshop_download_dir_template` TEXT NULL,
`server_root_template` TEXT NULL,
`install_path_template` TEXT NULL,
`folder_naming_format` VARCHAR(64) NOT NULL DEFAULT '@{MOD_NAME}',
`mod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-mod=',
`servermod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-serverMod=',
`install_script_template` TEXT NULL,
`update_script_template` TEXT NULL,
`copy_bikeys_enabled` TINYINT(1) NOT NULL DEFAULT 1,
`notes` TEXT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_config_name` (`config_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `PREFIX_steam_workshop_server_mods` (
`id` INT NOT NULL AUTO_INCREMENT,
`home_id` INT NOT NULL,
`profile_id` INT NOT NULL,
`workshop_id` VARCHAR(64) NOT NULL,
`mod_name` VARCHAR(255) NOT NULL DEFAULT '',
`folder_name` VARCHAR(255) NOT NULL DEFAULT '',
`mod_type` ENUM('client','server') NOT NULL DEFAULT 'client',
`sort_order` INT NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`install_status` VARCHAR(32) NOT NULL DEFAULT '',
`last_installed_at` DATETIME NULL,
`last_updated_at` DATETIME NULL,
`last_error` TEXT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ── Example: DayZ profile ────────────────────────────────────────────────
-- After running the above, insert an example DayZ profile.
-- Adjust config_name to match your actual DayZ game_key from config_homes.
-- (Run `SELECT game_key, game_name FROM PREFIX_config_homes WHERE game_name LIKE '%DayZ%';`
-- to find the right config_name.)
--
-- INSERT INTO `PREFIX_steam_workshop_game_profiles`
-- (`config_name`, `game_name`, `enabled`,
-- `steam_app_id`, `workshop_app_id`,
-- `steamcmd_path`,
-- `workshop_download_dir_template`,
-- `server_root_template`,
-- `install_path_template`,
-- `folder_naming_format`,
-- `mod_launch_param_template`,
-- `servermod_launch_param_template`,
-- `copy_bikeys_enabled`)
-- VALUES
-- ('dayz_win64', 'DayZ', 1,
-- '223350', '221100',
-- '/home/gameserver/steamcmd/steamcmd.sh',
-- '{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}',
-- '/home/gameserver/servers/{HOME_ID}',
-- '{SERVER_ROOT}/{MOD_FOLDER}',
-- '@{MOD_NAME}',
-- '-mod=',
-- '-serverMod=',
-- 1);

View file

@ -1,300 +0,0 @@
<?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 type',
'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.',
'hint_admin_only' => 'Managed by your administrator.',
'adapter_locked_note' => 'The game type for this server is managed by your administrator.',
'admin_heading_game_mapping' => 'Game type mapping',
'admin_subheading_game_mapping' => 'Pick which game configuration becomes the default whenever a server of that game opens the Workshop UI.',
'admin_col_game_key' => 'Game key',
'admin_col_adapter' => 'Game configuration',
'admin_no_game_keys' => 'No server configuration XML files were detected.',
'admin_heading_adapters' => 'Available game configurations',
'admin_col_key' => 'Key',
'admin_col_mods_dir' => 'Mods directory',
'admin_col_notes' => 'Notes',
'admin_heading_per_game' => 'Per-game Workshop configurations',
'admin_subheading_per_game' => 'Each game should have its own Workshop configuration to control how mods are installed.',
'admin_col_status' => 'Status',
'admin_col_updated' => 'Last updated',
'admin_col_actions' => 'Actions',
'admin_heading_edit_adapter' => 'Editing Workshop configuration for %s',
'admin_hint_select_game' => 'Select a game in the table above to edit or create its Workshop configuration.',
'status_no_adapter' => 'Not configured',
'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' => 'Game',
'summary_interval' => 'Interval',
'summary_mods' => 'Mods',
'summary_last_saved' => 'Last saved',
'summary_hot_reload' => 'Hot reload',
'raw_definition_label' => 'Raw Workshop list',
'message_mappings_saved' => 'Game type mappings saved.',
'message_adapter_saved' => 'Game configuration saved.',
'message_adapter_deleted' => 'Game configuration deleted.',
'error_admin_only' => 'Administrator access required.',
'mod_picker_heading' => 'Workshop library',
'mod_picker_hint' => 'Search Steam Workshop and add mods to keep them synced automatically.',
'mod_picker_search_label' => 'Search Steam Workshop',
'mod_picker_search_placeholder' => 'Example: 221100 or QoL tweaks',
'mod_picker_search_button' => 'Search',
'mod_picker_selected_heading' => 'Selected mods',
'mod_picker_selected_hint' => 'Enable syncing to pull each mod before game server restarts.',
'mod_picker_selected_empty' => 'No mods selected yet. Use the search above to add your first entry.',
'mod_picker_results_heading' => 'Search results',
'mod_picker_results_select' => 'Select',
'mod_picker_results_title' => 'Title',
'mod_picker_results_author' => 'Author',
'mod_picker_action_add' => 'Add',
'mod_picker_action_remove' => 'Remove',
'mod_picker_status_loading' => 'Searching Steam Workshop…',
'mod_picker_status_error' => 'Unable to contact the Steam Workshop. Try again in a moment.',
'mod_picker_results_empty' => 'No workshop items matched that search.',
'mod_picker_status_need_query' => 'Enter a Workshop ID or keyword before searching.',
'mod_picker_toggle_label' => 'Sync',
'mod_picker_request_label' => 'Submitting request',
'mod_picker_request_hint' => 'Exact Steam URL preview. The input shows the text that will be submitted.',
'mod_picker_request_input_label' => 'Workshop query preview',
'error_game_key_required' => 'Select a valid game key before editing the Workshop configuration.',
'error_adapter_delete_failed' => 'Game configuration could not be deleted.',
'button_edit_adapter' => 'Edit',
'button_create_adapter' => 'Create',
'button_delete_adapter' => 'Delete',
'button_save_adapter' => 'Save game configuration',
'confirm_delete_adapter' => 'Delete this game configuration? Servers mapped to it will fall back to defaults.',
'label_game_key' => 'Game key',
'label_adapter_name' => 'Game display name',
'label_adapter_app_id' => 'Steam App ID',
'label_adapter_mods_dir' => 'Mods directory',
'label_adapter_keys_dir' => 'Keys directory (optional)',
'label_adapter_hot_reload' => 'Supports hot reload',
'label_adapter_activation' => 'Activation template',
'label_adapter_notes' => 'Notes',
'error_missing_query' => 'Enter a search term before querying the Workshop.',
// -------------------------------------------------------
// Workshop game configuration admin (WorkshopProfileController)
// -------------------------------------------------------
'config_heading_list' => 'Workshop Game Configurations',
'config_intro' => 'One configuration per supported game. Each configuration controls how SteamCMD downloads and installs Workshop mods for servers of that game type.',
'config_btn_create' => 'Add Game Configuration',
'config_list_empty' => 'No Workshop configurations defined yet. Add one for each game that supports Steam Workshop mods.',
'config_confirm_delete' => 'Delete this Workshop configuration? Servers using it will no longer have Workshop mod support.',
'config_back_list' => 'Back to configurations',
'config_heading_edit' => 'Edit Workshop Configuration: %s',
'config_heading_create' => 'Add Workshop Game Configuration',
'config_steamcmd_heading' => 'How mods are downloaded',
'config_steamcmd_note' => 'Workshop mods are downloaded using SteamCMD with the command: +workshop_download_item {app_id} {mod_id}. The cache path below is where SteamCMD stores downloaded mod files on the agent machine. The install path below is where those files are copied into the game server directory.',
'config_label_app_id' => 'Steam App ID',
'config_hint_app_id' => 'The Steam App ID used with +workshop_download_item, e.g. 107410 for Arma 3',
'config_hint_game_key' => 'Short identifier matching the game XML key, e.g. arma3_linux',
'config_section_copy' => 'Copy / sync method',
'config_hint_launch_tpl' => 'Extra launch parameters added when this game has Workshop mods enabled. E.g. -mod=@{mod_id}',
'config_label_enabled' => 'Configuration enabled (allows servers to use Workshop mods for this game)',
'profile_col_game' => 'Game',
'profile_col_key' => 'Game Key',
'profile_col_method' => 'Install Method',
'profile_col_restart' => 'Restart?',
'profile_col_status' => 'Status',
'profile_section_basic' => 'Basic info',
'profile_section_paths' => 'Paths & templates',
'profile_section_config' => 'Config & launch parameters',
'profile_section_flags' => 'Flags',
'profile_label_game_name' => 'Game name',
'profile_label_os' => 'Supported OS',
'profile_label_cache_path' => 'SteamCMD cache path template',
'profile_hint_cache_path' => 'Where SteamCMD downloads mods on the agent. E.g. {steamcmd_path}/steamapps/workshop/content/{workshop_app_id}/{mod_id}',
'profile_label_install_path' => 'Server install path template',
'profile_hint_install_path' => 'Where mod files are placed inside the game server directory. E.g. {server_path}/mods/{mod_folder}',
'profile_label_folder_name' => 'Mod folder name template',
'profile_hint_folder_name' => 'Folder name for each mod inside the install path. Default: @{mod_id}',
'profile_label_copy_method' => 'Method used to copy mod files from SteamCMD cache to the server',
'profile_label_install_script'=> 'Custom install script (optional, admin-defined)',
'profile_hint_install_script' => 'Only used when copy method is custom_script. Template variables are replaced before execution.',
'profile_label_config_tpl' => 'Config file template (optional)',
'profile_label_launch_tpl' => 'Launch parameter template (optional)',
'profile_label_requires_restart' => 'Server restart required after mod install or update',
'profile_template_vars' => 'Available: {home_id} {agent_id} {workshop_app_id} {mod_id} {mod_title} {mod_folder} {steamcmd_path} {server_path} {install_path} {cache_path}',
'profile_saved' => 'Workshop configuration saved.',
'profile_save_error' => 'Failed to save Workshop configuration.',
'profile_deleted' => 'Workshop configuration deleted.',
'profile_delete_error' => 'Failed to delete Workshop configuration.',
'profile_not_found' => 'Configuration not found.',
'button_delete' => 'Delete',
'error_game_key_invalid' => 'Game key may only contain letters, digits, underscores, dots, and hyphens.',
'error_game_name_required' => 'Game name is required.',
'error_app_id_required' => 'Workshop App ID is required (numeric).',
'error_cache_path_required' => 'SteamCMD cache path template is required.',
'error_install_path_required' => 'Server install path template is required.',
// -------------------------------------------------------
// User mod management (WorkshopModController)
// -------------------------------------------------------
'user_workshop_heading' => 'Steam Workshop',
'user_workshop_server_heading' => 'Workshop Mods %s',
'col_server' => 'Server',
'col_game' => 'Game',
'col_mods_count' => 'Installed mods',
'col_profile' => 'Profile',
'col_mod_id' => 'Workshop ID',
'col_mod_title' => 'Title',
'col_load_order' => 'Load order',
'col_cache_status' => 'Cache status',
'no_profile' => 'No profile',
'hint_no_profile' => 'Ask an admin to create a Workshop profile for this game.',
'btn_manage_mods' => 'Manage Mods',
'no_profile_notice' => 'No Workshop profile is configured for this game. An administrator needs to create one first.',
'heading_installed_mods' => 'Installed Mods',
'no_installed_mods' => 'No mods installed yet.',
'heading_cached_mods' => 'Available Cached Mods (this agent)',
'heading_install_mod' => 'Install Mod by Workshop ID',
'label_workshop_id_input' => 'Workshop ID',
'placeholder_workshop_id' => 'e.g. 1234567890',
'btn_install_mod' => 'Install',
'btn_remove_mod' => 'Remove',
'btn_sync_now' => 'Sync now',
'confirm_remove_mod' => 'Remove this mod from this server? (Files on disk are not deleted.)',
'mod_installed' => 'Mod installed successfully.',
'mod_install_error' => 'Install failed: ',
'restart_required' => 'A server restart is required to activate this mod.',
'mod_removed' => 'Mod removed from this server.',
'mod_remove_error' => 'Failed to remove mod.',
'sync_success' => 'Mod synced successfully.',
'sync_no_change' => 'Mod is already up to date.',
'sync_error' => 'Sync failed: ',
'error_missing_params' => 'Missing required parameters.',
'error_no_profile' => 'No Workshop profile configured for this game.',
'error_mod_not_found' => 'Mod or profile not found.',
'error_toggle_failed' => 'Failed to update mod status.',
'error_order_failed' => 'Failed to update load order.',
// -------------------------------------------------------
// New v2 labels
// -------------------------------------------------------
// Admin profile form new fields
'config_steamcmd_note' => 'Workshop mods are downloaded using SteamCMD: +workshop_download_item <App ID> <Mod ID>. Configure the paths and scripts below to control how mods are installed for servers of this game type.',
'config_label_enabled' => 'Profile enabled',
'config_hint_game_key' => 'Short identifier matching the game XML key, e.g. dayz_linux',
'config_hint_app_id' => 'The App ID used with +workshop_download_item, e.g. 221100 for DayZ',
'config_hint_launch_tpl' => 'Complete launch parameter string appended to server start. Each mod folder is joined with the separator above.',
'profile_section_basic' => 'Basic identification',
'profile_section_steam' => 'Steam & SteamCMD settings',
'profile_section_paths' => 'Download & install paths',
'profile_section_folder' => 'Mod folder naming',
'profile_section_launch' => 'Launch parameters',
'profile_section_scripts' => 'Bash scripts',
'profile_section_flags' => 'Options & validation',
'profile_label_game_name' => 'Game display name',
'profile_label_steam_app_id' => 'Steam App ID',
'profile_hint_steam_app_id' => 'The Steam game App ID (e.g. 221100 for DayZ). Used when Steam login is required.',
'config_label_app_id' => 'Workshop App ID',
'config_hint_app_id' => 'The App ID used with +workshop_download_item, e.g. 221100 for DayZ',
'profile_label_steamcmd_path' => 'SteamCMD path on agent',
'profile_hint_steamcmd_path' => 'Full path to steamcmd.sh on the remote agent. Leave blank to use the agent default (/home/gameserver/steamcmd/steamcmd.sh).',
'profile_label_steam_login_required'=> 'Steam login required (game is not free / requires ownership)',
'profile_label_steamcmd_login_mode' => 'SteamCMD login mode',
'profile_hint_steamcmd_login_mode' => 'Use anonymous for free Workshop mods. Configured account mode stores the intent; full credential injection requires panel-level Steam account configuration (see admin docs).',
'profile_label_os' => 'Supported OS',
'profile_label_cache_path' => 'Workshop download/cache path',
'profile_hint_cache_path' => 'Where SteamCMD stores downloaded mod content on the agent. E.g. /home/gameserver/steamcmd/steamapps/workshop/content/%workshop_app_id%/%workshop_id%',
'profile_label_install_path' => 'Server mod install root',
'profile_hint_install_path' => 'Base directory inside the server where mods are installed. E.g. %server_path%/mods/%install_name%',
'profile_label_folder_format' => 'Folder naming format',
'profile_hint_folder_format' => 'How each mod folder is named inside the install root.',
'profile_label_folder_name' => 'Custom folder name template',
'profile_hint_folder_name' => 'Use %workshop_id% or %mod_name%. E.g. @%workshop_id%',
'profile_label_mod_launch_param' => 'Mod launch parameter format',
'profile_hint_mod_launch_param' => 'How the full mod list is passed to the server start command. E.g. -mod=%mods%',
'profile_label_mod_separator' => 'Mod separator',
'profile_hint_mod_separator' => 'Character used to join multiple mod folder names in the launch parameter.',
'profile_label_launch_tpl' => 'Full launch parameter template (optional)',
'profile_label_copy_method' => 'Copy method',
'profile_label_copy_keys' => 'Copy mod keys (*.bikey) to server keys directory',
'profile_label_key_source' => 'Key source path',
'profile_hint_key_source' => 'Path inside the mod cache where key files live. E.g. %source_path%/keys',
'profile_label_key_dest' => 'Key destination path',
'profile_hint_key_dest' => 'Where keys are copied on the server. E.g. %server_path%/keys',
'profile_label_pre_script' => 'Pre-update bash script',
'profile_hint_pre_script' => 'Runs once before any mod is downloaded/installed. Variables: %home_id% %server_path% %workshop_app_id%',
'profile_label_install_script' => 'Per-mod install bash script',
'profile_hint_install_script' => 'Runs once for each mod. All template variables listed above are available.',
'profile_label_post_script' => 'Post-update bash script',
'profile_hint_post_script' => 'Runs once after all mods have been installed. Variables: %home_id% %server_path% %workshop_app_id%',
'profile_label_requires_restart' => 'Server restart required after mod install or update',
'profile_label_validation_notes' => 'Validation notes / help text (shown to server owners)',
'profile_label_config_tpl' => 'Config file template (optional)',
'profile_template_vars' => 'Variables: %home_id% %server_path% %steam_app_id% %workshop_app_id% %workshop_id% %mod_name% %install_name% %download_path% %source_path% %target_path% %keys_source_path% %keys_target_path% %steamcmd_path%',
'profile_template_vars_heading' => 'Template variables:',
'profile_scripts_order' => 'Execution order:',
'profile_scripts_per_mod' => 'repeated for each mod',
'profile_script_example_toggle' => 'Show DayZ-style example',
'profile_col_app_ids' => 'App IDs',
'profile_col_login' => 'Login',
'profile_col_steam' => 'Steam',
'profile_col_workshop' => 'Workshop',
'profile_badge_login_required' => 'Login req.',
'profile_col_game' => 'Game',
'profile_col_key' => 'Game Key',
'profile_col_method' => 'Install Method',
'profile_col_restart' => 'Restart?',
'profile_col_status' => 'Status',
'error_folder_template_required' => 'Custom folder name template is required when format is set to custom.',
// Server-level settings (user/server page)
'heading_server_settings' => 'Workshop Settings for this server',
'label_workshop_enabled' => 'Enable Workshop for this server',
'label_select_profile' => 'Workshop game profile',
'label_auto_detect' => 'Auto-detect from game type',
'label_update_mode' => 'Update mode',
'label_restart_behavior' => 'Restart behavior',
'update_mode_manual' => 'Manual only',
'update_mode_scheduled' => 'Scheduled',
'update_mode_on_restart' => 'Before server restart',
'restart_behavior_none' => 'No restart',
'restart_behavior_queue' => 'Queue restart',
'restart_behavior_stop' => 'Stop / Update / Start',
'btn_queue_update' => 'Queue manual update',
'label_last_update_status' => 'Last update status',
'label_last_update_time' => 'Last update time',
'label_last_success_time' => 'Last successful update',
'label_last_update_error' => 'Last error',
'update_queued_notice' => 'A manual update is queued and will run on the next scheduler cycle.',
'settings_saved' => 'Workshop settings saved.',
'update_queued' => 'Manual update queued. It will run on the next scheduler cycle.',
'col_mod_folder' => 'Install folder',
'label_admin_notes' => 'Admin notes:',
];

View file

@ -1,16 +0,0 @@
<?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

@ -1,17 +0,0 @@
<?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

@ -1,14 +0,0 @@
<?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

@ -1,17 +0,0 @@
<?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

@ -1,13 +0,0 @@
<?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

@ -1,37 +0,0 @@
<?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>

File diff suppressed because it is too large Load diff

View file

@ -1,594 +0,0 @@
<?php
declare(strict_types=1);
/*
* OGP / GSP Steam Workshop
* WorkshopInstaller: handles mod download (via agent SteamCMD) and
* copy/sync from agent cache to server install path.
*
* Template variables supported in all paths/scripts (%var% style):
* %home_id% numeric home id
* %server_path% game server home_path
* %steam_app_id% Steam game App ID (e.g. 221100 for DayZ)
* %workshop_app_id% Workshop App ID used for +workshop_download_item
* %workshop_id% Workshop mod item id (numeric)
* %mod_name% mod title sanitised for use as a folder name
* %install_name% resolved mod folder name (from folder_naming_format)
* %download_path% alias for %source_path% (SteamCMD cache dir for this mod)
* %source_path% SteamCMD cache directory for this mod
* %target_path% resolved install directory for this mod
* %keys_source_path% key source path (resolved from profile key_source_path)
* %keys_target_path% key destination path (resolved from profile key_dest_path)
* %steamcmd_path% path to steamcmd.sh on the agent
*
* Legacy {var} style placeholders are also resolved for backward compat.
*/
require_once __DIR__ . '/WorkshopRepository.php';
class WorkshopInstaller
{
private WorkshopRepository $repo;
private string $logDir;
public function __construct(WorkshopRepository $repo)
{
$this->repo = $repo;
$this->logDir = __DIR__ . '/../logs';
if (!is_dir($this->logDir)) {
mkdir($this->logDir, 0775, true);
}
}
// ------------------------------------------------------------------
// Public API
// ------------------------------------------------------------------
/**
* Install a workshop mod for a game server.
*
* @param array $home Row from getGameHome/getUserGameHome
* @param array $profile Row from gsp_workshop_game_profiles
* @param string $workshopId Numeric workshop item id
* @return array{success:bool, message:string, restart_required:bool, log:list<string>}
*/
public function install(
array $home,
array $profile,
string $workshopId
): array {
$log = [];
$workshopId = preg_replace('/[^0-9]/', '', $workshopId) ?? '';
if ($workshopId === '') {
return $this->fail('Workshop ID must be numeric.', $log);
}
$homeId = (int)($home['home_id'] ?? 0);
$agentId = (int)($home['remote_server_id'] ?? 0);
$appId = (string)($profile['workshop_app_id'] ?? '');
$osType = $this->detectOsType($home);
if ($homeId <= 0 || $agentId <= 0 || $appId === '') {
return $this->fail('Invalid home, agent, or app ID.', $log);
}
$remote = $this->buildRemote($home);
if ($remote === null) {
return $this->fail('Unable to connect to agent.', $log);
}
if ($remote->status_chk() !== 1) {
return $this->fail('Agent is offline.', $log);
}
// Build template vars (source/target paths filled after resolution below)
$vars = $this->buildTemplateVars($home, $profile, $workshopId);
// Run pre-update script once (before mods)
$preScript = trim((string)($profile['pre_update_script'] ?? ''));
if ($preScript !== '') {
$log[] = 'Running pre-update script.';
$this->runScript($remote, $preScript, $vars, $log);
}
// Download
$cacheResult = $this->ensureCached($remote, $agentId, $osType, $appId, $workshopId, $profile, $vars, $log);
if (!$cacheResult) {
return $this->fail('SteamCMD download failed.', $log);
}
// Copy/sync to server
$syncResult = $this->syncToServer($remote, $profile, $vars, $log);
if (!$syncResult) {
return $this->fail('Sync from cache to server failed. Check agent logs.', $log);
}
// Per-mod install script
$installScript = trim((string)($profile['install_script'] ?? ''));
if ($installScript !== '') {
$log[] = 'Running per-mod install script.';
$this->runScript($remote, $installScript, $vars, $log);
}
// Copy keys if configured
if (!empty($profile['copy_keys'])) {
$this->copyKeys($remote, $profile, $vars, $log);
}
// Post-update script
$postScript = trim((string)($profile['post_update_script'] ?? ''));
if ($postScript !== '') {
$log[] = 'Running post-update script.';
$this->runScript($remote, $postScript, $vars, $log);
}
// Record in database
$this->repo->insertOrUpdateMod(
$homeId, $agentId, (int)$profile['id'], $appId, $workshopId,
$vars['%target_path%'] ?? '', '', 0
);
$restartRequired = !empty($profile['requires_restart']);
$log[] = $restartRequired ? 'Restart required after mod install.' : 'Hot-reload capable (no restart required).';
return [
'success' => true,
'message' => 'Mod installed successfully.',
'restart_required' => $restartRequired,
'log' => $log,
];
}
/**
* Sync a single installed mod's cache into the server path.
* Called from pre-start and from the user "Sync now" button.
*
* @param array $home Game home row
* @param array $modRow Row from gsp_server_workshop_mods
* @param array $profile Row from gsp_workshop_game_profiles
* @return array{success:bool, changed:bool, message:string, log:list<string>}
*/
public function syncMod(array $home, array $modRow, array $profile): array
{
$log = [];
$workshopId = (string)($modRow['workshop_id'] ?? '');
$agentId = (int)($modRow['agent_id'] ?? 0);
$appId = (string)($modRow['workshop_app_id'] ?? '');
$cacheEntry = $this->repo->getCacheEntry($agentId, $appId, $workshopId);
if ($cacheEntry === null || ($cacheEntry['status'] ?? '') !== 'cached') {
return ['success' => false, 'changed' => false, 'message' => 'Mod not cached yet.', 'log' => $log];
}
$remote = $this->buildRemote($home);
if ($remote === null || $remote->status_chk() !== 1) {
return ['success' => false, 'changed' => false, 'message' => 'Agent offline.', 'log' => $log];
}
$vars = $this->buildTemplateVars($home, $profile, $workshopId, $modRow['title'] ?? '');
$changed = $this->checkNeedsSync($remote, $vars['%source_path%'], $vars['%target_path%'], $profile, $log);
if (!$changed) {
$log[] = 'No changes detected skipping sync.';
return ['success' => true, 'changed' => false, 'message' => 'Already up to date.', 'log' => $log];
}
$log[] = 'Changes detected syncing.';
$ok = $this->syncToServer($remote, $profile, $vars, $log);
if ($ok) {
$installScript = trim((string)($profile['install_script'] ?? ''));
if ($installScript !== '') {
$this->runScript($remote, $installScript, $vars, $log);
}
if (!empty($profile['copy_keys'])) {
$this->copyKeys($remote, $profile, $vars, $log);
}
}
return [
'success' => $ok,
'changed' => true,
'message' => $ok ? 'Sync complete.' : 'Sync failed.',
'log' => $log,
];
}
// ------------------------------------------------------------------
// Template resolution (public used by WorkshopUpdater)
// ------------------------------------------------------------------
/**
* Replace template placeholders in a string.
* Supports both %var% (canonical) and {var} (legacy) style.
*
* @param array<string,string> $vars
*/
public function resolveTemplate(string $template, array $vars): string
{
// %var% style (canonical)
$result = str_replace(array_keys($vars), array_values($vars), $template);
// Legacy {var} style aliases map old keys to same values
$legacy = [];
foreach ($vars as $k => $v) {
$legacyKey = '{' . trim($k, '%') . '}';
$legacy[$legacyKey] = $v;
}
// Extra legacy aliases
$legacy['{mod_id}'] = $vars['%workshop_id%'] ?? '';
$legacy['{mod_title}'] = $vars['%mod_name%'] ?? '';
$legacy['{mod_folder}'] = $vars['%install_name%'] ?? '';
$legacy['{install_path}'] = $vars['%target_path%'] ?? '';
$legacy['{cache_path}'] = $vars['%source_path%'] ?? '';
return str_replace(array_keys($legacy), array_values($legacy), $result);
}
/**
* Build the standard template variable map for a home + profile + mod.
*
* @return array<string,string>
*/
public function buildTemplateVars(
array $home,
array $profile,
string $workshopId,
string $modTitle = ''
): array {
$serverPath = rtrim((string)($home['home_path'] ?? ''), '/');
$steamcmdPath = trim((string)($profile['steamcmd_path'] ?? ''));
if ($steamcmdPath === '') {
$steamcmdPath = '/home/gameserver/steamcmd/steamcmd.sh';
}
$safeName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $modTitle) ?? '';
// Resolve folder name from format
$folderFormat = (string)($profile['folder_naming_format'] ?? '@%workshop_id%');
if ($folderFormat === '@%mod_name%') {
$installName = '@' . $safeName;
} elseif ($folderFormat === '@%workshop_id%') {
$installName = '@' . $workshopId;
} else {
// custom use folder_name_template as-is, resolve %workshop_id%/%mod_name% inline
$tpl = (string)($profile['folder_name_template'] ?? '@%workshop_id%');
$installName = str_replace(['%workshop_id%', '%mod_name%'], [$workshopId, $safeName], $tpl);
}
$steamAppId = (string)($profile['steam_app_id'] ?? '');
$workshopAppId = (string)($profile['workshop_app_id'] ?? '');
// Resolve cache/source path template
$cachePathTpl = (string)($profile['cache_path_template'] ?? '');
$sourcePath = str_replace(
['%workshop_app_id%', '%workshop_id%', '%mod_name%', '%install_name%', '%steam_app_id%', '%steamcmd_path%'],
[$workshopAppId, $workshopId, $safeName, $installName, $steamAppId, dirname($steamcmdPath)],
$cachePathTpl
);
// Resolve target/install path template
$installPathTpl = (string)($profile['install_path_template'] ?? '');
$targetPath = str_replace(
['%server_path%', '%workshop_app_id%', '%workshop_id%', '%mod_name%', '%install_name%', '%steam_app_id%'],
[$serverPath, $workshopAppId, $workshopId, $safeName, $installName, $steamAppId],
$installPathTpl
);
// Resolve key paths
$keySourceRaw = (string)($profile['key_source_path'] ?? '');
$keyDestRaw = (string)($profile['key_dest_path'] ?? '');
$keySource = str_replace(['%source_path%', '%server_path%'], [$sourcePath, $serverPath], $keySourceRaw);
$keyDest = str_replace(['%target_path%', '%server_path%'], [$targetPath, $serverPath], $keyDestRaw);
return [
'%home_id%' => (string)($home['home_id'] ?? ''),
'%server_path%' => $serverPath,
'%steam_app_id%' => $steamAppId,
'%workshop_app_id%' => $workshopAppId,
'%workshop_id%' => $workshopId,
'%mod_name%' => $safeName,
'%install_name%' => $installName,
'%download_path%' => $sourcePath,
'%source_path%' => $sourcePath,
'%target_path%' => $targetPath,
'%keys_source_path%' => $keySource,
'%keys_target_path%' => $keyDest,
'%steamcmd_path%' => $steamcmdPath,
];
}
// ------------------------------------------------------------------
// Private helpers
// ------------------------------------------------------------------
/**
* Ensure a mod is downloaded/cached on the agent.
* Returns true if cached and available.
*
* @param list<string> $log
*/
private function ensureCached(
object $remote,
int $agentId,
string $osType,
string $appId,
string $workshopId,
array $profile,
array &$vars,
array &$log
): bool {
$sourcePath = $vars['%source_path%'];
$cacheEntry = $this->repo->getCacheEntry($agentId, $appId, $workshopId);
$log[] = "Cache check: agent={$agentId} app={$appId} mod={$workshopId}";
if ($cacheEntry !== null && ($cacheEntry['status'] ?? '') === 'cached') {
$log[] = 'Cache HIT using existing cached copy.';
return true;
}
$log[] = 'Cache MISS triggering SteamCMD download on agent.';
$ok = $this->triggerSteamCmdDownload($remote, $agentId, $appId, $workshopId, $profile, $sourcePath, $log);
$status = $ok ? 'cached' : 'missing';
$this->repo->upsertCacheEntry($agentId, $osType, $appId, $workshopId, $sourcePath, $status);
if ($ok) {
$log[] = 'SteamCMD download success.';
}
return $ok;
}
/** Build an OGPRemoteLibrary instance from a home row. */
private function buildRemote(array $home): ?object
{
if (!class_exists('OGPRemoteLibrary')) {
@require_once __DIR__ . '/../../../includes/lib_remote.php';
}
if (!class_exists('OGPRemoteLibrary')) {
return null;
}
$ip = (string)($home['agent_ip'] ?? '');
$port = (string)($home['agent_port'] ?? '');
$key = (string)($home['encryption_key'] ?? '');
$timeout = isset($home['timeout']) ? (int)$home['timeout'] : 30;
if ($ip === '' || $port === '') {
return null;
}
return new OGPRemoteLibrary($ip, $port, $key, $timeout);
}
/**
* Trigger a SteamCMD workshop_download_item on the agent.
* Returns true on success.
*
* @param list<string> $log
*/
private function triggerSteamCmdDownload(
object $remote,
int $agentId,
string $appId,
string $workshopId,
array $profile,
string $cachePath,
array &$log
): bool {
$steamcmdPath = trim((string)($profile['steamcmd_path'] ?? ''));
if ($steamcmdPath === '') {
$steamcmdPath = '/home/gameserver/steamcmd/steamcmd.sh';
}
$loginMode = (string)($profile['steamcmd_login_mode'] ?? 'anonymous');
// TODO: When login_mode is 'account', replace 'anonymous' with the
// configured SteamCMD credentials (username + password) loaded from
// a secure panel-side credential store. Until that feature is
// implemented, 'account' mode logs in anonymously (which works for
// free/publicly-accessible Workshop items).
$loginArg = 'anonymous';
$cmd = implode(' ', [
escapeshellarg($steamcmdPath),
'+login', escapeshellarg($loginArg),
'+workshop_download_item', escapeshellarg($appId), escapeshellarg($workshopId),
'validate',
'+quit',
]);
$log[] = "SteamCMD start: agent={$agentId} app={$appId} mod={$workshopId}";
$this->writeLog("STEAMCMD START agent={$agentId} app={$appId} mod={$workshopId}");
$output = $remote->exec($cmd);
if ($output === null) {
$log[] = 'SteamCMD: no response from agent (command may still be running).';
} else {
$log[] = 'SteamCMD output: ' . substr((string)$output, 0, 500);
}
// Verify by checking whether the cache path now exists
$exists = $remote->rfile_exists($cachePath);
if ($exists === 1) {
$this->writeLog("STEAMCMD SUCCESS agent={$agentId} app={$appId} mod={$workshopId} path={$cachePath}");
return true;
}
$this->writeLog("STEAMCMD FAILURE agent={$agentId} app={$appId} mod={$workshopId} path={$cachePath}");
return false;
}
/**
* Check if cache path differs from install path (dry-run compare).
* Returns true if sync is needed.
*
* @param list<string> $log
*/
private function checkNeedsSync(
object $remote,
string $sourcePath,
string $targetPath,
array $profile,
array &$log
): bool {
$copyMethod = (string)($profile['copy_method'] ?? 'rsync');
$log[] = "Pre-start compare: source={$sourcePath} target={$targetPath} method={$copyMethod}";
if ($copyMethod === 'rsync') {
$cmd = sprintf(
'rsync -rcn --delete %s %s 2>/dev/null; echo "RSYNC_EXIT:$?"',
escapeshellarg(rtrim($sourcePath, '/') . '/'),
escapeshellarg(rtrim($targetPath, '/') . '/')
);
$out = (string)$remote->exec($cmd);
$body = preg_replace('/RSYNC_EXIT:\d+\s*$/', '', $out) ?? '';
return preg_match('/\S/', $body) === 1;
}
// copy / symlink: always sync
return true;
}
/**
* Perform the actual copy/sync from cache to install path on the agent.
*
* @param array<string,string> $vars
* @param list<string> $log
*/
private function syncToServer(
object $remote,
array $profile,
array &$vars,
array &$log
): bool {
$copyMethod = (string)($profile['copy_method'] ?? 'rsync');
$sourcePath = $vars['%source_path%'];
$targetPath = $vars['%target_path%'];
if ($sourcePath === '' || $targetPath === '') {
$log[] = 'Sync skipped: empty source or target path.';
return false;
}
$log[] = "Sync start: method={$copyMethod} source={$sourcePath} target={$targetPath}";
$this->writeLog("COPY START method={$copyMethod} source={$sourcePath} target={$targetPath}");
if ($copyMethod === 'rsync') {
$cmd = sprintf(
'mkdir -p %s && rsync -a --delete %s %s 2>&1; echo "EXIT:$?"',
escapeshellarg($targetPath),
escapeshellarg(rtrim($sourcePath, '/') . '/'),
escapeshellarg(rtrim($targetPath, '/') . '/')
);
} elseif ($copyMethod === 'symlink') {
$cmd = sprintf(
'mkdir -p %s && ln -sfn %s %s 2>&1; echo "EXIT:$?"',
escapeshellarg(dirname($targetPath)),
escapeshellarg($sourcePath),
escapeshellarg($targetPath)
);
} else {
// 'copy' basic cp
$cmd = sprintf(
'mkdir -p %s && cp -r %s %s 2>&1; echo "EXIT:$?"',
escapeshellarg($targetPath),
escapeshellarg(rtrim($sourcePath, '/') . '/.'),
escapeshellarg($targetPath)
);
}
$out = (string)$remote->exec($cmd);
$log[] = 'Sync output: ' . substr($out, 0, 500);
if (preg_match('/EXIT:(\d+)/', $out, $m)) {
$ok = (int)$m[1] === 0;
} else {
$ok = true;
}
if ($ok) {
$log[] = 'Sync success.';
$this->writeLog("COPY SUCCESS source={$sourcePath} target={$targetPath}");
} else {
$log[] = 'Sync failed (non-zero exit).';
$this->writeLog("COPY FAILURE source={$sourcePath} target={$targetPath}");
}
return $ok;
}
/**
* Copy key files from the mod's keys directory to the server keys directory.
*
* @param array<string,string> $vars
* @param list<string> $log
*/
private function copyKeys(
object $remote,
array $profile,
array $vars,
array &$log
): void {
$keySrc = $vars['%keys_source_path%'];
$keyDest = $vars['%keys_target_path%'];
if ($keySrc === '' || $keyDest === '') {
$log[] = 'Key copy skipped: key paths not configured.';
return;
}
$log[] = "Copying keys: {$keySrc}{$keyDest}";
$cmd = sprintf(
'if [ -d %s ]; then mkdir -p %s && cp -f %s/*.bikey %s/ 2>/dev/null; fi; echo "EXIT:$?"',
escapeshellarg($keySrc),
escapeshellarg($keyDest),
escapeshellarg($keySrc),
escapeshellarg($keyDest)
);
$out = (string)$remote->exec($cmd);
$log[] = 'Key copy output: ' . substr($out, 0, 200);
}
/**
* Run an admin-defined bash script on the agent after resolving template vars.
*
* @param array<string,string> $vars
* @param list<string> $log
*/
private function runScript(
object $remote,
string $script,
array $vars,
array &$log
): void {
$resolved = $this->resolveTemplate($script, $vars);
$out = (string)$remote->exec($resolved . ' 2>&1');
$log[] = 'Script output: ' . substr($out, 0, 500);
$this->writeLog('SCRIPT OUTPUT: ' . substr($out, 0, 1000));
}
private function detectOsType(array $home): string
{
$gameKey = strtolower((string)($home['game_key'] ?? ''));
return preg_match('/win/', $gameKey) ? 'windows' : 'linux';
}
private function writeLog(string $message): void
{
$file = $this->logDir . '/workshop_install.log';
$line = '[' . date('Y-m-d H:i:s') . '] ' . $message . "\n";
@file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
}
private function fail(string $message, array $log): array
{
$this->writeLog('FAIL: ' . $message);
return [
'success' => false,
'message' => $message,
'restart_required' => false,
'log' => $log,
];
}
}

View file

@ -1,118 +0,0 @@
<?php
declare(strict_types=1);
/*
* OGP / GSP Steam Workshop
* WorkshopPreStart: syncs updated cached mods into the game server folder
* before the server is launched.
*
* Intended to be called from the game XML <pre_start> tag or from a
* pre-start hook in the panel.
*
* Design rules:
* - Does NOT restart running servers.
* - Only syncs if the cache differs from the installed path.
* - Logs every check and sync attempt.
*/
require_once __DIR__ . '/WorkshopRepository.php';
require_once __DIR__ . '/WorkshopInstaller.php';
class WorkshopPreStart
{
private WorkshopRepository $repo;
private WorkshopInstaller $installer;
private string $logFile;
public function __construct(WorkshopRepository $repo, WorkshopInstaller $installer)
{
$this->repo = $repo;
$this->installer = $installer;
$logDir = __DIR__ . '/../logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0775, true);
}
$this->logFile = $logDir . '/workshop_prestart.log';
}
// ------------------------------------------------------------------
// Public API
// ------------------------------------------------------------------
/**
* Sync all enabled mods for the given home_id before server start.
*
* @param array $home Full game home row (from getGameHome / getUserGameHome)
* @return array{synced:int, skipped:int, failed:int, log:list<string>}
*/
public function syncModsForHome(array $home): array
{
$homeId = (int)($home['home_id'] ?? 0);
$log = [];
$synced = 0;
$skipped = 0;
$failed = 0;
$this->log("PRE-START home={$homeId}");
$mods = $this->repo->listEnabledModsForHome($homeId);
if (empty($mods)) {
$log[] = 'No enabled Workshop mods for this server.';
$this->log("PRE-START home={$homeId}: no mods");
return ['synced' => 0, 'skipped' => 0, 'failed' => 0, 'log' => $log];
}
foreach ((array)$mods as $modRow) {
$workshopId = (string)($modRow['workshop_id'] ?? '');
$profileId = (int)($modRow['profile_id'] ?? 0);
$log[] = "Checking mod {$workshopId}";
$profile = $profileId > 0 ? $this->repo->getProfileById($profileId) : null;
if ($profile === null) {
$log[] = " Profile not found (profile_id={$profileId}) skipped.";
$this->log("PRE-START home={$homeId} mod={$workshopId}: profile missing");
$skipped++;
continue;
}
$result = $this->installer->syncMod($home, $modRow, $profile);
if ($result['success'] && $result['changed']) {
$log[] = " Synced: " . ($result['message'] ?? '');
$this->log("PRE-START home={$homeId} mod={$workshopId}: synced");
$synced++;
} elseif ($result['success'] && !$result['changed']) {
$log[] = ' Already up to date no sync needed.';
$skipped++;
} else {
$log[] = " Sync failed: " . ($result['message'] ?? 'unknown error');
$this->log("PRE-START home={$homeId} mod={$workshopId}: FAILED");
$failed++;
}
// Append sub-log
foreach ((array)($result['log'] ?? []) as $line) {
$log[] = ' ' . $line;
}
}
$this->log("PRE-START home={$homeId} done: synced={$synced} skipped={$skipped} failed={$failed}");
return [
'synced' => $synced,
'skipped' => $skipped,
'failed' => $failed,
'log' => $log,
];
}
// ------------------------------------------------------------------
// Private helpers
// ------------------------------------------------------------------
private function log(string $message): void
{
$line = '[' . date('Y-m-d H:i:s') . '] ' . $message . "\n";
@file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}

View file

@ -1,589 +0,0 @@
<?php
declare(strict_types=1);
/*
* OGP / GSP Steam Workshop
* WorkshopRepository: database access layer for the three Workshop tables.
*/
class WorkshopRepository
{
private OGPDatabase $db;
private string $prefix;
public function __construct(OGPDatabase $db)
{
$this->db = $db;
$this->prefix = $db->getTablePrefix();
}
// ------------------------------------------------------------------
// Internal helpers
// ------------------------------------------------------------------
private function esc(mixed $val): string
{
return $this->db->realEscapeSingle((string)$val);
}
/** Execute a query that returns no result set (INSERT / UPDATE / DELETE). */
private function exec(string $sql): bool
{
return $this->db->query($sql) !== false;
}
/** Execute a SELECT query; returns array of rows or empty array. */
private function select(string $sql): array
{
$result = $this->db->resultQuery($sql);
return is_array($result) ? $result : [];
}
/** Return the first row or null. */
private function selectOne(string $sql): ?array
{
$rows = $this->select($sql);
return $rows[0] ?? null;
}
private function lastInsertId(): int
{
$row = $this->selectOne('SELECT LAST_INSERT_ID() AS id');
return isset($row['id']) ? (int)$row['id'] : 0;
}
// ------------------------------------------------------------------
// WORKSHOP GAME PROFILES
// ------------------------------------------------------------------
/** @return array<int,array<string,mixed>> */
public function listProfiles(bool $enabledOnly = false): array
{
$where = $enabledOnly ? ' WHERE enabled = 1' : '';
return $this->select(
"SELECT * FROM `{$this->prefix}workshop_game_profiles`{$where} ORDER BY game_name ASC"
);
}
public function getProfileById(int $id): ?array
{
return $this->selectOne(
"SELECT * FROM `{$this->prefix}workshop_game_profiles` WHERE id = {$id} LIMIT 1"
);
}
public function getProfileByGameKey(string $gameKey): ?array
{
return $this->selectOne(
"SELECT * FROM `{$this->prefix}workshop_game_profiles`
WHERE game_key = '" . $this->esc($gameKey) . "' AND enabled = 1 LIMIT 1"
);
}
public function getProfileByAppId(string $appId): ?array
{
return $this->selectOne(
"SELECT * FROM `{$this->prefix}workshop_game_profiles`
WHERE workshop_app_id = '" . $this->esc($appId) . "' AND enabled = 1 LIMIT 1"
);
}
/**
* Insert (id = 0) or update (id > 0) a Workshop game profile.
* Returns the row id.
*/
public function saveProfile(array $data): int
{
$id = isset($data['id']) ? (int)$data['id'] : 0;
$gameKey = $this->esc($data['game_key'] ?? '');
$gameName = $this->esc($data['game_name'] ?? '');
$steamAppId = $this->esc($data['steam_app_id'] ?? '');
$workshopAppId = $this->esc($data['workshop_app_id'] ?? '');
$steamLoginRequired = empty($data['steam_login_required']) ? 0 : 1;
$steamcmdLoginMode = in_array($data['steamcmd_login_mode'] ?? '', ['anonymous', 'account'], true)
? $this->esc($data['steamcmd_login_mode'])
: 'anonymous';
$steamcmdPath = $this->esc($data['steamcmd_path'] ?? '');
$supportedOs = $this->esc($data['supported_os'] ?? 'linux');
$cachePathTpl = $this->esc($data['cache_path_template'] ?? '');
$installPathTpl = $this->esc($data['install_path_template'] ?? '');
$folderNamingFormat = in_array($data['folder_naming_format'] ?? '', ['@%mod_name%', '@%workshop_id%', 'custom'], true)
? $this->esc($data['folder_naming_format'])
: '@%workshop_id%';
$folderNameTpl = $this->esc($data['folder_name_template'] ?? '@%workshop_id%');
$modLaunchParam = $this->esc($data['mod_launch_param'] ?? '');
$modSeparator = in_array($data['mod_separator'] ?? '', ['semicolon', 'comma', 'space'], true)
? $this->esc($data['mod_separator'])
: 'semicolon';
$copyMethod = in_array($data['copy_method'] ?? '', ['copy', 'rsync', 'symlink'], true)
? $this->esc($data['copy_method'])
: 'rsync';
$copyKeys = empty($data['copy_keys']) ? 0 : 1;
$keySourcePath = $this->nullOrStr($data['key_source_path'] ?? '');
$keyDestPath = $this->nullOrStr($data['key_dest_path'] ?? '');
$preUpdateScript = $this->nullOrStr($data['pre_update_script'] ?? '');
$installScript = $this->nullOrStr($data['install_script'] ?? '');
$postUpdateScript = $this->nullOrStr($data['post_update_script'] ?? '');
$configFileTpl = $this->nullOrStr($data['config_file_template'] ?? '');
$launchParamTpl = $this->nullOrStr($data['launch_param_template'] ?? '');
$requiresRestart = empty($data['requires_restart']) ? 0 : 1;
$validationNotes = $this->nullOrStr($data['validation_notes'] ?? '');
$enabled = isset($data['enabled']) && !$data['enabled'] ? 0 : 1;
if ($id > 0) {
$this->exec(
"UPDATE `{$this->prefix}workshop_game_profiles` SET
game_key = '{$gameKey}',
game_name = '{$gameName}',
steam_app_id = '{$steamAppId}',
workshop_app_id = '{$workshopAppId}',
steam_login_required = {$steamLoginRequired},
steamcmd_login_mode = '{$steamcmdLoginMode}',
steamcmd_path = '{$steamcmdPath}',
supported_os = '{$supportedOs}',
cache_path_template = '{$cachePathTpl}',
install_path_template = '{$installPathTpl}',
folder_naming_format = '{$folderNamingFormat}',
folder_name_template = '{$folderNameTpl}',
mod_launch_param = '{$modLaunchParam}',
mod_separator = '{$modSeparator}',
copy_method = '{$copyMethod}',
copy_keys = {$copyKeys},
key_source_path = {$keySourcePath},
key_dest_path = {$keyDestPath},
pre_update_script = {$preUpdateScript},
install_script = {$installScript},
post_update_script = {$postUpdateScript},
config_file_template = {$configFileTpl},
launch_param_template = {$launchParamTpl},
requires_restart = {$requiresRestart},
validation_notes = {$validationNotes},
enabled = {$enabled},
updated_at = NOW()
WHERE id = {$id}"
);
return $id;
}
$this->exec(
"INSERT INTO `{$this->prefix}workshop_game_profiles`
(game_key, game_name, steam_app_id, workshop_app_id, steam_login_required,
steamcmd_login_mode, steamcmd_path, supported_os, cache_path_template,
install_path_template, folder_naming_format, folder_name_template,
mod_launch_param, mod_separator, copy_method, copy_keys,
key_source_path, key_dest_path, pre_update_script, install_script,
post_update_script, config_file_template, launch_param_template,
requires_restart, validation_notes, enabled, created_at)
VALUES
('{$gameKey}', '{$gameName}', '{$steamAppId}', '{$workshopAppId}', {$steamLoginRequired},
'{$steamcmdLoginMode}', '{$steamcmdPath}', '{$supportedOs}', '{$cachePathTpl}',
'{$installPathTpl}', '{$folderNamingFormat}', '{$folderNameTpl}',
'{$modLaunchParam}', '{$modSeparator}', '{$copyMethod}', {$copyKeys},
{$keySourcePath}, {$keyDestPath}, {$preUpdateScript}, {$installScript},
{$postUpdateScript}, {$configFileTpl}, {$launchParamTpl},
{$requiresRestart}, {$validationNotes}, {$enabled}, NOW())"
);
return $this->lastInsertId();
}
/** Return NULL or an escaped quoted string, for optional TEXT columns. */
private function nullOrStr(string $value): string
{
return $value !== '' ? "'" . $this->esc($value) . "'" : 'NULL';
}
public function deleteProfile(int $id): bool
{
return $this->exec(
"DELETE FROM `{$this->prefix}workshop_game_profiles` WHERE id = {$id}"
);
}
// ------------------------------------------------------------------
// WORKSHOP CACHE
// ------------------------------------------------------------------
public function getCacheEntry(int $agentId, string $appId, string $workshopId): ?array
{
return $this->selectOne(
"SELECT * FROM `{$this->prefix}workshop_cache`
WHERE agent_id = {$agentId}
AND workshop_app_id = '" . $this->esc($appId) . "'
AND workshop_id = '" . $this->esc($workshopId) . "'
LIMIT 1"
);
}
/**
* Insert or update a cache row.
* $status: 'missing' | 'cached' | 'failed'
*/
public function upsertCacheEntry(
int $agentId,
string $osType,
string $appId,
string $workshopId,
string $cachePath,
string $status,
?string $title = null,
?string $error = null
): void {
$osType = $this->esc($osType);
$appId = $this->esc($appId);
$workshopId = $this->esc($workshopId);
$cachePath = $this->esc($cachePath);
$status = $this->esc($status);
$titleSql = $title !== null ? "'" . $this->esc($title) . "'" : 'NULL';
$errorSql = $error !== null ? "'" . $this->esc($error) . "'" : 'NULL';
$updatedSql = ($status === 'cached') ? 'NOW()' : 'NULL';
$this->exec(
"INSERT INTO `{$this->prefix}workshop_cache`
(agent_id, os_type, workshop_app_id, workshop_id, title, cache_path, status, last_checked, last_updated, last_error)
VALUES
({$agentId}, '{$osType}', '{$appId}', '{$workshopId}', {$titleSql}, '{$cachePath}', '{$status}', NOW(), {$updatedSql}, {$errorSql})
ON DUPLICATE KEY UPDATE
os_type = '{$osType}',
cache_path = '{$cachePath}',
status = '{$status}',
title = {$titleSql},
last_checked = NOW(),
last_updated = {$updatedSql},
last_error = {$errorSql}"
);
}
/** Return all cached entries for a specific agent+appId (for the "available mods" picker). */
public function listCacheForAgent(int $agentId, string $appId): array
{
return $this->select(
"SELECT * FROM `{$this->prefix}workshop_cache`
WHERE agent_id = {$agentId}
AND workshop_app_id = '" . $this->esc($appId) . "'
ORDER BY COALESCE(title, workshop_id) ASC"
);
}
/** Return all cache rows that should be refreshed (enabled mods installed somewhere). */
public function listCacheEntriesForAgent(int $agentId): array
{
return $this->select(
"SELECT DISTINCT c.*
FROM `{$this->prefix}workshop_cache` c
JOIN `{$this->prefix}server_workshop_mods` m
ON m.agent_id = c.agent_id
AND m.workshop_app_id = c.workshop_app_id
AND m.workshop_id = c.workshop_id
WHERE c.agent_id = {$agentId} AND m.enabled = 1"
);
}
// ------------------------------------------------------------------
// SERVER WORKSHOP MODS
// ------------------------------------------------------------------
public function getServerMod(int $homeId, string $workshopId): ?array
{
return $this->selectOne(
"SELECT * FROM `{$this->prefix}server_workshop_mods`
WHERE home_id = {$homeId}
AND workshop_id = '" . $this->esc($workshopId) . "'
LIMIT 1"
);
}
/** @return array<int,array<string,mixed>> */
public function listModsForHome(int $homeId): array
{
return $this->select(
"SELECT m.*, p.game_name, p.game_key, p.requires_restart, p.copy_method
FROM `{$this->prefix}server_workshop_mods` m
LEFT JOIN `{$this->prefix}workshop_game_profiles` p ON m.profile_id = p.id
WHERE m.home_id = {$homeId}
ORDER BY m.load_order ASC, m.installed_at ASC"
);
}
/** @return array<int,array<string,mixed>> */
public function listEnabledModsForHome(int $homeId): array
{
return $this->select(
"SELECT * FROM `{$this->prefix}server_workshop_mods`
WHERE home_id = {$homeId} AND enabled = 1
ORDER BY load_order ASC"
);
}
/**
* Insert (id = 0) or update (id > 0) a Workshop mod entry for a game home.
* Returns the row id.
*/
public function insertOrUpdateMod(
int $homeId,
int $agentId,
int $profileId,
string $appId,
string $workshopId,
string $installPath,
string $title = '',
int $loadOrder = 0,
string $customFolder = ''
): int {
$appId = $this->esc($appId);
$workshopId = $this->esc($workshopId);
$installPath = $this->esc($installPath);
$title = $this->esc($title);
$customFolder = $this->esc($customFolder);
$existing = $this->getServerMod($homeId, $workshopId);
if ($existing !== null) {
$this->exec(
"UPDATE `{$this->prefix}server_workshop_mods` SET
agent_id = {$agentId},
profile_id = {$profileId},
workshop_app_id = '{$appId}',
title = '{$title}',
custom_folder = '{$customFolder}',
install_path = '{$installPath}',
load_order = {$loadOrder},
enabled = 1,
updated_at = NOW()
WHERE home_id = {$homeId} AND workshop_id = '{$workshopId}'"
);
return (int)$existing['id'];
}
$this->exec(
"INSERT INTO `{$this->prefix}server_workshop_mods`
(home_id, agent_id, profile_id, workshop_app_id, workshop_id, title, custom_folder, enabled, install_path, load_order, installed_at)
VALUES
({$homeId}, {$agentId}, {$profileId}, '{$appId}', '{$workshopId}', '{$title}', '{$customFolder}', 1, '{$installPath}', {$loadOrder}, NOW())"
);
return $this->lastInsertId();
}
public function removeMod(int $homeId, string $workshopId): bool
{
return $this->exec(
"DELETE FROM `{$this->prefix}server_workshop_mods`
WHERE home_id = {$homeId} AND workshop_id = '" . $this->esc($workshopId) . "'"
);
}
public function toggleMod(int $homeId, string $workshopId, bool $enabled): bool
{
$val = $enabled ? 1 : 0;
return $this->exec(
"UPDATE `{$this->prefix}server_workshop_mods`
SET enabled = {$val}, updated_at = NOW()
WHERE home_id = {$homeId} AND workshop_id = '" . $this->esc($workshopId) . "'"
);
}
public function updateLoadOrder(int $homeId, string $workshopId, int $order): bool
{
return $this->exec(
"UPDATE `{$this->prefix}server_workshop_mods`
SET load_order = {$order}, updated_at = NOW()
WHERE home_id = {$homeId} AND workshop_id = '" . $this->esc($workshopId) . "'"
);
}
/**
* Return all enabled installed mods joined with their profile data.
* Used by the scheduled updater to know what needs refreshing.
*
* @return array<int,array<string,mixed>>
*/
public function listAllEnabledMods(): array
{
return $this->select(
"SELECT m.*,
p.steam_app_id, p.cache_path_template, p.install_path_template,
p.folder_naming_format, p.folder_name_template,
p.copy_method, p.copy_keys, p.key_source_path, p.key_dest_path,
p.pre_update_script, p.install_script, p.post_update_script,
p.steamcmd_path, p.steamcmd_login_mode,
p.config_file_template, p.launch_param_template,
p.requires_restart
FROM `{$this->prefix}server_workshop_mods` m
JOIN `{$this->prefix}workshop_game_profiles` p ON m.profile_id = p.id
WHERE m.enabled = 1 AND p.enabled = 1
ORDER BY m.agent_id ASC, m.workshop_app_id ASC, m.workshop_id ASC"
);
}
// ------------------------------------------------------------------
// Agent / remote server helpers (for WorkshopUpdater)
// ------------------------------------------------------------------
public function getPrefix(): string
{
return $this->prefix;
}
/**
* Return the agent connection row for a remote_server_id.
* Returns null if not found.
*/
public function getAgentRow(int $agentId): ?array
{
return $this->selectOne(
"SELECT remote_server_id AS agent_id, agent_ip, agent_port, encryption_key, timeout
FROM `{$this->prefix}remote_servers`
WHERE remote_server_id = {$agentId}
LIMIT 1"
);
}
// ------------------------------------------------------------------
// Distinct Workshop ID queries (for WorkshopUpdater)
// ------------------------------------------------------------------
/**
* Return distinct (agent_id, workshop_app_id, workshop_id) triplets for enabled mods.
* Used by the updater to avoid duplicate SteamCMD calls.
*
* @return array<int,array<string,mixed>>
*/
public function listDistinctEnabledWorkshopIds(): array
{
return $this->select(
"SELECT DISTINCT m.agent_id, m.workshop_app_id, m.workshop_id, m.title
FROM `{$this->prefix}server_workshop_mods` m
JOIN `{$this->prefix}workshop_game_profiles` p ON m.profile_id = p.id
WHERE m.enabled = 1 AND p.enabled = 1
ORDER BY m.agent_id ASC, m.workshop_app_id ASC"
);
}
/** Distinct (agent_id, workshop_app_id, workshop_id) for a single agent. */
public function listDistinctEnabledWorkshopIdsForAgent(int $agentId): array
{
return $this->select(
"SELECT DISTINCT m.agent_id, m.workshop_app_id, m.workshop_id, m.title
FROM `{$this->prefix}server_workshop_mods` m
JOIN `{$this->prefix}workshop_game_profiles` p ON m.profile_id = p.id
WHERE m.enabled = 1 AND p.enabled = 1 AND m.agent_id = {$agentId}
ORDER BY m.workshop_app_id ASC"
);
}
/** Distinct Workshop IDs for a specific home. */
public function listDistinctEnabledWorkshopIdsForHome(int $homeId): array
{
return $this->select(
"SELECT DISTINCT m.agent_id, m.workshop_app_id, m.workshop_id, m.title
FROM `{$this->prefix}server_workshop_mods` m
JOIN `{$this->prefix}workshop_game_profiles` p ON m.profile_id = p.id
WHERE m.enabled = 1 AND p.enabled = 1 AND m.home_id = {$homeId}"
);
}
/** Distinct Workshop IDs for a specific profile. */
public function listDistinctEnabledWorkshopIdsForProfile(int $profileId): array
{
return $this->select(
"SELECT DISTINCT m.agent_id, m.workshop_app_id, m.workshop_id, m.title
FROM `{$this->prefix}server_workshop_mods` m
WHERE m.enabled = 1 AND m.profile_id = {$profileId}"
);
}
// ------------------------------------------------------------------
// SERVER WORKSHOP SETTINGS (per-server/home configuration)
// ------------------------------------------------------------------
/**
* Return the workshop settings row for a game home, or null if not set.
*/
public function getServerSettings(int $homeId): ?array
{
return $this->selectOne(
"SELECT * FROM `{$this->prefix}server_workshop_settings`
WHERE home_id = {$homeId} LIMIT 1"
);
}
/**
* Upsert server-level workshop settings.
*/
public function saveServerSettings(int $homeId, array $data): void
{
$workshopEnabled = empty($data['workshop_enabled']) ? 0 : 1;
$profileId = isset($data['profile_id']) && (int)$data['profile_id'] > 0
? (int)$data['profile_id']
: 'NULL';
$updateMode = in_array($data['update_mode'] ?? '', ['manual', 'scheduled', 'on_restart'], true)
? "'" . $this->esc($data['update_mode']) . "'"
: "'manual'";
$restartBehavior = in_array($data['restart_behavior'] ?? '', ['none', 'queue', 'stop_update_start'], true)
? "'" . $this->esc($data['restart_behavior']) . "'"
: "'none'";
$updateQueued = empty($data['update_queued']) ? 0 : 1;
$this->exec(
"INSERT INTO `{$this->prefix}server_workshop_settings`
(home_id, workshop_enabled, profile_id, update_mode, restart_behavior, update_queued, updated_at)
VALUES
({$homeId}, {$workshopEnabled}, {$profileId}, {$updateMode}, {$restartBehavior}, {$updateQueued}, NOW())
ON DUPLICATE KEY UPDATE
workshop_enabled = {$workshopEnabled},
profile_id = {$profileId},
update_mode = {$updateMode},
restart_behavior = {$restartBehavior},
update_queued = {$updateQueued},
updated_at = NOW()"
);
}
/**
* Record the result of an update run for a home.
*/
public function recordUpdateResult(int $homeId, string $status, string $error = ''): void
{
$status = $this->esc($status);
$errorSql = $error !== '' ? "'" . $this->esc($error) . "'" : 'NULL';
$successSql = $status === 'success' ? 'NOW()' : 'last_success_time';
$this->exec(
"INSERT INTO `{$this->prefix}server_workshop_settings`
(home_id, last_update_status, last_update_error, last_update_time, last_success_time)
VALUES
({$homeId}, '{$status}', {$errorSql}, NOW(), " . ($status === 'success' ? 'NOW()' : 'NULL') . ")
ON DUPLICATE KEY UPDATE
last_update_status = '{$status}',
last_update_error = {$errorSql},
last_update_time = NOW(),
last_success_time = {$successSql}"
);
}
/**
* Mark a manual update as queued (or clear the queue flag).
*/
public function setUpdateQueued(int $homeId, bool $queued): void
{
$val = $queued ? 1 : 0;
$this->exec(
"INSERT INTO `{$this->prefix}server_workshop_settings` (home_id, update_queued)
VALUES ({$homeId}, {$val})
ON DUPLICATE KEY UPDATE update_queued = {$val}"
);
}
/**
* Return all home IDs that have a queued manual update.
*
* @return array<int,int>
*/
public function listQueuedUpdateHomes(): array
{
$rows = $this->select(
"SELECT home_id FROM `{$this->prefix}server_workshop_settings`
WHERE update_queued = 1 AND workshop_enabled = 1"
);
return array_column($rows, 'home_id');
}
}

View file

@ -1,314 +0,0 @@
<?php
declare(strict_types=1);
/*
* OGP / GSP Steam Workshop
* WorkshopUpdater: scheduled / background cache update functions.
*
* Design rules:
* - Do NOT copy into running servers during a scheduled update.
* - Do NOT restart servers automatically.
* - Log every attempt.
* - Group SteamCMD calls by (agent_id, workshop_app_id, workshop_id) to
* avoid redundant downloads when multiple servers share a mod.
*/
require_once __DIR__ . '/WorkshopRepository.php';
require_once __DIR__ . '/WorkshopInstaller.php';
class WorkshopUpdater
{
private WorkshopRepository $repo;
private WorkshopInstaller $installer;
private string $logDir;
private string $logFile;
public function __construct(WorkshopRepository $repo, WorkshopInstaller $installer)
{
$this->repo = $repo;
$this->installer = $installer;
$this->logDir = __DIR__ . '/../logs';
$this->logFile = $this->logDir . '/workshop_update.log';
if (!is_dir($this->logDir)) {
mkdir($this->logDir, 0775, true);
}
}
// ------------------------------------------------------------------
// Public API entry points called by cron_update.php
// ------------------------------------------------------------------
/**
* Update Workshop cache for all enabled installed mods across all agents.
*
* @return array<string,mixed>
*/
public function updateAll(): array
{
$this->log('=== updateAll start ===');
$rows = $this->repo->listDistinctEnabledWorkshopIds();
$results = $this->processBatch($rows);
$this->log('=== updateAll end: ' . count($results) . ' items processed ===');
return $results;
}
/**
* Update Workshop cache for all mods installed on a specific agent.
*
* @return array<string,mixed>
*/
public function updateWorkshopCacheForAgent(int $agentId): array
{
$this->log("=== updateWorkshopCacheForAgent agent={$agentId} start ===");
$rows = $this->repo->listDistinctEnabledWorkshopIdsForAgent($agentId);
$results = $this->processBatch($rows);
$this->log("=== updateWorkshopCacheForAgent agent={$agentId} end ===");
return $results;
}
/**
* Update Workshop cache for all mods installed on a specific home.
*
* @return array<string,mixed>
*/
public function updateWorkshopCacheForHome(int $homeId): array
{
$this->log("=== updateWorkshopCacheForHome home={$homeId} start ===");
$rows = $this->repo->listDistinctEnabledWorkshopIdsForHome($homeId);
$results = $this->processBatch($rows);
$this->log("=== updateWorkshopCacheForHome home={$homeId} end ===");
return $results;
}
/**
* Update Workshop cache for all mods associated with a specific profile.
*
* @return array<string,mixed>
*/
public function updateWorkshopCacheForProfile(int $profileId): array
{
$this->log("=== updateWorkshopCacheForProfile profile={$profileId} start ===");
$rows = $this->repo->listDistinctEnabledWorkshopIdsForProfile($profileId);
$results = $this->processBatch($rows);
$this->log("=== updateWorkshopCacheForProfile profile={$profileId} end ===");
return $results;
}
/**
* Update a single Workshop mod on a specific agent.
*
* @return array<string,mixed>
*/
public function updateSingleWorkshopMod(int $agentId, string $appId, string $workshopId): array
{
$workshopId = preg_replace('/[^0-9]/', '', $workshopId) ?? '';
if ($workshopId === '') {
return ['success' => false, 'error' => 'Workshop ID must be numeric.'];
}
$this->log("=== updateSingleWorkshopMod agent={$agentId} app={$appId} mod={$workshopId} ===");
$row = [
'agent_id' => $agentId,
'workshop_app_id' => $appId,
'workshop_id' => $workshopId,
'title' => '',
];
$results = $this->processBatch([$row]);
return $results[0] ?? ['success' => false, 'error' => 'No result.'];
}
// ------------------------------------------------------------------
// Internal batch processor
// ------------------------------------------------------------------
/**
* For each (agent_id, workshop_app_id, workshop_id) triplet, run a
* SteamCMD validate download and update the cache table.
*
* @param array<int,array<string,mixed>> $rows
* @return array<int,array<string,mixed>>
*/
private function processBatch(array $rows): array
{
$results = [];
// Group by agent_id so we can build one connection per agent
$grouped = [];
foreach ($rows as $row) {
$aid = (int)($row['agent_id'] ?? 0);
if ($aid <= 0) {
continue;
}
$grouped[$aid][] = $row;
}
foreach ((array)$grouped as $agentId => $agentRows) {
$home = $this->getAgentHome((int)$agentId);
if ($home === null) {
$this->log("Agent {$agentId}: cannot build remote skipping.");
foreach ((array)$agentRows as $row) {
$results[] = $this->buildResult($row, false, 'Agent home not found.');
}
continue;
}
$remote = $this->buildRemote($home);
if ($remote === null || $remote->status_chk() !== 1) {
$this->log("Agent {$agentId}: offline or unreachable skipping.");
foreach ((array)$agentRows as $row) {
$this->repo->upsertCacheEntry(
(int)$agentId,
$this->detectOsType($home),
(string)($row['workshop_app_id'] ?? ''),
(string)($row['workshop_id'] ?? ''),
'',
'failed',
null,
'Agent offline during scheduled update.'
);
$results[] = $this->buildResult($row, false, 'Agent offline.');
}
continue;
}
$osType = $this->detectOsType($home);
foreach ((array)$agentRows as $row) {
$appId = (string)($row['workshop_app_id'] ?? '');
$workshopId = (string)($row['workshop_id'] ?? '');
$result = $this->runSingleUpdate($remote, (int)$agentId, $osType, $appId, $workshopId, $home);
$results[] = $result;
}
}
return $results;
}
/**
* Run SteamCMD workshop_download_item validate for a single mod and
* update the cache table accordingly.
*
* @return array<string,mixed>
*/
private function runSingleUpdate(
object $remote,
int $agentId,
string $osType,
string $appId,
string $workshopId,
array $home
): array {
$this->log("Update: agent={$agentId} app={$appId} mod={$workshopId}");
// Build cache path from the profile (if available) or a sensible default
$profile = $this->repo->getProfileByAppId($appId);
$steamCmdPath = '/home/gameserver/steamcmd/steamcmd.sh';
$cachePath = '';
if ($profile !== null) {
$vars = $this->installer->buildTemplateVars($home, $profile, $workshopId, '', $steamCmdPath);
$cachePath = $this->installer->resolveTemplate((string)($profile['cache_path_template'] ?? ''), $vars);
$steamCmdPath = $vars['{steamcmd_path}'];
}
if ($cachePath === '') {
$cachePath = "/home/gameserver/steamcmd/steamapps/workshop/content/{$appId}/{$workshopId}";
}
// Run SteamCMD with validate flag
$cmd = implode(' ', [
escapeshellarg($steamCmdPath),
'+login', 'anonymous',
'+workshop_download_item', escapeshellarg($appId), escapeshellarg($workshopId),
'validate',
'+quit',
]);
$this->log("STEAMCMD CMD: {$cmd}");
$output = (string)$remote->exec($cmd);
$this->log('STEAMCMD OUTPUT: ' . substr($output, 0, 300));
// Verify by checking path existence
$exists = $remote->rfile_exists($cachePath);
$success = ($exists === 1);
if ($success) {
$this->log("STEAMCMD SUCCESS app={$appId} mod={$workshopId}");
$this->repo->upsertCacheEntry($agentId, $osType, $appId, $workshopId, $cachePath, 'cached');
} else {
$errorMsg = 'SteamCMD validate completed but cache path not found: ' . $cachePath;
$this->log("STEAMCMD FAILURE app={$appId} mod={$workshopId}: {$errorMsg}");
$this->repo->upsertCacheEntry($agentId, $osType, $appId, $workshopId, $cachePath, 'failed', null, $errorMsg);
}
return $this->buildResult(
['agent_id' => $agentId, 'workshop_app_id' => $appId, 'workshop_id' => $workshopId],
$success,
$success ? 'OK' : 'SteamCMD failed or cache path missing.'
);
}
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
/** Return a minimal home-like array for a given agent so we can build a remote. */
private function getAgentHome(int $agentId): ?array
{
// We just need ip/port/key/timeout for the remote library connection.
// Query the remote_servers table directly via the repository's db.
// Use the OGPDatabase instance stored inside WorkshopRepository.
$prefix = $this->repo->getPrefix();
$row = $this->repo->getAgentRow($agentId);
return $row;
}
private function buildRemote(array $home): ?object
{
if (!class_exists('OGPRemoteLibrary')) {
@require_once __DIR__ . '/../../../includes/lib_remote.php';
}
if (!class_exists('OGPRemoteLibrary')) {
return null;
}
$ip = (string)($home['agent_ip'] ?? '');
$port = (string)($home['agent_port'] ?? '');
$key = (string)($home['encryption_key'] ?? '');
$timeout = isset($home['timeout']) ? (int)$home['timeout'] : 30;
if ($ip === '' || $port === '') {
return null;
}
return new OGPRemoteLibrary($ip, $port, $key, $timeout);
}
private function detectOsType(array $home): string
{
$gameKey = strtolower((string)($home['game_key'] ?? ''));
if (preg_match('/win/', $gameKey)) {
return 'windows';
}
return 'linux';
}
/** @return array<string,mixed> */
private function buildResult(array $row, bool $success, string $message): array
{
return [
'agent_id' => $row['agent_id'] ?? 0,
'workshop_app_id' => $row['workshop_app_id'] ?? '',
'workshop_id' => $row['workshop_id'] ?? '',
'success' => $success,
'message' => $message,
];
}
private function log(string $message): void
{
$line = '[' . date('Y-m-d H:i:s') . '] ' . $message . "\n";
@file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}

View file

@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
/*
* OGP / GSP Steam Workshop module entrypoint.
* Routes to either the new DB-driven WorkshopModController or the
* legacy SteamWorkshopController, depending on the action requested.
*/
require_once __DIR__ . '/controllers/SteamWorkshopController.php';
require_once __DIR__ . '/controllers/WorkshopModController.php';
function exec_ogp_module(): void
{
global $db;
$action = $_GET['action'] ?? '';
$postAction = $_POST['ws_action'] ?? '';
// JSON search endpoint no heading
if ($action === 'search') {
$controller = new SteamWorkshopController($db);
$controller->handle();
return;
}
echo '<h2>' . get_lang('steam_workshop') . '</h2>';
// New DB-driven actions
$newActions = ['index', 'mods'];
$newPostActions = ['install', 'remove', 'toggle', 'load_order', 'sync'];
if (in_array($action, $newActions, true) || in_array($postAction, $newPostActions, true)) {
$controller = new WorkshopModController($db);
$controller->handle();
return;
}
// Legacy controller for old Workshop page actions
$controller = new SteamWorkshopController($db);
$controller->handle();
}

View file

@ -1,65 +0,0 @@
-- GSP Steam Workshop database-driven tables
-- Run once against your panel database (replace `gsp_` with your table_prefix if different).
-- -------------------------------------------------------
-- Workshop game profiles (one row per supported game)
-- -------------------------------------------------------
CREATE TABLE IF NOT EXISTS `gsp_workshop_game_profiles` (
`id` INT NOT NULL AUTO_INCREMENT,
`game_key` VARCHAR(100) NOT NULL,
`game_name` VARCHAR(255) NOT NULL,
`workshop_app_id` VARCHAR(32) NOT NULL,
`supported_os` SET('linux','windows') NOT NULL DEFAULT 'linux',
`cache_path_template` TEXT NOT NULL,
`install_path_template` TEXT NOT NULL,
`folder_name_template` VARCHAR(255) NOT NULL DEFAULT '@{mod_id}',
`copy_method` ENUM('rsync','robocopy','custom_script') NOT NULL DEFAULT 'rsync',
`install_script` TEXT NULL,
`config_file_template` TEXT NULL,
`launch_param_template` TEXT NULL,
`requires_restart` TINYINT(1) NOT NULL DEFAULT 1,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_game_key` (`game_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- -------------------------------------------------------
-- Per-agent workshop download cache
-- -------------------------------------------------------
CREATE TABLE IF NOT EXISTS `gsp_workshop_cache` (
`id` INT NOT NULL AUTO_INCREMENT,
`agent_id` INT NOT NULL,
`os_type` ENUM('linux','windows') NOT NULL DEFAULT 'linux',
`workshop_app_id` VARCHAR(32) NOT NULL,
`workshop_id` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NULL,
`cache_path` TEXT NOT NULL,
`status` ENUM('missing','cached','failed') NOT NULL DEFAULT 'missing',
`last_checked` DATETIME NULL,
`last_updated` DATETIME NULL,
`last_error` TEXT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_agent_workshop` (`agent_id`, `workshop_app_id`, `workshop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- -------------------------------------------------------
-- Per-server installed Workshop mods
-- -------------------------------------------------------
CREATE TABLE IF NOT EXISTS `gsp_server_workshop_mods` (
`id` INT NOT NULL AUTO_INCREMENT,
`home_id` INT NOT NULL,
`agent_id` INT NOT NULL,
`profile_id` INT NOT NULL,
`workshop_app_id` VARCHAR(32) NOT NULL,
`workshop_id` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NULL,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`install_path` TEXT NOT NULL,
`load_order` INT NOT NULL DEFAULT 0,
`installed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View file

@ -1,223 +1,103 @@
<?php
/*
* GSP Steam Workshop module
* Copyright (C) 2025 WDS / GameServerPanel
*
* 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.
*
* 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
* (at your option) any later version.
*/
// Module general information
$module_title = "Steam Workshop";
$module_version = "2.3";
$db_version = 2;
$module_required = TRUE;
$module_menus = array();
// -----------------------------------------------------------------------
// $install_queries[0] executed for FRESH installs (all keys run).
// Contains the full v2 schema with every column.
// $install_queries[2] executed when upgrading an existing v1 install
// to v2 (ALTER TABLE + new settings table).
// $db_version = 2 (v1 = original release; v2 = this rewrite).
// -----------------------------------------------------------------------
$install_queries = array();
// ── Module metadata ──────────────────────────────────────────────────────
$module_title = "Steam Workshop";
$module_version = "3.0";
$db_version = 3;
$module_required = FALSE;
$module_menus = array(
array('subpage' => 'admin', 'name' => 'Steam Workshop', 'group' => 'admin'),
);
// Full schema for fresh installs (includes every column from all versions).
$install_queries[0] = array(
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."workshop_game_profiles` (
`id` INT NOT NULL AUTO_INCREMENT,
`game_key` VARCHAR(100) NOT NULL,
`game_name` VARCHAR(255) NOT NULL,
`steam_app_id` VARCHAR(32) NOT NULL DEFAULT '',
`workshop_app_id` VARCHAR(32) NOT NULL,
`steam_login_required` TINYINT(1) NOT NULL DEFAULT 0,
`steamcmd_login_mode` ENUM('anonymous','account') NOT NULL DEFAULT 'anonymous',
`steamcmd_path` VARCHAR(512) NOT NULL DEFAULT '',
`supported_os` SET('linux','windows') NOT NULL DEFAULT 'linux',
`cache_path_template` TEXT NOT NULL,
`install_path_template` TEXT NOT NULL,
`folder_naming_format` ENUM('@%mod_name%','@%workshop_id%','custom') NOT NULL DEFAULT '@%workshop_id%',
`folder_name_template` VARCHAR(255) NOT NULL DEFAULT '@%workshop_id%',
`mod_launch_param` VARCHAR(512) NOT NULL DEFAULT '',
`mod_separator` ENUM('semicolon','comma','space') NOT NULL DEFAULT 'semicolon',
`copy_method` ENUM('copy','rsync','symlink') NOT NULL DEFAULT 'rsync',
`copy_keys` TINYINT(1) NOT NULL DEFAULT 0,
`key_source_path` TEXT NULL,
`key_dest_path` TEXT NULL,
`pre_update_script` TEXT NULL,
`install_script` TEXT NULL,
`post_update_script` TEXT NULL,
`config_file_template` TEXT NULL,
`launch_param_template` TEXT NULL,
`requires_restart` TINYINT(1) NOT NULL DEFAULT 1,
`validation_notes` TEXT NULL,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
// ── SQL helpers ──────────────────────────────────────────────────────────
// All OGP_DB_PREFIX tokens are replaced at runtime by $db->query() /
// $db->resultQuery() before the SQL reaches MySQL. Do not replace them
// here with literal strings.
$_sw_drop_old = array(
"DROP TABLE IF EXISTS `OGP_DB_PREFIXworkshop_game_profiles`",
"DROP TABLE IF EXISTS `OGP_DB_PREFIXworkshop_cache`",
"DROP TABLE IF EXISTS `OGP_DB_PREFIXserver_workshop_mods`",
"DROP TABLE IF EXISTS `OGP_DB_PREFIXserver_workshop_settings`",
);
$_sw_create_new = array(
"CREATE TABLE IF NOT EXISTS `OGP_DB_PREFIXsteam_workshop_game_profiles` (
`id` INT NOT NULL AUTO_INCREMENT,
`config_name` VARCHAR(100) NOT NULL,
`game_name` VARCHAR(255) NOT NULL DEFAULT '',
`enabled` TINYINT(1) NOT NULL DEFAULT 0,
`steam_app_id` VARCHAR(32) NOT NULL DEFAULT '',
`workshop_app_id` VARCHAR(32) NOT NULL DEFAULT '',
`steam_login_required` TINYINT(1) NOT NULL DEFAULT 0,
`steamcmd_login_mode` ENUM('anonymous','account') NOT NULL DEFAULT 'anonymous',
`steamcmd_path` VARCHAR(512) NOT NULL DEFAULT '/home/gameserver/steamcmd/steamcmd.sh',
`workshop_download_dir_template` TEXT NULL,
`server_root_template` TEXT NULL,
`install_path_template` TEXT NULL,
`folder_naming_format` VARCHAR(64) NOT NULL DEFAULT '@{MOD_NAME}',
`mod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-mod=',
`servermod_launch_param_template` VARCHAR(255) NOT NULL DEFAULT '-serverMod=',
`install_script_template` TEXT NULL,
`update_script_template` TEXT NULL,
`copy_bikeys_enabled` TINYINT(1) NOT NULL DEFAULT 1,
`notes` TEXT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_game_key` (`game_key`)
UNIQUE KEY `uniq_config_name` (`config_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."workshop_cache` (
`id` INT NOT NULL AUTO_INCREMENT,
`agent_id` INT NOT NULL,
`os_type` ENUM('linux','windows') NOT NULL DEFAULT 'linux',
`workshop_app_id` VARCHAR(32) NOT NULL,
`workshop_id` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NULL,
`cache_path` TEXT NOT NULL,
`status` ENUM('missing','cached','failed') NOT NULL DEFAULT 'missing',
`last_checked` DATETIME NULL,
`last_updated` DATETIME NULL,
`last_error` TEXT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_agent_workshop` (`agent_id`, `workshop_app_id`, `workshop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_workshop_mods` (
"CREATE TABLE IF NOT EXISTS `OGP_DB_PREFIXsteam_workshop_server_mods` (
`id` INT NOT NULL AUTO_INCREMENT,
`home_id` INT NOT NULL,
`agent_id` INT NOT NULL,
`profile_id` INT NOT NULL,
`workshop_app_id` VARCHAR(32) NOT NULL,
`workshop_id` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NULL,
`custom_folder` VARCHAR(255) NOT NULL DEFAULT '',
`mod_name` VARCHAR(255) NOT NULL DEFAULT '',
`folder_name` VARCHAR(255) NOT NULL DEFAULT '',
`mod_type` ENUM('client','server') NOT NULL DEFAULT 'client',
`sort_order` INT NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`install_path` TEXT NOT NULL,
`load_order` INT NOT NULL DEFAULT 0,
`installed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`install_status` VARCHAR(32) NOT NULL DEFAULT '',
`last_installed_at` DATETIME NULL,
`last_updated_at` DATETIME NULL,
`last_error` TEXT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_home_workshop` (`home_id`, `workshop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_workshop_settings` (
`home_id` INT NOT NULL,
`workshop_enabled` TINYINT(1) NOT NULL DEFAULT 0,
`profile_id` INT NULL,
`update_mode` ENUM('manual','scheduled','on_restart') NOT NULL DEFAULT 'manual',
`restart_behavior` ENUM('none','queue','stop_update_start') NOT NULL DEFAULT 'none',
`update_queued` TINYINT(1) NOT NULL DEFAULT 0,
`last_update_status` VARCHAR(20) NOT NULL DEFAULT '',
`last_update_error` TEXT NULL,
`last_update_time` DATETIME NULL,
`last_success_time` DATETIME NULL,
`updated_at` DATETIME NULL,
PRIMARY KEY (`home_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
// Migration: upgrade existing v1 installs to v2 schema.
// ── Install queries ──────────────────────────────────────────────────────
//
// ADD COLUMN IF NOT EXISTS is not supported in MySQL 5.7 (MariaDB only).
// Each column addition is therefore performed via a PHP closure that:
// 1. Queries INFORMATION_SCHEMA.COLUMNS to check whether the column exists.
// 2. Runs ALTER TABLE ADD COLUMN only when it does not exist.
// This makes the migration safe to run multiple times without errors.
// OGP_DB_PREFIX in SQL strings is replaced at runtime by the panel DB wrapper.
$install_queries[2] = array(
// $install_queries[0] runs on fresh install (module manager iterates all keys).
// Drops any legacy tables and creates the new schema.
// $install_queries[3] runs when upgrading from db_version 2 → 3.
// Same content; idempotent because of IF [NOT] EXISTS.
//
// Note: the module manager loops $install_queries[$i+1] for each step from
// current db_version up to target. Keys 1 and 2 are intentionally absent;
// the manager safely skips undefined keys (PHP returns NULL → empty array).
// Add new columns to workshop_game_profiles one-by-one (MySQL 5.7 safe).
function($db) {
// 'OGP_DB_PREFIX' is the literal token that $db->query() / $db->resultQuery()
// replaces with the configured table prefix (e.g. 'gsp_') via str_replace at
// runtime. Using it directly in string literals below is intentional and is
// the same mechanism used everywhere else in the panel.
$tbl_profiles = 'OGP_DB_PREFIXworkshop_game_profiles';
$install_queries = array();
// column_name => column definition (no AFTER clause for portability)
// $col is always a value from this hardcoded array — not from user input.
$columns = array(
'steam_app_id' => "VARCHAR(32) NOT NULL DEFAULT ''",
'steam_login_required' => "TINYINT(1) NOT NULL DEFAULT 0",
'steamcmd_login_mode' => "ENUM('anonymous','account') NOT NULL DEFAULT 'anonymous'",
'steamcmd_path' => "VARCHAR(512) NOT NULL DEFAULT ''",
'folder_naming_format' => "ENUM('@%mod_name%','@%workshop_id%','custom') NOT NULL DEFAULT '@%workshop_id%'",
'mod_launch_param' => "VARCHAR(512) NOT NULL DEFAULT ''",
'mod_separator' => "ENUM('semicolon','comma','space') NOT NULL DEFAULT 'semicolon'",
'copy_keys' => "TINYINT(1) NOT NULL DEFAULT 0",
'key_source_path' => "TEXT NULL",
'key_dest_path' => "TEXT NULL",
'pre_update_script' => "TEXT NULL",
'post_update_script' => "TEXT NULL",
'validation_notes' => "TEXT NULL",
);
foreach ($columns as $col => $def) {
// INFORMATION_SCHEMA.COLUMNS always returns one row for COUNT(*),
// so resultQuery returns an array (never FALSE for this query form).
// Escape $col when embedding it in the SQL string literal.
$safe_col = $db->realEscapeSingle($col);
$check = $db->resultQuery(
"SELECT COUNT(*) AS n
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '" . $tbl_profiles . "'
AND COLUMN_NAME = '" . $safe_col . "'"
);
// If n > 0 the column already exists; skip it.
if ($check !== false && isset($check[0]['n']) && (int)$check[0]['n'] > 0) {
continue;
}
// $col is backtick-quoted so it is safe as an identifier.
if (!$db->query(
"ALTER TABLE `" . $tbl_profiles . "`
ADD COLUMN `" . $col . "` " . $def
)) {
return false;
}
}
return true;
},
$install_queries[0] = array_merge($_sw_drop_old, $_sw_create_new);
$install_queries[3] = array_merge($_sw_drop_old, $_sw_create_new);
// Add custom_folder to server_workshop_mods (MySQL 5.7 safe).
function($db) {
// See note above: 'OGP_DB_PREFIX' is replaced by str_replace at runtime.
$tbl_mods = 'OGP_DB_PREFIXserver_workshop_mods';
$check = $db->resultQuery(
"SELECT COUNT(*) AS n
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '" . $tbl_mods . "'
AND COLUMN_NAME = 'custom_folder'"
);
if ($check !== false && isset($check[0]['n']) && (int)$check[0]['n'] > 0) {
return true; // Column already exists.
}
return (bool)$db->query(
"ALTER TABLE `" . $tbl_mods . "`
ADD COLUMN `custom_folder` VARCHAR(255) NOT NULL DEFAULT ''"
);
},
unset($_sw_drop_old, $_sw_create_new);
// New server-level settings table (CREATE IF NOT EXISTS is safe to re-run).
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_workshop_settings` (
`home_id` INT NOT NULL,
`workshop_enabled` TINYINT(1) NOT NULL DEFAULT 0,
`profile_id` INT NULL,
`update_mode` ENUM('manual','scheduled','on_restart') NOT NULL DEFAULT 'manual',
`restart_behavior` ENUM('none','queue','stop_update_start') NOT NULL DEFAULT 'none',
`update_queued` TINYINT(1) NOT NULL DEFAULT 0,
`last_update_status` VARCHAR(20) NOT NULL DEFAULT '',
`last_update_error` TEXT NULL,
`last_update_time` DATETIME NULL,
`last_success_time` DATETIME NULL,
`updated_at` DATETIME NULL,
PRIMARY KEY (`home_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
// ── Uninstall queries ─────────────────────────────────────────────────────
$uninstall_queries = array(
"DROP TABLE IF EXISTS `OGP_DB_PREFIXsteam_workshop_server_mods`",
"DROP TABLE IF EXISTS `OGP_DB_PREFIXsteam_workshop_game_profiles`",
);

View file

@ -1,55 +0,0 @@
<?php
/*
*
* 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.
*
*/
require_once __DIR__ . '/lib/SteamWorkshopService.php';
global $db;
$module_buttons = array();
if (isset($server_xml) && isset($server_home['home_id']))
{
$service = new SteamWorkshopService($db);
if ($service->gameSupportsWorkshop($server_xml))
{
$homeId = (int)$server_home['home_id'];
if ($homeId > 0)
{
$label = get_lang('steam_workshop');
if ($label === 'steam_workshop')
{
$label = 'Steam Workshop';
}
$href = "?m=steam_workshop&p=server_mods&action=edit&home_id=" . $homeId;
$module_buttons = array(
"<a class='monitorbutton' href='" . $href . "'>
<img src='" . check_theme_image("images/steam_workshop.png") . "' title='" . $label . "'>
<span>" . $label . "</span>
</a>"
);
}
}
}
?>

View file

@ -1,5 +1,6 @@
<navigation>
<page key="server_mods" file="main.php" access="admin" />
<page key="uninstall" file="uninstall.php" access="admin" />
<page key="workshop_admin" file="workshop_admin.php" access="admin" />
</navigation>
<!-- Admin: manage Steam Workshop game profiles -->
<page key="admin" file="admin.php" access="admin" />
<!-- User: manage per-server mods -->
<page key="user_mods" file="user.php" access="admin,user,subuser" />
</navigation>

View file

@ -1,91 +0,0 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/*
* OGP / GSP Steam Workshop pre-start sync helper
*
* Called from the game XML <pre_start> tag or a server pre-start hook:
* php modules/steam_workshop/prestart_sync.php --home-id=<ID>
*
* This script:
* 1. Finds all enabled Workshop mods for the given home.
* 2. Checks each mod's local cache on the agent.
* 3. If the cache differs from the server install path, syncs it.
* 4. Continues normal server start (exits 0 on success).
* 5. Exits non-zero ONLY if a critical error prevents completion.
*
* Design note: sync failures are logged but do NOT abort the server start,
* because a stale mod is better than no start.
*/
$panelRoot = defined('PANEL_ROOT') ? PANEL_ROOT : realpath(__DIR__ . '/../../..');
if ($panelRoot === false) {
$panelRoot = __DIR__ . '/../../..';
}
chdir($panelRoot);
if (!is_file('includes/config.inc.php')) {
fwrite(STDERR, "[ERROR] Cannot locate includes/config.inc.php.\n");
exit(0); // don't block server start
}
require_once 'includes/config.inc.php';
require_once 'includes/database.php';
require_once 'includes/database_mysqli.php';
require_once 'includes/lib_remote.php';
if (!isset($db_host, $db_user, $db_pass, $db_name)) {
fwrite(STDERR, "[ERROR] Database configuration not set.\n");
exit(0);
}
$db = new OGPDatabaseMySQL();
$connResult = $db->connect($db_host, $db_user, $db_pass, $db_name, $table_prefix ?? 'gsp_', $db_port ?? null);
if ($connResult !== true) {
fwrite(STDERR, "[ERROR] DB connect failed: {$connResult}\n");
exit(0);
}
require_once __DIR__ . '/lib/WorkshopRepository.php';
require_once __DIR__ . '/lib/WorkshopInstaller.php';
require_once __DIR__ . '/lib/WorkshopPreStart.php';
$opts = getopt('', ['home-id:', 'help']);
if (isset($opts['help']) || !isset($opts['home-id'])) {
echo "Usage: php prestart_sync.php --home-id=<ID>\n";
exit(0);
}
$homeId = (int)$opts['home-id'];
if ($homeId <= 0) {
fwrite(STDERR, "[ERROR] --home-id must be a positive integer.\n");
exit(0);
}
$home = $db->getGameHome($homeId);
if (!is_array($home)) {
fwrite(STDERR, "[WARN] Home {$homeId} not found skipping pre-start sync.\n");
exit(0);
}
$repo = new WorkshopRepository($db);
$installer = new WorkshopInstaller($repo);
$preStart = new WorkshopPreStart($repo, $installer);
$result = $preStart->syncModsForHome($home);
echo sprintf(
"[PRE-START] home=%d synced=%d skipped=%d failed=%d\n",
$homeId,
$result['synced'],
$result['skipped'],
$result['failed']
);
foreach ((array)($result['log'] ?? []) as $line) {
echo " {$line}\n";
}
// Always exit 0 don't block server start due to Workshop sync issues
exit(0);

View file

@ -1,790 +0,0 @@
#scrolling_checkbox{
#scrolling_checkbox{
border:2px solid #ccc;
width:500px;
display: inline-block;
height: 80px;
overflow-y: scroll;
text-align:left;
}
.sw-monitor {
margin-top: 1rem;
}
.sw-monitor__back {
text-decoration: none;
color: #0b5ed7;
}
.sw-monitor__card {
border: 1px solid #dcdcdc;
border-radius: 8px;
padding: 1rem;
background: #fff;
}
.sw-monitor__intro {
color: #555;
margin-top: 0.25rem;
}
.sw-monitor__meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.75rem;
margin: 1rem 0;
}
.sw-monitor__meta dt {
margin: 0;
font-weight: 600;
}
.sw-monitor__meta dd {
margin: 0.15rem 0 0;
}
.sw-monitor__form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.sw-monitor__form label {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.sw-monitor__form input {
padding: 0.4rem;
border: 1px solid #c7c7c7;
border-radius: 4px;
}
.sw-monitor__form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.75rem;
}
.sw-monitor__summary {
margin-top: 1rem;
background: #f7f9ff;
border: 1px solid #d0dae9;
border-radius: 6px;
padding: 0.75rem;
}
.sw-monitor__summary-text {
margin-top: 0.25rem;
font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
word-break: break-all;
}
.sw-monitor__alert {
margin-top: 1rem;
border-radius: 6px;
padding: 0.75rem;
}
.sw-monitor__alert--error {
background: #fff5f4;
border: 1px solid #e66b5b;
color: #a33d30;
}
.sw-monitor__alert--info {
background: #f4f6f8;
border: 1px solid #d5dce3;
color: #4a5568;
}
.sw-monitor__results {
margin-top: 1.5rem;
}
.sw-monitor__hint {
color: #555;
margin: 0.25rem 0 0.75rem;
}
.sw-monitor__table-wrapper {
overflow-x: auto;
}
.sw-monitor__table {
width: 100%;
border-collapse: collapse;
}
.sw-monitor__table th,
.sw-monitor__table td {
border: 1px solid #e3e3e3;
padding: 0.45rem;
}
.sw-monitor__table code {
font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
}
#uninstall_scrolling_checkbox{
border:2px solid #ccc;
width:500px;
display: inline-block;
height: 80px;
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.btn-xs {
padding: 0.2rem 0.45rem;
font-size: 0.85rem;
}
.btn.primary {
background: #0b5ed7;
border-color: #0a58ca;
color: #fff;
}
.btn.secondary {
background: #f7f7f7;
border-color: #c7c7c7;
}
.sw-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
}
.sw-toggle input {
width: auto;
}
.sw-game-table__wrapper {
margin-top: 1rem;
overflow-x: auto;
}
.sw-game-table__row td {
vertical-align: top;
}
.sw-game-label {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.sw-game-label__title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
}
.sw-game-label__key {
font-weight: 600;
}
.sw-game-label__name {
font-weight: 600;
}
.sw-game-label__hint {
color: #666;
}
.sw-game-label__hint--warning {
color: #b15a00;
}
.sw-badge {
display: inline-flex;
align-items: center;
padding: 0.1rem 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
}
.sw-badge--custom {
background: #eef4ff;
color: #0b5ed7;
}
.sw-badge--app {
background: #f1f3f5;
color: #444;
}
.sw-game-variants {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.sw-chip {
background: #f4f6f8;
border-radius: 999px;
padding: 0.1rem 0.55rem;
font-size: 0.8rem;
}
.sw-actions {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.sw-inline-delete {
display: inline;
}
.sw-game-table__form-row {
display: none;
}
.sw-game-table__form-row.is-open {
display: table-row;
}
.sw-inline-form {
background: #f6f8fb;
border: 1px solid #d0dae9;
border-radius: 6px;
padding: 1rem;
}
.sw-checkbox {
display: flex;
align-items: center;
gap: 0.4rem;
}
.sw-admin__mapping-actions {
margin-top: 1rem;
}
.sw-picker {
display: flex;
flex-direction: column;
gap: 1rem;
border: 1px solid #dcdcdc;
border-radius: 8px;
padding: 1rem;
}
.sw-picker__header {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.sw-picker__header h4 {
margin: 0;
font-size: 1.1rem;
}
.sw-picker__hint {
margin: 0.25rem 0 0;
color: #555;
font-size: 0.9rem;
}
.sw-picker__search {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: flex-end;
}
.sw-picker__search label {
flex: 1;
}
.sw-picker__search-input {
width: 100%;
padding: 0.4rem;
border-radius: 4px;
border: 1px solid #c7c7c7;
}
.sw-picker__status {
min-height: 1.25rem;
font-size: 0.9rem;
}
.sw-picker__status--loading {
color: #0b5ed7;
}
.sw-picker__status--error {
color: #c0392b;
}
.sw-picker__status--info {
color: #555;
}
.sw-picker__status--clear {
color: inherit;
}
.sw-picker__selected,
.sw-picker__results {
border: 1px solid #e3e3e3;
border-radius: 6px;
padding: 0.75rem;
}
.sw-picker__selected-header {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 0.5rem;
}
.sw-picker__selected-header small {
color: #666;
}
.sw-picker__chip-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.sw-picker__chip {
display: flex;
justify-content: space-between;
gap: 0.75rem;
border: 1px solid #d0dae9;
border-radius: 6px;
padding: 0.5rem 0.75rem;
background: #f8fafd;
}
.sw-picker__chip-text {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.sw-picker__chip-text span {
color: #555;
font-size: 0.85rem;
}
.sw-picker__chip-controls {
display: flex;
gap: 0.5rem;
align-items: center;
}
.sw-picker__chip-remove {
border: 1px solid #c0392b;
background: #fff5f4;
color: #c0392b;
border-radius: 4px;
padding: 0.25rem 0.6rem;
cursor: pointer;
}
.sw-picker__toggle {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.85rem;
}
.sw-picker__results-table {
width: 100%;
border-collapse: collapse;
}
.sw-picker__results-table th,
.sw-picker__results-table td {
border: 1px solid #e3e3e3;
padding: 0.4rem;
vertical-align: top;
}
.sw-picker__result-meta {
color: #666;
font-size: 0.85rem;
}
.sw-picker__results-table-wrapper {
overflow-x: auto;
}
.sw-picker__results-hint {
margin: 0.35rem 0 0.6rem;
color: #555;
font-size: 0.9rem;
}
.sw-picker__request-row {
margin-top: 0.75rem;
}
.sw-picker__request-label {
display: block;
font-weight: 600;
margin-bottom: 0.15rem;
}
.sw-picker__request-hint {
display: block;
margin-bottom: 0.35rem;
color: #6b6b6b;
}
.sw-picker__request-line {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
align-items: center;
font-family: Consolas, 'Courier New', monospace;
}
.sw-picker__request-summary {
padding: 0.2rem 0.4rem;
background: #f6f6f6;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.sw-picker__request-input {
flex: 1;
min-width: 160px;
padding: 0.25rem 0.4rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-family: inherit;
background: #fff;
}
.sw-picker__action {
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: #0b5ed7;
border: 1px solid #0a58ca;
color: #fff;
}
.sw-picker__result-toggle {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.85rem;
}
.sw-picker__result-toggle input {
width: auto;
}
.sw-picker__empty {
color: #777;
font-size: 0.9rem;
}
/* Info box used on the configuration form to explain SteamCMD usage */
.sw-info-box {
background: #e8f4fd;
border: 1px solid #b3d7f5;
border-radius: 4px;
padding: 0.75rem 1rem;
margin-bottom: 1.25rem;
font-size: 0.9rem;
}
.sw-info-box strong {
display: block;
margin-bottom: 0.25rem;
}
.sw-info-box p {
margin: 0;
color: #2c5f8a;
}
/* ── v2 form additions ── */
.sw-form__grid--3col {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
}
.sw-form__grid--2col {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
}
.sw-form__row {
display: flex;
gap: 0.75rem;
align-items: flex-end;
}
.sw-form__row label {
flex: 1;
}
.sw-form fieldset {
border: 1px solid #dde;
border-radius: 6px;
padding: 1rem 1.25rem;
margin-bottom: 1.25rem;
}
.sw-form fieldset legend {
font-weight: 600;
font-size: 0.95rem;
padding: 0 0.5rem;
color: #333;
}
.sw-form__os-group {
border: none !important;
padding: 0.5rem 0 0 !important;
margin: 0.5rem 0 0 !important;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.sw-script-textarea {
font-family: monospace;
font-size: 0.85rem;
width: 100%;
box-sizing: border-box;
}
.sw-info-box--compact {
padding: 0.5rem 0.75rem;
margin: 0.5rem 0 0.75rem;
font-size: 0.85rem;
}
.sw-code-pre {
background: #f4f4f8;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0.6rem 0.9rem;
font-family: monospace;
font-size: 0.82rem;
white-space: pre;
overflow-x: auto;
margin: 0.5rem 0;
}
.sw-example-block summary {
cursor: pointer;
color: #0b5ed7;
font-size: 0.85rem;
margin-bottom: 0.25rem;
}
.sw-inline {
display: inline-block;
margin: 0;
}
/* Server settings section */
.sw-server-settings {
background: #f8f8fc;
border: 1px solid #e0e0ee;
border-radius: 6px;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
}
.sw-server-settings h4 {
margin-top: 0;
}
.sw-update-status {
margin-top: 1rem;
padding-top: 0.75rem;
border-top: 1px solid #e0e0ee;
}
.sw-status-grid {
display: grid;
grid-template-columns: max-content 1fr;
gap: 0.25rem 1rem;
font-size: 0.9rem;
margin: 0 0 0.75rem;
}
.sw-status-grid dt {
font-weight: 600;
margin: 0;
}
.sw-status-grid dd {
margin: 0;
}
.sw-error-text code {
color: #c0392b;
font-size: 0.82rem;
word-break: break-all;
}
.sw-notice {
background: #fff8e1;
border: 1px solid #ffe082;
border-radius: 4px;
padding: 0.6rem 0.9rem;
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.sw-notice--info {
background: #e8f4fd;
border-color: #b3d7f5;
}
.sw-badge--danger {
background: #f8d7da;
color: #721c24;
}
.sw-badge--info {
background: #cce5ff;
color: #004085;
}
.sw-badge--warning {
background: #fff3cd;
color: #856404;
}
.sw-badge--app, .sw-badge--custom {
background: #e9ecef;
color: #495057;
}
.sw-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.sw-order-input {
width: 4.5rem;
text-align: center;
}
.sw-profiles__table code {
font-size: 0.82rem;
background: #f4f4f4;
padding: 0 3px;
border-radius: 2px;
}
.sw-settings-form .sw-checkbox {
padding-top: 0.5rem;
}

View file

@ -1,391 +0,0 @@
(function () {
'use strict';
var Picker = /** @class */ (function () {
function Picker(root) {
this.root = root;
this.endpoint = root.getAttribute('data-endpoint') || '';
this.detailBase = root.getAttribute('data-detail-base') || 'https://steamcommunity.com/sharedfiles/filedetails/?id=';
this.lang = {
add: root.getAttribute('data-lang-add') || 'Add',
remove: root.getAttribute('data-lang-remove') || 'Remove',
loading: root.getAttribute('data-lang-loading') || 'Loading…',
error: root.getAttribute('data-lang-error') || 'Something went wrong.',
empty: root.getAttribute('data-lang-empty') || 'No results found.',
query: root.getAttribute('data-lang-query') || 'Enter a Workshop ID or keyword.',
sync: root.getAttribute('data-lang-sync') || 'Sync',
};
this.selectedInput = root.querySelector('.js-sw-selected-input');
this.selectedList = root.querySelector('.js-sw-selected-list');
this.resultsBody = root.querySelector('.js-sw-results');
this.statusEl = root.querySelector('.js-sw-picker-status');
this.searchForm = root.querySelector('.js-sw-search-form');
this.searchInput = root.querySelector('.js-sw-search-input');
this.searchButton = root.querySelector('.js-sw-search-button');
this.requestInput = root.querySelector('.js-sw-request-input');
this.requestSummary = root.querySelector('.js-sw-request-summary');
this.requestSummaryBase = this.requestSummary ? (this.requestSummary.getAttribute('data-base') || '') : '';
this.state = {
selected: this.readInitialSelection(),
};
this.lastResults = [];
this.bindEvents();
this.renderSelected();
this.updateRequestPreview();
}
Picker.prototype.readInitialSelection = function () {
if (!this.selectedInput) {
return [];
}
try {
var parsed = JSON.parse(this.selectedInput.value || '[]');
if (Array.isArray(parsed)) {
return parsed.filter(function (item) { return item && item.id; })
.map(function (item) { return ({
id: String(item.id),
label: String(item.label || ('@' + item.id)),
author: String(item.author || ''),
preview_url: String(item.preview_url || ''),
enabled: !(item.enabled === false || item.enabled === 'false' || item.enabled === 0 || item.enabled === '0'),
source: String(item.source || 'manual'),
}); });
}
}
catch (err) {
console.warn('Invalid Workshop JSON state', err);
}
return [];
};
Picker.prototype.bindEvents = function () {
var _this = this;
if (this.searchForm && this.searchForm.tagName === 'FORM') {
this.searchForm.addEventListener('submit', function (event) {
event.preventDefault();
_this.performSearch();
});
}
if (this.searchButton) {
this.searchButton.addEventListener('click', function (event) {
event.preventDefault();
_this.performSearch();
});
}
if (this.searchInput && (!this.searchForm || this.searchForm.tagName !== 'FORM')) {
this.searchInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
_this.performSearch();
}
});
}
if (this.searchInput) {
this.searchInput.addEventListener('input', function () {
_this.updateRequestPreview();
});
}
if (this.selectedList) {
this.selectedList.addEventListener('click', function (event) {
var target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (target.matches('.js-sw-remove')) {
var id = target.getAttribute('data-id');
if (id) {
_this.removeSelected(id);
}
}
});
this.selectedList.addEventListener('change', function (event) {
var target = event.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
if (target.matches('.js-sw-toggle')) {
var id = target.getAttribute('data-id');
if (id) {
_this.toggleSelected(id, target.checked);
}
}
});
}
if (this.resultsBody) {
this.resultsBody.addEventListener('change', function (event) {
var target = event.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
if (target.matches('.js-sw-result-toggle')) {
var payload = target.getAttribute('data-payload');
if (payload) {
try {
var data = JSON.parse(payload);
if (target.checked) {
_this.addSelected(data);
}
else {
_this.removeSelected(String(data.id));
}
}
catch (err) {
console.warn('Invalid payload', err);
}
}
}
});
}
};
Picker.prototype.performSearch = function () {
var _this = this;
if (!this.endpoint || !this.searchInput) {
return;
}
var term = this.searchInput.value.trim();
this.updateRequestPreview();
if (!term) {
this.setStatus(this.lang.query, 'error');
return;
}
if (this.isSearching) {
return;
}
this.isSearching = true;
this.setStatus(this.lang.loading, 'loading');
var url = this.endpoint + '&q=' + encodeURIComponent(term);
fetch(url, {
headers: { 'Accept': 'application/json' },
})
.then(function (response) {
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
return response.json();
})
.then(function (data) {
if (!data || data.ok === false) {
var message = (data && data.error) ? data.error : _this.lang.error;
_this.setStatus(message, 'error');
_this.renderResults([]);
return;
}
if (Array.isArray(data.results) && data.results.length) {
_this.setStatus('', 'clear');
_this.renderResults(data.results);
}
else {
_this.setStatus(_this.lang.empty, 'info');
_this.renderResults([]);
}
})
.catch(function (error) {
console.error('Workshop search failed', error);
_this.setStatus(_this.lang.error, 'error');
_this.renderResults([]);
})
.finally(function () {
_this.isSearching = false;
});
};
Picker.prototype.setStatus = function (message, kind) {
if (!this.statusEl) {
return;
}
this.statusEl.textContent = message || '';
this.statusEl.className = 'sw-picker__status js-sw-picker-status' + (kind ? ' sw-picker__status--' + kind : '');
};
Picker.prototype.renderResults = function (results) {
if (!this.resultsBody) {
return;
}
this.resultsBody.innerHTML = '';
if (!Array.isArray(results) || !results.length) {
this.lastResults = [];
return;
}
this.lastResults = results.slice();
var _loop_1 = function (item) {
var normalized = {
id: String(item.id),
label: String(item.label || ('@' + item.id)),
author: String(item.author || ''),
preview_url: String(item.preview_url || ''),
enabled: true,
source: String(item.source || 'search'),
};
var row = document.createElement('tr');
var selectCell = document.createElement('td');
var toggle = document.createElement('label');
toggle.className = 'sw-picker__result-toggle';
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'js-sw-result-toggle';
checkbox.setAttribute('data-payload', JSON.stringify(normalized));
checkbox.checked = this_1.isSelected(normalized.id);
toggle.appendChild(checkbox);
var toggleText = document.createElement('span');
toggleText.textContent = this_1.lang.add;
toggle.appendChild(toggleText);
selectCell.appendChild(toggle);
var titleCell = document.createElement('td');
titleCell.innerHTML = '<strong>' + this_1.escape(normalized.label) + '</strong><div class="sw-picker__result-meta">#' + this_1.escape(normalized.id) + '</div>';
var authorCell = document.createElement('td');
authorCell.textContent = normalized.author;
row.appendChild(selectCell);
row.appendChild(titleCell);
row.appendChild(authorCell);
this_1.resultsBody.appendChild(row);
};
var this_1 = this;
for (var _i = 0, results_1 = results; _i < results_1.length; _i++) {
var item = results_1[_i];
_loop_1(item);
}
};
Picker.prototype.updateRequestPreview = function () {
if (!this.searchInput) {
return;
}
var term = this.searchInput.value.trim();
if (this.requestInput) {
this.requestInput.value = term;
}
if (!this.requestSummary) {
return;
}
var base = this.requestSummaryBase || '';
if (!base) {
this.requestSummary.textContent = '';
return;
}
if (!term) {
this.requestSummary.textContent = base;
return;
}
// Numeric-only terms are treated as Workshop item IDs and link to detail pages instead of search.
var isWorkshopId = /^\d+$/.test(term);
if (isWorkshopId) {
this.requestSummary.textContent = this.detailBase + encodeURIComponent(term);
return;
}
this.requestSummary.textContent = base + encodeURIComponent(term);
};
Picker.prototype.isSelected = function (id) {
return this.state.selected.some(function (item) { return item.id === id; });
};
Picker.prototype.addSelected = function (item) {
if (!item || !item.id || this.isSelected(String(item.id))) {
return;
}
this.state.selected.push({
id: String(item.id),
label: String(item.label || ('@' + item.id)),
author: String(item.author || ''),
preview_url: String(item.preview_url || ''),
enabled: true,
source: String(item.source || 'search'),
});
this.persist();
this.renderSelected();
};
Picker.prototype.removeSelected = function (id) {
var next = this.state.selected.filter(function (item) { return item.id !== id; });
this.state.selected = next;
this.persist();
this.renderSelected();
};
Picker.prototype.toggleSelected = function (id, enabled) {
var changed = false;
this.state.selected = this.state.selected.map(function (item) {
if (item.id === id) {
changed = true;
return {
id: item.id,
label: item.label,
author: item.author,
preview_url: item.preview_url,
enabled: enabled,
source: item.source,
};
}
return item;
});
if (changed) {
this.persist();
}
};
Picker.prototype.renderSelected = function () {
if (!this.selectedList) {
return;
}
this.selectedList.innerHTML = '';
if (!this.state.selected.length) {
var emptyText = this.selectedList.getAttribute('data-empty-text') || '';
if (emptyText) {
var empty = document.createElement('div');
empty.className = 'sw-picker__empty';
empty.textContent = emptyText;
this.selectedList.appendChild(empty);
}
return;
}
var this_2 = this;
for (var _i = 0, _a = this.state.selected; _i < _a.length; _i++) {
var item = _a[_i];
var chip = document.createElement('div');
chip.className = 'sw-picker__chip';
chip.innerHTML = '<div class="sw-picker__chip-text"><strong>' + this.escape(item.label) + '</strong><span>#' + this.escape(item.id) + '</span></div>';
var controls = document.createElement('div');
controls.className = 'sw-picker__chip-controls';
var toggle = document.createElement('label');
toggle.className = 'sw-picker__toggle';
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'js-sw-toggle';
checkbox.checked = item.enabled !== false;
checkbox.setAttribute('data-id', item.id);
toggle.appendChild(checkbox);
var toggleText = document.createElement('span');
toggleText.textContent = this_2.lang.sync;
toggle.appendChild(toggleText);
var removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'sw-picker__chip-remove js-sw-remove';
removeBtn.setAttribute('data-id', item.id);
removeBtn.textContent = this_2.lang.remove;
controls.appendChild(toggle);
controls.appendChild(removeBtn);
chip.appendChild(controls);
this.selectedList.appendChild(chip);
}
this.persist();
};
Picker.prototype.persist = function () {
if (!this.selectedInput) {
return;
}
try {
this.selectedInput.value = JSON.stringify(this.state.selected);
}
catch (err) {
console.error('Unable to serialize workshop selection', err);
}
};
Picker.prototype.escape = function (value) {
var div = document.createElement('div');
div.textContent = value;
return div.innerHTML;
};
return Picker;
}());
document.addEventListener('DOMContentLoaded', function () {
var nodes = document.querySelectorAll('.sw-picker');
Array.prototype.forEach.call(nodes, function (node) {
try {
new Picker(node);
}
catch (err) {
console.error('Failed to boot Steam Workshop picker', err);
}
});
});
})();

View file

@ -1,178 +0,0 @@
<?php
/*
*
* 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.
*
*/
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');
function exec_ogp_module()
{
Global $db,$view;
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['uninstall']))
{
echo "<ul>".
"<li><a href='?m=steam_workshop&p=main&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."'>".get_lang('install_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;
}
preg_match('/(linux|win)(32|64)?/i', $home_cfg['game_key'], $matches);
if(!isset($matches[1]))
{
print_failure(get_lang_f('unable_to_get_os_from_game_key', $home_cfg['game_key']));
return;
}
if(strtolower($matches[1]) == 'linux')
$os = "Linux";
elseif(strtolower($matches[1]) == 'win')
$os = "Windows";
if(!isset($os))
{
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('no_workshop_configuration_available_for_this_game'));
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($_POST['uninstall']) and isset($_POST['mod_string']))
{
$output = "";
foreach ((array)$_POST['mod_string'] as $mod_string)
{
$result = remove_mod($home_cfg, $remote, $xml, $mod_string);
if($result !== FALSE)
$output .= $result."\n";
else
$output .= get_lang_f('failed_uninstalling_mod', $mod_string)."\n";
}
echo "<pre>$output</pre>";
echo "<a href='?m=steam_workshop&p=uninstall&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port']."'>".get_lang('back')."</a>";
}
else
{
$mods = get_installed_mods($home_cfg, $remote, $xml);
if($mods and count((array)$mods) > 0)
{
$ft = new FormTable();
$ft->start_form("?m=steam_workshop&p=uninstall&home_id-mod_id-ip-port=".$_GET['home_id-mod_id-ip-port'], "post", "autocomplete=\"off\"");
$ft->start_table();
echo '<tr><td><div id="uninstall_scrolling_checkbox">';
foreach ((array)$mods as $mod_id => $mod_name)
echo "<input type='checkbox' id='select_mod_$mod_id' name='mod_string[]' value='$mod_id'><label for='select_mod_$mod_id'>$mod_name</label><br>";
echo '</div></td></tr>';
$ft->end_table();
$ft->add_button("submit", "uninstall", get_lang('uninstall_mods'));
$ft->end_form();
}
else
{
print_failure(get_lang('there_are_no_mods_installed_on_this_game_server'));
return;
}
}
}
else
{
print_failure(get_lang('workshop_configuration_file_has_bad_format'));
return;
}
}
else
{
print_failure(get_lang('game_home_not_found'));
return;
}
}
?>

View file

@ -0,0 +1,535 @@
<?php
/*
* GSP Steam Workshop: User mod management
* Copyright (C) 2025 WDS / GameServerPanel
*
* Accessible via: home.php?m=steam_workshop&p=user_mods&home_id=123
*
* 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
* (at your option) any later version.
*/
require_once __DIR__ . '/includes/functions.php';
function exec_ogp_module()
{
global $db;
echo '<h2>Steam Workshop Mod Manager</h2>';
$home_id = isset($_REQUEST['home_id']) ? (int)$_REQUEST['home_id'] : 0;
if (!$home_id) {
sw_error('No server selected. Please access this page from your game server manager.');
return;
}
// Ownership check
if (!sw_user_owns_home($db, (int)$_SESSION['user_id'], $home_id)) {
sw_error('Access denied. You do not own this server.');
return;
}
// Load server info
$home = sw_get_home_info($db, $home_id);
if (!$home) {
sw_error('Server not found.');
return;
}
// Find matching Workshop profile
$profile = sw_get_profile_for_home($db, $home_id);
if (!$profile) {
echo '<p>Steam Workshop is not enabled for this game type (<strong>'
. sw_h($home['game_name']) . '</strong>).</p>';
echo '<p>An administrator must enable Workshop support for this game under '
. '<em>Steam Workshop &rsaquo; Admin</em>.</p>';
return;
}
$action = $_POST['action'] ?? ($_GET['action'] ?? '');
// ── POST handlers ─────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
switch ($action) {
case 'add_mod':
sw_user_add_mod($db, $home_id, $profile);
break;
case 'save_mod':
sw_user_save_mod($db, $home_id);
break;
case 'delete_mod':
sw_user_delete_mod($db, $home_id);
break;
case 'toggle_mod':
sw_user_toggle_mod($db, $home_id);
break;
case 'move_up':
case 'move_down':
sw_user_reorder_mod($db, $home_id, $action);
break;
case 'queue_update':
sw_user_queue_update($db, $home_id);
break;
}
}
// ── Render page ───────────────────────────────────────────────────
sw_user_render($db, $home_id, $home, $profile);
}
// ─────────────────────────────────────────────────────────────────────────
// POST action handlers
// ─────────────────────────────────────────────────────────────────────────
function sw_user_add_mod($db, $home_id, array $profile)
{
$workshop_id = trim($_POST['workshop_id'] ?? '');
if (!preg_match('/^\d{1,20}$/', $workshop_id)) {
sw_error('Invalid Workshop ID must be a numeric Steam Workshop item ID.');
return;
}
// Prevent duplicates
$safe_wid = $db->realEscapeSingle($workshop_id);
$exists = $db->resultQuery(
"SELECT id FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
WHERE `home_id` = $home_id AND `workshop_id` = '$safe_wid' LIMIT 1"
);
if ($exists) {
sw_error("Workshop ID $workshop_id is already in the list.");
return;
}
// Determine next sort_order
$last = $db->resultQuery(
"SELECT MAX(`sort_order`) AS m FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
WHERE `home_id` = $home_id"
);
$sort = ($last && isset($last[0]['m'])) ? ((int)$last[0]['m'] + 1) : 0;
$mod_name = $db->realEscapeSingle(trim($_POST['mod_name'] ?? ''));
$mod_type = (($_POST['mod_type'] ?? 'client') === 'server') ? 'server' : 'client';
$profile_id = (int)$profile['id'];
// Auto-generate folder name from naming format template
$folder_name = sw_apply_template(
$profile['folder_naming_format'],
array(
'MOD_NAME' => !empty($mod_name) ? $mod_name : $workshop_id,
'WORKSHOP_ID' => $workshop_id,
'WORKSHOP_APP_ID'=> $profile['workshop_app_id'],
)
);
$safe_fname = $db->realEscapeSingle($folder_name);
$safe_mname = $mod_name; // already escaped above via realEscapeSingle
$ok = $db->query(
"INSERT INTO `OGP_DB_PREFIXsteam_workshop_server_mods`
(`home_id`, `profile_id`, `workshop_id`, `mod_name`, `folder_name`,
`mod_type`, `sort_order`, `enabled`, `install_status`, `created_at`)
VALUES ($home_id, $profile_id, '$safe_wid', '$safe_mname', '$safe_fname',
'$mod_type', $sort, 1, '', NOW())"
);
if ($ok) {
sw_success("Workshop mod $workshop_id added.");
} else {
sw_error('Failed to add mod.');
}
}
function sw_user_save_mod($db, $home_id)
{
$mod_id = (int)($_POST['mod_id'] ?? 0);
if (!$mod_id) {
return;
}
$mod = sw_get_mod_by_id($db, $mod_id);
if (!$mod || (int)$mod['home_id'] !== $home_id) {
sw_error('Mod not found or access denied.');
return;
}
$mod_name = $db->realEscapeSingle(trim($_POST['mod_name'] ?? ''));
$folder_name = $db->realEscapeSingle(trim($_POST['folder_name'] ?? ''));
$mod_type = (($_POST['mod_type'] ?? 'client') === 'server') ? 'server' : 'client';
if (empty($folder_name)) {
sw_error('Folder name cannot be empty.');
return;
}
$ok = $db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `mod_name` = '$mod_name',
`folder_name` = '$folder_name',
`mod_type` = '$mod_type',
`updated_at` = NOW()
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
);
if ($ok) {
sw_success('Mod updated.');
} else {
sw_error('Failed to update mod.');
}
}
function sw_user_delete_mod($db, $home_id)
{
$mod_id = (int)($_POST['mod_id'] ?? 0);
if (!$mod_id) {
return;
}
$mod = sw_get_mod_by_id($db, $mod_id);
if (!$mod || (int)$mod['home_id'] !== $home_id) {
sw_error('Mod not found or access denied.');
return;
}
$db->query(
"DELETE FROM `OGP_DB_PREFIXsteam_workshop_server_mods`
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
);
sw_success('Mod removed from list.');
}
function sw_user_toggle_mod($db, $home_id)
{
$mod_id = (int)($_POST['mod_id'] ?? 0);
if (!$mod_id) {
return;
}
$mod = sw_get_mod_by_id($db, $mod_id);
if (!$mod || (int)$mod['home_id'] !== $home_id) {
sw_error('Mod not found or access denied.');
return;
}
$new_state = $mod['enabled'] ? 0 : 1;
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `enabled` = $new_state, `updated_at` = NOW()
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
);
}
function sw_user_reorder_mod($db, $home_id, $direction)
{
$mod_id = (int)($_POST['mod_id'] ?? 0);
if (!$mod_id) {
return;
}
$mod = sw_get_mod_by_id($db, $mod_id);
if (!$mod || (int)$mod['home_id'] !== $home_id) {
return;
}
$mods = sw_get_server_mods($db, $home_id);
if (!$mods) {
return;
}
// Normalise sort_order to 0-based sequential integers
$sorted = array_values($mods);
foreach ($sorted as $idx => $m) {
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `sort_order` = $idx
WHERE `id` = " . (int)$m['id'] . " AND `home_id` = $home_id LIMIT 1"
);
}
// Find the position of the target mod
$pos = -1;
foreach ($sorted as $idx => $m) {
if ((int)$m['id'] === $mod_id) {
$pos = $idx;
break;
}
}
if ($pos < 0) {
return;
}
if ($direction === 'move_up' && $pos > 0) {
$swap_pos = $pos - 1;
} elseif ($direction === 'move_down' && $pos < (count($sorted) - 1)) {
$swap_pos = $pos + 1;
} else {
return; // already at boundary
}
$swap_id = (int)$sorted[$swap_pos]['id'];
// Swap sort_order values
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `sort_order` = $swap_pos
WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1"
);
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `sort_order` = $pos
WHERE `id` = $swap_id AND `home_id` = $home_id LIMIT 1"
);
}
function sw_user_queue_update($db, $home_id)
{
// Mark all enabled mods as 'queued' so the agent picks them up.
$db->query(
"UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods`
SET `install_status` = 'queued', `updated_at` = NOW()
WHERE `home_id` = $home_id AND `enabled` = 1"
);
sw_success('All enabled mods queued for update. Run the agent to process downloads.');
}
// ─────────────────────────────────────────────────────────────────────────
// Render
// ─────────────────────────────────────────────────────────────────────────
function sw_user_render($db, $home_id, array $home, array $profile)
{
$mods = sw_get_server_mods($db, $home_id) ?: array();
// Generate launch params from enabled mods
$enabled_mods = array_filter($mods, function ($m) {
return !empty($m['enabled']);
});
$params = sw_generate_launch_params(array_values($enabled_mods), $profile);
$base_url = 'home.php?m=steam_workshop&p=user_mods&home_id=' . $home_id;
?>
<p>
<strong>Server:</strong> <?= sw_h($home['home_name']) ?>
&nbsp;&nbsp;
<strong>Game:</strong> <?= sw_h($home['game_name']) ?>
&nbsp;&nbsp;
<strong>Workshop Profile:</strong> <?= sw_h($profile['config_name']) ?>
</p>
<!-- Add Mod form -->
<h3>Add Workshop Mod</h3>
<form method="post" action="<?= sw_h($base_url) ?>">
<input type="hidden" name="action" value="add_mod">
<table>
<tr>
<td style="padding:4px 8px;"><label for="workshop_id">Workshop ID</label></td>
<td style="padding:4px 8px;">
<input type="text" id="workshop_id" name="workshop_id" value=""
placeholder="e.g. 2863534533" style="width:180px;" required>
</td>
</tr>
<tr>
<td style="padding:4px 8px;"><label for="add_mod_name">Display Name (optional)</label></td>
<td style="padding:4px 8px;">
<input type="text" id="add_mod_name" name="mod_name" value=""
placeholder="e.g. CF" style="width:180px;">
</td>
</tr>
<tr>
<td style="padding:4px 8px;"><label for="add_mod_type">Mod Type</label></td>
<td style="padding:4px 8px;">
<select id="add_mod_type" name="mod_type">
<option value="client">Client mod (-mod=)</option>
<option value="server">Server-side only (-serverMod=)</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td style="padding:4px 8px;">
<button type="submit" class="button">Add Mod</button>
</td>
</tr>
</table>
</form>
<hr>
<!-- Mod list -->
<h3>Installed Mods (<?= count($mods) ?>)</h3>
<?php if (empty($mods)): ?>
<p>No mods added yet. Use the form above to add Workshop IDs.</p>
<?php else: ?>
<form method="post" action="<?= sw_h($base_url) ?>">
<table width="100%" style="border-collapse:collapse;">
<thead>
<tr style="background:#f0f0f0;">
<th style="padding:6px 8px;text-align:center;">#</th>
<th style="padding:6px 8px;text-align:left;">Workshop ID</th>
<th style="padding:6px 8px;text-align:left;">Mod Name</th>
<th style="padding:6px 8px;text-align:left;">Folder Name</th>
<th style="padding:6px 8px;text-align:center;">Type</th>
<th style="padding:6px 8px;text-align:center;">Enabled</th>
<th style="padding:6px 8px;text-align:center;">Status</th>
<th style="padding:6px 8px;text-align:center;">Order</th>
<th style="padding:6px 8px;text-align:center;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($mods as $idx => $mod): ?>
<tr style="border-bottom:1px solid #ddd;<?= !$mod['enabled'] ? 'opacity:0.55;' : '' ?>">
<td style="padding:6px 8px;text-align:center;"><?= $idx + 1 ?></td>
<td style="padding:6px 8px;font-family:monospace;"><?= sw_h($mod['workshop_id']) ?></td>
<!-- Inline edit: name -->
<td style="padding:6px 8px;">
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<input type="hidden" name="action" value="save_mod">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
<input type="hidden" name="folder_name" value="<?= sw_h($mod['folder_name']) ?>">
<input type="hidden" name="mod_type" value="<?= sw_h($mod['mod_type']) ?>">
<input type="text" name="mod_name" value="<?= sw_h($mod['mod_name']) ?>"
style="width:120px;" title="Click Save to apply">
</td>
<!-- Inline edit: folder name -->
<td style="padding:6px 8px;">
<input type="text" name="folder_name" value="<?= sw_h($mod['folder_name']) ?>"
style="width:140px;" title="Folder name inside server root">
</td>
<!-- Inline edit: mod type -->
<td style="padding:6px 8px;text-align:center;">
<select name="mod_type" style="width:100px;">
<option value="client" <?= $mod['mod_type'] === 'client' ? 'selected' : '' ?>>-mod=</option>
<option value="server" <?= $mod['mod_type'] === 'server' ? 'selected' : '' ?>>-serverMod=</option>
</select>
<button type="submit" class="button small" title="Save changes">Save</button>
</form>
</td>
<!-- Toggle enabled -->
<td style="padding:6px 8px;text-align:center;">
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<input type="hidden" name="action" value="toggle_mod">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
<button type="submit" class="button small"
style="<?= $mod['enabled'] ? 'background:#5cb85c;color:#fff;' : '' ?>"
title="<?= $mod['enabled'] ? 'Click to disable' : 'Click to enable' ?>">
<?= $mod['enabled'] ? 'On' : 'Off' ?>
</button>
</form>
</td>
<!-- Install status -->
<td style="padding:6px 8px;text-align:center;font-size:0.85em;">
<?php
$s = $mod['install_status'];
if ($s === 'installed') {
echo '<span style="color:green;">Installed</span>';
} elseif ($s === 'queued') {
echo '<span style="color:orange;">Queued</span>';
} elseif ($s === 'failed') {
echo '<span style="color:red;" title="' . sw_h($mod['last_error']) . '">Failed</span>';
} elseif ($s === 'updating') {
echo '<span style="color:blue;">Updating</span>';
} else {
echo '<span style="color:#999;">Not installed</span>';
}
?>
</td>
<!-- Order buttons -->
<td style="padding:6px 8px;text-align:center;white-space:nowrap;">
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<input type="hidden" name="action" value="move_up">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
<button type="submit" class="button small" <?= $idx === 0 ? 'disabled' : '' ?>>&#9650;</button>
</form>
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<input type="hidden" name="action" value="move_down">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
<button type="submit" class="button small"
<?= $idx === (count($mods) - 1) ? 'disabled' : '' ?>>&#9660;</button>
</form>
</td>
<!-- Delete -->
<td style="padding:6px 8px;text-align:center;">
<form method="post" action="<?= sw_h($base_url) ?>" style="display:inline;">
<input type="hidden" name="action" value="delete_mod">
<input type="hidden" name="mod_id" value="<?= (int)$mod['id'] ?>">
<button type="submit" class="button small danger"
onclick="return confirm('Remove this mod from the list?');"
style="background:#d9534f;color:#fff;">Remove</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</form>
<?php endif; ?>
<hr>
<!-- Launch params display -->
<h3>Generated Launch Parameters</h3>
<p style="color:#666;font-size:0.9em;">
Based on the enabled mods above (sorted by order). Copy these into your server startup command.
</p>
<?php if (empty($enabled_mods)): ?>
<p>No enabled mods launch parameters will be empty.</p>
<?php else: ?>
<?php if ($params['mod']): ?>
<p>
<strong>Client mods (<code>-mod=</code>):</strong><br>
<input type="text" value="<?= sw_h($params['mod']) ?>"
readonly style="width:100%;font-family:monospace;"
onclick="this.select();">
</p>
<?php endif; ?>
<?php if ($params['servermod']): ?>
<p>
<strong>Server-side mods (<code>-serverMod=</code>):</strong><br>
<input type="text" value="<?= sw_h($params['servermod']) ?>"
readonly style="width:100%;font-family:monospace;"
onclick="this.select();">
</p>
<?php endif; ?>
<p>
<strong>Combined:</strong><br>
<input type="text" value="<?= sw_h($params['combined']) ?>"
readonly style="width:100%;font-family:monospace;"
onclick="this.select();">
</p>
<?php endif; ?>
<hr>
<!-- Install/Update -->
<h3>Install / Update Mods</h3>
<p>
Clicking <strong>Queue Update</strong> marks all enabled mods as <em>queued</em>.
Then run the agent <strong>on the game server host</strong> (where SteamCMD and the game files are located)
to download and install the mods. Adjust the path to the panel's
<code>modules/steam_workshop/agent_update_workshop.php</code> for your server:
</p>
<pre style="background:#222;color:#eee;padding:10px 14px;border-radius:4px;overflow-x:auto;"
>php /path/to/panel/modules/steam_workshop/agent_update_workshop.php --home-id=<?= $home_id ?></pre>
<form method="post" action="<?= sw_h($base_url) ?>">
<input type="hidden" name="action" value="queue_update">
<button type="submit" class="button"
onclick="return confirm('Queue all enabled mods for update?');">
Queue Update for All Enabled Mods
</button>
</form>
<?php
}

View file

@ -1,220 +0,0 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array $gameRows */
/** @var array $adapterOptions */
/** @var array $adapters */
/** @var string $activeGameKey */
?>
<div class="sw-admin">
<div class="sw-admin__intro">
<h3><?php echo htmlspecialchars($lang['admin_heading_game_mapping'] ?? 'Game type adapter mapping'); ?></h3>
<p><?php echo htmlspecialchars($lang['admin_subheading_game_mapping'] ?? 'Assign an adapter and edit its XML without leaving the table.'); ?></p>
</div>
<form id="sw-mapping-form" method="post">
<input type="hidden" name="admin_action" value="save_mappings">
</form>
<div class="sw-game-table__wrapper">
<table class="table sw-game-table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['admin_col_game_key'] ?? 'Game key'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_adapter'] ?? 'Mapping'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_status'] ?? 'Adapter status'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_updated'] ?? 'Last updated'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($gameRows)): ?>
<tr>
<td colspan="5"><?php echo htmlspecialchars($lang['admin_no_game_keys'] ?? 'No Steam Workshop-enabled game definitions were detected.'); ?></td>
</tr>
<?php else: ?>
<?php foreach ((array)$gameRows as $row): ?>
<?php
$groupKey = $row['group_key'];
$primaryKey = $row['primary_game_key'];
$selectValue = $row['selected_adapter'] ?: ($row['exists'] ? $primaryKey : '');
$statusLabel = $row['exists']
? ($row['adapter']['name'] ?? $primaryKey)
: ($lang['status_no_adapter'] ?? 'No adapter');
$isOpen = ($activeGameKey !== '' && $activeGameKey === $primaryKey);
$formId = 'adapter-panel-' . preg_replace('/[^a-z0-9_-]/i', '', $groupKey);
$form = $row['form'];
?>
<tr class="sw-game-table__row">
<td>
<div class="sw-game-label">
<div class="sw-game-label__title">
<span class="sw-game-label__name"><?php echo htmlspecialchars($row['game_name']); ?></span>
<span class="sw-badge sw-badge--app">App ID <?php echo htmlspecialchars($row['app_id']); ?></span>
<?php if ($row['exists']): ?>
<span class="sw-badge sw-badge--custom"><?php echo htmlspecialchars($lang['badge_custom_xml'] ?? 'Custom config'); ?></span>
<?php endif; ?>
</div>
<div class="sw-game-variants">
<?php foreach ((array)$row['game_keys'] as $variantKey): ?>
<span class="sw-chip"><?php echo htmlspecialchars($variantKey); ?></span>
<?php endforeach; ?>
</div>
</div>
<small class="sw-game-label__hint"><?php echo htmlspecialchars($lang['admin_hint_inline_edit'] ?? 'Use the toggle to configure this game inline.'); ?></small>
</td>
<td>
<select form="sw-mapping-form" name="mapping[<?php echo htmlspecialchars($groupKey); ?>]">
<option value="">--</option>
<?php foreach ((array)$adapterOptions as $key => $label): ?>
<option value="<?php echo htmlspecialchars($key); ?>" <?php echo ($selectValue === $key) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($label); ?>
</option>
<?php endforeach; ?>
</select>
<?php if (!empty($row['mixed_mapping'])): ?>
<small class="sw-game-label__hint sw-game-label__hint--warning"><?php echo htmlspecialchars($lang['admin_hint_mixed_mapping'] ?? 'Different adapters assigned across variants. Saving will sync them.'); ?></small>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($statusLabel); ?></td>
<td>
<?php if (!empty($row['updated_at'])): ?>
<?php echo htmlspecialchars(date('Y-m-d H:i', (int)$row['updated_at'])); ?>
<?php else: ?>
&mdash;
<?php endif; ?>
</td>
<td class="sw-actions">
<button type="button" class="btn secondary js-toggle-adapter" data-target="<?php echo htmlspecialchars($formId); ?>" aria-expanded="<?php echo $isOpen ? 'true' : 'false'; ?>">
<?php echo htmlspecialchars($row['exists'] ? ($lang['button_edit_adapter'] ?? 'Edit') : ($lang['button_create_adapter'] ?? 'Create')); ?>
</button>
<?php if ($row['exists']): ?>
<form method="post" class="sw-inline-delete">
<input type="hidden" name="admin_action" value="delete_adapter">
<input type="hidden" name="game_key" value="<?php echo htmlspecialchars($primaryKey); ?>">
<button type="submit" class="btn danger" onclick="return confirm('<?php echo htmlspecialchars($lang['confirm_delete_adapter'] ?? 'Delete this game configuration?'); ?>');">
<?php echo htmlspecialchars($lang['button_delete_adapter'] ?? 'Delete'); ?>
</button>
</form>
<?php endif; ?>
</td>
</tr>
<tr id="<?php echo htmlspecialchars($formId); ?>" class="sw-game-table__form-row <?php echo $isOpen ? 'is-open' : ''; ?>">
<td colspan="5">
<form method="post" class="sw-form sw-inline-form">
<input type="hidden" name="admin_action" value="save_adapter">
<input type="hidden" name="game_key" value="<?php echo htmlspecialchars($form['game_key']); ?>">
<div class="sw-form__grid">
<label>
<?php echo htmlspecialchars($lang['label_game_key'] ?? 'Game key'); ?>
<input type="text" value="<?php echo htmlspecialchars($form['game_key']); ?>" readonly>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_name'] ?? 'Game display name'); ?>
<input type="text" name="adapter[name]" value="<?php echo htmlspecialchars($form['name']); ?>" required>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_app_id'] ?? 'Steam App ID'); ?>
<input type="text" name="adapter[steam_app_id]" value="<?php echo htmlspecialchars($form['steam_app_id']); ?>" required>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_mods_dir'] ?? 'Mods directory'); ?>
<input type="text" name="adapter[mods_dir]" value="<?php echo htmlspecialchars($form['mods_dir']); ?>" required>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_keys_dir'] ?? 'Keys directory (optional)'); ?>
<input type="text" name="adapter[keys_dir]" value="<?php echo htmlspecialchars($form['keys_dir']); ?>">
</label>
<label class="sw-checkbox">
<input type="checkbox" name="adapter[supports_hot_reload]" value="1" <?php echo !empty($form['supports_hot_reload']) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($lang['label_adapter_hot_reload'] ?? 'Supports hot reload'); ?></span>
</label>
</div>
<label>
<?php echo htmlspecialchars($lang['label_adapter_activation'] ?? 'Activation template'); ?>
<textarea name="adapter[activation_template]" rows="3"><?php echo htmlspecialchars($form['activation_template']); ?></textarea>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_notes'] ?? 'Notes'); ?>
<textarea name="adapter[notes]" rows="2"><?php echo htmlspecialchars($form['notes']); ?></textarea>
</label>
<div class="sw-form__actions">
<button class="btn primary" type="submit"><?php echo htmlspecialchars($lang['button_save_adapter'] ?? 'Save game configuration'); ?></button>
<button type="button" class="btn js-toggle-adapter" data-target="<?php echo htmlspecialchars($formId); ?>"><?php echo htmlspecialchars($lang['button_cancel'] ?? 'Cancel'); ?></button>
</div>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="sw-form__actions sw-admin__mapping-actions">
<button class="btn primary" type="submit" form="sw-mapping-form"><?php echo htmlspecialchars($lang['button_save']); ?></button>
</div>
<h3><?php echo htmlspecialchars($lang['admin_heading_adapters'] ?? 'Available game configurations'); ?></h3>
<table class="table sw-mods__table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['admin_col_key'] ?? 'Key'); ?></th>
<th><?php echo htmlspecialchars($lang['summary_adapter'] ?? 'Game'); ?></th>
<th>Steam App ID</th>
<th><?php echo htmlspecialchars($lang['admin_col_mods_dir'] ?? 'Mods Dir'); ?></th>
<th><?php echo htmlspecialchars($lang['summary_hot_reload']); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_notes'] ?? 'Notes'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$adapters as $adapter): ?>
<tr>
<td><?php echo htmlspecialchars($adapter['key']); ?></td>
<td><?php echo htmlspecialchars($adapter['name']); ?></td>
<td><?php echo htmlspecialchars($adapter['steam_app_id']); ?></td>
<td><?php echo htmlspecialchars($adapter['mods_dir']); ?></td>
<td><?php echo !empty($adapter['supports_hot_reload']) ? htmlspecialchars($lang['status_hot_reload']) : htmlspecialchars($lang['status_restart_required']); ?></td>
<td><?php echo htmlspecialchars($adapter['notes']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const toggleRow = function (targetId) {
const row = document.getElementById(targetId);
if (!row) {
return;
}
row.classList.toggle('is-open');
const expanded = row.classList.contains('is-open');
const toggleButtons = document.querySelectorAll('.js-toggle-adapter[data-target="' + targetId + '"]');
toggleButtons.forEach(btn => btn.setAttribute('aria-expanded', expanded ? 'true' : 'false'));
if (expanded) {
const focusable = row.querySelector('input:not([type="hidden"]), textarea, select');
if (focusable) {
focusable.focus();
}
}
};
document.querySelectorAll('.js-toggle-adapter').forEach(button => {
button.addEventListener('click', function () {
const targetId = button.getAttribute('data-target');
if (targetId) {
toggleRow(targetId);
}
});
});
});
</script>

View file

@ -1,336 +0,0 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array|null $profile existing row when editing, null when creating */
/** @var int $profileId */
$isEdit = $profileId > 0 && $profile !== null;
$heading = $isEdit
? sprintf($lang['config_heading_edit'] ?? 'Edit Workshop Configuration: %s', htmlspecialchars($profile['game_name'] ?? ''))
: ($lang['config_heading_create'] ?? 'Add Workshop Game Configuration');
/** Helper: return html-safe value from profile array (or default). */
$v = static function (string $key, array $p, string $default = ''): string {
return htmlspecialchars((string)($p[$key] ?? $default), ENT_QUOTES);
};
$osList = ['linux' => 'Linux', 'windows' => 'Windows'];
$currentOs = array_filter(explode(',', (string)($profile['supported_os'] ?? 'linux')));
$folderFormats = ['@%mod_name%' => '@%mod_name% (mod title)', '@%workshop_id%' => '@%workshop_id% (numeric ID)', 'custom' => 'Custom template'];
$curFolderFormat = (string)($profile['folder_naming_format'] ?? '@%workshop_id%');
$separatorList = ['semicolon' => 'Semicolon ( ; )', 'comma' => 'Comma ( , )', 'space' => 'Space ( )'];
$curSeparator = (string)($profile['mod_separator'] ?? 'semicolon');
$copyMethods = ['rsync' => 'rsync (Linux/Unix)', 'copy' => 'cp / basic copy', 'symlink' => 'Symlink (requires persistent cache path)'];
$curCopyMethod = (string)($profile['copy_method'] ?? 'rsync');
$loginModes = ['anonymous' => 'Anonymous (recommended for free mods)', 'account' => 'Configured account (paid games)'];
$curLoginMode = (string)($profile['steamcmd_login_mode'] ?? 'anonymous');
$tplVarNote = $lang['profile_template_vars'] ?? 'Variables: %home_id% %server_path% %steam_app_id% %workshop_app_id% %workshop_id% %mod_name% %install_name% %download_path% %source_path% %target_path% %keys_source_path% %keys_target_path% %steamcmd_path%';
?>
<div class="sw-admin sw-profile-form">
<h3><?php echo $heading; ?></h3>
<p><a href="?m=steam_workshop&p=workshop_admin">&larr; <?php echo htmlspecialchars($lang['config_back_list'] ?? 'Back to configurations'); ?></a></p>
<div class="sw-info-box">
<strong><?php echo htmlspecialchars($lang['config_steamcmd_heading'] ?? 'How mods are downloaded'); ?></strong>
<p><?php echo htmlspecialchars($lang['config_steamcmd_note'] ?? 'Workshop mods are downloaded using SteamCMD: +workshop_download_item <App ID> <Mod ID>. Configure the paths and scripts below to control how mods are installed for servers of this game type.'); ?></p>
<p><strong><?php echo htmlspecialchars($lang['profile_template_vars_heading'] ?? 'Template variables:'); ?></strong><br>
<code><?php echo htmlspecialchars($tplVarNote); ?></code></p>
</div>
<form method="post" action="?m=steam_workshop&p=workshop_admin" class="sw-form">
<input type="hidden" name="sw_action" value="profile_save">
<input type="hidden" name="profile_id" value="<?php echo $profileId; ?>">
<!-- Basic identification -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_basic'] ?? 'Basic identification'); ?></legend>
<div class="sw-form__grid sw-form__grid--3col">
<label>
<?php echo htmlspecialchars($lang['label_game_key'] ?? 'Game key'); ?> <em>*</em>
<small><?php echo htmlspecialchars($lang['config_hint_game_key'] ?? 'Short identifier matching the game XML key, e.g. dayz_linux'); ?></small>
<input type="text" name="game_key" value="<?php echo $v('game_key', $profile ?? []); ?>"
pattern="[A-Za-z0-9_\-.]+" required maxlength="100"
<?php echo $isEdit ? 'readonly' : ''; ?>>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_game_name'] ?? 'Game display name'); ?> <em>*</em>
<input type="text" name="game_name" value="<?php echo $v('game_name', $profile ?? []); ?>"
required maxlength="255">
</label>
<label class="sw-checkbox" style="align-self:end;padding-bottom:0.75rem;">
<input type="checkbox" name="enabled" value="1"
<?php echo ($profile['enabled'] ?? 1) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($lang['config_label_enabled'] ?? 'Profile enabled'); ?></span>
</label>
</div>
</fieldset>
<!-- Steam / SteamCMD settings -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_steam'] ?? 'Steam &amp; SteamCMD settings'); ?></legend>
<div class="sw-form__grid sw-form__grid--3col">
<label>
<?php echo htmlspecialchars($lang['profile_label_steam_app_id'] ?? 'Steam App ID'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_steam_app_id'] ?? 'The Steam game App ID (e.g. 221100 for DayZ). Used when Steam login is required.'); ?></small>
<input type="text" name="steam_app_id"
value="<?php echo $v('steam_app_id', $profile ?? []); ?>"
pattern="[0-9]*" maxlength="32">
</label>
<label>
<?php echo htmlspecialchars($lang['config_label_app_id'] ?? 'Workshop App ID'); ?> <em>*</em>
<small><?php echo htmlspecialchars($lang['config_hint_app_id'] ?? 'The App ID used with +workshop_download_item, e.g. 221100 for DayZ'); ?></small>
<input type="text" name="workshop_app_id"
value="<?php echo $v('workshop_app_id', $profile ?? []); ?>"
pattern="[0-9]+" required maxlength="32">
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_steamcmd_path'] ?? 'SteamCMD path on agent'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_steamcmd_path'] ?? 'Full path to steamcmd.sh on the remote agent. Leave blank to use the agent default (/home/gameserver/steamcmd/steamcmd.sh).'); ?></small>
<input type="text" name="steamcmd_path"
value="<?php echo $v('steamcmd_path', $profile ?? []); ?>"
placeholder="/home/gameserver/steamcmd/steamcmd.sh" maxlength="512">
</label>
</div>
<div class="sw-form__grid sw-form__grid--2col">
<label class="sw-checkbox">
<input type="checkbox" name="steam_login_required" value="1"
id="sw-login-required"
<?php echo !empty($profile['steam_login_required']) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($lang['profile_label_steam_login_required'] ?? 'Steam login required (game is not free / requires ownership)'); ?></span>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_steamcmd_login_mode'] ?? 'SteamCMD login mode'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_steamcmd_login_mode'] ?? 'Use anonymous for free Workshop mods. Use configured account for games requiring ownership.'); ?></small>
<select name="steamcmd_login_mode">
<?php foreach ($loginModes as $mVal => $mLabel): ?>
<option value="<?php echo $mVal; ?>" <?php echo $curLoginMode === $mVal ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($mLabel); ?>
</option>
<?php endforeach; ?>
</select>
</label>
</div>
<fieldset class="sw-form__os-group">
<legend><?php echo htmlspecialchars($lang['profile_label_os'] ?? 'Supported OS'); ?></legend>
<?php foreach ($osList as $osVal => $osLabel): ?>
<label class="sw-checkbox">
<input type="checkbox" name="supported_os[]" value="<?php echo $osVal; ?>"
<?php echo in_array($osVal, $currentOs, true) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($osLabel); ?></span>
</label>
<?php endforeach; ?>
</fieldset>
</fieldset>
<!-- Download & install paths -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_paths'] ?? 'Download &amp; install paths'); ?></legend>
<small class="sw-hint"><?php echo htmlspecialchars($tplVarNote); ?></small>
<label>
<?php echo htmlspecialchars($lang['profile_label_cache_path'] ?? 'Workshop download/cache path'); ?> <em>*</em>
<small><?php echo htmlspecialchars($lang['profile_hint_cache_path'] ?? 'Where SteamCMD stores downloaded mod content on the agent. E.g. /home/gameserver/steamcmd/steamapps/workshop/content/%workshop_app_id%/%workshop_id%'); ?></small>
<input type="text" name="cache_path_template"
value="<?php echo $v('cache_path_template', $profile ?? []); ?>" required>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_install_path'] ?? 'Server mod install root'); ?> <em>*</em>
<small><?php echo htmlspecialchars($lang['profile_hint_install_path'] ?? 'Base directory inside the server where mods are installed. E.g. %server_path%/mods/%install_name%'); ?></small>
<input type="text" name="install_path_template"
value="<?php echo $v('install_path_template', $profile ?? []); ?>" required>
</label>
</fieldset>
<!-- Mod folder naming -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_folder'] ?? 'Mod folder naming'); ?></legend>
<label>
<?php echo htmlspecialchars($lang['profile_label_folder_format'] ?? 'Folder naming format'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_folder_format'] ?? 'How each mod folder is named inside the install root.'); ?></small>
<select name="folder_naming_format" id="sw-folder-format">
<?php foreach ($folderFormats as $fVal => $fLabel): ?>
<option value="<?php echo $fVal; ?>" <?php echo $curFolderFormat === $fVal ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($fLabel); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<div id="sw-custom-folder-wrap" <?php echo $curFolderFormat !== 'custom' ? 'style="display:none"' : ''; ?>>
<label>
<?php echo htmlspecialchars($lang['profile_label_folder_name'] ?? 'Custom folder name template'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_folder_name'] ?? 'Use %workshop_id% or %mod_name%. E.g. @%workshop_id%'); ?></small>
<input type="text" name="folder_name_template"
value="<?php echo $v('folder_name_template', $profile ?? [], '@%workshop_id%'); ?>">
</label>
</div>
</fieldset>
<!-- Launch parameters -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_launch'] ?? 'Launch parameters'); ?></legend>
<div class="sw-form__grid sw-form__grid--2col">
<label>
<?php echo htmlspecialchars($lang['profile_label_mod_launch_param'] ?? 'Mod launch parameter format'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_mod_launch_param'] ?? 'How the full mod list is passed to the server start command. E.g. -mod=%mods%'); ?></small>
<input type="text" name="mod_launch_param"
value="<?php echo $v('mod_launch_param', $profile ?? []); ?>"
placeholder="-mod=%mods%" maxlength="512">
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_mod_separator'] ?? 'Mod separator'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_mod_separator'] ?? 'Character used to join multiple mod folder names in the launch parameter.'); ?></small>
<select name="mod_separator">
<?php foreach ($separatorList as $sVal => $sLabel): ?>
<option value="<?php echo $sVal; ?>" <?php echo $curSeparator === $sVal ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($sLabel); ?>
</option>
<?php endforeach; ?>
</select>
</label>
</div>
<label>
<?php echo htmlspecialchars($lang['profile_label_launch_tpl'] ?? 'Full launch parameter template (optional)'); ?>
<small><?php echo htmlspecialchars($lang['config_hint_launch_tpl'] ?? 'Complete launch parameter string appended to server start. Each mod folder name is joined with the separator above.'); ?></small>
<input type="text" name="launch_param_template"
value="<?php echo $v('launch_param_template', $profile ?? []); ?>">
</label>
</fieldset>
<!-- Copy / sync method -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['config_section_copy'] ?? 'Copy / sync method'); ?></legend>
<div class="sw-form__grid sw-form__grid--2col">
<label>
<?php echo htmlspecialchars($lang['profile_label_copy_method'] ?? 'Copy method'); ?>
<select name="copy_method">
<?php foreach ($copyMethods as $mVal => $mLabel): ?>
<option value="<?php echo $mVal; ?>" <?php echo $curCopyMethod === $mVal ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($mLabel); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="sw-checkbox" style="align-self:end;padding-bottom:0.5rem;">
<input type="checkbox" name="copy_keys" value="1"
id="sw-copy-keys"
<?php echo !empty($profile['copy_keys']) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($lang['profile_label_copy_keys'] ?? 'Copy mod keys (*.bikey) to server keys directory'); ?></span>
</label>
</div>
<div id="sw-key-paths-wrap" <?php echo empty($profile['copy_keys']) ? 'style="display:none"' : ''; ?>>
<div class="sw-form__grid sw-form__grid--2col">
<label>
<?php echo htmlspecialchars($lang['profile_label_key_source'] ?? 'Key source path'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_key_source'] ?? 'Path inside the mod cache where key files live. E.g. %source_path%/keys'); ?></small>
<input type="text" name="key_source_path"
value="<?php echo $v('key_source_path', $profile ?? []); ?>"
placeholder="%source_path%/keys">
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_key_dest'] ?? 'Key destination path'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_key_dest'] ?? 'Where keys are copied on the server. E.g. %server_path%/keys'); ?></small>
<input type="text" name="key_dest_path"
value="<?php echo $v('key_dest_path', $profile ?? []); ?>"
placeholder="%server_path%/keys">
</label>
</div>
</div>
</fieldset>
<!-- Bash scripts -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_scripts'] ?? 'Bash scripts'); ?></legend>
<div class="sw-info-box sw-info-box--compact">
<strong><?php echo htmlspecialchars($lang['profile_scripts_order'] ?? 'Execution order:'); ?></strong>
1. <?php echo htmlspecialchars($lang['profile_label_pre_script'] ?? 'Pre-update script'); ?> &rarr;
2. <?php echo htmlspecialchars($lang['profile_label_install_script'] ?? 'Per-mod install script'); ?> (<?php echo htmlspecialchars($lang['profile_scripts_per_mod'] ?? 'repeated for each mod'); ?>) &rarr;
3. <?php echo htmlspecialchars($lang['profile_label_post_script'] ?? 'Post-update script'); ?>
</div>
<label>
<?php echo htmlspecialchars($lang['profile_label_pre_script'] ?? 'Pre-update bash script'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_pre_script'] ?? 'Runs once before any mod is downloaded/installed. Variables: %home_id% %server_path% %workshop_app_id%'); ?></small>
<textarea name="pre_update_script" rows="4" class="sw-script-textarea"><?php echo $v('pre_update_script', $profile ?? []); ?></textarea>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_install_script'] ?? 'Per-mod install bash script'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_install_script'] ?? 'Runs once for each mod. All template variables listed above are available.'); ?></small>
<details class="sw-example-block">
<summary><?php echo htmlspecialchars($lang['profile_script_example_toggle'] ?? 'Show DayZ-style example'); ?></summary>
<pre class="sw-code-pre">mkdir -p "%target_path%"
rsync -a --delete "%source_path%/" "%target_path%/"
if [ -d "%source_path%/keys" ]; then
mkdir -p "%keys_target_path%"
cp -f "%source_path%/keys/"*.bikey "%keys_target_path%/" 2>/dev/null || true
fi</pre>
</details>
<textarea name="install_script" rows="8" class="sw-script-textarea"><?php echo $v('install_script', $profile ?? []); ?></textarea>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_post_script'] ?? 'Post-update bash script'); ?>
<small><?php echo htmlspecialchars($lang['profile_hint_post_script'] ?? 'Runs once after all mods have been installed. Variables: %home_id% %server_path% %workshop_app_id%'); ?></small>
<textarea name="post_update_script" rows="4" class="sw-script-textarea"><?php echo $v('post_update_script', $profile ?? []); ?></textarea>
</label>
</fieldset>
<!-- Options & validation -->
<fieldset>
<legend><?php echo htmlspecialchars($lang['profile_section_flags'] ?? 'Options &amp; validation'); ?></legend>
<label class="sw-checkbox">
<input type="checkbox" name="requires_restart" value="1"
<?php echo !empty($profile['requires_restart']) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($lang['profile_label_requires_restart'] ?? 'Server restart required after mod install or update'); ?></span>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_validation_notes'] ?? 'Validation notes / help text (shown to server owners)'); ?>
<textarea name="validation_notes" rows="3"><?php echo $v('validation_notes', $profile ?? []); ?></textarea>
</label>
<label>
<?php echo htmlspecialchars($lang['profile_label_config_tpl'] ?? 'Config file template (optional)'); ?>
<textarea name="config_file_template" rows="3"><?php echo $v('config_file_template', $profile ?? []); ?></textarea>
</label>
</fieldset>
<div class="sw-form__actions">
<button class="btn primary" type="submit">
<?php echo htmlspecialchars($lang['button_save'] ?? 'Save'); ?>
</button>
<a class="btn" href="?m=steam_workshop&p=workshop_admin">
<?php echo htmlspecialchars($lang['button_cancel'] ?? 'Cancel'); ?>
</a>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Show/hide custom folder template field
const formatSel = document.getElementById('sw-folder-format');
const customWrap = document.getElementById('sw-custom-folder-wrap');
if (formatSel && customWrap) {
formatSel.addEventListener('change', function () {
customWrap.style.display = this.value === 'custom' ? '' : 'none';
});
}
// Show/hide key path fields
const copyKeysChk = document.getElementById('sw-copy-keys');
const keyPathsWrap = document.getElementById('sw-key-paths-wrap');
if (copyKeysChk && keyPathsWrap) {
copyKeysChk.addEventListener('change', function () {
keyPathsWrap.style.display = this.checked ? '' : 'none';
});
}
});
</script>

View file

@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array[] $profiles */
?>
<div class="sw-admin sw-profiles">
<div class="sw-admin__intro">
<h3><?php echo htmlspecialchars($lang['config_heading_list'] ?? 'Workshop Game Configurations'); ?></h3>
<p><?php echo htmlspecialchars($lang['config_intro'] ?? 'One configuration per supported game. Each configuration controls how SteamCMD downloads and installs Workshop mods for servers of that game type.'); ?></p>
<a class="btn primary" href="?m=steam_workshop&p=workshop_admin&sw_action=config_form">
<?php echo htmlspecialchars($lang['config_btn_create'] ?? 'Add Game Configuration'); ?>
</a>
</div>
<?php if (empty($profiles)): ?>
<p class="sw-empty"><?php echo htmlspecialchars($lang['config_list_empty'] ?? 'No Workshop configurations defined yet. Add one for each game that supports Steam Workshop mods.'); ?></p>
<?php else: ?>
<table class="table sw-profiles__table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['profile_col_game'] ?? 'Game'); ?></th>
<th><?php echo htmlspecialchars($lang['profile_col_key'] ?? 'Game Key'); ?></th>
<th><?php echo htmlspecialchars($lang['profile_col_app_ids'] ?? 'App IDs'); ?></th>
<th><?php echo htmlspecialchars($lang['profile_col_login'] ?? 'Login'); ?></th>
<th><?php echo htmlspecialchars($lang['profile_col_method'] ?? 'Install Method'); ?></th>
<th><?php echo htmlspecialchars($lang['profile_col_restart'] ?? 'Restart?'); ?></th>
<th><?php echo htmlspecialchars($lang['profile_col_status'] ?? 'Status'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$profiles as $profile): ?>
<tr>
<td><?php echo htmlspecialchars($profile['game_name']); ?></td>
<td><code><?php echo htmlspecialchars($profile['game_key']); ?></code></td>
<td>
<small><?php echo htmlspecialchars($lang['profile_col_steam'] ?? 'Steam'); ?>:</small> <?php echo htmlspecialchars($profile['steam_app_id'] !== '' ? $profile['steam_app_id'] : '—'); ?><br>
<small><?php echo htmlspecialchars($lang['profile_col_workshop'] ?? 'Workshop'); ?>:</small> <?php echo htmlspecialchars($profile['workshop_app_id']); ?>
</td>
<td>
<?php echo htmlspecialchars($profile['steamcmd_login_mode'] ?? 'anonymous'); ?>
<?php if (!empty($profile['steam_login_required'])): ?>
<span class="sw-badge sw-badge--warning"><?php echo htmlspecialchars($lang['profile_badge_login_required'] ?? 'Login req.'); ?></span>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($profile['copy_method']); ?></td>
<td><?php echo $profile['requires_restart'] ? '&#10004;' : '&#10008;'; ?></td>
<td>
<?php if ($profile['enabled']): ?>
<span class="sw-badge sw-badge--enabled"><?php echo htmlspecialchars($lang['status_enabled'] ?? 'Enabled'); ?></span>
<?php else: ?>
<span class="sw-badge sw-badge--disabled"><?php echo htmlspecialchars($lang['status_disabled'] ?? 'Disabled'); ?></span>
<?php endif; ?>
</td>
<td class="sw-actions">
<a class="btn secondary"
href="?m=steam_workshop&p=workshop_admin&sw_action=config_form&profile_id=<?php echo (int)$profile['id']; ?>">
<?php echo htmlspecialchars($lang['button_edit'] ?? 'Edit'); ?>
</a>
<form method="post" action="?m=steam_workshop&p=workshop_admin" class="sw-inline-delete">
<input type="hidden" name="sw_action" value="profile_delete">
<input type="hidden" name="profile_id" value="<?php echo (int)$profile['id']; ?>">
<button type="submit" class="btn danger"
onclick="return confirm('<?php echo htmlspecialchars($lang['config_confirm_delete'] ?? 'Delete this Workshop configuration? Servers using it will no longer have Workshop mod support.'); ?>')">
<?php echo htmlspecialchars($lang['button_delete'] ?? 'Delete'); ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>

View file

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
/** @var array $home */
/** @var array $config */
/** @var array $lang */
/** @var array $adapterOptions */
/** @var string|null $appId */
$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'; ?>
<?php include __DIR__ . '/partials/mod_picker.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

@ -1,20 +0,0 @@
<?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 ((array)$records as $record): ?>
<?php $currentRecord = $record; include __DIR__ . '/partials/server_card.php'; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>

View file

@ -1,120 +0,0 @@
<?php
declare(strict_types=1);
/** @var array $home */
/** @var array $lang */
/** @var int $homeId */
/** @var string $query */
/** @var int $page */
/** @var int $perPage */
/** @var array $results */
/** @var string|null $error */
/** @var array|null $request */
/** @var string|null $requestSummary */
/** @var string|null $appId */
$homeName = htmlspecialchars($home['home_name'] ?? ('#' . $homeId), ENT_QUOTES, 'UTF-8');
$backUrl = '?m=gamemanager&p=game_monitor';
$requestSummaryText = $requestSummary ?? '';
?>
<div class="sw-monitor">
<p><a class="sw-monitor__back" href="<?php echo $backUrl; ?>">&larr; <?php echo htmlspecialchars($lang['simple_search_back'] ?? 'Back to Game Monitor'); ?></a></p>
<div class="sw-monitor__card">
<h3><?php echo htmlspecialchars($lang['simple_search_heading'] ?? 'Steam Workshop quick search'); ?></h3>
<p class="sw-monitor__intro">
<?php echo htmlspecialchars(sprintf($lang['simple_search_intro'] ?? 'Look up Workshop mods for %s and copy the IDs into your config.', $homeName)); ?>
</p>
<dl class="sw-monitor__meta">
<div>
<dt><?php echo htmlspecialchars($lang['simple_search_server'] ?? 'Server'); ?></dt>
<dd><?php echo $homeName; ?></dd>
</div>
<div>
<dt><?php echo htmlspecialchars($lang['simple_search_app'] ?? 'Steam App ID'); ?></dt>
<dd><?php echo $appId !== null ? htmlspecialchars($appId, ENT_QUOTES, 'UTF-8') : ($lang['simple_search_app_missing'] ?? 'Not configured'); ?></dd>
</div>
</dl>
<?php if ($appId === null): ?>
<div class="sw-monitor__alert sw-monitor__alert--error">
<?php echo htmlspecialchars($lang['simple_search_app_warning'] ?? 'This server is missing a Steam App ID. Ask an administrator to finish the Workshop adapter setup.'); ?>
</div>
<?php endif; ?>
<form method="get" class="sw-monitor__form">
<input type="hidden" name="m" value="steam_workshop" />
<input type="hidden" name="p" value="main" />
<input type="hidden" name="action" value="monitor_search" />
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>" />
<label>
<span><?php echo htmlspecialchars($lang['simple_search_label'] ?? 'Workshop keyword or ID'); ?></span>
<input type="text" name="q" value="<?php echo htmlspecialchars($query ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="<?php echo htmlspecialchars($lang['simple_search_placeholder'] ?? 'Example: basebuilding or 2289460122'); ?>" <?php echo $appId === null ? 'disabled' : ''; ?> />
</label>
<div class="sw-monitor__form-row">
<label>
<span><?php echo htmlspecialchars($lang['simple_search_page'] ?? 'Page'); ?></span>
<input type="number" min="1" name="page" value="<?php echo (int)$page; ?>" <?php echo $appId === null ? 'disabled' : ''; ?> />
</label>
<label>
<span><?php echo htmlspecialchars($lang['simple_search_per_page'] ?? 'Items per page'); ?></span>
<input type="number" min="1" max="100" name="per_page" value="<?php echo (int)$perPage; ?>" <?php echo $appId === null ? 'disabled' : ''; ?> />
</label>
</div>
<button type="submit" class="btn primary" <?php echo $appId === null ? 'disabled' : ''; ?>><?php echo htmlspecialchars($lang['simple_search_submit'] ?? 'Search Workshop'); ?></button>
</form>
<?php if ($requestSummaryText !== ''): ?>
<div class="sw-monitor__summary">
<strong><?php echo htmlspecialchars($lang['simple_search_request_label'] ?? 'Request summary'); ?>:</strong>
<div class="sw-monitor__summary-text"><?php echo htmlspecialchars($requestSummaryText); ?></div>
</div>
<?php endif; ?>
<?php if ($error !== null): ?>
<div class="sw-monitor__alert sw-monitor__alert--error">
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<?php if ($query !== ''): ?>
<?php if (!empty($results)): ?>
<div class="sw-monitor__results">
<h4><?php echo htmlspecialchars($lang['simple_search_results'] ?? 'Matching Workshop items'); ?></h4>
<p class="sw-monitor__hint"><?php echo htmlspecialchars($lang['simple_search_copy_hint'] ?? 'Copy the Workshop ID for each mod you want to add.'); ?></p>
<div class="sw-monitor__table-wrapper">
<table class="sw-monitor__table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['mods_header_id'] ?? 'Workshop ID'); ?></th>
<th><?php echo htmlspecialchars($lang['mods_header_label'] ?? 'Title'); ?></th>
<th><?php echo htmlspecialchars($lang['mods_header_source'] ?? 'Source'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$results as $item): ?>
<?php
$itemId = htmlspecialchars($item['id'] ?? '', ENT_QUOTES, 'UTF-8');
$label = htmlspecialchars($item['label'] ?? ('@' . $itemId), ENT_QUOTES, 'UTF-8');
$source = htmlspecialchars($item['source'] ?? 'search', ENT_QUOTES, 'UTF-8');
?>
<tr>
<td>
<code><?php echo $itemId; ?></code>
</td>
<td><?php echo $label; ?></td>
<td><?php echo $source; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php elseif ($error === null): ?>
<div class="sw-monitor__alert sw-monitor__alert--info">
<?php echo htmlspecialchars($lang['simple_search_empty'] ?? 'No Workshop items matched that search.'); ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>

View file

@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
/** @var array $formConfig */
/** @var array $adapterOptions */
/** @var array $lang */
/** @var bool $adapterLocked */
/** @var bool $isAdmin */
$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'];
$currentAdapterName = $adapterOptions[$formConfig['adapter_key']] ?? strtoupper($formConfig['adapter_key']);
?>
<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'] ?? 'Game type'); ?></span>
<?php if ($adapterLocked): ?>
<input type="text" value="<?php echo htmlspecialchars($currentAdapterName); ?>" disabled />
<small><?php echo htmlspecialchars($lang['adapter_locked_note'] ?? 'The game type for this server is managed by the administrator.'); ?></small>
<?php else: ?>
<select name="workshop[adapter_key]">
<?php foreach ((array)$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>
<?php endif; ?>
</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>
<input type="hidden" name="workshop[staging_dir]" value="<?php echo $stagingDir; ?>" />
<input type="hidden" name="workshop[install_strategy]" value="<?php echo htmlspecialchars($installStrategy); ?>" />
<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>
<input type="hidden" name="workshop[post_install_script]" value="<?php echo $postInstall; ?>" />
</div>
<?php if ($isAdmin): ?>
<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>
<?php else: ?>
<input type="hidden" name="workshop[raw_items]" value="<?php echo $rawDefinition; ?>" />
<?php endif; ?>

View file

@ -1,102 +0,0 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array $config */
/** @var array $home */
/** @var int $homeId */
/** @var string|null $appId */
$homeId = (int)($home['home_id'] ?? 0);
$scriptPath = (string)($_SERVER['PHP_SELF'] ?? '/index.php');
if ($scriptPath === '') {
$scriptPath = '/index.php';
}
if ($scriptPath[0] !== '/') {
$scriptPath = '/' . ltrim($scriptPath, '/');
}
$endpoint = sprintf('%s?m=steam_workshop&p=main&action=search&home_id=%d', $scriptPath, $homeId);
$steamBase = 'https://steamcommunity.com/workshop/browse/?appid=';
$steamAppIdParam = $appId ?? '';
$initialItems = [];
foreach ($config['workshop_items'] ?? [] as $item) {
if (!is_array($item)) {
continue;
}
$id = preg_replace('/[^0-9]/', '', (string)($item['id'] ?? ''));
if ($id === '') {
continue;
}
$initialItems[] = [
'id' => $id,
'label' => (string)($item['label'] ?? ('@' . $id)),
'author' => (string)($item['author'] ?? ''),
'preview_url' => (string)($item['preview_url'] ?? ''),
'enabled' => !empty($item['enabled']),
'source' => (string)($item['source'] ?? 'manual'),
];
}
$initialJson = htmlspecialchars(json_encode($initialItems, JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8');
$pickerId = 'sw-picker-' . $homeId;
$langAttrs = [
'add' => $lang['mod_picker_action_add'] ?? 'Add',
'remove' => $lang['mod_picker_action_remove'] ?? 'Remove',
'loading' => $lang['mod_picker_status_loading'] ?? 'Searching Steam Workshop…',
'error' => $lang['mod_picker_status_error'] ?? 'Unable to load workshop data.',
'empty' => $lang['mod_picker_results_empty'] ?? 'No results matched your search.',
'query' => $lang['mod_picker_status_need_query'] ?? 'Enter a Workshop ID or keyword.',
'sync' => $lang['mod_picker_toggle_label'] ?? 'Sync',
];
?>
<div class="sw-picker" id="<?php echo $pickerId; ?>" data-endpoint="<?php echo htmlspecialchars($endpoint, ENT_QUOTES, 'UTF-8'); ?>" data-detail-base="https://steamcommunity.com/sharedfiles/filedetails/?id="
<?php foreach ((array)$langAttrs as $key => $value): ?>data-lang-<?php echo $key; ?>="<?php echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); ?>" <?php endforeach; ?>>
<div class="sw-picker__header">
<h4><?php echo htmlspecialchars($lang['mod_picker_heading'] ?? 'Workshop library'); ?></h4>
<p class="sw-picker__hint"><?php echo htmlspecialchars($lang['mod_picker_hint'] ?? 'Search by Workshop ID or keyword to add mods.'); ?></p>
</div>
<div class="sw-picker__search js-sw-search-form" data-home-id="<?php echo $homeId; ?>" role="search">
<label>
<span><?php echo htmlspecialchars($lang['mod_picker_search_label'] ?? 'Search Steam Workshop'); ?></span>
<input type="text" class="sw-picker__search-input js-sw-search-input" placeholder="<?php echo htmlspecialchars($lang['mod_picker_search_placeholder'] ?? 'Example: 221100 or QoL'); ?>" />
</label>
<button type="button" class="btn secondary js-sw-search-button"><?php echo htmlspecialchars($lang['mod_picker_search_button'] ?? 'Search'); ?></button>
</div>
<div class="sw-picker__request-row">
<span class="sw-picker__request-label"><?php echo htmlspecialchars($lang['mod_picker_request_label'] ?? 'Submitting request'); ?></span>
<small class="sw-picker__request-hint"><?php echo htmlspecialchars($lang['mod_picker_request_hint'] ?? 'Exact URL preview. The field below mirrors your search text.'); ?></small>
<div class="sw-picker__request-line">
<?php $baseRequest = $steamAppIdParam !== '' ? $steamBase . $steamAppIdParam . '&browsesort=textsearch&section=readytouseitems&searchtext=' : ''; ?>
<code class="sw-picker__request-summary js-sw-request-summary" data-base="<?php echo htmlspecialchars($baseRequest, ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($baseRequest, ENT_QUOTES, 'UTF-8'); ?></code>
<input type="text" class="sw-picker__request-input js-sw-request-input" value="" readonly aria-label="<?php echo htmlspecialchars($lang['mod_picker_request_input_label'] ?? 'Workshop search text preview'); ?>" />
</div>
</div>
<div class="sw-picker__status js-sw-picker-status" role="status" aria-live="polite"></div>
<div class="sw-picker__selected">
<div class="sw-picker__selected-header">
<h5><?php echo htmlspecialchars($lang['mod_picker_selected_heading'] ?? 'Selected mods'); ?></h5>
<small><?php echo htmlspecialchars($lang['mod_picker_selected_hint'] ?? 'Checked mods will stay synced automatically.'); ?></small>
</div>
<div class="sw-picker__chip-list js-sw-selected-list" data-empty-text="<?php echo htmlspecialchars($lang['mod_picker_selected_empty'] ?? 'No mods selected yet.'); ?>"></div>
</div>
<div class="sw-picker__results">
<h5><?php echo htmlspecialchars($lang['mod_picker_results_heading'] ?? 'Search results'); ?></h5>
<p class="sw-picker__results-hint"><?php echo htmlspecialchars($lang['mod_picker_results_hint'] ?? 'Check the mods you want to add.'); ?></p>
<div class="sw-picker__results-table-wrapper">
<table class="sw-picker__results-table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['mod_picker_results_select'] ?? 'Select'); ?></th>
<th><?php echo htmlspecialchars($lang['mod_picker_results_title'] ?? 'Title'); ?></th>
<th><?php echo htmlspecialchars($lang['mod_picker_results_author'] ?? 'Author'); ?></th>
</tr>
</thead>
<tbody class="js-sw-results"></tbody>
</table>
</div>
</div>
<input type="hidden" name="workshop[selected_items]" value="<?php echo $initialJson; ?>" class="js-sw-selected-input" />
</div>

View file

@ -1,33 +0,0 @@
<?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 ((array)$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

@ -1,63 +0,0 @@
<?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((array)$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>

View file

@ -1,65 +0,0 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array[] $records each: {home, profile, mods} */
/** @var bool $isAdmin */
?>
<div class="sw-user sw-ws-index">
<h3><?php echo htmlspecialchars($lang['user_workshop_heading'] ?? 'Steam Workshop'); ?></h3>
<?php if (empty($records)): ?>
<p class="sw-empty">
<?php echo htmlspecialchars($isAdmin ? ($lang['empty_state_admin'] ?? 'No game homes assigned.') : ($lang['empty_state_user'] ?? 'No servers available.')); ?>
</p>
<?php else: ?>
<table class="table sw-ws-index__table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['col_server'] ?? 'Server'); ?></th>
<th><?php echo htmlspecialchars($lang['col_game'] ?? 'Game'); ?></th>
<th><?php echo htmlspecialchars($lang['col_mods_count'] ?? 'Installed mods'); ?></th>
<th><?php echo htmlspecialchars($lang['col_profile'] ?? 'Profile'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$records as $record): ?>
<?php
$home = $record['home'];
$profile = $record['profile'];
$mods = $record['mods'];
$homeId = (int)($home['home_id'] ?? 0);
?>
<tr>
<td><?php echo htmlspecialchars($home['home_name'] ?? ('#' . $homeId)); ?></td>
<td><?php echo htmlspecialchars($home['game_key'] ?? ''); ?></td>
<td><?php echo count((array)$mods); ?></td>
<td>
<?php if ($profile !== null): ?>
<span class="sw-badge sw-badge--enabled">
<?php echo htmlspecialchars($profile['game_name']); ?>
</span>
<?php else: ?>
<span class="sw-badge sw-badge--disabled">
<?php echo htmlspecialchars($lang['no_profile'] ?? 'No profile'); ?>
</span>
<?php endif; ?>
</td>
<td class="sw-actions">
<?php if ($profile !== null): ?>
<a class="btn secondary"
href="?m=steam_workshop&p=main&action=mods&home_id=<?php echo $homeId; ?>">
<?php echo htmlspecialchars($lang['btn_manage_mods'] ?? 'Manage Mods'); ?>
</a>
<?php else: ?>
<span class="sw-hint">
<?php echo htmlspecialchars($lang['hint_no_profile'] ?? 'Ask an admin to create a Workshop profile for this game.'); ?>
</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>

View file

@ -1,351 +0,0 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array $home */
/** @var int $homeId */
/** @var array|null $profile */
/** @var string|null $appId */
/** @var array[] $installedMods */
/** @var array[] $availableMods */
/** @var array $serverSettings */
/** @var array[] $allProfiles */
/** @var bool $isAdmin */
$homeName = htmlspecialchars($home['home_name'] ?? ('#' . $homeId));
$baseAction = '?m=steam_workshop&p=main';
$wsEnabled = !empty($serverSettings['workshop_enabled']);
$curProfileId = (int)($serverSettings['profile_id'] ?? 0);
$updateMode = (string)($serverSettings['update_mode'] ?? 'manual');
$restartBehav = (string)($serverSettings['restart_behavior'] ?? 'none');
$lastStatus = (string)($serverSettings['last_update_status'] ?? '');
$lastError = (string)($serverSettings['last_update_error'] ?? '');
$lastUpdateTime = (string)($serverSettings['last_update_time'] ?? '');
$lastSuccess = (string)($serverSettings['last_success_time'] ?? '');
$updateQueued = !empty($serverSettings['update_queued']);
$updateModes = [
'manual' => $lang['update_mode_manual'] ?? 'Manual only',
'scheduled' => $lang['update_mode_scheduled'] ?? 'Scheduled',
'on_restart' => $lang['update_mode_on_restart'] ?? 'Before server restart',
];
$restartBehaviors = [
'none' => $lang['restart_behavior_none'] ?? 'No restart',
'queue' => $lang['restart_behavior_queue'] ?? 'Queue restart',
'stop_update_start'=> $lang['restart_behavior_stop'] ?? 'Stop / Update / Start',
];
$statusClass = match($lastStatus) {
'success' => 'sw-badge--enabled',
'failed' => 'sw-badge--danger',
'running' => 'sw-badge--info',
'pending' => 'sw-badge--warning',
default => '',
};
?>
<div class="sw-user sw-ws-mods">
<p><a href="<?php echo $baseAction; ?>">&larr; <?php echo htmlspecialchars($lang['button_cancel'] ?? 'Back'); ?></a></p>
<h3><?php echo sprintf(htmlspecialchars($lang['user_workshop_server_heading'] ?? 'Workshop Mods %s'), $homeName); ?></h3>
<!-- ── Workshop server settings ── -->
<section class="sw-server-settings">
<h4><?php echo htmlspecialchars($lang['heading_server_settings'] ?? 'Workshop Settings for this server'); ?></h4>
<form method="post" action="<?php echo $baseAction; ?>" class="sw-form sw-settings-form">
<input type="hidden" name="ws_action" value="save_settings">
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
<div class="sw-form__grid sw-form__grid--2col">
<label class="sw-checkbox">
<input type="checkbox" name="workshop_enabled" value="1" id="sw-ws-enabled"
<?php echo $wsEnabled ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($lang['label_workshop_enabled'] ?? 'Enable Workshop for this server'); ?></span>
</label>
<label>
<?php echo htmlspecialchars($lang['label_select_profile'] ?? 'Workshop game profile'); ?>
<select name="profile_id">
<option value="0">-- <?php echo htmlspecialchars($lang['label_auto_detect'] ?? 'Auto-detect from game type'); ?> --</option>
<?php foreach ((array)$allProfiles as $p): ?>
<option value="<?php echo (int)$p['id']; ?>"
<?php echo $curProfileId === (int)$p['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($p['game_name'] . ' (' . $p['workshop_app_id'] . ')'); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label>
<?php echo htmlspecialchars($lang['label_update_mode'] ?? 'Update mode'); ?>
<select name="update_mode">
<?php foreach ($updateModes as $mVal => $mLabel): ?>
<option value="<?php echo $mVal; ?>" <?php echo $updateMode === $mVal ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($mLabel); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label>
<?php echo htmlspecialchars($lang['label_restart_behavior'] ?? 'Restart behavior'); ?>
<select name="restart_behavior">
<?php foreach ($restartBehaviors as $rVal => $rLabel): ?>
<option value="<?php echo $rVal; ?>" <?php echo $restartBehav === $rVal ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($rLabel); ?>
</option>
<?php endforeach; ?>
</select>
</label>
</div>
<div class="sw-form__actions">
<button type="submit" class="btn primary">
<?php echo htmlspecialchars($lang['button_save'] ?? 'Save'); ?>
</button>
</div>
</form>
<!-- Update status summary -->
<div class="sw-update-status">
<dl class="sw-status-grid">
<?php if ($lastStatus !== ''): ?>
<dt><?php echo htmlspecialchars($lang['label_last_update_status'] ?? 'Last update status'); ?></dt>
<dd><span class="sw-badge <?php echo $statusClass; ?>"><?php echo htmlspecialchars($lastStatus); ?></span></dd>
<?php endif; ?>
<?php if ($lastUpdateTime !== ''): ?>
<dt><?php echo htmlspecialchars($lang['label_last_update_time'] ?? 'Last update time'); ?></dt>
<dd><?php echo htmlspecialchars($lastUpdateTime); ?></dd>
<?php endif; ?>
<?php if ($lastSuccess !== ''): ?>
<dt><?php echo htmlspecialchars($lang['label_last_success_time'] ?? 'Last successful update'); ?></dt>
<dd><?php echo htmlspecialchars($lastSuccess); ?></dd>
<?php endif; ?>
<?php if ($lastError !== ''): ?>
<dt><?php echo htmlspecialchars($lang['label_last_update_error'] ?? 'Last error'); ?></dt>
<dd class="sw-error-text"><code><?php echo htmlspecialchars($lastError); ?></code></dd>
<?php endif; ?>
</dl>
<?php if ($updateQueued): ?>
<p class="sw-notice sw-notice--info">
<?php echo htmlspecialchars($lang['update_queued_notice'] ?? 'A manual update is queued and will run on the next scheduler cycle.'); ?>
</p>
<?php endif; ?>
<!-- Queue manual update -->
<form method="post" action="<?php echo $baseAction; ?>" class="sw-inline">
<input type="hidden" name="ws_action" value="queue_update">
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
<button type="submit" class="btn secondary"
<?php echo !$wsEnabled ? 'disabled title="Enable Workshop for this server first."' : ''; ?>>
<?php echo htmlspecialchars($lang['btn_queue_update'] ?? 'Queue manual update'); ?>
</button>
</form>
</div>
</section>
<?php if ($profile === null): ?>
<div class="sw-notice">
<p><?php echo htmlspecialchars($lang['no_profile_notice'] ?? 'No Workshop profile is configured for this game. An administrator needs to create one first.'); ?></p>
</div>
<?php else: ?>
<?php if (!empty($profile['validation_notes'])): ?>
<div class="sw-notice sw-notice--info">
<strong><?php echo htmlspecialchars($lang['label_admin_notes'] ?? 'Admin notes:'); ?></strong>
<?php echo htmlspecialchars($profile['validation_notes']); ?>
</div>
<?php endif; ?>
<!-- ── Installed mods table ── -->
<h4><?php echo htmlspecialchars($lang['heading_installed_mods'] ?? 'Installed Mods'); ?></h4>
<?php if (empty($installedMods)): ?>
<p class="sw-empty"><?php echo htmlspecialchars($lang['no_installed_mods'] ?? 'No mods installed yet.'); ?></p>
<?php else: ?>
<table class="table sw-ws-mods__table" id="sw-installed-<?php echo $homeId; ?>">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['col_mod_id'] ?? 'Workshop ID'); ?></th>
<th><?php echo htmlspecialchars($lang['col_mod_title'] ?? 'Title'); ?></th>
<th><?php echo htmlspecialchars($lang['col_mod_folder'] ?? 'Install folder'); ?></th>
<th><?php echo htmlspecialchars($lang['mods_header_enabled'] ?? 'Enabled'); ?></th>
<th><?php echo htmlspecialchars($lang['col_load_order'] ?? 'Load order'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$installedMods as $mod): ?>
<?php $wid = htmlspecialchars($mod['workshop_id']); ?>
<tr data-workshop-id="<?php echo $wid; ?>">
<td>
<a href="https://steamcommunity.com/sharedfiles/filedetails/?id=<?php echo $wid; ?>"
target="_blank" rel="noopener"><?php echo $wid; ?></a>
</td>
<td><?php echo htmlspecialchars($mod['title'] ?? $mod['workshop_id']); ?></td>
<td><code><?php echo htmlspecialchars($mod['custom_folder'] !== '' ? $mod['custom_folder'] : ($mod['install_path'] ?? '')); ?></code></td>
<td>
<form method="post" action="<?php echo $baseAction; ?>" class="sw-toggle-form">
<input type="hidden" name="ws_action" value="toggle">
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
<input type="hidden" name="workshop_id" value="<?php echo $wid; ?>">
<label class="sw-toggle">
<input type="checkbox" name="enabled" value="1"
class="js-ws-toggle"
<?php echo !empty($mod['enabled']) ? 'checked' : ''; ?>>
<span><?php echo !empty($mod['enabled']) ? htmlspecialchars($lang['status_enabled'] ?? 'Yes') : htmlspecialchars($lang['status_disabled'] ?? 'No'); ?></span>
</label>
</form>
</td>
<td>
<form method="post" action="<?php echo $baseAction; ?>" class="sw-order-form">
<input type="hidden" name="ws_action" value="load_order">
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
<input type="hidden" name="workshop_id" value="<?php echo $wid; ?>">
<input type="number" name="load_order"
value="<?php echo (int)$mod['load_order']; ?>"
min="0" max="9999" class="sw-order-input js-ws-order"
style="width:5em">
</form>
</td>
<td class="sw-actions">
<!-- Sync now -->
<form method="post" action="<?php echo $baseAction; ?>" class="sw-inline">
<input type="hidden" name="ws_action" value="sync">
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
<input type="hidden" name="workshop_id" value="<?php echo $wid; ?>">
<button type="submit" class="btn secondary">
<?php echo htmlspecialchars($lang['btn_sync_now'] ?? 'Sync now'); ?>
</button>
</form>
<!-- Remove -->
<form method="post" action="<?php echo $baseAction; ?>" class="sw-inline">
<input type="hidden" name="ws_action" value="remove">
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
<input type="hidden" name="workshop_id" value="<?php echo $wid; ?>">
<button type="submit" class="btn danger"
onclick="return confirm('<?php echo htmlspecialchars($lang['confirm_remove_mod'] ?? 'Remove this mod?'); ?>')">
<?php echo htmlspecialchars($lang['btn_remove_mod'] ?? 'Remove'); ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<!-- ── Available cached mods ── -->
<?php if (!empty($availableMods)): ?>
<h4><?php echo htmlspecialchars($lang['heading_cached_mods'] ?? 'Available Cached Mods (this agent)'); ?></h4>
<table class="table sw-ws-mods__cache-table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['col_mod_id'] ?? 'Workshop ID'); ?></th>
<th><?php echo htmlspecialchars($lang['col_mod_title'] ?? 'Title'); ?></th>
<th><?php echo htmlspecialchars($lang['col_cache_status'] ?? 'Cache status'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$availableMods as $cached): ?>
<?php $cid = htmlspecialchars($cached['workshop_id']); ?>
<tr>
<td><?php echo $cid; ?></td>
<td><?php echo htmlspecialchars($cached['title'] ?? $cached['workshop_id']); ?></td>
<td><?php echo htmlspecialchars($cached['status']); ?></td>
<td>
<form method="post" action="<?php echo $baseAction; ?>">
<input type="hidden" name="ws_action" value="install">
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
<input type="hidden" name="workshop_id" value="<?php echo $cid; ?>">
<button type="submit" class="btn secondary">
<?php echo htmlspecialchars($lang['btn_install_mod'] ?? 'Install'); ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<!-- ── Install by Workshop ID ── -->
<h4><?php echo htmlspecialchars($lang['heading_install_mod'] ?? 'Install Mod by Workshop ID'); ?></h4>
<form method="post" action="<?php echo $baseAction; ?>" class="sw-form sw-install-form">
<input type="hidden" name="ws_action" value="install">
<input type="hidden" name="home_id" value="<?php echo $homeId; ?>">
<div class="sw-form__row">
<label>
<?php echo htmlspecialchars($lang['label_workshop_id_input'] ?? 'Workshop ID'); ?>
<input type="text" name="workshop_id" pattern="[0-9]+" required
placeholder="<?php echo htmlspecialchars($lang['placeholder_workshop_id'] ?? 'e.g. 1234567890'); ?>">
</label>
<button type="submit" class="btn primary">
<?php echo htmlspecialchars($lang['btn_install_mod'] ?? 'Install'); ?>
</button>
</div>
</form>
<!-- ── Steam Workshop search widget ── -->
<?php
$requestPath = (string)($_SERVER['PHP_SELF'] ?? '/index.php');
$searchEndpoint = sprintf('%s?m=steam_workshop&p=main&action=search&home_id=%d', $requestPath, $homeId);
$langAttrs = [
'add' => $lang['mod_picker_action_add'] ?? 'Add',
'remove' => $lang['mod_picker_action_remove'] ?? 'Remove',
'loading' => $lang['mod_picker_status_loading'] ?? 'Searching…',
'error' => $lang['mod_picker_status_error'] ?? 'Search failed.',
'empty' => $lang['mod_picker_results_empty'] ?? 'No results.',
'query' => $lang['mod_picker_status_need_query'] ?? 'Enter a query.',
'sync' => $lang['mod_picker_toggle_label'] ?? 'Sync',
];
?>
<div class="sw-picker" id="sw-picker-ws-<?php echo $homeId; ?>"
data-endpoint="<?php echo htmlspecialchars($searchEndpoint, ENT_QUOTES); ?>"
data-detail-base="https://steamcommunity.com/sharedfiles/filedetails/?id="
data-install-action="<?php echo $baseAction; ?>"
data-home-id="<?php echo $homeId; ?>"
<?php foreach ((array)$langAttrs as $lk => $lv): ?>data-lang-<?php echo $lk; ?>="<?php echo htmlspecialchars($lv, ENT_QUOTES); ?>" <?php endforeach; ?>>
<div class="sw-picker__header">
<h5><?php echo htmlspecialchars($lang['mod_picker_heading'] ?? 'Search Steam Workshop'); ?></h5>
</div>
<div class="sw-picker__search js-sw-search-form" role="search">
<label>
<span><?php echo htmlspecialchars($lang['mod_picker_search_label'] ?? 'Search'); ?></span>
<input type="text" class="sw-picker__search-input js-sw-search-input"
placeholder="<?php echo htmlspecialchars($lang['mod_picker_search_placeholder'] ?? 'ID or keyword'); ?>">
</label>
<button type="button" class="btn secondary js-sw-search-button">
<?php echo htmlspecialchars($lang['mod_picker_search_button'] ?? 'Search'); ?>
</button>
</div>
<div class="sw-picker__status js-sw-picker-status" role="status" aria-live="polite"></div>
<div class="sw-picker__results">
<table class="sw-picker__results-table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['col_mod_id'] ?? 'ID'); ?></th>
<th><?php echo htmlspecialchars($lang['col_mod_title'] ?? 'Title'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Action'); ?></th>
</tr>
</thead>
<tbody class="js-sw-results"></tbody>
</table>
</div>
</div>
<?php endif; // profile !== null ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Toggle enable/disable: submit the parent form immediately on change
document.querySelectorAll('.js-ws-toggle').forEach(function (cb) {
cb.addEventListener('change', function () { cb.closest('form').submit(); });
});
// Load order: submit on change
document.querySelectorAll('.js-ws-order').forEach(function (inp) {
inp.addEventListener('change', function () { inp.closest('form').submit(); });
});
});
</script>

View file

@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/controllers/WorkshopProfileController.php';
function exec_ogp_module(): void
{
global $db;
echo '<h2>' . get_lang('steam_workshop') . '</h2>';
$controller = new WorkshopProfileController($db);
$controller->handle();
}