For AI agents: the complete documentation index is available at https://renmori.mewbaka.cn/en/llms.txt, the full documentation bundle is available at https://renmori.mewbaka.cn/en/llms-full.txt, and this page is available as Markdown at https://renmori.mewbaka.cn/en/technical/loader-hook.md.
  • English
  • Loader hook

    This page records findings from reading the Ren'Py 8.5.2 source (renpy/loader.py, renpy/main.py, renpy/script.py, renpy/pygame/rwobject.pyx) and inspecting the runtimes shipped in the SDK. These findings must be re-verified on every Ren'Py version RenMori supports.

    1. Every asset goes through one choke point

    renpy.loader.load(name) → load_core(name) → tries each function in renpy.loader.file_open_callbacks in order and returns the first non-None result. The default order is config.file_open_callback, the filesystem, archives (RPA), APK, remote.

    Audit of the callers:

    ResourceCall site
    Imagesloader.load() → pygame.image.load(f, filename) (display/pgrender.py)
    Audio and videorenpy.audio.audio.load() → loader.load(fn, directory="audio"); movies play through audio channels
    Fontsloader.load(fn, directory="fonts") (text/font.py)
    Scripts.rpyc read with loader.load(fn, tl=False) (script.py), regardless of whether the file came from disk or an index

    Only bypass: renpy.exists() uses transfn() and is documented as disk-only. A game that calls renpy.exists() on a file that has been moved into the container sees False. The hook patches it.

    RenMori inserts its callback at index 0 of file_open_callbacks, so the container takes precedence over anything on disk.

    2. File discovery: scandirfiles_callbacks

    listdirfiles() (used for script discovery, renpy.list_files(), automatic image definitions and filling loadable_cache) is built by calling each function in renpy.loader.scandirfiles_callbacks with add(dn, fn, files, seen). RenMori registers a function that adds every container entry with dn=None, the same way archive entries are added. This makes loadable() true for container files and lets script discovery find .rpyc files that exist only inside the container.

    3. Timing: the hook must be installed before index_files()

    Ren'Py scripts are discovered at startup, before any game .rpy code runs, so a normal init python block is too late. Ren'Py 8.5 provides an official early hook: Ren'Py Extensions. renpy.main.main() loads, in sorted order, every *.rpe (a zip containing autorun.py, placed on sys.path) and *.rpe.py from the Ren'Py base directory, game/ and game/libs, before renpy.loader.index_files() and before the script is loaded.

    RenMori ships its runtime as game/renmori.rpe. (This is planned to move into the native launcher in a later version; .rpe is also an attacker injection point and must be locked down to RenMori's own signed extension.)

    4. The return type matters: RWopsIO and the reopen trap

    Ren'Py 8.5 hands file objects to C code (SDL_image, FreeType, ffmpeg) through to_rwops() in rwobject.pyx. Its behavior, in order:

    1. Unwraps .raw repeatedly
    2. For an RWopsIOImpl, uses its C SDL_RWops directly (fast, no GIL)
    3. For an io.IOBase with a string .name, reopens the file from disk by that name (with .base/.length if present)
    4. Otherwise, wraps anything with a read method in Python callbacks that take the GIL on every read

    Consequences for RenMori:

    • Trap: a decrypting file object that subclasses io.IOBase and exposes a string .name is reopened from disk, where it either fails to find the file or hands SDL the raw ciphertext; RenMori's stream objects must therefore expose neither .name nor .raw
    • Small and medium files: decrypt fully and return RWopsIO.from_buffer(plaintext, name=name), a pure C buffer RWops that takes no GIL afterwards and is the fastest path Ren'Py has
    • Large files (video, long background music): return RWopsIO(stream, name=name), where stream decrypts chunks on demand; this path goes through Python callbacks with one GIL acquisition per read and must be benchmarked, and the production answer is a native nogil decrypting SDL_RWops in the closed runtime

    5. AES inside Ren'Py's own Python: no dependency needed

    Ren'Py's bundled Python includes neither cryptography nor pycryptodome, but it does ship ctypes (PyInit__ctypes is present in all three runtimes), and:

    RuntimeAES-256-GCM source
    macOS (librenpython.dylib, universal)OpenSSL statically linked and exported (EVP_aes_256_gcm, EVP_CIPHER_CTX_new, EVP_DecryptUpdate and others)
    Linux (librenpython.so)Same, exported
    Windows (librenpython.dll, libpython3.12.dll)EVP symbols not exported; use the operating system's CNG (bcrypt.dll), which provides hardware-accelerated AES-GCM on every Windows version since Vista

    The runtime reader therefore calls OpenSSL (macOS/Linux) or CNG (Windows) through ctypes. Decryption runs in native code with AES-NI or ARMv8 cryptography extensions; only the per-chunk call overhead is in Python. The build tool uses the cryptography package. All backends produce standard AES-256-GCM, so the container format does not depend on the backend.

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