跳到主要内容

Lua 脚本:故障排除

webapp_banner.jpg

本页涵盖编写或调试 Lua 能力时可能遇到的常见问题,以及面向从 EliteScript 迁移而来的作者的迁移建议。如果你正在调试 NPC 脚本,请参阅 NPC 脚本。如果你正在寻找可用的示例,请参阅示例与模式。如果你刚刚起步,请参阅入门指南。

共享的 Lua 引擎

EliteMobs 使用在各个 Nightbreak 插件之间共享的 MagmaCore Lua 脚本引擎。关于沙盒、调度器、区域、世界 API、实体表以及玩家 UI 方法等共享概念的文档,请参阅 MagmaCore Lua 脚本引擎 页面。


常见问题​

1. 能力完全无法加载​

在服务器启动时检查服务器控制台是否有错误。最常见的原因是 Lua 语法错误(缺少 end、括号不匹配等)。还要确认文件以 .lua 结尾,并放置在正确的 powers 目录中。

对于自定义文件名,还要检查同目录、同基本名称的 .yml 或 .yaml 元数据是否设置了 isEnabled: false;这会阻止发现该文件。放在内置能力旁边、同基本名称的元数据文件,也会对该能力应用其 isEnabled、effect、powerType 和冷却字段。如果不同文件夹中有两个能力文件同名,只会加载其中一个;控制台会指出被跳过的副本。

文件必须返回包含 api_version = 1 的表。该表只能包含 api_version、可选的 priority 和支持的钩子名称;钩子值必须是函数。辅助函数应保持为局部函数,不要给返回表添加任意键。顶层代码会在定义验证和运行时初始化时执行,此时没有钩子上下文。

2. 钩子从不触发​

确认钩子名称的拼写与钩子列表中所列完全一致。常见错误:on_boss_hit(错误)与 on_boss_damaged_by_player(正确),或 on_tick(错误)与 on_game_tick(正确)。

返回表中的不支持键会阻止整个定义加载。如果已加载但不运行,还要检查 Boss 是否引用了能力文件名、相应事件是否发生,以及运行时是否已失败、暂停或被抑制了能力。

3. context.player 为 nil​

只有当底层事件本身携带玩家时,钩子才会填充 context.player。它在 on_spawn、on_game_tick、on_boss_damaged、on_boss_damaged_by_elite、on_exit_combat、on_heal、on_death、on_phase_switch 和 on_mind_action 中始终为 nil。

它会在 on_boss_damaged_by_player、on_reinforcement_damaged_by_player、on_player_damaged_by_boss、on_enter_combat 和 on_boss_target_changed 中被填充;在 on_zone_enter / on_zone_leave 中,当涉及的实体是玩家时也会被填充。使用前请始终检查 nil。完整表格参见钩子与生命周期。

4. 超时 / 超出执行预算​

每个钩子、定时回调和文件求值都与嵌套调用共享预算:250,000 条 Lua 指令和 50 毫秒线程 CPU 时间。若 JVM 无法测量线程 CPU 时间,则改用 250 毫秒实际经过时间的备用限制。错误会指出超出的限制:

Lua instruction budget exceeded (250000 instruction limit)
Lua CPU-time budget exceeded (50ms current-thread CPU limit)
Lua elapsed-time fallback budget exceeded (250ms fallback; current-thread CPU time unavailable)

VM 会在运行过程中中断失控的 Lua 循环,但无法中断正在执行的 Java API 调用;调用返回后也没有单独的 50 毫秒实际经过时间限制。常见原因包括遍历过多实体、未设置 coverage 就在大区域生成粒子,以及每 tick 执行昂贵的操作。请使用冷却、减少采样位置,或通过 context.scheduler:run_every(...) 将工作分散到多个 tick。

5. 调度器回调使用了过时的数据​

你很可能使用了外层的 context,而非回调参数。将 function() ... context.boss ... end 改为 function(tick_context) ... tick_context.boss ... end。

