Try   HackMD

[macOS]Use Electron to deploy output RPG Maker MV/MZ games

This article was translated using "DeepL Translator".

This article will teach you to deploy your RPG Maker MV/MZ games using Electron.
This tutorial will use the Intel-based macOS 10.15.7 (Catalina) operating system.

Electron does not currently support OS X 10.10 (Yosemite) and older versions of operating systems.

Update Info

2022/05/03 Update part of the article.
2022/05/01 Fix some contents of package.json.
2022/01/04 Add block the feature of capturing screen.
2021/11/12 Added encrypted content about asar files.

1.First, download Node.js for macOS from the following URL and install it on your computer.

https://nodejs.org/en/download/

2.After the installation is complete, for "Spotlight search" the upper right corner,and Run "Terminal", then type node -v and npm -v respectively to check if the installation is successful.

Then, because the "Xcode Command Lines Tool" is needed to deploy the game on macOS, type xcode-select --install in the "Terminal" to download and install this tool.

3.After creating a folder, and add a new file named "package.json" and edit the file to add the following syntax content.

After adding the following syntax, remember to remove // and the comments that follow.

{ "name": "RPG_Game", //The project name used for the output. //It must be alphanumeric and not contain any half-space. "version": "1.0.0", //Version number. //The format must be in the numeric format X.X.X. "description": "This is the Electron Deployment Test.", //File description. "main": "index.js", //[RPG Maker MZ] "chromium-args": "--force-color-profile=srgb", //[RPG Maker MV] "js-flags": "--expose-gc", "build": { "appId": "com.rpgmaker.game", //Application ID. //*You can usually enter com.xxxxx.yyyyy in alphanumeric format. //xxxxx is the English name of your game title. //yyyyy is the author's/team's name. "productName": "Game", "asar": true, //It is recommended to set this item to true if you need to seal the game content... "afterPack": "./myAfterPackHook.js", // * If you need to use the "asarmor" encryption suite, please make sure to include this parameter. "mac": { "category": "public.app-category.role-playing-games", "icon": "icon/icon.icns", //Game icon, the image format is icns format. //* Use the png to icns online conversion site and place the icns format image in the icon folder. "target": { "target": "dir", "arch": "universal" //When "universal" is specified, it is compatible with both x64 (Intel) and arm64 (Apple Silicon). } } }, "scripts": { "start": "electron .", "pack": "electron-builder --dir", "dist": "electron-builder" }, "author": "Mirai", //The author's/team's name. "copyright": "Copyright © 2021 ${author} All rights reserved.", //Copyright Notice. "devDependencies": { "electron": "^11.5.0", "electron-builder": "^22.5.1", "asarmor": "^2.0.0" // * If you need to use the "asarmor" encryption suite... } }

If you need to use the "asarmor" encryption suite, create a file called "myAfterPackHook.js" and put the following code into this file and save it in this output folder.

const asarmor = require('asarmor'); const { join } = require("path"); exports.default = async ({ appOutDir, packager }) => { try { const asarPath = join(packager.getResourcesDir(appOutDir), 'app.asar'); console.log(`applying asarmor patches to ${asarPath}`); const archive = await asarmor.open(asarPath); archive.patch(); // apply default patches await archive.write(asarPath); } catch (err) { console.error(err); } };

[Demo]

Then use the cd command on the "Terminal" to specify the path to the folder, and enter the npm install command on the "Terminal" to install the required package.

Remember that folder names can only be alphanumeric and do not contain a half-line margin. If you need to use a half-line margin, please replace it with a ( _ ).

4.Open the RPG Maker MV/MZ game project and click "File" → "Deployment", select "Web Browsers / Android / iOS".
When using RPG Maker MV, select "Web Browsers" and then click "OK" to wait for the deployment to complete.

[RPG Maker MZ]

[RPG Maker MV]

5.Create an "index.js" file ,and enter the following:

const { app, BrowserWindow, ipcMain, shell } = require('electron'); function createWindow() { const win = new BrowserWindow({ width: 816, //Screen Width height: 624, //Screen Height icon: 'icon/icon.png', useContentSize: true, autoHideMenuBar: true, backgroundColor: '#000000', webPreferences: { nodeIntegration: true, contextIsolation: false } }) const gametitleString = "Game Test"; //Game Title const isTest = false; //Whether it is a test mode. const testString = isTest ? '?test' : ''; app.allowRendererProcessReuse = false; win.loadURL(`file://${__dirname}/index.html${testString}`); if (process.platform !== 'darwin') { win.setMenu(null); } else { var { Menu } = require('electron'); var menu = Menu.buildFromTemplate([ { label: 'Electron', submenu: [ { label: `About ${gametitleString}`, selector: 'orderFrontStandardAboutPanel:' }, {type: 'separator'}, { label: `Hide ${gametitleString}`, accelerator: 'Command+H', selector: 'hide:' }, { label: 'Hide Others', accelerator: 'Command+Alt+H', selector: 'hideOtherApplications:' }, { label: 'Show All', selector: 'unhideAllApplications:' }, { type: 'separator' }, { label: `Quit ${gametitleString}`, accelerator: 'Command+Q', click: function() { app.quit(); } } ] } ]); Menu.setApplicationMenu(menu); } require('electron').ipcMain.on('focusMainWindow', function (e) { win.focus(); }); require('electron').ipcMain.on('openDevTools', function (e) { win.webContents.openDevTools(); }); require('electron').ipcMain.on('openExternal', function (e, arg) { shell.openExternal(arg); }); //* Required with NekoGakuen_BlockedCapture.js plugin. require('electron').ipcMain.on('setProtection', function (e, arg) { win.setContentProtection(arg); }); }; app.whenReady().then(createWindow); app.on('window-all-closed', () => { app.quit(); }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } });

6.Go to the output folder ,and copy all the files except “package.json” and paste them into the output folder for this of Electron.

[RPG Maker MZ]

[RPG Maker MV]

7.Next, we will divide it into two projects and modify some of the contents in the js folder.

[RPG Maker MZ]

Modify "main.js", "rmmz_core.js" and "rmmz_managers.js" to the following.

main.js

isPathRandomized() { // [Note] We cannot save the game properly when Gatekeeper Path // Randomization is in effect. return ( Utils.isNwjs() && process.mainModule.filename.startsWith("/private/var") ); }

Replace..

isPathRandomized() { // [Note] We cannot save the game properly when Gatekeeper Path // Randomization is in effect. if (Utils.isElectronjs()) { return (__filename.startsWith("/private/var")); } else { return (Utils.isNwjs() && process.mainModule.filename.startsWith("/private/var")); } }

rmmz_core.js

Utils.isNwjs = function() { return typeof require === "function" && typeof process === "object"; };

Replace..

Utils.isNwjs = function() { return typeof require === "function" && typeof process === "object"; }; Utils.isElectronjs = function () { return window && window.process && window.process.versions && window.process.versions['electron']; };

Utils.isOptionValid = function(name) { const args = location.search.slice(1); if (args.split("&").includes(name)) { return true; } if (this.isNwjs() && nw.App.argv.length > 0) { return nw.App.argv[0].split("&").includes(name); } return false; };

Replace..

Utils.isOptionValid = function (name) { const args = location.search.slice(1); if (args.split("&").includes(name)) { return true; } if (this.isElectronjs()) { if (process.argv.length > 0) { return process.argv[0].split("&").includes(name); } } else { if (this.isNwjs() && nw.App.argv.length > 0) { return nw.App.argv[0].split("&").includes(name); } } return false; };

rmmz_managers.js

StorageManager.fileDirectoryPath = function() { const path = require("path"); const base = path.dirname(process.mainModule.filename); return path.join(base, "save/"); };

Replace..

StorageManager.fileDirectoryPath = function () { const path = require("path"); if (Utils.isElectronjs()) { const base = path.dirname(__filename); return Utils.isOptionValid('test') ? path.join(base, "save/") : path.join(base, '../../save/'); } else { const base = path.dirname(process.mainModule.filename); return path.join(base, "save/"); } };

SceneManager.reloadGame = function () { if (Utils.isNwjs()) { chrome.runtime.reload(); } }; SceneManager.showDevTools = function () { if (Utils.isNwjs() && Utils.isOptionValid("test")) { nw.Window.get().showDevTools(); } };

Replace..

SceneManager.reloadGame = function () { if (Utils.isElectronjs()) { location.reload(); } else { if (Utils.isNwjs()) { chrome.runtime.reload(); } } }; SceneManager.showDevTools = function () { if (Utils.isElectronjs()) { if (Utils.isOptionValid("test")) { require('electron').ipcRenderer.send('openDevTools'); } } else { if (Utils.isNwjs() && Utils.isOptionValid("test")) { nw.Window.get().showDevTools(); } } };

[RPG Maker MV]

Modify “rpg_core.js” and “rpg_managers.js” to the following.

rpg_core.js

Utils.isNwjs = function() { return typeof require === 'function' && typeof process === 'object'; };

Replace..

Utils.isNwjs = function() { return typeof require === 'function' && typeof process === 'object'; }; Utils.isElectronjs = function() { return window && window.process && window.process.versions && window.process.versions['electron']; };

Utils.isOptionValid = function (name) { if (location.search.slice(1).split('&').contains(name)) { return 1; }; if (typeof nw !== "undefined" && nw.App.argv.length > 0 && nw.App.argv[0].split('&').contains(name)) { return 1; }; return 0; };

Replace..

Utils.isOptionValid = function (name) { if (location.search.slice(1).split('&').contains(name)) { return 1; }; if (this.isElectronjs()) { if (this.isNwjs() && process.argv.length > 0 && process.argv[0].split('&').contains(name)) { return 1; }; } else { if (typeof nw !== "undefined" && nw.App.argv.length > 0 && nw.App.argv[0].split('&').contains(name)) { return 1; }; } return 0; };

Input._wrapNwjsAlert = function() { if (Utils.isNwjs()) { var _alert = window.alert; window.alert = function() { var gui = require('nw.gui'); var win = gui.Window.get(); _alert.apply(this, arguments); win.focus(); Input.clear(); }; } };

Replace..

Input._wrapNwjsAlert = function () { if (Utils.isElectronjs()) { var _alert = window.alert; window.alert = function () { _alert.apply(this, arguments); require('electron').ipcRenderer.send('focusMainWindow'); Input.clear(); }; } else { if (Utils.isNwjs()) { var _alert = window.alert; window.alert = function () { var gui = require('nw.gui'); var win = gui.Window.get(); _alert.apply(this, arguments); win.focus(); Input.clear(); }; } } };

rpg_managers.js

StorageManager.localFileDirectoryPath = function() { var path = require('path'); var base = path.dirname(process.mainModule.filename); return path.join(base, 'save/'); };

Replace..

StorageManager.localFileDirectoryPath = function () { var path = require('path'); if (Utils.isElectronjs()) { var base = path.dirname(__filename); return Utils.isOptionValid('test') ? path.join(base, 'save/') : path.join(base, '../../save/'); } else { var base = path.dirname(process.mainModule.filename); return path.join(base, 'save/'); } };

SceneManager.initialize = function() { this.initGraphics(); this.checkFileAccess(); this.initAudio(); this.initInput(); this.initNwjs(); this.checkPluginErrors(); this.setupErrorHandlers(); };

Replace..

SceneManager.initialize = function () { this.initGraphics(); this.checkFileAccess(); this.initAudio(); this.initInput(); if (!Utils.isElectronjs()) { this.initNwjs(); } this.checkPluginErrors(); this.setupErrorHandlers(); };

SceneManager.onKeyDown = function (event) { if (!event.ctrlKey && !event.altKey) { switch (event.keyCode) { case 116: // F5 if (Utils.isNwjs()) { location.reload(); } break; case 119: // F8 if (Utils.isNwjs() && Utils.isOptionValid('test')) { require('nw.gui').Window.get().showDevTools(); } break; } } };

Replace..

SceneManager.onKeyDown = function (event) { if (!event.ctrlKey && !event.altKey) { switch (event.keyCode) { case 116: // F5 if (Utils.isElectronjs()) { location.reload(); } else { if (Utils.isNwjs()) { location.reload(); } } break; case 119: // F8 if (Utils.isElectronjs()) { if (Utils.isOptionValid('test')) { require('electron').ipcRenderer.send('openDevTools'); } } else { if (Utils.isNwjs() && Utils.isOptionValid('test')) { require('nw.gui').Window.get().showDevTools(); } } break; } } };

If the above is very complicated, you can also download quick start template directly from the following GitLab.
https://gitlab.com/MiraiSoSad/rpg_maker_mv_and_mz_electron/-/tree/en_US

8.Go back to "Terminal" and type npm start to check if the game can run normally. If the game runs OK, type npm run dist to run the deployment output.

9.Finally, after the deployment is finished, you can find the contents of the deployment in the “dist/mac-universal/” folder and run Game.app.

If the error message "Please move the Game.app to a different folder." pops up on the game screen when you first run the game, you can create a folder and move Gamp.app to this folder and run the game again.

Reference Material

https://www.electronjs.org

https://github.com/electron/electron-quick-start

https://qiita.com/RaTTiE/items/63f2e351a93f81bc8039

https://a091234765.pixnet.net/blog/post/402450719-[electron學習筆記]electron安裝檔打包攻略補充_el

https://www.electron.build

https://www.npmjs.com/package/asarmor

Mirai - Patreon:
https://www.patreon.com/MiraiDiary

Mirai - Twitter(X):
https://twitter.com/Mirai_so_Sad

Mirai - itch.io
https://miraisosad.itch.io

Mirai - Instagram
https://www.instagram.com/miraisosad/

tags: 貓咪學園 NekoGakuen macOS RPG Maker RPG Maker MV RPG Maker MZ