.pplx 扩展规范
PPL 自有扩展格式:包结构、清单字段、校验规则、加载顺序与打包。
为什么另起一套
社区里已有一套基于 JVM mixin 的加载器(RWML / RWPP)。PPL 不复用它的目录、描述符或包格式, 原因不是「想再造一个」:
- 许可证不兼容。那套项目用的是自定义团队许可证,抄格式、抄字段都有风险。
- 目录会打架。它占用
<游戏>/extensions/。两边共用同一目录时, 「谁启用了谁」根本说不清。 - 它不是我们能修的。依赖别人的加载器,意味着对方的崩溃与兼容性变更 都会直接砸到 PPL 用户头上。
| 社区方案 | PPL | |
|---|---|---|
| 目录 | <游戏>/extensions/ | <游戏>/ppl/extensions/ |
| 描述符 | META-INF/extension.properties | 包根 ppl-extension.json |
| 包后缀 | .jar | .pplx(本质是 zip) |
| 依赖解析 | 加载器自己算 | 启动器算好顺序,运行时只执行 |
包格式
一个 .pplx 就是普通 zip。目录层级没有强制要求,但描述符必须在包根:启动器只读这一个条目,不解压整个包。
my-extension/
├─ ppl-extension.json 必需,必须在包根
├─ lib/ 可选,扩展自带的依赖
└─ org/example/zoom/
├─ ZoomExtension.class
└─ mixin/CameraMixin.class描述符以外的东西启动器不关心,也不会执行——它只做静态校验与排序。 真正加载类、注入 mixin 是运行时的事。
清单字段
一份最小清单只有三个字段:
{
"schema": 1,
"id": "org.example.hello",
"name": "Hello"
}完整示例:
{
"schema": 1,
"id": "org.example.zoom",
"name": "缩放控制",
"version": "1.0.0",
"apiVersion": 1,
"mainClass": "org.example.zoom.ZoomExtension",
"mixins": ["org.example.zoom.mixin.CameraMixin"],
"dependsOn": ["org.ppl.core@>=1.0"],
"loadAfter": [],
"loadBefore": [],
"author": "someone",
"description": "把相机缩放上限放宽到 4 倍",
"minGame": "1.15"
}| 字段 | 必填 | 说明 |
|---|---|---|
schema | 否 | 格式版本,默认 1。只接受 1,其它值直接拒收。 |
id | 是 | 唯一标识,也是依赖关系的键。只允许字母、数字、.、-、_, 不超过 128 字符,不能以 . 开头。 |
name | 否 | 展示名。留空时用 id。 |
version | 否 | 仅用于展示,不参与任何求解。 |
apiVersion | 否 | 需要的运行时 API 版本,默认 1。高于启动器支持的值会被跳过并在界面提示。 |
mainClass | 否 | 入口类,由运行时实例化。 |
mixins | 否 | 要注入的类清单。 |
dependsOn | 否 | 硬依赖。可以带 @ 版本约束,但启动器只取 @ 前面的 id,不做版本求解。 |
loadAfter / loadBefore | 否 | 软顺序偏好,只在没有硬依赖冲突时生效。 |
author / description | 否 | 展示用。 |
minGame | 否 | 最低游戏版本,仅提示,不参与校验。 |
列表字段会被归一化
dependsOn、loadAfter、loadBefore 里的字符串会被拆分、去空格、 去掉 @ 之后的版本约束,然后排序并去重。所以["b, a@1.0", "a"] 等价于 ["a", "b"]。
这样做的后果要清楚:写版本约束不会锁版本。想约束别人,只能靠文档和社区约定。
目录与启停
<游戏目录>/
└─ ppl/
└─ extensions/
├─ org.example.zoom.pplx 启用中
└─ .disabled/
└─ org.example.old.pplx 已停用启用 / 停用就是在这两个目录之间移动文件:可逆、不删数据,用户手动也能还原。 只认 .pplx 与 .zip,其它文件会被忽略。
报告「包已损坏」不会把文件删掉,也不会静默跳过:包里没有ppl-extension.json、zip 打不开、JSON 非法或字段不合规,都会在列表里标出来并写下原因。
加载顺序
启动器在启动游戏前算好顺序,规则是确定的:
dependsOn声明的依赖必须先加载(硬边)。- 再叠加
loadAfter/loadBefore的软边。 - 同时可用时按 id 字典序取最小。
- 有环 → 整组都不加载,界面明确报出来。
- 缺依赖 → 只跳过缺依赖的那一个,其余照常加载。
apiVersion高于运行时 → 跳过并提示。
「必须是确定的」不是洁癖:同一批扩展在两台机器上顺序不同,问题会变得无法复现。 这几条都有单元测试覆盖(crates/ppl-core/src/extensions.rs),因为顺序错会让游戏直接崩。
打包
cd my-extension
zip -r ../org.example.zoom.pplx . -x '.*'文件名建议直接用 id,方便用户和依赖关系对上。包里的内容不限, 但启动器只校验描述符——坏包会拒收并标为「包已损坏」,不会悄悄放行一个永远不会生效的文件。
运行时现状
静态部分只负责「装好、排好、告诉运行时」。真正改写游戏类的是一个 JVM agent,目前没有任何代码,不要把它当成已实现:
启动器 运行时(agent)
│ 算出 order │
│ 写 ppl/runtime/plan.json │
├──────────────────────────────▶│ 读 plan.json
│ java -javaagent:... │ 按顺序加载各 .pplx
│ │ ClassFileTransformer 注入 mixin在写生产代码之前,需要先验证四件事:
| 问题 | 为什么必须先验证 |
|---|---|
| 游戏启动入口能否传 JVM 参数 | 有些打包器会把参数吃掉,-javaagent 可能根本传不进去。 |
| 游戏用哪个 JRE、版本多少 | agent 编译出的 class 版本不能高于运行时。 |
| 类名映射从哪来 | 混淆过的 jar,没有映射就写不出可用的 mixin。 |
| Android 端怎么办 | 那边是 dex,JVM agent 完全不适用,需要另一套。 |
RWML 兼容模式
启动器也能把社区加载器的扩展当文件管理:读取对方的META-INF/extension.properties 用于展示,开关就是移动文件。默认关闭,推荐用 PPL 格式。
- 只做互操作:不加载、不注入、不包含对方任何代码。
- 不替用户安装加载器:只检测
game-lib.jar.original与rwml-runtime。 - 停用方式尚未实测:把它移进
extensions/.disabled/是基于「按目录扫描」的推测。
给扩展作者
id一旦发布就别改:依赖关系、文件名都挂在它上面。- 别用
dependsOn里的版本约束来「锁版本」,写了也会被忽略。 - 多个扩展要协作,就抽一个
core扩展出来,让别的扩展依赖它,而不是互相loadAfter打转。 - 描述符里的
description会直接显示在扩展页,写清楚「这个扩展改了什么」, 比写「优化体验」有用得多。
规范的权威版本在仓库 docs/EXTENSIONS.md,解析器在crates/ppl-core/src/extensions.rs。两处不一致时以代码为准, 并欢迎到 Issues 指出。