From b7b05f9aba756563f33c4e29e14b71f48aee506c Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 13 Oct 2026 20:14:51 -0400 Subject: [PATCH 1/3] Guide new users directly to native setup installation --- CHANGELOG.md | 1 + README.md | 2 +- src/App.svelte | 2 +- src/EngineSettings.svelte | 8 ++++---- src/LiveCatalog.svelte | 11 ++++++----- src/LiveJobs.svelte | 4 ++-- src/Welcome.svelte | 19 +++++++++++-------- tests/electron.test.mjs | 4 ++-- tests/live.test.mjs | 2 +- tests/onboarding.test.mjs | 6 +++--- 10 files changed, 32 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54964d3..e2059ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- First-run setup opens Discover directly for native .loadstone installation. Welcome, engine settings and Wabbajack empty states now explain which prerequisites belong to each installation path. - Native MO2 recipes can declare a Steam or GOG edition matched to the target game requirement, avoiding MO2's edition-selection dialog. Launch checks protect the declared edition. Legacy recipes retain their original configuration bytes and receipts. - The first real TTW Steam-client check verified candidate admission, the running-client safeguard, shortcut preservation and launch to the main menu after manually selecting an edition. Steam produced no recording file; gameplay, save/controller acceptance and an uninterrupted launch from a revised recipe remain open. See [the acceptance record](docs/native-steam-acceptance.md). diff --git a/README.md b/README.md index 0a04a83..5e449af 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ See [the review checklist and verification notes](docs/review.md). 2. Open setup details, then Install setup. Choose an installation plan and start the demo. 3. Pause, resume, cancel, or retry in Downloads. A complete demo takes approximately 20 seconds. 4. Open My library to launch a demo session, verify the simulated profile, or remove it. -5. On first run, choose **Set up real installations** for engine/account setup, then import a live manifest in Discover. Choose **Explore the demo** for the fictional catalog. Switch modes later with **Use dummy data** in Settings. Your choice, reduced motion, shortcut preferences, and folder selection persist. +5. On first run, choose **Set up real installations**, then **Open .loadstone** in Discover. The guide requests the package’s games, source archives and prepared files; local-only packages need no Wabbajack engine or Nexus connection. Wabbajack setups use the separate manifest import and engine in Settings. Choose **Explore the demo** for the fictional catalog. Switch modes later with **Use dummy data** in Settings. Your choice, reduced motion, shortcut preferences, and folder selection persist. 6. Export a privacy-conscious diagnostic summary or reset the demo workspace from Settings. Raw installation output remains local in Downloads. The demo catalog, requirements, installation sizes, profile validation, launch, and shortcut creation are simulated. Demo mode does not download mods, launch game processes, change game files, or modify Steam. Turning demo mode off prevents demo mutations in the main process as well as hiding sample data in the renderer. diff --git a/src/App.svelte b/src/App.svelte index c400403..fb9a090 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -401,7 +401,7 @@ }), ) ) - navigate(demo ? 'discover' : 'settings'); + navigate('discover'); }} /> {:else} diff --git a/src/EngineSettings.svelte b/src/EngineSettings.svelte index c5933a9..d45f55e 100644 --- a/src/EngineSettings.svelte +++ b/src/EngineSettings.svelte @@ -48,12 +48,12 @@ } -
+
-

Installation engine

+

Wabbajack installation engine

- Loadstone uses Wabbajack to reproduce curated setups from their original - sources. + Prepare this engine for imported Wabbajack setups. Native .loadstone + packages use Loadstone’s built-in installer and do not require it.

