文章

这是一个能保护你 Ren'Py 游戏的东西,它叫 RenMori

我们试图填补 Ren'Py 资源保护工具的空白,用简单的方式保护最好的GalGame,这个东西就是 RenMori。本文主要讲述技术架构以及性能测试

这是一个能保护你 Ren'Py 游戏的东西,它叫 RenMori

RenMori:Ren'Py Resource Protector

我们试图填补了 Ren’Py 资源保护工具的空白,用简单的方式保护最好的GalGame,这个东西就是 RenMori。本文主要讲述技术架构以及性能测试,想直接使用?请直接前往:https://renmori.mewbaka.cn/integration/steam-release

RenMori 是一款面向 Ren’Py 8.x 视觉小说的全量数据保护工具,在发行版构建阶段将脚本、立绘、CG、音频、视频、字体、翻译与配置统一封入加密容器,仅在运行中的引擎内部解密,且对任何作品(包括商业作品)完全免费。

名称中的「Ren」指代 Ren’Py,「Mori」取自日文「守る」,意为守护,这一命名概括了项目的定位:在不改动游戏源码、不牺牲运行性能的前提下,为开发者的创作成果提供一道可验证、可度量的防线。

本文将依次介绍 Ren’Py 生态在资源保护方面的现状、RenMori 的设计原则与五层架构、实测性能数据、使用方式,以及项目对自身能力边界的明确界定。

制作背景:Ren’Py 生态中的保护空白

Ren’Py 是视觉小说领域使用最广泛的引擎之一,但其自有的发行格式并不具备保护的能力。rpa 封包本质上是一份索引加上首尾相接的文件,最多附带一个 XOR 混淆密钥;rpyc 则是经 zlib 压缩的 Ren’Py AST pickle 序列化结果。Ren’Py 官方曾明确表示现有的混淆只能阻挡用户随意浏览游戏源文件,并已将在 GitHub 上的资源加密请求以不可行为由关闭。

与此同时,目前公开的工具均站在了攻击方的一侧,基本都是面向大众的解包工具。例如 unrpa、unrpyc、rpatool 等。社区中常见的自保手段包括修改封包魔数、偏移文件头,或将载荷以 zlib、base64、hex 等编码层层包裹。而对 unrpyc 去混淆模块的分析表明,它会扫描 zlib 起始字节并校验,同时最多逐层尝试 10 层解码,因此上述做法都无法形成有效阻碍。

Ren'Py 生态中现有的工具与保护手段

截至目前,市面上尚不存在一款同时覆盖脚本、全部资源与引擎的 Ren’Py 保护工具,少数既有尝试仍停留在原型阶段或仅覆盖单一层面。我们也在调研阶段看见部分游戏社团不希望自己的游戏被破解或使用重软件壳来保护自己的游戏。我们注意到了这些需求,RenMori 正是为填补这一空白而设计。

RenMori 设计原则

RenMori 的全部技术决策围绕三项原则展开:

  1. 性能优先:任何与运行时性能冲突的保护手段都会被我们舍弃和削弱,绝不反向取舍。默认不使用 VMProtect、Themida 等重型加壳或虚拟化方案,热路径上不做反调试与自校验,只采用对称、硬件加速的 AES 算法。

  2. 无法批量破解:保护的核心承载者是逐构建多态,而非运行时设防,因此每次构建都会随机化容器格式参数、脚本编码映射与密钥派生路径,使通用解包器无从下手;这是一次构建期变换,不引入任何运行时开销,因而与性能优先原则天然契合。

  3. 诚实的威胁模型:游戏必然会在玩家的机器上运行,解密逻辑与密钥材料必然会随游戏一同下发。我们需要承认这就是密码学中的白盒问题,历史上没有任何本地 DRM 方案能规避它,对于视觉小说这种轻量游戏来说,联网验证 / 重型 DRM 是不值得的。

    RenMori 不承诺不可破解,其目标是使所有现成工具失效,并将提取成本抬高到针对单一游戏、单一版本的专家级投入。

技术架构

