> 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.

# renmori protect

`renmori protect` 将一个 Ren'Py 发行版构建转换为受保护的发行版构建：`game/` 下的资产与已编译脚本被封入加密容器 `game/renmori.rmc`，已编译脚本额外经过脚本层变换，明文原件被删除，加载器扩展 `game/renmori.rpe` 与构建清单 `renmori-manifest.json` 被写入。游戏启动时，Ren'Py 自动加载该扩展，由其在引擎内部解密并供给资源。

## 语法

```bash
renmori protect <game-dir | game.zip> [options]
```

| 参数         | 说明                                                            |
| ---------- | ------------------------------------------------------------- |
| `game-dir` | 发行版构建的根目录，即直接包含 `game/` 子目录的目录。不要传入 `game/` 本身                |
| `game.zip` | Ren'Py 启动器生成的发行归档，例如 `MyGame-1.0-pc.zip`、`MyGame-1.0-mac.zip` |

## 输入

### 构建目录

输入目录必须直接包含 `game/` 子目录，否则命令以退出码 2 失败。若 `game/` 中已存在 `renmori.rmc`，说明该构建已经受保护，命令同样以退出码 2 失败，不会进行二次保护。请始终以全新的发行版构建，或保护前生成的备份作为输入。

### 发行归档（.zip）

当输入是一个有效的 `.zip` 文件时，`protect` 先将其解压，再对解压结果执行保护：

1. 确定输出目录：指定 `-o` 时使用该目录，否则使用与归档同级、以归档名（不含扩展名）命名的目录，例如 `MyGame-1.0-pc.zip` 对应 `MyGame-1.0-pc/`。该目录必须不存在
2. 在输出目录的上级目录中创建临时目录并解压。解压前会逐项检查归档成员路径，任何会解压到目标目录之外的成员（zip-slip）都会导致命令失败
3. 定位构建根目录：解压根目录本身包含 `game/` 时直接使用；否则在顶层子目录中查找唯一一个包含 `game/` 的目录，这对应启动器将构建包裹在一层同名文件夹中的常见情况。找不到或找到多个时命令失败
4. 将构建根目录移动到输出目录，随后按原地保护的方式处理该目录，并清理临时目录

原归档始终保持不变并可作为原件，因此从归档输入时不会另外创建 `_bak` 备份。

## 输出模式

| 模式       | 触发条件                      | 原始构建      | 备份                   |
| -------- | ------------------------- | --------- | -------------------- |
| 原地保护（默认） | 未指定 `-o`，输入为目录            | 被改写为受保护构建 | 先复制到同级目录 `<dir>_bak` |
| 原地保护，不备份 | 未指定 `-o`，指定 `--no-backup` | 被改写为受保护构建 | 不创建，操作不可撤销           |
| 输出到新目录   | 指定 `-o OUTPUT`            | 保持不变      | 不需要，`OUTPUT` 必须不存在   |
| 从归档保护    | 输入为 `.zip`                | 归档保持不变    | 不需要，归档即原件            |

:::warning
原地保护会删除 `game/` 下的明文原件。若 `<dir>_bak` 已存在，命令会在执行任何改动之前以退出码 2 失败；请删除旧备份、改用 `-o`，或在确认无需备份时使用 `--no-backup`。备份目录包含完整明文资源，切勿将其随发行版一同上传。
:::

## 执行流程

命令按以下顺序执行，所有可预见的冲突都在任何文件被改动之前检查完毕。