定时回调收到新的上下文,不带触发时的事件、直接目标或玩家。请在调度前显式保存所需目标,并在使用时检查其是否仍然有效。重新读取当前位置等会变化的属性,不要一直使用之前的表快照。

6. 区域查询返回不到任何实体​

仔细检查区域定义。对于原生区域,确保 kind 为小写("sphere",而非 "SPHERE")。对于脚本工具,确保 shape 为大写("CONE",而非 "cone")。还要确认 origin 或 Target 确实解析为一个有效的位置。

同时检查实体过滤器和世界。Boss 原生区域查询默认使用 living,排除 Boss 自身并搜索其所在世界。脚本工具区域默认使用 PLAYER。通过区域句柄的 full_target() 或 border_target() 获取 Lua 区域目标;单独的 context.script:target({targetType = "ZONE_FULL"}) 没有关联区域。

7. 粒子未出现​

使用有效的 Bukkit Particle 名称;"FLAME" 和 "flame" 都会转换为大写。还要检查位置、数量和所需数据。BLOCK 需要此世界辅助方法未提供的方块数据;请改用支持的粒子,或使用带材质选项的脚本粒子 API。

8. 冷却似乎不起作用​

check_local(key, duration) 在一次调用中检查并设置冷却。也可以先用 local_ready(key),再在攻击真正开始时调用 set_local(duration, key);仅检查是否就绪不会启动冷却。显式键由同一个 Boss 的各个能力共享,因此请使用能力专属前缀。YAML 元数据不会自动限制 Lua 钩子的执行。

9. Boss 死亡后仍持续运行能力​

能力运行时关闭时会取消 context.scheduler 所属任务。如果攻击循环需要在移除之前停止,请在 on_exit_combat 中取消它。也要检查所调用的辅助方法,有些 Java 辅助方法会启动自己的任务。例如,world:run_fake_gold_nugget_damage(...) 已经运行一个带独立超时的逐 tick 循环,每批投射物只能调用一次。


阅读错误消息​

Lua 能力运行时失败时,控制台会输出带 [Lua] 前缀的错误块,标明文件和钩子或回调。如果能从原始错误中提取行号,也会显示行号。调试前请阅读完整消息。对于使用 -- @include 的文件,行号对应展开后的源代码。

一个典型的错误看起来像这样:

[Lua] Error in 'push_zone.lua' at line 35 during 'on_boss_damaged_by_player':
[Lua] -> You tried to call a method or function that doesn't exist.
[Lua] -> Check the method name for typos, or make sure you're using ':' (colon) for method calls, not '.' (dot).
[Lua] -> Script has been disabled for this entity to prevent further errors.

该系统会将常见的 Lua 错误翻译成通俗的英文。以下是最常见的几种:

原始 Lua 错误控制台告诉你的内容
attempt to call nil你尝试调用了一个不存在的方法或函数。检查方法名是否有拼写错误,或确保你在方法调用时使用的是 :(冒号)而非 .(点)。
index expected, got nil你尝试访问某个为 nil 的对象上的字段。检查前面的代码是否已对其进行初始化。
attempt to index你尝试访问某个 nil 或无效值上的属性。
bad argument显示具体的参数不匹配详情(期望类型与实际类型)。
超出执行预算消息会指出指令数、CPU 时间或备用实际经过时间限制;请参阅上方预算说明。
提示

失败的脚本实例会针对该实体被禁用。修复文件后,需要重新加载定义并创建新的运行时;未更改的已禁用实例不会每 tick 重试失败的钩子。


不要假设存在未记录的别名​