RenMori 由一套构建工具和一个原生运行时组成。Ren’Py 启动器的构建作为输入,程序输出保护后的发行版构建。全过程无需改动任何游戏源码。保护分布在五个层级上,每一层各自应对一类攻击面。

  1. 数据层:game/ 文件夹下的全部文件被封入自定义加密容器,完全取代 rpa 封包格式,每个文件拥有独立的密钥与完整性标签。运行时接管 renpy.loader 的文件打开事件与索引入口,立绘、CG、音频与视频均统一从此处读取。
  2. 脚本层:rpyc 的 pickle 载荷以独立的脚本密钥经 AES-GCM 加密,完整保护模式下进一步改用逐构建随机化的自定义 AST 编码,使依赖伪造 renpy 模块的反编译器失去稳定目标。
  3. 引擎层:发行版构建仅加载构建期登记的脚本,拒绝投放到游戏目录中的其他 rpy 与 rpyc 文件,并禁用开发者控制台和导出数据的 CLI 命令,从而封堵 un.rpyc 式注入。
  4. 原生启动器:分平台的原生启动器取代 Ren’Py 自带入口,在运行时参与密钥派生,硬盘上不留存任何明文密钥。若发行版中存在散装脚本或未知扩展,它将会拒绝启动。
  5. 逐构建多态:每次构建随机化容器布局、脚本编码与密钥派生路径,对一款游戏的完整逆向只能得到适用于该游戏、该脚本的工具。

在交付形态上,RenMori 采用开核拆分。CLI、配置解析、CI 集成、renpy.loader 钩子框架与基线容器格式规范全部源码可见。开发者可以自行审阅,确认其中不存在后门与数据回传。多态引擎、密钥派生混淆与原生运行时则以预编译插件交付,拆分的准则是任何公开的部分即使被完全理解,也不得使任何人写出对所有 RenMori 游戏通用的解包器。多态引擎仅在开发者机器或 CI 上运行,绝不下发给玩家,让最关键的变换空间始终不暴露在攻击者可及之处。闭源插件只参与构建期变换,玩家机器上的全部资源读取都经由 renpy.loader 钩子完成流式解密。

保护效果

受 RenMori 保护后的 game 目录

被 RenMori 保护的游戏 game/ 文件夹目录中只保留 renmori.rmc 与 renmori.rpe 运行时,不再残留任何明文封包、只保留加密容器、运行时与各平台原生核心。目前已验证的结果如下:

  • unrpa 无法识别容器格式,版本自动探测失败。

  • unrpyc(含 --try-harder 参数)与 rpycdec 无可反编译目标,植入的脚本均会被拒绝加载。

    unrpyc 与 rpycdec 的测试结果

  • 对容器进行了特征签名雕刻,未发现 png、oggs、riff、renpy 等任何可恢复的多字节魔数,容器与随机数据在统计上无从区分。

    容器特征签名雕刻结果

  • 同一款游戏保护两次,得到的容器 ID、逐文件密钥与容器字节均不相同,一个构建的密钥无法读取另一个构建。

性能验证

此处记录 RenMori 1.2.0 在一款真实 GalGame 发行版构建上的实测结果:以同一份构建的原版与 RenMori 完整保护版为对照,在同一台设备上测量顺序读取吞吐、随机读取延迟与冷启动延迟;受保护版通过原生 launcher 启动,与玩家实际运行的方式一致。

样本是一款真实作品的发行版构建,资源规模约 3.5 GB,包含 CG、立绘、UI 素材、BGM、语音与视频。使用的是已被授权的游戏,根据对方要求此处不公开该作品名称。

结论

对玩家而言,加密几乎无感。

  • 随机读取的额外延迟最大约 0.15 ms,不足 60 FPS 下单帧时间(16.7 ms)的 1%
  • 冷缓存下全部资源的顺序读取吞吐为 584 MB/s,是实时音视频解码所需带宽的百倍以上
  • 平均大小约 1.9 MB 的单张图像,冷缓存下读取耗时增加约 1.8 ms,且 Ren’Py 通常在后台预加载图像
  • 冷启动至首帧的耗时由 7.16 s 增加到 8.44 s,额外的 1.3 s 只在启动时发生一次,进入游戏后不再出现
  • 受保护版 game/ 目录体积仅增加 0.09%,其中包含三个平台的原生运行时

