Merge pull request #120 from GameServerPanel/copilot/delete-old-steam-workshop-implementation
This commit is contained in:
commit
8c7c63bb72
82 changed files with 2068 additions and 14237 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
424
modules/steam_workshop/admin.php
Normal file
424
modules/steam_workshop/admin.php
Normal 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">« Back to profile list</a></p>
|
||||
|
||||
<h3>Edit Profile: <?= sw_h($profile['config_name']) ?> – <?= 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>
|
||||
<code>{SERVER_ID}</code>
|
||||
<code>{REMOTE_SERVER_ID}</code>
|
||||
<code>{GAME_NAME}</code>
|
||||
<code>{CONFIG_NAME}</code>
|
||||
<code>{WORKSHOP_ID}</code>
|
||||
<code>{MOD_NAME}</code>
|
||||
<code>{FOLDER_NAME}</code>
|
||||
<code>{STEAM_APP_ID}</code>
|
||||
<code>{WORKSHOP_APP_ID}</code>
|
||||
<code>{STEAMCMD_PATH}</code>
|
||||
<code>{WORKSHOP_DOWNLOAD_DIR}</code>
|
||||
<code>{SERVER_ROOT}</code>
|
||||
<code>{INSTALL_PATH}</code>
|
||||
<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 & 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>
|
||||
|
||||
<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
|
||||
}
|
||||
572
modules/steam_workshop/agent_update_workshop.php
Normal file
572
modules/steam_workshop/agent_update_workshop.php
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -1 +0,0 @@
|
|||
{}
|
||||
|
|
@ -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");
|
||||
}
|
||||
?>
|
||||
|
|
@ -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 "%mods_full_path%/steamapps/workshop/content/221100/%workshop_mod_id%" "%mods_full_path%/%workshop_mod_id%"
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/221100/%workshop_mod_id%"
|
||||
</post_install>
|
||||
<uninstall>printf "\nUninstalling...\n"
|
||||
rm -Rf "%mods_full_path%/%workshop_mod_id%"</uninstall>
|
||||
</workshop_settings>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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 "=" </post_install>
|
||||
<uninstall>a=%mods_full_path%
|
||||
|
||||
modid=$(find $a -iname "%mod_string%")
|
||||
|
||||
modfolder=$(dirname $modid)
|
||||
echo $modfolder
|
||||
rm -Rf $modfolder
|
||||
printf "\n %mod_string% automatically uninstall. \n" </uninstall>
|
||||
</workshop_settings>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||
|
|
@ -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'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 's/<Mods \/>/<Mods>\n<\/Mods>/' Sandbox_config.sbc
|
||||
|
||||
sed -i "s/<Mods>/<Mods>\n<ModItem>\n<Name>$modID.sbm<\/Name>\n<PublishedFileId>$modID<\/PublishedFileId>\n<\/Moditem>\n/" Sandbox_config.sbc</post_install>
|
||||
<uninstall />
|
||||
</workshop_settings>
|
||||
|
|
@ -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 "\nMoving item %workshop_mod_id% ..."
|
||||
cp -Rf "%mods_full_path%/steamapps/workshop/content/228380/%workshop_mod_id%" "%mods_full_path%/."
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/228380/%workshop_mod_id%"
|
||||
printf "\nSuccess."</post_install>
|
||||
<uninstall>printf "\nUninstalling item %mod_string% ...\n"
|
||||
rm -Rf "%mods_full_path%/%mod_string%"
|
||||
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
|
|
@ -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 "%mods_full_path%/steamapps/workshop/content/4000/%workshop_mod_id%"
|
||||
cp -f %first_file% myfile.gma
|
||||
7z x myfile.gma -aoa > /dev/null 2>&1
|
||||
cp -f myfile content.gma
|
||||
rm -f myfile.gma myfile
|
||||
"%mods_full_path%/../bin/gmad_linux" content.gma > /dev/null 2>&1
|
||||
rm -f "content.gma"
|
||||
cd content
|
||||
zip -r "%mods_full_path%/steamapps/workshop/content/4000/%first_file%.zip" * > /dev/null 2>&1
|
||||
cd ../..
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/4000/%workshop_mod_id%"
|
||||
unzip -Z1 "%first_file%.zip" > "%first_file%.list"
|
||||
tac "%first_file%.list" > "%first_file%.listinv"
|
||||
cp -f "%first_file%.listinv" "%first_file%.list"
|
||||
rm "%first_file%.listinv"
|
||||
unzip -o "%first_file%.zip" -d "%mods_full_path%/../garrysmod" > /dev/null 2>&1
|
||||
rm -f "%first_file%.zip"
|
||||
|
||||
if [ -f "%mods_full_path%/steamapps/workshop/content/4000/%first_file%.list" ];then
|
||||
cd "%mods_full_path%/../garrysmod"
|
||||
luaFile="%mods_full_path%/../garrysmod/lua/autorun/server/resources.lua"
|
||||
while read p; do
|
||||
if [ -f "$p" ] && [ ! -d "$p" ]; then
|
||||
filename=$(basename -- "$p")
|
||||
extension="${filename##*.}"
|
||||
if [ "$extension" != "bsp" ] && [ "$extension" != "png" ]; then
|
||||
newstring="resource.AddSingleFile(\"$p\")"
|
||||
if ! grep -Fxq "$newstring" "$luaFile"; then
|
||||
echo "$newstring" >> "$luaFile"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done <"%mods_full_path%/steamapps/workshop/content/4000/%first_file%.list"
|
||||
printf "\nContents of %first_file% successfully installed!"
|
||||
else
|
||||
printf "\nFile listing not found, try it again after reinstalling the mod."
|
||||
fi
|
||||
|
||||
</post_install>
|
||||
<uninstall>if [ -f "%mods_full_path%/steamapps/workshop/content/4000/%mod_string%.list" ];then
|
||||
cd "%mods_full_path%/../garrysmod"
|
||||
luaFile="%mods_full_path%/../garrysmod/lua/autorun/server/resources.lua"
|
||||
while read p; do
|
||||
if [ -d "$p" ]; then
|
||||
if [ -z "$(ls -A "$p")" ]; then
|
||||
rm -vRf "$p"
|
||||
fi
|
||||
else
|
||||
if [ -f "$p" ]; then
|
||||
rm -vf "$p"
|
||||
filestring="resource.AddSingleFile(\"$p\")"
|
||||
if grep -Fxq "$filestring" "$luaFile"; then
|
||||
escaped_filestring=$(sed -e 's/[]\/$*.^[]/\\&/g' <<< $filestring)
|
||||
sed -i "/$escaped_filestring/d" "$luaFile"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done <"%mods_full_path%/steamapps/workshop/content/4000/%mod_string%.list"
|
||||
printf "\nContents of %mod_string% successfully uninstalled!"
|
||||
else
|
||||
printf "\nFile listing not found, try it again after reinstalling the mod."
|
||||
fi</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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 "\nRunning post installation for mod %workshop_mod_id%"
|
||||
printf "\nMovin Folders"
|
||||
mv %mods_full_path%/steamapps/workshop/content/440900/%workshop_mod_id%/%first_file% %mods_full_path%/%first_file%
|
||||
printf "\nCleaning up"
|
||||
rm -Rf %mods_full_path%/steamapps/workshop/content/440900/%workshop_mod_id%
|
||||
printf "\nInstallation for mod %workshop_mod_id% completed!"
|
||||
</post_install>
|
||||
<uninstall>printf "\nUninstalling...\n"
|
||||
rm -vf %mods_full_path%/%mod_string%
|
||||
printf "\nDone!"
|
||||
</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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 "\nMoving item %workshop_mod_id% ..."
|
||||
mv -f "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%/contents.pak" "%mods_full_path%/%workshop_mod_id%.pak"
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%"
|
||||
printf "\nSuccess."</post_install>
|
||||
<uninstall>printf "\nUninstalling item %mod_string% ...\n"
|
||||
rm -Rf "%mods_full_path%/%mod_string%.pak"
|
||||
printf "\nSuccess."</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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 "\nMoving item %workshop_mod_id% ..."
|
||||
mv -f "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%/contents.pak" "%mods_full_path%/%workshop_mod_id%.pak"
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%"
|
||||
printf "\nSuccess."</post_install>
|
||||
<uninstall>printf "\nUninstalling item %mod_string% ...\n"
|
||||
rm -Rf "%mods_full_path%/%mod_string%.pak"
|
||||
printf "\nSuccess."</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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 "\nRunning post installation for mod %workshop_mod_id%"
|
||||
printf "\nInstalling Map %first_file%\n"
|
||||
unzip -o "%mods_full_path%/steamapps/workshop/content/730/%workshop_mod_id%/%first_file%" -d "%mods_full_path%/../csgo/maps"
|
||||
printf "\nCleaning up"
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/730/%workshop_mod_id%"
|
||||
map=%first_file%
|
||||
map=${map%.bsp}
|
||||
maplist_file="%mods_full_path%/../csgo/maplist.txt"
|
||||
maplist_content=$(cat "$maplist_file")
|
||||
if [ ! -z "${maplist_content##*$map*}" ];then
|
||||
printf "\nAdding Map to maplist.txt"
|
||||
echo $map >> "$maplist_file"
|
||||
else
|
||||
printf "\nMap already in maplist.txt"
|
||||
fi
|
||||
printf "\nInstallation for map %first_file% completed!"
|
||||
</post_install>
|
||||
<uninstall>map=%mod_string%
|
||||
if [ -f "%mods_full_path%/../csgo/maps/$map" ];then
|
||||
rm -f $map
|
||||
fi
|
||||
map=${map%.bsp}
|
||||
maplist_file="%mods_full_path%/../csgo/maplist.txt"
|
||||
sed -i "/^$map$/d" $maplist_file
|
||||
</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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%"
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/221100/%workshop_mod_id%"
|
||||
</post_install>
|
||||
<uninstall>printf "\nUninstalling...\n"
|
||||
rm -Rf "%mods_full_path%/%workshop_mod_id%"</uninstall>
|
||||
</workshop_settings>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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%
|
||||

