Lua 脚本:故障排除
本页涵盖编写或调试 Lua 能力时可能遇到的常见问题,以及面向从 EliteScript 迁移而来的作者的迁移建议。如果你正在调试 NPC 脚本,请参阅 NPC 脚本。如果你正在寻找可用的示例,请参阅示例与模式。如果你刚刚起步,请参阅入门指南。
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 能力的最简单方法是:
-
继续以事件、目标、区域、相对向量和粒子的方式来思考。 概念是相同的——只是语法变了。EliteScript 事件变成了诸如
on_spawn或on_boss_damaged_by_player之类的钩子名称。目标和区域作为表传递给context.script,使用的字段名与 EliteScript 区域 和 EliteScript 目标 页面中记录的相同。 -
把你的控制流移到 Lua 中。 随机掷骰、共享辅助函数、循环、持久状态(
context.state)以及任务调度(context.scheduler)是 Lua 所新增、而纯 EliteScript 难以轻松做到的东西。先从把一个带分支或条件的能力转换为 Lua 开始,同时保持其他一切不变。 -
使用
context.script进行目标选取和区域几何。 其规格表通过实际的 EliteScript 解析器处理targetType、shape、Target、Target2、FinalTarget、range、offset、relativeOffset、coverage和filter。字段名和默认值可查阅 EliteScript 文档,同时请阅读 Lua 句柄限制。静态 Lua 句柄不执行 YAML 动作的track: false缓存,也不具备继承的动作数据。offset和relativeOffset作用于解析出的位置,包括LOCATION、LOCATIONS以及采样的区域位置。区域必须固定时,请显式使用固定原点。 -
注意两套区域系统。
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 能力来编写。
