自定义函数
AchieveMaster 当前自定义函数目录是:
text
plugins/AchieveMaster/scripts/functions/
├─ yml/
├─ tqs/
├─ java/
└─ js/CustomFunctionLoader 会在加载时自动创建这些目录,并按固定顺序扫描:
yml/与.yamltqs/java/js/下的.js/.av
四种函数形态总表
| 目录 | 解释器 | 适合场景 | 和 AST 的关系 |
|---|---|---|---|
tqs/ | AST | 最推荐,和当前正式脚本同语义 | 直接编译到 AST |
java/ | Java 风格转译器 | 想保留 Java 外观,但不做真正 javac 编译 | 先翻译成 AST 再执行 |
yml/ | YML DSL | 偏配置化团队 | 独立执行器,条件 / 计算桥接到 AST |
js/ | AviatorScript | 需要 lambda、自定义表达式生态 | 走 Aviator,不走 AST 主执行器 |
加载顺序与重名规则
这是当前最重要的运行规则之一。
CustomFunctionLoader.loadAll() 的加载顺序固定是:
- YML
- TQS
- Java
- JS / AV
而 FunctionRegistry.register(...) 的策略是:
- 同名函数如果已经存在,后注册直接跳过
- 不是“后者覆盖前者”,而是“前者保留,后者失效”
- 别名冲突也会跳过,不会强行覆盖
这意味着如果四个目录里都写了同名函数,最终优先级就是:
yml/tqs/java/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/falsenull- 整数
- 小数
也就是说,默认值不是完整 AST 表达式求值。 像 player.name、1 + 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/长整数FloatDouble/Number/数字/小数
如果转换失败,会回退成原值,不会直接让整个函数注册失败。
Java 风格自定义函数
Java 风格加载器不是运行时 javac 动态编译。 它做的事情是:
- 解析
import - 解析
@Function与@Param - 解析方法签名
- 把方法体翻译成 AST 可执行脚本
- 最终仍然交给
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 / elsewhilefortry / catch- 变量声明
- 赋值
returnbreakcontinue- 自增 / 自减
- 普通表达式调用
当前明确不支持什么
当前源码里已经写死的一条边界是:
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 | 任一条件成立 |
for | for 循环 |
while | while 循环 |
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 真正会收集的键只有:
namecategorydescriptionaliasesexecute
并且自定义 Aviator 函数真正执行时,ctx 会作为第一个参数传进去。
返回值与作用域
当前回归和实现都说明了几件事:
- TQS 自定义函数参数默认值可用
- TQS / Java 自定义函数会复制父级
ScriptContext - TQS / Java 的局部变量不会直接污染调用方上下文
- YML 条件桥接里的临时变量不会直接写回主 AST 上下文
- JS / AV 自定义函数通过参数拿到
ctx,不是直接共享一份顶层player/event环境
你可以把自定义函数整体理解成“受控局部执行单元”,而不是直接把外层脚本粘进去运行。
选择建议
- 首选
tqs/:最贴近当前 AST 主路径,迁移成本最低。 - 需要配置式表达时用
yml/。 - 团队偏 Java 书写风格时用
java/,但要记住它不是完整 Java。 - 需要 Aviator lambda、函数式表达式时用
js/。 - 同一个函数不要在多个目录里写同名版本,否则会受加载顺序影响。