Lua API 公开了一组特定的方法名。如果你是手动编写能力或在 AI 辅助下编写,请不要假设存在简写或替代名称。以下是一些不存在且会导致错误的名称示例:

  • show_temporary_boss_bar() —— 改用 player:show_boss_bar(title, color, style, duration)。
  • run_command_as_player() —— 改用 player:run_command(command)。
  • em.location(...) —— 方法名错误。改用 em.create_location(x, y, z),或 context.boss:get_location() / context.player.current_location。
  • em.vector(...) —— 方法名错误。改用 em.create_vector(x, y, z),或纯 {x=0, y=1, z=0} 表。
  • em.zone.sphere(...) —— 方法名错误。改用 em.zone.create_sphere_zone(radius),或一个区域定义表,如 {kind = "sphere", radius = 5, origin = location}。
  • entity:teleport_to(...) —— 改用 entity:teleport_to_location(location)。
  • 对于 EliteMobs 的 Boss 专用实体包装器,请使用 entity:set_velocity_vector(vector)。共享的 MagmaCore 包装器也提供 set_velocity(...);请检查上下文返回的是哪种包装器。
  • entity:set_facing(...) —— 改用 entity:face_direction_or_location(direction_or_location)。

请查阅所用包装器的 API 参考:Boss 与实体、世界与环境、区域与目标选取或共享 API。相似的上下文也可能提供不同方法;一个看似合理的别名并不一定可用。


给 EliteScript 作者的迁移建议​

如果你已经能写出不错的 EliteScript,那么学习 Lua 能力的最简单方法是:

  1. 继续以事件、目标、区域、相对向量和粒子的方式来思考。 概念是相同的——只是语法变了。EliteScript 事件变成了诸如 on_spawn 或 on_boss_damaged_by_player 之类的钩子名称。目标和区域作为表传递给 context.script,使用的字段名与 EliteScript 区域 和 EliteScript 目标 页面中记录的相同。

  2. 把你的控制流移到 Lua 中。 随机掷骰、共享辅助函数、循环、持久状态(context.state)以及任务调度(context.scheduler)是 Lua 所新增、而纯 EliteScript 难以轻松做到的东西。先从把一个带分支或条件的能力转换为 Lua 开始,同时保持其他一切不变。

  3. 使用 context.script 进行目标选取和区域几何。 其规格表通过实际的 EliteScript 解析器处理 targetType、shape、Target、Target2、FinalTarget、range、offset、relativeOffset、coverage 和 filter。字段名和默认值可查阅 EliteScript 文档,同时请阅读 Lua 句柄限制。静态 Lua 句柄不执行 YAML 动作的 track: false 缓存,也不具备继承的动作数据。offset 和 relativeOffset 作用于解析出的位置,包括 LOCATION、LOCATIONS 以及采样的区域位置。区域必须固定时,请显式使用固定原点。

  4. 注意两套区域系统。 context.script:zone({shape = "SPHERE", ...}) 使用 EliteScript 引擎、大写枚举和 Target 规格表。context.zones 使用小写 kind 和普通的 origin/destination 位置。混用这些键格式可能产生空区域或意外区域、警告或运行时错误。


新手进阶路径​

如果你想从零开始学习这套系统,以下进阶顺序效果很好:

  1. 编写一个只包含 api_version = 1 和 on_spawn 的文件。
  2. 让 Boss 发送一条消息或播放一个声音。
  3. 用 context.cooldowns 添加一个冷却。
  4. 添加一个由玩家触发的钩子,例如 on_boss_damaged_by_player。
  5. 用 context.scheduler:run_after(...) 添加一个延迟动作。
  6. 添加一个简单的原生 Lua 区域查询或一个简单的 context.script:target(...)。
  7. 只有到了那时,才开始转入轮转攻击、状态机和多步机制。

每一步都建立在前一步之上,你可以在每个阶段进行测试。不要试图把多阶段 Boss 作为你的第一个 Lua 能力来编写。


后续步骤​

  • 入门指南 —— 文件结构、钩子、第一个能力的演练、可复制粘贴的模板
  • NPC 脚本 —— NPC 临近、交互与生命周期脚本
  • 示例与模式 —— 你可以研究并改编的完整可用能力