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/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); } 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; } 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 diff --git a/www/modloader/start_game.js b/www/modloader/start_game.js index a92662d..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