Lua 脚本:故障排除
本页涵盖编写或调试 Lua 能力时可能遇到的常见问题,以及面向从 EliteScript 迁移而来的作者的迁移建议。如果你正在调试 NPC 脚本,请参阅 NPC 脚本。如果你正在寻找可用的示例,请参阅示例与模式。如果你刚刚起步,请参阅入门指南。
EliteMobs 使用在各个 Nightbreak 插件之间共享的 MagmaCore Lua 脚本引擎。关于沙盒、调度器、区域、世界 API、实体表以及玩家 UI 方法等共享概念的文档,请参阅 MagmaCore Lua 脚本引擎 页面。
常见问题
1. 能力完全无法加载
在服务器启动时检查服务器控制台是否有错误。最常见的原因是 Lua 语法错误(缺少 end、括号不匹配等)。还要确认文件以 .lua 结尾,并放置在正确的 powers 目录中。
2. 钩子从不触发
确认钩子名称的拼写与钩子列表中所列完全一致。常见错误:on_boss_hit(错误)与 on_boss_damaged_by_player(正确),或 on_tick(错误)与 on_game_tick(正确)。
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 中始终为 nil。
它会在 on_boss_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 毫秒限制。请使用冷却、减少采样点,或通过 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 确实解析为一个有效的位置。
7. 粒子未出现
使用有效的 Bukkit Particle 名称;"FLAME" 和 "flame" 都会转换为大写。还要检查位置、数量和所需数据。BLOCK 需要此世界辅助方法未提供的方块数据;请改用支持的粒子,或使用带材质选项的脚本粒子 API。
8. 冷却似乎不起作用
确保你使用的是 check_local(key, duration)(它会在一次调用中同时检查并设置),而不是先用 local_ready(key) 再单独使用 set_local(duration, key)。如果你单独使用 local_ready,你就只检查了冷却却从未设置它。
9. Boss 死亡后仍持续运行能力
在 on_exit_combat 和/或 on_death 中添加清理逻辑以取消调度器任务。如果 Boss 死亡,on_exit_combat 应当会触发,但在两个钩子中都添加显式清理会更安全。
阅读错误消息
当 Lua 能力中出现问题时,控制台会打印一个以 [Lua] 为前缀的友好错误块。这些消息会用通俗的英文准确告诉你是哪个文件、哪一行、哪个钩子,以及出了什么问题。在调试之前,请务必通读完整消息。
一个典型的错误看起来像这样:
[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 | 显示具体的参数不匹配详情(期望类型与实际类型)。 |
| 超出执行预算 | 调用超过 250,000 条 Lua 指令或当前线程 50 毫秒 CPU 时间时会被中止。如果无法测量 CPU 时间,则使用 250 毫秒实际经过时间作为备用上限。此错误会禁用该脚本实例。 |
当你在控制台中看到 [Lua] 错误时,错误消息会用通俗的英文准确告诉你是哪个文件、哪一行、哪个钩子,以及出了什么问题。在深入代码之前,请通读完整消息——它通常会直接指向修复方法。
不要假设存在未记录的别名
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)。entity:set_velocity(...)—— 改用entity:set_velocity_vector(vector)。entity:set_facing(...)—— 改用entity:face_direction_or_location(direction_or_location)。
如有疑问,请查阅 API 参考页面(Boss 与实体、世界与环境、区域与目标选取)。如果那里没有记录,它就不存在。
给 EliteScript 作者的迁移建议
如果你已经能写出不错的 EliteScript,那么学习 Lua 能力的最简单方法是:
-
继续以事件、目标、区域、相对向量和粒子的方式来思考。 概念是相同的——只是语法变了。EliteScript 事件变成了诸如
on_spawn或on_boss_damaged_by_player之类的钩子名称。目标和区域作为表传递给context.script,使用的字段名与 EliteScript 区域 和 EliteScript 目标 页面中记录的相同。 -
把你的控制流移到 Lua 中。 随机掷骰、共享辅助函数、循环、持久状态(
context.state)以及任务调度(context.scheduler)是 Lua 所新增、而纯 EliteScript 难以轻松做到的东西。先从把一个带分支或条件的能力转换为 Lua 开始,同时保持其他一切不变。 -
使用
context.script进行目标选取和区域几何。 它们并不是"长得像"而已 —— 你传入的规格表会被交给真正的 EliteScript 引擎,因此 EliteScript 页面上记录的每一个字段(targetType、shape、Target、Target2、FinalTarget、range、offset、relativeOffset、coverage、filter……)都能原样使用,默认值和大小写写法也完全一致。请把 EliteScript 文档作为你的规格参考随时打开,而把 Lua 纯粹用于逻辑层。 -
当心两套区域系统。
context.script:zone({shape = "SPHERE", ...})走的是 EliteScript 引擎(大写枚举、Target规格表)。context.zones则是一套独立的轻量实现(小写kind、普通的origin/destination位置)。混用它们的键风格会静默地产生一个空区域。
新手进阶路径
如果你想从零开始学习这套系统,以下进阶顺序效果很好:
- 编写一个只包含
api_version = 1和on_spawn的文件。 - 让 Boss 发送一条消息或播放一个声音。
- 用
context.cooldowns添加一个冷却。 - 添加一个由玩家触发的钩子,例如
on_boss_damaged_by_player。 - 用
context.scheduler:run_after(...)添加一个延迟动作。 - 添加一个简单的原生 Lua 区域查询或一个简单的
context.script:target(...)。 - 只有到了那时,才开始转入轮转攻击、状态机和多步机制。
每一步都建立在前一步之上,你可以在每个阶段进行测试。不要试图把多阶段 Boss 作为你的第一个 Lua 能力来编写。