1. **解析输入**：目录直接使用；`.zip` 先解压并定位构建根目录
2. **前置检查**：确认 `game/` 存在且尚未受保护
3. **合并配置**：读取 `renmori.toml`（若存在），与命令行参数合并，参阅 [配置文件](https://renmori.mewbaka.cn/cli/configuration.md)
4. **确定保护强度**：未指定强度时使用完整保护，需要闭源插件；指定 `baseline` 时使用开源基线格式且不下载插件；指定 `auto` 时插件可用则使用完整保护，否则使用基线。插件的获取方式参阅 [保护强度与闭源插件](https://renmori.mewbaka.cn/cli/plugin.md)
5. **准备密钥**（仅基线）：从 `[keys]` 指定的环境变量读取容器密钥与脚本密钥，未指定或变量未设置时为本次构建生成随机密钥
6. **预检输出位置**：检查 `-o` 目录或 `<dir>_bak` 是否已存在，统计待封包文件与脚本数量，没有可封包的文件时失败
7. **打印摘要并确认**：在 stderr 输出输入、模式、文件数量、脚本数量、锁定状态与密钥来源；交互式终端下请求确认 `[y/N]`，回答 `y` 或 `yes` 以外的内容都会取消，取消时输出 `{"status": "aborted"}` 并以退出码 2 结束
8. **复制或备份**：`-o` 模式复制整个构建到输出目录；原地模式先复制到 `<dir>_bak`（`--no-backup` 时跳过）
9. **封包与加密**：将待封包文件逐个写入 `game/renmori.rmc`，`.rpa` 封包被拆开后逐项写入，已编译脚本经脚本层变换后写入
10. **删除明文**：删除已封包的原件与被剔除的 `.rpy` 源码，并移除因此变空的子目录（`--keep-plaintext` 时跳过）
11. **写入加载器扩展**：生成 `game/renmori.rpe`
12. **写入构建清单**：在构建根目录生成 `renmori-manifest.json`，并在 stdout 输出结果 JSON

使用闭源插件时不执行第 5 步，密钥由插件自行派生；第 8 至 11 步由插件完成，构建清单仍由 `renmori` 写入。原地模式下 `renmori` 先将原始构建移至备份目录（`--no-backup` 时为临时目录，成功后删除），再由插件从该目录读取并写出全新的构建；插件失败时原始构建会被移回原位，`-o` 模式下产生的不完整输出会被删除。

## 文件处理规则

`protect` 遍历 `game/` 下的全部文件，并按以下规则分类。下表描述基线保护流程；完整保护由插件执行封包，其文件选择同样遵循 `exclude` 与 `keep_on_disk` 的设置。

| 文件                                | 处理方式                                              |
| --------------------------------- | ------------------------------------------------- |
| 图像、音频、视频、字体、翻译等资产                 | 封入容器，删除明文原件                                       |
| `.rpyc`、`.rpymc` 已编译脚本            | 经脚本层变换后封入容器，删除明文原件                                |
| `.rpy`、`.rpym` 源码，且存在同名已编译文件      | 不封包，直接删除。引擎只执行已编译脚本，随发行版附带源码会使任何一次解密都直接获得原始剧本     |
| `.rpy`、`.rpym` 源码，但不存在已编译文件       | 封入容器，确保内容不会丢失                                     |
| `.rpa` 封包（RPA-3.0、RPA-2.0）        | 拆开后将其中的每个条目作为独立文件封入容器，删除封包。与磁盘上同名的散文件冲突时，先遇到的一方保留 |
| `game/saves/`                     | 整个目录跳过，存档保持原样                                     |
| `game/cache/` 中的 `.rpyb`          | 封入容器。这类文件是由脚本派生的已编译缓存                             |
| `game/cache/` 中的其他文件              | 跳过，保持原样，例如 `shaders.txt`、`build_info.json`        |
| `steam_appid.txt`                 | 默认保留在磁盘上，不加密。Steam SDK 直接读取该文件，不经过 Ren'Py 加载器     |
| 匹配 `exclude` 或 `keep_on_disk` 的文件 | 保留在磁盘上，不加密                                        |
| 符号链接及非常规文件                        | 跳过                                                |

容器中的条目名称使用 Ren'Py 的游戏相对路径形式：正斜杠分隔，无前导斜杠，例如 `images/bg room.png`。运行时，容器在加载器链中处于最高优先级，因此即使磁盘上残留同名明文文件，引擎读取的也始终是容器中的版本。

:::danger
不要通过 `exclude` 排除 `.rpy`、`.rpyc`、`.rpym`、`.rpymc` 等脚本文件。运行时加载器会拒绝打开 `game/` 中位于磁盘上的脚本类文件，以阻止反编译器注入，因此被排除的脚本将无法加载。`exclude` 只应用于由引擎之外的程序读取的文件。
:::

## 选项

### 输出与备份

#### `-o, --output DIR`

将受保护构建写入 `DIR`，原始构建保持不变。`DIR` 必须不存在，否则以退出码 2 失败；其上级目录不存在时会自动创建。该模式不创建备份。输入为 `.zip` 时，`DIR` 是解压并保护的目标目录。

#### `--no-backup`

原地保护时不创建 `<dir>_bak` 备份，直接改写原始构建。该操作不可撤销，仅建议在 CI 中对一次性的构建目录使用。指定 `-o` 或输入为 `.zip` 时此选项无效果。

#### `-y, --yes`

跳过保护前的确认提示，以及首次下载闭源插件时的确认提示。stdin 不是终端，或指定了 `--quiet` 时，提示同样会被跳过。

### 配置

#### `-c, --config FILE`

从 `FILE` 读取配置。文件不存在时以退出码 2 失败。未指定时，依次查找 `<game-dir>/renmori.toml` 与当前工作目录下的 `renmori.toml`，使用找到的第一个；均不存在时使用默认配置。全部配置键参阅 [配置文件](https://renmori.mewbaka.cn/cli/configuration.md)。

### 保护强度与插件

#### `--strength S`

设置保护强度，对应配置键 `strength`。

| 取值         | 行为                                           |
| ---------- | -------------------------------------------- |
| 不指定（默认）    | 完整保护：使用闭源插件声明的最强强度。插件不可用时以退出码 2 失败，不会自动退化为基线 |
| `baseline` | 开源基线格式：固定的容器格式与随附的占位密钥，不下载、不调用插件             |
| `auto`     | 插件可用时使用其最强强度，否则使用基线                          |
| 插件声明的其他取值  | 交由插件处理，插件不可用或不支持该取值时以退出码 2 失败                |

两者的安全性差异参阅 [保护强度与闭源插件](https://renmori.mewbaka.cn/cli/plugin.md)。

#### `--plugin PATH`

使用 `PATH` 处的闭源插件，不再查找缓存或自动下载。等同于设置 `RENMORI_PLUGIN`，命令行参数优先。路径不存在时会提示并忽略该参数，继续查找缓存或自动下载。`PATH` 可以是可执行文件，也可以是由当前 Python 解释器运行的 `.py` 脚本。

#### `--platforms LIST`

仅对插件生效。以逗号分隔的目标平台列表，例如 `win-x64,linux-x64,mac-universal`，对应配置键 `platforms`。未指定时由插件根据构建中的 `lib/py3-*` 目录自动检测。

#### `--launcher`

仅对插件生效。将密钥绑定到原生启动器并随构建一同分发，使密钥只在通过该启动器启动游戏时才被重建。对应配置键 `launcher`。

### 加密参数

#### `--chunk-size N`

容器分块大小，单位为字节，对应配置键 `chunk_size`，默认 65536。每个分块独立加密并附带 16 字节认证标签。较大的分块有利于大型 CG 与视频，较小的分块可降低大量小文件的额外开销。该参数决定基线容器的分块大小；完整保护的分块参数由插件按构建确定。

#### `--exclude GLOB`

使匹配 `GLOB` 的文件保留在磁盘上且不加密，可重复指定。命令行中的模式追加在配置文件的 `exclude` 与 `keep_on_disk` 之后，不会替换它们。

匹配规则：

- 模式与游戏相对路径进行匹配，路径以正斜杠分隔，例如 `thirdparty/*` 匹配 `game/thirdparty/` 下的文件
- 使用 Python `fnmatch` 语义：`*` 匹配任意字符且可以跨越目录分隔符，`?` 匹配单个字符，`[seq]` 匹配字符集合
- 在 Windows 上匹配不区分大小写，在 macOS 与 Linux 上区分大小写
- 建议为每个模式加引号，避免被 shell 提前展开，例如 `--exclude "thirdparty/*"`

### 运行时行为

#### `--no-lockdown`

在受保护构建中保留开发者功能。默认开启的锁定会在运行时：

- 关闭 `config.developer` 与 `config.console`，使开发者模式与开发者控制台不可用
- 拒绝执行可导出脚本与文本数据的 Ren'Py 命令行子命令：`compile`、`dialogue`、`translate`、`add_from`、`merge_strings`

对应配置键 `lockdown`，发布版本应保持锁定开启。

#### `--keep-plaintext`

封包完成后不删除明文原件，也不删除被剔除的 `.rpy` 源码，仅用于排查问题。对应配置键 `keep_plaintext`。该选项作用于基线保护流程。

:::danger
启用 `--keep-plaintext` 生成的构建不具备任何保护效果，`renmori verify` 也会因明文残留而报错。切勿发布此类构建。
:::

### 输出控制

#### `--no-hints`

保护成功后不打印“打包并发布”指引。默认会提示：校验构建、为 itch.io 或直接下载压缩整个目录、通过 SteamPipe 上传目录内容、`renmori.rmc` 与 `renmori.rpe` 须一同分发，以及不要再次对该目录运行 Ren'Py 启动器构建。

#### 通用选项

`--lang`、`--no-color`、`--quiet` 同样适用，参阅 [CLI 概览](https://renmori.mewbaka.cn/cli/overview.md#%E9%80%9A%E7%94%A8%E9%80%89%E9%A1%B9)。

## 生成的文件

| 路径                      | 说明                                                                                                          |
| ----------------------- | ----------------------------------------------------------------------------------------------------------- |
| `game/renmori.rmc`      | 加密容器，包含全部被封包的资产与已变换的脚本                                                                                      |
| `game/renmori.rpe`      | 加载器扩展，Ren'Py 启动时自动加载，负责在引擎内部解密并供给资源                                                                         |
| `renmori-manifest.json` | 构建清单，记录工具版本、容器哈希、文件数量等信息，供 `renmori verify` 使用，格式参阅 [输出、构建清单与退出码](https://renmori.mewbaka.cn/cli/output.md) |
| `<dir>_bak/`            | 原地保护时生成的完整明文备份，不属于发行版                                                                                       |

运行时，Ren'Py 还会在 `game/` 下生成 `cache/` 目录，这是引擎的正常行为。

## 示例

保护一份副本，保留原始构建：

```bash
renmori protect ./dist/MyGame-1.0-pc -o ./dist/MyGame-1.0-pc-protected
```

原地保护，原件备份到 `./dist/MyGame-1.0-pc_bak`：

```bash
renmori protect ./dist/MyGame-1.0-pc
```

直接处理启动器生成的发行归档，结果写入 `./dist/MyGame-1.0-pc/`：

```bash
renmori protect ./dist/MyGame-1.0-pc.zip
```

在 CI 中以非交互方式执行，只输出 JSON：

```bash
renmori protect ./build/MyGame-pc --no-backup --quiet -y
```

排除由第三方程序直接读取的文件，并显式使用开源基线：

```bash
renmori protect ./dist/MyGame-1.0-pc -o ./out --strength baseline --exclude "thirdparty/*"
```

使用本地已有的插件并指定目标平台：

```bash
renmori protect ./dist/MyGame-1.0-pc -o ./out --plugin ./renmori-plugin-linux-x64 --platforms win-x64,linux-x64
```

:::warning
受保护目录即是发行版本身。不要再对其运行 Ren'Py 启动器的 _Build Distributions_，启动器会从源码工程重新构建并丢弃全部保护。
:::
