
-

+

From 0fe8146ad19cc3b1034d15d2b3c44965a7b1bc4b Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 23 Sep 2026 14:01:56 -0400
Subject: [PATCH 04/12] docs: restore dashboard Codecov badge
---
.github/workflows/ci-tests.yml | 6 +++---
README.md | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 94e595a047..2be839ec22 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -32,6 +32,9 @@ jobs:
id: test
run: npm run test:ci
+ - name: Lint
+ run: npm run lint
+
- name: Upload test coverage
# any except cancelled or skipped
if:
@@ -65,6 +68,3 @@ jobs:
report_type: test_results
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
-
- - name: Lint
- run: npm run lint
diff --git a/README.md b/README.md
index 2bf7281eee..f9d94551b8 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@

-

+

@@ -67,7 +67,7 @@ Game submissions resolve the submitted slug through [IGDB's authenticated API](h
The Pages workflow creates an archive from the database and site template, then calls the same [LizardByte Jekyll build workflow](https://github.com/LizardByte/LizardByte.github.io/blob/master/.github/workflows/jekyll-build.yml) used by GameDB and ThemerrDB. It deploys to `gh-pages` after changes to `master` or an approved database update.
-Before enabling automation in a new repository, create the `database` and `gh-pages` branches from `master`, set Pages to deploy from `gh-pages`, and configure `GH_BOT_TOKEN`, `GH_BOT_EMAIL`, `GH_BOT_NAME`, `TWITCH_CLIENT_ID`, and `TWITCH_CLIENT_SECRET` as in the existing LizardByte database projects. `GH_BOT_TOKEN` needs issue, contents, and Actions write access to publish the database and dispatch the Pages build. The Twitch credentials resolve IGDB slugs; GameDB itself needs no credentials. Create the `request-game-preset`, `request-app-preset`, `approve-queue`, and `approve-preset` labels. Set the GitHub repository description to the heading description above. The `database` branch retains the `database/` directory; approved records are written there.
+The empty `database` branch is initialized with `database/apps` and `database/games`. Before enabling automation, create the `gh-pages` branch from `master`, set Pages to deploy from `gh-pages`, and configure `GH_BOT_TOKEN`, `GH_BOT_EMAIL`, `GH_BOT_NAME`, `TWITCH_CLIENT_ID`, and `TWITCH_CLIENT_SECRET` as in the existing LizardByte database projects. `GH_BOT_TOKEN` needs issue, contents, and Actions write access to publish the database and dispatch the Pages build. The Twitch credentials resolve IGDB slugs; GameDB itself needs no credentials. Create the `request-game-preset`, `request-app-preset`, `approve-queue`, and `approve-preset` labels. Set the GitHub repository description to the heading description above. The `database` branch retains the `database/` directory; approved records are written there.
Connect this repository to Read the Docs and enable pull request preview builds. The preview configuration downloads the `site-source` artifact from the Pages build job and uses the shared LizardByte theme. See [developer setup](docs/developerSetup.md) for local commands.
From 827faaf91433cfb7f34c17f4850cf8f9b5b3a1af Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 23 Sep 2026 14:44:02 -0400
Subject: [PATCH 05/12] Refine preset submission methods and generated names
---
.github/ISSUE_TEMPLATE/app-preset.yml | 24 +--------
.github/ISSUE_TEMPLATE/game-preset.yml | 15 +++---
.readthedocs.yaml | 2 +-
README.md | 12 ++---
docs/approverGuide.md | 4 +-
docs/developerSetup.md | 10 +++-
docs/presetGuidelines.md | 10 ++--
gh-pages-template/assets/js/app.js | 4 +-
src/database.js | 26 +++++++---
src/issue.js | 3 +-
src/presets.js | 71 +++++++++++++++++---------
tests/presets.test.js | 61 +++++++++++++++-------
12 files changed, 144 insertions(+), 98 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/app-preset.yml b/.github/ISSUE_TEMPLATE/app-preset.yml
index c06eac75e8..ffe9811795 100644
--- a/.github/ISSUE_TEMPLATE/app-preset.yml
+++ b/.github/ISSUE_TEMPLATE/app-preset.yml
@@ -9,7 +9,7 @@ body:
attributes:
value: |
Apps have no GameDB record. A maintainer reviews the official URL and command before approval.
- Submit one host OS and launch option per issue.
+ Submit one host OS and command per issue. The preset name is generated from the app name and OS.
- type: input
id: app_name
attributes:
@@ -39,33 +39,13 @@ body:
- macOS
validations:
required: true
- - type: dropdown
- id: method
- attributes:
- label: Launch method
- options:
- - Native
- - Steam
- - Epic Games
- - GOG
- - Emulator
- - Other
- validations:
- required: true
- - type: input
- id: preset_name
- attributes:
- label: Preset name
- description: Short display name for this launch option.
- validations:
- required: true
- type: input
id: command
attributes:
label: Command
description: |
Use {{HOME}} for a variable path. Windows also allows {{SYSTEM_DRIVE}}, {{PROGRAM_FILES}}, and
- {{PROGRAM_FILES_X86}}. Steam launcher URIs are stored as Sunshine detached commands automatically.
+ {{PROGRAM_FILES_X86}}. Use only host-independent paths; literal home directories are rejected.
validations:
required: true
- type: input
diff --git a/.github/ISSUE_TEMPLATE/game-preset.yml b/.github/ISSUE_TEMPLATE/game-preset.yml
index 3b5e90272c..1e9a0f4c9a 100644
--- a/.github/ISSUE_TEMPLATE/game-preset.yml
+++ b/.github/ISSUE_TEMPLATE/game-preset.yml
@@ -9,7 +9,7 @@ body:
attributes:
value: |
One issue represents one launch option. Provide the IGDB game URL; the bot resolves its ID and checks GameDB.
- Use a descriptive preset name to distinguish stores, emulators, cores, or other choices.
+ Add an emulator variant only when the operating system and launch method cannot distinguish the preset.
- type: input
id: game_url
attributes:
@@ -36,24 +36,23 @@ body:
- Steam
- Epic Games
- GOG
+ - Microsoft Store
- Emulator
- - Other
validations:
required: true
- type: input
- id: preset_name
+ id: variant_name
attributes:
- label: Preset name
- description: Short display name, for example RetroArch with Snes9x.
- validations:
- required: true
+ label: Emulator variant name
+ description: Optional emulator, console, or core label when needed to distinguish launch options.
- type: input
id: command
attributes:
label: Command
description: |
Use {{ROM_PATH}} or {{HOME}} for variable paths. Windows also allows {{SYSTEM_DRIVE}}, {{PROGRAM_FILES}},
- and {{PROGRAM_FILES_X86}}. Steam launcher URIs are stored as Sunshine detached commands automatically.
+ and {{PROGRAM_FILES_X86}}. Use only host-independent paths; literal home directories are rejected.
+ Steam launcher URIs require detached commands; use a Steam executable command.
validations:
required: true
- type: input
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index e57188179f..89e2d2c8f2 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -22,4 +22,4 @@ build:
chmod +x "./tmp/readthedocs_build.sh"
build:
html:
- - GITHUB_WORKFLOW=build SITE_ARTIFACT=site-source EXTRACT_ARCHIVE=build.zip ./tmp/readthedocs_build.sh
+ - ./tmp/readthedocs_build.sh
diff --git a/README.md b/README.md
index f9d94551b8..d565c9dad0 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
-Community maintained launch presets for [Sunshine](https://github.com/LizardByte/Sunshine). Each game or app can have many presets across Windows, Linux, and macOS. Launch methods include direct executables, Steam, Epic Games, GOG, emulators, and other options. GitHub issue numbers provide stable preset IDs, while descriptive names distinguish emulator cores or store editions.
+Community maintained launch presets for [Sunshine](https://github.com/LizardByte/Sunshine). Each game or app can have many presets across Windows, Linux, and macOS. Game launch methods include Native, Steam, Epic Games, GOG, Microsoft Store, and Emulator. App presets use the app name and host OS. GitHub issue numbers provide stable IDs, while the bot generates names and accepts optional emulator variants.
Each GitHub issue requests **one preset**. Game requests must identify a [GameDB](https://app.lizardbyte.dev/GameDB/) record. App requests use a separate form and require a name and official source URL for maintainer review. A bot validates the request, and an authorized reviewer enters it into the approval queue. No code or pull request is needed to contribute.
@@ -46,15 +46,15 @@ The website publishes [catalog statistics](https://app.lizardbyte.dev/PresetDB/s
## Contribute
1. Read the [preset guidelines](docs/presetGuidelines.md). For a game, copy its [IGDB game URL](https://www.igdb.com/) and [open a game preset request](https://github.com/LizardByte/PresetDB/issues/new?template=game-preset.yml). The bot resolves the URL slug to the IGDB numeric ID and checks GameDB. For another app, [open an app preset request](https://github.com/LizardByte/PresetDB/issues/new?template=app-preset.yml) with its official URL.
-2. Fill in **one** launch option. Choose the host OS and launch method, then give the preset a descriptive name such as `RetroArch with Snes9x` or `Steam URI`. Open another issue for another OS or option.
-3. Fill in one **Command**. Sunshine's [app examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md) show different Steam URI forms for Windows, Linux, and macOS. The bot stores Steam launcher URIs as Sunshine detached commands. Epic launcher URIs and executable commands become `cmd`. Commands are stored as text and never executed by validation or by the website.
+2. Fill in **one** launch option. Choose the host OS and, for a game, its launch method. Add an emulator variant such as `RetroArch Snes9x` only when needed to distinguish emulator presets. The bot generates the preset name.
+3. Fill in one **Command**. The bot publishes it as Sunshine `cmd`. Sunshine's [app examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md) use detached commands for Steam launcher URIs, so use a Steam executable command instead. Commands are stored as text and never executed by validation or by the website.
4. To replace a preset, provide its issue number and explain the change. The bot preserves the original preset ID.
After validation, a listed trusted game contributor enters the approval queue automatically. Other requests wait for a listed approver or repository admin to comment `@LizardByte-bot approve`. App requests always require this separate review. The queue processes one approval at a time; see the [approver guide](docs/approverGuide.md) and [bot commands](docs/botCommands.md).
-Supported path placeholders in Command and Working directory are `{{ROM_PATH}}` and `{{HOME}}`; Windows also allows `{{SYSTEM_DRIVE}}`, `{{PROGRAM_FILES}}`, and `{{PROGRAM_FILES_X86}}`. Unknown or malformed placeholders are rejected. Literal commands still require maintainer review.
+Supported path placeholders in Command and Working directory are `{{ROM_PATH}}` for emulator games and `{{HOME}}`; Windows also allows `{{SYSTEM_DRIVE}}`, `{{PROGRAM_FILES}}`, and `{{PROGRAM_FILES_X86}}`. Unknown placeholders, literal home directories, and Windows reserved device names are rejected. Literal commands still require maintainer review.
-Game submissions resolve the submitted slug through [IGDB's authenticated API](https://api-docs.igdb.com/), then check the resolved ID and slug against the live [GameDB JSON API](https://github.com/LizardByte/GameDB#data). An unavailable or mismatched result blocks approval. App submissions have no GameDB counterpart and always need separate maintainer review. Steam and Epic launcher URI syntax is checked against the selected host OS and method; binary commands still need human review.
+Game submissions resolve the submitted slug through [IGDB's authenticated API](https://api-docs.igdb.com/), then check the resolved ID and slug against the live [GameDB JSON API](https://github.com/LizardByte/GameDB#data). An unavailable or mismatched result blocks approval. App submissions have no GameDB counterpart and always need separate maintainer review. Steam URIs are rejected because they require detached commands; Epic URI syntax is checked against Windows and the Epic Games method. Binary commands still need human review.
## Repository and deployment
@@ -69,7 +69,7 @@ The Pages workflow creates an archive from the database and site template, then
The empty `database` branch is initialized with `database/apps` and `database/games`. Before enabling automation, create the `gh-pages` branch from `master`, set Pages to deploy from `gh-pages`, and configure `GH_BOT_TOKEN`, `GH_BOT_EMAIL`, `GH_BOT_NAME`, `TWITCH_CLIENT_ID`, and `TWITCH_CLIENT_SECRET` as in the existing LizardByte database projects. `GH_BOT_TOKEN` needs issue, contents, and Actions write access to publish the database and dispatch the Pages build. The Twitch credentials resolve IGDB slugs; GameDB itself needs no credentials. Create the `request-game-preset`, `request-app-preset`, `approve-queue`, and `approve-preset` labels. Set the GitHub repository description to the heading description above. The `database` branch retains the `database/` directory; approved records are written there.
-Connect this repository to Read the Docs and enable pull request preview builds. The preview configuration downloads the `site-source` artifact from the Pages build job and uses the shared LizardByte theme. See [developer setup](docs/developerSetup.md) for local commands.
+Connect this repository to Read the Docs and enable pull request preview builds. Set `GITHUB_WORKFLOW=build`, `SITE_ARTIFACT=site-source`, and `EXTRACT_ARCHIVE=build.zip` in the Read the Docs project environment. The shared script downloads the Pages build artifact and extracts the nested site archive. See [developer setup](docs/developerSetup.md) for local commands.
## Local checks
diff --git a/docs/approverGuide.md b/docs/approverGuide.md
index eb883e44e0..d80bd280e6 100644
--- a/docs/approverGuide.md
+++ b/docs/approverGuide.md
@@ -4,8 +4,8 @@ This guide is for listed approvers and repository administrators reviewing game
1. Open the oldest validated request in the [game queue](https://github.com/LizardByte/PresetDB/issues?q=is%3Aopen+label%3Arequest-game-preset) or [app queue](https://github.com/LizardByte/PresetDB/issues?q=is%3Aopen+label%3Arequest-app-preset). Check the latest validation comment after the last issue edit.
2. For games, check the IGDB URL and resolved GameDB record. For apps, inspect the official URL and optional image URL. App requests always need a separate review, including when a trusted contributor submits them.
-3. Check the host OS, launch method, command, working directory, and setup notes against the [preset guidelines](presetGuidelines.md) and [Sunshine examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md). Review literal commands carefully; automated validation does not run them.
-4. Search the published record for an equivalent name, OS, and method. A replacement must identify an existing preset issue and explain the change.
+3. Check the host OS, game launch method, command, working directory, and setup notes against the [preset guidelines](presetGuidelines.md) and [Sunshine examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md). Review literal commands carefully; automated validation does not run them.
+4. Search the published record for the same OS, method, and emulator variant. A replacement must identify an existing preset issue and explain the change.
5. Comment `@LizardByte-bot approve` to enter the approval queue. The bot adds `approve-queue` and starts `approve-preset` when the active slot is free. It revalidates against IGDB and GameDB before writing to the `database` branch.
The approval workflow comments its result. A successful approval closes the issue and requests a Pages rebuild. On failure, it removes the issue from the queue so the next request can proceed; fix the issue and queue it again. The `approve-preset` label remains on successfully closed issues for the approved count badge.
diff --git a/docs/developerSetup.md b/docs/developerSetup.md
index ea4da8a0a1..78205ad036 100644
--- a/docs/developerSetup.md
+++ b/docs/developerSetup.md
@@ -16,4 +16,12 @@ node src/build-site.js --database database --output site-build
The site builder writes `index.json`, `stats.json`, two SVG contribution charts, and game and app records. It uses the `gh-pages-template` directory as the Jekyll source. The Pages workflow packages that output for the shared LizardByte Jekyll workflow.
-Read the Docs pull request previews use `.readthedocs.yaml` and the shared `readthedocs_build.sh` script. Connect the repository to Read the Docs, enable pull request builds, and ensure the `Build Pages` workflow can upload its `site-source` artifact for the PR commit. The shared script downloads `build.zip` from that artifact and builds the preview with the organization theme. Hosted preview and Pages deployment require the repository, credentials, and branch settings described in the README.
+Read the Docs pull request previews use `.readthedocs.yaml` and the shared `readthedocs_build.sh` script. Connect the repository to Read the Docs, enable pull request builds, and set these project environment variables:
+
+```text
+GITHUB_WORKFLOW=build
+SITE_ARTIFACT=site-source
+EXTRACT_ARCHIVE=build.zip
+```
+
+The `Build Pages` workflow publishes a check run named `build`, uploads a `site-source` artifact containing `build.zip`, and the shared script extracts that nested archive before building with the organization theme. Hosted preview and Pages deployment require the repository, credentials, and branch settings described in the README.
diff --git a/docs/presetGuidelines.md b/docs/presetGuidelines.md
index ba7ee8ebea..3955f0215e 100644
--- a/docs/presetGuidelines.md
+++ b/docs/presetGuidelines.md
@@ -1,21 +1,21 @@
# Preset Guidelines
-One issue requests one Sunshine launch option for one host operating system. Submit another issue for a different store, emulator, core, or operating system. Give each option a descriptive name so it can be distinguished from presets already listed on the website.
+One issue requests one Sunshine launch option for one host operating system. Submit another issue for a different store, emulator, core, or operating system. The bot generates a name from the game or app name, host OS, and game launch method.
## Games
Use the public IGDB game URL. The bot resolves its slug to a numeric IGDB ID and requires the same game and slug in GameDB. The catalog obtains the game cover from GameDB, so the form has no image field.
-Choose the launch method that describes the command: Native, Steam, Epic Games, GOG, Emulator, or Other. Emulator details such as console, launcher, and core belong in the preset name or notes. They are not required fields.
+Choose the launch method that describes the command: Native, Steam, Epic Games, GOG, Microsoft Store, or Emulator. Microsoft Store is available on Windows only. An optional emulator variant can identify the console, launcher, or core when multiple emulator presets share an OS.
## Apps
-Provide the official HTTPS homepage or source repository. An app image is optional and must be an HTTPS URL. App requests have no GameDB record and always require separate maintainer review.
+Provide the official HTTPS homepage or source repository. An app image is optional and must be an HTTPS URL. App requests have no launch method or variant field; the generated name uses the app name and OS. App requests have no GameDB record and always require separate maintainer review.
## Commands and paths
-Enter one command. Sunshine's [application examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md) show the platform-specific Steam and Epic forms. Steam launcher URIs are stored as Sunshine detached commands; Epic launcher URIs and executable commands are stored as normal commands. The validator checks known URI forms against the selected OS and launch method. A maintainer reviews other commands before approval.
+Enter one command. PresetDB publishes it as Sunshine `cmd`. Sunshine's [application examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md) use detached commands for Steam launcher URIs, so submit a Steam executable command instead. Epic launcher URIs are supported for Windows Epic Games presets. A maintainer reviews other commands before approval.
-The supported path placeholders are `{{ROM_PATH}}` and `{{HOME}}`. Windows also supports `{{SYSTEM_DRIVE}}`, `{{PROGRAM_FILES}}`, and `{{PROGRAM_FILES_X86}}`. These placeholders must be replaced with paths on the Sunshine host before use. Do not include a private username or a path that only exists on your computer.
+The supported path placeholders are `{{ROM_PATH}}` and `{{HOME}}`. Windows also supports `{{SYSTEM_DRIVE}}`, `{{PROGRAM_FILES}}`, and `{{PROGRAM_FILES_X86}}`. Replace placeholders with paths on the Sunshine host before use. `{{ROM_PATH}}` is for emulator games. Literal user home paths and Windows reserved device names are rejected in Command and Working directory.
For replacements, enter the issue number displayed with the published preset and explain what changed. The original preset ID is retained.
diff --git a/gh-pages-template/assets/js/app.js b/gh-pages-template/assets/js/app.js
index 46cf6ddedf..7690e4acbc 100644
--- a/gh-pages-template/assets/js/app.js
+++ b/gh-pages-template/assets/js/app.js
@@ -111,9 +111,7 @@ function boot() {
const column = element('div', 'col');
const card = element('article', 'card h-100 rounded-0 shadow-sm');
const body = element('div', 'card-body');
- const methodLabels = { native: 'Native', steam: 'Steam', 'epic-games': 'Epic Games', gog: 'GOG', emulator: 'Emulator', other: 'Other' };
- const label = [preset.os, methodLabels[preset.method] || preset.method, preset.name].join(' · ');
- body.append(element('h3', 'h5 card-title fw-bold', label));
+ body.append(element('h3', 'h5 card-title fw-bold', preset.name));
if (preset.notes) body.append(element('p', 'card-text', preset.notes));
const command = element('pre', 'p-3 rounded bg-dark text-light overflow-auto');
command.append(element('code', '', sunshineSnippet(preset)));
diff --git a/src/database.js b/src/database.js
index cfd868862a..9573b6b02a 100644
--- a/src/database.js
+++ b/src/database.js
@@ -43,6 +43,17 @@ function prepareRecord(root, preset) {
return { file, record, name };
}
+function displayName(name, preset) {
+ if (preset.kind === 'app') return `${name} (${preset.os})`;
+ const labels = {
+ native: 'Native', steam: 'Steam', 'epic-games': 'Epic Games',
+ gog: 'GOG', 'microsoft-store': 'Microsoft Store', emulator: 'Emulator'
+ };
+ const method = labels[preset.method];
+ const variant = preset.variantName ? `: ${preset.variantName}` : '';
+ return `${name} (${preset.os}, ${method}${variant})`;
+}
+
function replacementIndex(record, preset) {
const previous = preset.replacementIssue == null ? -1 : record.presets.findIndex(item =>
item.origin_issue === preset.replacementIssue || item.source_issue === preset.replacementIssue
@@ -50,10 +61,11 @@ function replacementIndex(record, preset) {
if (preset.replacementIssue != null && previous < 0) {
throw new PresetError(`No preset for issue #${preset.replacementIssue} exists under this ${preset.kind}`);
}
- const normalizedName = preset.presetName.normalize('NFKC').toLocaleLowerCase();
+ const normalizedVariant = (preset.variantName || '').normalize('NFKC').toLocaleLowerCase();
if (record.presets.some((item, index) => index !== previous && item.os === preset.os &&
- item.method === preset.method && item.name.normalize('NFKC').toLocaleLowerCase() === normalizedName)) {
- throw new PresetError('A preset with this name, OS, and method already exists');
+ item.method === preset.method &&
+ (item.variant_name || '').normalize('NFKC').toLocaleLowerCase() === normalizedVariant)) {
+ throw new PresetError('A preset with this OS, method, and variant already exists');
}
return previous;
}
@@ -73,11 +85,13 @@ function mergePreset(root, preset, {
issue: issueNumber, action: previous >= 0 ? 'replace' : 'add',
author_id: authorId, author_login: authorLogin, approved_at: approvedAt
});
+ const generatedName = displayName(name, preset);
const entry = {
- id: presetId, name: preset.presetName, os: preset.os, method: preset.method,
+ id: presetId, name: generatedName, os: preset.os, method: preset.method,
+ ...(preset.variantName ? { variant_name: preset.variantName } : {}),
sunshine: {
- name: `${name} (${preset.presetName})`,
- ...(preset.commandMode === 'detached' ? { detached: [preset.command] } : { cmd: preset.command }),
+ name: generatedName,
+ cmd: preset.command,
...(preset.workingDir ? { 'working-dir': preset.workingDir } : {})
},
notes: preset.notes,
diff --git a/src/issue.js b/src/issue.js
index 70e1d22d63..80cfaff31e 100644
--- a/src/issue.js
+++ b/src/issue.js
@@ -49,8 +49,9 @@ async function main(args = process.argv.slice(2)) {
});
const item = result.kind === 'game' ? `GameDB game ${result.preset.gameId}` : `app ${result.preset.appName}`;
const entry = result.record.presets.find(preset => preset.id === result.id);
+ const methodLine = result.kind === 'game' ? `- Method: ${result.preset.method}\n` : '';
message = `Preset ${result.action === 'replace' ? 'replacement' : 'request'} validated for ${item}.\n\n` +
- `- Host: ${result.preset.os}\n- Method: ${result.preset.method}\n- Preset ID: \`${result.id}\`\n` +
+ `- Host: ${result.preset.os}\n` + methodLine + `- Preset ID: \`${result.id}\`\n` +
`- Status: ${options.mode === 'approve' ? 'approved and saved' : 'awaiting maintainer review'}\n\n` +
`Sunshine application preview:\n\n\`\`\`json\n${JSON.stringify(entry.sunshine, null, 2)}\n\`\`\`\n`;
success = true;
diff --git a/src/presets.js b/src/presets.js
index d43f96fd54..20a26664d2 100644
--- a/src/presets.js
+++ b/src/presets.js
@@ -10,7 +10,7 @@ const FIELD_NAMES = {
'App image URL': 'appImageUrl',
'Host operating system': 'os',
'Launch method': 'method',
- 'Preset name': 'presetName',
+ 'Emulator variant name': 'variantName',
Command: 'command',
'Working directory': 'workingDir',
Notes: 'notes',
@@ -70,6 +70,19 @@ function validatePlaceholders(value, os, label) {
}
}
+function validatePortablePath(value, os, label) {
+ // Personal home locations cannot be shared between Sunshine hosts.
+ const normalized = value.replace(/\\/g, '/');
+ const literalHome = /(?:^|[\s"'=])(?:~(?:\/|$)|[a-z]:\/(?:users|documents and settings)\/[^/\s"']+|\/(?:home|users)\/[^/\s"']+|%userprofile%|%homepath%|\$(?:home|\{home\}|\(home\)))/i;
+ if (literalHome.test(normalized)) {
+ throw new PresetError(`${label} contains a literal home directory; use {{HOME}}`);
+ }
+ if (os === 'Windows' &&
+ /(?:^|[\/\s"'=])(?:con|prn|aux|nul|com[1-9\u00B9\u00B2\u00B3]|lpt[1-9\u00B9\u00B2\u00B3])(?:\.[^/\s"']*)?(?=$|[\/\s"'])/i.test(normalized)) {
+ throw new PresetError(`${label} contains a reserved Windows device name`);
+ }
+}
+
function gameIdentity(values) {
const gameUrl = field(values, 'gameUrl', { required: true, limit: 300, singleLine: true });
let url;
@@ -97,35 +110,27 @@ function appIdentity(values) {
httpsUrl(appUrl, 'Official app URL');
const appImageUrl = field(values, 'appImageUrl', { limit: 500, singleLine: true });
if (appImageUrl) httpsUrl(appImageUrl, 'App image URL');
+ const appId = slug(appName);
+ if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/.test(appId)) {
+ throw new PresetError('App name resolves to a reserved Windows file name');
+ }
return {
- gameId: null, gameSlug: null, appId: slug(appName), appName, appUrl, appImageUrl
+ gameId: null, gameSlug: null, appId, appName, appUrl, appImageUrl
};
}
-function validateSteamUri(command, os) {
- const uri = String.raw`steam://(?:rungameid/\d+|open/bigpicture)`;
- let pattern = `^${uri}$`;
- if (os === 'Linux') pattern = `^setsid steam ${uri}$`;
- else if (os === 'macOS') pattern = `^open ${uri}$`;
- if (!new RegExp(pattern, 'i').test(command)) {
- throw new PresetError(`Steam URI must use Sunshine's ${os} command form`);
- }
-}
-
function validateLaunchCommand(command, os, method) {
const steamUri = /steam:\/\//i.test(command);
const epicUri = /com\.epicgames\.launcher:\/\//i.test(command);
- if (steamUri && method !== 'Steam') {
- throw new PresetError('Steam URI requires the Steam launch method');
+ if (steamUri) {
+ throw new PresetError('Steam URI needs a detached command; use a Steam executable command instead');
}
if (epicUri && method !== 'Epic Games') {
throw new PresetError('Epic Games URI requires the Epic Games launch method');
}
- if (steamUri) validateSteamUri(command, os);
if (epicUri && (os !== 'Windows' || !/^com\.epicgames\.launcher:\/\/apps\/[^\s?]+(?:\?[^\s]+)?$/i.test(command))) {
throw new PresetError('Epic Games launcher URI is supported for Windows only');
}
- return steamUri ? 'detached' : 'cmd';
}
function replacementFields(values) {
@@ -141,21 +146,39 @@ function replacementFields(values) {
function validateFields(values, kind) {
if (kind !== 'game' && kind !== 'app') throw new PresetError('Exactly one request type is required');
const os = field(values, 'os', { required: true, limit: 20, singleLine: true });
- const method = field(values, 'method', { required: true, limit: 30, singleLine: true });
- if (!['Windows', 'Linux', 'macOS'].includes(os) ||
- !['Native', 'Steam', 'Epic Games', 'GOG', 'Emulator', 'Other'].includes(method)) {
- throw new PresetError('Choose a supported host OS and launch method');
+ if (!['Windows', 'Linux', 'macOS'].includes(os)) {
+ throw new PresetError('Choose a supported host OS');
+ }
+ const suppliedMethod = field(values, 'method', { limit: 30, singleLine: true });
+ const method = kind === 'app' ? 'Native' : suppliedMethod;
+ if (kind === 'app' && suppliedMethod) {
+ throw new PresetError('App requests do not have a launch method');
+ }
+ if (kind === 'game' &&
+ !['Native', 'Steam', 'Epic Games', 'GOG', 'Microsoft Store', 'Emulator'].includes(method)) {
+ throw new PresetError('Choose a supported game launch method');
+ }
+ if (method === 'Microsoft Store' && os !== 'Windows') {
+ throw new PresetError('Microsoft Store is available on Windows only');
+ }
+ const variantName = field(values, 'variantName', { limit: 100, singleLine: true });
+ if (variantName && method !== 'Emulator') {
+ throw new PresetError('An emulator variant name requires the Emulator launch method');
}
- const presetName = field(values, 'presetName', { required: true, limit: 100, singleLine: true });
const identity = kind === 'game' ? gameIdentity(values) : appIdentity(values);
const command = field(values, 'command', { required: true, singleLine: true });
const workingDir = field(values, 'workingDir', { limit: 512, singleLine: true });
validatePlaceholders(command, os, 'Launch command');
validatePlaceholders(workingDir, os, 'Working directory');
- const commandMode = validateLaunchCommand(command, os, method);
+ if (method !== 'Emulator' && (command.includes('{{ROM_PATH}}') || workingDir.includes('{{ROM_PATH}}'))) {
+ throw new PresetError('{{ROM_PATH}} requires the Emulator launch method');
+ }
+ validatePortablePath(command, os, 'Launch command');
+ validatePortablePath(workingDir, os, 'Working directory');
+ validateLaunchCommand(command, os, method);
return {
- kind, ...identity, presetName, ...replacementFields(values),
- os, method: slug(method), command, commandMode,
+ kind, ...identity, variantName: variantName || null, ...replacementFields(values),
+ os, method: slug(method), command,
workingDir: workingDir || null,
notes: field(values, 'notes', { limit: 2000 }) || null
};
diff --git a/tests/presets.test.js b/tests/presets.test.js
index e3fd47ca35..df33aec8d2 100644
--- a/tests/presets.test.js
+++ b/tests/presets.test.js
@@ -14,7 +14,7 @@ const { filterItems, sunshineSnippet, normalizeBasePath } = require('../gh-pages
const credentials = { clientId: 'client-id', clientSecret: 'client-secret' };
const gameValues = {
gameUrl: 'https://www.igdb.com/games/one-tap-hero', os: 'Windows', method: 'Emulator',
- presetName: 'RetroArch Snes9x', command: 'retroarch -L snes9x "{{ROM_PATH}}"',
+ variantName: 'RetroArch Snes9x', command: 'retroarch -L snes9x "{{ROM_PATH}}"',
workingDir: '{{HOME}}', notes: 'Install the core first.'
};
@@ -43,7 +43,7 @@ function formBody(values) {
const labels = {
gameUrl: 'IGDB game URL', appName: 'App name', appUrl: 'Official app URL',
appImageUrl: 'App image URL', os: 'Host operating system', method: 'Launch method',
- presetName: 'Preset name', command: 'Command', workingDir: 'Working directory',
+ variantName: 'Emulator variant name', command: 'Command', workingDir: 'Working directory',
notes: 'Notes', replacementIssue: 'Preset to replace (issue number)',
replacementReason: 'Replacement reason'
};
@@ -59,11 +59,22 @@ test('game submission needs no ID and validates path placeholders', () => {
const preset = validateFields(gameValues, 'game');
assert.equal(preset.gameId, null);
assert.equal(preset.gameSlug, 'one-tap-hero');
- assert.equal(preset.commandMode, 'cmd');
+ assert.equal(preset.variantName, 'RetroArch Snes9x');
assert.equal(validateFields({ ...gameValues, command: '{{PROGRAM_FILES}}\\Game\\game.exe' }, 'game').os, 'Windows');
assert.throws(() => validateFields({ ...gameValues, os: 'Linux', command: '{{PROGRAM_FILES}}/game' }, 'game'), /Windows only/);
assert.throws(() => validateFields({ ...gameValues, command: '{{EMULATOR_PATH}} -L core' }, 'game'), /unsupported placeholder/);
assert.throws(() => validateFields({ ...gameValues, command: '{{rom_path}}' }, 'game'), /malformed path placeholder/);
+ for (const command of ['C:\\Users\\Alice\\Game\\game.exe', '%USERPROFILE%\\Game\\game.exe']) {
+ assert.throws(() => validateFields({ ...gameValues, command }, 'game'), /literal home directory/);
+ }
+ for (const workingDir of ['C:\\Games\\CON.txt', 'C:\\Games\\COM1\\Game', 'C:\\Games\\COM\u00B9']) {
+ assert.throws(() => validateFields({ ...gameValues, workingDir }, 'game'), /reserved Windows device name/);
+ }
+ assert.throws(() => validateFields({ ...gameValues, os: 'Linux', workingDir: '/home/alice/Games' }, 'game'),
+ /literal home directory/);
+ assert.throws(() => validateFields({ ...gameValues, os: 'macOS', workingDir: '/Users/alice/Games' }, 'game'),
+ /literal home directory/);
+ assert.throws(() => validateFields({ ...gameValues, workingDir: '~/Games' }, 'game'), /literal home directory/);
});
test('IGDB slug resolves to ID and GameDB verifies it', async () => {
@@ -90,26 +101,33 @@ test('IGDB resolution rejects missing credentials and missing or ambiguous recor
test('apps use an HTTPS image URL and have separate review identity', () => {
const values = { appName: 'My App', appUrl: 'https://example.org/app', appImageUrl: 'https://example.org/icon.png',
- os: 'macOS', method: 'Native', presetName: 'Installed', command: '{{HOME}}/Applications/my-app' };
+ os: 'macOS', command: '{{HOME}}/Applications/my-app' };
const app = validateFields(values, 'app');
assert.equal(app.appId, 'my-app');
assert.equal(app.appImageUrl, values.appImageUrl);
assert.throws(() => validateFields({ ...values, appUrl: 'http://example.org' }, 'app'), /HTTPS/);
assert.throws(() => validateFields({ ...values, appImageUrl: 'C:\\icon.png' }, 'app'), /image URL/);
+ assert.throws(() => validateFields({ ...values, appName: 'CON' }, 'app'), /reserved Windows file name/);
assert.throws(() => validateFields({ ...values, command: '{{APP_PATH}}' }, 'app'), /unsupported placeholder/);
+ assert.equal(app.method, 'native');
+ assert.throws(() => validateFields({ ...values, method: 'Steam' }, 'app'), /do not have a launch method/);
+ assert.throws(() => validateFields({ ...values, command: '{{ROM_PATH}}' }, 'app'), /ROM_PATH.*Emulator/);
});
-test('one command classifies Steam and Epic URI forms by host OS', () => {
- const base = { ...gameValues, method: 'Steam', presetName: 'Steam URI' };
- assert.equal(validateFields({ ...base, command: 'steam://rungameid/464920' }, 'game').commandMode, 'detached');
- assert.throws(() => validateFields({ ...base, os: 'Linux', command: 'steam://rungameid/464920' }, 'game'), /Linux/);
- assert.equal(validateFields({ ...base, os: 'Linux', command: 'setsid steam steam://rungameid/464920' }, 'game').commandMode, 'detached');
- assert.equal(validateFields({ ...base, os: 'macOS', command: 'open steam://rungameid/464920' }, 'game').commandMode, 'detached');
- assert.throws(() => validateFields({ ...base, method: 'Native', command: 'steam://rungameid/464920' }, 'game'), /Steam launch method/);
- assert.equal(validateFields({ ...base, command: 'steam -applaunch 464920' }, 'game').commandMode, 'cmd');
+test('game methods validate host OS and command forms', () => {
+ const base = { ...gameValues, variantName: '' };
+ assert.throws(() => validateFields({ ...base, method: 'Steam', command: 'steam://rungameid/464920' }, 'game'),
+ /detached command/);
+ assert.equal(validateFields({ ...base, method: 'Steam', command: 'steam -applaunch 464920' }, 'game').method, 'steam');
const epic = { ...base, method: 'Epic Games', command: 'com.epicgames.launcher://apps/abc?action=launch&silent=true' };
- assert.equal(validateFields(epic, 'game').commandMode, 'cmd');
+ assert.equal(validateFields(epic, 'game').method, 'epic-games');
assert.throws(() => validateFields({ ...epic, os: 'Linux' }, 'game'), /Windows only/);
+ assert.equal(validateFields({ ...base, method: 'Microsoft Store', command: 'explorer.exe shell:AppsFolder\\Game!App' }, 'game').method,
+ 'microsoft-store');
+ assert.throws(() => validateFields({ ...base, os: 'Linux', method: 'Microsoft Store' }, 'game'), /Windows only/);
+ assert.throws(() => validateFields({ ...base, method: 'Other' }, 'game'), /supported game launch method/);
+ assert.throws(() => validateFields({ ...base, method: 'Native' }, 'game'), /ROM_PATH.*Emulator/);
+ assert.throws(() => validateFields({ ...base, method: 'Native', variantName: 'Alternate' }, 'game'), /Emulator/);
});
test('approved presets get issue IDs and replacements preserve identity', async t => {
@@ -118,7 +136,9 @@ test('approved presets get issue IDs and replacements preserve identity', async
const first = await validateGameDb(validateFields(gameValues, 'game'), mockApis, credentials);
assert.equal(mergePreset(root, first, { issueNumber: 10, approvedBy: 'maintainer' }, { write: true }).id, 'issue-10');
assert.throws(() => mergePreset(root, first, { issueNumber: 11, approvedBy: 'maintainer' }), /already exists/);
- const second = await validateGameDb(validateFields({ ...gameValues, presetName: 'RetroArch Bsnes' }, 'game'), mockApis, credentials);
+ assert.throws(() => mergePreset(root, { ...first, variantName: 'retroarch snes9x' },
+ { issueNumber: 11, approvedBy: 'maintainer' }), /already exists/);
+ const second = await validateGameDb(validateFields({ ...gameValues, variantName: 'RetroArch Bsnes' }, 'game'), mockApis, credentials);
mergePreset(root, second, { issueNumber: 12, approvedBy: 'maintainer' }, { write: true });
const replacement = await validateGameDb(validateFields({ ...gameValues, command: 'new-command',
replacementIssue: '10', replacementReason: 'Old command failed' }, 'game'), mockApis, credentials);
@@ -126,6 +146,7 @@ test('approved presets get issue IDs and replacements preserve identity', async
const record = JSON.parse(fs.readFileSync(path.join(root, 'games/100245.json')));
assert.equal(record.presets.length, 2);
assert.equal(record.presets.find(item => item.id === 'issue-10').sunshine.cmd, 'new-command');
+ assert.equal(record.presets.find(item => item.id === 'issue-10').name, 'One Tap Hero (Windows, Emulator: RetroArch Snes9x)');
assert.equal(record.presets.find(item => item.id === 'issue-10').origin_issue, 10);
assert.equal(record.presets.find(item => item.id === 'issue-10').source_issue, 13);
assert.throws(() => validateFields({ ...gameValues, replacementIssue: '10' }, 'game'), /replacement needs both/);
@@ -133,14 +154,15 @@ test('approved presets get issue IDs and replacements preserve identity', async
{ issueNumber: 14, approvedBy: 'maintainer' }), /No preset/);
});
-test('Steam URI publishes as Sunshine detached command without an image path', async t => {
+test('Steam executable publishes as a Sunshine command without an image path', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'preset-steam-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const steam = await validateGameDb(validateFields({ ...gameValues, method: 'Steam',
- presetName: 'Steam URI', command: 'steam://rungameid/464920' }, 'game'), mockApis, credentials);
+ variantName: '', command: 'steam -applaunch 464920' }, 'game'), mockApis, credentials);
const { record } = mergePreset(root, steam, { issueNumber: 30, approvedBy: 'reviewer' }, { write: true });
- assert.deepEqual(record.presets[0].sunshine.detached, ['steam://rungameid/464920']);
- assert.ok(!Object.hasOwn(record.presets[0].sunshine, 'cmd'));
+ assert.equal(record.presets[0].sunshine.cmd, 'steam -applaunch 464920');
+ assert.equal(record.presets[0].sunshine.name, 'One Tap Hero (Windows, Steam)');
+ assert.ok(!Object.hasOwn(record.presets[0].sunshine, 'detached'));
assert.ok(!Object.hasOwn(record.presets[0].sunshine, 'image-path'));
});
@@ -153,12 +175,13 @@ test('issue processing and site build publish both game and app JSON', async t =
database, { approve: true, actor: 'reviewer', fetcher: mockApis, credentials });
await processIssue({ issue: { number: 22, labels: [{ name: 'request-app-preset' }], body: formBody({
appName: 'App One', appUrl: 'https://example.org', appImageUrl: 'https://example.org/icon.png',
- os: 'Linux', method: 'Native', presetName: 'Installed', command: '{{HOME}}/app-one'
+ os: 'Linux', command: '{{HOME}}/app-one'
}) } }, database, { approve: true, actor: 'reviewer' });
const index = buildSite(database, path.join(__dirname, '..', 'gh-pages-template'), output);
assert.equal(index.games[0].preset_count, 1);
assert.equal(index.apps[0].id, 'app-one');
assert.equal(index.apps[0].image_url, 'https://example.org/icon.png');
+ assert.equal(JSON.parse(fs.readFileSync(path.join(output, 'apps/app-one.json'))).presets[0].name, 'App One (Linux)');
assert.ok(fs.existsSync(path.join(output, 'games/100245.json')));
assert.ok(fs.existsSync(path.join(output, 'apps/app-one.json')));
assert.ok(fs.existsSync(path.join(output, 'top_contributors.svg')));
From c811f69f9af25c50e0cf9d4418f7117ff4735a5c Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 23 Sep 2026 14:52:40 -0400
Subject: [PATCH 06/12] Simplify portable path validation for Sonar
---
.readthedocs.yaml | 2 +-
src/presets.js | 9 +++++----
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 89e2d2c8f2..e57188179f 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -22,4 +22,4 @@ build:
chmod +x "./tmp/readthedocs_build.sh"
build:
html:
- - ./tmp/readthedocs_build.sh
+ - GITHUB_WORKFLOW=build SITE_ARTIFACT=site-source EXTRACT_ARCHIVE=build.zip ./tmp/readthedocs_build.sh
diff --git a/src/presets.js b/src/presets.js
index 20a26664d2..ca085fafc8 100644
--- a/src/presets.js
+++ b/src/presets.js
@@ -72,13 +72,14 @@ function validatePlaceholders(value, os, label) {
function validatePortablePath(value, os, label) {
// Personal home locations cannot be shared between Sunshine hosts.
- const normalized = value.replace(/\\/g, '/');
- const literalHome = /(?:^|[\s"'=])(?:~(?:\/|$)|[a-z]:\/(?:users|documents and settings)\/[^/\s"']+|\/(?:home|users)\/[^/\s"']+|%userprofile%|%homepath%|\$(?:home|\{home\}|\(home\)))/i;
- if (literalHome.test(normalized)) {
+ const normalized = value.replaceAll('\\', '/');
+ const homeRoot = /(?:^|[\s"'=])(?:~\/|[a-z]:\/(?:users|documents and settings)\/|\/(?:home|users)\/)/i;
+ const homeVariable = /%userprofile%|%homepath%|\$home|\$\{home\}|\$\(home\)/i;
+ if (homeRoot.test(normalized) || homeVariable.test(normalized)) {
throw new PresetError(`${label} contains a literal home directory; use {{HOME}}`);
}
if (os === 'Windows' &&
- /(?:^|[\/\s"'=])(?:con|prn|aux|nul|com[1-9\u00B9\u00B2\u00B3]|lpt[1-9\u00B9\u00B2\u00B3])(?:\.[^/\s"']*)?(?=$|[\/\s"'])/i.test(normalized)) {
+ /(?:^|[/\s"'=])(?:con|prn|aux|nul|com[1-9\u00B9\u00B2\u00B3]|lpt[1-9\u00B9\u00B2\u00B3])(?:\.[^/\s"']*)?(?=$|[/\s"'])/i.test(normalized)) {
throw new PresetError(`${label} contains a reserved Windows device name`);
}
}
From f283ece88b1ed67de99c94ce380888178bb1ebc2 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 23 Sep 2026 15:00:07 -0400
Subject: [PATCH 07/12] Generate ThemerrDB-style request titles and fix preview
artifact
---
.github/ISSUE_TEMPLATE/app-preset.yml | 2 +-
.github/ISSUE_TEMPLATE/game-preset.yml | 2 +-
.github/workflows/check-preset.yml | 19 ++++++++++++++++++-
.gitignore | 2 ++
.readthedocs.yaml | 2 +-
README.md | 4 ++--
docs/developerSetup.md | 2 +-
src/issue.js | 8 +++++++-
tests/presets.test.js | 9 ++++++---
9 files changed, 39 insertions(+), 11 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/app-preset.yml b/.github/ISSUE_TEMPLATE/app-preset.yml
index ffe9811795..2abf89a986 100644
--- a/.github/ISSUE_TEMPLATE/app-preset.yml
+++ b/.github/ISSUE_TEMPLATE/app-preset.yml
@@ -1,7 +1,7 @@
---
name: App launch preset
description: Request one Sunshine launch option for a non-game app.
-title: '[APP PRESET]: '
+title: '[APP]: '
labels:
- request-app-preset
body:
diff --git a/.github/ISSUE_TEMPLATE/game-preset.yml b/.github/ISSUE_TEMPLATE/game-preset.yml
index 1e9a0f4c9a..58321c0ae5 100644
--- a/.github/ISSUE_TEMPLATE/game-preset.yml
+++ b/.github/ISSUE_TEMPLATE/game-preset.yml
@@ -1,7 +1,7 @@
---
name: Game launch preset
description: Request one Sunshine launch option for a GameDB game.
-title: '[GAME PRESET]: '
+title: '[GAME]: '
labels:
- request-game-preset
body:
diff --git a/.github/workflows/check-preset.yml b/.github/workflows/check-preset.yml
index 550e06698d..11758bcb7d 100644
--- a/.github/workflows/check-preset.yml
+++ b/.github/workflows/check-preset.yml
@@ -53,7 +53,9 @@ jobs:
EVENT_FILE: ${{ github.event_path }}
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
- run: node src/issue.js --event "$EVENT_FILE" --database database-branch/database --mode check --report report.md
+ run: |
+ node src/issue.js --event "$EVENT_FILE" --database database-branch/database \
+ --mode check --report report.md --title title.md
- name: Post result
if: always() && hashFiles('report.md') != ''
@@ -62,6 +64,21 @@ jobs:
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: gh issue comment "$ISSUE_NUMBER" --body-file report.md
+ - name: Name request
+ if: steps.validate.outcome == 'success' && hashFiles('title.md') != ''
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const fs = require('node:fs')
+ const title = fs.readFileSync('title.md', 'utf8').trim()
+ if (title !== context.payload.issue.title) {
+ await github.rest.issues.update({
+ ...context.repo,
+ issue_number: context.issue.number,
+ title
+ })
+ }
+
- name: Queue trusted game submission
if: steps.validate.outcome == 'success' && contains(github.event.issue.labels.*.name, 'request-game-preset')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
diff --git a/.gitignore b/.gitignore
index c18fedade2..52b11a9714 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@ node_modules/
coverage/
junit.xml
lcov.info
+report.md
+title.md
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index e57188179f..596738f4f2 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -22,4 +22,4 @@ build:
chmod +x "./tmp/readthedocs_build.sh"
build:
html:
- - GITHUB_WORKFLOW=build SITE_ARTIFACT=site-source EXTRACT_ARCHIVE=build.zip ./tmp/readthedocs_build.sh
+ - GITHUB_WORKFLOW=build SITE_ARTIFACT=site-source.zip EXTRACT_ARCHIVE=build.zip ./tmp/readthedocs_build.sh
diff --git a/README.md b/README.md
index d565c9dad0..28e492ff28 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ The website publishes [catalog statistics](https://app.lizardbyte.dev/PresetDB/s
## Contribute
1. Read the [preset guidelines](docs/presetGuidelines.md). For a game, copy its [IGDB game URL](https://www.igdb.com/) and [open a game preset request](https://github.com/LizardByte/PresetDB/issues/new?template=game-preset.yml). The bot resolves the URL slug to the IGDB numeric ID and checks GameDB. For another app, [open an app preset request](https://github.com/LizardByte/PresetDB/issues/new?template=app-preset.yml) with its official URL.
-2. Fill in **one** launch option. Choose the host OS and, for a game, its launch method. Add an emulator variant such as `RetroArch Snes9x` only when needed to distinguish emulator presets. The bot generates the preset name.
+2. Fill in **one** launch option. Choose the host OS and, for a game, its launch method. Add an emulator variant such as `RetroArch Snes9x` only when needed to distinguish emulator presets. The bot generates the preset name and updates the issue title, as ThemerrDB does.
3. Fill in one **Command**. The bot publishes it as Sunshine `cmd`. Sunshine's [app examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md) use detached commands for Steam launcher URIs, so use a Steam executable command instead. Commands are stored as text and never executed by validation or by the website.
4. To replace a preset, provide its issue number and explain the change. The bot preserves the original preset ID.
@@ -69,7 +69,7 @@ The Pages workflow creates an archive from the database and site template, then
The empty `database` branch is initialized with `database/apps` and `database/games`. Before enabling automation, create the `gh-pages` branch from `master`, set Pages to deploy from `gh-pages`, and configure `GH_BOT_TOKEN`, `GH_BOT_EMAIL`, `GH_BOT_NAME`, `TWITCH_CLIENT_ID`, and `TWITCH_CLIENT_SECRET` as in the existing LizardByte database projects. `GH_BOT_TOKEN` needs issue, contents, and Actions write access to publish the database and dispatch the Pages build. The Twitch credentials resolve IGDB slugs; GameDB itself needs no credentials. Create the `request-game-preset`, `request-app-preset`, `approve-queue`, and `approve-preset` labels. Set the GitHub repository description to the heading description above. The `database` branch retains the `database/` directory; approved records are written there.
-Connect this repository to Read the Docs and enable pull request preview builds. Set `GITHUB_WORKFLOW=build`, `SITE_ARTIFACT=site-source`, and `EXTRACT_ARCHIVE=build.zip` in the Read the Docs project environment. The shared script downloads the Pages build artifact and extracts the nested site archive. See [developer setup](docs/developerSetup.md) for local commands.
+Connect this repository to Read the Docs and enable pull request preview builds. Set `GITHUB_WORKFLOW=build`, `SITE_ARTIFACT=site-source.zip`, and `EXTRACT_ARCHIVE=build.zip` in the Read the Docs project environment. The shared script downloads the Pages build artifact and extracts the nested site archive. See [developer setup](docs/developerSetup.md) for local commands.
## Local checks
diff --git a/docs/developerSetup.md b/docs/developerSetup.md
index 78205ad036..99f2d0fba0 100644
--- a/docs/developerSetup.md
+++ b/docs/developerSetup.md
@@ -20,7 +20,7 @@ Read the Docs pull request previews use `.readthedocs.yaml` and the shared `read
```text
GITHUB_WORKFLOW=build
-SITE_ARTIFACT=site-source
+SITE_ARTIFACT=site-source.zip
EXTRACT_ARCHIVE=build.zip
```
diff --git a/src/issue.js b/src/issue.js
index 80cfaff31e..aa1e68fdc0 100644
--- a/src/issue.js
+++ b/src/issue.js
@@ -23,7 +23,9 @@ async function processIssue(event, database, { approve = false, actor = '', fetc
issueNumber: issue.number, approvedBy: actor,
authorId: issue.user?.id ?? null, authorLogin: issue.user?.login ?? null
}, { write: approve });
- return { ...merged, preset, kind };
+ const entry = merged.record.presets.find(item => item.id === merged.id);
+ const title = `[${kind.toUpperCase()}]: ${entry.name}`;
+ return { ...merged, preset, kind, title };
}
function argsToObject(args) {
@@ -54,6 +56,10 @@ async function main(args = process.argv.slice(2)) {
`- Host: ${result.preset.os}\n` + methodLine + `- Preset ID: \`${result.id}\`\n` +
`- Status: ${options.mode === 'approve' ? 'approved and saved' : 'awaiting maintainer review'}\n\n` +
`Sunshine application preview:\n\n\`\`\`json\n${JSON.stringify(entry.sunshine, null, 2)}\n\`\`\`\n`;
+ if (options.title) {
+ fs.mkdirSync(path.dirname(options.title), { recursive: true });
+ fs.writeFileSync(options.title, `${result.title}\n`);
+ }
success = true;
} catch (error) {
message = `Preset validation failed: ${String(error.message).replace(/[\r\n]+/g, ' ').slice(0, 500)}\n`;
diff --git a/tests/presets.test.js b/tests/presets.test.js
index df33aec8d2..777053d812 100644
--- a/tests/presets.test.js
+++ b/tests/presets.test.js
@@ -171,12 +171,15 @@ test('issue processing and site build publish both game and app JSON', async t =
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const database = path.join(root, 'database');
const output = path.join(root, 'site');
- await processIssue({ issue: { number: 21, labels: [{ name: 'request-game-preset' }], body: formBody(gameValues) } },
- database, { approve: true, actor: 'reviewer', fetcher: mockApis, credentials });
- await processIssue({ issue: { number: 22, labels: [{ name: 'request-app-preset' }], body: formBody({
+ const gameRequest = await processIssue({
+ issue: { number: 21, labels: [{ name: 'request-game-preset' }], body: formBody(gameValues) }
+ }, database, { approve: true, actor: 'reviewer', fetcher: mockApis, credentials });
+ assert.equal(gameRequest.title, '[GAME]: One Tap Hero (Windows, Emulator: RetroArch Snes9x)');
+ const appRequest = await processIssue({ issue: { number: 22, labels: [{ name: 'request-app-preset' }], body: formBody({
appName: 'App One', appUrl: 'https://example.org', appImageUrl: 'https://example.org/icon.png',
os: 'Linux', command: '{{HOME}}/app-one'
}) } }, database, { approve: true, actor: 'reviewer' });
+ assert.equal(appRequest.title, '[APP]: App One (Linux)');
const index = buildSite(database, path.join(__dirname, '..', 'gh-pages-template'), output);
assert.equal(index.games[0].preset_count, 1);
assert.equal(index.apps[0].id, 'app-one');
From c144c43501257429c9c82da7c686742789d7bf6c Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 23 Sep 2026 15:11:14 -0400
Subject: [PATCH 08/12] Align Read the Docs previews with ThemerrDB
---
.github/ISSUE_TEMPLATE/app-preset.yml | 2 +-
.github/ISSUE_TEMPLATE/game-preset.yml | 2 +-
.github/workflows/build-pages.yml | 5 ++---
.github/workflows/check-preset.yml | 19 +------------------
.gitignore | 2 --
.readthedocs.yaml | 5 ++++-
README.md | 4 ++--
docs/developerSetup.md | 6 +++---
src/issue.js | 8 +-------
tests/presets.test.js | 9 +++------
10 files changed, 18 insertions(+), 44 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/app-preset.yml b/.github/ISSUE_TEMPLATE/app-preset.yml
index 2abf89a986..ffe9811795 100644
--- a/.github/ISSUE_TEMPLATE/app-preset.yml
+++ b/.github/ISSUE_TEMPLATE/app-preset.yml
@@ -1,7 +1,7 @@
---
name: App launch preset
description: Request one Sunshine launch option for a non-game app.
-title: '[APP]: '
+title: '[APP PRESET]: '
labels:
- request-app-preset
body:
diff --git a/.github/ISSUE_TEMPLATE/game-preset.yml b/.github/ISSUE_TEMPLATE/game-preset.yml
index 58321c0ae5..1e9a0f4c9a 100644
--- a/.github/ISSUE_TEMPLATE/game-preset.yml
+++ b/.github/ISSUE_TEMPLATE/game-preset.yml
@@ -1,7 +1,7 @@
---
name: Game launch preset
description: Request one Sunshine launch option for a GameDB game.
-title: '[GAME]: '
+title: '[GAME PRESET]: '
labels:
- request-game-preset
body:
diff --git a/.github/workflows/build-pages.yml b/.github/workflows/build-pages.yml
index 2918cc807c..757cbabde7 100644
--- a/.github/workflows/build-pages.yml
+++ b/.github/workflows/build-pages.yml
@@ -47,14 +47,13 @@ jobs:
- name: Upload site artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
- name: site-source
+ name: update
path: build.zip
if-no-files-found: error
retention-days: 1
call-jekyll-build:
needs: build
- if: github.event_name != 'pull_request'
permissions:
contents: read
uses: LizardByte/LizardByte.github.io/.github/workflows/jekyll-build.yml@master
@@ -65,5 +64,5 @@ jobs:
clean_gh_pages: true
extract_archive: build.zip
gh_bot_name: ${{ vars.GH_BOT_NAME }}
- site_artifact: site-source
+ site_artifact: update
target_branch: gh-pages
diff --git a/.github/workflows/check-preset.yml b/.github/workflows/check-preset.yml
index 11758bcb7d..550e06698d 100644
--- a/.github/workflows/check-preset.yml
+++ b/.github/workflows/check-preset.yml
@@ -53,9 +53,7 @@ jobs:
EVENT_FILE: ${{ github.event_path }}
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
- run: |
- node src/issue.js --event "$EVENT_FILE" --database database-branch/database \
- --mode check --report report.md --title title.md
+ run: node src/issue.js --event "$EVENT_FILE" --database database-branch/database --mode check --report report.md
- name: Post result
if: always() && hashFiles('report.md') != ''
@@ -64,21 +62,6 @@ jobs:
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: gh issue comment "$ISSUE_NUMBER" --body-file report.md
- - name: Name request
- if: steps.validate.outcome == 'success' && hashFiles('title.md') != ''
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- with:
- script: |
- const fs = require('node:fs')
- const title = fs.readFileSync('title.md', 'utf8').trim()
- if (title !== context.payload.issue.title) {
- await github.rest.issues.update({
- ...context.repo,
- issue_number: context.issue.number,
- title
- })
- }
-
- name: Queue trusted game submission
if: steps.validate.outcome == 'success' && contains(github.event.issue.labels.*.name, 'request-game-preset')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
diff --git a/.gitignore b/.gitignore
index 52b11a9714..c18fedade2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,3 @@ node_modules/
coverage/
junit.xml
lcov.info
-report.md
-title.md
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 596738f4f2..587e03ee99 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -22,4 +22,7 @@ build:
chmod +x "./tmp/readthedocs_build.sh"
build:
html:
- - GITHUB_WORKFLOW=build SITE_ARTIFACT=site-source.zip EXTRACT_ARCHIVE=build.zip ./tmp/readthedocs_build.sh
+ - |
+ GITHUB_WORKFLOW="call-jekyll-build / Build Jekyll" \
+ SITE_ARTIFACT=update.zip EXTRACT_ARCHIVE=build.zip \
+ ./tmp/readthedocs_build.sh
diff --git a/README.md b/README.md
index 28e492ff28..9700a53d3b 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ The website publishes [catalog statistics](https://app.lizardbyte.dev/PresetDB/s
## Contribute
1. Read the [preset guidelines](docs/presetGuidelines.md). For a game, copy its [IGDB game URL](https://www.igdb.com/) and [open a game preset request](https://github.com/LizardByte/PresetDB/issues/new?template=game-preset.yml). The bot resolves the URL slug to the IGDB numeric ID and checks GameDB. For another app, [open an app preset request](https://github.com/LizardByte/PresetDB/issues/new?template=app-preset.yml) with its official URL.
-2. Fill in **one** launch option. Choose the host OS and, for a game, its launch method. Add an emulator variant such as `RetroArch Snes9x` only when needed to distinguish emulator presets. The bot generates the preset name and updates the issue title, as ThemerrDB does.
+2. Fill in **one** launch option. Choose the host OS and, for a game, its launch method. Add an emulator variant such as `RetroArch Snes9x` only when needed to distinguish emulator presets. The bot generates the preset name.
3. Fill in one **Command**. The bot publishes it as Sunshine `cmd`. Sunshine's [app examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md) use detached commands for Steam launcher URIs, so use a Steam executable command instead. Commands are stored as text and never executed by validation or by the website.
4. To replace a preset, provide its issue number and explain the change. The bot preserves the original preset ID.
@@ -69,7 +69,7 @@ The Pages workflow creates an archive from the database and site template, then
The empty `database` branch is initialized with `database/apps` and `database/games`. Before enabling automation, create the `gh-pages` branch from `master`, set Pages to deploy from `gh-pages`, and configure `GH_BOT_TOKEN`, `GH_BOT_EMAIL`, `GH_BOT_NAME`, `TWITCH_CLIENT_ID`, and `TWITCH_CLIENT_SECRET` as in the existing LizardByte database projects. `GH_BOT_TOKEN` needs issue, contents, and Actions write access to publish the database and dispatch the Pages build. The Twitch credentials resolve IGDB slugs; GameDB itself needs no credentials. Create the `request-game-preset`, `request-app-preset`, `approve-queue`, and `approve-preset` labels. Set the GitHub repository description to the heading description above. The `database` branch retains the `database/` directory; approved records are written there.
-Connect this repository to Read the Docs and enable pull request preview builds. Set `GITHUB_WORKFLOW=build`, `SITE_ARTIFACT=site-source.zip`, and `EXTRACT_ARCHIVE=build.zip` in the Read the Docs project environment. The shared script downloads the Pages build artifact and extracts the nested site archive. See [developer setup](docs/developerSetup.md) for local commands.
+Connect this repository to Read the Docs and enable pull request preview builds. Set `GITHUB_WORKFLOW=call-jekyll-build / Build Jekyll`, `SITE_ARTIFACT=update.zip`, and `EXTRACT_ARCHIVE=build.zip` in the Read the Docs project environment. The shared script downloads the Pages build artifact and extracts the nested site archive. See [developer setup](docs/developerSetup.md) for local commands.
## Local checks
diff --git a/docs/developerSetup.md b/docs/developerSetup.md
index 99f2d0fba0..6659258375 100644
--- a/docs/developerSetup.md
+++ b/docs/developerSetup.md
@@ -19,9 +19,9 @@ The site builder writes `index.json`, `stats.json`, two SVG contribution charts,
Read the Docs pull request previews use `.readthedocs.yaml` and the shared `readthedocs_build.sh` script. Connect the repository to Read the Docs, enable pull request builds, and set these project environment variables:
```text
-GITHUB_WORKFLOW=build
-SITE_ARTIFACT=site-source.zip
+GITHUB_WORKFLOW=call-jekyll-build / Build Jekyll
+SITE_ARTIFACT=update.zip
EXTRACT_ARCHIVE=build.zip
```
-The `Build Pages` workflow publishes a check run named `build`, uploads a `site-source` artifact containing `build.zip`, and the shared script extracts that nested archive before building with the organization theme. Hosted preview and Pages deployment require the repository, credentials, and branch settings described in the README.
+The `Build Pages` workflow publishes a check run named `call-jekyll-build / Build Jekyll`, uploads an `update` artifact containing `build.zip`, and the shared script extracts that nested archive before building with the organization theme. Hosted preview and Pages deployment require the repository, credentials, and branch settings described in the README.
diff --git a/src/issue.js b/src/issue.js
index aa1e68fdc0..80cfaff31e 100644
--- a/src/issue.js
+++ b/src/issue.js
@@ -23,9 +23,7 @@ async function processIssue(event, database, { approve = false, actor = '', fetc
issueNumber: issue.number, approvedBy: actor,
authorId: issue.user?.id ?? null, authorLogin: issue.user?.login ?? null
}, { write: approve });
- const entry = merged.record.presets.find(item => item.id === merged.id);
- const title = `[${kind.toUpperCase()}]: ${entry.name}`;
- return { ...merged, preset, kind, title };
+ return { ...merged, preset, kind };
}
function argsToObject(args) {
@@ -56,10 +54,6 @@ async function main(args = process.argv.slice(2)) {
`- Host: ${result.preset.os}\n` + methodLine + `- Preset ID: \`${result.id}\`\n` +
`- Status: ${options.mode === 'approve' ? 'approved and saved' : 'awaiting maintainer review'}\n\n` +
`Sunshine application preview:\n\n\`\`\`json\n${JSON.stringify(entry.sunshine, null, 2)}\n\`\`\`\n`;
- if (options.title) {
- fs.mkdirSync(path.dirname(options.title), { recursive: true });
- fs.writeFileSync(options.title, `${result.title}\n`);
- }
success = true;
} catch (error) {
message = `Preset validation failed: ${String(error.message).replace(/[\r\n]+/g, ' ').slice(0, 500)}\n`;
diff --git a/tests/presets.test.js b/tests/presets.test.js
index 777053d812..df33aec8d2 100644
--- a/tests/presets.test.js
+++ b/tests/presets.test.js
@@ -171,15 +171,12 @@ test('issue processing and site build publish both game and app JSON', async t =
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const database = path.join(root, 'database');
const output = path.join(root, 'site');
- const gameRequest = await processIssue({
- issue: { number: 21, labels: [{ name: 'request-game-preset' }], body: formBody(gameValues) }
- }, database, { approve: true, actor: 'reviewer', fetcher: mockApis, credentials });
- assert.equal(gameRequest.title, '[GAME]: One Tap Hero (Windows, Emulator: RetroArch Snes9x)');
- const appRequest = await processIssue({ issue: { number: 22, labels: [{ name: 'request-app-preset' }], body: formBody({
+ await processIssue({ issue: { number: 21, labels: [{ name: 'request-game-preset' }], body: formBody(gameValues) } },
+ database, { approve: true, actor: 'reviewer', fetcher: mockApis, credentials });
+ await processIssue({ issue: { number: 22, labels: [{ name: 'request-app-preset' }], body: formBody({
appName: 'App One', appUrl: 'https://example.org', appImageUrl: 'https://example.org/icon.png',
os: 'Linux', command: '{{HOME}}/app-one'
}) } }, database, { approve: true, actor: 'reviewer' });
- assert.equal(appRequest.title, '[APP]: App One (Linux)');
const index = buildSite(database, path.join(__dirname, '..', 'gh-pages-template'), output);
assert.equal(index.games[0].preset_count, 1);
assert.equal(index.apps[0].id, 'app-one');
From 6005fb57ebf77966a4ad92bdb18090bdb9a2e67b Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 23 Sep 2026 15:17:31 -0400
Subject: [PATCH 09/12] Document initialized gh-pages branch
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 9700a53d3b..dc794bc2d2 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,7 @@ Game submissions resolve the submitted slug through [IGDB's authenticated API](h
The Pages workflow creates an archive from the database and site template, then calls the same [LizardByte Jekyll build workflow](https://github.com/LizardByte/LizardByte.github.io/blob/master/.github/workflows/jekyll-build.yml) used by GameDB and ThemerrDB. It deploys to `gh-pages` after changes to `master` or an approved database update.
-The empty `database` branch is initialized with `database/apps` and `database/games`. Before enabling automation, create the `gh-pages` branch from `master`, set Pages to deploy from `gh-pages`, and configure `GH_BOT_TOKEN`, `GH_BOT_EMAIL`, `GH_BOT_NAME`, `TWITCH_CLIENT_ID`, and `TWITCH_CLIENT_SECRET` as in the existing LizardByte database projects. `GH_BOT_TOKEN` needs issue, contents, and Actions write access to publish the database and dispatch the Pages build. The Twitch credentials resolve IGDB slugs; GameDB itself needs no credentials. Create the `request-game-preset`, `request-app-preset`, `approve-queue`, and `approve-preset` labels. Set the GitHub repository description to the heading description above. The `database` branch retains the `database/` directory; approved records are written there.
+The empty `database` branch is initialized with `database/apps` and `database/games`. GitHub Pages serves the empty orphan `gh-pages` branch. Configure `GH_BOT_TOKEN`, `GH_BOT_EMAIL`, `GH_BOT_NAME`, `TWITCH_CLIENT_ID`, and `TWITCH_CLIENT_SECRET` as in the existing LizardByte database projects. `GH_BOT_TOKEN` needs issue, contents, and Actions write access to publish the database and dispatch the Pages build. The Twitch credentials resolve IGDB slugs; GameDB itself needs no credentials. Create the `request-game-preset`, `request-app-preset`, `approve-queue`, and `approve-preset` labels. Set the GitHub repository description to the heading description above. The `database` branch retains the `database/` directory; approved records are written there.
Connect this repository to Read the Docs and enable pull request preview builds. Set `GITHUB_WORKFLOW=call-jekyll-build / Build Jekyll`, `SITE_ARTIFACT=update.zip`, and `EXTRACT_ARCHIVE=build.zip` in the Read the Docs project environment. The shared script downloads the Pages build artifact and extracts the nested site archive. See [developer setup](docs/developerSetup.md) for local commands.
From 216ce00028c326d54ccbe70a684eb95a426794ab Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 23 Sep 2026 16:36:02 -0400
Subject: [PATCH 10/12] Generate storefront commands from launch IDs
---
.github/ISSUE_TEMPLATE/game-preset.yml | 22 +++++--
README.md | 6 +-
docs/approverGuide.md | 2 +-
docs/presetGuidelines.md | 6 +-
src/database.js | 1 +
src/presets.js | 56 ++++++++++++----
tests/presets.test.js | 88 +++++++++++++++++++++-----
7 files changed, 140 insertions(+), 41 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/game-preset.yml b/.github/ISSUE_TEMPLATE/game-preset.yml
index 1e9a0f4c9a..c54d9200bf 100644
--- a/.github/ISSUE_TEMPLATE/game-preset.yml
+++ b/.github/ISSUE_TEMPLATE/game-preset.yml
@@ -9,6 +9,7 @@ body:
attributes:
value: |
One issue represents one launch option. Provide the IGDB game URL; the bot resolves its ID and checks GameDB.
+ For Steam, Epic Games, or Microsoft Store, enter the matching launch ID; the bot creates the command.
Add an emulator variant only when the operating system and launch method cannot distinguish the preset.
- type: input
id: game_url
@@ -40,6 +41,17 @@ body:
- Emulator
validations:
required: true
+ - type: input
+ id: launch_id
+ attributes:
+ label: Launch ID
+ description: |
+ Required for Steam, Epic Games, and Microsoft Store; leave blank for Native, GOG, or Emulator.
+ Steam: numeric app ID from a store URL, for example 464920.
+ Epic Games: three-part ID between /apps/ and ? in a launcher shortcut,
+ for example fn%3A4fe75bbc5a674f4f9b356b5c90567da5%3AFortnite.
+ Microsoft Store: installed app's AUMID from Get-StartApps,
+ for example Microsoft.WindowsCalculator_8wekyb3d8bbwe!App. A Store product ID will not launch it.
- type: input
id: variant_name
attributes:
@@ -50,16 +62,14 @@ body:
attributes:
label: Command
description: |
- Use {{ROM_PATH}} or {{HOME}} for variable paths. Windows also allows {{SYSTEM_DRIVE}}, {{PROGRAM_FILES}},
- and {{PROGRAM_FILES_X86}}. Use only host-independent paths; literal home directories are rejected.
- Steam launcher URIs require detached commands; use a Steam executable command.
- validations:
- required: true
+ Required for Native, GOG, and Emulator. Leave blank for Steam, Epic Games, and Microsoft Store.
+ Use {{ROM_PATH}} or {{HOME}} for variable paths. Windows also allows {{SYSTEM_DRIVE}},
+ {{PROGRAM_FILES}}, and {{PROGRAM_FILES_X86}}. Literal home directories are rejected.
- type: input
id: working_dir
attributes:
label: Working directory
- description: Optional Sunshine working-dir value.
+ description: Optional Sunshine working-dir value for Native, GOG, and Emulator only.
- type: textarea
id: notes
attributes:
diff --git a/README.md b/README.md
index dc794bc2d2..a9feb2b5ab 100644
--- a/README.md
+++ b/README.md
@@ -47,14 +47,14 @@ The website publishes [catalog statistics](https://app.lizardbyte.dev/PresetDB/s
1. Read the [preset guidelines](docs/presetGuidelines.md). For a game, copy its [IGDB game URL](https://www.igdb.com/) and [open a game preset request](https://github.com/LizardByte/PresetDB/issues/new?template=game-preset.yml). The bot resolves the URL slug to the IGDB numeric ID and checks GameDB. For another app, [open an app preset request](https://github.com/LizardByte/PresetDB/issues/new?template=app-preset.yml) with its official URL.
2. Fill in **one** launch option. Choose the host OS and, for a game, its launch method. Add an emulator variant such as `RetroArch Snes9x` only when needed to distinguish emulator presets. The bot generates the preset name.
-3. Fill in one **Command**. The bot publishes it as Sunshine `cmd`. Sunshine's [app examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md) use detached commands for Steam launcher URIs, so use a Steam executable command instead. Commands are stored as text and never executed by validation or by the website.
+3. For Steam, Epic Games, or Microsoft Store, enter one **Launch ID** in the shared field: a Steam app ID, Epic's three-part launch ID, or a Microsoft Store AUMID. The bot generates Sunshine `cmd` from that ID and the host OS. For Native, GOG, and Emulator, enter one **Command**. Validation and the website never execute commands.
4. To replace a preset, provide its issue number and explain the change. The bot preserves the original preset ID.
After validation, a listed trusted game contributor enters the approval queue automatically. Other requests wait for a listed approver or repository admin to comment `@LizardByte-bot approve`. App requests always require this separate review. The queue processes one approval at a time; see the [approver guide](docs/approverGuide.md) and [bot commands](docs/botCommands.md).
-Supported path placeholders in Command and Working directory are `{{ROM_PATH}}` for emulator games and `{{HOME}}`; Windows also allows `{{SYSTEM_DRIVE}}`, `{{PROGRAM_FILES}}`, and `{{PROGRAM_FILES_X86}}`. Unknown placeholders, literal home directories, and Windows reserved device names are rejected. Literal commands still require maintainer review.
+Supported path placeholders in manual Command and Working directory are `{{ROM_PATH}}` for emulator games and `{{HOME}}`; Windows also allows `{{SYSTEM_DRIVE}}`, `{{PROGRAM_FILES}}`, and `{{PROGRAM_FILES_X86}}`. Unknown placeholders, literal home directories, and Windows reserved device names are rejected. Literal commands still require maintainer review.
-Game submissions resolve the submitted slug through [IGDB's authenticated API](https://api-docs.igdb.com/), then check the resolved ID and slug against the live [GameDB JSON API](https://github.com/LizardByte/GameDB#data). An unavailable or mismatched result blocks approval. App submissions have no GameDB counterpart and always need separate maintainer review. Steam URIs are rejected because they require detached commands; Epic URI syntax is checked against Windows and the Epic Games method. Binary commands still need human review.
+Game submissions resolve the submitted slug through [IGDB's authenticated API](https://api-docs.igdb.com/), then check the resolved ID and slug against the live [GameDB JSON API](https://github.com/LizardByte/GameDB#data). An unavailable or mismatched result blocks approval. App submissions have no GameDB counterpart and always need separate maintainer review. Steam supports all three host OSes, Epic Games supports Windows and macOS, and Microsoft Store requires a Windows AUMID. GOG and other manual commands need human review. Launcher ID commands may outlive Sunshine's process tracking; the stream may need to be ended manually.
## Repository and deployment
diff --git a/docs/approverGuide.md b/docs/approverGuide.md
index d80bd280e6..2420537f4c 100644
--- a/docs/approverGuide.md
+++ b/docs/approverGuide.md
@@ -4,7 +4,7 @@ This guide is for listed approvers and repository administrators reviewing game
1. Open the oldest validated request in the [game queue](https://github.com/LizardByte/PresetDB/issues?q=is%3Aopen+label%3Arequest-game-preset) or [app queue](https://github.com/LizardByte/PresetDB/issues?q=is%3Aopen+label%3Arequest-app-preset). Check the latest validation comment after the last issue edit.
2. For games, check the IGDB URL and resolved GameDB record. For apps, inspect the official URL and optional image URL. App requests always need a separate review, including when a trusted contributor submits them.
-3. Check the host OS, game launch method, command, working directory, and setup notes against the [preset guidelines](presetGuidelines.md) and [Sunshine examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md). Review literal commands carefully; automated validation does not run them.
+3. Check the host OS, game launch method, store launch ID or manual command, working directory, and setup notes against the [preset guidelines](presetGuidelines.md) and [Sunshine examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md). Confirm the store ID belongs to the requested game. Automated validation checks ID syntax and never runs commands.
4. Search the published record for the same OS, method, and emulator variant. A replacement must identify an existing preset issue and explain the change.
5. Comment `@LizardByte-bot approve` to enter the approval queue. The bot adds `approve-queue` and starts `approve-preset` when the active slot is free. It revalidates against IGDB and GameDB before writing to the `database` branch.
diff --git a/docs/presetGuidelines.md b/docs/presetGuidelines.md
index 3955f0215e..27448972fb 100644
--- a/docs/presetGuidelines.md
+++ b/docs/presetGuidelines.md
@@ -6,7 +6,7 @@ One issue requests one Sunshine launch option for one host operating system. Sub
Use the public IGDB game URL. The bot resolves its slug to a numeric IGDB ID and requires the same game and slug in GameDB. The catalog obtains the game cover from GameDB, so the form has no image field.
-Choose the launch method that describes the command: Native, Steam, Epic Games, GOG, Microsoft Store, or Emulator. Microsoft Store is available on Windows only. An optional emulator variant can identify the console, launcher, or core when multiple emulator presets share an OS.
+Choose Native, Steam, Epic Games, GOG, Microsoft Store, or Emulator. Steam accepts Windows, Linux, and macOS; Epic Games accepts Windows and macOS; Microsoft Store accepts Windows only. An optional emulator variant can identify the console, launcher, or core when multiple emulator presets share an OS.
## Apps
@@ -14,7 +14,9 @@ Provide the official HTTPS homepage or source repository. An app image is option
## Commands and paths
-Enter one command. PresetDB publishes it as Sunshine `cmd`. Sunshine's [application examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md) use detached commands for Steam launcher URIs, so submit a Steam executable command instead. Epic launcher URIs are supported for Windows Epic Games presets. A maintainer reviews other commands before approval.
+Use the single **Launch ID** field for Steam, Epic Games, or Microsoft Store. For Steam, enter the numeric app ID from a Steam store URL. For Epic Games, enter the three-part Sandbox ID, Catalog ID, and Artifact ID from the game's launcher shortcut, separated by colons or `%3A`. For Microsoft Store, enter the installed app's [AUMID](https://learn.microsoft.com/en-us/windows/configuration/store/find-aumid), available through `Get-StartApps`; a Store product ID opens a store page and cannot launch the installed game. Leave Command and Working directory blank for these methods. The bot generates the OS-specific Sunshine `cmd` and stores the launch ID with the preset.
+
+For Native, GOG, and Emulator, provide one Command. GOG games can launch without Galaxy, so no single GOG ID command is assumed. A maintainer reviews these commands before approval. Store launchers can exit before their game; Sunshine may keep the stream open until the user ends it. PresetDB uses only Sunshine `cmd`, with no detached command field.
The supported path placeholders are `{{ROM_PATH}}` and `{{HOME}}`. Windows also supports `{{SYSTEM_DRIVE}}`, `{{PROGRAM_FILES}}`, and `{{PROGRAM_FILES_X86}}`. Replace placeholders with paths on the Sunshine host before use. `{{ROM_PATH}}` is for emulator games. Literal user home paths and Windows reserved device names are rejected in Command and Working directory.
diff --git a/src/database.js b/src/database.js
index 9573b6b02a..3eb3dfa360 100644
--- a/src/database.js
+++ b/src/database.js
@@ -89,6 +89,7 @@ function mergePreset(root, preset, {
const entry = {
id: presetId, name: generatedName, os: preset.os, method: preset.method,
...(preset.variantName ? { variant_name: preset.variantName } : {}),
+ ...(preset.launchId ? { launch_id: preset.launchId } : {}),
sunshine: {
name: generatedName,
cmd: preset.command,
diff --git a/src/presets.js b/src/presets.js
index ca085fafc8..598587773e 100644
--- a/src/presets.js
+++ b/src/presets.js
@@ -10,6 +10,7 @@ const FIELD_NAMES = {
'App image URL': 'appImageUrl',
'Host operating system': 'os',
'Launch method': 'method',
+ 'Launch ID': 'launchId',
'Emulator variant name': 'variantName',
Command: 'command',
'Working directory': 'workingDir',
@@ -120,17 +121,41 @@ function appIdentity(values) {
};
}
-function validateLaunchCommand(command, os, method) {
- const steamUri = /steam:\/\//i.test(command);
- const epicUri = /com\.epicgames\.launcher:\/\//i.test(command);
- if (steamUri) {
- throw new PresetError('Steam URI needs a detached command; use a Steam executable command instead');
+function generatedLaunch(values, method, os) {
+ const id = field(values, 'launchId', { limit: 300, singleLine: true });
+ if (!['Steam', 'Epic Games', 'Microsoft Store'].includes(method)) {
+ if (id) throw new PresetError('Launch ID requires Steam, Epic Games, or Microsoft Store');
+ return null;
}
- if (epicUri && method !== 'Epic Games') {
- throw new PresetError('Epic Games URI requires the Epic Games launch method');
+
+ if (method === 'Steam') {
+ if (!/^[1-9]\d{0,9}$/.test(id) || Number(id) > 4294967295) {
+ throw new PresetError('Steam app ID must be a positive 32-bit number');
+ }
+ const uri = 'steam://rungameid/' + id;
+ const command = os === 'Windows' ? 'cmd /c start "" "' + uri + '"'
+ : os === 'macOS' ? 'open "' + uri + '"' : 'steam "' + uri + '"';
+ return { command, launchId: id };
+ }
+ if (method === 'Epic Games') {
+ const parts = id.split(/%3A|:/i);
+ if (parts.length !== 3 || parts.some(part => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(part))) {
+ throw new PresetError('Epic launch ID must contain Sandbox ID, Catalog ID, and Artifact ID');
+ }
+ const launchId = parts.join('%3A');
+ const uri = 'com.epicgames.launcher://apps/' + launchId + '?action=launch&silent=true';
+ const command = os === 'Windows' ? 'cmd /c start "" "' + uri + '"' : 'open "' + uri + '"';
+ return { command, launchId };
}
- if (epicUri && (os !== 'Windows' || !/^com\.epicgames\.launcher:\/\/apps\/[^\s?]+(?:\?[^\s]+)?$/i.test(command))) {
- throw new PresetError('Epic Games launcher URI is supported for Windows only');
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*_[A-Za-z0-9]{13}![A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
+ throw new PresetError('Microsoft Store AUMID must contain a package family name and application ID');
+ }
+ return { command: 'explorer.exe shell:AppsFolder\\' + id, launchId: id };
+}
+
+function validateManualLaunchCommand(command) {
+ if (/steam:\/\//i.test(command) || /com\.epicgames\.launcher:\/\//i.test(command)) {
+ throw new PresetError('Launcher URIs require their matching store ID field');
}
}
@@ -162,13 +187,20 @@ function validateFields(values, kind) {
if (method === 'Microsoft Store' && os !== 'Windows') {
throw new PresetError('Microsoft Store is available on Windows only');
}
+ if (method === 'Epic Games' && os === 'Linux') {
+ throw new PresetError('Epic Games launcher IDs are available on Windows and macOS only');
+ }
const variantName = field(values, 'variantName', { limit: 100, singleLine: true });
if (variantName && method !== 'Emulator') {
throw new PresetError('An emulator variant name requires the Emulator launch method');
}
const identity = kind === 'game' ? gameIdentity(values) : appIdentity(values);
- const command = field(values, 'command', { required: true, singleLine: true });
+ const launch = generatedLaunch(values, method, os);
+ const submittedCommand = field(values, 'command', { required: !launch, singleLine: true });
const workingDir = field(values, 'workingDir', { limit: 512, singleLine: true });
+ if (launch && submittedCommand) throw new PresetError('Do not provide a command for a store ID launch');
+ if (launch && workingDir) throw new PresetError('Working directory is not used with a store ID launch');
+ const command = launch ? launch.command : submittedCommand;
validatePlaceholders(command, os, 'Launch command');
validatePlaceholders(workingDir, os, 'Working directory');
if (method !== 'Emulator' && (command.includes('{{ROM_PATH}}') || workingDir.includes('{{ROM_PATH}}'))) {
@@ -176,10 +208,10 @@ function validateFields(values, kind) {
}
validatePortablePath(command, os, 'Launch command');
validatePortablePath(workingDir, os, 'Working directory');
- validateLaunchCommand(command, os, method);
+ if (!launch) validateManualLaunchCommand(command);
return {
kind, ...identity, variantName: variantName || null, ...replacementFields(values),
- os, method: slug(method), command,
+ os, method: slug(method), command, launchId: launch ? launch.launchId : null,
workingDir: workingDir || null,
notes: field(values, 'notes', { limit: 2000 }) || null
};
diff --git a/tests/presets.test.js b/tests/presets.test.js
index df33aec8d2..f433b77958 100644
--- a/tests/presets.test.js
+++ b/tests/presets.test.js
@@ -43,6 +43,7 @@ function formBody(values) {
const labels = {
gameUrl: 'IGDB game URL', appName: 'App name', appUrl: 'Official app URL',
appImageUrl: 'App image URL', os: 'Host operating system', method: 'Launch method',
+ launchId: 'Launch ID',
variantName: 'Emulator variant name', command: 'Command', workingDir: 'Working directory',
notes: 'Notes', replacementIssue: 'Preset to replace (issue number)',
replacementReason: 'Replacement reason'
@@ -111,23 +112,75 @@ test('apps use an HTTPS image URL and have separate review identity', () => {
assert.throws(() => validateFields({ ...values, command: '{{APP_PATH}}' }, 'app'), /unsupported placeholder/);
assert.equal(app.method, 'native');
assert.throws(() => validateFields({ ...values, method: 'Steam' }, 'app'), /do not have a launch method/);
+ assert.throws(() => validateFields({ ...values, launchId: '464920' }, 'app'), /Launch ID requires/);
assert.throws(() => validateFields({ ...values, command: '{{ROM_PATH}}' }, 'app'), /ROM_PATH.*Emulator/);
});
-test('game methods validate host OS and command forms', () => {
- const base = { ...gameValues, variantName: '' };
- assert.throws(() => validateFields({ ...base, method: 'Steam', command: 'steam://rungameid/464920' }, 'game'),
- /detached command/);
- assert.equal(validateFields({ ...base, method: 'Steam', command: 'steam -applaunch 464920' }, 'game').method, 'steam');
- const epic = { ...base, method: 'Epic Games', command: 'com.epicgames.launcher://apps/abc?action=launch&silent=true' };
- assert.equal(validateFields(epic, 'game').method, 'epic-games');
- assert.throws(() => validateFields({ ...epic, os: 'Linux' }, 'game'), /Windows only/);
- assert.equal(validateFields({ ...base, method: 'Microsoft Store', command: 'explorer.exe shell:AppsFolder\\Game!App' }, 'game').method,
- 'microsoft-store');
- assert.throws(() => validateFields({ ...base, os: 'Linux', method: 'Microsoft Store' }, 'game'), /Windows only/);
- assert.throws(() => validateFields({ ...base, method: 'Other' }, 'game'), /supported game launch method/);
- assert.throws(() => validateFields({ ...base, method: 'Native' }, 'game'), /ROM_PATH.*Emulator/);
- assert.throws(() => validateFields({ ...base, method: 'Native', variantName: 'Alternate' }, 'game'), /Emulator/);
+test('store IDs generate OS-specific Sunshine commands', () => {
+ const base = { ...gameValues, variantName: '', command: '', workingDir: '' };
+ const steamCommands = {
+ Windows: 'cmd /c start "" "steam://rungameid/464920"',
+ Linux: 'steam "steam://rungameid/464920"',
+ macOS: 'open "steam://rungameid/464920"'
+ };
+ for (const [os, command] of Object.entries(steamCommands)) {
+ const preset = validateFields({ ...base, os, method: 'Steam', launchId: '464920' }, 'game');
+ assert.equal(preset.command, command);
+ assert.equal(preset.launchId, '464920');
+ }
+ const epic = 'fn%3A4fe75bbc5a674f4f9b356b5c90567da5%3AFortnite';
+ const epicUri = 'com.epicgames.launcher://apps/' + epic + '?action=launch&silent=true';
+ for (const [os, command] of [
+ ['Windows', 'cmd /c start "" "' + epicUri + '"'],
+ ['macOS', 'open "' + epicUri + '"']
+ ]) {
+ const preset = validateFields({ ...base, os, method: 'Epic Games',
+ launchId: 'fn:4fe75bbc5a674f4f9b356b5c90567da5:Fortnite' }, 'game');
+ assert.equal(preset.launchId, epic);
+ assert.equal(preset.command, command);
+ }
+ const aumid = 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App';
+ const store = validateFields({ ...base, method: 'Microsoft Store', launchId: aumid }, 'game');
+ assert.equal(store.command, 'explorer.exe shell:AppsFolder\\' + aumid);
+ assert.equal(store.launchId, aumid);
+ assert.equal(parseIssue(formBody({ ...base, method: 'Steam', launchId: '464920' })).launchId, '464920');
+});
+
+test('store IDs validate syntax, method, and host OS', () => {
+ const base = { ...gameValues, variantName: '', command: '', workingDir: '' };
+ assert.throws(() => validateFields({ ...base, method: 'Steam' }, 'game'), /Steam app ID/);
+ assert.throws(() => validateFields({ ...base, method: 'Steam', launchId: '1&whoami' }, 'game'), /Steam app ID/);
+ assert.throws(() => validateFields({ ...base, method: 'Steam', launchId: '4294967296' }, 'game'), /Steam app ID/);
+ assert.throws(() => validateFields({ ...base, method: 'Steam', launchId: '464920',
+ command: 'other.exe' }, 'game'), /Do not provide a command/);
+ assert.throws(() => validateFields({ ...base, method: 'Steam', launchId: '464920',
+ workingDir: '{{HOME}}' }, 'game'), /Working directory is not used/);
+ assert.throws(() => validateFields({ ...base, method: 'Native', launchId: '464920',
+ command: 'game.exe' }, 'game'), /Launch ID requires/);
+ assert.throws(() => validateFields({ ...base, method: 'Epic Games', os: 'Linux',
+ launchId: 'fn:catalog:Fortnite' }, 'game'), /Windows and macOS only/);
+ assert.throws(() => validateFields({ ...base, method: 'Epic Games',
+ launchId: 'fn:catalog:Game&calc' }, 'game'), /Epic launch ID/);
+ assert.throws(() => validateFields({ ...base, method: 'Microsoft Store',
+ launchId: '9WZDNCRFHVJL' }, 'game'), /Microsoft Store AUMID/);
+ assert.throws(() => validateFields({ ...base, method: 'Microsoft Store', os: 'Linux',
+ launchId: 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' }, 'game'), /Windows only/);
+});
+
+test('Native, GOG, and Emulator still require reviewed commands', () => {
+ const base = { ...gameValues, variantName: '', command: '', workingDir: '' };
+ for (const method of ['Native', 'GOG', 'Emulator']) {
+ assert.throws(() => validateFields({ ...base, method }, 'game'), /command is required/);
+ }
+ assert.equal(validateFields({ ...base, method: 'GOG', command: 'game.exe' }, 'game').command, 'game.exe');
+ assert.throws(() => validateFields({ ...base, method: 'Other', command: 'game.exe' }, 'game'),
+ /supported game launch method/);
+ assert.throws(() => validateFields({ ...base, method: 'Native', command: 'steam://rungameid/464920' }, 'game'),
+ /Launcher URIs require/);
+ assert.throws(() => validateFields({ ...base, method: 'Native', command: '{{ROM_PATH}}' }, 'game'),
+ /ROM_PATH.*Emulator/);
+ assert.throws(() => validateFields({ ...base, method: 'Native', command: 'game.exe',
+ variantName: 'Alternate' }, 'game'), /Emulator/);
});
test('approved presets get issue IDs and replacements preserve identity', async t => {
@@ -154,13 +207,14 @@ test('approved presets get issue IDs and replacements preserve identity', async
{ issueNumber: 14, approvedBy: 'maintainer' }), /No preset/);
});
-test('Steam executable publishes as a Sunshine command without an image path', async t => {
+test('Steam app ID publishes a generated Sunshine command without detached or image paths', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'preset-steam-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const steam = await validateGameDb(validateFields({ ...gameValues, method: 'Steam',
- variantName: '', command: 'steam -applaunch 464920' }, 'game'), mockApis, credentials);
+ variantName: '', command: '', workingDir: '', launchId: '464920' }, 'game'), mockApis, credentials);
const { record } = mergePreset(root, steam, { issueNumber: 30, approvedBy: 'reviewer' }, { write: true });
- assert.equal(record.presets[0].sunshine.cmd, 'steam -applaunch 464920');
+ assert.equal(record.presets[0].launch_id, '464920');
+ assert.equal(record.presets[0].sunshine.cmd, 'cmd /c start "" "steam://rungameid/464920"');
assert.equal(record.presets[0].sunshine.name, 'One Tap Hero (Windows, Steam)');
assert.ok(!Object.hasOwn(record.presets[0].sunshine, 'detached'));
assert.ok(!Object.hasOwn(record.presets[0].sunshine, 'image-path'));
From c0864ec501961d4d16a0d1093df544e8c79d65b8 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Wed, 23 Sep 2026 17:01:18 -0400
Subject: [PATCH 11/12] Split game preset forms and show ProtonDB compatibility
---
.github/ISSUE_TEMPLATE/game-emulator.yml | 55 +++++++
.github/ISSUE_TEMPLATE/game-epic-games.yml | 45 ++++++
.github/ISSUE_TEMPLATE/game-gog.yml | 60 +++++++
.../ISSUE_TEMPLATE/game-microsoft-store.yml | 45 ++++++
.github/ISSUE_TEMPLATE/game-native.yml | 60 +++++++
.github/ISSUE_TEMPLATE/game-preset.yml | 87 ----------
.github/ISSUE_TEMPLATE/game-steam.yml | 44 ++++++
README.md | 14 +-
docs/approverGuide.md | 4 +-
docs/presetGuidelines.md | 12 +-
gh-pages-template/assets/js/app.js | 34 +++-
gh-pages-template/index.html | 2 +-
src/build-site.js | 50 +++++-
src/database.js | 31 ++--
src/issue.js | 25 ++-
src/presets.js | 127 ++++++++-------
tests/presets.test.js | 148 ++++++++++++------
17 files changed, 611 insertions(+), 232 deletions(-)
create mode 100644 .github/ISSUE_TEMPLATE/game-emulator.yml
create mode 100644 .github/ISSUE_TEMPLATE/game-epic-games.yml
create mode 100644 .github/ISSUE_TEMPLATE/game-gog.yml
create mode 100644 .github/ISSUE_TEMPLATE/game-microsoft-store.yml
create mode 100644 .github/ISSUE_TEMPLATE/game-native.yml
delete mode 100644 .github/ISSUE_TEMPLATE/game-preset.yml
create mode 100644 .github/ISSUE_TEMPLATE/game-steam.yml
diff --git a/.github/ISSUE_TEMPLATE/game-emulator.yml b/.github/ISSUE_TEMPLATE/game-emulator.yml
new file mode 100644
index 0000000000..5b05081a39
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/game-emulator.yml
@@ -0,0 +1,55 @@
+---
+name: Emulator game preset
+description: Request a Sunshine Emulator launch option for a GameDB game.
+title: '[EMULATOR GAME PRESET]: '
+labels:
+ - request-game-preset
+ - method-emulator
+body:
+ - type: markdown
+ attributes:
+ value: |
+ One issue represents one Emulator launch option. Provide the IGDB game URL;
+ the bot resolves its ID and checks GameDB.
+ - type: input
+ id: game_url
+ attributes:
+ label: IGDB game URL
+ description: Official URL containing the game slug, for example https://www.igdb.com/games/surviving-mars.
+ validations:
+ required: true
+ - type: input
+ id: variant_name
+ attributes:
+ label: Emulator variant name
+ description: Optional emulator, console, or core label when needed to distinguish launch options.
+ - type: input
+ id: command
+ attributes:
+ label: Command
+ description: |
+ Portable Sunshine command for the emulator. Use {{ROM_PATH}} for the game and
+ {{HOME}} for a variable home path. Put the emulator executable on the host PATH.
+ Windows-only path placeholders, literal home directories, and reserved system names are rejected.
+ validations:
+ required: true
+ - type: input
+ id: working_dir
+ attributes:
+ label: Working directory
+ description: Optional portable Sunshine working-dir value.
+ - type: textarea
+ id: notes
+ attributes:
+ label: Notes
+ description: Setup instructions or prerequisites.
+ - type: textarea
+ id: replacement_reason
+ attributes:
+ label: Replacement reason
+ description: Explain why the existing preset should change. Required with the issue number below.
+ - type: input
+ id: replacement_issue
+ attributes:
+ label: Preset to replace (issue number)
+ description: Optional issue number shown with the published preset. Leave blank to add a new preset.
diff --git a/.github/ISSUE_TEMPLATE/game-epic-games.yml b/.github/ISSUE_TEMPLATE/game-epic-games.yml
new file mode 100644
index 0000000000..cc218fe74d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/game-epic-games.yml
@@ -0,0 +1,45 @@
+---
+name: Epic Games game preset
+description: Request a Sunshine Epic Games launch option for a GameDB game.
+title: '[EPIC GAMES GAME PRESET]: '
+labels:
+ - request-game-preset
+ - method-epic-games
+body:
+ - type: markdown
+ attributes:
+ value: |
+ One issue represents one Epic Games launch option. Provide the IGDB game URL;
+ the bot resolves its ID and checks GameDB.
+ - type: input
+ id: game_url
+ attributes:
+ label: IGDB game URL
+ description: Official URL containing the game slug, for example https://www.igdb.com/games/surviving-mars.
+ validations:
+ required: true
+ - type: input
+ id: launch_id
+ attributes:
+ label: Launch ID
+ description: |
+ Three-part Sandbox ID, Catalog ID, and Artifact ID from the Epic launcher shortcut,
+ separated by colons or %3A. For example fn%3A4fe75bbc5a674f4f9b356b5c90567da5%3AFortnite.
+ The bot generates the Sunshine command for each supported launcher OS.
+ validations:
+ required: true
+ - type: textarea
+ id: notes
+ attributes:
+ label: Notes
+ description: Setup instructions or prerequisites.
+ - type: textarea
+ id: replacement_reason
+ attributes:
+ label: Replacement reason
+ description: Explain why the existing preset should change. Required with the issue number below.
+ - type: input
+ id: replacement_issue
+ attributes:
+ label: Preset to replace (issue number)
+ description: Optional issue number shown with the published preset. Leave blank to add a new preset.
diff --git a/.github/ISSUE_TEMPLATE/game-gog.yml b/.github/ISSUE_TEMPLATE/game-gog.yml
new file mode 100644
index 0000000000..0e06dfd23c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/game-gog.yml
@@ -0,0 +1,60 @@
+---
+name: GOG game preset
+description: Request a Sunshine GOG launch option for a GameDB game.
+title: '[GOG GAME PRESET]: '
+labels:
+ - request-game-preset
+ - method-gog
+body:
+ - type: markdown
+ attributes:
+ value: |
+ One issue represents one GOG launch option. Provide the IGDB game URL;
+ the bot resolves its ID and checks GameDB.
+ - type: input
+ id: game_url
+ attributes:
+ label: IGDB game URL
+ description: Official URL containing the game slug, for example https://www.igdb.com/games/surviving-mars.
+ validations:
+ required: true
+ - type: dropdown
+ id: os
+ attributes:
+ label: Host operating system
+ options:
+ - Windows
+ - Linux
+ - macOS
+ validations:
+ required: true
+ - type: input
+ id: command
+ attributes:
+ label: Command
+ description: |
+ Sunshine command to launch the game. Use {{HOME}} for a variable home path.
+ Windows also allows {{SYSTEM_DRIVE}}, {{PROGRAM_FILES}}, and {{PROGRAM_FILES_X86}}.
+ Literal home directories and reserved system names are rejected.
+ validations:
+ required: true
+ - type: input
+ id: working_dir
+ attributes:
+ label: Working directory
+ description: Optional Sunshine working-dir value.
+ - type: textarea
+ id: notes
+ attributes:
+ label: Notes
+ description: Setup instructions or prerequisites.
+ - type: textarea
+ id: replacement_reason
+ attributes:
+ label: Replacement reason
+ description: Explain why the existing preset should change. Required with the issue number below.
+ - type: input
+ id: replacement_issue
+ attributes:
+ label: Preset to replace (issue number)
+ description: Optional issue number shown with the published preset. Leave blank to add a new preset.
diff --git a/.github/ISSUE_TEMPLATE/game-microsoft-store.yml b/.github/ISSUE_TEMPLATE/game-microsoft-store.yml
new file mode 100644
index 0000000000..753e77a58a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/game-microsoft-store.yml
@@ -0,0 +1,45 @@
+---
+name: Microsoft Store game preset
+description: Request a Sunshine Microsoft Store launch option for a GameDB game.
+title: '[MICROSOFT STORE GAME PRESET]: '
+labels:
+ - request-game-preset
+ - method-microsoft-store
+body:
+ - type: markdown
+ attributes:
+ value: |
+ One issue represents one Microsoft Store launch option. Provide the IGDB game URL;
+ the bot resolves its ID and checks GameDB.
+ - type: input
+ id: game_url
+ attributes:
+ label: IGDB game URL
+ description: Official URL containing the game slug, for example https://www.igdb.com/games/surviving-mars.
+ validations:
+ required: true
+ - type: input
+ id: launch_id
+ attributes:
+ label: Launch ID
+ description: |
+ Installed app AUMID from Get-StartApps, for example
+ Microsoft.WindowsCalculator_8wekyb3d8bbwe!App. A Store product ID will not launch the game.
+ The bot generates the Sunshine command for each supported launcher OS.
+ validations:
+ required: true
+ - type: textarea
+ id: notes
+ attributes:
+ label: Notes
+ description: Setup instructions or prerequisites.
+ - type: textarea
+ id: replacement_reason
+ attributes:
+ label: Replacement reason
+ description: Explain why the existing preset should change. Required with the issue number below.
+ - type: input
+ id: replacement_issue
+ attributes:
+ label: Preset to replace (issue number)
+ description: Optional issue number shown with the published preset. Leave blank to add a new preset.
diff --git a/.github/ISSUE_TEMPLATE/game-native.yml b/.github/ISSUE_TEMPLATE/game-native.yml
new file mode 100644
index 0000000000..808564a39a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/game-native.yml
@@ -0,0 +1,60 @@
+---
+name: Native game preset
+description: Request a Sunshine Native launch option for a GameDB game.
+title: '[NATIVE GAME PRESET]: '
+labels:
+ - request-game-preset
+ - method-native
+body:
+ - type: markdown
+ attributes:
+ value: |
+ One issue represents one Native launch option. Provide the IGDB game URL;
+ the bot resolves its ID and checks GameDB.
+ - type: input
+ id: game_url
+ attributes:
+ label: IGDB game URL
+ description: Official URL containing the game slug, for example https://www.igdb.com/games/surviving-mars.
+ validations:
+ required: true
+ - type: dropdown
+ id: os
+ attributes:
+ label: Host operating system
+ options:
+ - Windows
+ - Linux
+ - macOS
+ validations:
+ required: true
+ - type: input
+ id: command
+ attributes:
+ label: Command
+ description: |
+ Sunshine command to launch the game. Use {{HOME}} for a variable home path.
+ Windows also allows {{SYSTEM_DRIVE}}, {{PROGRAM_FILES}}, and {{PROGRAM_FILES_X86}}.
+ Literal home directories and reserved system names are rejected.
+ validations:
+ required: true
+ - type: input
+ id: working_dir
+ attributes:
+ label: Working directory
+ description: Optional Sunshine working-dir value.
+ - type: textarea
+ id: notes
+ attributes:
+ label: Notes
+ description: Setup instructions or prerequisites.
+ - type: textarea
+ id: replacement_reason
+ attributes:
+ label: Replacement reason
+ description: Explain why the existing preset should change. Required with the issue number below.
+ - type: input
+ id: replacement_issue
+ attributes:
+ label: Preset to replace (issue number)
+ description: Optional issue number shown with the published preset. Leave blank to add a new preset.
diff --git a/.github/ISSUE_TEMPLATE/game-preset.yml b/.github/ISSUE_TEMPLATE/game-preset.yml
deleted file mode 100644
index c54d9200bf..0000000000
--- a/.github/ISSUE_TEMPLATE/game-preset.yml
+++ /dev/null
@@ -1,87 +0,0 @@
----
-name: Game launch preset
-description: Request one Sunshine launch option for a GameDB game.
-title: '[GAME PRESET]: '
-labels:
- - request-game-preset
-body:
- - type: markdown
- attributes:
- value: |
- One issue represents one launch option. Provide the IGDB game URL; the bot resolves its ID and checks GameDB.
- For Steam, Epic Games, or Microsoft Store, enter the matching launch ID; the bot creates the command.
- Add an emulator variant only when the operating system and launch method cannot distinguish the preset.
- - type: input
- id: game_url
- attributes:
- label: IGDB game URL
- description: Official URL containing the game slug, for example https://www.igdb.com/games/surviving-mars.
- validations:
- required: true
- - type: dropdown
- id: os
- attributes:
- label: Host operating system
- options:
- - Windows
- - Linux
- - macOS
- validations:
- required: true
- - type: dropdown
- id: method
- attributes:
- label: Launch method
- options:
- - Native
- - Steam
- - Epic Games
- - GOG
- - Microsoft Store
- - Emulator
- validations:
- required: true
- - type: input
- id: launch_id
- attributes:
- label: Launch ID
- description: |
- Required for Steam, Epic Games, and Microsoft Store; leave blank for Native, GOG, or Emulator.
- Steam: numeric app ID from a store URL, for example 464920.
- Epic Games: three-part ID between /apps/ and ? in a launcher shortcut,
- for example fn%3A4fe75bbc5a674f4f9b356b5c90567da5%3AFortnite.
- Microsoft Store: installed app's AUMID from Get-StartApps,
- for example Microsoft.WindowsCalculator_8wekyb3d8bbwe!App. A Store product ID will not launch it.
- - type: input
- id: variant_name
- attributes:
- label: Emulator variant name
- description: Optional emulator, console, or core label when needed to distinguish launch options.
- - type: input
- id: command
- attributes:
- label: Command
- description: |
- Required for Native, GOG, and Emulator. Leave blank for Steam, Epic Games, and Microsoft Store.
- Use {{ROM_PATH}} or {{HOME}} for variable paths. Windows also allows {{SYSTEM_DRIVE}},
- {{PROGRAM_FILES}}, and {{PROGRAM_FILES_X86}}. Literal home directories are rejected.
- - type: input
- id: working_dir
- attributes:
- label: Working directory
- description: Optional Sunshine working-dir value for Native, GOG, and Emulator only.
- - type: textarea
- id: notes
- attributes:
- label: Notes
- description: Setup instructions and required emulator settings.
- - type: textarea
- id: replacement_reason
- attributes:
- label: Replacement reason
- description: Explain why the existing preset should change. Required with the issue number below.
- - type: input
- id: replacement_issue
- attributes:
- label: Preset to replace (issue number)
- description: Optional issue number shown with the published preset. Leave blank to add a new preset.
diff --git a/.github/ISSUE_TEMPLATE/game-steam.yml b/.github/ISSUE_TEMPLATE/game-steam.yml
new file mode 100644
index 0000000000..447e4a39b0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/game-steam.yml
@@ -0,0 +1,44 @@
+---
+name: Steam game preset
+description: Request a Sunshine Steam launch option for a GameDB game.
+title: '[STEAM GAME PRESET]: '
+labels:
+ - request-game-preset
+ - method-steam
+body:
+ - type: markdown
+ attributes:
+ value: |
+ One issue represents one Steam launch option. Provide the IGDB game URL;
+ the bot resolves its ID and checks GameDB.
+ - type: input
+ id: game_url
+ attributes:
+ label: IGDB game URL
+ description: Official URL containing the game slug, for example https://www.igdb.com/games/surviving-mars.
+ validations:
+ required: true
+ - type: input
+ id: launch_id
+ attributes:
+ label: Launch ID
+ description: |
+ Steam numeric app ID from its store URL, for example 464920.
+ The bot generates the Sunshine command for each supported launcher OS.
+ validations:
+ required: true
+ - type: textarea
+ id: notes
+ attributes:
+ label: Notes
+ description: Setup instructions or prerequisites.
+ - type: textarea
+ id: replacement_reason
+ attributes:
+ label: Replacement reason
+ description: Explain why the existing preset should change. Required with the issue number below.
+ - type: input
+ id: replacement_issue
+ attributes:
+ label: Preset to replace (issue number)
+ description: Optional issue number shown with the published preset. Leave blank to add a new preset.
diff --git a/README.md b/README.md
index a9feb2b5ab..b8447d815b 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
-Community maintained launch presets for [Sunshine](https://github.com/LizardByte/Sunshine). Each game or app can have many presets across Windows, Linux, and macOS. Game launch methods include Native, Steam, Epic Games, GOG, Microsoft Store, and Emulator. App presets use the app name and host OS. GitHub issue numbers provide stable IDs, while the bot generates names and accepts optional emulator variants.
+Community maintained launch presets for [Sunshine](https://github.com/LizardByte/Sunshine). Each game or app can have many presets. Native, GOG, and app commands target a chosen host OS. Steam, Epic Games, and Microsoft Store submissions use a launch ID to generate commands for their supported launcher OSes; emulator submissions use a portable command. GitHub issue numbers provide stable IDs, while the bot generates names and accepts optional emulator variants.
Each GitHub issue requests **one preset**. Game requests must identify a [GameDB](https://app.lizardbyte.dev/GameDB/) record. App requests use a separate form and require a name and official source URL for maintainer review. A bot validates the request, and an authorized reviewer enters it into the approval queue. No code or pull request is needed to contribute.
@@ -45,16 +45,18 @@ The website publishes [catalog statistics](https://app.lizardbyte.dev/PresetDB/s
## Contribute
-1. Read the [preset guidelines](docs/presetGuidelines.md). For a game, copy its [IGDB game URL](https://www.igdb.com/) and [open a game preset request](https://github.com/LizardByte/PresetDB/issues/new?template=game-preset.yml). The bot resolves the URL slug to the IGDB numeric ID and checks GameDB. For another app, [open an app preset request](https://github.com/LizardByte/PresetDB/issues/new?template=app-preset.yml) with its official URL.
-2. Fill in **one** launch option. Choose the host OS and, for a game, its launch method. Add an emulator variant such as `RetroArch Snes9x` only when needed to distinguish emulator presets. The bot generates the preset name.
-3. For Steam, Epic Games, or Microsoft Store, enter one **Launch ID** in the shared field: a Steam app ID, Epic's three-part launch ID, or a Microsoft Store AUMID. The bot generates Sunshine `cmd` from that ID and the host OS. For Native, GOG, and Emulator, enter one **Command**. Validation and the website never execute commands.
+1. Read the [preset guidelines](docs/presetGuidelines.md). For a game, copy its [IGDB game URL](https://www.igdb.com/) and [choose a game method form](https://github.com/LizardByte/PresetDB/issues/new/choose). The bot resolves the URL slug to the IGDB numeric ID and checks GameDB. For another app, [open an app preset request](https://github.com/LizardByte/PresetDB/issues/new?template=app-preset.yml) with its official URL.
+2. Fill in one launch option. Native and GOG forms ask for the host OS. Store forms ask only for the launch ID; the emulator form asks for a portable command and an optional variant name. The bot generates the preset name. App requests still ask for the host OS.
+3. The store forms share one Launch ID concept: a numeric Steam app ID, an Epic three-part launch ID, or a Microsoft Store AUMID. The bot generates OS-specific Sunshine commands. Native, GOG, and Emulator forms ask for a Command. Validation and the website never execute commands.
4. To replace a preset, provide its issue number and explain the change. The bot preserves the original preset ID.
+ProtonDB compatibility summaries are attributed to [ProtonDB contributors](https://github.com/bdefore/protondb-data) and published under the [Open Database License](https://opendatacommons.org/licenses/odbl/). The website refreshes available tiers during its Pages build.
+
After validation, a listed trusted game contributor enters the approval queue automatically. Other requests wait for a listed approver or repository admin to comment `@LizardByte-bot approve`. App requests always require this separate review. The queue processes one approval at a time; see the [approver guide](docs/approverGuide.md) and [bot commands](docs/botCommands.md).
Supported path placeholders in manual Command and Working directory are `{{ROM_PATH}}` for emulator games and `{{HOME}}`; Windows also allows `{{SYSTEM_DRIVE}}`, `{{PROGRAM_FILES}}`, and `{{PROGRAM_FILES_X86}}`. Unknown placeholders, literal home directories, and Windows reserved device names are rejected. Literal commands still require maintainer review.
-Game submissions resolve the submitted slug through [IGDB's authenticated API](https://api-docs.igdb.com/), then check the resolved ID and slug against the live [GameDB JSON API](https://github.com/LizardByte/GameDB#data). An unavailable or mismatched result blocks approval. App submissions have no GameDB counterpart and always need separate maintainer review. Steam supports all three host OSes, Epic Games supports Windows and macOS, and Microsoft Store requires a Windows AUMID. GOG and other manual commands need human review. Launcher ID commands may outlive Sunshine's process tracking; the stream may need to be ended manually.
+Game submissions resolve the submitted slug through [IGDB authenticated API](https://api-docs.igdb.com/), then check the resolved ID and slug against the live [GameDB JSON API](https://github.com/LizardByte/GameDB#data). An unavailable or mismatched result blocks approval. App submissions have no GameDB counterpart and always need separate maintainer review. Steam generates Windows, Linux, and macOS commands; Epic Games generates Windows and macOS commands; Microsoft Store generates a Windows command. Launcher support does not prove that a particular game has a build for every OS, so reviewers should check availability. Steam presets link to [ProtonDB](https://www.protondb.com/) and the website shows its Linux compatibility tier when available at build time. Compatibility reports are community data and do not block approval. Launcher ID commands may outlive Sunshine process tracking; the stream may need to be ended manually.
## Repository and deployment
@@ -67,7 +69,7 @@ Game submissions resolve the submitted slug through [IGDB's authenticated API](h
The Pages workflow creates an archive from the database and site template, then calls the same [LizardByte Jekyll build workflow](https://github.com/LizardByte/LizardByte.github.io/blob/master/.github/workflows/jekyll-build.yml) used by GameDB and ThemerrDB. It deploys to `gh-pages` after changes to `master` or an approved database update.
-The empty `database` branch is initialized with `database/apps` and `database/games`. GitHub Pages serves the empty orphan `gh-pages` branch. Configure `GH_BOT_TOKEN`, `GH_BOT_EMAIL`, `GH_BOT_NAME`, `TWITCH_CLIENT_ID`, and `TWITCH_CLIENT_SECRET` as in the existing LizardByte database projects. `GH_BOT_TOKEN` needs issue, contents, and Actions write access to publish the database and dispatch the Pages build. The Twitch credentials resolve IGDB slugs; GameDB itself needs no credentials. Create the `request-game-preset`, `request-app-preset`, `approve-queue`, and `approve-preset` labels. Set the GitHub repository description to the heading description above. The `database` branch retains the `database/` directory; approved records are written there.
+The empty `database` branch is initialized with `database/apps` and `database/games`. GitHub Pages serves the empty orphan `gh-pages` branch. Configure `GH_BOT_TOKEN`, `GH_BOT_EMAIL`, `GH_BOT_NAME`, `TWITCH_CLIENT_ID`, and `TWITCH_CLIENT_SECRET` as in the existing LizardByte database projects. `GH_BOT_TOKEN` needs issue, contents, and Actions write access to publish the database and dispatch the Pages build. The Twitch credentials resolve IGDB slugs; GameDB itself needs no credentials. The request, approval, and method labels are configured in the repository. Set the GitHub repository description to the heading description above. The `database` branch retains the `database/` directory; approved records are written there.
Connect this repository to Read the Docs and enable pull request preview builds. Set `GITHUB_WORKFLOW=call-jekyll-build / Build Jekyll`, `SITE_ARTIFACT=update.zip`, and `EXTRACT_ARCHIVE=build.zip` in the Read the Docs project environment. The shared script downloads the Pages build artifact and extracts the nested site archive. See [developer setup](docs/developerSetup.md) for local commands.
diff --git a/docs/approverGuide.md b/docs/approverGuide.md
index 2420537f4c..c5c791e2e4 100644
--- a/docs/approverGuide.md
+++ b/docs/approverGuide.md
@@ -4,8 +4,8 @@ This guide is for listed approvers and repository administrators reviewing game
1. Open the oldest validated request in the [game queue](https://github.com/LizardByte/PresetDB/issues?q=is%3Aopen+label%3Arequest-game-preset) or [app queue](https://github.com/LizardByte/PresetDB/issues?q=is%3Aopen+label%3Arequest-app-preset). Check the latest validation comment after the last issue edit.
2. For games, check the IGDB URL and resolved GameDB record. For apps, inspect the official URL and optional image URL. App requests always need a separate review, including when a trusted contributor submits them.
-3. Check the host OS, game launch method, store launch ID or manual command, working directory, and setup notes against the [preset guidelines](presetGuidelines.md) and [Sunshine examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md). Confirm the store ID belongs to the requested game. Automated validation checks ID syntax and never runs commands.
-4. Search the published record for the same OS, method, and emulator variant. A replacement must identify an existing preset issue and explain the change.
+3. Check the selected host OS for Native, GOG, or app requests. For Steam, Epic Games, and Microsoft Store, check the launch ID, generated commands, and which platforms the game actually supports. For Emulator, confirm the manual command and placeholders are portable. Review setup notes against the [preset guidelines](presetGuidelines.md) and [Sunshine examples](https://github.com/LizardByte/Sunshine/blob/master/docs/app_examples.md). Automated validation checks syntax and never runs commands. ProtonDB reports inform Linux compatibility for Steam but are not an approval gate.
+4. Search the published record for the same method, host OS when present, and emulator variant. A replacement must identify an existing preset issue and explain the change.
5. Comment `@LizardByte-bot approve` to enter the approval queue. The bot adds `approve-queue` and starts `approve-preset` when the active slot is free. It revalidates against IGDB and GameDB before writing to the `database` branch.
The approval workflow comments its result. A successful approval closes the issue and requests a Pages rebuild. On failure, it removes the issue from the queue so the next request can proceed; fix the issue and queue it again. The `approve-preset` label remains on successfully closed issues for the approved count badge.
diff --git a/docs/presetGuidelines.md b/docs/presetGuidelines.md
index 27448972fb..da256ebeb7 100644
--- a/docs/presetGuidelines.md
+++ b/docs/presetGuidelines.md
@@ -1,12 +1,12 @@
# Preset Guidelines
-One issue requests one Sunshine launch option for one host operating system. Submit another issue for a different store, emulator, core, or operating system. The bot generates a name from the game or app name, host OS, and game launch method.
+One issue requests one Sunshine launch option. Native, GOG, and app commands target a chosen host OS. Store IDs generate commands for each supported launcher OS; emulator commands must be portable across hosts. Submit another issue for a different store, emulator, or core. The bot generates names from the game or app name and launch method, plus the host OS when selected.
## Games
Use the public IGDB game URL. The bot resolves its slug to a numeric IGDB ID and requires the same game and slug in GameDB. The catalog obtains the game cover from GameDB, so the form has no image field.
-Choose Native, Steam, Epic Games, GOG, Microsoft Store, or Emulator. Steam accepts Windows, Linux, and macOS; Epic Games accepts Windows and macOS; Microsoft Store accepts Windows only. An optional emulator variant can identify the console, launcher, or core when multiple emulator presets share an OS.
+Choose the Native, Steam, Epic Games, GOG, Microsoft Store, or Emulator issue form. Only Native and GOG game forms ask for a host OS. Steam can generate Windows, Linux, and macOS commands; Epic Games can generate Windows and macOS commands; Microsoft Store can generate a Windows command. An optional emulator variant can identify a console, launcher, or core.
## Apps
@@ -14,10 +14,12 @@ Provide the official HTTPS homepage or source repository. An app image is option
## Commands and paths
-Use the single **Launch ID** field for Steam, Epic Games, or Microsoft Store. For Steam, enter the numeric app ID from a Steam store URL. For Epic Games, enter the three-part Sandbox ID, Catalog ID, and Artifact ID from the game's launcher shortcut, separated by colons or `%3A`. For Microsoft Store, enter the installed app's [AUMID](https://learn.microsoft.com/en-us/windows/configuration/store/find-aumid), available through `Get-StartApps`; a Store product ID opens a store page and cannot launch the installed game. Leave Command and Working directory blank for these methods. The bot generates the OS-specific Sunshine `cmd` and stores the launch ID with the preset.
+Steam, Epic Games, and Microsoft Store forms each have one Launch ID field. Steam takes the numeric app ID from its store URL. Epic Games takes the three-part Sandbox ID, Catalog ID, and Artifact ID from the launcher shortcut, separated by colons or %3A. Microsoft Store takes the installed app [AUMID](https://learn.microsoft.com/en-us/windows/configuration/store/find-aumid) from Get-StartApps; a Store product ID cannot launch the installed game. The bot generates Sunshine commands for each supported launcher OS. These are launcher capabilities; reviewers must check whether the game itself supports each platform.
-For Native, GOG, and Emulator, provide one Command. GOG games can launch without Galaxy, so no single GOG ID command is assumed. A maintainer reviews these commands before approval. Store launchers can exit before their game; Sunshine may keep the stream open until the user ends it. PresetDB uses only Sunshine `cmd`, with no detached command field.
+Native, GOG, and Emulator forms require a Command. GOG games can launch without Galaxy, so no single GOG ID command is assumed. Emulator commands must work across host OSes, using executable names on the host PATH and portable path placeholders. A maintainer reviews manual commands before approval. Store launchers can exit before their game; Sunshine may keep the stream open until the user ends it. PresetDB uses only Sunshine cmd.
-The supported path placeholders are `{{ROM_PATH}}` and `{{HOME}}`. Windows also supports `{{SYSTEM_DRIVE}}`, `{{PROGRAM_FILES}}`, and `{{PROGRAM_FILES_X86}}`. Replace placeholders with paths on the Sunshine host before use. `{{ROM_PATH}}` is for emulator games. Literal user home paths and Windows reserved device names are rejected in Command and Working directory.
+The supported path placeholders are ROM_PATH and HOME, written with double curly braces in the form. Windows Native, GOG, and app commands also support SYSTEM_DRIVE, PROGRAM_FILES, and PROGRAM_FILES_X86. Replace placeholders with paths on the Sunshine host before use. ROM_PATH is for emulator games. Literal user home paths and Windows reserved device names are rejected in Command and Working directory.
+
+Steam presets link to [ProtonDB](https://www.protondb.com/). The website shows the reported Linux compatibility tier when the Pages build can fetch one; no rating means unknown, not incompatible. Compatibility data comes from [ProtonDB contributors](https://github.com/bdefore/protondb-data) under the [Open Database License](https://opendatacommons.org/licenses/odbl/).
For replacements, enter the issue number displayed with the published preset and explain what changed. The original preset ID is retained.
diff --git a/gh-pages-template/assets/js/app.js b/gh-pages-template/assets/js/app.js
index 7690e4acbc..d4249821b1 100644
--- a/gh-pages-template/assets/js/app.js
+++ b/gh-pages-template/assets/js/app.js
@@ -12,8 +12,10 @@ function filterItems(index, query, kind, os) {
).sort((a, b) => a.name.localeCompare(b.name));
}
-function sunshineSnippet(preset) {
- return JSON.stringify(preset.sunshine, null, 2);
+function sunshineSnippet(preset, os = 'Windows') {
+ const sunshine = preset.sunshine_by_os?.[os] || preset.sunshine;
+ if (!sunshine) throw new Error('Choose an available host OS');
+ return JSON.stringify(sunshine, null, 2);
}
function normalizeBasePath(value) {
@@ -113,14 +115,38 @@ function boot() {
const body = element('div', 'card-body');
body.append(element('h3', 'h5 card-title fw-bold', preset.name));
if (preset.notes) body.append(element('p', 'card-text', preset.notes));
+ if (preset.protondb_url) {
+ const tier = preset.protondb?.tier;
+ const reports = preset.protondb?.reports;
+ const rating = tier ? 'ProtonDB: ' + tier[0].toUpperCase() + tier.slice(1) +
+ (reports === null ? '' : ' (' + reports + ' reports)') : 'Check Linux compatibility on ProtonDB';
+ const proton = element('p', 'card-text');
+ proton.append(safeLink(preset.protondb_url, rating));
+ body.append(proton);
+ }
+ let hostSelect;
+ if (preset.sunshine_by_os) {
+ const hostLabel = element('label', 'form-label', 'Host OS');
+ hostSelect = element('select', 'form-select rounded-0 mb-3');
+ for (const host of Object.keys(preset.sunshine_by_os)) {
+ const option = element('option', '', host);
+ option.value = host;
+ hostSelect.append(option);
+ }
+ hostLabel.append(hostSelect);
+ body.append(hostLabel);
+ }
+ const snippet = () => sunshineSnippet(preset, hostSelect?.value);
const command = element('pre', 'p-3 rounded bg-dark text-light overflow-auto');
- command.append(element('code', '', sunshineSnippet(preset)));
+ const code = element('code', '', snippet());
+ command.append(code);
+ if (hostSelect) hostSelect.addEventListener('change', () => { code.textContent = snippet(); });
body.append(command);
const copy = element('button', 'btn btn-warning rounded-0', 'Copy Sunshine JSON');
copy.type = 'button';
copy.addEventListener('click', async () => {
try {
- await navigator.clipboard.writeText(sunshineSnippet(preset));
+ await navigator.clipboard.writeText(snippet());
copy.textContent = 'Copied';
} catch {
copy.textContent = 'Select and copy the JSON above';
diff --git a/gh-pages-template/index.html b/gh-pages-template/index.html
index d6ea6b9fa8..042ee808db 100644
--- a/gh-pages-template/index.html
+++ b/gh-pages-template/index.html
@@ -17,7 +17,7 @@
Community launch options for Windows, Linux, and macOS. Find a game or app, then choose the preset that fits your host.