{#if engine} diff --git a/src/LiveCatalog.svelte b/src/LiveCatalog.svelte index cc661a4..43ab05b 100644 --- a/src/LiveCatalog.svelte +++ b/src/LiveCatalog.svelte @@ -70,7 +70,7 @@
-

Your setup manifests

+

Your Wabbajack setup manifests

Import a creator’s manifest and its original Wabbajack archive to check compatibility. @@ -92,11 +92,12 @@ {#if error}

{/if} {#if !lists.length}
-

No live setups imported

+

No Wabbajack setups imported

- Live installations need a versioned manifest with game hashes, storage - requirements, launch settings, and author credits. Prepare the engine - and connect your Nexus account in Settings before installing. + Wabbajack installations need a creator’s manifest and original archive. + Prepare the Wabbajack engine in Settings, then import the manifest to + check its game and source requirements. For a .loadstone package, use + Open .loadstone above.

{/if}
diff --git a/src/LiveJobs.svelte b/src/LiveJobs.svelte index 16f11ac..2073201 100644 --- a/src/LiveJobs.svelte +++ b/src/LiveJobs.svelte @@ -59,7 +59,7 @@

YOUR LIVE WORKSPACE

-

{library ? 'Wabbajack setups.' : 'Your installations.'}

+

{library ? 'Wabbajack setups.' : 'Wabbajack installations.'}

Real installation activity and saved recovery state.

@@ -68,7 +68,7 @@

{library ? 'No Wabbajack setups installed yet' - : 'No live installations started'} + : 'No Wabbajack installations started'}

Import a setup in Discover and check its compatibility to get started. diff --git a/src/Welcome.svelte b/src/Welcome.svelte index 98a041e..7bf30d4 100644 --- a/src/Welcome.svelte +++ b/src/Welcome.svelte @@ -32,20 +32,22 @@

  • An installed, supported copy of New Vegas, Skyrim Special Edition, - or Fallout 4, with the version and DLC required by your list.Your installed game, with the version and DLC required by the + setup. Tale of Two Wastelands requires both New Vegas and Fallout 3.
  • A trusted creator’s Loadstone manifest and original Wabbajack - archive, plus a separate folder with enough free space.A trusted creator’s .loadstone package and a separate folder with + enough free space. The installation guide asks for its required + games, original archives and prepared files.
  • The installation engine and, for automatic Nexus downloads, a Nexus - account with Premium access. Connect your key privately in Settings.For Nexus sources, select your manually downloaded originals or + connect a Premium account in Settings for automatic downloads. + Local-only packages need no Nexus connection.
@@ -53,8 +55,9 @@ >Set up real installations

- Next: prepare the engine and account, then import a setup in Discover. - Nothing downloads until you request it. + Next: open your .loadstone package in Discover. Wabbajack setups use the + separate manifest import and optional engine in Settings. Nothing + downloads until you request it.

diff --git a/tests/electron.test.mjs b/tests/electron.test.mjs index c7ee71e..025899d 100644 --- a/tests/electron.test.mjs +++ b/tests/electron.test.mjs @@ -280,7 +280,7 @@ test( await page.getByRole('switch', { name: 'Use dummy data' }).click(); await page.getByRole('button', { name: 'Discover', exact: true }).click(); await page - .getByRole('heading', { name: 'No live setups imported' }) + .getByRole('heading', { name: 'No Wabbajack setups imported' }) .waitFor(); assert.equal( await page @@ -303,7 +303,7 @@ test( await app.close(); page = await start(); await page - .getByRole('heading', { name: 'No live setups imported' }) + .getByRole('heading', { name: 'No Wabbajack setups imported' }) .waitFor(); await page.getByRole('button', { name: 'Settings', exact: true }).click(); assert.equal( diff --git a/tests/live.test.mjs b/tests/live.test.mjs index 15d6077..67469e4 100644 --- a/tests/live.test.mjs +++ b/tests/live.test.mjs @@ -196,7 +196,7 @@ test( window.loadstone.settings({ dummyData: false }), ); await page - .getByRole('heading', { name: 'No live setups imported' }) + .getByRole('heading', { name: 'No Wabbajack setups imported' }) .waitFor(); await app.evaluate(({ dialog }, manifestFile) => { dialog.showOpenDialog = async () => ({ diff --git a/tests/onboarding.test.mjs b/tests/onboarding.test.mjs index 7aee90e..c8a1950 100644 --- a/tests/onboarding.test.mjs +++ b/tests/onboarding.test.mjs @@ -7,7 +7,7 @@ import { createRequire } from 'node:module'; import { _electron as electron } from 'playwright'; const require = createRequire(import.meta.url); test( - 'first run explains live requirements, keeps the demo opt-in and remembers the real workspace', + 'first run opens native installation without an engine and remembers the real workspace', { timeout: 60000 }, async () => { const root = fs.mkdtempSync( @@ -55,7 +55,7 @@ test( .getByRole('button', { name: 'Set up real installations', exact: true }) .click(); await page - .getByRole('heading', { name: 'Installation engine', exact: true }) + .getByRole('button', { name: 'Open .loadstone', exact: true }) .waitFor(); const state = await page.evaluate(() => window.loadstone.getState()); assert.equal(state.settings.onboardingComplete, true); @@ -68,7 +68,7 @@ test( app = null; page = await start(); await page - .getByRole('heading', { name: 'No live setups imported' }) + .getByRole('heading', { name: 'No Wabbajack setups imported' }) .waitFor(); assert.equal( await page From 6acab578c394479ec98be330e9ff5b2e901d2d65 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 13 Oct 2026 21:28:31 -0400 Subject: [PATCH 2/3] Preserve declared Steam launch context and tolerate MO2 plugin state --- CHANGELOG.md | 1 + docs/loadstone-format.md | 2 +- docs/native-mo2-configuration.md | 13 ++++++++++-- electron/native-library.cjs | 19 ++++++++++++++--- electron/native-mo2-config.cjs | 13 +++++++++++- tests/native-library-fixture.mjs | 3 +++ tests/native-library.test.mjs | 24 +++++++++++++++++++-- tests/native-mo2-config.test.mjs | 36 ++++++++++++++++++++++++++++++++ 8 files changed, 102 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2059ae..d5b7bbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - First-run setup opens Discover directly for native .loadstone installation. Welcome, engine settings and Wabbajack empty states now explain which prerequisites belong to each installation path. +- Native MO2 executable declarations can retain the target game's Steam app ID without changing legacy receipts. Relaunch validation accepts unrelated plugin persistence keys written by MO2 while retaining checks on launch paths, profile isolation and declared executable settings. - Native MO2 recipes can declare a Steam or GOG edition matched to the target game requirement, avoiding MO2's edition-selection dialog. Launch checks protect the declared edition. Legacy recipes retain their original configuration bytes and receipts. - The first real TTW Steam-client check verified candidate admission, the running-client safeguard, shortcut preservation and launch to the main menu after manually selecting an edition. Steam produced no recording file; gameplay, save/controller acceptance and an uninterrupted launch from a revised recipe remain open. See [the acceptance record](docs/native-steam-acceptance.md). diff --git a/docs/loadstone-format.md b/docs/loadstone-format.md index eafb996..e183eff 100644 --- a/docs/loadstone-format.md +++ b/docs/loadstone-format.md @@ -77,7 +77,7 @@ Copy/extract outputs are `{path, bytes, sha256, action}`. A generated MO2 config - `{type: "copy", source: "source-id"}` copies a `game-file`, `prerequisite-file` or `payload`. Expected output bytes/hash must equal the input. - `{type: "extract", source: "archive-id", member: "Data/Example.esp"}` selects one safe member of a locked Nexus/HTTPS archive. The output hash is the hash of the extracted member. Nested archives, FOMOD execution, binary patches and arbitrary text substitution are not implemented by this version. -- `{type: "mo2-config", gameEdition: "Steam", executables: [...]}` generates only `ModOrganizer.ini` from bounded, relative executable declarations and recipe game/launch fields. `gameEdition` accepts `Steam` or `GOG` matching the target game requirement; it is optional for legacy recipes, whose exact generated bytes remain unchanged. It has no source or static output fingerprint. The materializer computes the exact destination-specific bytes/hash before writing and records them in the receipt after verification. An empty, fingerprinted `portable.txt` output is required. See the [complete generator contract](native-mo2-configuration.md), including serialization, size bounds and unsupported settings. +- `{type: "mo2-config", gameEdition: "Steam", executables: [...]}` generates only `ModOrganizer.ini` from bounded, relative executable declarations and recipe game/launch fields. `gameEdition` accepts `Steam` or `GOG` matching the target game requirement; executable entries may declare `steamAppId` matching the supported target game and its Steam requirement. Both fields are optional for legacy recipes, whose exact generated bytes remain unchanged. It has no source or static output fingerprint. The materializer computes the exact destination-specific bytes/hash before writing and records them in the receipt after verification. An empty, fingerprinted `portable.txt` output is required. See the [complete generator contract](native-mo2-configuration.md), including serialization, size bounds and unsupported settings. Outputs must include `ModOrganizer.exe`, `ModOrganizer.ini`, the declared profile's `modlist.txt` and `plugins.txt`, and the target game executable beneath `launch.gameRoot`. Mod priorities, disabled entries and plugin order are carried by the exact profile files. They are not inferred from source order. Final-file verification checks each static or derived output fingerprint after reconstruction. Save paths may never be claimed as owned recipe outputs. diff --git a/docs/native-mo2-configuration.md b/docs/native-mo2-configuration.md index 6f5bb83..29839d1 100644 --- a/docs/native-mo2-configuration.md +++ b/docs/native-mo2-configuration.md @@ -16,7 +16,8 @@ This is the only output kind without a static `bytes`/`sha256` pair or a source { "title": "New Vegas", "binary": "Game/FalloutNV.exe", - "workingDirectory": "Game" + "workingDirectory": "Game", + "steamAppId": "22380" } ] } @@ -33,6 +34,12 @@ New recipes should declare `gameEdition` as `Steam` or `GOG`, matching the `stor The field is optional only for compatibility with existing recipes and reconstruction receipts. Omitting it preserves the original generated bytes exactly and can leave MO2 asking for an edition at first launch. It does not silently upgrade an existing candidate. Recipes using the field have a different canonical fingerprint and are rejected by older Loadstone implementations. Build a new candidate from the revised recipe; retain any existing saves and configuration. This declaration does not certify GOG runtime support or convert game files between stores. +### Executable Steam app ID + +An executable may declare `steamAppId` as a string matching Loadstone's supported target game: `22380` for New Vegas/TTW, `489830` for Skyrim SE or `377160` for Fallout 4. The target game's requirement must use `store: "steam"`; TTW's Fallout 3 requirement does not determine this value. Other IDs, numbers, empty values and GOG requirements are rejected. Regional app IDs are not supported by this declaration. + +This writes that executable's `steamAppID` setting for MO2's Steam launch context. The field is optional to preserve existing generated bytes and receipts. Adding it changes the recipe identity and requires a new candidate; it does not change an existing receipt or grant game ownership. Native launch rejects a changed or missing declared app ID. A private TTW check found this setting in the installed baseline but absent from earlier generated configuration; actual launch and recording outcomes are tracked in the [acceptance record](native-steam-acceptance.md). + ## Deterministic rendering and verification The canonical recipe fingerprint locks all generator inputs. Before creating a destination, the materializer renders UTF-8/LF configuration for that absolute Windows drive path and computes the resulting exact byte length and SHA-256. Stream verification and the final on-disk pass use this derived fingerprint. The receipt's `generatedConfigurations` array records `{path, generator, bytes, sha256}` without embedding the destination. Static copy/extract outputs retain their existing exact fingerprint contract. A `mo2-config` output cannot also declare a static fingerprint: rendered bytes depend on the destination. @@ -41,7 +48,7 @@ The generator produces these sections in order, with a blank line between sectio - `General`: game name (`TTW`, `New Vegas`, `Skyrim Special Edition` or `Fallout 4`), local game path, selected profile, `first_start=false`, then `game_edition` as a quoted Qt string when declared. - `Settings`: local base, downloads, mods, profiles, overwrite and webcache paths; `profile_local_inis=true` and `profile_local_saves=true` defaults. -- `customExecutables`: array size, then each entry's title, local binary, local working directory and arguments (empty when omitted, otherwise a space-separated sequence of quoted local paths), preserving declared entry order. +- `customExecutables`: array size, then each entry's title, local binary, local working directory and arguments (empty when omitted, otherwise a space-separated sequence of quoted local paths), followed by `steamAppID` when declared, preserving declared entry order. Paths use `/`. Qt string values are double-quoted, escape backslash and quote, and double a leading `@`. Game path and selected profile use `@ByteArray(...)` with every UTF-8 byte encoded as lowercase `\xNN`. The implementation in `electron/native-mo2-config.cjs` defines the exact serialization; byte changes require a new generator action identity, not a silent change to an existing recipe's meaning. @@ -55,4 +62,6 @@ Absolute paths are deliberately bound at installation. Moving a finished candida Path and executable semantics were checked against the [ModdingLinked MO2 settings implementation](https://github.com/ModdingLinked/modorganizer/blob/9c130cb/src/settings.cpp), [MO2 executable loading](https://github.com/ModOrganizer2/modorganizer/blob/master/src/executableslist.cpp), and [Qt INI serialization](https://github.com/qt/qtbase/blob/6.7/src/corelib/io/qsettings.cpp). The original generator does not copy upstream implementation code. MO2's mod-directory setting is `mod_directory` (singular); installed inventory now honors it. +MO2's [process launcher](https://github.com/ModdingLinked/modorganizer/blob/9c130cb/src/spawn.cpp) uses the executable's Steam app ID for its child environment. Native launch validation reads the required General, Settings and customExecutables sections, retaining strict alias/duplicate/path checks. Unrelated plugin and UI section values are left alone, including MO2's normal `Python%20Proxy` persistence key. Profile validation reads its General section separately. + Tests cover different destinations, non-ASCII and INI delimiter encoding, invalid references and fields, final-file tampering, destination refusal and the real Electron review/build/restart workflow. Inventory and existing launch-layout checks decode the generated UTF-8 byte arrays and quoted strings; unrelated binary INI state is not decoded or exported. A private interoperability check also read synthetic generated configuration through Qt 6.7.1's actual `QSettings`, including UTF-8 byte arrays, literal `@`, quoted delimiters and executable arrays. This verifies serialization, not MO2 launch or gameplay. diff --git a/electron/native-library.cjs b/electron/native-library.cjs index c360129..42b4c4a 100644 --- a/electron/native-library.cjs +++ b/electron/native-library.cjs @@ -42,7 +42,7 @@ function read(file, limit) { ); return data; } -function iniEntries(data) { +function iniEntries(data, requiredSections) { const sections = new Map(); const sectionNames = new Set(); let section, keys; @@ -59,6 +59,12 @@ function iniEntries(data) { 'Duplicate MO2 configuration section.', ); sectionNames.add(heading[1].toLowerCase()); + // MO2 adds mutable plugin/UI state, including percent-encoded keys. + // Parse launch settings only; section aliases stay rejected above. + if (!requiredSections.includes(heading[1])) { + section = null; + continue; + } section = new Map(); keys = new Set(); sections.set(heading[1], section); @@ -138,8 +144,9 @@ function launchLayout(meta) { 'Restore the empty portable marker before launching this candidate.', ); const ini = read(path.join(root, 'ModOrganizer.ini'), 2_000_000); - const actual = iniEntries(ini), - expected = iniEntries(rendered.data); + const requiredSections = ['General', 'Settings', 'customExecutables']; + const actual = iniEntries(ini, requiredSections), + expected = iniEntries(rendered.data, requiredSections); demand( actual.get('General')?.get('gameName') === expected.get('General').get('gameName'), @@ -183,6 +190,11 @@ function launchLayout(meta) { (v) => v.title === recipe.launch.executable, ); const declared = config.action.executables[index]; + if (declared.steamAppId) + demand( + entries.get(prefix + 'steamAppID') === declared.steamAppId, + 'The selected MO2 Steam app ID differs from the registered recipe.', + ); for (const key of ['binary', 'workingDirectory', 'arguments']) { const configured = entries.get(prefix + key) ?? ''; const wanted = expected @@ -207,6 +219,7 @@ function launchLayout(meta) { directory(profile); const settings = iniEntries( read(path.join(profile, 'settings.ini'), 2_000_000), + ['General'], ).get('General'); demand( settings?.get('LocalSaves') === 'true' && diff --git a/electron/native-mo2-config.cjs b/electron/native-mo2-config.cjs index 6b5a548..b62e63f 100644 --- a/electron/native-mo2-config.cjs +++ b/electron/native-mo2-config.cjs @@ -1,6 +1,6 @@ const path = require('node:path'); const crypto = require('node:crypto'); -const { relativeFile } = require('./manifest.cjs'); +const { relativeFile, games } = require('./manifest.cjs'); const configBytes = 1_000_000; const gameNames = { @@ -81,7 +81,17 @@ function validateMo2Config(output, recipe) { 'binary', 'workingDirectory', ...(Object.hasOwn(entry || {}, 'arguments') ? ['arguments'] : []), + ...(Object.hasOwn(entry || {}, 'steamAppId') ? ['steamAppId'] : []), ]); + if (Object.hasOwn(entry, 'steamAppId')) { + const store = recipe.requirements.find( + (v) => v.id === recipe.game.id, + )?.store; + demand( + store === 'steam' && entry.steamAppId === games[recipe.game.id]?.appId, + 'steamAppId must match the supported target game and its Steam requirement.', + ); + } demand( typeof entry.title === 'string' && entry.title.trim() === entry.title && @@ -226,6 +236,7 @@ function renderMo2Config(output, recipe, destination) { entry.arguments.map((v) => '"' + local(v.path) + '"').join(' '), ) : ''), + ...(entry.steamAppId ? [prefix + 'steamAppID=' + entry.steamAppId] : []), ); } const data = Buffer.from(lines.join('\n') + '\n', 'utf8'); diff --git a/tests/native-library-fixture.mjs b/tests/native-library-fixture.mjs index e1de5d6..e0f8e04 100644 --- a/tests/native-library-fixture.mjs +++ b/tests/native-library-fixture.mjs @@ -12,6 +12,9 @@ const fingerprint = (data) => ({ }); export async function libraryFixture({ extraFiles = [] } = {}) { const f = createNativeFixture({ mo2Config: true }); + f.recipe.outputs.find( + (v) => v.action.type === 'mo2-config', + ).action.executables[0].steamAppId = '22380'; const game = 'MZ synthetic owned game; never execute'; Object.assign(f.recipe.requirements[0].files[0], fingerprint(game)); Object.assign(f.recipe.sources[0], fingerprint(game)); diff --git a/tests/native-library.test.mjs b/tests/native-library.test.mjs index 5136f7e..313b369 100644 --- a/tests/native-library.test.mjs +++ b/tests/native-library.test.mjs @@ -83,6 +83,13 @@ test('native checks reject redirected saves, changed executables and unsafe prof originalIni.replace('game_edition="Steam"', 'game_edition=Steam'), ); assert.doesNotThrow(() => nativeLaunchPlan(record)); + for (const replacement of ['1\\steamAppID=22490', '']) { + fs.writeFileSync( + iniFile, + originalIni.replace(/1\\steamAppID=.*/, replacement), + ); + assert.throws(() => nativeLaunchPlan(record), /Steam app ID differs/); + } fs.writeFileSync( iniFile, originalIni.replace(/mod_directory=.*/, `mod_directory=${f.owned}`), @@ -90,9 +97,14 @@ test('native checks reject redirected saves, changed executables and unsafe prof assert.throws(() => nativeLaunchPlan(record), /inside this candidate/); fs.writeFileSync( iniFile, - originalIni + '\n[Geometry]\nwindow_state=synthetic\n', + originalIni + + '\n[Geometry]\nwindow_state=synthetic\n' + + '[PluginPersistance]\nPython%20Proxy\\tryInit=false\n' + + '[recentDirectories]\nopen%20directory=C:/synthetic\n', ); - await library.verify(record.id); // Unrelated MO2 window state is mutable. + await library.verify(record.id); // MO2's unrelated plugin/UI state is mutable. + assert.doesNotThrow(() => nativeLaunchPlan(record)); + assert.match(fs.readFileSync(iniFile, 'utf8'), /Python%20Proxy/); fs.writeFileSync( iniFile, originalIni.replace(/1\\arguments=.*/, '1\\arguments=changed'), @@ -103,6 +115,14 @@ test('native checks reject redirected saves, changed executables and unsafe prof originalIni.replace('[Settings]', '[Settings]\nMOD_DIRECTORY=outside'), originalIni + '\n [general]\ngamePath=outside\n', originalIni.replace('[General]', '[General]\ngame%50ath=outside'), + originalIni.replace('[Settings]', '[Settings]\nmod%5Fdirectory=outside'), + originalIni.replace( + '[customExecutables]', + '[customExecutables]\n1\\steam%41ppID=22490', + ), + originalIni + '\n[%47eneral]\ngamePath=outside\n', + originalIni + + '\n[PluginPersistance]\nPython%20Proxy\\tryInit=false\n[general]\ngamePath=outside\n', ]) { fs.writeFileSync(iniFile, replacement); assert.throws(() => nativeLaunchPlan(record), /Duplicate|Encoded/); diff --git a/tests/native-mo2-config.test.mjs b/tests/native-mo2-config.test.mjs index 3baf424..9cb5878 100644 --- a/tests/native-mo2-config.test.mjs +++ b/tests/native-mo2-config.test.mjs @@ -74,6 +74,42 @@ test('recipes without an edition retain their pre-extension configuration finger ); }); +test('executable Steam app IDs are explicit and tied to the target game storefront', () => { + const { recipe } = createNativeFixture({ mo2Config: true }); + const config = recipe.outputs.find((v) => v.action.type === 'mo2-config'); + const entry = config.action.executables[0]; + const render = () => renderMo2Config(config, recipe, 'C:/Loadstone/Steam'); + const legacy = render(); + entry.steamAppId = '22380'; + assert.match(render().data.toString(), /^1\\steamAppID=22380$/m); + assert.notEqual(render().sha256, legacy.sha256); + recipe.game.variant = 'ttw'; + recipe.requirements.unshift({ id: 'fallout3', store: 'gog', files: [] }); + assert.doesNotThrow(render); + for (const invalid of [ + '22490', + '489830', + 22380, + '', + null, + '22380\n[Settings]', + ]) { + entry.steamAppId = invalid; + assert.throws(render, /steamAppId must match/); + } + entry.steamAppId = '22380'; + recipe.requirements.find((v) => v.id === 'fnv').store = 'gog'; + config.action.gameEdition = 'GOG'; + assert.throws(render, /steamAppId must match/); + delete entry.steamAppId; + assert.doesNotThrow(render); + recipe.game.variant = 'base'; + recipe.requirements.shift(); + recipe.requirements[0].store = 'steam'; + config.action.gameEdition = 'Steam'; + assert.deepEqual(render(), legacy); +}); + test('one recipe generates isolated MO2 paths at two destinations with exact derived fingerprints', async () => { const f = await fixture(); assert.equal(f.inspected.summary.generatedConfigurations, 1); From 49f683c028b5ffd344465ef9af2c1046d4806ea1 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 13 Oct 2026 21:35:53 -0400 Subject: [PATCH 3/3] Record fresh TTW admission and unresolved direct Steam startup --- CHANGELOG.md | 1 + docs/native-steam-acceptance.md | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b7bbf..7856deb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Native MO2 executable declarations can retain the target game's Steam app ID without changing legacy receipts. Relaunch validation accepts unrelated plugin persistence keys written by MO2 while retaining checks on launch paths, profile isolation and declared executable settings. - Native MO2 recipes can declare a Steam or GOG edition matched to the target game requirement, avoiding MO2's edition-selection dialog. Launch checks protect the declared edition. Legacy recipes retain their original configuration bytes and receipts. - The first real TTW Steam-client check verified candidate admission, the running-client safeguard, shortcut preservation and launch to the main menu after manually selecting an edition. Steam produced no recording file; gameplay, save/controller acceptance and an uninterrupted launch from a revised recipe remain open. See [the acceptance record](docs/native-steam-acceptance.md). +- A separate full-size TTW version passed guided reconstruction and 13,952-file library admission. Its edition prompt is resolved, but direct launch reaches a Steam load error, including a diagnostic with the baseline's explicit Steam app ID. No footage or gameplay acceptance is claimed; the acceptance record retains both results and the remaining client checks. ## 0.4.0 — native installation preview diff --git a/docs/native-steam-acceptance.md b/docs/native-steam-acceptance.md index 72961ac..d42826f 100644 --- a/docs/native-steam-acceptance.md +++ b/docs/native-steam-acceptance.md @@ -23,6 +23,27 @@ On October 13, 2026, the private 0.4.0 portable was exercised against the isolat The generator previously omitted the edition field present in the installed source configuration. The optional, validated `gameEdition` recipe declaration addresses that missing field without silently changing legacy generated bytes or receipt fingerprints. Recipes using it are new recipe identities. Unit/integration checks cover storefront consistency, mixed-store TTW requirements, old output identity and launch-time drift checks; synthetic Electron flows exercise the extended declaration. This source change is newer than the tested 0.4.0 portable above. -A fresh candidate built with the revised declaration still needs an uninterrupted real launch. Diagnose recording and inspect a short game-video sample before longer play; do not substitute desktop capture. New-game/save/reload, controller input, mod behavior, maintenance after actual play, Desktop launch and clean-machine/provider acceptance remain open. No runtime support claim follows from the GOG declaration validator. Featured-pack source completion, permissions and publication are separate gates. +A fresh candidate was built and tested below. An uninterrupted real launch remains required. Diagnose recording and inspect a short game-video sample before longer play; do not substitute desktop capture. New-game/save/reload, controller input, mod behavior, maintenance after actual play, Desktop launch and clean-machine/provider acceptance remain open. No runtime support claim follows from the GOG declaration validator. Featured-pack source completion, permissions and publication are separate gates. Private evidence is retained under the ignored `test-results/native-acceptance/ttw-recorded/` directory. It contains local paths and account-specific shortcut backups and must not enter public archives. No game footage was produced for publication. + +## Fresh edition candidate and direct-launch diagnosis + +Later on October 13 (October 14 UTC), the actual Electron app's **Install another version** guide built private version `2026.10.13-edition` into a separate folder. It used the same owned-game/local-prerequisite inputs and all 13,951 unchanged static output records, plus the new Steam edition declaration in generated configuration. No source files or payloads were published. + +- Build source: `a7d18db882f556ce48236dcfc835c31c84aefb55`. +- Recipe SHA-256: `54169106ed7275e55c8aa7cb6564a19217e2adda3bc7a44041abd210957aa8f1`. +- Build completed at `2026-10-14T00:33:01.971Z`, with all 13,952 outputs verified. Its generated configuration was 2,200 bytes, SHA-256 `ec02f324d022e524c27fae63cba4b3fd1e05132e9e0d9aaf32206c347d9cf130`. +- Clean source build `b7b05f9aba756563f33c4e29e14b71f48aee506c` subsequently rechecked every output and admitted the separate version through Downloads. Both old and new library entries persisted. + +The earlier Steam recording log associated MO2 with the non-Steam shortcut and the child game with app ID 22380. It reported that another recording was already starting and later that the shortcut's timeline was missing. This suggests competing recording identities; it does not establish the complete cause or a working remedy. + +For the next check, Steam was running with background recording, a verified output folder, microphone off and game audio only. Launching the new version from Loadstone started the correct candidate-local MO2 and FalloutNV processes without an edition prompt. The game then displayed **Application load error 5:0000065434** before reaching its menu. + +The installed baseline explicitly declares `steamAppID=22380` for its selected MO2 executable. Earlier generated configuration omitted that field. A backed-up, candidate-only diagnostic restored that setting without changing the recipe or receipt. The first retry exposed a separate Loadstone rejection of MO2's normal `Python%20Proxy` plugin persistence key. Clean build `6acab578c394479ec98be330e9ff5b2e901d2d65` fixed that parser scope and reached the game process again; the same Steam error remained. **The app-ID setting alone did not resolve direct startup.** The diagnostic setting was restored afterward, and all intermediate configurations and logs were retained privately. + +The source now supports explicit, target-matched Steam app IDs while preserving legacy rendering. Launch checks protect declared IDs and continue rejecting aliases, duplicates, redirected paths, unsafe profile settings and changed executables. Unrelated plugin/UI state no longer blocks relaunch. The full fixture suite passed 183 tests with one optional skip; Svelte checks reported zero errors and warnings. These checks do not certify the failing real-game launch. + +Both actual game attempts ended at the Steam error. No new recording appeared, no clip could be exported, and no game/save/controller acceptance is claimed. The microphone remained off and the prior recording audio source was restored. All 31 protected original/previous-candidate profile, configuration and save files remained unchanged. New-candidate profile/configuration writes were backed up; no save was created or loaded. The existing portable/depot preview retains its older provenance. + +Evidence for this pass is ignored under `test-results/native-acceptance/ttw-edition/`. Next work must resolve the direct Steam initialization failure and separately retest a fresh recipe with all required declarations, then verify actual game footage before longer play. The previous successful Steam-shortcut menu launch does not establish direct-launch or recording success for this candidate.