RPG Maker MV / NW.js 游戏的外置修改器框架 —— 桥接器架构 · 自动检测 · 单文件 exe
面向本地单人游戏的存档/状态修改工具。本仓库只包含工具代码,不含任何游戏本体、素材或存档。
| 项 | 内容 |
|---|---|
| 游戏 | 《苏沫:爸妈不在家》v1.0.11 |
| 引擎 | RPG Maker MV + NW.js(Game.exe + www/) |
| 系统插件 | 作者自写的 8BOX_* 系列:8BOX_bondage.js(拘束)、8BOX_endurance.js(耐力)、8BOX_quickButton.js(界面) |
| 数据结构 | $gameSystem.BOX_data_BD(穿着/钥匙/力量)、$gameSystem.BOX_data_EP(耐力) |
| 规模 | 135 件拘束防具、18 把钥匙武器、4 种解除方式(解缚/挣脱/剪开/强制) |
| 定制 API | BOX_Force / BOX_putOnEquip / BOX_takeOff / BOX_changeEquip / BOX_canRelive / BOX_takeOffAll / BOX_DeclineEP |
通用部分(约 80% 代码):进程检测、磁盘扫描、HTTP 服务、同源界面、自动安装/卸载桥接器、独立窗口、心跳退出 —— 与游戏无关,任何 MV 游戏都能直接复用。
游戏相关部分(约 20% 代码):特征插件名、界面里发给 /eval 的业务表达式、快捷变量 ID。见下一节。
RPG Maker MV 的定制系统(拘束、状态、任务、好感度……)全部是明文 JavaScript 插件,逻辑就在 www/js/plugins/*.js 里。但这类游戏用 NW.js 打包后:
--remote-debugging-port=9222 → 不监听
--inspect=9229 → 不监听
package.json chromium-args → 被忽略
游戏进程的监听端口 → 一个都没有
也就是说 CDP / 远程调试 / 内存外挂都走不通。本项目用另一种结构解决:
┌──────────────────────┐ HTTP ┌────────────────────────────┐
│ 外置修改器 (exe) │ ────────▶ │ 游戏内桥接器 (30 行, 无界面) │
│ 127.0.0.1:8799 │ │ 127.0.0.1:8790 │
│ · 内置 HTTP 服务 │ │ · GET /status │
│ · 界面同源,无 CORS │ ◀──────── │ · GET /eval?js=<表达式> │
│ · 自动检测/自动安装 │ │ · require('http') │
└──────────────────────┘ └────────────────────────────┘
- 修改器本体完全外置:独立进程、独立窗口、关窗即退出
- 游戏内只有一个不可见文件:不渲染任何 UI,只开一个本机端口转发
eval - 自动检测游戏状态:枚举进程 → 取 exe 路径 → 确认是 MV 工程;找不到再扫盘
- 自动安装桥接器:首次运行自动写入
www/js/plugins/,并备份plugins.js
| 模块 | 能力 |
|---|---|
| 数值 | 金钱 / 耐力 / 全部命名变量 / 全部开关,读取与写入 |
| 道具 | 物品 · 武器 · 防具,按名称或 ID 搜索,批量增减 |
| 系统 | 当前装备列表(含锁定状态)、四种解除方式实时判定、批量解除、穿戴并锁定、钥匙取回 |
「四种解除方式」是按目标游戏插件的实现映射的:解缚 / 挣脱 / 剪开 / 强制,对应
canRelive(mode) 返回的可解列表,界面实时显示每种方式的可用件数。
- 下载 Release 里的
SumuTrainer.exe(约 7.5 MB,单文件免安装) - 双击运行 —— 弹出独立窗口(Edge/Chrome 的
--app模式,无地址栏) - 打开游戏;程序每 2 秒检测一次,连上后绿点亮起
- 关窗即退出(心跳 20 秒无响应自动结束进程)
换电脑也不用配置:程序自己找游戏、自己装桥接器,装完提示重启游戏。
- 同源 —— 界面和 API 都由程序自己的
127.0.0.1:8799提供,页面只写相对路径/api/...。这避免了file://页面直连本地端口时被 CORS/权限拦掉(换台机器就"检测不到进程"的经典原因)。 - 服务端转发 ——
/api/eval由 Python 侧转发到桥接器,转发时用ProxyHandler({})明确禁用系统代理(否则127.0.0.1可能被本地代理劫持)。 - 检测三级 —— 运行中的
Game.exe→QueryFullProcessImageNameW取路径 → 同级存在www/js/plugins.js;失败再扫盘(深度 2,按特征插件名匹配)。 - 幂等安装 —— 写桥接器前先查
plugins.js是否已注册;备份只在首次创建。 - 失败可诊断 —— 未连接时界面展开四行状态:游戏进程 / 游戏目录 / 桥接器文件 / 注册项,一眼分清"没找到游戏"还是"桥接器没装"。
# 1) 构建环境(uv 可选,普通 venv 也行)
uv venv build_venv --python 3.12
uv pip install --python build_venv\Scripts\python.exe pyinstaller
# 2) 打包
build_venv\Scripts\pyinstaller.exe --noconfirm --onefile --noconsole `
--name SumuTrainer --distpath dist src\sumu_trainer_app.py体积约 7.5 MB(Python 3.12 + PyInstaller 6.x)。
调试时可设环境变量 SUMU_NO_WINDOW=1,只起服务不开窗口,方便用 curl 验证接口。
GET /api/status → {running, exe, game_dir, bridge_installed,
bridge_registered, connected, ready, scene}
GET /api/eval?js=<expr> → {ok, value} | {ok:false, error}
GET /api/scan → 扫描磁盘找游戏目录
GET /api/install → 安装/修复桥接器
GET /api/uninstall → 还原 plugins.js 并删除桥接器
GET /api/launch → 启动游戏
GET /api/quit → 退出程序
桥接器侧:
GET /status → {ok, ready, scene, bridge}
GET /eval?js=<expr> → {ok, value} | {ok:false, error}
/api/uninstall 或手动:
www/js/plugins.js.bak_trainer覆盖回www/js/plugins.js- 删除
www/js/plugins/<PREFIX>_Bridge.js
桥接器、HTTP 服务、窗口、检测逻辑都不用动,只有 3 处要改,全部在 src/sumu_trainer_app.py 里:
# ① 扫盘时用来确认"这是目标游戏"的特征插件(最稳的识别物,比游戏名可靠)
def find_game_by_disk(...):
def check(d):
return os.path.isfile(os.path.join(d, "www", "js", "plugins", "8BOX_bondage.js"))
# ^ 换成目标游戏的定制插件文件名
# ② 进程白名单(一般不用动,MV 打包基本都是 Game.exe;NW.js 原生是 nw.exe)
if low not in ("game.exe", "nw.exe", "苏沫.exe"):若目标游戏没有明显的定制插件,可改成检查
www/js/plugins.js里是否出现某个作者前缀(如PKD_、YEP_)。
APP_TITLE = "苏沫:爸妈不在家 · 修改器" # 窗口标题
BRIDGE_NAME = "SUMU_Bridge" # 插件文件名 / plugins.js 注册名(换游戏就换前缀,避免串味)
BRIDGE_PORT = 8790 # 本机端口,冲突时换(前端不用改,走 /api 转发)这是唯一需要读懂目标游戏插件的地方。当前实现对应的映射:
| 界面上做的事 | 现在的表达式 | 通用 MV 写法 |
|---|---|---|
| 读金钱 / 改金钱 | $gameParty.gold() / $gameParty.gainGold(n) |
MV 原生,任何游戏都不用改 |
| 读变量 / 写变量 | $gameVariables.value(id) / .setValue(id, v) |
原生,不用改 |
| 开关 | $gameSwitches.value(id) / .setValue(id, v) |
原生,不用改 |
| 刷道具 | $gameParty.gainItem($dataItems[i], n) / loseItem |
原生,不用改(武器/防具同理换 $dataWeapons / $dataArmors) |
| 耐力 | $gameSystem.BOX_data_EP.now / .max |
换成目标游戏的对应数值(没这个系统就删掉这一行) |
| 力量 | $gameSystem.BOX_data_BD.selfPower / .otherPower |
同上 |
| 穿着列表 | $gameSystem.BOX_data_BD.playerEquip |
换成目标系统的装备/状态容器 |
| 穿戴 / 上锁 / 开锁 | BOX_Force(id) / BOX_changeEquip(1,id) / (2,id) |
换成目标插件的公开函数 |
| 四种解除 | BOX_canRelive(mode) → BOX_takeOff(id) |
同上;mode 是目标插件的枚举 |
| 批量脱缚 | BOX_takeOffAll(mode) |
同上 |
| 快捷变量按钮 | [[15,'服从度'],[16,'欠债'],[13,'任务等级'],…] |
换成目标游戏 $dataSystem.variables 里真实存在的 ID |
找这些函数的办法:
grep -nE "prototype\.[A-Za-z_]+ *=|^function [A-Za-z_]+" www/js/plugins/<定制插件>.js, 读插件头的@plugindesc / @help,再从游戏事件调用的地方反查参数。
SUMU_NO_WINDOW=1 python src/sumu_trainer_app.py # 只起服务
curl "http://127.0.0.1:8799/api/status" # running/game_dir/bridge_installed/connected 四项都要 true
curl "http://127.0.0.1:8799/api/eval?js=%24gameParty.gold%28%29" # 读一个原生值
curl "http://127.0.0.1:8790/eval?js=<你的系统函数调用>" # 直连桥接器,确认真调到了四项全绿再动界面;UI 改动只影响显示,不影响通路。
- 仅限本地单人游戏的离线修改,不涉及联机、不绕过付费、不分发游戏本体
- 不含任何游戏素材;截图仅为工具界面
- 使用造成的存档变化由使用者自行负责,建议先备份存档
MIT
