Skip to main content

Lua Scripting: Troubleshooting

webapp_banner.jpg

This page covers common problems you may encounter when writing or debugging Lua powers, plus migration advice for authors coming from EliteScript. If you are debugging NPC scripts, see NPC Scripts. If you are looking for working examples, see Examples & Patterns. If you are just getting started, see Getting Started.

Shared Lua Engine

EliteMobs uses the MagmaCore Lua scripting engine shared across Nightbreak plugins. For documentation on shared concepts like the sandbox, scheduler, zones, world API, entity tables, and player UI methods, see the MagmaCore Lua Scripting Engine page.


Common Issues

1. Power does not load at all

Check the server console for errors when the server starts. The most common cause is a Lua syntax error (missing end, unmatched parentheses, etc.). Also verify the file ends in .lua and is placed in the correct powers directory.

2. Hook never fires

Verify the hook name is spelled exactly as listed in the hooks list. Common mistakes: on_boss_hit (wrong) vs. on_boss_damaged_by_player (correct), or on_tick (wrong) vs. on_game_tick (correct).

3. context.player is nil

Only hooks whose underlying event carries a player populate context.player. It is always nil in on_spawn, on_game_tick, on_boss_damaged, on_boss_damaged_by_elite, on_exit_combat, on_heal, on_death, and on_phase_switch.

It is populated in on_boss_damaged_by_player, on_player_damaged_by_boss, on_enter_combat and on_boss_target_changed, and in on_zone_enter / on_zone_leave when the entity involved happens to be a player. Always add a nil guard before using it. See Hooks & Lifecycle for the full table.

4. Timeout / execution budget exceeded

Each hook, scheduled callback and file evaluation has a shared nested budget: 250,000 Lua bytecode instructions and 50 ms of thread CPU time. If the JVM cannot measure thread CPU time, the runtime uses a 250 ms elapsed-time fallback. The error identifies the limit:

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)

The VM aborts runaway Lua loops during execution. It cannot interrupt a Java API call while that call is running, and there is no separate post-return 50 ms wall-clock cutoff. Common causes include iterating over too many entities, spawning particles across a large zone without a coverage value, and expensive work every tick. Use cooldowns, fewer sample locations, or context.scheduler:run_every(...) to spread work across ticks.

5. Scheduler callback uses stale data

You are probably using the outer context instead of the callback parameter. Change function() ... context.boss ... end to function(tick_context) ... tick_context.boss ... end.

6. Zone query returns no entities

Double-check the zone definition. For native zones, ensure kind is lowercase ("sphere", not "SPHERE"). For script utilities, ensure shape is uppercase ("CONE", not "cone"). Also verify that origin or Target actually resolves to a valid location.

7. Particles do not appear

Verify the particle name is a valid Bukkit Particle enum value; names such as "FLAME" and "flame" are normalized to uppercase. Check the location, amount, and required particle data. For example, BLOCK needs block data that the simple world particle helper does not supply; use a supported particle or the script particle API with its material option.

8. Cooldown does not seem to work

Make sure you are using check_local(key, duration) (which checks AND sets in one call), not local_ready(key) followed by a separate set_local(duration, key). If you use local_ready alone, you only check but never set the cooldown.

9. Boss keeps running power after death

Add cleanup logic in on_exit_combat and/or on_death to cancel scheduler tasks. If the boss dies, on_exit_combat should fire, but adding explicit cleanup in both hooks is safer.


Reading Error Messages

When something goes wrong in a Lua power, the console prints a friendly error block prefixed with [Lua]. These messages tell you exactly which file, which line, which hook, and what went wrong -- in plain English. Always read the full message before debugging.

A typical error looks like this:

[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.

The system translates common Lua errors into plain English. Here are the most common ones:

Raw Lua errorWhat the console tells you
attempt to call nilYou tried to call a method or function that doesn't exist. Check the method name for typos, or make sure you're using : (colon) for method calls, not . (dot).
index expected, got nilYou tried to access a field on something that is nil. Check that earlier code initialized it.
attempt to indexYou tried to access a property on a nil or invalid value.
bad argumentShows the specific argument mismatch details (expected type vs. actual type).
Execution budget exceededThe message identifies the instruction, CPU-time, or elapsed-time fallback limit; see the budget section above.
tip

When you see a [Lua] error in the console, the error message tells you exactly which file, which line, which hook, and what went wrong in plain English. Read the full message before diving into the code -- it usually points you straight to the fix.


Do Not Assume Undocumented Aliases Exist

The Lua API exposes a specific set of method names. If you are writing powers by hand or with AI assistance, do not assume shorthand or alternative names exist. The following are examples of names that do not exist and will cause errors:

  • show_temporary_boss_bar() -- use player:show_boss_bar(title, color, style, duration) instead.
  • run_command_as_player() -- use player:run_command(command) instead.
  • em.location(...) -- the method name is wrong. Use em.create_location(x, y, z) instead, or context.boss:get_location() / context.player.current_location.
  • em.vector(...) -- the method name is wrong. Use em.create_vector(x, y, z) instead, or plain {x=0, y=1, z=0} tables.
  • em.zone.sphere(...) -- the method name is wrong. Use em.zone.create_sphere_zone(radius) instead, or a zone definition table like {kind = "sphere", radius = 5, origin = location}.
  • entity:teleport_to(...) -- use entity:teleport_to_location(location).
  • entity:set_velocity(...) -- use entity:set_velocity_vector(vector).
  • entity:set_facing(...) -- use entity:face_direction_or_location(direction_or_location).

When in doubt, check the API Reference pages (Boss & Entities, World & Environment, Zones & Targeting). If it is not documented there, it does not exist.


Migration Advice For EliteScript Authors

If you already write good EliteScripts, the easiest way to learn Lua powers is:

  1. Keep thinking in terms of events, targets, zones, relative vectors, and particles. The concepts are the same -- only the syntax changes. EliteScript events become hook names like on_spawn or on_boss_damaged_by_player. Targets and zones are passed as tables to context.script using the same field names documented in the EliteScript Zones and EliteScript Targets pages.

  2. Move your control flow into Lua. Random rolls, shared helper functions, loops, persistent state (context.state), and task scheduling (context.scheduler) are the things Lua adds that pure EliteScript cannot do easily. Start by converting one branching or conditional power to Lua while keeping everything else the same.

  3. Use context.script for targeting and zone geometry. These are not lookalikes -- the spec table you pass is handed to the actual EliteScript engine, so every field documented on the EliteScript pages (targetType, shape, Target, Target2, FinalTarget, range, offset, relativeOffset, coverage, filter, ...) works verbatim, with the same defaults and the same capitalisation. Keep the EliteScript docs open as your spec reference and use Lua purely for the logic layer.

  4. Watch out for the two zone systems. context.script:zone({shape = "SPHERE", ...}) is the EliteScript engine (UPPERCASE enums, Target spec tables). context.zones is a separate lightweight implementation (lowercase kind, plain origin/destination locations). Mixing their key styles silently produces an empty zone.


Beginner Progression Path

If you want to learn this system from scratch, this progression works well:

  1. Write a file with only api_version = 1 and on_spawn.
  2. Make the boss send a message or play a sound.
  3. Add a cooldown with context.cooldowns.
  4. Add one player-triggered hook such as on_boss_damaged_by_player.
  5. Add one delayed action with context.scheduler:run_after(...).
  6. Add one simple native Lua zone query or one simple context.script:target(...).
  7. Only then move into rotating attacks, state machines, and multi-step mechanics.

Each step builds on the previous one, and you can test at every stage. Do not try to write a multi-phase boss as your first Lua power.


Next Steps

  • Getting Started -- file structure, hooks, first power walkthrough, copy-paste templates
  • NPC Scripts -- NPC proximity, interaction, and lifecycle scripts
  • Examples & Patterns -- complete working powers you can study and adapt