|
||||
modid=$(find $a -iname "%mod_string%")
|
||||

|
||||
modfolder=$(dirname $modid)
|
||||
echo $modfolder 
|
||||
rm -Rf $modfolder
|
||||
printf "\n %mod_string% automatically uninstall. \n" </uninstall>
|
||||
</workshop_settings>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||
|
|
@ -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%
|
||||
echo $modID
|
||||

|
||||
sed -i 's/<Mods \/>/<Mods>\n<\/Mods>/' Sandbox_config.sbc
|
||||

|
||||
sed -i "s/<Mods>/<Mods>\n<ModItem>\n<Name>$modID.sbm<\/Name>\n<PublishedFileId>$modID<\/PublishedFileId>\n<\/Moditem>\n/" Sandbox_config.sbc</post_install>
|
||||
<uninstall/>
|
||||
</workshop_settings>
|
||||
|
|
@ -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% ..."
|
||||
cp -Rf "%mods_full_path%/steamapps/workshop/content/228380/%workshop_mod_id%" "%mods_full_path%/."
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/228380/%workshop_mod_id%"
|
||||
printf "\nSuccess."</post_install>
|
||||
<uninstall>printf "\nUninstalling item %mod_string% ...\n"
|
||||
rm -Rf "%mods_full_path%/%mod_string%"
|
||||
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
|
|
@ -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%"
|
||||
cp -f %first_file% myfile.gma
|
||||
7z x myfile.gma -aoa > /dev/null 2>&1
|
||||
cp -f myfile content.gma
|
||||
rm -f myfile.gma myfile
|
||||
"%mods_full_path%/../bin/gmad_linux" content.gma > /dev/null 2>&1
|
||||
rm -f "content.gma"
|
||||
cd content
|
||||
zip -r "%mods_full_path%/steamapps/workshop/content/4000/%first_file%.zip" * > /dev/null 2>&1
|
||||
cd ../..
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/4000/%workshop_mod_id%"
|
||||
unzip -Z1 "%first_file%.zip" > "%first_file%.list"
|
||||
tac "%first_file%.list" > "%first_file%.listinv"
|
||||
cp -f "%first_file%.listinv" "%first_file%.list"
|
||||
rm "%first_file%.listinv"
|
||||
unzip -o "%first_file%.zip" -d "%mods_full_path%/../garrysmod" > /dev/null 2>&1
|
||||
rm -f "%first_file%.zip"
|
||||

