Merge pull request #54 from GameServerPanel/copilot/search-steam-workshop-mods

Improve Steam Workshop search AppID resolution and picker selection
This commit is contained in:
Frank Harris 2026-01-31 13:13:13 -06:00 committed by GitHub
commit d235d5e91e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 57 additions and 16 deletions

View file

@ -1,6 +1,8 @@
# Changelog
## 2026-01-31
- Added adapter AppID lookup for Workshop search so the picker can query Steam even when the server XML lacks a clear default installer entry.
- Switched Workshop picker results to checkbox selection, letting customers toggle multiple mods directly from the search list.
- Added a hardened Workshop scraping helper (the same HTML workflow we validated manually) and wired it into the Steam Workshop service as a fallback whenever the official API errors out or returns empty data.
- Added a native PHP HTTP scraper fallback (auto-selected when bash/proc_open is unavailable, e.g., on Windows XAMPP installs) so the Game Monitor search stops showing “Unable to contact the Steam Workshop” when the API dies but the HTML workflow still works.
- Surface scraper vs API attempts (including shell commands, exit codes, and stderr) in the JSON response so the Game Monitor can show exactly which backend produced the results.

View file

@ -2,3 +2,4 @@
- Allow players/admins to reorder selected Workshop mods in the new picker UI so load order matches game expectations.
- Surface pagination controls in the Workshop picker so users can request additional batches from the new Steam Web API search endpoint.
- Add an admin-facing toggle that makes it clear when the HTML scraper fallback is in use and lets staff force API-only mode if Valve ever objects.
- Add Workshop result preview thumbnails and author links in the picker for easier browsing.

View file

@ -1 +1 @@
Last Updated at 11:40am on 2025-06-12
Last Updated at 6:32pm on 2026-01-31

View file

@ -620,6 +620,20 @@ class SteamWorkshopService
public function getSteamAppIdForGameKey(string $gameKey): ?string
{
$gameKey = trim($gameKey);
if ($gameKey === '') {
return null;
}
$adapterKey = $this->getAdapterKeyForGame($gameKey);
if ($adapterKey !== null && $adapterKey !== '') {
$adapter = $this->getAdapterByKey($adapterKey);
$adapterAppId = isset($adapter['steam_app_id']) ? trim((string)$adapter['steam_app_id']) : '';
if ($adapterAppId !== '') {
return $adapterAppId;
}
}
$xml = $this->loadServerConfigXml($gameKey);
if ($xml === null) {
return null;

View file

@ -514,6 +514,12 @@
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;
}
@ -563,6 +569,17 @@
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;

View file

@ -109,17 +109,22 @@
});
}
if (this.resultsBody) {
this.resultsBody.addEventListener('click', function (event) {
this.resultsBody.addEventListener('change', function (event) {
var target = event.target;
if (!(target instanceof HTMLElement)) {
if (!(target instanceof HTMLInputElement)) {
return;
}
if (target.matches('.js-sw-add')) {
if (target.matches('.js-sw-result-toggle')) {
var payload = target.getAttribute('data-payload');
if (payload) {
try {
var data = JSON.parse(payload);
_this.addSelected(data);
if (target.checked) {
_this.addSelected(data);
}
else {
_this.removeSelected(String(data.id));
}
}
catch (err) {
console.warn('Invalid payload', err);
@ -208,15 +213,18 @@
};
var row = document.createElement('tr');
var selectCell = document.createElement('td');
var action = document.createElement('button');
action.type = 'button';
action.className = 'btn btn-xs sw-picker__action js-sw-add';
action.textContent = this_1.lang.add;
action.setAttribute('data-payload', JSON.stringify(normalized));
if (this_1.isSelected(normalized.id)) {
action.disabled = true;
}
selectCell.appendChild(action);
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');
@ -261,14 +269,12 @@
});
this.persist();
this.renderSelected();
this.renderResults(this.lastResults || []);
};
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();
this.renderResults(this.lastResults || []);
};
Picker.prototype.toggleSelected = function (id, enabled) {
var changed = false;

View file

@ -80,6 +80,7 @@ $langAttrs = [
<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>