玩家能够感知的性能问题主要是卡顿与等待:卡顿取决于单次读取能否在一帧之内完成,等待取决于大块资源能否及时就绪。随机读取最多增加约 0.15 ms,不足单帧时间的 1%;顺序吞吐即使在冷缓存下也远高于音视频流式播放所需的带宽;游玩过程中加密不会引入可察觉的卡顿或等待。

唯一可以察觉的差异是启动时多出的约 1.3 s,它来自不写明文缓存的安全取舍,只在启动时发生一次。

性能测试结果(一)

性能测试结果(二)

性能测试结果(三)

测试环境

项目 取值
测试日期 2026-10-31
设备 Apple M1,8 GB 内存,内置 SSD
操作系统 macOS 26.5.1(arm64)
Ren’Py 8.5.2.26010301
RenMori 1.2.0,完整保护(poly),启用 lockdown,通过原生 launcher 启动

测试样本

原版为作品的发行版构建,game/ 目录约 3.5 GB,受保护版由同一份构建经 renmori protect 生成,全部资源位于加密容器 renmori.rmc 中。

测试覆盖两个构建中的全部对应条目,共 1407 个,合计 3593 MB,其中包含 21 个已编译脚本(.rpyc 与 .rpymc);这些脚本在受保护版中经过脚本层变换,大小与原版不同,顺序读取按引擎实际加载路径将其一并计入。

类别 文件数 合计
图像 774 1477 MB
音频 599 540 MB
视频 2 1497 MB
脚本 21 0.3 MB
其他 11 79 MB
合计 1407 3593 MB

测试方法

全部测量均在引擎进程内进行,并通过引擎自身的资源加载路径 renpy.loader.load 读取,因此受保护版的数据包含解密、认证与容器索引查找的全部开销;两个构建均直接运行 .app 中的可执行文件,受保护版因此经过 launcher 的启动流程。

冷缓存与热缓存

每次冷缓存测量前,先以 APFS 克隆(cp -c)复制一份全新的构建;克隆与原文件共享磁盘数据,但在操作系统看来是新的文件,页缓存中不存在其任何内容,因此首次读取必然访问磁盘。预先验证显示,克隆后首轮随机 4 KB 读取的中位延迟为 104 µs,第二轮为 1.4 µs,证实首轮确为冷缓存。

热缓存测量在同一份克隆上重复读取,此时数据已位于页缓存中,结果只反映 CPU 开销。

测量项目

项目 方法
顺序读取 以 1 MB 为单位完整读取全部 1407 个条目;第 1 轮为冷缓存,第 2 至 4 轮为热缓存,取中位数;共进行 3 次,每次使用全新克隆
随机 4 KB / 64 KB 在最大的 64 个文件(均不小于 4 MB,合计 2214 MB)中随机选择文件与偏移,seek 后读取 4 KB 或 64 KB,各采样 2000 次
随机打开小文件 在 822 个不大于 256 KB 的文件中随机选择,打开并完整读取,采样 1000 次
冷启动 从运行可执行文件开始,到引擎绘制第一帧为止;冷启动每次使用全新克隆,共 5 次;热启动在同一份克隆上预热 1 次后测量 5 次

随机读取的冷、热两轮使用相同的随机序列,3 次测量的全部样本合并后计算中位数与 p95。

顺序读取测试结果

单位为 MB/s,数值越大越好。

类别 原版(冷) 受保护版(冷) 原版(热) 受保护版(热)
图像 1154 555 1302 480
音频 1132 430 1022 480
视频 1895 658 2281 557
脚本 118 85 96 75
其他 1644 568 1621 458
全部 1303 584 1547 419

受保护版的冷缓存吞吐约为原版的 45%,热缓存约为 27%;热缓存下已不存在磁盘等待,受保护版约 420 至 560 MB/s 的吞吐即为当前解密的上限。

按冷缓存吞吐换算,读取一张平均大小的图像(约 1.9 MB),原版约需 1.6 ms,受保护版约需 3.4 ms;视频与 BGM 由引擎流式读取,其码率通常仅为每秒数 MB,与 584 MB/s 相差两个数量级以上。

随机读取测试结果

单位为 ms,数值越小越好。

测试 缓存 原版中位数 受保护版中位数 原版 p95 受保护版 p95
随机 4 KB 冷 0.216 0.181 0.443 0.399
随机 4 KB 热 0.200 0.187 0.479 0.456
随机 64 KB 冷 0.252 0.403 0.452 0.760
随机 64 KB 热 0.249 0.348 0.397 0.699
随机打开小文件 冷 0.196 0.265 0.937 0.988
随机打开小文件 热 0.100 0.143 0.438 0.675

