MNI函数
声明
如果我们想要声明一个函数的具体实现逻辑是由Java代码实现的,我们就需要将它声明为Native函数。Native函数的声明格式如下:
func 函数名(参数列表) = Java类名.函数名;例如我们刚刚例子中提到的print函数,它的声明是:
func print(i as text) = top.mcfpp.mni.System.print;它表示,print函数拥有一个int类型的参数,同时它的逻辑交给了top.mcfpp.lang.System类的print函数来实现。注意这里的类名需要是完全限定名,需要包含包名,否则编译器将无法找到这个类。
实现
System类被称为MNI实现类,print函数的具体逻辑就是在这个类中实现的。通过使用@MNIRegister注解,我们可以将一个Java函数注册为MNI实现函数。这个注解的源码如下,它的参数在注释中已经有详细的解释。
package top.mcfpp.annotations;
//import ...
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MNIRegister {
/**
* 只读参数。格式是类型+空格+参数名
*/
String[] readOnlyParams() default {};
/**
* 普通参数。格式是类型+空格+参数名
*/
String[] normalParams() default {};
/**
* 调用者类型。默认为void
*/
String caller() default "void";
/**
* 函数的返回类型。默认为void
*/
String returnType() default "void";
/**
* 是否重写了父类中的函数。默认为false
*/
boolean override() default false;
}而具体实现用的这个Java函数的参数顺序,遵循:只读参数 + 普通参数 + 调用者 + ValueWrapper<返回值类型>的原则。
我们回到刚刚的例子:
func print(a as any) = top.mcfpp.lang.System.print; //...
@MNIRegister(normalParams = {"any a"})
public static void print(@NotNull Var<?> value){
Function.Companion.addCommand("tellraw @a " + "\"" + value + "\"");
}
//...print函数有一个any类型的变量,因此注解中,我们使用normalParams = {"any a"}来声明这个参数。而在具体实现中,我们需要接受一个普通参数。在这里,我们用所有变量类的基类Var类,来表示任意类型的变量都可以被接收。
我们看一个稍微复杂一些的例子:
@MNIRegister(caller = "DataObject", returnType = "text", override = true)
public static void toText(DataTemplateObject caller, ValueWrapper<JsonTextConcrete> returnValue) throws IOException {
var l = new ListChatComponent();
if(caller instanceof DataTemplateObjectConcrete callerC){
l.getComponents().add(new PlainChatComponent(SNBTUtil.toSNBT(callerC.getValue())));
}else {
l.getComponents().add(new NBTChatComponent(caller.toNBTVar(), false, null));
}
returnValue.setValue(new JsonTextConcrete(l, "re"));
}这是toText函数,类似于java中的toString方法,旨在将任意类型转换为可以打印在聊天栏的原始Json文本。这个函数没有参数,调用者是DataObject,返回值是text类型。因此在Java方法的参数中,我们先写一个DataTemplateObject caller用于接收调用者,然后再写一个ValueWrapper<JsonTextConcrete> returnValue用于处理返回值。
TIP
Function.addCommand函数用于向当前正在编译的mcf函数的末尾添加一条命令
ValueWrapper是一个包装类,用于包装返回值。
package top.mcfpp.util
class ValueWrapper<T>(var value: T)使用getValue和setValue来修改其中的值。
xxxConcrete这种命名的类表示是xxx类型变量的编译器可追踪版本,就是编译器知道这个变量里面的值是什么。在标准库的实现中随处可见这种分类处理,为的是尽可能地优化性能。
注入
CompoundData类拥有成员方法injected(cls: Class<*>),用于向当前类型中注入来自类cls中的所有方法。
class MCFPPBaseType {
object Int: MCFPPType(arrayListOf(Any)){
override val instanceData by lazy {
CompoundData("int","mcfpp").apply {
this.commonType = Int
extends(Any.instanceData)
injectedBy(MCIntData::class.java)
}
}
override val concreteInstanceData by lazy {
CompoundData("int","mcfpp").apply {
this.commonType = Int
extends(Any.concreteInstanceData)
injectedBy(MCIntConcreteData::class.java)
}
}
}
}此外,你也可以在mcfpp代码中使用注解@From<类的完全限定名>,来向这个类或者数据模板中注入方法。
@From<top.mcfpp.mni.minecraft.AreaData>
data Area{
startX as int;
startY as int;
startZ as int;
endX as int;
endY as int;
endZ as int;
}调用
在MCFPP中,我们可以直接调用MNI函数,就像调用普通函数一样。例如:
print(5);但是MNI函数和普通函数一个非常重要的区别就是,MNI是在编译期执行的。在编译到此函数的时候,将会执行这个函数。也就是说,MNI函数的执行和逻辑语句无关,MNI函数也不会出现在数据包中。
例如:
if(b){
print(7);
}else{
print(8);
}编译器编译的时候是从上往下编译的,并且编译过程中不会在意逻辑语句。因此,无论b的值是什么,编译器始终都会先后遇到print(7)和print(8),并且执行这两个函数。因此,数据包中始终会有tellraw @a 7和tellraw @a 8这两条命令。
当然,由于if的两个分支会对应两个不同的函数,因此tellraw @a 7和tellraw @a 8会分别出现在两个函数中。但是若调用的MNI函数的实现不同,那么可能会出现更多意想不到的情况。因此在使用MNI函数的时候,应当额外注意这一点。