MzLib
模块介绍
MzLib 是一个充满魔法的 Minecraft 插件,主要包含以下三个模块:
MzLibCore
- 描述:MzLibCore 是 MzLib 的根基,所有与 Minecraft 无关的功能都在这里。
- 功能:主要是一些 Java 工具类和实用方法。
MzLibMinecraft
- 描述:MzLibMinecraft 包含 Minecraft 插件的主要代码,依赖于 MzLibCore。
- 功能:提供 Minecraft 相关的功能和工具。
MzLibDemo
- 描述:这里是一些示例代码,帮助开发者快速上手。
- 功能:演示如何使用 MzLib 的各个功能。
Powered by MzLib © 2023 BugCleanser, All rights reserved.
快速开始
这里是MzLibCore的基础教程
如果遇到问题,可以在QQ群中讨论:763416502
Hello World
MzLib的架构是模块化的,一个程序至少需要一个主模块
模块一般是单例
public class Main extends MzModule
{
public static Main instance = new Main();
@Override
public void onLoad()
{
System.out.println("Hello World!");
}
}
无论程序以哪种方式引导,你都需要在启动(启用)时加载主模块,结束(禁用)时卸载主模块。
例如作为JavaApplication
public class MyApplication
{
public static void main(String[] args)
{
Main.instance.load();
// do something
Main.instance.unload();
}
}
模块加载时,onLoad
方法会被调用,成功打印出HelloWorld
为了使用MzLib的基本工具,请确保MzLib的模块已经加载
- 作为MzLib的附属插件加载
- 或者将MzLib shade到你的程序中,然后手动load它
手动load MzLib(不推荐):
public class MyApplication
{
public static void main(String[] args)
{
MzLib.instance.load();
Main.instance.load();
// do something
Main.instance.unload();
MzLib.instance.unload();
}
}
Option
表示一个可空对象,类似J8+的Optional
,并且可与Optional
相互转换,旨在减少lambda的使用
命名和用法则更像Rust的Option
基本用法
实例化
Option<String> s = Option.some("Hello, world!");
Option<String> n = Option.none();
匹配
for(String str: s) // 若s非空,则执行该块代码,取其值str
{
System.out.println("some: "+str);
}
if(s.isNone()) // s为空时执行
{
System.out.println("none");
}
与可空对象转换
@Nullable String str = awa;
Option<String> opt = Option.fromNullable(str);
若str
为null
,则得到Option.none()
,否则得到Option.some(str)
String s1 = opt.unwrapOr("default"); // 若opt非空,则得到其值,否则得到"default"
String s2 = opt.toNullable(); // 等价于unwrapOr(null)
与Optional获得
Optional<String> opt = Optional.of("Hello, world!");
Option<String> op = Option.fromOptional(opt);
Optional<String> opt = op.toOptional();
从wrapper转换
有时wrapper包装的对象可空,使用需要isPresent()额外判断
为严谨和简便,将其包装为Option
WrapperObject wrapper = ...;
Option<WrapperObject> opt = Option.fromWrapper(wrapper);
当包装非空时,得到Option.some(wrapper)
;否则得到Option.none()
事件
注册事件类
创建一个自己的事件类,直接或间接继承Event,实现call方法,然后将类注册到你的模块中
call方法中不需要写任何代码,但它必须有方法体
public class MyEvent extends Event
{
// 实现call方法
@Override
public void call()
{
// 这里不用写任何代码
}
// 处理该事件的模块
public static class Module extends MzModule
{
// 模块实例,在其父模块中注册
public static Module instance=new Module();
@Override
public void onLoad()
{
// 注册事件类
this.register(MyEvent.class);
}
}
}
在合适的时候触发该事件即可
触发事件
若你创建了一个事件实例,调用call方法即可触发所有监听器
如果事件没有被取消,则执行事件
无论如何,最后都应该调用complete方法来结束事件
MyEvent event=new MyEvent();
// 触发监听器
event.call();
if(!event.isCancelled())
{
// 执行事件
}
// 完成事件
event.complete();
包装类
包装类是MzLib中非常常用的对象
因为MC的类在不同版本可能有所变化,所以我们必须给它包装一层,包装后你还可以越权访问它的成员
这里介绍包装类的基本用法
包装已知类
如果你可以直接访问一个类,可以使用@WrapClass
这里以ClassLoader为例
// 包装ClassLoader
@WrapClass(ClassLoader.class)
// 包装类必须是interface,并且是WrapperObject的子类
public interface WrapperClassLoader extends WrapperObject
{
/**
* 包装器的构造器,需要注解@WrapperCreator用于优化性能
* 一般直接复制粘贴,然后替换1处和2处为自己的包装类,将3处替换为目标类或任意其已知父类(如Object)
*/
@WrapperCreator
static WrapperClassLoader/*1*/ create(ClassLoader/*3*/ wrapped)
{
return WrapperObject.create(WrapperClassLoader/*2*/.class, wrapped);
}
/**
* findClass是非public方法,因此对齐包装
* @WrapMethod("findClass")表示目标方法的名称是findClass
* 包装方法的返回值类型必须和目标方法的一致,或者是其封装类
* 没必要写throws
*/
@WrapMethod("findClass")
Class<?> findClass(String name) throws ClassNotFoundException;
}
若目标类的类名固定且已知,但代码中无法直接访问,可以使用@WrapClassForName代替@WrapClass
@WrapClassForName("java.lang.ClassLoader")
使用包装类
如果有一个目标类的实例,可以使用create将其包装为包装类实例,从而访问其成员
// 目标类实例
ClassLoader cl = this.getClass().getClassLoader();
// 创建包装类实例
WrapperClassLoader wcl = WrapperClassLoader.create(cl);
// 调用包装方法或访问字段
wcl.findClass("java.lang.String");
拓展包装类
包装类可以被继承,当你需要包装它目标类的子类,或者你单纯想要拓展包装类的功能
// 要包装的类和WrapperClassLoader的相同
@WrapSameClass(WrapperClassLoader.class)
// 继承WrapperClassLoader,也可以先显式继承WrapperObject
public interface ExtendedWrapperClassLoader extends WrapperObject, WrapperClassLoader
{
/**
* 复制静态方法creator
* 记得将1处和2处替换为ExtendedWrapperClassLoader
*/
@WrapperCreator
static ExtendedWrapperClassLoader/*1*/ create(ClassLoader/*3*/ wrapped)
{
return WrapperObject.create(ExtendedWrapperClassLoader/*2*/.class, wrapped);
}
/**
* 这时候可以封装更多方法
*/
@WrapMethod("resolveClass")
void resolveClass(Class<?> c);
}
如果你已有父封装类的实例,你当然可以拿到目标类的实例然后使用拓展封装类重新封装,从而调用拓展封装类中的方法
ClassLoader cl = this.getClass().getClassLoader();
WrapperClassLoader wcl = WrapperClassLoader.create(cl);
// getWrapped得到被包装的对象,然后使用拓展包装类的create重新封装
ExtendedWrapperClassLoader ewcl = ExtendedWrapperClassLoader.create(wcl.getWrapped());
// 调用拓展封装类中的方法
ewcl.resolveClass(String.class);
我们一般简化为castTo方法,参数是包装类的create方法引用,而不是包装类的Class实例
ClassLoader cl = this.getClass().getClassLoader();
WrapperClassLoader wcl = WrapperClassLoader.create(cl);
// castTo将包装对象wcl转换为另一个包装类的对象,请勿使用强制转换
ExtendedWrapperClassLoader ewcl = wcl.castTo(ExtendedWrapperClassLoader::create);
ewcl.resolveClass(String.class);
包装字段访问器
显然由于我们的包装类是interface无法创建字段,所以我们将字段封装为getter和setter(也可以只封装其中一个)
使用@WrapFieldAccessor,若你的方法没有参数,代表这是一个getter,否则代表setter,setter的返回值应该为void
@WrapSameClass(WrapperClassLoader.class)
public interface ExtendedWrapperClassLoader extends WrapperObject, WrapperClassLoader
{
@WrapperCreator
static ExtendedWrapperClassLoader create(ClassLoader wrapped)
{
return WrapperObject.create(ExtendedWrapperClassLoader.class, wrapped);
}
// 包装parent字段的getter和setter
@WrapFieldAccessor("parent")
void setParent(ClassLoader parent);
// 返回值上的ClassLoader换成它的包装类则会自动进行包装,getter的参数也可以这样
@WrapFieldAccessor("parent")
ExtendedWrapperClassLoader getParent();
}
// 设置一个ClassLoader的parent的parent
ExtendedWrapperClassLoader.create(this.getClass().getClassLoader()) // 包装ClassLoader
.getParent() // 这样仍然得到一个包装过的ClassLoader
.setParent(null);
包装构造器
包装构造器使用@WrapConstructor注解,返回值必须是当前包装类,构造的实例会自动包装
// 简单包装个Object类
@WrapClass(Object.class)
public interface ExampleWrapper extends WrapperObject
{
// 记得复制creator
@WrapperCreator
static ExampleWrapper create(Object wrapped)
{
return WrapperObject.create(ExampleWrapper.class, wrapped);
}
// 包装Object的无参构造器
@WrapConstructor
ExampleWrapper staticNewInstance();
}
对了,你包装的方法必须是非静态的,这样我们才能继承和实现它,一般包装的构造器叫做staticNewInstance
static开头的命名表示它的目标是静态的(构造器我们看成静态方法,这里指的不是
作为包装类非静态方法,想调用它显然需要一个包装类实例,这样我们可以用create(null)直接创建一个,表示目标实例是null,因为我们调用目标类的静态方法所以不需要目标类的实例
为方便使用,我们可以再把它封装成静态方法
// 然后我们自己封装成静态方法
static ExampleWrapper newInstance()
{
// 使用create(null)调用
return create(null).staticNewInstance();
}
// 先用注解包装成非静态方法
@WrapConstructor
ExampleWrapper staticNewInstance();
构造器如此,静态方法和静态字段的访问器也是同理
Compound类
Compound类是基于包装类的,请确保你已经学习了使用和创建包装类
Compound类的主要作用是继承包装类和多继承
通过这种方法继承包装类你可以得到继承类的包装
假设类a有包装类A,而a是你不能直接访问的,但你想要构造一个a的子类b,这时候你就可以用Compound创建一个A的子类,也就是b的包装类B
这个类本质上还是一个包装类,你至少继承一个WrapperObject
Compound类的子类也必须是Compound类
a的这个子类b会原封不动地继承a的所有构造器(并调用父类构造器),因此你只需要像封装a的构造器那样封装b的构造器
覆写父类的方法必须带上@CompoundOverride注解,如果要调用父方法,则需先用@CompoundSuper封装父方法
详见代码中注释:
继承包装类
// 必须加上这个注解
@Compound
public interface WindowSlotButton extends WindowSlot // 直接继承即可
{
// 作为一个WrapperObject的子类,应有creator
@WrapperCreator
static WindowSlotButton create(Object wrapped)
{
return WrapperObject.create(WindowSlotButton.class, wrapped);
}
/**
* 构造器会直接原封不同继承
* 对其包装即可
*/
@WrapConstructor
WindowSlot staticNewInstance(Inventory inventory, int index, int x, int y);
static WindowSlot newInstance(Inventory inventory, int index)
{
return create(null).staticNewInstance(inventory, index, 0, 0);
}
/**
* 继承一个方法需要加@CompoundOverride注解
* parent表示这个方法所在的包装类
* method表示对应的包装方法
* 参数和返回值类型必须与被继承方法完全一致
*/
@Override
@CompoundOverride(parent=WindowSlot.class, method="canPlace")
default boolean canPlace(ItemStack itemStack)
{
return false;
}
/**
* 封装父方法,签名也必须一致
*/
@CompoundSuper(parent=WindowSlot.class, method="canTake")
boolean superCanTake(AbstractEntityPlayer player);
@Override
@CompoundOverride(parent=WindowSlot.class, method="canTake")
default boolean canTake(AbstractEntityPlayer player)
{
// 这样就可以调用父方法了
return superCanTake(player);
}
}
多继承和属性
作为interface,包装类天然支持多继承
为了让它像class一样好用,我们为其添加属性
类似于字段,但你只需要写他的getter和setter,并加上注解
注解的参数为属性名,相同的名称代表同一个属性
@Compound
public interface ExampleCompound extends WrapperObject
{
WrapperFactory<ExampleCompound> FACTORY = WrapperFactory.find(ExampleCompound.class);
@Deprecated
@WrapperCreator
static ExampleCompound create(Object wrapped)
{
return WrapperObject.create(ExampleCompound.class, wrapped);
}
/**
* 封装构造器,用不到的话也可以子类再封装
*/
static ExampleCompound newInstance()
{
return create(null).staticNewInstance();
}
@WrapConstructor
ExampleCompound staticNewInstance();
@PropAccessor("p1")
int getP1();
@PropAccessor("p1")
void setP1(int value);
}
异步函数
也许你常有这样的烦恼(实则不然):你需要在主线程上处理一些事物,但是其中你想进行一些延迟,显然你不能睡在主线程上
设置定时任务
最常见的方式就是创建一个计划任务了,服务端实例可以代表主线程的调度器
MinecraftServer.instance.schedule(() ->
{
System.out.println("Hello World");
}, new SleepTicks(20));
这将会在主线程的20个ticks后打印Hello World
其中SleepTicks
的实例是被MinecraftServer#schedule
处理的,实现了任务的创建
创建异步函数
你显然不太满意计划任务,因为它容易是你陷入回调地狱,因此我们使用嵌入控制流的延迟方法
首先你需要创建一个类并继承AsyncFunction<R>
,其中R是返回值类型。这个类就表示一个异步函数,我们暂时先返回Void
public class MyAsyncFunction extends AsyncFunction<Void>
{
// 实现template,异步函数的逻辑写在这里
@Override
public Void template()
{
// 循环5次
for(int i=0; i<5; i++)
{
// 打印Hello World
System.out.println("Hello World");
// “调用” await,延迟20个ticks
await(new SleepTicks(20));
}
return null;
}
// 实现run方法,方法体留空
@Override
public void run()
{
}
}
这里的await是异步函数让步的一个标记,并不是真正调用了这个方法
启动异步函数
调用异步函数后,你可以认为它启动了一个协程,因此我们先构造它,然后调用start方法来启动
new MyAsyncFunction().start(MinecraftServer.instance);
start
方法的参数就是这个异步函数的runner,MinecraftServer.instance
则让它在主线程中运行
异步函数await
的SleepTicks
实例也由MinecraftServer.instance
处理
异步函数启动后start
方法会返回一个CompletableFuture<R>
实例,当异步函数返回时它被完成
等待CompletableFuture
在异步函数中,你可以等待一个CompletableFuture的完成
例如,你可以启动另一个异步函数并等待
public class MyAsyncFunction2 extends AsyncFunction<Void>
{
// 实现template,异步函数的逻辑写在这里
@Override
public Void template()
{
System.out.println("Hello CompletableFuture");
// 使用相同的runner启动MyAsyncFunction,得到其CompletableFuture
CompletableFuture<Void> cf = new MyAsyncFunction().start(this.getRunner());
// 使用await0等待它完成
await0(cf);
System.out.println("Hello CompletableFuture");
return null;
}
// 实现run方法,方法体留空
@Override
public void run()
{
}
}
在匿名内部类中使用
你也许想到:作为函数,异步函数应当可以有参数,但这样你需要创建构造器传入参数到字段,然后在template
中使用
将异步函数写成匿名内部类并封装成方法可能会简化许多
public static AsyncFunction<Void> newMyAsyncFunction(int i)
{
return new AsyncFunction<Void>()
{
public Void template()
{
System.out.println("Hello World " + i);
}
public void run()
{
}
};
}
通常情况下,如果你需要一个固定的runner,那你可以直接调用start
再返回CompletableFuture
public static CompletableFuture<Void> startMyAsyncFunction(int i)
{
return new AsyncFunction<Void>()
{
public Void template()
{
await(new SleepTicks(20));
System.out.println("Hello World " + i);
}
public void run()
{
}
}.start(MinecraftServer.instance);
}
快速开始(Minecraft)
开始之前,您需要先学习MzLibCore的基本用法
建议:
- 掌握一定的插件或mod的开发基础
- 了解BukkitAPI
MzLib现在正处于早期开发阶段,你可能需要配合BukkitAPI来完成功能,见配合Bukkit使用
基本结构与约定
我们的设计哲学与Fabric类似,但我们要支持热加卸载和多版本兼容
这几乎依赖于我们的模块化和包装器
模块化
你的插件应该由若干个模块构成,其它东西几乎都应该被注册到模块中,模块卸载时会自动将它们注销
注册是一个耗时操作,一般只在模块加载时注册其它对象
在下一章你将学会创建和加载模块
包装器
包装器是指WrapperObject的子类,是对Minecraft类的基本封装
相比被它包装的类,它是一个interface,因此它没有JVM概念的构造器,取而代之的是静态方法newInstance
例如你要新建一个ItemStack实例
ItemStack is = ItemStack.newInstance(Identifier.newInstance("minecraft:diamond"));
若要取得被包装的Minecraft的对象,使用getWrapped方法
Object nms = is.getWrapped();
不过你一般用不到这样,只应使用包装器来操作这些Minecraft对象。
若你通过某种方法得到了Minecraft的对象,通过它创建一个包装器实例,使用包装器的create方法
Entity entity = Entity.create(nmsEntity);
值得注意的是,wrapper不应该使用JVM概念的instanceof和强制转换,因为Wrapper.create返回的永远只是Wrapper的实例
取而代之的是wrapper.isInstanceOf和wrapper.castTo,参数是另一个包装器的create方法引用,例如
if(entity.isInstanceOf(EntityPlayer::create))
{
EntityPlayer player = entity.castTo(EntityPlayer::create);
// do something
}
有关如何创建一个包装器类型,见包装器的进阶教程
版本表示和名称约定
为了简单起见,我们使用一个整数表示一个MC版本,即第二位版本号乘100加上第三位版本号
若MC版本1.x.y,用整数表示为x*100+y
例如1.12.2表示为1202,1.14表示为1400
如果一个元素只在特定的版本段生效,则在标识符后加上v版本段,若有多个版本段,使用两个下划线隔开
一个版本段是一个左开右闭区间[a,b),表示为a_b;[a,+∞)表示为a,(+∞,b)表示为_b
例如,exampleV1300表示这个元素从1.13开始有效;exampleV_1400表示在1.14之前有效;exampleV1300_1400表示从1.13开始有效,从1.14开始失效
例如,exampleV_1300__1400_1600表示在(-∞, 1.13) ∪ [1.14, 1.16)有效;exampleV_1600__1903表示1.16之前和从1.19.3开始都有效,在[1.16, 1.19.3)无效
创建插件和模块
让我们创建一个Bukkit插件
在项目资源中添加plugin.yml,并创建一个Bukkit的入口(JavaPlugin子类)
name: MzLibDemo
version: 0.1
authors: [ mz ]
main: mz.mzlib.demo.DemoBukkit
depend: [ MzLib ]
api-version: 1.13
现在开始对接MzLib
创建主模块
public class Demo extends MzModule
{
public static String MOD_ID = "mzlibdemo";
public static Demo instance = new Demo();
@Override
public void onLoad()
{
// 加载子模块和其它对象(如果有的话)
this.register(DemoSubmodule.instance);
}
}
从Bukkit加载主模块
主模块需要被手动加载和卸载,你有两种方法来实现
法一:调用load与unload
public class DemoPlugin extends JavaPlugin
{
@Override
public void onEnable()
{
Demo.instance.load();
}
@Override
public void onDisable()
{
Demo.instance.unload();
}
}
法二:将其注册到MzLib并手动将其注销
在这种情况下,MzLib作为它的父模块,MzLib卸载时它也会被一起卸载
public class DemoPlugin extends JavaPlugin
{
@Override
public void onEnable()
{
MzLib.instance.register(Demo.instance);
}
@Override
public void onDisable()
{
MzLib.instance.unregister(Demo.instance);
}
}
创建简单命令
这是MzLib命令系统的入门教程
一个命令应该是mz.mzlib.minecraft.command.Command的实例
它的基本设置方法都会返回自身,因此我们可以用链式构造它,但下面的教程中我们会分步创建
创建Command实例
直接调用构造器,第一个参数是命令的名称,后面的参数是它的别名(可选)
Command command = new Command("mzlibdemo", "mzd");
设置命名空间
命令可以以两种名称调用以防重名,/name 和 /namespace:name
其中name是你的命令名,namespace是命令的命名空间,默认是minecraft,一般设置成你的插件名(MOD_ID)
command.setNamespace(Demo.MOD_ID);
设置命令处理器
你当然需要规定如何处理你的命令,使用setHandler
command.setHandler(context->
{
if(!context.successful)
return;
if(context.doExecute)
{
// do sth. on execute
context.source.sendMessage(Text.literal("Hello World!"));
}
});
其中context是一个CommandContext的实例
我们先判断context.successful,它代表命令是否被成功解析
然后我们判断context.doExecute,它表示命令是否应该被执行(否则只是需要补全命令)
示例中我们发送一条Hello World给命令发送者
注册命令
一般地,我们在一个模块中注册这个命令,不需要手动注销
public class Demo extends MzModule
{
public static Demo instance = new Demo();
public Command command;
@Override
public void onLoad()
{
this.register(this.command=new Command("demo", "d").setHandler(context->{/* ... */}));
}
}
添加子命令
使用Command#addChild添加子命令,你可以在父命令创建时直接添加
如果子命令足够复杂,你也可以单独为它创建一个模块,并在onLoad中添加到父命令,并在onUnload中从父命令移除
public class DemoSubcommand extends MzModule
{
public static DemoSubcommand instance = new DemoSubcommand();
public Command command;
@Override
public void onLoad()
{
Demo.instance.command.addChild(this.command=new Command("sub").setHandler(context->{/* ... */}));
}
@Override
public void onUnload()
{
Demo.instance.command.removeChild(this.command);
}
}
然后在父命令注册后注册这个模块
设置命令权限检查器
使用setPermissionChecker方法设置权限检查器
检查命令源的权限,如果权限不足,返回一个Text类型的提示,否则返回null
可以使用预设的静态方法Command#checkPermission,也可以使用Command#permissionChecker直接构造这个检查器
记得注册你的权限到模块中
public class Demo extends MzModule
{
public static Demo instance = new Demo();
public String MOD_ID="mzlibdemo";
public Permission permission=new Permission(this.MOD_ID+".command.demo");
public Command command;
@Override
public void onLoad()
{
// 注册权限
this.register(this.permission);
// 注册命令
this.register(this.command=new Command("demo").setNamespace(this.MOD_ID).setPermissionChecker(Command.permissionChecker(this.permission)));
}
}
可以使用静态方法Command#checkPermissionSenderPlayer要求发送者必须是一个玩家
可以使用setPermissionCheckers方法设置若干个权限检查器,通过全部检查器的发送者才能执行命令
command.setPermissionCheckers(Command::checkPermissionSenderPlayer, Command.permissionChecker(this.permission));
进阶
命令系统的进阶用法参见进阶教程
监听事件
对于一个已注册的事件,你可以直接在模块上创建监听器实例并注册
例如假设我们要监听所有EventEntity的实例
// in your module
@Override
public void onLoad()
{
this.register(new EventListener<>(EventEntity.class, e->
{
System.out.println("Event is called: "+e.getClass());
}));
}
配置文件
创建和加载
类似Bukkit的saveDefaultConfig,我们也可以自动保存和加载配置
首先在项目的资源文件中添加配置,我们使用json
resources
├── config.json
└── plugin.yml
然后你可以在你的主模块加载时加载这个配置
public class Demo extends MzModule
{
public static Demo instance = new Demo();
public File dataFolder;
@Override
public void onLoad()
{
try
{
this.config = Config.load(Objects.requireNonNull(this.getClass().getResourceAsStream("/config.json")), new File(this.dataFolder, "config.json"));
}
catch(Throwable e)
{
throw RuntimeUtil.sneakilyThrow(e);
}
}
}
记得提前给dataFolder赋值
方法Config.load的第一个参数是你默认配置的InputStream,直接从jar的ClassLoader中获得
第二个参数就是配置保存的位置,没有会自动生成,用户可以修改它
你可以为插件添加一个reload命令来重新执行这行Config.load
读取配置
假设你的配置文件结构如下
{
"test": "Demo",
"a":
{
"b": "c"
}
}
欲访问其中test字段,只需使用
this.config.getString("test");
// 或
this.config.getString("test", "default");
欲得到字段a的值(一个JsonObject),只需使用
this.config.get("a").getAsJsonObject();
你也可以直接得到a中b的值
this.config.getString("a.b");
配合BukkitAPI使用
目前MzLib的功能并不完善,有时你可能需要监听Bukkit的事件
或者,使用其它插件的API时,你也需要用到Bukkit的对象
转换物品堆
通过BukkitItemUtil将Bukkit和MzLib的ItemStack相互转换
org.bukkit.inventory.ItemStack bukkitItemStack = BukkitItemUtil.toBukkit(itemStack);
ItemStack itemStack = BukkitItemUtil.fromBukkit(bukkitItemStack);
转换实体
通过BukkitEntityUtil将Bukkit和MzLib的实体相互转换
org.bukkit.entity.Player bukkitPlayer = (org.bukkit.entity.Player) BukkitEntityUtil.toBukkit(player);
// 注意:包装类不要使用强转
EntityPlayer player = BukkitEntityUtil.fromBukkit(bukkitPlayer).castTo(EntityPlayer::create);
文本组件
文本组件是MC中富文本的基本单元,包括样式、颜色、hoverEvent
和clickEvent
,文本组件是Text
的实例可以嵌套
基本类型
literal
字面值组件是最常见的组件,包含一个字符串作为其显示内容
创建字面值组件:
Text t1 = Text.literal("Hello, world!");
获取字面值组件的文本(字串):
String str = t1.getLiteral(); // 若t1不是字面值组件,得到null
translatable
可翻译组件包含一个翻译键和若干个参数,一般根据客户端的语言文件显示内容
创建可翻译组件:
Text t2 = Text.translatable("item.minecraft.egg");
Text t3 = Text.translatable("pack.nameAndSource", Text.literal("testName"), Text.literal("testSource"));
获取可翻译组件的翻译键和参数:
String key = t2.getTranslatableKey();
Text[] args = t2.getTranslatableArgs();
(若t2
不是可翻译组件,key
和args
均得到null
)
keybindV1200
按键绑定组件包含一个按键绑定(按键的本地化键名),显示为客户端设置的对应按键
这种组件从MC1.12开始可用
创建按键绑定组件:
Text t4 = Text.keybindV1200("key.jump");
得到按键绑定组件的键名:
String keyKey = t4.getKeybindV1200();
(至少MC1.12开始才能调用,若t4
不是按键绑定组件得到null
)
score
计分板组件,显示计分板中的一个值,不常用
开发中,暂不可用
selector
选择器组件,显示选择器选中的目标实体,不常用
颜色
TextColor的实例,若为null
则使用父组件的颜色或默认颜色
设置组件颜色:
Text t5 = Text.literal("Red text").setColor(TextColor.RED);
fromRgbV1600
从MC1.16开始可以使用RGB颜色
设置RGB颜色:
Text t6 = Text.literal("RGB text").setColor(TextColor.fromRgbV1600(0xABCDEF));
样式
每种样式用一个Boolean表示,若为null
则使用父组件的样式或默认样式
设置组件样式:
Text t7 = Text.literal("special text").setBold(true).setItalic(true).setUnderlined(true).setStrikethrough(true).setObfuscated(true);
hoverEvent
TODO
文档待完善
clickEvent
TODO
文档待完善
网络数据包
发包
发送一个数据包非常简单,只需调用EntityPlayer#sendPacket
收包
使得服务器认为收到了一个玩家的数据包并进行处理,调用EntityPlayer#receivePacket
收发包监听
对于一个Packet的子类,我们可以监听它实例的发送或接收
只需创建一个PacketListener实例并注册到你的模块中
监听器不一定在主线程上触发
例如我们需要监听PacketC2sChatMessage
// in your module
@Override
public void onLoad()
{
this.register(new PacketListener<>(PacketC2sChatMessage::create, packetEvent->
{
packetEvent.setCancelled(true); // 取消收包,如果你想这么做的话
}));
}
从数据包事件中获取和修改数据包
当数据包监听器被调用时,相关信息会被封装在数据包事件PacketEvent.Specialized<P>
其中包含了这个数据包,使用时调用packetEvent.getPacket()
但不要将这个结果储存,因为它随时可能会改变
如果需要修改发包的内容,你可能需要先确保这个数据包是副本(因为Mojang可能会把同一个数据包发送给不同玩家),通过packetEvent.ensureCopied()
this.register(new PacketListener<>(PacketS2cWindowSlotUpdate::create, packetEvent->
{
// 确保数据包是副本
packetEvent.ensureCopied();
// 修改数据包
packetEvent.getPacket().setItemStack(ItemStack.empty());
}));
同步监听
异步监听能进行的操作有限
有时处理收发包时你可能需要进行同步操作
这时调用sync方法,参数是你要执行的操作action
如果该监听器本身是在主线程中触发,则该action会立即完成
否则该数据包会被推迟到主线程下一个tick再继续处理
如果MC本身就要在主线程处理这个数据包(如PacketC2sCloseWindow),则调用并sync()不会导致更多延迟
this.register(new PacketListener<>(PacketC2sCloseWindow::create, packetEvent->
{
// 如果需要同步处理
packetEvent.sync(()->
{
// 如果需要则取消事件
packetEvent.setCancelled(true); // 取消事件
});
}));
窗口
在此之前,请确保你已经学习了Compound类
在Bukkit中,一个物品栏可以被玩家直接打开,但在原版中并非如此
窗口
玩家实际上打开的是一个窗口,你也可以管它叫物品栏界面,Mojang叫它菜单,Fabric(yarn)叫它屏幕,Spigot叫它容器
窗口是玩家可以访问的界面,玩家通过窗口操作物品栏
窗口中有若干个槽位(WindowSlot),槽位一般对应物品栏中的一个索引,你也可以自定义它的行为
创建窗口
例如我们可以创建一个箱子界面,也就是WindowChest的实例
Inventory inventory=InventorySimple.newInstance(9*5);
Window window=WindowChest.newInstance(UnionWindowType.GENERIC_9x5.typeV1400, syncId, player.getInventory(), inventory, 5);
WindowTypeV1400表示从1.14开始的窗口类型,可以通过UnionWindowType#typeV1400得到
其中UnionWindowType.GENERIC_9x5表示一个9*5的箱子界面
现在你发现你没有syncId来创建窗口,但你先别急
打开窗口
即使你创建了一个窗口,你也不能让玩家直接打开它,AbstractEntityPlayer#openWindow的参数是WindowFactory,你需要提供WindowFactory实例来创建这个窗口
窗口的标题由WindowFactory提供
虽然这个设计可能很愚蠢,但Mojang代码就是这样写的(
因此我们提供了一个WindowFactorySimple让你可以很容易创建WindowFactory的实例
注意:在1.14之前,窗口的类型id由WindowFactory提供;从1.14开始,窗口的类型由Window提供
Inventory inventory=InventorySimple.newInstance(9*5);
WindowFactory windowFactory=WindowFactorySimple.newInstance(UnionWindowType.GENERIC_9x5.typeV1400, Text.literal("标题"), (syncId, inventoryPlayer)->
{
// 在这里创建窗口
return WindowChest.newInstance(UnionWindowType.GENERIC_9x5.typeV1400, syncId, inventoryPlayer, inventory, 5);
});
// 让玩家打开WindowFactory
player.openWindow(windowFactory);
我们将箱子界面进行了简单封装,因此上面的代码可以简化为
Inventory inventory=InventorySimple.newInstance(9*5);
WindowFactory windowFactory=WindowFactorySimple.chest(Text.literal("标题"), inventory, 5, window->
{
// 可以对窗口进行设置
});
player.openWindow(windowFactory);
设置槽位
我们可以设置某个槽位的行为,它是WindowSlot的实例
我们先以WindowSlotButton为例,它的canPlace和canTake始终返回false
Inventory inventory=InventorySimple.newInstance(9*5);
// 设置0号物品
inventory.setItemStack(0, new ItemStackBuilder("minecraft:stick").build());
WindowFactory windowFactory=WindowFactorySimple.chest(Text.literal("标题"), inventory, 5, window->
{
// 设置0号槽位
window.setSlot(0, WindowSlotButton.newInstance(inventory, 0));
});
player.openWindow(windowFactory);
这样玩家就不能在0号槽位放入或拿出物品
自定义WindowSlot
你已经迫不及待想要实现自己的WindowSlot了,它是一个封装类,我们需要用Compound类继承
// 必须加上这个注解
@Compound
public interface WindowSlotButton extends WindowSlot
{
// 作为一个WrapperObject的子类,应有creator
@WrapperCreator
static WindowSlotButton create(Object wrapped)
{
return WrapperObject.create(WindowSlotButton.class, wrapped);
}
/**
* 构造器会直接原封不同继承
* 对其包装即可
*/
@WrapConstructor
WindowSlot staticNewInstance(Inventory inventory, int index, int x, int y);
static WindowSlot newInstance(Inventory inventory, int index)
{
return create(null).staticNewInstance(inventory, index, 0, 0);
}
/**
* 继承一个方法需要加@CompoundOverride注解
* parent表示这个方法所在的包装类
* method表示对应的包装方法
* 参数和返回值类型必须与被继承方法完全一致
*/
@Override
@CompoundOverride(parent=WindowSlot.class, method="canPlace")
default boolean canPlace(ItemStack itemStack)
{
return false;
}
@Override
@CompoundOverride(parent=WindowSlot.class, method="canTake")
default boolean canTake(AbstractEntityPlayer player)
{
return false;
}
}