本篇教程由作者设定使用 CC BY-NC-SA 协议。
零、写在前面
协议与声明
注意,本文内容使用 CC: BY-NC-SA 协议,个人实现的代码以相同协议开源。而其它有说明的代码以 Forge 反编译的 Minecraft 源代码为主,所有函数、类和成员变量的命名使用了官方的反混淆表,函数参数和局域变量根据笔者的个人理解命名,仅作学习交流使用。
Curios API 的代码以 LGPL 协议开源,因而联动或依赖 Curios API 时,你可以用任何协议来开源你的 mod。
Curios API 有官方的英文文档,但内容非常少,本文在完整介绍 Wiki 的同时,加入了个人理解,并作出了适当补充。你也可以查看Curios API 的 Wiki 原文。
此教程以默认读者具备 Java 代码能力和 mod 开发的基础了解。
配置 build.gradle
首先你要在这里找到合适的 Curios API 版本,如1.18.2 的 Curios API v5.0.9.0。
然后,你要在 build.gradle 中加入对应的 maven 下载 url:
repositories {
maven { // TOP
url "https://cursemaven.com"
}
maven { //mirror
name = "ModMaven"
url = "https://modmaven.dev"
}
}
然后,在 dependencies 中加入 curios:
dependencies {
minecraft 'net.minecraftforge:forge:1.18.2-40.1.0'
compileOnly fg.deobf("top.theillusivec4.curios:curios-forge:1.18.2-5.0.9.0")
}
这里使用了 compileOnly,目标是联动 curios,你也可以使用 implementation 保证运行测试时 curios 被加载(但多数情况下 implementation 用于必要前置模组)。
执行构建,gradle 会帮你下载好 curios 供你调用其 API。
配置 mods.toml(联动则无需)
当你希望 curios 作为你的必要或可选前置时,你需要配置 mods.toml。
比如你可以这样写:
[[dependencies.your_modid]]
modId="curios"
mandatory=false
versionRange="[5.0.9.0, 5.1.0.0)"
ordering="NONE"
side="BOTH"
此时 curios 成为了你的 mod 的可选前置。如果不安装,fml 不会报错,如果你的代码也是正确的,则相当于独立运行了你的 mod,没有任何联动内容被加载;如果安装了,但版本低于 5.0.9.0 或高于 5.1.0.0,则 fml 报错;如果安装且版本正确,则模组正常加载并与 Curios API 联动执行部分功能。
一、饰品栏相关
1.20 以前
1. 饰品栏的申请
Curios API 默认玩家没有任何饰品栏,如果你的 mod 注册了饰品,你需要将对应栏位申请下来,才能正常使用,否则玩家无法装备饰品。
饰品栏的申请方法很简单,需要订阅 InterModEnqueueEvent 事件,通过 InterModComms.sendTo 方法与 Curios API mod 交流。如:
private void enqueueIMC(final InterModEnqueueEvent event) {
if(ModList.get().isLoaded("curios")) {
InterModComms.sendTo("curios", SlotTypeMessage.REGISTER_TYPE, () -> SlotTypePreset.HEAD.getMessageBuilder().build());
InterModComms.sendTo("curios", SlotTypeMessage.REGISTER_TYPE, () -> SlotTypePreset.NECKLACE.getMessageBuilder().build());
}
}
上述代码向 curios 申请了 Head(头部)与 Necklace(项链)两个饰品栏位。进入游戏后,在物品栏中点击 curios 图标,你将可以看到这两个栏位。
个人建议用到什么申请什么,尽量申请 SlotTypePreset 中有的栏位,如果真的没有合适的再自己创造。
2. 饰品栏的创造
如果你的 mod 单独创造了一个饰品栏,这意味着只有它和其附属 mod 与代码层面联动的 mod 可以使用。所以这个操作可能会引起兼容性问题,请尽量避免。
创造新的栏位,只需要在上述代码的基础上,将 SlotTypePreset.XXX.getMessageBuilder().build() 去掉,换成额外 build 出的 Message 即可:
private void enqueueIMC(final InterModEnqueueEvent event) {
if(ModList.get().isLoaded("curios")) {
InterModComms.sendTo("curios", SlotTypeMessage.REGISTER_TYPE, () -> new SlotTypeMessage.Builder("earings").ico(new ResourceLocation(MODID, "slot/earings")).priority(240).size(1).build());
}
}
此时注册了一个名为耳环的栏位,且图标位于 assets/<modid>/textures/slot/earings.png。
3. 饰品栏的修改
Curios API 还提供了一种通过 IMC 方法实现对应功能的操作,即修改一个饰品栏。当你希望修改其它 mod 定义的栏位或预设栏位时,你可以使用它。此时第三个参数的格式和前文相似,但你可以使用 Builder::size(int) 方法增加栏位可放置饰品的数量、Builder::lock()(相当于.size(0))方法锁定栏位、Builder::hide() 方法暂时隐藏栏位,以及 Builder::cosmetic() 方法为栏位添加装饰栏位。
用法与上述两个操作相同,在此不再赘述和举例。
1.20 及以后
Curios API 计划于 1.22 及之后的版本取消上述繁琐的利用 IMC 实现注册添加和修改的步骤,而是改用数据包的形式进行操作,使得整合包开发者、服务器管理员们无需了解模组开发即可自由操作 Curios API 的饰品栏成为了可能。当然,后文也介绍了如何允许任意物品被放入饰品栏中,同样是使用了数据包形式操作。
首先,在 data/<modid>/curios/slots 中新建 xxx.json,其中 xxx 为你的栏位名字,<modid>为你的模组/数据包的 id。
接着,修改 xxx.json。size 字段用于前文的饰品栏注册;若 xxx 并非任意预设的栏位名,则相当于你创建了一个新的饰品栏位——当然,其它模组如果创建了相同名字的栏位,则也可以使用它;而修改 schema 中的字段的值则可以实现前文的饰品栏修改(如果字段 replace 设为 true,则将替换掉其它数据包或预设的栏位设置,而非与其它设置相合并;否则,会按一定规则合并)。下表展示了这个 json 文件的 schema 与合并规则,请按照要求修改:
字段 | 字段类型 | 默认值 | 描述 | 合并规则 | 可空性 |
replace | boolean | false | 如果设为 true,则替换掉更低优先级的数据包中同一个栏位的设置 | - | 是 |
size | int | 1 | 饰品栏位的默认数量,与使用 IMC 注册栏位等价 | 最大值 | |
operation | enum["SET, "ADD", "REMOVE"] | SET | 与 size 配合使用。SET:设置栏位数量(最大值合并);ADD:增加栏位数量(求和合并);REMOVE:减少栏位数量(求和合并) | - | |
order | int | 1000 | 栏位在物品栏中的顺序值,越低的顺序值使该栏位排名靠前,越高则排名靠后。 | 最小值 | |
icon | String | curios:slot/empty_curios_slot | 饰品栏图标的资源 ID | 替换 | |
add_cosmetic | boolean | false | 槽位是否包含一个仅提供渲染不提供功能的美化槽 | 或 | |
use_native_gui | boolean | true | 槽位是否在 Curios 的 GUI 中显示 | 与 | |
render_toggle | boolean | true | 槽位的物品是否渲染(渲染部分参见本文第三章) | 与 |
二、物品相关
首先,任何物品都可以被注册为饰品,不过具体细节需要说明。
1. 物品类
该物品必须实现 ICurioItem 接口,这一接口没有提供任何抽象函数,无须重写。但倘若你需要具体实现物品佩戴在身上后玩家可以获得的增益,则需要重写一些函数。如 onEquip 函数代表玩家将饰品佩戴在饰品栏上时触发的事件、onUnequip 函数则代表玩家将饰品从饰品栏中摘下时触发的事件、curioTick 则是玩家佩戴饰品时每一个 tick 执行的操作等,此外还支持装备声音、装备前的判断等等函数的重写。
下面的代码则实现了一个对装备饰品的实体在每个tick上执行固定逻辑的操作的抽象类:
public abstract class CuriosItem extends Item implements ICurioItem {
public CuriosItem(Properties properties) {
super(properties);
}
@Override
public void curioTick(SlotContext slotContext, ItemStack stack) {
this.equipmentTick(slotContext.entity());
}
protected abstract void equipmentTick(LivingEntity livingEntity);
}
实现 equipmentTick 函数则可以完成其功能,如:
@Override
protected void equipmentTick(LivingEntity livingEntity) {
livingEntity.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 20, 0, false, false, true));
}
上述代码为佩戴该饰品的生物提供了永久夜视的效果。
当然你甚至可以通过 livingEntity.level.getEntities().forEach 实现杀戮光环。
2. 栏位绑定
利用前文方法申请栏位后,不管栏位是预设的还是你自己创造的,都可以通过下述方式极快地实现饰品的栏位绑定。
你需要在 data/curios/tags/items 中对你申请的每个栏位分别创建一个 xxx.json 的 tag。其中 xxx 为你的栏位名字,预设的名字即是大写枚举名对应的小写,如 necklace.json 中定义了下述两种项链:
{
"values": [
"metal_necklaces:uranium_necklace",
"metal_necklaces:thorium_necklace"
]
}
分别是铀项链和钍项链。身在辐中不知辐
这样,对应 id 的 item 可通过 tag 被绑定在对应的栏位。
3. 如果 Curios 是你的联动或可选前置
由于 Forge 注册 Item 时用了 Supplier,所以你可以动态判断 curios 是否被加载,根据加载与否实例化不同的 Item 类,如:
public static final RegistryObject<Item> PEARL_BRACELET = REGISTER.register(
"pearl_bracelet", () -> makeCuriosItem(
ITEM_GROUP, livingEntity -> livingEntity.addEffect(new MobEffectInstance(MobEffects.DAMAGE_BOOST, 10, 0, false, false, true))
)
);
public static Item makeCuriosItem(CreativeModeTab tab, Consumer<LivingEntity> effect) {
if(modList.isLoaded("curios")) {
return CuriosItemFactory.make(new Item.Properties().stacksTo(1).tab(tab), effect);
}
return new Item(new Item.Properties().stacksTo(1).tab(tab));
}
而 CuriosItemFactory 类则可以这样写:
public final class CuriosItemFactory {
public static Item make(Item.Properties props, Consumer<LivingEntity> effect) {
return new CuriosItem(props) {
@Override
protected void equipmentTick(LivingEntity livingEntity) {
effect.accept(livingEntity);
}
};
}
}
通过这两层封装,结合 Java 的类懒惰加载方式,则可以实现注册同一个 Item 时根据 curios 加载与否采取不同的逻辑。
如果不这样做,curios 没有运行时,你的 mod 也会报错,并提示你无法找到类。所以这也是 Forge 模组开发的常用联动技巧。
三、渲染
非常反直觉的是,Curios API 并没有为用户简化代码而提供饰品渲染的接口。不过 Curios 提供了装备饰品后调用渲染器的注册接口,即 CuriosRendererRegistry::register。因此你可以在订阅 FMLClientSetupEvent 的函数中通过如下方式注册渲染器:
@SubscribeEvent
public static void onClientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> {
if(ModList.get().isLoaded("curios")) {
CuriosRenderers.registerRenderers();
}
});
}
CuriosRenderers 类中:
public static void registerRenderers() {
CuriosRendererRegistry.register(MNItems.URANIUM_NECKLACE.get(), NecklaceRenderer::new);
CuriosRendererRegistry.register(MNItems.THORIUM_NECKLACE.get(), NecklaceRenderer::new);
}
而 NecklaceRenderer 可以通过类似于盔甲的方式渲染项链:
public class NecklaceRenderer implements ICurioRenderer {
public static final ModelLayerLocation NECKLACE = new ModelLayerLocation(new ResourceLocation(MODID, "necklace"), "main");
private final HumanoidModel<LivingEntity> model;
public NecklaceRenderer() {
this.model = new HumanoidModel<>(Minecraft.getInstance().getEntityModels().bakeLayer(NECKLACE));
}
@Override
public <T extends LivingEntity, M extends EntityModel<T>> void render(ItemStack stack, SlotContext slotContext, PoseStack matrixStack, RenderLayerParent<T, M> renderLayerParent,
MultiBufferSource renderTypeBuffer, int light, float limbSwing, float limbSwingAmount,
float partialTicks, float ageInTicks, float netHeadYaw, float headPitch) {
if(stack.getItem() instanceof CuriosItem curiosItem) {
ICurioRenderer.followBodyRotations(slotContext.entity(), this.model);
VertexConsumer vertexConsumer = renderTypeBuffer.getBuffer(RenderType.entityCutout(new ResourceLocation(
MODID, "textures/model/curios/" + ForgeRegistries.ITEMS.getKey(curiosItem).getPath() + ".png"
)));
this.model.renderToBuffer(matrixStack, vertexConsumer, light, OverlayTexture.NO_OVERLAY, 1.0F, 1.0F, 1.0F, 1.0F);
}
}
}
那么你可以在assets/<modid>/textures/model/curios/xxx.png里放置项链的模型贴图,其中xxx是物品名,而贴图对应的模型必须是像Zombie和Player那样的模型。需要注意的是,NECKLACE和其它ModelLayerLocation一样,需要在订阅EntityRenderersEvent.RegisterLayerDefinitions事件的函数中通过event.registerLayerDefinition方法注册:
event.registerLayerDefinition(NecklaceRenderer.NECKLACE, () -> LayerDefinition.create(HumanoidModel.createMesh(new CubeDeformation(0.25F), 0.0F), 64, 64));
四、总结
以上则是笔者关于 Curios API 模组用法和教程的拙见,如有不完美或不详细之处,敬请指出。如有其他需要更新的内容,笔者也会尽快修改!