|
||||
if [ -f "%mods_full_path%/steamapps/workshop/content/4000/%first_file%.list" ];then
|
||||
cd "%mods_full_path%/../garrysmod"
|
||||
luaFile="%mods_full_path%/../garrysmod/lua/autorun/server/resources.lua"
|
||||
while read p; do
|
||||
if [ -f "$p" ] && [ ! -d "$p" ]; then
|
||||
filename=$(basename -- "$p")
|
||||
extension="${filename##*.}"
|
||||
if [ "$extension" != "bsp" ] && [ "$extension" != "png" ]; then
|
||||
newstring="resource.AddSingleFile(\"$p\")"
|
||||
if ! grep -Fxq "$newstring" "$luaFile"; then
|
||||
echo "$newstring" >> "$luaFile"
|
||||
fi 
|
||||
fi
|
||||
fi
|
||||
done <"%mods_full_path%/steamapps/workshop/content/4000/%first_file%.list"
|
||||
printf "\nContents of %first_file% successfully installed!"
|
||||
else
|
||||
printf "\nFile listing not found, try it again after reinstalling the mod."
|
||||
fi
|
||||

|
||||
</post_install>
|
||||
<uninstall>if [ -f "%mods_full_path%/steamapps/workshop/content/4000/%mod_string%.list" ];then
|
||||
cd "%mods_full_path%/../garrysmod"
|
||||
luaFile="%mods_full_path%/../garrysmod/lua/autorun/server/resources.lua"
|
||||
while read p; do
|
||||
if [ -d "$p" ]; then
|
||||
if [ -z "$(ls -A "$p")" ]; then
|
||||
rm -vRf "$p"
|
||||
fi
|
||||
else
|
||||
if [ -f "$p" ]; then
|
||||
rm -vf "$p"
|
||||
filestring="resource.AddSingleFile(\"$p\")"
|
||||
if grep -Fxq "$filestring" "$luaFile"; then
|
||||
escaped_filestring=$(sed -e 's/[]\/$*.^[]/\\&/g' <<< $filestring)
|
||||
sed -i "/$escaped_filestring/d" "$luaFile"
|
||||
fi 
|
||||
fi
|
||||
fi
|
||||
done <"%mods_full_path%/steamapps/workshop/content/4000/%mod_string%.list"
|
||||
printf "\nContents of %mod_string% successfully uninstalled!"
|
||||
else
|
||||
printf "\nFile listing not found, try it again after reinstalling the mod."
|
||||
fi</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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%"
|
||||
printf "\nMovin Folders"
|
||||
mv %mods_full_path%/steamapps/workshop/content/440900/%workshop_mod_id%/%first_file% %mods_full_path%/%first_file%
|
||||
printf "\nCleaning up"
|
||||
rm -Rf %mods_full_path%/steamapps/workshop/content/440900/%workshop_mod_id%
|
||||
printf "\nInstallation for mod %workshop_mod_id% completed!"
|
||||
</post_install>
|
||||
<uninstall>printf "\nUninstalling...\n"
|
||||
rm -vf %mods_full_path%/%mod_string%
|
||||
printf "\nDone!"
|
||||
</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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% ..."
|
||||
mv -f "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%/contents.pak" "%mods_full_path%/%workshop_mod_id%.pak"
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%"
|
||||
printf "\nSuccess."</post_install>
|
||||
<uninstall>printf "\nUninstalling item %mod_string% ...\n"
|
||||
rm -Rf "%mods_full_path%/%mod_string%.pak"
|
||||
printf "\nSuccess."</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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% ..."
|
||||
mv -f "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%/contents.pak" "%mods_full_path%/%workshop_mod_id%.pak"
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/211820/%workshop_mod_id%"
|
||||
printf "\nSuccess."</post_install>
|
||||
<uninstall>printf "\nUninstalling item %mod_string% ...\n"
|
||||
rm -Rf "%mods_full_path%/%mod_string%.pak"
|
||||
printf "\nSuccess."</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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%"
|
||||
printf "\nInstalling Map %first_file%\n"
|
||||
unzip -o "%mods_full_path%/steamapps/workshop/content/730/%workshop_mod_id%/%first_file%" -d "%mods_full_path%/../csgo/maps"
|
||||
printf "\nCleaning up"
|
||||
rm -Rf "%mods_full_path%/steamapps/workshop/content/730/%workshop_mod_id%"
|
||||
map=%first_file%
|
||||
map=${map%.bsp}
|
||||
maplist_file="%mods_full_path%/../csgo/maplist.txt"
|
||||
maplist_content=$(cat "$maplist_file")
|
||||
if [ ! -z "${maplist_content##*$map*}" ];then
|
||||
printf "\nAdding Map to maplist.txt"
|
||||
echo $map >> "$maplist_file"
|
||||
else
|
||||
printf "\nMap already in maplist.txt"
|
||||
fi
|
||||
printf "\nInstallation for map %first_file% completed!"
|
||||
</post_install>
|
||||
<uninstall>map=%mod_string%
|
||||
if [ -f "%mods_full_path%/../csgo/maps/$map" ];then
|
||||
rm -f $map
|
||||
fi
|
||||
map=${map%.bsp}
|
||||
maplist_file="%mods_full_path%/../csgo/maplist.txt"
|
||||
sed -i "/^$map$/d" $maplist_file
|
||||
</uninstall>
|
||||
</workshop_settings>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
366
modules/steam_workshop/includes/functions.php
Normal file
366
modules/steam_workshop/includes/functions.php
Normal 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');
|
||||
}
|
||||
89
modules/steam_workshop/install.sql
Normal file
89
modules/steam_workshop/install.sql
Normal 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);
|
||||
|
|
@ -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: 15–360 minutes.',
|
||||
'label_staging_dir' => 'Staging directory (optional)',
|
||||
'label_install_strategy' => 'Install strategy',
|
||||
'label_on_update_action' => 'On update action',
|
||||
'label_post_install_script' => 'Post-install script (absolute path)',
|
||||
'label_mod_import' => 'Workshop IDs list (one "id,@ModName" per line)',
|
||||
'hint_mod_import' => 'Paste from Modlist.txt or import from a collection. IDs are sanitized automatically.',
|
||||
'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:',
|
||||
];
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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`",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
?>
|
||||
535
modules/steam_workshop/user.php
Normal file
535
modules/steam_workshop/user.php
Normal 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 › 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']) ?>
|
||||
|
||||
<strong>Game:</strong> <?= sw_h($home['game_name']) ?>
|
||||
|
||||
<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' : '' ?>>▲</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' : '' ?>>▼</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
|
||||
}
|
||||
|
|
@ -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: ?>
|
||||
—
|
||||
<?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>
|
||||
|
|
@ -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">← <?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 & 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 & 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'); ?> →
|
||||
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'); ?>) →
|
||||
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 & 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>
|
||||
|
|
@ -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'] ? '✔' : '✘'; ?></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>
|
||||
|
|
@ -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&p=main">← <?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&p=main&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&p=main"><?php echo htmlspecialchars($lang['button_cancel']); ?></a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php include __DIR__ . '/partials/mod_table.php'; ?>
|
||||
</div>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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; ?>">← <?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>
|
||||
|
|
@ -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 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; ?>
|
||||
|
|
@ -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§ion=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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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&p=main&action=edit&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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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; ?>">← <?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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue