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(錯誤)vs. on_boss_damaged_by_player(正確),或 on_tick(錯誤)vs. 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 | 顯示具體的參數不匹配細節(預期型別 vs. 實際型別)。 |
| 預算超限 | 訊息會指出超出的是指令數、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 能力來寫。