随机 4 KB 读取两者相当,差异在测量误差范围内;随机 64 KB 读取的中位数增加 0.10 至 0.15 ms,原因是一次 64 KB 读取通常跨越两个分块,需要解密并认证两个分块;随机打开小文件的中位数增加 0.04 至 0.07 ms,来自容器索引查找与逐文件密钥派生。

冷启动测试结果

单位为 s,从运行可执行文件开始计时,取中位数,括号内为最小值与最大值。

阶段 缓存 原版 受保护版 差值
资源索引就绪 冷 3.13(2.46–3.82) 3.18(2.80–3.74) +0.05
绘制首帧 冷 7.16(6.01–7.70) 8.44(7.99–9.04) +1.28
资源索引就绪 热 0.97(0.72–1.07) 0.76(0.65–1.33) −0.21
绘制首帧 热 5.05(3.82–6.50) 5.83(4.61–6.54) +0.78

launcher 的启动流程与容器索引的建立几乎不增加耗时,冷缓存下资源索引就绪的时间仅相差 0.05 s;热启动各次之间波动较大,两者的差异以冷启动数据为准。

额外的耗时集中在脚本加载阶段:为避免在磁盘上留下明文,受保护版不写入 Ren’Py 的字节码、界面分析等运行时缓存,每次启动都需重新编译脚本中的 Python 代码;单次运行的 Ren’Py 日志中,「Loading script」阶段原版为 169 ms,受保护版为 1967 ms。

体积开销测试结果

构建 game/ 体积
原版 3,768,321,012 字节
受保护版 3,771,724,069 字节
差值 +3,403,057 字节(+0.09%)

已知测试局限

  • 未测量实际游玩中的帧时间,本页数据只覆盖资源读取,不包括图像解码、音频解码与渲染
  • 仅在一台 Apple M1 设备上测试,Windows、Linux 与 Steam Deck 上的数据尚未测量
  • 测试设备使用内置 SSD,机械硬盘上冷缓存读取受磁盘限制更明显,受保护版与原版之间的相对差距通常会缩小
  • 热启动数据波动较大,仅供参考

平台兼容性

RenMori 已在 Windows(系统 CNG,硬件 AES)、macOS 与 Linux(Ren’Py 自带的 OpenSSL)上完成引擎内验证;Steam 成就与云存档经由 Ren’Py 的 Python API 实现且未受触动,加密容器为静态文件,Steam 的文件校验与增量更新均照常工作;macOS 构建会自动完成 Apple Silicon 所需的临时(ad-hoc)签名,并保留以 Developer ID 重新签名与公证的能力。

如何使用

RenMori 以 Python 包形式发布于 PyPI,开发者机器需具备 Python 3.9 或更高版本,完整流程仅包含四个步骤:

  1. 安装——执行 pip install renmori,并以 renmori --version 确认安装成功
  2. 构建游戏——在 Ren’Py 启动器中执行 Build Distributions,RenMori 只处理构建产物,不触及项目源码
  3. 保护构建——执行 renmori protect ./dist/MyGame-1.0-pc,输入也可以是启动器产出的 .zip;首次运行时会从公开 Release 下载对应平台的插件,完成校验后缓存
  4. 校验与发布——执行 renmori verify 校验结果,随后将受保护的目录作为发行版上传,切勿再对其执行 Ren’Py 启动器构建

关于具体如何打包并上传到 Steam,请查看我们的官方文档。

使用 RenMori 保护构建

校验保护结果

在最后

对于独立开发者以及商业社团而言,现实中绝大多数的脚本泄露以及盗用素材事件均可避免,无非就是他人运行了某个主流的、现成的工具,RenMori 正是以最低的接入成本以及接近于零的运行时代价,将这一广泛存在的风险降到最低,让社团不再纠结于反盗版的问题。

RenMori 采用 RenMori License 1.0 授权:保护任何 Ren’Py 作品(无论是否商业)均免费,运行时可随发行版免费再分发。欢迎通过以下渠道了解更多:

本文由作者按照 CC BY 4.0 进行授权