
掠夺引擎
更多着色器能力的基础依赖,以及为专门提升着色器沉浸感的各种新模组提供的数据总线。
PlunderEngine
PlunderEngine.com 为开发者提供了新的 CC0 开源标准。
PlunderEngine 是 PlunderPixels Link 系列模组的核心模组。我在构建 PlunderPixels 光影(链接)的过程中,不断遇到光影单独无法获知的信息:风实际在哪里、水体的起点和终点在哪里、玩家安装的草纹理实际长什么样。因此,我构建了一个客户端引擎,它能够读取实时游戏数据,并通过清晰、带版本号的通道将这些数据传递给光影和功能模组。这个 jar 就是那个引擎。
它本身不产生任何可见效果。它是 Link 功能模组的必需前置,而 WindLink(链接)是第一个此类模组。请将 PlunderEngine 与某个 Link 模组一起安装。
该系列模组正在积极开发中,你直接告诉我的信息将决定下一步的开发方向。
一个核心,让模组套件不会变成配置泥潭
每个 Link 都接入同一个核心。这带来了三大好处:
- 一个配置文件。
config/plunderlink.json,带防抖自动保存,支持导出和导入。每个已安装的 Link 都在同一文件中的专属分区保存其设置,并鼓励光影开发者分发这些设置作为其默认配置(原生光影钩子正在开发中)。 - 一个设置界面。 可通过按键绑定、ModMenu 或视频设置按钮(同时支持原版和 Sodium)打开。每个已安装的 Link 都会添加自己的标签页,引擎会渲染所有这些标签页,因此即使安装了五个 Link,用起来也像一个模组。
- 一个状态命令。
/plunderengine会打印实时报告:每个已安装的 Link、其光影通道绑定、各系统帧耗时以及配置摘要,还支持从聊天框重载和导出配置。
无论你运行多少个 Link,你都可以在一个地方配置整个套件,并用一条命令进行调试。
一个你可以真正基于其构建的 API
PlunderEngine 的另一半是一个公共 API。这不仅仅是我模组套件的私有管道。如果你制作模组,你可以依赖 plunderengine 并获得:
- 无视加载顺序的模块注册。 你的模组实现
plunderengine入口点并注册一个模块。引擎会先收集所有注册信息,然后在所有注册完成后仅加载一次配置,因此设置绝不会因为某个 jar 加载顺序不同而静默重置。 - 免费获得配置持久化。 你的模块声明其配置分区;引擎负责保存、加载、导出和导入。你无需直接操作文件。
- 免费获得设置标签页。 通过一套小型控件词汇表声明开关、滑块和循环选择器;引擎会为你构建界面并跟踪脏状态。
- 通过 SpriteMetrics 进行纹理解析。 测量玩家当前活动资源包堆栈中的任何纹理:可见图案的起始和结束位置、不透明覆盖率、平均颜色。带缓存、失败降级,并在资源重载时刷新。这就是 WindLink 测量草精灵图并发布真实草叶高度的方式,使得光影能够弯曲玩家在任何资源包中实际看到的草叶。
- Iris 通道桥接模式。 将实时游戏数据发布为可供光影读取的通道的约定,以及 PlunderData——套件发布的每个通道的机器可读目录。Iris 保持可选;没有它,一切都能干净地降级。
此外,还有一个基于注册表的渲染接管机制,用于那些需要抑制方块的原版渲染并绘制自己的替代几何体的模组。
API 版本在模组元数据中声明。破坏性变更会提升版本号;计划是增量增长,而非频繁更替。
光影连接
Link 模组使用此引擎为 PlunderPixels 光影(链接)提供实时数据通道:风与阵风、地形流动、水体与岸边、光照、热量、季节。这正是整个套件的意义所在:让光影获得仅靠自身无法得知的信息。不过,这种配对并非硬性依赖。没有这些模组,光影包会回退到其自身的数学计算。没有光影,Link 模组仍能保留其游戏内效果。引擎本身与光影无关。
兼容性
仅客户端,且对原版服务器安全。PlunderEngine 不会向世界添加任何内容,不会向任何服务器发送数据,并且可以随时添加或移除。需要 Fabric、Minecraft 26.1.x、Java 25 或更新版本以及 Fabric API。Sodium、Iris 和 ModMenu 为可选;推荐安装 Sodium 和 Iris。
未来发展方向
核心已经足够稳定,WindLink 现已基于它发布,但这仍是第一个版本。更多 Link 模组正在开发中(水体、光照、热量、地表覆盖),每个都将作为独立模组接入这个相同的核心,引擎将不断成长,我将提供更多可视数据通道,供光影和其他模组调用以创造更多可能性。
我正在构建的整个套件都力求易用:合理的默认值、单一界面、所有内容均为可选、缺失组件时干净的回退。如果你基于 API 构建,或者只是运行套件时感觉有什么不对劲,请告诉我。社区的反馈将激励我保持热情,继续推动这个项目向前发展。
文档
第一部分:面向玩家
环境要求
- Minecraft 26.1.x、Fabric Loader 0.19.2 或更新版本、Fabric API、Java 25 或更新版本。
- Sodium 和 Iris 为可选但推荐安装。
- 至少安装一个 Link 模组,否则引擎无事可做。目前可用的是 WindLink(链接)。
配置文件:config/plunderlink.json
整个套件共用一个 JSON 文件。每个已安装的 Link 在其中拥有自己的分区,以模块 ID 作为键(例如,WindLink 会添加 "wind" 和 "windViz")。首次运行时,文件会写入默认值,方便你查找和编辑。
其行为如下:
- 在设置界面中所做的更改会自动保存,因此拖动滑块只会产生一次写入。
- 写入操作会先写入临时文件,然后替换到位,因此写入中途崩溃绝不会损坏配置。
- 支持手动编辑。缺失的键会回退到默认值,未知的键会被忽略,加载时超出范围的值会被限制。损坏的文件会被原样保留在磁盘上;默认值仅在内存中应用。
- 导出文件存放在
config/plunderlink/exports/下,为带时间戳的副本(样式如plunderlink-20260722-153000.json)。
文件名是 plunderlink 而非 plunderengine。这是出于历史原因且为了兼容性而固定不变;你的设置能跨更新保留,正因为这个名称从不改变。
设置界面
整个套件共用一个界面。顶部是标签页,每个已安装的 Link 一个,外加一个用于配置工具的 ENGINE 标签页。悬停控件会在底部栏显示其描述。进入方式:
- 按键绑定。 控制中新增一个"PlunderEngine"分类。默认未绑定,以免与你现有的按键冲突;你可以将其设置为任意按键。
- ModMenu。 如果你安装了 ModMenu,模组的配置按钮会打开此界面。
- 视频设置(使用 Sodium)。 Sodium 的视频设置中会出现一个带有品牌图标的 PlunderEngine 条目;点击后会进入此界面,点击"完成"返回视频设置。
- 视频设置(未使用 Sodium)。 原版视频设置界面的右上角会出现一个小的"PlunderEngine…"按钮。
根据是否安装 Sodium,上述两个视频设置入口只会显示其中一个。
另外:当当前激活的 Iris 光影包是 PlunderPixels 系列时,此界面还会显示该光影包自身的一组精选选项(风、天空、水体、后期效果、性能),并将它们写入光影包正常的设置文件,然后请求 Iris 重载。此操作仅对 PlunderPixels 光影包生效,绝不会影响第三方光影包。
/plunderengine 命令
仅限客户端,绝不会发送到服务器。
/plunderengine或/plunderengine status会打印引擎版本以及检测到的 Minecraft、Iris 和 Sodium 版本,然后是每个已安装 Link 自身的状态行(当 Iris 存在时,包括其光影通道 ID)、耗时快照和配置文件状态。/plunderengine config reload会重新读取config/plunderlink.json并实时应用。在手动编辑文件后非常有用。/plunderengine config export会将当前设置的带时间戳副本写入config/plunderlink/exports/。
如果状态显示 modules: none installed,说明引擎正在运行但没有安装 Link 模组。请安装一个;从 WindLink(链接)开始。
第二部分:面向模组开发者(API)
PlunderEngine 不仅仅是我模组套件的内部管道。它是一个公共 API 表面,第三方模组可以依赖并基于其构建。注册一个模块,你就能免费获得配置持久化、设置标签页和状态报告;纹理解析和光影通道层在需要时可供使用。以下所有内容均仅限客户端,并且按设计原则做到失败降级。
API 版本位于核心模组 fabric.mod.json 中的 "plunderengine:module_api" 自定义键(当前为 v1)。破坏性变更会提升版本号;增量增长是常态。
依赖引擎
在你的 fabric.mod.json 中:
{
"depends": {
"fabricloader": ">=0.19.2",
"fabric-api": "*",
"minecraft": "~26.1",
"java": ">=25",
"plunderengine": ">=0.1.0"
}
}
对于你的开发环境,使用 modImplementation 添加已发布的 jar。目前还没有 Maven 端点(这是第一个版本);请使用本地文件依赖,或者等项目页面上线后使用 Modrinth Maven。在 Modrinth/CurseForge 上,将 PlunderEngine 列为必需依赖,以便启动器自动安装。
"plunderengine" 入口点
引擎通过一个自定义 Fabric 入口点来发现你的模组。将其与你正常的客户端入口点一起声明:
"entrypoints": {
"client": [ "com.example.mylink.MyLinkClient" ],
"plunderengine": [ "com.example.mylink.MyLinkInit" ]
}
并实现 com.plunderpixels.engine.module.PlunderEngineInit:
public final class MyLinkInit implements PlunderEngineInit {
@Override
public void registerModules() {
EngineModules.register(new MyModule());
}
}
为什么用自定义入口点而不是你的客户端初始化代码:引擎的引导过程会首先迭代每个已安装模组的 "plunderengine" 入口点,然后才加载 config/plunderlink.json。自定义入口点的迭代在模组间与加载顺序无关,因此配置应用前模块注册表总是完整的。否则,如果某个模组的客户端入口点恰好在配置加载之后运行,它就会在每次启动时静默地将自己的设置重置为默认值。
两条硬性规则:
registerModules()仅用于注册。不得涉及事件、纹理、世界访问或依赖任何其他模组已初始化。你的运行时接线应放在你自己的"client"入口点中。- 切勿自行加载配置。核心会在所有注册完成后仅加载一次配置。如果你的入口点抛出异常,引擎会记录日志并继续运行;一个损坏的 jar 绝不能拖垮整个套件。
实现 EngineModule
com.plunderpixels.engine.module.EngineModule 是契约,每个你拥有的配置分区对应一个实例:
public interface EngineModule {
String id(); // 配置分区键和标签页 ID,小写,发布后冻结
default String displayName() { ... } // 人类可读名称,默认为 id()
void save(JsonObject section); // 将你的实时状态写入你的分区
void load(JsonObject section); // 将你的分区应用到实时状态;限制所有值
default List<String> status() { ... } // 0..n 行,用于 /plunderengine status
default String screenTab() { ... } // 设置标签页 ID,若无需界面显示则为 null
default void buildScreen(EnginePanel panel) { } // 布局你的控件
}
各部分在实际中的含义:
id()是config/plunderlink.json中的 JSON 分区键,并且从你发布的那一刻起就向后兼容冻结。请谨慎选择。save(JsonObject section)使用普通的addProperty调用写入你的实时值。load(JsonObject section)可能收到null或部分对象(旧文件或手动编辑的文件)。对缺失内容使用默认值,限制所有值,遇到坏值绝不抛出异常。核心为此提供了com.plunderpixels.engine.module.JsonCfg:optBool、optInt、optLong、optFloat、optDouble、optString(null 分区、缺失键或错误的 JSON 类型都会返回你的默认值)以及clampInt、clampLong、clampFloat、clampDouble。load 方法应该读起来像一长串field = clamp(opt(section, key, DEFAULT), lo, hi)行。status()为/plunderengine status提供数据。在这里包含你的 Iris 通道 GL ID,但在独立方法中构建这些行,并用FabricLoader.getInstance().isModLoaded("iris")保护,以便在 Iris 不存在时不会类加载任何 Iris 类型。screenTab()返回你的面板所在的标签页(语言键plunderengine.tab.<value>)。共享相同值的模块按注册顺序堆叠在同一个标签页上,各自位于plunderengine.section.<id>标题下。若无需界面显示则返回null。buildScreen(EnginePanel)通过下面的面板词汇表布局你的控件。
注册位于 com.plunderpixels.engine.module.EngineModules:register(EngineModule)(在客户端初始化时调用一次;null 和重复 ID 会被忽略)、all()、byId(String)、count()。注册顺序即为标签页和状态的显示顺序。
EnginePanel 控件词汇表
com.plunderpixels.engine.module.EnginePanel 是你的模块贡献设置界面 UI 的方式,而无需引用任何 GUI 类。核心界面实现它并将其传递给 buildScreen;该接口有意不包含任何 Minecraft 类型,因此你的模块保持可单元测试。
public interface EnginePanel {
enum Format { INT, ONE_DP, TWO_DP, PERCENT }
void toggle(int row, int col, String key, BooleanSupplier getter, Consumer<Boolean> setter);
void slider(int index, String key, double min, double max, double step, Format format,
DoubleSupplier getter, DoubleConsumer setter);
void cycler(int row, int col, String key, Supplier<String> stateLangKey, Runnable next);
interface Track {
double toTrack(double real); // 实际值 -> 归一化 0..1 轨道位置
double fromTrack(double norm); // 反之亦然
String format(double real); // 显示字符串
}
void curved(int index, String key, Track track, DoubleSupplier getter, DoubleConsumer setter);
}
布局约定:两列。toggle 和 cycler 放置在相对于你分区显式的 (row, col) 位置;slider 和 curved 接受一个运行索引(col = index % 2, row = index / 2)。界面会按你的分区基准偏移所有内容,因此堆叠的模块面板永远不会冲突。值通过原始类型的供应商和消费者流动,直接修改你的实时状态;界面在每次修改后将配置标记为脏,防抖保存会处理其余事宜。你永远不需要在面板中处理持久化。
curved 用于非线性滑块:你的 Track 在实际值和轨道位置之间映射,因此你可以提供一个分段曲线(一个滑块上有精细区间和粗略区间),而核心无需了解曲线。
标签是语言键:plunderengine.opt.<key> 用于标签,plunderengine.opt.<key>.tip 用于底部栏的悬停描述。
语言文件
贡献你自己的 assets/plunderlink/lang/en_us.json 片段。语言文件会跨 jar 合并,因此你的文件只需包含你自己的键:
plunderengine.tab.<tab>用于你的标签页plunderengine.section.<moduleId>用于你的分区标题plunderengine.opt.<key>和plunderengine.opt.<key>.tip用于你的控件
SpriteMetrics:测量玩家实际看到的图案
com.plunderpixels.engine.texture.SpriteMetrics 测量当前活动资源栈中的任何纹理,并返回缓存、失败降级的度量结果。这就是模组了解光影单独无法得知的信息的方式:精灵图可见图案的起始和结束位置、纹理的实心程度、平均颜色,无论玩家运行的是什么资源包。WindLink(链接)使用它来测量草精灵图并发布草叶高度,使光影的风在任何资源包上都能弯曲可见的草叶,而不是整个四边形。
结果是一个记录,采用与分辨率无关的分数表示:
public record Metrics(boolean valid, int width, int height,
float visibleTopFraction, float visibleBottomFraction,
float opaqueFraction, int averageColor) { }
visibleTopFraction 表示图案从精灵图底部向上延伸的高度,0..1(一个像素在 16px 精灵图中最高达到 13px 的草叶读取为 0.8125)。visibleBottomFraction 表示图案从底部开始的位置(0 表示它触到底部行)。opaqueFraction 是达到或超过 alpha 阈值(255 中的 128)的像素比例。averageColor 是可见像素上的 0xRRGGBB。
用法:
import com.plunderpixels.engine.texture.SpriteMetrics;
import net.minecraft.resources.Identifier;
Identifier id = Identifier.withDefaultNamespace("textures/block/short_grass.png");
SpriteMetrics.Metrics m = SpriteMetrics.get(id);
if (m.valid()) {
float bladeTop = m.visibleTopFraction(); // 将其输入到你的通道或几何体中
}
契约:get 在客户端线程中执行,懒加载并缓存;第一次调用会读取和测量,后续调用是映射命中。整个缓存在资源重载、F3+A 和视频重新初始化时清空(核心为每个消费者连接一个 InvalidateRenderStateCallback 到 invalidateAll()),因此只需重新查询即可始终看到当前激活的资源包。任何失败都会返回 Metrics.INVALID(valid() == false,全范围全不透明的默认值),并且失败也会被缓存,因此不会出现逐帧重试的冲击。动画条带仅测量第一帧。纯 measure(int[] argbPixels, int width, int height, int alphaThreshold) 是公共的且可单元测试,如果你想要无需资源管理器的数学计算。
Iris 通道:向光影包发布实时数据
Link 模组将实时数据以小纹理的形式绑定到命名采样器发布给光影。引擎的作用是约定和目录;注册本身是你拥有的一个小 mixin。
注册模式。 复制 WindLink 的做法:mixin 到 Iris 的 CustomTextureManager 构造函数的尾部,并将你的纹理访问对象 putIfAbsent 到 irisCustomTextures 中:
@Mixin(CustomTextureManager.class)
public class MyCustomTextureManagerMixin {
@Shadow @Final private Object2ObjectMap<String, TextureAccess> irisCustomTextures;
@Inject(method = "<init>", at = @At("TAIL"), require = 0)
private void mylink$registerChannels(CallbackInfo ci) {
irisCustomTextures.putIfAbsent(MyFieldTexture.SAMPLER_NAME, MyFieldTexture.INSTANCE.access());
}
}
细节都很重要:
require = 0,因此 Iris 内部变动绝不会导致硬崩溃;你的模组在没有绑定的情况下仍可使用。- 用 mixin 插件将整个 mixin 门控起来,检查
FabricLoader.getInstance().isModLoaded("iris")。Iris 是仅编译期依赖;当其不存在时,其任何类型都不得被类加载。 putIfAbsent而非put。多个 Link 在同一插入点注册是安全的,并且你绝不会清除合法声明了相同名称的光影包。- 管理器在每次光影重载和维度切换时重建,因此注册会自动重新运行。
- 绑定仅被声明了该采样器的程序消耗(
uniform sampler2D plunderMyField;),因此注册对所有其他光影包都是惰性的。
目录。 同时向 com.plunderpixels.engine.data.PlunderChannels 注册你的通道元数据,以便 /plunderengine status、设置界面和其他模组能发现它:
PlunderChannels.register("my-field", "plunderMyField", "MyConvention v1", "one line saying what it carries");
Channel 是纯元数据(名称、采样器 uniform、约定版本、描述);活动 GL 纹理保留在你的模块中。采样器 ID 是冻结的光影契约:描述它们,绝不要重命名。com.plunderpixels.engine.data.PlunderData 保存当前套件发布的已知通道目录;将其用作命名风格的参考。
配对规则。 如果你发布一个与光影包配对的模组,该模组必须注册配对光影包声明的每个通道。如果光影包源读取一个没有运行中的 jar 注册的采样器,那就是一个破坏的契约:该读取没有定义的输入源,且没有回退能拯救它。总是先发布注册,再发布光影读取。反向规则同样严格:一个通道仅仅被注册绝不能改变光影行为。它必须在不活动时保持惰性。这是我在季节通道上吃过的教训——它写入一个默认 alpha 值并静默覆盖了光影自身的季节日历;一个已注册但空闲的通道必须读取为不存在,而不是一个值。
渲染接管
com.plunderpixels.engine.render.BlockRenderTakeover 适用于绘制自己的替代几何体的模块。在客户端初始化时 register(String name, Predicate<BlockState> hidden);任何匹配的方块都会被网格化为不可见(原版网格器和 Sodium 都咨询的唯一 getRenderShape 接缝),然后由你的模块自行绘制。仅限渲染:碰撞、掉落物和行为保持不变。谓词在网格化时逐方块运行,因此请保持其为廉价短路判断,并用你自己的启用标志进行门控;禁用的模块绝不能隐藏任何东西,而且你隐藏的集合必须等于你重新绘制的集合。
设计原则
每个 Link,无论是我写的还是你写的,都遵守与引擎相同的规则:
- 仅客户端。与服务器零接触,对原版服务器安全。
- 移除安全:卸载 Link 不会留下任何损坏。
- Iris 和 Sodium 是可选的。仅编译期依赖,通过入口点或 mixin 插件门控,因此它们的缺席绝不会导致类加载崩溃。
- 处处失败降级。降级,绝不崩溃。
- 先保持引擎纯净并测试,然后再接入游戏。
反馈
这是 0.1.0 版本,第一个公开版本,围绕它的整个套件正在积极开发中。WindLink(链接)是上述所有内容的最新参考实现,PlunderPixels 光影(链接)则是这些通道能力的深度体现。如果你构建了一个 Link、遇到了 API 缺口,或者对引擎下一步应该支持什么有自己的看法,请到项目页面留言。路线图确实由用户需求驱动。
正在加载版本记录…
正在加载评论…
评论在新手盒子客户端中发表,这里同步展示。