Skip to content
Open
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
18 changes: 14 additions & 4 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@
<meta name="viewport" content="user-scalable=no">
<title>OMORI</title>
<script>
if (window.nw.App.argv.length < 1) {
alert("OMORI must be launched directly from Steam.");
process.exit(0);
}
// Only the Steam build needs the launch argument: it carries the asset decryption
// key. Non-Steam builds ship plaintext assets and are launched without arguments,
// so gating them here would make the game unstartable.
(function () {
var fs = require('fs');
var path = require('path');
var base = path.dirname(process.mainModule.filename);
var isSteamBuild = fs.existsSync(path.join(base, "data", "System.KEL"));

if (isSteamBuild && window.nw.App.argv.length < 1) {
alert("OMORI must be launched directly from Steam.");
process.exit(0);
}
})();
</script>

<script src="modloader/lib/fast-json-patch.min.js"></script>
Expand Down
74 changes: 55 additions & 19 deletions www/modloader/_oneloader_configuration.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 = [
{
Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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" }
Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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
};
}
}
10 changes: 9 additions & 1 deletion www/modloader/early_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
7 changes: 5 additions & 2 deletions www/modloader/encryption.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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; }
Expand Down
14 changes: 9 additions & 5 deletions www/modloader/imagedelta_applier.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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(":")}`;
Expand Down Expand Up @@ -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] = {
Expand All @@ -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))
};
}
}
70 changes: 69 additions & 1 deletion www/modloader/start_game.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 <script> 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);
Expand All @@ -60,8 +88,43 @@ 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", {});

/*
* 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);
Expand All @@ -72,7 +135,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;
}