For AI agents: the complete documentation index is available at https://renmori.mewbaka.cn/llms.txt, the full documentation bundle is available at https://renmori.mewbaka.cn/llms-full.txt, and this page is available as Markdown at https://renmori.mewbaka.cn/technical/loader-hook.md.
  • 简体中文
  • 加载器钩子

    本页内容来自对 Ren'Py 8.5.2 源码(renpy/loader.py、renpy/main.py、renpy/script.py、renpy/pygame/rwobject.pyx)的阅读,以及对 SDK 所附运行时的检查。对于所支持的每一个 Ren'Py 版本,都需要重新核验以下结论。

    1. 所有资源都经由同一个入口

    renpy.loader.load(name) → load_core(name) → 依次尝试 renpy.loader.file_open_callbacks 中的每个函数,并返回第一个非 None 的结果。默认顺序为:config.file_open_callback、文件系统、封包(RPA)、APK、远程。

    调用方审计结果:

    资源调用点
    图像loader.load() → pygame.image.load(f, filename)(display/pgrender.py)
    音频与视频renpy.audio.audio.load() → loader.load(fn, directory="audio");影片经由音频通道播放
    字体loader.load(fn, directory="fonts")(text/font.py)
    脚本以 loader.load(fn, tl=False) 读取 .rpyc(script.py),无论该文件来自磁盘还是索引

    唯一的绕过点: renpy.exists() 使用 transfn(),并且在文档中明确说明仅检查磁盘。若游戏对一个已被移入容器的文件调用 renpy.exists(),将得到 False。钩子会对此进行修补。

    RenMori 将其回调插入 file_open_callbacks 的索引 0 处,使容器优先于磁盘上的任何内容。

    2. 文件发现:scandirfiles_callbacks

    listdirfiles()(用于脚本发现、renpy.list_files()、自动图像定义以及填充 loadable_cache)通过以 add(dn, fn, files, seen) 的形式调用 renpy.loader.scandirfiles_callbacks 中的每个函数来构建。RenMori 注册了一个函数,以 dn=None 添加每一个容器条目(与封包条目的添加方式相同)。这使容器内的文件在 loadable() 中返回真,并使脚本发现能够找到仅存在于容器内的 .rpyc 文件。

    3. 时序:钩子必须在 index_files() 之前安装

    Ren'Py 脚本在启动时、任何游戏 .rpy 代码运行之前就已被发现,因此普通的 init python 块为时已晚。Ren'Py 8.5 提供了官方的早期钩子:Ren'Py 扩展(Ren'Py Extensions)。renpy.main.main() 会按排序顺序,从 Ren'Py 基目录、game/ 与 game/libs 中加载每一个 *.rpe(包含 autorun.py 的 zip 文件,会被加入 sys.path)与 *.rpe.py,加载时机早于 renpy.loader.index_files(),也早于脚本加载。

    RenMori 将其运行时以 game/renmori.rpe 的形式随发行版构建分发。(计划在后续版本中迁入原生启动器;.rpe 同时也是攻击者的注入点,必须锁定为仅允许本项目签名的扩展。)

    4. 返回类型至关重要:RWopsIO 与重新打开陷阱

    Ren'Py 8.5 通过 rwobject.pyx 中的 to_rwops() 将文件对象交给 C 代码(SDL_image、FreeType、ffmpeg)。其处理逻辑依次为:

    1. 反复解开 .raw
    2. 若为 RWopsIOImpl,直接使用其 C 层 SDL_RWops(速度快,不持有 GIL)
    3. 若为带字符串 .name 的 io.IOBase,则按该名称从磁盘重新打开(若存在 .base/.length 则一并使用)
    4. 否则,对任何带 read 方法的对象,以 Python 回调进行包装,每次读取都需要获取 GIL

    对 RenMori 的影响:

    • 陷阱:继承 io.IOBase 并暴露字符串 .name 的解密文件对象会被从磁盘重新打开,结果要么找不到文件,要么把原始密文交给 SDL;因此流对象既不能暴露 .name,也不能暴露 .raw
    • 中小文件:完整解密后返回 RWopsIO.from_buffer(plaintext, name=name),这是纯 C 层的缓冲区 RWops,此后不再获取 GIL,是 Ren'Py 中最快的路径
    • 大文件(视频、长时间的背景音乐):返回 RWopsIO(stream, name=name),其中 stream 按需解密分块;该路径经由 Python 回调,每次读取都要获取一次 GIL,必须进行基准测试,生产环境的方案是闭源运行时中的原生 nogil 解密 SDL_RWops

    5. 在 Ren'Py 自带的 Python 中实现 AES:无需额外依赖

    Ren'Py 自带的 Python 不包含 cryptography 或 pycryptodome,但附带了 ctypes(三个运行时中均存在 PyInit__ctypes),并且:

    运行时AES-256-GCM 来源
    macOS(librenpython.dylib,通用二进制)静态链接并导出的 OpenSSL(EVP_aes_256_gcm、EVP_CIPHER_CTX_new、EVP_DecryptUpdate 等)
    Linux(librenpython.so)同上,已导出
    Windows(librenpython.dll、libpython3.12.dll)EVP 符号未导出;改用操作系统的 CNG(bcrypt.dll),自 Vista 起的所有 Windows 版本均提供硬件加速的 AES-GCM

    因此,运行时读取器通过 ctypes 调用 OpenSSL(macOS/Linux)或 CNG(Windows)。解密在原生代码中借助 AES-NI / ARMv8 加密扩展执行,只有每个分块的调用开销发生在 Python 中。构建工具使用 cryptography 包。所有后端产出的都是标准 AES-256-GCM,因此容器格式不依赖于具体后端。

    © 2026 笨猫工作室 MewBaka Studio. All Rights Reserved.