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. For custom filenames, check whether a same-directory, same-stem .yml or .yaml metadata file sets isEnabled: false; this prevents discovery. A same-stem metadata file beside a bundled power also applies its isEnabled, effect, powerType and cooldown fields to that power. If two power files share a name in different folders, only one loads; the console names the skipped copy.

The file must return a table containing api_version = 1. Only api_version, optional priority, and supported hook names belong in that returned table; hook values must be functions. Keep helper functions local instead of adding arbitrary keys to the returned table. Top-level code runs during definition validation as well as runtime initialization, without a hook context.

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

An unsupported key in the returned table prevents the whole definition from loading. If it loads but does not run, also check that the boss references the power filename, that the relevant event occurs, and that the runtime has not failed, been paused, or had its powers suppressed.

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, on_phase_switch, and on_mind_action.

It is populated in on_boss_damaged_by_player, on_reinforcement_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.

Scheduled callbacks receive a fresh context without the triggering event, direct target, or player. Capture a required target explicitly before scheduling and check that it is still valid when used. Re-read current locations and other changing properties instead of keeping an earlier table snapshot.

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.

Check the entity filter and world as well. Native boss zone queries default to living, exclude the boss, and search its world. Script utility zones default to PLAYER. Get Lua zone targets from the zone handle's full_target() or border_target(); a bare context.script:target({targetType = "ZONE_FULL"}) has no zone attached.

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​

check_local(key, duration) checks and sets in one call. You can also use local_ready(key) followed by set_local(duration, key) when the attack actually starts; checking readiness alone never starts the cooldown. Explicit keys are shared by the boss's powers, so use a power-specific prefix. YAML metadata does not automatically gate Lua hook execution.

9. Boss keeps running power after death​

Tasks owned by context.scheduler are cancelled when the power runtime closes. Cancel attack loops in on_exit_combat if they should stop before removal. Also inspect the helper you called: some Java helpers start their own tasks. For example, world:run_fake_gold_nugget_damage(...) already runs a per-tick loop with its own timeout and must be called only once per projectile batch.


Reading Error Messages​

When a Lua power fails at runtime, the console prints an error block prefixed with [Lua], identifying the file and hook or callback. A line number is included when it can be extracted from the underlying error. Read the full message before debugging. For files using -- @include, reported lines belong to the expanded source.

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

The failing script instance is disabled for that entity. Fixing the file requires reloading its definition and creating a fresh runtime; an unchanged disabled instance does not retry the failing hook every tick.


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).
  • On EliteMobs boss-specific entity wrappers, use entity:set_velocity_vector(vector). Shared MagmaCore wrappers also expose set_velocity(...); check which wrapper your context returns.
  • entity:set_facing(...) -- use entity:face_direction_or_location(direction_or_location).

Check the API reference for the wrapper you are using: Boss & Entities, World & Environment, Zones & Targeting, or the shared API. Similar contexts can expose different methods; spelling a plausible alias does not make it available.


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. Its spec tables use the actual EliteScript parsers for targetType, shape, Target, Target2, FinalTarget, range, offset, relativeOffset, coverage, and filter. Keep the EliteScript docs open for field names and defaults, but also read the Lua handle limitations. Static Lua handles do not perform the YAML action's track: false caching, and inherited action data is absent. offset and relativeOffset apply to resolved locations, including LOCATION, LOCATIONS and sampled zone locations. Use an explicit fixed origin when an area must stay in place.

  4. Watch out for the two zone systems. context.script:zone({shape = "SPHERE", ...}) uses the EliteScript engine with uppercase enums and Target spec tables. context.zones uses lowercase kind and plain origin/destination locations. Mixing these key styles can produce an empty or unintended zone, warnings, or a runtime error.


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