From c2ccaf51343f88350759c91ffc840d78b63b8a84 Mon Sep 17 00:00:00 2001 From: Issa Loubani Date: 2026年8月13日 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: 2026年8月13日 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: 2026年8月13日 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: 2026年8月13日 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: 2026年8月13日 00:35:53 +0300 Subject: [PATCH 5/5] Wait for plugin scripts before calling window.onload main.js calls PluginManager.setup, which appends one > per plugin. Normally the browser's load event holds off until those have run, so every plugin is applied before SceneManager.run. The loader replaces index.html and calls window.onload() itself, which loses that guarantee, and SceneManager.initialize ended up running against a stock engine with none of the game's overrides applied. On OMORI that showed up as "FPSMeter is not defined". A plugin replaces Graphics._createFPSMeter and it had not run yet, so the stock version asked for a global the game never loads. The plugin scripts turned up about 30ms after the crash. Each plugin script is now tracked as it is queued, and awaited before boot. Failed loads resolve as well, so one broken plugin cannot stall startup, and a 60s race guard stops the boot hanging outright. js/libs/fpsmeter.js is added to the script list too. It ships with the game and belongs to the stock RPG Maker MV library set, but the loader never loaded it, so the vanilla path had nothing to fall back on. The five second isInTestMode sleep works around the same problem. Left it alone, since it also covers playtest asset loading. --- www/modloader/start_game.js | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/www/modloader/start_game.js b/www/modloader/start_game.js index e0ed8d3..177c115 100644 --- a/www/modloader/start_game.js +++ b/www/modloader/start_game.js @@ -5,6 +5,9 @@ async function _start_game() { "js/libs/pixi.js", "js/libs/pixi-tilemap.js", "js/libs/pixi-picture.js", + // Part of the stock RPG Maker MV library set and shipped with the game, but absent + // from this list. Graphics._createFPSMeter uses the FPSMeter global it defines. + "js/libs/fpsmeter.js", "js/libs/lz-string.js", "js/libs/iphone-inline-video.browser.js", "js/rpg_core.js", @@ -48,10 +51,35 @@ async function _start_game() { await p; } } + let pluginScriptLoads = []; + for (let script of scripts) { $oneLoaderGui.inc(); $oneLoaderGui.setHt("This may take up to 25 seconds", true); await rafResolve(); + + /* + * main.js calls PluginManager.setup, which appends one > per plugin. In an + * ordinary boot the browser's load event does not fire until those have executed, so + * every plugin is in place before SceneManager.run. Invoking window.onload() by hand + * throws that guarantee away: SceneManager.initialize then runs against a stock + * engine with none of the game's overrides applied. Record each plugin script as it + * is queued so the boot can wait for them below. + */ + if (script === "js/main.js" && window.PluginManager && PluginManager.loadScript) { + const _oneloader_loadScript = PluginManager.loadScript; + PluginManager.loadScript = function (name) { + _oneloader_loadScript.call(this, name); + const el = document.body.lastChild; + if (el && el.tagName === "SCRIPT") { + pluginScriptLoads.push(new Promise(resolve => { + el.addEventListener("load", resolve); + el.addEventListener("error", resolve); // a broken plugin must not stall the boot + })); + } + }; + } + let s = document.createElement("script"); s.src = script; document.body.appendChild(s); @@ -60,6 +88,16 @@ async function _start_game() { }) } + if (pluginScriptLoads.length> 0) { + $oneLoaderGui.setHt("Waiting for " + pluginScriptLoads.length + " plugin scripts"); + window._logLine("Waiting for " + pluginScriptLoads.length + " plugin scripts to execute"); + await Promise.race([ + Promise.all(pluginScriptLoads), + new Promise(resolve => setTimeout(resolve, 60000)) // never hang the boot outright + ]); + window._logLine("Plugin scripts settled"); + } + await $modLoader.$runScripts("pre_window_onload", {}); /*

AltStyle によって変換されたページ (->オリジナル) /