Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

## 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).
- 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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/loadstone-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
13 changes: 11 additions & 2 deletions docs/native-mo2-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Expand All @@ -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.
Expand All @@ -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.

Expand All @@ -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.
23 changes: 22 additions & 1 deletion docs/native-steam-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
19 changes: 16 additions & 3 deletions electron/native-library.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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
Expand All @@ -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' &&
Expand Down
Loading
Loading