steam workshotp
This commit is contained in:
parent
d8c66c4c49
commit
e921a49d5b
14 changed files with 285 additions and 26 deletions
|
|
@ -215,8 +215,9 @@ Apply this as `$install_queries[2]` (db_version 3) in `module.php` when ready.
|
||||||
- [x] `addons_installer.php` and `user_addons.php` use category map for validation.
|
- [x] `addons_installer.php` and `user_addons.php` use category map for validation.
|
||||||
- [x] Full TODO/comment blocks added to installer for next phase work.
|
- [x] Full TODO/comment blocks added to installer for next phase work.
|
||||||
- [x] Module folder, table names, URL routes, function names unchanged.
|
- [x] Module folder, table names, URL routes, function names unchanged.
|
||||||
- [x] Workshop Content Phase 1: manual Workshop ID entry, per-server manifest,
|
- [x] Workshop Content Phase 1: manual Workshop ID/URL entry, per-server
|
||||||
agent script runner, install/update/remove actions (`db_version 3`).
|
manifest, bundled Linux and Windows/Cygwin SteamCMD handlers, generic
|
||||||
|
handler fallback, install/update/remove actions (`db_version 3`).
|
||||||
|
|
||||||
### Phase 2 — Schema & install_method support
|
### Phase 2 — Schema & install_method support
|
||||||
- [x] Apply Phase 2 schema: `install_method`, `content_version`, `requires_stop`,
|
- [x] Apply Phase 2 schema: `install_method`, `content_version`, `requires_stop`,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` modu
|
||||||
|
|
||||||
- No Steam Workshop browser/search UI yet.
|
- No Steam Workshop browser/search UI yet.
|
||||||
- No Steam scraping.
|
- No Steam scraping.
|
||||||
- User enters comma-separated numeric Workshop IDs.
|
- User enters Steam Workshop numeric IDs or full Workshop URLs.
|
||||||
|
- Input may be comma-separated, newline-separated, or whitespace-separated.
|
||||||
- Panel validates IDs, removes duplicates, and stores them per server home.
|
- Panel validates IDs, removes duplicates, and stores them per server home.
|
||||||
- Panel lists saved IDs and supports:
|
- Panel lists saved IDs and supports:
|
||||||
- Install New
|
- Install New
|
||||||
|
|
@ -15,7 +16,14 @@ Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` modu
|
||||||
- Update All
|
- Update All
|
||||||
- Panel generates a per-server manifest at:
|
- Panel generates a per-server manifest at:
|
||||||
- `%home_path%/gsp_server_content/workshop_manifest.json`
|
- `%home_path%/gsp_server_content/workshop_manifest.json`
|
||||||
- Panel runs an approved script path (safe default or game-specific config), never user-supplied command/path.
|
- Panel runs an approved handler only, never a user-supplied command/path.
|
||||||
|
- If a game does not define a custom Workshop script, the panel stages the
|
||||||
|
bundled generic handler for the agent OS:
|
||||||
|
- Linux: `generic_steam_workshop_linux.sh`
|
||||||
|
- Windows/Cygwin: `generic_steam_workshop_windows_cygwin.sh`
|
||||||
|
- If a custom configured script is missing on the agent, the panel falls back
|
||||||
|
to the bundled generic handler and logs a warning instead of failing with
|
||||||
|
"script not found."
|
||||||
|
|
||||||
## Security model
|
## Security model
|
||||||
|
|
||||||
|
|
@ -24,6 +32,9 @@ Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` modu
|
||||||
- IDs must be numeric only.
|
- IDs must be numeric only.
|
||||||
- Script path is not user-editable.
|
- Script path is not user-editable.
|
||||||
- Manifest path is validated to remain under server home.
|
- Manifest path is validated to remain under server home.
|
||||||
|
- Default generic installs stay under `{SERVER_ROOT}/workshop/@<workshop_id>`.
|
||||||
|
- Arma/DayZ-style strategies install under `{SERVER_ROOT}/@<workshop_id>` so
|
||||||
|
startup parameters can later use normal `-mod=@...` values.
|
||||||
- Remove is non-destructive in the generic scripts (preserve/move behavior for Phase 1).
|
- Remove is non-destructive in the generic scripts (preserve/move behavior for Phase 1).
|
||||||
- All actions are logged through panel logging.
|
- All actions are logged through panel logging.
|
||||||
|
|
||||||
|
|
@ -40,11 +51,21 @@ and keeps `OGP_DB_PREFIXaddons.addon_type` at `VARCHAR(32)` so `workshop` is val
|
||||||
Each game should define and document:
|
Each game should define and document:
|
||||||
|
|
||||||
- `workshop_app_id`
|
- `workshop_app_id`
|
||||||
- Linux workshop script path
|
- optional Linux workshop script path only when the bundled generic handler is
|
||||||
- Windows/Cygwin workshop script path
|
not sufficient
|
||||||
|
- optional Windows/Cygwin workshop script path only when the bundled generic
|
||||||
|
handler is not sufficient
|
||||||
- target install location
|
- target install location
|
||||||
- restart/update behavior
|
- restart/update behavior
|
||||||
|
|
||||||
|
If `workshop_app_id` is not defined in the content profile, server content
|
||||||
|
template, or game XML, Phase 1 returns a clear error rather than guessing.
|
||||||
|
|
||||||
|
Known Phase 1 app IDs:
|
||||||
|
|
||||||
|
- Arma 3 Workshop app ID: `107410`
|
||||||
|
- Arma 3 dedicated server installer app ID remains `233780`
|
||||||
|
|
||||||
## Phase 2 (not included here)
|
## Phase 2 (not included here)
|
||||||
|
|
||||||
- Workshop browsing/search/select UI
|
- Workshop browsing/search/select UI
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ try:
|
||||||
'FOLDER_NAME': folder_name,
|
'FOLDER_NAME': folder_name,
|
||||||
'MOD_FOLDER': folder_name,
|
'MOD_FOLDER': folder_name,
|
||||||
}
|
}
|
||||||
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
|
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}')
|
||||||
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
|
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
|
||||||
if len(items) != 1 or not target_path:
|
if len(items) != 1 or not target_path:
|
||||||
target_path = render_template(target_template, template_values)
|
target_path = render_template(target_template, template_values)
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ try:
|
||||||
'FOLDER_NAME': folder_name,
|
'FOLDER_NAME': folder_name,
|
||||||
'MOD_FOLDER': folder_name,
|
'MOD_FOLDER': folder_name,
|
||||||
}
|
}
|
||||||
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
|
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}')
|
||||||
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
|
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
|
||||||
if len(items) != 1 or not target_path:
|
if len(items) != 1 or not target_path:
|
||||||
target_path = render_template(target_template, template_values)
|
target_path = render_template(target_template, template_values)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ if (!defined('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT')) {
|
||||||
if (!defined('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT')) {
|
if (!defined('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT')) {
|
||||||
define('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT', 'generic_steam_workshop_windows_cygwin.sh');
|
define('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT', 'generic_steam_workshop_windows_cygwin.sh');
|
||||||
}
|
}
|
||||||
|
if (!defined('SCM_WORKSHOP_TARGET_GENERIC_DEFAULT')) {
|
||||||
|
define('SCM_WORKSHOP_TARGET_GENERIC_DEFAULT', '{SERVER_ROOT}/workshop/{MOD_FOLDER}');
|
||||||
|
}
|
||||||
|
if (!defined('SCM_WORKSHOP_TARGET_MOD_ROOT_DEFAULT')) {
|
||||||
|
define('SCM_WORKSHOP_TARGET_MOD_ROOT_DEFAULT', '{SERVER_ROOT}/{MOD_FOLDER}');
|
||||||
|
}
|
||||||
|
|
||||||
function scm_ensure_workshop_schema($db)
|
function scm_ensure_workshop_schema($db)
|
||||||
{
|
{
|
||||||
|
|
@ -246,12 +252,31 @@ function scm_get_workshop_script_path(array $home_info, $server_xml)
|
||||||
return scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
|
return scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scm_get_configured_workshop_script_path(array $home_info, $server_xml)
|
||||||
|
{
|
||||||
|
$key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux';
|
||||||
|
if (!isset($server_xml->$key)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$xml_path = trim((string)$server_xml->$key);
|
||||||
|
if ($xml_path === '' || !preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $xml_path;
|
||||||
|
}
|
||||||
|
|
||||||
function scm_get_bundled_workshop_script_source(array $home_info)
|
function scm_get_bundled_workshop_script_source(array $home_info)
|
||||||
{
|
{
|
||||||
$filename = scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
|
$filename = scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
|
||||||
return dirname(__FILE__) . '/scripts/workshop/' . $filename;
|
return dirname(__FILE__) . '/scripts/workshop/' . $filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scm_is_default_workshop_script_name($script_path)
|
||||||
|
{
|
||||||
|
$base = basename(trim((string)$script_path));
|
||||||
|
return in_array($base, array(SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT, SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT), true);
|
||||||
|
}
|
||||||
|
|
||||||
function scm_is_legacy_panel_workshop_script_path($script_path)
|
function scm_is_legacy_panel_workshop_script_path($script_path)
|
||||||
{
|
{
|
||||||
$script_path = trim((string)$script_path);
|
$script_path = trim((string)$script_path);
|
||||||
|
|
@ -276,13 +301,17 @@ function scm_get_agent_managed_workshop_script_path(array $home_info)
|
||||||
function scm_prepare_workshop_script_for_agent($remote, array $home_info, $server_xml, &$error = '')
|
function scm_prepare_workshop_script_for_agent($remote, array $home_info, $server_xml, &$error = '')
|
||||||
{
|
{
|
||||||
$error = '';
|
$error = '';
|
||||||
$configured_path = trim((string)scm_get_workshop_script_path($home_info, $server_xml));
|
$configured_path = scm_get_configured_workshop_script_path($home_info, $server_xml);
|
||||||
if ($configured_path !== '' && !scm_is_legacy_panel_workshop_script_path($configured_path) && preg_match('/^[^\\r\\n\\0]+$/', $configured_path)) {
|
if ($configured_path !== '' && !scm_is_default_workshop_script_name($configured_path) && !scm_is_legacy_panel_workshop_script_path($configured_path)) {
|
||||||
if ((int)$remote->rfile_exists($configured_path) === 1) {
|
if ((int)$remote->rfile_exists($configured_path) === 1) {
|
||||||
return $configured_path;
|
return $configured_path;
|
||||||
}
|
}
|
||||||
$error = 'Configured workshop script not found on agent host: ' . $configured_path;
|
scm_log_content_install_action(array(
|
||||||
return false;
|
'type' => 'workshop_script_fallback',
|
||||||
|
'home_id' => isset($home_info['home_id']) ? (int)$home_info['home_id'] : 0,
|
||||||
|
'configured_path' => $configured_path,
|
||||||
|
'message' => 'Configured workshop script was not found on the agent; falling back to bundled generic handler.',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
$source_path = scm_get_bundled_workshop_script_source($home_info);
|
$source_path = scm_get_bundled_workshop_script_source($home_info);
|
||||||
|
|
@ -313,6 +342,15 @@ function scm_prepare_workshop_script_for_agent($remote, array $home_info, $serve
|
||||||
return $remote_path;
|
return $remote_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scm_get_default_workshop_target_template($install_strategy = '')
|
||||||
|
{
|
||||||
|
$install_strategy = strtolower(trim((string)$install_strategy));
|
||||||
|
if (in_array($install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true)) {
|
||||||
|
return SCM_WORKSHOP_TARGET_MOD_ROOT_DEFAULT;
|
||||||
|
}
|
||||||
|
return SCM_WORKSHOP_TARGET_GENERIC_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
function scm_get_csrf_token()
|
function scm_get_csrf_token()
|
||||||
{
|
{
|
||||||
if (empty($_SESSION['addonsmanager_workshop_csrf'])) {
|
if (empty($_SESSION['addonsmanager_workshop_csrf'])) {
|
||||||
|
|
@ -583,9 +621,10 @@ function scm_build_workshop_runtime_context($db, array $home_info, $server_xml,
|
||||||
}
|
}
|
||||||
|
|
||||||
$workshop_item_id = trim((string)(isset($payload['workshop_item_id']) ? $payload['workshop_item_id'] : ''));
|
$workshop_item_id = trim((string)(isset($payload['workshop_item_id']) ? $payload['workshop_item_id'] : ''));
|
||||||
$target_path_template = trim((string)$payload['target_path_template']);
|
$target_path_template = trim((string)(isset($payload['target_path_template']) ? $payload['target_path_template'] : ''));
|
||||||
$optional_folder_name = trim((string)$payload['optional_folder_name']);
|
$optional_folder_name = trim((string)(isset($payload['optional_folder_name']) ? $payload['optional_folder_name'] : ''));
|
||||||
$workshop_app_id_override = trim((string)$payload['workshop_app_id']);
|
$workshop_app_id_override = trim((string)(isset($payload['workshop_app_id']) ? $payload['workshop_app_id'] : ''));
|
||||||
|
$install_strategy = isset($payload['install_strategy']) ? trim((string)$payload['install_strategy']) : '';
|
||||||
$fallback_profile = function_exists('sw_get_profile_for_home') ? sw_get_profile_for_home($db, (int)$home_info['home_id']) : false;
|
$fallback_profile = function_exists('sw_get_profile_for_home') ? sw_get_profile_for_home($db, (int)$home_info['home_id']) : false;
|
||||||
$resolved = function_exists('steam_workshop_install_item_to_home')
|
$resolved = function_exists('steam_workshop_install_item_to_home')
|
||||||
? steam_workshop_install_item_to_home($db, $home_info, $workshop_item_id, $target_path_template, array(
|
? steam_workshop_install_item_to_home($db, $home_info, $workshop_item_id, $target_path_template, array(
|
||||||
|
|
@ -625,7 +664,7 @@ function scm_build_workshop_runtime_context($db, array $home_info, $server_xml,
|
||||||
if ($effective_template === '') {
|
if ($effective_template === '') {
|
||||||
$effective_template = (is_array($fallback_profile) && !empty($fallback_profile['install_path_template']))
|
$effective_template = (is_array($fallback_profile) && !empty($fallback_profile['install_path_template']))
|
||||||
? (string)$fallback_profile['install_path_template']
|
? (string)$fallback_profile['install_path_template']
|
||||||
: '{SERVER_ROOT}/{MOD_FOLDER}';
|
: scm_get_default_workshop_target_template($install_strategy);
|
||||||
}
|
}
|
||||||
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => isset($server_xml->exe_location) ? (string)$server_xml->exe_location : ''), array(
|
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => isset($server_xml->exe_location) ? (string)$server_xml->exe_location : ''), array(
|
||||||
'WORKSHOP_ID' => $workshop_item_id,
|
'WORKSHOP_ID' => $workshop_item_id,
|
||||||
|
|
|
||||||
116
Panel/modules/addonsmanager/tests/workshop_helpers_test.php
Normal file
116
Panel/modules/addonsmanager/tests/workshop_helpers_test.php
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if (!function_exists('clean_path')) {
|
||||||
|
function clean_path($path)
|
||||||
|
{
|
||||||
|
$path = str_replace('\\', '/', (string)$path);
|
||||||
|
$path = preg_replace('#/+#', '/', $path);
|
||||||
|
return rtrim($path, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once(dirname(__DIR__) . '/server_content_helpers.php');
|
||||||
|
|
||||||
|
class ScmWorkshopFakeRemote
|
||||||
|
{
|
||||||
|
public $files = array();
|
||||||
|
public $commands = array();
|
||||||
|
public $existing = array();
|
||||||
|
|
||||||
|
public function rfile_exists($path)
|
||||||
|
{
|
||||||
|
return in_array($path, $this->existing, true) || isset($this->files[$path]) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remote_writefile($path, $contents)
|
||||||
|
{
|
||||||
|
$this->files[$path] = $contents;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exec($command)
|
||||||
|
{
|
||||||
|
$this->commands[] = $command;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scm_workshop_test_assert($condition, $message)
|
||||||
|
{
|
||||||
|
if (!$condition) {
|
||||||
|
fwrite(STDERR, "FAIL: {$message}\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
echo "PASS: {$message}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$invalid = array();
|
||||||
|
$ids = scm_parse_workshop_ids("https://steamcommunity.com/sharedfiles/filedetails/?id=450814997\n450814997, 463939057 463939057", $invalid);
|
||||||
|
scm_workshop_test_assert($ids === array('450814997', '463939057'), 'parses URLs, whitespace, commas, and duplicates');
|
||||||
|
scm_workshop_test_assert(empty($invalid), 'valid mixed Workshop input has no invalid entries');
|
||||||
|
|
||||||
|
$ids = scm_parse_workshop_ids("abc, https://steamcommunity.com/sharedfiles/filedetails/?id=0, 123", $invalid);
|
||||||
|
scm_workshop_test_assert($ids === array('123'), 'keeps valid IDs when invalid values are present');
|
||||||
|
scm_workshop_test_assert(count($invalid) === 2, 'reports invalid Workshop entries');
|
||||||
|
|
||||||
|
$linuxHome = array(
|
||||||
|
'home_id' => 10,
|
||||||
|
'home_path' => '/srv/games/arma3',
|
||||||
|
'home_cfg_file' => 'arma3_linux64.xml',
|
||||||
|
'game_key' => 'arma3_linux64',
|
||||||
|
'game_name' => 'Arma 3',
|
||||||
|
);
|
||||||
|
$windowsHome = array(
|
||||||
|
'home_id' => 11,
|
||||||
|
'home_path' => '/cygdrive/c/OGP_User_Files/11',
|
||||||
|
'home_cfg_file' => 'arma3_win64.xml',
|
||||||
|
'game_key' => 'arma3_win64',
|
||||||
|
'game_name' => 'Arma 3',
|
||||||
|
);
|
||||||
|
|
||||||
|
$emptyXml = simplexml_load_string('<game_config></game_config>');
|
||||||
|
$linuxRemote = new ScmWorkshopFakeRemote();
|
||||||
|
$error = '';
|
||||||
|
$script = scm_prepare_workshop_script_for_agent($linuxRemote, $linuxHome, $emptyXml, $error);
|
||||||
|
scm_workshop_test_assert($script === '/srv/games/arma3/gsp_server_content/scripts/workshop/generic_steam_workshop_linux.sh', 'stages default Linux script under server home');
|
||||||
|
scm_workshop_test_assert(isset($linuxRemote->files[$script]), 'writes Linux bundled script to fake agent');
|
||||||
|
scm_workshop_test_assert($error === '', 'default Linux script staging does not report missing script');
|
||||||
|
|
||||||
|
$windowsRemote = new ScmWorkshopFakeRemote();
|
||||||
|
$script = scm_prepare_workshop_script_for_agent($windowsRemote, $windowsHome, $emptyXml, $error);
|
||||||
|
scm_workshop_test_assert($script === '/cygdrive/c/OGP_User_Files/11/gsp_server_content/scripts/workshop/generic_steam_workshop_windows_cygwin.sh', 'stages default Windows/Cygwin script under server home');
|
||||||
|
scm_workshop_test_assert(isset($windowsRemote->files[$script]), 'writes Windows/Cygwin bundled script to fake agent');
|
||||||
|
scm_workshop_test_assert($error === '', 'default Windows/Cygwin script staging does not report missing script');
|
||||||
|
|
||||||
|
$configuredXml = simplexml_load_string('<game_config><workshop_script_linux>/agent/custom/workshop.sh</workshop_script_linux></game_config>');
|
||||||
|
$customRemote = new ScmWorkshopFakeRemote();
|
||||||
|
$customRemote->existing[] = '/agent/custom/workshop.sh';
|
||||||
|
$script = scm_prepare_workshop_script_for_agent($customRemote, $linuxHome, $configuredXml, $error);
|
||||||
|
scm_workshop_test_assert($script === '/agent/custom/workshop.sh', 'uses existing configured custom script');
|
||||||
|
|
||||||
|
$missingCustomRemote = new ScmWorkshopFakeRemote();
|
||||||
|
$script = scm_prepare_workshop_script_for_agent($missingCustomRemote, $linuxHome, $configuredXml, $error);
|
||||||
|
scm_workshop_test_assert($script === '/srv/games/arma3/gsp_server_content/scripts/workshop/generic_steam_workshop_linux.sh', 'falls back to bundled script when configured custom script is missing');
|
||||||
|
scm_workshop_test_assert($error === '', 'missing custom script fallback does not expose script-not-found error');
|
||||||
|
@unlink(dirname(__DIR__) . '/logs/content_install.log');
|
||||||
|
@rmdir(dirname(__DIR__) . '/logs');
|
||||||
|
|
||||||
|
$genericHome = array(
|
||||||
|
'home_id' => 12,
|
||||||
|
'home_path' => '/srv/games/generic',
|
||||||
|
'home_cfg_file' => 'generic_linux.xml',
|
||||||
|
'game_key' => 'generic_linux',
|
||||||
|
'game_name' => 'Generic Game',
|
||||||
|
);
|
||||||
|
$runtime = scm_build_workshop_runtime_context(new stdClass(), $genericHome, $emptyXml, array('workshop_item_id' => '12345'), $message);
|
||||||
|
scm_workshop_test_assert($runtime['target_path_template'] === '{SERVER_ROOT}/workshop/{MOD_FOLDER}', 'generic fallback target path uses server_root/workshop');
|
||||||
|
scm_workshop_test_assert($runtime['target_path_resolved'] === '/srv/games/generic/workshop/@12345', 'generic fallback target path resolves under workshop folder');
|
||||||
|
|
||||||
|
$runtime = scm_build_workshop_runtime_context(new stdClass(), $linuxHome, $emptyXml, array('workshop_item_id' => '450814997', 'install_strategy' => 'arma_mod_folder'), $message);
|
||||||
|
scm_workshop_test_assert($runtime['target_path_template'] === '{SERVER_ROOT}/{MOD_FOLDER}', 'Arma strategy keeps @mod folder at server root');
|
||||||
|
scm_workshop_test_assert($runtime['target_path_resolved'] === '/srv/games/arma3/@450814997', 'Arma target path resolves to root @WorkshopID folder');
|
||||||
|
|
||||||
|
$appXml = simplexml_load_string('<game_config><workshop_app_id>107410</workshop_app_id></game_config>');
|
||||||
|
scm_workshop_test_assert(scm_extract_workshop_app_id($appXml) === '107410', 'extracts explicit Workshop app ID from game XML');
|
||||||
|
|
||||||
|
echo "All Workshop helper smoke tests passed.\n";
|
||||||
|
|
@ -100,6 +100,7 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
||||||
foreach ($item_ids as $item_id) {
|
foreach ($item_ids as $item_id) {
|
||||||
$payload = $template_payload;
|
$payload = $template_payload;
|
||||||
$payload['workshop_item_id'] = (string)$item_id;
|
$payload['workshop_item_id'] = (string)$item_id;
|
||||||
|
$payload['install_strategy'] = $install_strategy;
|
||||||
// A fixed optional folder name is safe only when installing one item. For
|
// A fixed optional folder name is safe only when installing one item. For
|
||||||
// multi-item installs, use @<workshop_id> so items cannot overwrite each
|
// multi-item installs, use @<workshop_id> so items cannot overwrite each
|
||||||
// other by sharing the same target folder.
|
// other by sharing the same target folder.
|
||||||
|
|
@ -122,7 +123,7 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
||||||
'workshop_item_id' => (string)$item_id,
|
'workshop_item_id' => (string)$item_id,
|
||||||
'title' => '',
|
'title' => '',
|
||||||
'folder_name' => isset($runtime['folder_name']) && $runtime['folder_name'] !== '' ? (string)$runtime['folder_name'] : '@' . $item_id,
|
'folder_name' => isset($runtime['folder_name']) && $runtime['folder_name'] !== '' ? (string)$runtime['folder_name'] : '@' . $item_id,
|
||||||
'target_path_template' => isset($runtime['target_path_template']) ? (string)$runtime['target_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}',
|
'target_path_template' => isset($runtime['target_path_template']) ? (string)$runtime['target_path_template'] : scm_get_default_workshop_target_template($install_strategy),
|
||||||
'target_path_resolved' => isset($runtime['target_path_resolved']) ? (string)$runtime['target_path_resolved'] : '',
|
'target_path_resolved' => isset($runtime['target_path_resolved']) ? (string)$runtime['target_path_resolved'] : '',
|
||||||
'install_strategy' => $install_strategy,
|
'install_strategy' => $install_strategy,
|
||||||
'copy_keys' => $copy_keys ? 1 : 0,
|
'copy_keys' => $copy_keys ? 1 : 0,
|
||||||
|
|
@ -140,7 +141,7 @@ function scm_workshop_build_manifest_context($db, array $home_info, $server_xml,
|
||||||
'server_root' => rtrim((string)$home_info['home_path'], '/'),
|
'server_root' => rtrim((string)$home_info['home_path'], '/'),
|
||||||
'install_strategy' => $install_strategy,
|
'install_strategy' => $install_strategy,
|
||||||
'copy_keys' => $copy_keys ? 1 : 0,
|
'copy_keys' => $copy_keys ? 1 : 0,
|
||||||
'target_path_template' => isset($template['target_path_template']) && trim((string)$template['target_path_template']) !== '' ? trim((string)$template['target_path_template']) : '{SERVER_ROOT}/{MOD_FOLDER}',
|
'target_path_template' => isset($template['target_path_template']) && trim((string)$template['target_path_template']) !== '' ? trim((string)$template['target_path_template']) : scm_get_default_workshop_target_template($install_strategy),
|
||||||
'post_install_script' => isset($template['post_script']) ? trim((string)$template['post_script']) : '',
|
'post_install_script' => isset($template['post_script']) ? trim((string)$template['post_script']) : '',
|
||||||
'launch_param_additions' => isset($template['launch_param_additions']) ? trim((string)$template['launch_param_additions']) : '',
|
'launch_param_additions' => isset($template['launch_param_additions']) ? trim((string)$template['launch_param_additions']) : '',
|
||||||
'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0,
|
'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0,
|
||||||
|
|
@ -183,7 +184,7 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
|
||||||
'items' => array_values($item_ids),
|
'items' => array_values($item_ids),
|
||||||
'item_details' => !empty($extra_manifest['item_details']) && is_array($extra_manifest['item_details']) ? $extra_manifest['item_details'] : array(),
|
'item_details' => !empty($extra_manifest['item_details']) && is_array($extra_manifest['item_details']) ? $extra_manifest['item_details'] : array(),
|
||||||
'install_strategy' => !empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : '',
|
'install_strategy' => !empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : '',
|
||||||
'target_path' => !empty($extra_manifest['target_path_template']) ? (string)$extra_manifest['target_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}',
|
'target_path' => !empty($extra_manifest['target_path_template']) ? (string)$extra_manifest['target_path_template'] : scm_get_default_workshop_target_template(!empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : ''),
|
||||||
'generated_at' => date('Y-m-d H:i:s'),
|
'generated_at' => date('Y-m-d H:i:s'),
|
||||||
);
|
);
|
||||||
if (!empty($extra_manifest)) {
|
if (!empty($extra_manifest)) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<game_key>arma3_linux64</game_key>
|
<game_key>arma3_linux64</game_key>
|
||||||
<installer>steamcmd</installer>
|
<installer>steamcmd</installer>
|
||||||
<game_name>Arma 3</game_name>
|
<game_name>Arma 3</game_name>
|
||||||
|
<workshop_app_id>107410</workshop_app_id>
|
||||||
<server_exec_name>arma3server_x64</server_exec_name>
|
<server_exec_name>arma3server_x64</server_exec_name>
|
||||||
<cli_template>%CONFIG% %CFG% %PROFILES% %NAME% %IP% %PORT% %PLAYERS% %MODLIST% %SERVERMODLIST% %AUTOINIT%</cli_template>
|
<cli_template>%CONFIG% %CFG% %PROFILES% %NAME% %IP% %PORT% %PLAYERS% %MODLIST% %SERVERMODLIST% %AUTOINIT%</cli_template>
|
||||||
<cli_params>
|
<cli_params>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<game_key>arma3_win64</game_key>
|
<game_key>arma3_win64</game_key>
|
||||||
<installer>steamcmd</installer>
|
<installer>steamcmd</installer>
|
||||||
<game_name>Arma 3</game_name>
|
<game_name>Arma 3</game_name>
|
||||||
|
<workshop_app_id>107410</workshop_app_id>
|
||||||
<server_exec_name>arma3server.exe</server_exec_name>
|
<server_exec_name>arma3server.exe</server_exec_name>
|
||||||
<cli_template>-profiles=profile -name=player -config=profile\server.cfg -cfg=profile\basic.cfg %PORT% %PLAYERS% %RANKING% %AUTOINIT% %DEBUG% %MODS% %SERVERMODS%</cli_template>
|
<cli_template>-profiles=profile -name=player -config=profile\server.cfg -cfg=profile\basic.cfg %PORT% %PLAYERS% %RANKING% %AUTOINIT% %DEBUG% %MODS% %SERVERMODS%</cli_template>
|
||||||
<cli_params>
|
<cli_params>
|
||||||
|
|
|
||||||
|
|
@ -518,22 +518,37 @@ function sw_detect_profile_defaults_from_xml($configName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$xmlBlob = $matched->asXML();
|
|
||||||
$workshopAppId = '';
|
$workshopAppId = '';
|
||||||
if ($xmlBlob !== false && preg_match('/steamapps\/workshop\/content\/(\d+)/i', $xmlBlob, $m)) {
|
foreach (array('workshop_app_id', 'workshop_appid', 'steam_workshop_app_id', 'steam_workshop_appid') as $tag) {
|
||||||
|
if (isset($matched->$tag)) {
|
||||||
|
$candidate = trim((string)$matched->$tag);
|
||||||
|
if ($candidate !== '' && preg_match('/^\d+$/', $candidate)) {
|
||||||
|
$workshopAppId = $candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$xmlBlob = $matched->asXML();
|
||||||
|
if ($workshopAppId === '' && $xmlBlob !== false && preg_match('/steamapps\/workshop\/content\/(\d+)/i', $xmlBlob, $m)) {
|
||||||
$workshopAppId = $m[1];
|
$workshopAppId = $m[1];
|
||||||
}
|
}
|
||||||
if ($workshopAppId === '') {
|
if ($workshopAppId === '') {
|
||||||
$workshopAppId = $steamAppId;
|
$workshopAppId = $steamAppId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$gameKey = strtolower(trim((string)$matched->game_key));
|
||||||
|
$gameName = strtolower(trim((string)$matched->game_name));
|
||||||
|
$installPathTemplate = (strpos($gameKey . ' ' . $gameName, 'arma') !== false || strpos($gameKey . ' ' . $gameName, 'dayz') !== false)
|
||||||
|
? '{SERVER_ROOT}/{MOD_FOLDER}'
|
||||||
|
: '{SERVER_ROOT}/workshop/{MOD_FOLDER}';
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'steam_app_id' => $steamAppId,
|
'steam_app_id' => $steamAppId,
|
||||||
'workshop_app_id' => $workshopAppId,
|
'workshop_app_id' => $workshopAppId,
|
||||||
'steamcmd_path' => '/home/gameserver/steamcmd/steamcmd.sh',
|
'steamcmd_path' => '/home/gameserver/steamcmd/steamcmd.sh',
|
||||||
'server_root_template' => '{SERVER_ROOT}',
|
'server_root_template' => '{SERVER_ROOT}',
|
||||||
'workshop_download_dir_template' => '{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}',
|
'workshop_download_dir_template' => '{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}',
|
||||||
'install_path_template' => '{SERVER_ROOT}/{MOD_FOLDER}',
|
'install_path_template' => $installPathTemplate,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -608,7 +623,7 @@ function steam_workshop_resolve_paths($db, array $home_info, $workshop_id, $targ
|
||||||
);
|
);
|
||||||
$target_template = trim((string)$target_path_template);
|
$target_template = trim((string)$target_path_template);
|
||||||
if ($target_template === '') {
|
if ($target_template === '') {
|
||||||
$target_template = !empty($profile['install_path_template']) ? (string)$profile['install_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}';
|
$target_template = !empty($profile['install_path_template']) ? (string)$profile['install_path_template'] : '{SERVER_ROOT}/workshop/{MOD_FOLDER}';
|
||||||
}
|
}
|
||||||
$resolved_target = sw_apply_template($target_template, $vars);
|
$resolved_target = sw_apply_template($target_template, $vars);
|
||||||
return array(
|
return array(
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,8 @@ Phase 1 implements this decision by routing the user-facing Workshop install flo
|
||||||
- Workshop input accepts numeric IDs or Steam URLs, then stores numeric IDs only.
|
- Workshop input accepts numeric IDs or Steam URLs, then stores numeric IDs only.
|
||||||
- Manifests are written under the server home in `gsp_server_content`.
|
- Manifests are written under the server home in `gsp_server_content`.
|
||||||
- Bundled Linux/Cygwin scripts are copied from the Panel module to an agent-managed folder under the server home before execution.
|
- Bundled Linux/Cygwin scripts are copied from the Panel module to an agent-managed folder under the server home before execution.
|
||||||
|
- Default script names are treated as bundled handlers, not as existing agent paths.
|
||||||
|
- Missing custom scripts fall back to bundled handlers and log the fallback.
|
||||||
|
- Generic content installs under `{SERVER_ROOT}/workshop/{MOD_FOLDER}` by default.
|
||||||
- DayZ/Arma-style installs default to `@<workshop_id>` folders and copy `.bikey` files into `keys` when present.
|
- DayZ/Arma-style installs default to `@<workshop_id>` folders and copy `.bikey` files into `keys` when present.
|
||||||
- Startup parameter generation remains a later phase.
|
- Startup parameter generation remains a later phase.
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,13 @@ The admin template defines capability and policy. The customer supplies only Wor
|
||||||
- `bash <script> <manifest>`
|
- `bash <script> <manifest>`
|
||||||
7. The script runs SteamCMD, copies downloaded content into the target mod folder, logs output, and copies `.bikey` files for DayZ/Arma-style strategies.
|
7. The script runs SteamCMD, copies downloaded content into the target mod folder, logs output, and copies `.bikey` files for DayZ/Arma-style strategies.
|
||||||
|
|
||||||
|
Current repair:
|
||||||
|
|
||||||
|
- The default script filename is no longer treated as an agent-host path.
|
||||||
|
- Missing custom scripts fall back to the bundled generic handler and log a warning.
|
||||||
|
- Generic installs default to `{SERVER_ROOT}/workshop/{MOD_FOLDER}`.
|
||||||
|
- DayZ/Arma installs keep `{SERVER_ROOT}/{MOD_FOLDER}` for `@<workshop_id>` compatibility.
|
||||||
|
|
||||||
## Manifest Fields
|
## Manifest Fields
|
||||||
|
|
||||||
Phase 1 manifests include:
|
Phase 1 manifests include:
|
||||||
|
|
@ -95,6 +102,11 @@ Default install folder:
|
||||||
|
|
||||||
- `@<workshop_id>`
|
- `@<workshop_id>`
|
||||||
|
|
||||||
|
Arma 3 game XML now declares:
|
||||||
|
|
||||||
|
- Workshop app ID: `107410`
|
||||||
|
- Dedicated server app ID: `233780` through the existing `installer_name`
|
||||||
|
|
||||||
Key-copy behavior:
|
Key-copy behavior:
|
||||||
|
|
||||||
- `.bikey` files found anywhere in the installed mod folder are copied to `{SERVER_HOME}/keys`.
|
- `.bikey` files found anywhere in the installed mod folder are copied to `{SERVER_HOME}/keys`.
|
||||||
|
|
@ -149,10 +161,16 @@ Phase 2 should generate structured mod lists from enabled `server_content_worksh
|
||||||
- full Steam URL accepted
|
- full Steam URL accepted
|
||||||
- invalid text rejected
|
- invalid text rejected
|
||||||
- Temporary manifest test with fake SteamCMD confirmed:
|
- Temporary manifest test with fake SteamCMD confirmed:
|
||||||
- Linux script installs into `@<workshop_id>`
|
- Linux script installs generic content into `workshop/@<workshop_id>`
|
||||||
- Windows/Cygwin script installs into `@<workshop_id>`
|
- Windows/Cygwin script installs generic content into `workshop/@<workshop_id>`
|
||||||
|
- Arma/DayZ strategies resolve to root `@<workshop_id>` folders
|
||||||
- `.bikey` files are copied to `keys`
|
- `.bikey` files are copied to `keys`
|
||||||
- useful logs are written under `gsp_server_content`
|
- useful logs are written under `gsp_server_content`
|
||||||
|
- Current smoke tests also confirm:
|
||||||
|
- default Linux/Windows script staging no longer reports `script not found`
|
||||||
|
- full URLs, numeric IDs, whitespace, comma, newline, duplicates, and invalid values parse correctly
|
||||||
|
- generic no-template installs use `server_root/workshop`
|
||||||
|
- missing SteamCMD returns a clear failure
|
||||||
|
|
||||||
## Phase 2 Work
|
## Phase 2 Work
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,13 @@ The Panel syncs the bundled install script to:
|
||||||
|
|
||||||
The agent executes the synced script with the manifest path. Customers do not need to place scripts manually on the agent.
|
The agent executes the synced script with the manifest path. Customers do not need to place scripts manually on the agent.
|
||||||
|
|
||||||
|
Script selection rules:
|
||||||
|
|
||||||
|
1. If game XML defines a custom `workshop_script_linux` or `workshop_script_windows` and that script exists on the agent, use it.
|
||||||
|
2. If the custom script is missing, log a fallback warning and stage the bundled generic handler.
|
||||||
|
3. If no custom script is defined, stage the bundled generic handler for the server OS.
|
||||||
|
4. The default script filename must never be treated as a pre-existing agent path. The Panel must copy the bundled script first.
|
||||||
|
|
||||||
The manifest includes:
|
The manifest includes:
|
||||||
|
|
||||||
- `home_id`
|
- `home_id`
|
||||||
|
|
@ -52,7 +59,17 @@ The manifest includes:
|
||||||
- key-copy settings
|
- key-copy settings
|
||||||
- content template metadata
|
- content template metadata
|
||||||
|
|
||||||
DayZ/Arma-style installs default to `dayz_mod_folder` or `arma_mod_folder` based on the game key/name/config file. Those strategies install to `@<workshop_id>` by default and copy `.bikey` files into the server `keys` folder when found. Missing key files are logged but do not fail the install.
|
Default install paths:
|
||||||
|
|
||||||
|
- Generic Workshop installs default to `{SERVER_ROOT}/workshop/{MOD_FOLDER}`.
|
||||||
|
- DayZ/Arma-style installs default to `dayz_mod_folder` or `arma_mod_folder` based on the game key/name/config file. Those strategies install to `{SERVER_ROOT}/{MOD_FOLDER}` so `@<workshop_id>` folders remain compatible with existing `-mod=` workflows.
|
||||||
|
- DayZ/Arma key-copy behavior copies `.bikey` files into the server `keys` folder when found. Missing key files are logged but do not fail the install.
|
||||||
|
|
||||||
|
App ID rules:
|
||||||
|
|
||||||
|
- `workshop_app_id` must come from a Server Content template, Steam Workshop profile, or game XML.
|
||||||
|
- Do not silently use the dedicated server Steam app ID as the Workshop app ID unless a legacy profile explicitly does so.
|
||||||
|
- Arma 3 XML declares Workshop app ID `107410`; its dedicated server Steam app ID remains `233780`.
|
||||||
|
|
||||||
## Database State
|
## Database State
|
||||||
|
|
||||||
|
|
@ -101,6 +118,15 @@ The current direction already supports:
|
||||||
- caching and cleanup policy need product-level design, not just ad hoc scripts.
|
- caching and cleanup policy need product-level design, not just ad hoc scripts.
|
||||||
- `-mod=` / `-serverMod=` generation still needs a safe structured implementation.
|
- `-mod=` / `-serverMod=` generation still needs a safe structured implementation.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Meaning | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| `Configured workshop script not found on agent host: generic_steam_workshop_windows_cygwin.sh` | Old Panel logic treated the default script filename as an agent path. | Update the Panel. Current logic stages the bundled handler under `gsp_server_content/scripts/workshop/`. |
|
||||||
|
| `SteamCMD is missing on the agent host.` | The handler could not find SteamCMD at the configured path, `STEAMCMD_PATH`, or common locations. | Install SteamCMD on the agent and/or set the SteamCMD path in the Workshop profile/template. |
|
||||||
|
| `Workshop App ID is missing` | No template/profile/XML provided an app ID. | Add `workshop_app_id` to the Server Content template or game XML. |
|
||||||
|
| Download succeeds but mod does not load | Startup parameters are not yet regenerated from installed Workshop rows. | Manually add the installed `@...` folders to the game startup params until Phase 2 startup integration is complete. |
|
||||||
|
|
||||||
## Recommended Mental Model
|
## Recommended Mental Model
|
||||||
|
|
||||||
Use `addonsmanager` as the main future home for:
|
Use `addonsmanager` as the main future home for:
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,23 @@ For Workshop items, the current flow lets users enter Workshop IDs or full Steam
|
||||||
9. Agent executes the script with the manifest path.
|
9. Agent executes the script with the manifest path.
|
||||||
10. Script runs SteamCMD, copies Workshop content into the configured target path, copies DayZ/Arma `.bikey` files when applicable, and writes a log under `gsp_server_content`.
|
10. Script runs SteamCMD, copies Workshop content into the configured target path, copies DayZ/Arma `.bikey` files when applicable, and writes a log under `gsp_server_content`.
|
||||||
|
|
||||||
|
Current script fallback behavior:
|
||||||
|
|
||||||
|
- Admin-defined custom scripts are supported when they exist on the agent.
|
||||||
|
- Missing custom scripts fall back to the bundled generic handler and are logged.
|
||||||
|
- Default script names such as `generic_steam_workshop_windows_cygwin.sh` are copied from the Panel module source and must not be checked as bare files on the agent.
|
||||||
|
|
||||||
|
Current default install paths:
|
||||||
|
|
||||||
|
- Generic Steam Workshop content: `{SERVER_ROOT}/workshop/{MOD_FOLDER}`
|
||||||
|
- DayZ / Arma strategy content: `{SERVER_ROOT}/{MOD_FOLDER}` for root `@<workshop_id>` folder compatibility
|
||||||
|
|
||||||
|
SteamCMD requirements:
|
||||||
|
|
||||||
|
- Linux agents need SteamCMD available at the configured profile/template path, `STEAMCMD_PATH`, `/home/gameserver/steamcmd/steamcmd.sh`, or in `PATH`.
|
||||||
|
- Windows agents currently use the existing Cygwin agent model and run the bundled Cygwin-compatible shell handler. SteamCMD may be provided as `steamcmd.exe`, `steamcmd.sh`, an explicit configured path, or via `STEAMCMD_PATH`.
|
||||||
|
- Missing SteamCMD should return a clear error, not a generic script failure.
|
||||||
|
|
||||||
The legacy `steam_workshop` monitor button is intentionally suppressed so users are not sent to the deprecated standalone module.
|
The legacy `steam_workshop` monitor button is intentionally suppressed so users are not sent to the deprecated standalone module.
|
||||||
|
|
||||||
## Current Limitations
|
## Current Limitations
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue