Skip to content

自定义函数

AchieveMaster 当前自定义函数目录是:

text
plugins/AchieveMaster/scripts/functions/
├─ yml/
├─ tqs/
├─ java/
└─ js/

CustomFunctionLoader 会在加载时自动创建这些目录,并按固定顺序扫描:

  1. yml/.yaml
  2. tqs/
  3. java/
  4. js/ 下的 .js / .av

四种函数形态总表

目录解释器适合场景和 AST 的关系
tqs/AST最推荐,和当前正式脚本同语义直接编译到 AST
java/Java 风格转译器想保留 Java 外观,但不做真正 javac 编译先翻译成 AST 再执行
yml/YML DSL偏配置化团队独立执行器,条件 / 计算桥接到 AST
js/AviatorScript需要 lambda、自定义表达式生态走 Aviator,不走 AST 主执行器

加载顺序与重名规则

这是当前最重要的运行规则之一。

CustomFunctionLoader.loadAll() 的加载顺序固定是:

  1. YML
  2. TQS
  3. Java
  4. JS / AV

FunctionRegistry.register(...) 的策略是:

  • 同名函数如果已经存在,后注册直接跳过
  • 不是“后者覆盖前者”,而是“前者保留,后者失效”
  • 别名冲突也会跳过,不会强行覆盖

这意味着如果四个目录里都写了同名函数,最终优先级就是:

  1. yml/
  2. tqs/
  3. java/
  4. js/

建议不要依赖这个优先级做设计,最稳的做法还是保证函数名全局唯一。

重载行为

执行重载时,当前实现会先清掉旧注册,再重新扫描目录:

  • CustomYML 来源函数先卸载
  • CustomTQS 来源函数先卸载
  • CustomJava 来源函数先卸载
  • js/ 自定义函数通过 JsFunctionLoader.unloadAll() 清理

然后再重新加载四个目录。

所以修改函数文件后,正常做法仍然是重载插件或触发函数重载流程。

TQS 自定义函数

这是当前最贴近正式 AST 主路径的一种方式。

基本写法

text
导入类 Arrays = java.util.Arrays

定义函数 我的函数(前缀: 文本 = "v") 返回 文本:
分类: 示例
描述: 一个最小示例
别名: myAlias
尝试 {
    赋值 numbers = Arrays.asList(1, 2, 3)
    返回 前缀 + "-" + numbers.size()
} 捕获 (Exception ex) {
    返回 ex.class.getSimpleName()
}
结束函数

函数头当前正式模式是:

text
定义函数 函数名(参数列表) 返回 类型:

其中:

  • 返回 类型 可以省略,默认按 Object 处理
  • 结束标记必须写 结束函数

当前支持的函数头能力

能力说明
参数名直接写在括号里
参数类型参数: 类型
默认值参数: 类型 = 默认值
返回类型返回 文本返回 Number
分类分类:
描述描述:
别名别名:,支持逗号列表

导入写法

TQS 函数当前支持两种导入形式:

text
导入类 HashMap = java.util.HashMap
导入 java.util.ArrayList

第二种会自动用简单类名当别名,也就是上例等价于把 ArrayList 当别名导入。

默认参数值的真实边界

TqsFunctionLoader 当前只会把默认值按字面量解析成这些类型:

  • 字符串
  • true / false
  • null
  • 整数
  • 小数

也就是说,默认值不是完整 AST 表达式求值。 像 player.name1 + 2 这类内容不会按运行时表达式去算。

运行时上下文

TQS 自定义函数最终由 AstLoadedFunction 执行。 调用时会先复制父级 ScriptContext,然后额外注入这些标准变量:

变量含义
ctx / 上下文当前执行上下文
args / 参数参数数组
arg0 / arg1 ...位置参数
参数1 / 参数2 ...中文位置参数
plugin / 插件插件实例
logger日志对象
server / 服务器Bukkit Server
player / 玩家当前玩家
event / 事件当前事件
entity / 实体当前实体
item / 物品当前物品

同时,函数声明里的命名参数也会被写入上下文变量表。

类型收敛

AstLoadedFunction 在绑定参数时,会按声明类型做一轮轻量转换。 当前明确支持的目标类型包括:

  • String / Text / 文本
  • Boolean / 布尔
  • Integer / Int / 整数
  • Long / 长整数
  • Float
  • Double / Number / 数字 / 小数

如果转换失败,会回退成原值,不会直接让整个函数注册失败。

Java 风格自定义函数

Java 风格加载器不是运行时 javac 动态编译。 它做的事情是:

  1. 解析 import
  2. 解析 @Function@Param
  3. 解析方法签名
  4. 把方法体翻译成 AST 可执行脚本
  5. 最终仍然交给 AstLoadedFunction 执行

所以它本质上是“Java 外观 DSL”,不是自由 Java。

最小示例

java
import java.util.Arrays;

@Function(
    name = "我的Java函数",
    category = "示例",
    description = "Java 风格自定义函数",
    returns = "String",
    aliases = {"myJavaAlias"}
)
@Param(name = "prefix", type = "文本", defaultValue = "j")
public String myFunc(String prefix) {
    try {
        Object numbers = Arrays.asList(1, 2, 3);
        return prefix + "-" + numbers.size();
    } catch (Exception ex) {
        return ex.getClass().getSimpleName();
    }
}

当前支持的声明元素

元素说明
import x.y.Class;导入类
@Function(...)函数元信息
@Param(...)参数元信息
public / static可以写,但不代表真正 Java 编译语义
throws ...方法签名里可出现,主要是被解析器跳过

@Function 当前读取哪些键

说明
name函数名,省略时回退方法名
category分类
description描述
returns文档返回类型
aliases别名数组

@Param 当前读取哪些键

说明
name参数名
type脚本文档类型
description参数描述
defaultValue默认值

当前翻译器能覆盖什么

Java 转译器当前明确支持这些控制结构:

  • if / else if / else
  • while
  • for
  • try / catch
  • 变量声明
  • 赋值
  • return
  • break
  • continue
  • 自增 / 自减
  • 普通表达式调用

当前明确不支持什么

当前源码里已经写死的一条边界是:

  • throw ... 语句不支持,遇到会直接报错

另外要注意:

  • 这不是完整 Java 语法
  • 不要写复杂泛型、匿名类、流式 API 期待被完整保真翻译
  • 通配导入 import x.y.*; 不会变成 AST 导入类 语句,正式函数里尽量写明确类

YML DSL 自定义函数

YML 函数走的是另一套配置式执行器。

最小示例

yaml
imports:
  - java.util.Arrays

functions:
  示例Yml:
    category: 示例
    description: YML DSL 示例
    aliases: [ymlAlias]
    params:
      - name: prefix
        type: 文本
        default: "y"
    returns: 文本
    logic:
      - calculate: "prefix + '-ok'"
        save: result
      - return: result

顶层结构

说明
imports要导入的类全名列表
functions函数字典

每个函数定义下常用的键有:

说明
category分类
description描述
aliases别名数组
params参数定义列表
returns返回类型说明
logic执行步骤列表

当前 DSL 支持的主要 step

step作用
call调函数
get取值
set写局部变量
calculate计算表达式
if / else条件分支
switch分支匹配
check_all全部条件成立
check_any任一条件成立
forfor 循环
whilewhile 循环
foreach遍历循环
break / continue循环控制
create创建对象
invoke调方法链
return返回

当前 YML 的语义边界

  • logic 是顺序执行的步骤列表
  • 局部变量存在一个独立 locals 映射
  • calculate / 条件判断不是自己手写解析,而是通过 LoaderAstBridge 借 AST 表达式系统求值
  • create / invoke 则走 YML 执行器自己的反射逻辑

也就是说,YML 不是“翻译成 AST 整段执行”,而是:

  • 条件 / 表达式桥接到 AST
  • 控制流程和方法调用由 YML 执行器自己掌控

YML 的循环作用域

当前实现会在循环里创建 loopLocals,执行结束后再同步回父级局部变量。 这意味着:

  • 循环内部新产生的局部值不会无脑污染所有外层
  • 但你显式修改的局部变量可以通过同步逻辑带回父作用域

如果你想写最稳的 YML DSL,仍然建议把重要结果明确 save 到你自己命名的变量里。

JS / AV 自定义函数

scripts/functions/js/ 下的 .av / .js 文件,会走 AviatorScript 自定义函数加载器。

这部分详细语义见 script-js / AviatorScript

这里只说函数注册本身。

registerFunction

javascript
registerFunction("示例JS_是否白天", lambda(ctx) ->
    ctx.player != nil && ctx.player.getWorld().getTime() < 13000
end);

registerFunctionEx

javascript
registerFunctionEx({
    "name": "示例JS_计算伤害加成",
    "category": "战斗",
    "description": "根据玩家等级计算加成",
    "aliases": seq.list("calcDamageBonus"),
    "execute": lambda(ctx, baseDamage, levelCoef) ->
        let coef = levelCoef == nil ? 1.0 : levelCoef;
        return math.round(baseDamage * (1 + coef) * 100) / 100;
    end
});

当前 AviatorUtil 真正会收集的键只有:

  • name
  • category
  • description
  • aliases
  • execute

并且自定义 Aviator 函数真正执行时,ctx 会作为第一个参数传进去。

返回值与作用域

当前回归和实现都说明了几件事:

  • TQS 自定义函数参数默认值可用
  • TQS / Java 自定义函数会复制父级 ScriptContext
  • TQS / Java 的局部变量不会直接污染调用方上下文
  • YML 条件桥接里的临时变量不会直接写回主 AST 上下文
  • JS / AV 自定义函数通过参数拿到 ctx,不是直接共享一份顶层 player/event 环境

你可以把自定义函数整体理解成“受控局部执行单元”,而不是直接把外层脚本粘进去运行。

选择建议

  • 首选 tqs/:最贴近当前 AST 主路径,迁移成本最低。
  • 需要配置式表达时用 yml/
  • 团队偏 Java 书写风格时用 java/,但要记住它不是完整 Java。
  • 需要 Aviator lambda、函数式表达式时用 js/
  • 同一个函数不要在多个目录里写同名版本,否则会受加载顺序影响。

TQ Minecraft Server Plugin Docs