前往我的主页以阅读完整内容,并获取源码
DearXuan的主页
MOD介绍
使用漏斗链进行分类或传递物品时,常常会发现漏斗速度太慢,难以收集全部掉落物.或者漏斗太多,影响性能.而现有的漏斗加速mod则是引入新的快速漏斗,存在各种兼容问题.开服时发现paper服务器可以修改原版漏斗速度,而因此萌生想法,编写一个可以修改原版漏斗速度的mod.
截至发布本文时,mod已拥有物品过滤功能,未来将会添加投掷器自动发射功能.
支持版本
本mod支持Minecraft 1.19.4
版本,需要Fabric
及Fabric-api
,以及前置mod: modmenu(≥6.2.1)
该mod仅限客户端使用.局域网联机时,仅房主需要安装,而其他成员无需安装,即使安装了也无法修改.
该MOD理论上适用于多个版本,可以在自己电脑上尝试修改版本号重新编译.
创建项目
本项目采用 IDEA 开发,在插件市场下载Minecraft Development,来快速初始化项目.
如果项目有大写字母,则创建完成后,需要前往src/main/resources/fabric.mod.json
中修改id
一项为小写,因为modid
不支持大写字母.
创建项目需要下载大量文件,需耐心等待.创建完成后,先彻底关闭IDEA,再重写打开项目.
开发思路
本mod的功能是修改原版漏斗代码,因此需要用到fabric
提供的Mixin功能,该功能可将自己的代码注入到游戏源码中,而无需对其进行修改.
此外modmenu
模组提供了可视化菜单,可以方便菜单制作,因而列为前置mod.实际上即使没有该前置也可以正常运行,但是无法打开菜单.
引入依赖
在build.gradle
中引入以下maven和依赖
repositories {
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
/**
* EasyHopper 依赖项
*/
maven { url "https://maven.shedaniel.me/" }
maven { url "https://maven.terraformersmc.com/releases/" }
maven { url "https://maven.architectury.dev/" }
/**
* EasyHopper 依赖项结束
*/
}
dependencies {
// To change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
/**
* EasyHopper 依赖项
*/
modApi("me.shedaniel.cloth:cloth-config-fabric:10.0.96") {
exclude(group: "net.fabricmc.fabric-api")
}
modApi "com.terraformersmc:modmenu:6.2.1"
/**
* EasyHopper 依赖项结束
*/
}
此时右上角会提示重写构建,等待一段时间即可.
反编译源码
展开Gradle
一栏,运行genSources
,即可进行反编译.所需时间视电脑配置而定.
如果找不到这一栏,则可以在项目根目录下执行以下命令.
./gradlew genSources
如果失败则重启IDEA,多尝试几次.
Mixin配置文件
创建Mixin配置文件src/main/resources/modid.mixin.json
,如图所示.注意要修改为你自己的modid
修改fabric.mod.json
,天上你的Mixin配置文件名称
"mixins": [
"modid.mixins.json"
]
注册Mixin
以我的项目为例,modid
为"EasyHopper",则在com.dearxuan.easyhopper
下创建目录mixin
,并在该目录下创建EasyHopperMixin.java
修改冷却时间
由于我们要修改漏斗函数,因此我们直接搜索漏斗的英文,简单查看后,可以发现控制漏斗事件的类为HopperBlockEntity.class
,因此我们注入这个类.
在EasyHopperMixin.java
中编写代码如下,注意修改为你的包名.
@Mixin(HopperBlockEntity.class)
public abstract class EasyHopperMixin extends LootableContainerBlockEntity {
}
其中@Mixin
注释用于标识我们要注入的类.此时IDEA会将这段代码标红,因为缺少了构造函数,可以使用IDEA来自动创建.
现在这个类已经被成功注入,但我们还不知道漏斗的控制逻辑,因此无法进行编码.接下来查看HopperBlockEntity.class
的源码(按住Ctrl+右键
可以快速跳转),因为该游戏的事件都是基于刻(tick)来处理的,因此我们搜索"tick",仅有一个函数与此相符.为了便于分析,此处额外展示了两个用到的函数.
public static void serverTick(World world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity) {
--blockEntity.transferCooldown;
blockEntity.lastTickTime = world.getTime();
if (!blockEntity.needsCooldown()) {
blockEntity.setTransferCooldown(0);
HopperBlockEntity.insertAndExtract(world, pos, state, blockEntity, () -> HopperBlockEntity.extract(world, blockEntity));
}
}
private boolean needsCooldown() {
return this.transferCooldown > 0;
}
private void setTransferCooldown(int transferCooldown) {
this.transferCooldown = transferCooldown;
}
于是我们可以猜测,该函数控制这漏斗何时输送物品,其中blockEntity.transferCooldown
是冷却时间,每个tick会减一,而needsCooldown()
函数根据blockEntity.transferCooldown
是否为正来判断漏斗是否处于冷却中,为零或负则是冷却完毕.
一旦冷却完毕,则执行blockEntity.setTransferCooldown(0)
,推测是用于防止出现负数的.而HopperBlockEntity.insertAndExtract()
函数显然是用来输入输出物品的.
因此我们只需要修改setTransferCooldown()
这个函数,即可实现修改冷却时间的功能.
@Mixin(HopperBlockEntity.class)
public abstract class EasyHopperMixin extends LootableContainerBlockEntity {
@Shadow
private int transferCooldown;
protected EasyHopperMixin(BlockEntityType<?> blockEntityType, BlockPos blockPos, BlockState blockState) {
super(blockEntityType, blockPos, blockState);
}
@Inject(
method = {"setTransferCooldown"},
at = {@At("HEAD")},
cancellable = true
)
private void EasyCooldown_head(int cooldown, CallbackInfo info){
if(cooldown > 0){
this.transferCooldown = cooldown - 8 + ModConfig.INSTANCE.TRANSFER_COOLDOWN;
info.cancel();
}
}
}
由于transferCooldown
是私有变量,无法直接访问,因此使用@Shadow
来映射.这将把原私有变量映射到你自己创建的类中,以便开发者进行修改.
你会发现我在函数参数里加了CallbackInfo info
一项,这是用来控制返回值的,我们需要利用它来实现中途退出.
现在开始修改setTransferCooldown()
函数,这里我们选择@Inject
注释,它可以将你的代码插入到原函数中,methed
指原函数名,at
指插入位置,为了屏蔽掉原函数,我们应该把代码插入到原函数头部,然后直接返回,以使后面部分失效.cancellable
指原函数能否中途退出,显然需要为true
.
下面代码中的cooldown - 8
是为了减去原有的冷却时间,从而改成我们需要的值,ModConfig.INSTANCE.TRANSFER_COOLDOWN
是指我们自己规定的冷却时间,该部分在配置文件中定义,会在接下来介绍.
最后的info.cancel()
指取消原函数执行,相当于在我们代码的尾部,原函数头部插入了return
,即屏取消了原函数的执行.
修改传输数量
上面已分析出HopperBlockEntity.insertAndExtract()
是用于执行输入输出操作,因此我们继续查看源码.
private static boolean insertAndExtract(World world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity, BooleanSupplier booleanSupplier) {
if (world.isClient) {
return false;
}
if (!blockEntity.needsCooldown() && state.get(HopperBlock.ENABLED).booleanValue()) {
boolean bl = false;
if (!blockEntity.isEmpty()) {
bl = HopperBlockEntity.insert(world, pos, state, blockEntity);
}
if (!blockEntity.isFull()) {
bl |= booleanSupplier.getAsBoolean();
}
if (bl) {
blockEntity.setTransferCooldown(8);
HopperBlockEntity.markDirty(world, pos, state);
return true;
}
}
return false;
}
先猜测代码含义,显然第一步是判断自己是客户端还是服务端,该函数仅在服务端执行.然后检查漏斗是否在冷却中或不可用.
boolean bl = false
则定义了一个布尔变量,用于判断该漏斗是否使用过,如果为true
,则已经使用,那么就需要blockEntity.setTransferCooldown(8)
来重新设置冷却时间.而blockEntity.isEmpty()
和blockEntity.isFull()
显然是用来检查容器中是否有物品的,即输入输出功能.因此我们只需要在这里加个循环即可实现任意数量物品的输入输出.
我们的思路仍然是在原函数头部插入自己的代码,注意到其中用到了四个private
函数,因此我们需要先提取出这四个函数
@Mixin(HopperBlockEntity.class)
interface IEasyHopperEntity {
@Invoker("needsCooldown")
public boolean Invoke_needsCooldown();
@Invoker("isFull")
public boolean Invoke_isFull();
@Invoker("setTransferCooldown")
public void Invoke_setTransferCooldown(int transferCooldown);
@Invoker("insert")
public static boolean Invoke_insert(World world, BlockPos pos, BlockState state, Inventory inventory){
return false;
};
}
@Invoker
允许你访问一个私有的方法,类似@Shadow
,它可以将原本不可见的方法映射为你新定义的方法.其中静态函数必须要有函数体,不过它永远也不会执行.
编写的函数如下
@Inject(
method = {"insertAndExtract"},
at = {@At("HEAD")},
cancellable = true
)
private static void Insert(
World world,
BlockPos pos,
BlockState state,
HopperBlockEntity blockEntity,
BooleanSupplier booleanSupplier,
CallbackInfoReturnable<Boolean> info){
if (world.isClient) {
info.setReturnValue(false);
}
if (!((IEasyHopperEntity) blockEntity).Invoke_needsCooldown() && state.get(HopperBlock.ENABLED).booleanValue()) {
boolean bl = false;
for(int i=0;i<ModConfig.INSTANCE.TRANSFER_OUTPUT_COUNT;i++){
if (!blockEntity.isEmpty()) {
bl = IEasyHopperEntity.Invoke_insert(world, pos, state, blockEntity);
}
}
for(int i=0;i<ModConfig.INSTANCE.TRANSFER_INPUT_COUNT;i++){
if (!((IEasyHopperEntity) blockEntity).Invoke_isFull()) {
bl |= booleanSupplier.getAsBoolean();
}
}
if (bl) {
((IEasyHopperEntity) blockEntity).Invoke_setTransferCooldown(8);
HopperBlockEntity.markDirty(world, pos, state);
info.setReturnValue(true);
}
}
info.setReturnValue(false);
}
由于原函数有返回值,因此末尾需要改用CallbackInfoReturnable<Boolean> info
,将原本的return true
改为info.setReturnValue(true)
,即可实现中途退出的功能.其他代码直接复制下来即可,部分私有函数需要转换成自己刚刚定义的接口来调用.
最后,我们在easyhopper.mixin.json
中注册上面的两个Mixin
{
"required": true,
"minVersion": "0.8",
"package": "com.dearxuan.easyhopper.mixin",
"compatibilityLevel": "JAVA_17",
"mixins": [
"EasyHopperMixin",
"IEasyHopperEntity"
],
"client": [
],
"injectors": {
"defaultRequire": 1
}
}
完整的EasyHopperMixin.java
代码可在我的主页查看
保存配置
创建Config
目录和其中的两个java文件,如下图所示
为ModConfig
编写如下代码
@Config(
name = "easyhopper"
)
public class ModConfig implements ConfigData {
@Excluded
public static ModConfig INSTANCE;
@Comment("控制漏斗输送物品冷却时间")
public int TRANSFER_COOLDOWN = 8;
@Comment("控制漏斗每次输入多少个物品")
public int TRANSFER_INPUT_COUNT = 1;
@Comment("控制漏斗每次输出多少个物品")
public int TRANSFER_OUTPUT_COUNT = 1;
public ModConfig(){
}
public static void init(){
AutoConfig.register(ModConfig.class, GsonConfigSerializer::new);
INSTANCE = (ModConfig) AutoConfig
.getConfigHolder(ModConfig.class)
.getConfig();
}
}
@Config
表面这个类是一个配置类,它将会被序列化后保存在游戏目录的config
文件夹下.
@Excluded
注释表面该字段不会被保存,@Comment
则是注释,在设置界面,当鼠标悬浮于某一项上方时显示.init()
函数用于注册和初始化这个类.在EasyHopper.java
中执行这个函数
public class EasyHopper implements ModInitializer {
@Override
public void onInitialize() {
ModConfig.init();
}
}
在ModMenu.java
中编写如下代码
package com.dearxuan.easyhopper.Config;
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
import com.terraformersmc.modmenu.api.ModMenuApi;
import me.shedaniel.autoconfig.AutoConfig;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
@Environment(EnvType.CLIENT)
public class ModMenu implements ModMenuApi {
@Override
public ConfigScreenFactory<?> getModConfigScreenFactory(){
return parent -> AutoConfig
.getConfigScreen(ModConfig.class, parent)
.get();
}
}
此代码将会为该配置类自动生成一个图形界面,便于用户可视化修改.只有安装了前置模组modmenu
才会看到设置按钮,同时许多模组也使用了该前置mod.即使不安装,也可以正常运行,但是修改配置较为麻烦,因此列入到前置模组名单,来强制用户安装.同时还要添加modmenu
的入口点.修改fabric.mod.json
如下
{
//***//
"entrypoints": {
"client": [
"com.dearxuan.easyhopper.client.EasyHopperClient"
],
"main": [
"com.dearxuan.easyhopper.EasyHopper"
],
"modmenu": [
"com.dearxuan.easyhopper.Config.ModMenu"
]
},
"mixins": [
"easyhopper.mixins.json"
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabric": "*",
"fabric-api": "*",
"minecraft": "${minecraft_version}",
"modmenu": ">=6.2.1"
}
}
至此mod开发完毕.