Skip to content

script-js / AviatorScript

这一页先讲一个最重要的事实:

AchieveMaster 里的 script-js,名字虽然叫 js,但实际执行引擎是 AviatorScript 5.4.3,不是浏览器 JavaScript,也不是 Node.js。

所以你在这里写的是 Aviator 表达式 / 脚本语法,不是 AST/TQS 中文语法。

当前有哪些入口

ConfigManager 当前支持这些配置入口:

位置支持键名说明
triggerscript-js / 脚本js / js触发阶段 AviatorScript
rewards[]script-js / 脚本js / js奖励阶段 AviatorScript
scripts/functions/js/*.av文件Aviator 自定义函数
scripts/functions/js/*.js文件仍然走 Aviator,自定义函数文件后缀兼容为 .js

脚本内容本身支持三种 YAML 写法:

  • 单行字符串
  • | 多行字符串
  • 字符串列表,加载时会用换行拼接

它和 AST/TQS 的关系

当前正式关系应该这样理解:

  • AST/TQS 是主执行路径
  • script-js 是并存的 Aviator 执行入口
  • 两边共享同一套 FunctionRegistry
  • 两边上下文来源相同,但暴露出来的对象形态不一样

所以不要把 AST 文档里的这些关键字直接原样复制进 script-js

  • 判断
  • 循环
  • 遍历
  • 延迟执行
  • 导入类

script-js 里要按 Aviator 语法写。

执行时会注入什么

JsScriptEngine.execute(...) 运行时会向 Aviator 环境注入这些对象:

名称何时注入含义
ctx / context总是ScriptContext
player / 玩家有玩家时当前玩家对象
achievement / 成就有成就时当前成就对象
progress / 进度有进度时AchievementProgress 对象
event / 事件有事件时当前 Bukkit 事件对象
entity / 实体有实体时当前实体对象
item / 物品有物品时当前物品对象
plugin总是插件主类实例
Bukkit总是org.bukkit.Bukkit.class
fn / $fn总是函数桥接对象

这里最容易和 AST 混淆的一点是:

  • AST 里 progress 默认是数字
  • script-jsprogressAchievementProgress 对象

也就是说,在 script-js 里你可以直接写:

javascript
progress.getProgress()
progress.getPercentage(achievement.getCompletionTarget())

而不是把 progress 当 AST 数字变量那样理解。

script-js 返回值到底有什么用

这是当前最容易被误解的边界。

ScriptService.executeJavaScript(...) 执行 script-js 后,会把 Aviator 的执行结果放进 ScriptResult.returnValue。 但是当前成就触发与奖励执行路径:

  • 不会把 return true/false 当成 AST 那种“继续 / 停止”控制信号
  • 也不会自动把返回数字当作进度变化

当前实际结论是:

  • script-js 的返回值会被保留
  • 但触发器 / 奖励主流程本身目前不消费这个返回值做判定

所以如果你想让 script-js 真正产生效果,应该直接调用内置函数或操作注入对象,例如:

  • 增加进度(...)
  • 设置进度(...)
  • 增加目标进度(...)
  • 发送消息(...)
  • 执行指令(...)

而不是只写一个 return true 期待它自动推动成就流程。

内置函数在 script-js 里怎么用

JsScriptEngine 初始化时,会把当前 FunctionRegistry 里的函数名和别名都注册进 Aviator 全局环境。

所以这些调用是成立的:

javascript
获取金币()
世界是否白天()
取玩家变量("签到次数")
设置进度(10)
发送消息("&a脚本触发成功")

这和 AST 共享的是同一套函数注册表,不是另一份独立 API。

需要注意的是,Aviator 注册阶段如果发现同名函数已存在,会跳过重复注册。

fn / $fn 桥接对象

除了“直接把函数名当全局函数调用”,运行时还额外注入了 fn / $fn

它们是 JsFunctionBridge,支持这些方法:

写法说明
fn.invoke("函数名", arg1, arg2)变参调用
fn.call("函数名", seq.list(arg1, arg2))用数组 / 列表调用
fn.has("函数名")检查函数是否存在
fn.list()返回已注册函数名数组

示例:

javascript
if fn.has("获取金币") {
  let money = fn.invoke("获取金币");
  if money >= 1000 {
    fn.invoke("发送消息", "&6你已经满足金币条件");
  }
}

fn.invoke(...) 最适合下面两种场景:

  • 函数名是动态拼出来的
  • 你不想让脚本在编写时直接依赖某个全局函数名

触发脚本写法建议

有副作用的正式写法

当前更推荐这样写:

yaml
trigger:
  type: PLAYER_JOIN
  script-js: |
    if player == nil {
      return nil;
    }

    if 获取金币() >= 1000 {
      增加进度(1);
      发送消息("&a欢迎回来,已为你推进成就进度");
    }

这段脚本真正推动进度的,是 增加进度(1),不是 return

直接访问注入对象

yaml
trigger:
  type: PLAYER_CHAT
  script-js: |
    if event == nil {
      return nil;
    }

    let msg = event.getMessage();
    if string.contains(msg, "签到") {
      增加进度(1);
    }

奖励脚本写法建议

奖励阶段也是同样逻辑。 当前奖励 script-js 的返回值不会被奖励系统额外消费,所以重点还是直接执行动作。

yaml
rewards:
  - type: SCRIPT
    script-js: |
      发送消息("&6你完成了成就奖励脚本");
      执行指令("say " + player.getName() + " 完成了奖励脚本");

Aviator 自定义函数文件

CustomFunctionLoader 会从 scripts/functions/js/ 读取 .av.js 文件,然后交给 JsFunctionLoader

文件本质

这里的 .js 只是兼容后缀,不代表它是浏览器 JavaScript。 .av.js 最终都走 Aviator。

加载时会做什么

AviatorUtil.loadCustomFunctions(script) 会:

  1. 注册 registerFunction
  2. 注册 registerFunctionEx
  3. 用一个空环境执行整份脚本
  4. 收集你在文件里声明的函数定义

这意味着一个重要边界:

  • 自定义函数文件的顶层代码会在“加载阶段”执行
  • 此时没有真实的 player / event / ctx 运行时上下文

所以函数文件顶层应该只做“注册函数”,不要写依赖实时上下文的副作用逻辑。

registerFunction

当前源码支持三种主要形态:

两参数

name + lambda

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

三参数

name + category + lambda

javascript
registerFunction("示例JS_玩家名", "玩家", lambda(ctx) ->
    ctx.player == nil ? "" : ctx.player.getName()
end);

四参数

name + category + description + lambda

javascript
registerFunction("示例JS_获取玩家血量百分比", "玩家", "返回玩家当前血量百分比",
    lambda(ctx) ->
        if ctx.player == nil {
            return 0;
        }
        let hp = ctx.player.getHealth();
        let maxHp = ctx.player.getMaxHealth();
        return math.round(hp / maxHp * 100);
    end
);

这里的第一个参数 ctx 很重要。

JsFunctionLoader 在真正调用自定义 Aviator 函数时,会把 ScriptContext 作为第一个参数手动塞进去。 所以自定义函数里请把第一个参数当作上下文对象来用。

registerFunctionEx

复杂写法可以用一个 map

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

当前加载器真正会读取的键只有:

说明
name函数名
category分类
description描述
aliases别名列表
executelambda 主体

自定义函数的运行语义

自定义 Aviator 函数注册到 FunctionRegistry 之后,AST 和 script-js 都可以调用它们。

调用时实际参数顺序是:

  1. ctx
  2. 你在脚本里传入的其余参数

也就是说,如果你定义:

javascript
registerFunction("示例JS_相加", lambda(ctx, a, b) -> a + b end);

那在 AST 或 Aviator 里调用 示例JS_相加(1, 2) 时,函数体里拿到的是:

  • ctx = 当前 ScriptContext
  • a = 1
  • b = 2

访问 Java / Bukkit 类

因为环境里注入了 Bukkit 类对象,同时 Aviator 也能直接访问 Java 对象方法,所以这类写法是成立的:

javascript
let online = Bukkit.getOnlinePlayers().size();
let world = player.getWorld();
let name = world.getName();

如果你要用其他 Java 类,通常直接写全限定类名更稳:

javascript
let Particle = org.bukkit.Particle;
let particle = Particle.valueOf(string.toUpperCase("flame"));
player.getWorld().spawnParticle(particle, player.getLocation(), 10);

和 AST 最容易混淆的差异

主题AST/TQSscript-js
执行引擎自研 ASTAviatorScript 5.4.3
progress数字AchievementProgress 对象
流程语法判断 / 循环 / 遍历if / let / lambda 等 Aviator 语法
反射入口导入类 + new + 方法链直接操作 Java 对象 / 类对象
返回值AST 控制流和 ScriptResult 深度整合当前触发 / 奖励主流程基本不消费返回值

什么时候更适合用 script-js

更适合:

  • 你已经熟悉 Aviator 语法
  • 需要 lambda 风格自定义函数
  • 想直接操作注入对象和 Java API
  • 只是做一小段表达式型逻辑或桥接逻辑

不太适合:

  • 团队主维护语言是 AST/TQS
  • 你希望所有脚本都统一中文关键字风格
  • 你期待它和 AST 一样有完整的块控制流语义

使用建议

  • 把它当 AviatorScript,不要当浏览器 JS。
  • 触发 / 奖励脚本如果要产生效果,优先直接调用内置函数,不要只写 return true/false
  • 共享逻辑优先抽到 scripts/functions/js/ 自定义函数文件。
  • 自定义函数文件顶层只做注册,不要写依赖实时上下文的执行逻辑。
  • 如果只是普通成就主逻辑,优先继续用 AST/TQS;script-js 更适合作为补充入口。

TQ Minecraft Server Plugin Docs