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的模块已经加载

  1. 作为MzLib的附属插件加载
  2. 或者将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);

strnull,则得到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则让它在主线程中运行

异步函数awaitSleepTicks实例也由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中富文本的基本单元,包括样式、颜色、hoverEventclickEvent,文本组件是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不是可翻译组件,keyargs均得到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;
    }
}