From c2ccaf51343f88350759c91ffc840d78b63b8a84 Mon Sep 17 00:00:00 2001 From: Issa Loubani Date: Thu, 13 Aug 2026 00:34:46 +0300 Subject: [PATCH 1/5] Detect the install type instead of assuming a Steam build The install type came from argv[0], which on Steam is the asset decryption key. That is circular on a copy that has no key, and two things broke because of it: - encryption.js always read data/System.KEL to get the asset key. On a plaintext install that file does not exist, so it threw during startup and left _modloader_encryption undefined. - every injection point used the encrypted names (.KEL, .PLUTO, .HERO, .AUBREY, .OMORI, .rpgmvp). A game asking for stock RPG Maker MV names matched nothing in the overlay, so mods silently did nothing. The install is now detected from whether data/System.KEL is on disk, and the targets and encryption follow from that: steam .KEL/.PLUTO/.HERO/.AUBREY/.OMORI + AES / rpgmaker XOR plaintext stock RPG Maker MV names + pass-through Only Steam builds are asked for a launch argument now, since a non-Steam copy starts without one. The language mount point also comes from the game's own Text_Language_Processor parameters in both branches, and plugins.js is read relative to the game directory rather than the working directory. The old relative read quietly fell back to "en" any time the process started somewhere else. Steam and playtest rule sets come out byte for byte the same as before. --- www/index.html | 18 ++++-- www/modloader/_oneloader_configuration.js | 74 +++++++++++++++++------ www/modloader/encryption.js | 7 ++- 3 files changed, 74 insertions(+), 25 deletions(-) diff --git a/www/index.html b/www/index.html index 2e39476..5df99b2 100644 --- a/www/index.html +++ b/www/index.html @@ -7,10 +7,20 @@ OMORI diff --git a/www/modloader/_oneloader_configuration.js b/www/modloader/_oneloader_configuration.js index 7fd7275..26831a8 100644 --- a/www/modloader/_oneloader_configuration.js +++ b/www/modloader/_oneloader_configuration.js @@ -1,18 +1,39 @@ -if (window.nw.App.argv[0] !== "test") { - console.log("Loading basic oneloader configuration."); - const MAX_MANIFEST_VERSION = 1; - const ID_BLACKLIST = ["gomori"]; - const EXTENSION_RULES = { - "png": { "encrypt": "rpgmaker", "target_extension": "rpgmvp", "raw": ["img/system/window.png", "img/system/loading.png"] }, - "ogg": { "encrypt": "rpgmaker", "target_extension": "rpgmvo" } - }; +/* + * Which injection targets and which encryption layer are correct depends on how the copy of + * the game was distributed: + * + * steam - assets are AES encrypted and renamed (data/System.KEL, js/plugins/*.OMORI). + * plaintext - any non-Steam build: stock RPG Maker MV layout, nothing encrypted. + * test - RPG Maker playtest. Also plaintext, but changes more than this file. + * + * "test" is detected from argv because $modLoader.isInTestMode alters URL handling in the web + * VFS too. "steam" is detected from the presence of the encrypted System file rather than from + * argv, so a non-Steam copy is not required to be launched with a decryption key it lacks. + */ +var $ONELOADER_INSTALL_TYPE = (function () { + const fs = require("fs"); + const path = require("path"); + const base = path.dirname(process.mainModule.filename); + + if (window.nw.App.argv[0] === "test") return "test"; + if (fs.existsSync(path.join(base, "data", "System.KEL"))) return "steam"; + return "plaintext"; +})(); + +/* + * Language folder that text mods are mounted into, read from the game's own + * Text_Language_Processor parameters so mods land where the game will look for them. + */ +var $ONELOADER_LANGUAGE = (function () { + const fs = require("fs"); + const path = require("path"); + const base = path.dirname(process.mainModule.filename); - let LANGUAGE = "en"; + let language = "en"; try { - const fs = require("fs"); - const js = fs.readFileSync("www/js/plugins.js", "utf8") - eval(js.replace("var $plugins", "window.___tmp_plugins")) - LANGUAGE = window.___tmp_plugins.filter( + const js = fs.readFileSync(path.join(base, "js", "plugins.js"), "utf8"); + eval(js.replace("var $plugins", "window.___tmp_plugins")); + language = window.___tmp_plugins.filter( a => a.name.toLowerCase() === "text_language_processor" )[0].parameters["Default Language"]; } catch (error) { @@ -21,6 +42,18 @@ if (window.nw.App.argv[0] !== "test") { } finally { window.___tmp_plugins = undefined; } + return language; +})(); + +if ($ONELOADER_INSTALL_TYPE === "steam") { + console.log("Loading basic oneloader configuration."); + const MAX_MANIFEST_VERSION = 1; + const ID_BLACKLIST = ["gomori"]; + const IMAGE_EXTENSION = "rpgmvp"; + const EXTENSION_RULES = { + "png": { "encrypt": "rpgmaker", "target_extension": "rpgmvp", "raw": ["img/system/window.png", "img/system/loading.png"] }, + "ogg": { "encrypt": "rpgmaker", "target_extension": "rpgmvo" } + }; const DATA_RULES = [ { @@ -50,7 +83,7 @@ if (window.nw.App.argv[0] !== "test") { "yamld": { target: "HERO", delta: true, delta_method: "yaml", encrypt: true }, "hero": { target: "HERO", delta: false, encrypt: false } }, - mountPoint: "languages/" + LANGUAGE + mountPoint: "languages/" + $ONELOADER_LANGUAGE }, { jsonKeys: [ @@ -79,12 +112,14 @@ if (window.nw.App.argv[0] !== "test") { ]; window.$ONELOADER_CONFIG = { - MAX_MANIFEST_VERSION, ID_BLACKLIST, EXTENSION_RULES, DATA_RULES + MAX_MANIFEST_VERSION, ID_BLACKLIST, EXTENSION_RULES, DATA_RULES, IMAGE_EXTENSION, + INSTALL_TYPE: $ONELOADER_INSTALL_TYPE }; } else { - console.log("Loading playtest configuration"); + console.log("Loading plaintext configuration"); const MAX_MANIFEST_VERSION = 1; const ID_BLACKLIST = ["gomori"]; + const IMAGE_EXTENSION = "png"; const EXTENSION_RULES = { "png": { "encrypt": "rpgmaker", "target_extension": "png" }, "ogg": { "encrypt": "rpgmaker", "target_extension": "ogg" } @@ -118,7 +153,7 @@ if (window.nw.App.argv[0] !== "test") { "yamld": { target: "yaml", delta: true, delta_method: "yaml", encrypt: true }, "hero": { target: "HERO", delta: false, encrypt: false } // Mods that ship encrypted files WILL fail. Don't do that. Not cool. }, - mountPoint: "languages/en" + mountPoint: "languages/" + $ONELOADER_LANGUAGE }, { jsonKeys: [ @@ -147,6 +182,7 @@ if (window.nw.App.argv[0] !== "test") { ]; window.$ONELOADER_CONFIG = { - MAX_MANIFEST_VERSION, ID_BLACKLIST, EXTENSION_RULES, DATA_RULES + MAX_MANIFEST_VERSION, ID_BLACKLIST, EXTENSION_RULES, DATA_RULES, IMAGE_EXTENSION, + INSTALL_TYPE: $ONELOADER_INSTALL_TYPE }; -} \ No newline at end of file +} diff --git a/www/modloader/encryption.js b/www/modloader/encryption.js index 28e3a54..5239131 100644 --- a/www/modloader/encryption.js +++ b/www/modloader/encryption.js @@ -1,5 +1,8 @@ const crypto = require('crypto'); -if (window.nw.App.argv[0] !== "test") +// Only the Steam build is encrypted. Playtest and non-Steam builds keep plaintext assets, +// so they get the pass-through implementation below instead of an AES layer keyed on a +// Steam launch argument they were never given. +if (window.$ONELOADER_CONFIG.INSTALL_TYPE === "steam") { window._modloader_encryption = new (class Encryption { constructor() { @@ -85,7 +88,7 @@ if (window.nw.App.argv[0] !== "test") } })(); } else { - console.log("Loading fallback encryption system for test mode"); + console.log("Loading pass-through encryption system for a plaintext install"); window._modloader_encryption = new (class BSEncryption { decrypt(buffer) { return buffer; } encrypt(buffer) { return buffer; } From 70ec6d8e2967c8c2830d55ced430cd7d2904ad62 Mon Sep 17 00:00:00 2001 From: Issa Loubani Date: Thu, 13 Aug 2026 00:34:46 +0300 Subject: [PATCH 2/5] Use the configured picture extension for image deltas Image deltas had .rpgmvp hardcoded in five places, so .olid patching pointed at a file name that only exists on the encrypted Steam build. Take the extension from the active configuration instead. That also fixes image deltas under --test, where pictures were already mounted as .png while the delta applier still looked for .rpgmvp. --- www/modloader/imagedelta_applier.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/www/modloader/imagedelta_applier.js b/www/modloader/imagedelta_applier.js index 15f99ac..974186d 100644 --- a/www/modloader/imagedelta_applier.js +++ b/www/modloader/imagedelta_applier.js @@ -31,6 +31,10 @@ async function __modloader_image_delta(knownMods) { const cache_base = path.join(base, '..', '.oneloader-image-cache'); + // Pictures live under a different extension depending on how the game was distributed + // (rpgmvp when encrypted, png otherwise), so deltas have to target whichever is in use. + const imageTarget = i => i.replace(/\.png$/g, "." + $ONELOADER_CONFIG.IMAGE_EXTENSION); + if (!native_fs.existsSync(cache_base)) { native_fs.mkdirSync(cache_base); } @@ -86,7 +90,7 @@ async function __modloader_image_delta(knownMods) { if (deltas.length < 1) continue; - const image_data = _modloader_encryption.decryptAsset(await _vfs_resolve_file(image.replace(/\.png$/g, '.rpgmvp'))); + const image_data = _modloader_encryption.decryptAsset(await _vfs_resolve_file(imageTarget(image))); job_id = `${job_id}+${shaDigest(image_data)}`; job_id = `${job_id}:${deltas.sort((a, b) => {return a.salt > b.salt?1:-1}).map(a => a.salt).join(":")}`; @@ -163,8 +167,8 @@ async function __modloader_image_delta(knownMods) { $oneLoaderGui.inc(); - let oDeep = _ensure_overlay_path($modLoader.overlayFS, image.replace(/\.png$/g, '.rpgmvp').toLowerCase()); - let bruh = _overlay_fs_split_path(image.replace(/\.png$/g, '.rpgmvp').toLowerCase()); + let oDeep = _ensure_overlay_path($modLoader.overlayFS, imageTarget(image).toLowerCase()); + let bruh = _overlay_fs_split_path(imageTarget(image).toLowerCase()); let lastFile = bruh[bruh.length - 1]; oDeep[lastFile] = { @@ -173,9 +177,9 @@ async function __modloader_image_delta(knownMods) { type: "filesystem" }, delta: false, - injectionPoint: image.replace(/\.png$/g, '.rpgmvp').toLowerCase(), + injectionPoint: imageTarget(image).toLowerCase(), mode: "rpgmaker", - ogName: path.basename(image.replace(/\.png$/g, '.rpgmvp')) + ogName: path.basename(imageTarget(image)) }; } } \ No newline at end of file From a286398008bddaefdca41015563933169a7e1992 Mon Sep 17 00:00:00 2001 From: Issa Loubani Date: Thu, 13 Aug 2026 00:34:46 +0300 Subject: [PATCH 3/5] Shadow an empty argv as [] instead of [undefined] Non-Steam copies launch with no arguments, so the shadowed argv came out as [undefined]: length 1, holding nothing. Utils.isOptionValid checks argv.length > 0 and then calls argv[0].split('&'), which throws. The first caller is SceneManager.preferableRendererType, which runs at the second step of boot, before Graphics exists. Graphics.printError then threw on its own inside _clearUpperCanvas, so what actually surfaced was "Cannot read property 'getContext' of undefined" and the real cause was gone. --- www/modloader/early_loader.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/www/modloader/early_loader.js b/www/modloader/early_loader.js index a6f0fad..49beae3 100644 --- a/www/modloader/early_loader.js +++ b/www/modloader/early_loader.js @@ -77,10 +77,18 @@ /* Install the argv handler and shadow the true argv object to allow the base game to work normally */ { let key = window.nw.App.argv[0]; $modLoader.realArgv = window.nw.App.argv; + /* + * A non-Steam copy is launched with no arguments at all, so there is no key to keep. + * Shadowing with [undefined] would still report a length of 1, and every caller of + * Utils.isOptionValid does `nw.App.argv[0].split('&')` once the length check passes - + * which throws before Graphics exists and takes the whole boot down. Hide an empty + * argv as an empty array instead. + */ + let shadowedArgv = (key === undefined) ? [] : [key]; window.nw.App = new Proxy(window.nw.App, { get(t, p, r) { if (p === "argv") { - return [key]; + return shadowedArgv; } else { return Reflect.get(...arguments); } From ac7a41681cbcc80cbaedfcb3cc96a20e18bc1bd3 Mon Sep 17 00:00:00 2001 From: Issa Loubani Date: Thu, 13 Aug 2026 00:35:20 +0300 Subject: [PATCH 4/5] Log the real boot error before printError hides it Graphics.printError throws inside _clearUpperCanvas when it runs before the canvases exist. Anything failing during SceneManager.initialize therefore reports as "Cannot read property 'getContext' of undefined" whatever the actual cause was, so every early boot failure looks the same and none of them name anything. Wrap SceneManager.catchException so the true stack reaches latest.log first. Scripts that fail to load, and anything escaping window.onload(), are logged as well. --- www/modloader/start_game.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/www/modloader/start_game.js b/www/modloader/start_game.js index a92662d..e0ed8d3 100644 --- a/www/modloader/start_game.js +++ b/www/modloader/start_game.js @@ -62,6 +62,31 @@ async function _start_game() { await $modLoader.$runScripts("pre_window_onload", {}); + /* + * Boot diagnostics. + * + * When the game throws before Graphics has finished initializing, SceneManager hands the + * error to Graphics.printError, which itself throws ("Cannot read property 'getContext' + * of undefined" in _clearUpperCanvas) because the canvases do not exist yet. That + * secondary TypeError is what propagates out, so the original cause is lost. Record the + * real error, plus any script that fails to load, before that happens. + */ + window.addEventListener("error", function (e) { + if (e.target && e.target.tagName === "SCRIPT") { + window._logLine("[SCRIPT LOAD FAILED] " + (e.target.src || e.target._url || "unknown")); + } else if (e.error) { + window._logLine("[UNCAUGHT] " + (e.error.stack || e.error)); + } + }, true); + + if (window.SceneManager && SceneManager.catchException) { + const _oneloader_catchException = SceneManager.catchException; + SceneManager.catchException = function (e) { + window._logLine("[REAL BOOT ERROR] " + ((e && e.stack) ? e.stack : e)); + return _oneloader_catchException.call(this, e); + }; + } + if ($modLoader.isInTestMode) { $oneLoaderGui.setHt("Playtest: Giving the game time to load plugins."); $oneLoaderGui.setPbMax(5); @@ -72,7 +97,12 @@ async function _start_game() { $oneLoaderGui.setPbCurr(i + 1); } } - window.onload(); + try { + window.onload(); + } catch (e) { + window._logLine("[GAME BOOT THREW] " + ((e && e.stack) ? e.stack : e)); + throw e; + } $oneLoaderGui.container.style.opacity = 0; } \ No newline at end of file From 8853b5a07cb170c099adc97374be3b2fabe3a678 Mon Sep 17 00:00:00 2001 From: Issa Loubani Date: Thu, 13 Aug 2026 00:35:53 +0300 Subject: [PATCH 5/5] Wait for plugin scripts before calling window.onload main.js calls PluginManager.setup, which appends one