MagmaCore Lua Scripting Engine
MagmaCore provides the Lua sandbox and ScriptInstance runtime used by EliteMobs boss powers, EliteMobs NPC scripts, FreeMinecraftModels props, and authored enchantment effects. They share file validation and runtime ownership, but their available hooks, context tables, cooldown scopes, and lifetimes depend on the host. Enchantment queries also have a restricted context and provider-specific limits; use their own reference below.
FreeMinecraftModels props and EliteMobs NPC scripts inherit the generic MagmaCore context tables directly (context.world, context.zones, context.scheduler, context.cooldowns, context.log, context.event, context.player, and context.state) and add their owner table (context.prop or context.npc). EliteMobs boss powers run on the same runtime but expose richer boss-specific variants for several tables (context.world, context.zones, context.cooldowns, context.scheduler, context.log, context.event) plus boss-only helpers such as context.players, context.entities, context.vectors, context.settings, and context.script.
Currently, the following plugins use this engine:
- EliteMobs -- Lua powers for custom bosses (hooks like
on_boss_damaged_by_player,on_enter_combat, etc.) and NPC Lua scripts for interaction/proximity behavior (hooks likeon_npc_interact,on_npc_proximity_enter, etc.) - FreeMinecraftModels -- Lua scripts for props (hooks like
on_right_click,on_left_click, etc.) and authored enchantment effects for custom items
This page documents the generic MagmaCore engine features used by FreeMinecraftModels scripts, plus compatibility notes for EliteMobs where the same concepts have boss-specific or NPC-specific variants. For the exact EliteMobs boss context, use the EliteMobs Lua pages; for the exact NPC context, see NPC Lua Scripting.
Tiny Lua Primer
If you are completely new to Lua, here is the minimum syntax you need to write scripts for any Nightbreak plugin.
Variables
Use local to store a value:
local cooldown_key = "fire_burst"
local damage_multiplier = 1.5
local means the variable belongs only to this file or block.
Functions
Functions are reusable blocks of logic:
local function warn_player(player)
player:send_message("&cMove!")
end
Later, you can call it:
warn_player(some_player)
if checks
Use if when something should only happen sometimes:
if context.player == nil then
return
end
That means "if there is no player for this hook, stop here".
nil
nil means "no value". It is Lua's version of "nothing is here".
You will often check for nil with:
if context.event ~= nil then
-- do something with the event
end
~= means "is not equal to".
Tables
Lua uses tables for several jobs:
- Lists
- Objects with named keys
- The final returned script definition
Example of a table with named keys:
local particle = {
particle = "FLAME",
amount = 1,
speed = 0.05
}
Returning the script definition
At the end of every script file, you return one table:
return {
api_version = 1,
on_spawn = function(context)
end
}
That returned table is the script definition. The loader evaluates the file to validate it, then evaluates its stored source again for each runtime instance. File-local variables therefore belong to that instance, not to every entity using the same filename. Editing a file does not change an already-loaded definition or running instance; reload it through the owning plugin's workflow.
Comments
Use -- to write a note for humans:
-- This cooldown stops the attack from firing every hit
context.cooldowns:set_local(60, "fire_burst")
Lua Sandbox
All Lua scripts run inside a sandboxed LuaJ environment. Several globals that could access the filesystem or the Java runtime are removed. The sandbox rules are identical across all plugins using MagmaCore.
Removed Globals
The following standard Lua globals are set to nil and cannot be used:
| Removed | Why |
|---|---|
debug | Exposes internal VM state |
dofile | Filesystem access |
io | Filesystem access |
load | Arbitrary code loading |
loadfile | Filesystem access |
luajava | Direct Java class access |
module | Module system (not needed) |
os | Operating system access |
package | Module system (not needed) |
require | Module system / filesystem access |
Available Standard Library
The LuaJ standard environment provides these ordinary Lua facilities:
| Category | Functions |
|---|---|
| Math | math.abs, math.ceil, math.floor, math.max, math.min, math.random, math.sin, math.cos, math.sqrt, math.pi, and all other math.* functions |
| String | string.byte, string.char, string.find, string.format, string.gsub, string.len, string.lower, string.match, string.rep, string.sub, string.upper, and all other string.* functions |
| Table | table.insert, table.remove, table.sort, table.concat, and all other table.* functions |
| Iterators | pairs, ipairs, next |
| Type | type, tostring, tonumber, select, unpack |
| Error handling | pcall, xpcall, error, assert |
| Other | print, rawget, rawset, rawequal, rawlen, setmetatable, getmetatable |
os library is not availableThe os library is completely removed from the sandbox. Use context.world:get_time() for world time on the generic MagmaCore surfaces; EliteMobs boss powers receive it through their boss world table's shared base. For elapsed-time logic, store tick counters in context.state via the on_game_tick hook.
print writes to the server console, but prefer context.log:info(msg). Every surface exposes context.log: FreeMinecraftModels scripts and EliteMobs NPC scripts expose info, warn, and error; EliteMobs boss powers expose info, warn, and debug.
File Contract
Every Lua script must return a table. That table is intentionally strict.
Required and Optional Top-Level Fields
| Field | Required | Type | Notes |
|---|---|---|---|
api_version | Yes | Number | Currently must be 1 |
priority | No | Number | Defaults to 0 and is converted to an integer. EliteMobs boss and NPC scripts run lower values first. FMM validates the field, but prop scripts run in scripts: list order |
| supported hook keys | No | Function | Must use one of the exact hook names supported by the plugin |
Validation Rules
- The file must return a table.
api_versionis required and must currently be1.prioritymust be numeric if present.- Every extra top-level key must be a supported hook name.
- Every hook key must point to a function.
- Unknown top-level keys are rejected.
The Lua validator accepts priority for all MagmaCore-backed scripts. EliteMobs uses it for boss and NPC script order. FreeMinecraftModels currently does not sort by priority: props follow the config scripts: list order. Authored enchantments use their provider catalog and action/query lifecycle.
Where script files live
Each script provider selects its own directory. The generic Lua engine scans it recursively for .lua files and excludes archived configuration paths. It sorts normalized absolute paths (case-insensitive, then exact path), selects the first file for each lowercase filename, and logs skipped duplicates whose contents differ from the selected file. It does not fall back to a skipped duplicate if the selected file fails validation. Registered lookups retain the selected file's actual spelling, so use that spelling in references. Boss powers and enchantment catalogs have their own discovery rules; the shared validator does not make their directories interchangeable.
Scripts are keyed by file name, not by path. Two files called door.lua in different subfolders are the same key, and only one of them ends up registered. Give every script a distinct file name.
Helper functions and local constants should live above the final return, not inside the returned table unless they are actual hooks.
local ANIMATION_NAME = "idle"
local function do_something(context)
context.log:info("Doing something!")
end
return {
api_version = 1,
priority = 0,
on_spawn = function(context)
do_something(context)
end
}
Common Hook Names
These hook names appear across multiple Lua runtimes. Each script type enables only the subset that makes sense for that owner: FMM prop scripts use all of these, EliteMobs boss powers use their boss hook list, and EliteMobs NPC scripts use on_spawn, on_remove, on_game_tick, on_npc_interact, on_npc_proximity_enter, on_npc_proximity_leave, on_zone_enter, and on_zone_leave.
| Hook | When it fires |
|---|---|
on_spawn | Dispatched by the owning plugin's spawn lifecycle for FMM props, EliteMobs bosses, and NPC scripts; constructing a ScriptInstance alone does not fire it |
on_game_tick | Called each server tick while the initialized instance is active and not paused |
on_zone_enter | Called when a runtime-supported zone reports an enter event. FMM and EliteMobs NPC zone watches are player-based; EliteMobs top-level boss zone hooks come from EliteScript zone events. |
on_zone_leave | Called when a runtime-supported zone reports a leave event. FMM and EliteMobs NPC zone watches are player-based; EliteMobs top-level boss zone hooks come from EliteScript zone events. |
Method Syntax: : vs .
In Lua, object:method(arg) is shorthand for object.method(object, arg). MagmaCore's context-table methods strip that implicit first argument, so both forms work for methods such as context.log.info:
context.log:info("hello")
context.log.info("hello") -- same thing
Global helper functions under em are plain functions rather than context-table methods, so call them with ., as documented: em.create_location(...), em.zone.create_sphere_zone(...), and em.location.is_protected(...). EliteMobs event setters context.event.set_damage_amount(...) and context.event.multiply_damage_amount(...) also require .. The documentation uses : for context-table methods and . for plain functions.
Execution Budget
Every hook invocation, every scheduled callback, and the initial evaluation of the script file itself run under a hard execution budget. The budget is enforced inside the Lua VM, so it applies while your code is still running rather than only checking after the call returns.
The ordinary runtime applies these limits:
| Limit | Value |
|---|---|
| Current-thread CPU time | 50 milliseconds |
| Lua instructions executed | 250,000 |
Exceeding a limit raises a Lua error. The instruction counter is checked on each Lua instruction; the clock is checked every 1,024 instructions and after the call returns. This does not interrupt an in-progress Java/Bukkit method, and 50 milliseconds of CPU is not a 50-millisecond wall-clock deadline. During a hook or scheduled callback, an uncaught error closes that script instance. During initial file validation, the definition is rejected. If current-thread CPU timing is unavailable, the clock falls back to 250 milliseconds elapsed time with the same instruction limit. Enchantment query batches can impose smaller allowances.
The VM interrupts an ordinary while true do end as it executes Lua instructions. Such a loop still consumes server-thread time and closes the instance when the error escapes. Keep loops bounded even though the watchdog exists.
Nested calls share one budget: if a hook invokes a callback that invokes another, the whole chain is measured as a single 50ms-of-CPU / 250,000-instruction allowance, not one allowance each.
To stay within budget:
- Avoid unbounded loops inside hooks.
- Keep
on_game_tickhandlers lightweight -- they run every single tick. - Use
context.scheduler:run_repeating(...)to spread work across ticks. - Move expensive work behind
context.cooldownsor a reasonable interval.
An uncaught runtime error in a hook or scheduled callback closes that instance and cancels its owned work. Fix and reload the definition, then let the owning plugin create a new instance. Saving the file alone does not repair the closed instance. Enchantment hosts also apply their own failure-suppression policy.
context.state
A plain Lua table that persists for the script instance's entire lifetime. Use it to store flags, counters, task IDs, toggle states, and any data you need to share between hooks.
return {
api_version = 1,
on_spawn = function(context)
context.state.is_open = false
context.state.click_count = 0
context.state.task_id = nil
end,
on_right_click = function(context)
context.state.click_count = (context.state.click_count or 0) + 1
end
}
Use context.state for data shared across an instance's hooks. It is in memory, not saved across reloads or restarts. Other context tables are built lazily and can contain snapshots. FreeMinecraftModels caches context.prop, but fields such as current_location are read lazily. Read a live field again to refresh it; a table already saved in a variable does not update itself. Assigning to a lazy field shadows its getter.
context.log
Console logging methods, available on every script surface. Generic log.info messages carry a [Lua] prefix; include a script label in your message when you need to identify its source. Error diagnostics identify the failing script, but ordinary logging does not automatically add its filename. FreeMinecraftModels scripts and EliteMobs NPC scripts expose info, warn, and error; EliteMobs boss powers expose info, warn, and debug.
| Method | Notes |
|---|---|
log:info(message) | Informational message |
log:warn(message) | Warning message |
log:error(message) | Warning-level message (logged at WARN level, same as log:warn) |
The EliteMobs context.log registers debug instead of error. Use log:debug(message) for debug output (appears as info-level with a debug prefix).
context.log:info("Script loaded!")
context.log:warn("Something unexpected happened")
-- Generic/FMM/NPC logger:
context.log:error("Critical failure in zone setup")
-- EliteMobs boss logger:
context.log:debug("Boss debug detail")
context.cooldowns
The cooldowns table manages time-based cooldowns. Local cooldowns and generic global cooldowns convert each configured tick to 50 milliseconds of monotonic elapsed time, including pauses and low server TPS. Boss global cooldowns instead count server ticks. There are two scopes:
- Local cooldowns use exact string keys in the owner's store, whose scope is listed below. Pass
nilfor the default key,__lua:followed by the script filename. - Global cooldowns are shared across all scripts on the same owner entity where the runtime exposes global cooldown methods (e.g. all scripts on the same prop, or all Lua powers on the same boss).
cooldowns:check_local(key?, duration)
Checks a local key and, if ready, starts it and returns true; otherwise returns false. This is one synchronous Lua call. It does not check or start the global cooldown. The store is not a separate cross-thread locking API.
| Parameter | Type | Notes |
|---|---|---|
key | string (optional) | Cooldown identifier. nil selects the default key. |
duration | int | Cooldown duration in ticks (20 = 1 second) |
on_right_click = function(context)
-- Only allow this action once every 3 seconds
if not context.cooldowns:check_local("interact", 60) then
return
end
-- ... do the action
end
All script surfaces — boss powers, NPC scripts, and FreeMinecraftModels scripts — use the shared order shown above: check_local(key?, duration). To use the default key, pass check_local(nil, 60); check_local(60) does not mean a 60-tick default cooldown. (Earlier experimental NPC builds used check_local(duration, key?); update any old NPC scripts to the shared order.)
cooldowns:local_ready(key?)
Returns true if the local cooldown has expired (or was never set), false if still active.
cooldowns:local_remaining(key?)
Returns the number of ticks remaining on the local cooldown, or 0 if ready.
cooldowns:set_local(duration, key?)
Sets a local cooldown without checking if one is already active. Use this for unconditional cooldown resets.
| Parameter | Type | Notes |
|---|---|---|
duration | int | Duration in ticks. Pass 0 or negative to clear. |
key | string (optional) | Cooldown identifier. nil selects the default key. |
cooldowns:global_ready()
Returns true if the global cooldown (shared across all scripts on the same entity) has expired.
cooldowns:set_global(duration)
Sets the global cooldown. On generic MagmaCore tables, zero or a negative duration clears it. Boss global cooldowns keep the later expiry: a shorter duration never shortens an active global cooldown, and zero or a negative duration does nothing. Neither setting a local cooldown nor setting a global one automatically sets the other.
| Parameter | Type | Notes |
|---|---|---|
duration | int | Duration in ticks |
The "owner" of a global cooldown depends on the script type:
- EliteMobs Lua powers — shared per
EliteEntity(one bucket per boss).global_ready()/set_global()use the boss's built-in power cooldown system; local cooldowns are also shared across all powers on the same boss. - EliteMobs NPC scripts — both global cooldowns and explicit local keys are shared through the one adapter for that NPC. Two attached scripts using the same local key affect each other; omitted keys remain filename-specific.
- FMM prop scripts — global cooldowns are shared per placed prop; each attached script has its own adapter and local store.
- Enchantment actions — global cooldowns are shared per player within the provider; local cooldowns belong to the player and enchantment ID and survive equipment swaps and completed actions. See authored enchantments.
context.scheduler
The scheduler lets you run delayed and repeating tasks. Tasks created through it belong to the script instance and are cancelled on shutdown. This does not undo spawned entities, world edits, or timers created by separate UI/effect helpers. During a pause, a due one-shot task waits for resume and a repeating task skips its paused calls.
This section uses the MagmaCore scheduler names (run_later, run_repeating, cancel). The EliteMobs names (run_after(ticks, callback), run_every(interval, callback), cancel_task(taskId)) are accepted as aliases on every surface, so either style works in any script.
scheduler:run_later(ticks, callback)
Runs a callback once after a delay. Returns a numeric task ID.
| Parameter | Type | Notes |
|---|---|---|
ticks | int | Delay in server ticks (20 ticks = 1 second) |
callback | function | Called with a fresh context when the delay expires |
context.scheduler:run_later(40, function(delayed_context)
delayed_context.log:info("2 seconds have passed!")
end)
scheduler:run_repeating(delay, interval, callback)
Runs a callback repeatedly at a fixed interval. Returns a numeric task ID.
| Parameter | Type | Notes |
|---|---|---|
delay | int | Initial delay in ticks before the first run |
interval | int | Ticks between each subsequent run |
callback | function | Called with a fresh context each interval |
context.state.particle_task = context.scheduler:run_repeating(0, 20, function(tick_context)
tick_context.log:info("Another second passed!")
end)
scheduler:cancel(taskId)
Cancels a task owned by this instance by its ID. An unknown or another instance's ID does nothing.
| Parameter | Type | Notes |
|---|---|---|
taskId | int | The task ID returned by run_later or run_repeating |
Shutdown automatically cancels owned tasks. Cancel earlier when an interaction or combat phase ends and the instance remains alive, for example in a boss's on_exit_combat hook. Either scheduler naming family works. Use the owning plugin's lifecycle hooks; an error closes the instance without promising a custom cleanup hook.
Scheduled callbacks receive a fresh context without the triggering event or actor. Use that argument, not the outer context. If later work needs the original player, capture its UUID during the hook and resolve it again, checking that it is still available. A retained outer context can also resolve a lazy event field after the original event has ended.
context.world
The world table provides methods for querying and interacting with the Minecraft world. It captures the owner's current world when first built; storing the table does not make it follow later world changes.
The methods here describe the shared MagmaCore table received by FMM props and EliteMobs NPC scripts, including damaging strike_lightning(...). FMM adds optional EliteMobs transport and loot helpers; see context.world: FreeMinecraftModels Additions. EliteMobs adds loaded-chunk helpers for visual lightning, NPC spawning, and boss spawning to its shared tables. Boss powers then override or add their own methods; see EliteMobs World & Environment.
context.world accepts both coordinate-style calls (e.g. strike_lightning(x, y, z), spawn_particle(name, x, y, z, ...)) and location-table-style overloads (e.g. strike_lightning_at_location(loc), spawn_particle_at_location(loc, ...)) on the generic surfaces. EliteMobs boss powers also support both broad styles, but some location-table methods have boss-specific behavior.
Shared location arguments require x, y, and z. An omitted or unresolved world uses the table's world. Shared location-based particle calls still use that captured world, so supply a same-world location. Boss-specific converters and em.location.* queries follow different rules described in their references.
The complete World Table reference also documents entity lookup, passability/protection queries, colored dust, terrain impacts, and location-table overloads.
world.name
A string field containing the world name.
world:get_block_at(x, y, z)
Returns the material name of the block at the given coordinates as a lowercase string (e.g. "stone", "air"). If the target chunk is not loaded, this returns "air" instead of force-loading the chunk.
| Parameter | Type | Notes |
|---|---|---|
x | int | Block X coordinate |
y | int | Block Y coordinate |
z | int | Block Z coordinate |
world:set_block_at(x, y, z, material)
Sets the block at the given coordinates. Runs on the main thread and avoids force-loading chunks.
| Parameter | Type | Notes |
|---|---|---|
x | int | Block X coordinate |
y | int | Block Y coordinate |
z | int | Block Z coordinate |
material | string | Bukkit Material name, lowercase (e.g. "stone", "air", "oak_planks") |
Returns true when the material name is valid and the set was queued, false when the material is invalid. If the target chunk is unloaded by the time the queued task runs, the block change is skipped.
This does not automatically check region protection. Use a block material and the relevant protection query before placing it; queue acceptance does not prove the block was changed.
world:spawn_particle(particle, x, y, z, count, dx, dy, dz, speed)
Spawns particles at a location.
| Parameter | Type | Default | Notes |
|---|---|---|---|
particle | string | required | Bukkit Particle enum name; the lookup uppercases it (e.g. "FLAME", "HAPPY_VILLAGER") |
x | number | required | X coordinate |
y | number | required | Y coordinate |
z | number | required | Z coordinate |
count | int | 1 | Number of particles |
dx | number | 0 | X spread/offset |
dy | number | 0 | Y spread/offset |
dz | number | 0 | Z spread/offset |
speed | number | 0 | Particle speed |
Some particle types require extra data that is not supported by this simple API. DUST and FALLING_DUST, for example, need additional data. Use data-free alternatives such as CLOUD, SMOKE, CAMPFIRE_COSY_SMOKE, SNOWFLAKE, FLAME, HAPPY_VILLAGER, HEART, NOTE, or ENCHANT. For colored dust, use the dedicated world:spawn_dust(...) helper in the World Table reference.
Because the simple API does not provide Bukkit particle data objects, data-required particles may throw an underlying Bukkit exception and disable the script instance. Prefer the data-free particle names above for FMM/MagmaCore scripts.
world:play_sound(sound, x, y, z, volume, pitch)
Plays a sound at a location.
| Parameter | Type | Default | Notes |
|---|---|---|---|
sound | string | required | Bukkit Sound enum name (e.g. "ENTITY_EXPERIENCE_ORB_PICKUP") or a custom resource-pack sound key (e.g. "mypack:boss.roar") |
x | number | required | X coordinate |
y | number | required | Y coordinate |
z | number | required | Z coordinate |
volume | number | 1.0 | Volume |
pitch | number | 1.0 | Pitch |
The name is matched against Bukkit's registered sounds first (case-insensitive, so "ENTITY_ARROW_SHOOT" and "entity_arrow_shoot" are the same). If nothing matches, it is normalized to a lowercase namespaced key and played as a custom resource-pack sound. A value that is neither a known sound nor a valid namespaced key plays nothing instead of raising an error, so a typo silently does nothing rather than disabling your script.
The same applies to world:play_sound_at_location(loc, sound, volume, pitch).
world:strike_lightning(x, y, z)
Strikes lightning at a location (visual and damaging).
| Parameter | Type | Notes |
|---|---|---|
x | number | X coordinate |
y | number | Y coordinate |
z | number | Z coordinate |
world:get_time()
Returns the current world time in ticks.
world:set_time(ticks)
Sets the world time.
| Parameter | Type | Notes |
|---|---|---|
ticks | int | World time (0 = dawn, 6000 = noon, 13000 = night, 18000 = midnight) |
world:get_nearby_entities(x, y, z, radius)
Returns a one-based array of entity wrapper tables within an axis-aligned box centered at the coordinates. It does not exclude the script owner.
| Parameter | Type | Notes |
|---|---|---|
x | number | Center X |
y | number | Center Y |
z | number | Center Z |
radius | number | Search radius (used as half-extent in all three axes) |
This returns ALL entities in range, including non-living entities (dropped items, projectiles, etc.). Always guard with if entity.damage then before calling living-entity methods.
world:get_nearby_players(x, y, z, radius)
Returns a one-based array of player wrapper tables within an axis-aligned box, using radius as every half-extent. It does not filter player game modes.
| Parameter | Type | Notes |
|---|---|---|
x | number | Center X |
y | number | Center Y |
z | number | Center Z |
radius | number | Search radius |
world:spawn_entity(entity_type, x, y, z)
Spawns a vanilla Minecraft entity at the given location.
| Parameter | Type | Notes |
|---|---|---|
entity_type | string | Bukkit entity type name, lowercase (e.g. "zombie", "skeleton", "pig") |
x | number | X coordinate |
y | number | Y coordinate |
z | number | Z coordinate |
Returns an entity table with living methods when applicable, or nil for an unknown enum name. A valid but nonspawnable type can raise an error. This method has no loaded-chunk guard.
world:get_highest_block_y(x, z)
Returns Bukkit's highest-block Y at the given X/Z position. Unlike get_block_at, this query has no loaded-chunk guard.
| Parameter | Type | Notes |
|---|---|---|
x | int | Block X coordinate |
z | int | Block Z coordinate |
world:raycast(from_x, from_y, from_z, dir_x, dir_y, dir_z, [max_distance], [fluid_mode], [blocks_only], [loaded_only])
Casts a ray from a point in a direction and returns information about what it hits.
| Parameter | Type | Default | Notes |
|---|---|---|---|
from_x | number | required | Origin X |
from_y | number | required | Origin Y |
from_z | number | required | Origin Z |
dir_x | number | required | Direction X component |
dir_y | number | required | Direction Y component |
dir_z | number | required | Direction Z component |
max_distance | number | 50 | Maximum ray distance |
fluid_mode | string | "NEVER" | NEVER, SOURCE_ONLY, or ALWAYS |
blocks_only | boolean | false | Ignore entities and trace blocks only |
loaded_only | boolean | false | Return nil rather than raycast when any chunk in the ray bounding rectangle is unloaded |
With loaded_only=true, also guard against a nil result. Otherwise a miss returns a table whose hit fields are nil:
| Field | Type | Notes |
|---|---|---|
hit_entity | entity table or nil | The first entity hit by the ray, or nil if none |
hit_location | location table or nil | The exact point where the ray hit something |
hit_block | table or nil | {x, y, z, material} of the block hit, or nil if no block was hit |
Block materials are lowercase. The combined trace uses entity ray size 0.5 without an entity filter. The loaded-chunk guard checks the whole bounding rectangle, so it can reject an unloaded chunk away from the exact ray. An invalid fluid-mode name raises an error.
world:spawn_firework(x, y, z, colors, [type], [power])
Spawns a firework rocket at the given location.
| Parameter | Type | Default | Notes |
|---|---|---|---|
x | number | required | X coordinate |
y | number | required | Y coordinate |
z | number | required | Z coordinate |
colors | table | required | Array of color strings, e.g. {"RED", "BLUE", "WHITE"} |
type | string | "BALL" | Firework shape: "BALL", "BALL_LARGE", "STAR", "BURST", "CREEPER" |
power | int | 1 | Flight power, 0-127 |
-- Example: red and yellow firework burst
context.world:spawn_firework(loc.x, loc.y, loc.z, {"RED", "YELLOW"}, "BURST", 2)
world:place_temporary_block(x, y, z, material, [ticks], [require_air])
Places a temporary block, restoring saved BlockData after a positive duration. It does not capture the full block state or inventory.
| Parameter | Type | Default | Notes |
|---|---|---|---|
x | int | required | Block X coordinate |
y | int | required | Block Y coordinate |
z | int | required | Block Z coordinate |
material | string | required | Bukkit Material name (e.g. "stone", "ice") |
ticks | int | 0 | Duration in ticks before restoration. Zero or negative places the block permanently without tracking. |
require_air | boolean | false | If true, only places the block if the target is air |
Returns false if the material is invalid or the target chunk is unloaded. Otherwise returns true once the placement request is accepted. If require_air is true and the target block is not air, the manager skips the replacement even though the request itself was accepted.
With positive ticks, the manager saves the original block data and restores it when the timer ends, when the chunk or world unloads, or when the plugin shuts down. It restores only while the block still holds the temporary block, so a later edit is not overwritten. Placing again on a block that still holds a temporary block restarts its timer and keeps the first saved original. Blocks with block entities, such as chests and signs, and blocks already held by another temporary-block owner, such as an enchantment lease, are skipped. Breaking a temporary block drops nothing and ends tracking without restoring; placing a block there also ends tracking.
These timers are not cancelled with the Lua script, and placement does not automatically check region protection.
world:drop_item(x, y, z, material, [amount])
Drops an item entity at the given location with natural scatter.
| Parameter | Type | Default | Notes |
|---|---|---|---|
x | number | required | X coordinate |
y | number | required | Y coordinate |
z | number | required | Z coordinate |
material | string | required | Bukkit Material name |
amount | int | 1 | Stack size |
Returns an entity table for the dropped item, or nil if the material was invalid.
context.zones
The zones table lets scripts create spatial zones and watch them for player enter/leave events. FMM props and EliteMobs NPC scripts use the generic create_sphere(...) / create_cylinder(...) / create_cuboid(...) plus watch(...) / unwatch(...) methods documented here, firing the on_zone_enter / on_zone_leave hooks. Zones are tied to the script instance and cleaned up automatically when the script instance is destroyed. EliteMobs boss powers use a separate boss-specific zone table with methods such as get_entities_in_zone(...), get_locations_in_zone(...), and watch_zone(...); see EliteMobs Zones & Targeting.
zones:create_sphere(x, y, z, radius)
Creates a sphere zone centered at the coordinates. Returns a numeric handle, or nil if the owner has no world.
| Parameter | Type | Notes |
|---|---|---|
x | number | Center X |
y | number | Center Y |
z | number | Center Z |
radius | number | Sphere radius |
zones:create_cylinder(x, y, z, radius, height)
Creates a cylinder zone. Returns a numeric handle, or nil if the owner has no world.
| Parameter | Type | Notes |
|---|---|---|
x | number | Center X |
y | number | Center Y (base) |
z | number | Center Z |
radius | number | Cylinder radius |
height | number | Cylinder height |
zones:create_cuboid(x, y, z, xSize, ySize, zSize)
Creates a cuboid zone. Returns a numeric handle, or nil if the owner has no world. Generic watches test the player's location point, so these sizes are half-extents around the center. This differs from the entity-overlap and sampled-location operations used by EliteMobs boss zones. If zSize is zero and xSize is nonzero, the shared shape substitutes xSize for Z.
| Parameter | Type | Notes |
|---|---|---|
x | number | Center X |
y | number | Center Y |
z | number | Center Z |
xSize | number | Half-extent in X |
ySize | number | Half-extent in Y |
zSize | number | Half-extent in Z |
zones:watch(handle, onEnterCallback, onLeaveCallback)
Enables enter/leave dispatch for an already-created zone. The callback parameters must be Lua functions, but their bodies are not invoked directly. Logic runs through on_zone_enter / on_zone_leave. Membership tracking starts at creation, so adding watch later does not report an entry for someone already tracked inside. Passing nil on a later watch call does not clear a previously enabled dispatcher.
| Parameter | Type | Notes |
|---|---|---|
handle | int | Zone handle from a create_* call |
onEnterCallback | function or nil | A function enables the on_zone_enter hook for this zone; the function body is ignored |
onLeaveCallback | function or nil | A function enables the on_zone_leave hook for this zone; the function body is ignored |
Returns true if the watch was set up successfully, nil if the zone handle was invalid.
Zone checks run before on_game_tick against player location points in the owner's current world, not whole-player bounding boxes. Sphere points must be strictly inside the radius; cylinder points must be inside the radius and between the inclusive base and exclusive top. Zones retain their creation coordinates and world. Disconnecting or removing a watch does not guarantee a leave hook. Keep zone counts reasonable.
zones:unwatch(handle)
Stops watching a zone and cleans up its resources.
| Parameter | Type | Notes |
|---|---|---|
handle | int | Zone handle to stop watching |
Example: Proximity trigger zone
return {
api_version = 1,
on_spawn = function(context)
local loc = context.prop.current_location
if loc == nil then return end
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, 5)
-- Pass placeholder functions to enable on_zone_enter and on_zone_leave hooks.
-- The placeholder bodies are ignored; the actual logic goes in the hooks below.
context.zones:watch(handle, function() end, function() end)
context.state.zone_handle = handle
end,
on_zone_enter = function(context)
context.log:info("Player entered zone!")
end,
on_zone_leave = function(context)
context.log:info("Player left zone!")
end,
on_destroy = function(context)
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
end
end
}
context.event
The event table is present when the current hook has an event or an actor. FMM prop clicks use a plugin cancellation flag; NPC interactions and generic zone hooks can supply an actor without a Bukkit event. It is not present during on_game_tick, scheduled callbacks, or lifecycle hooks that have no triggering player.
FMM prop on_projectile_hit hooks receive the cancellable FreeMinecraftModels projectile-hit event, so cancel() and uncancel() are present. The hook runs after FMM has handled the hit, so cancelling does not stop a non-piercing projectile from being used up. It does not fire when another plugin already cancelled the hit. context.event.player is the shooter when it is a living entity, which can be a mob, and is absent otherwise; context.player is set only when a player fired the projectile. The hook also exposes context.projectile as an entity table for the projectile.
For generic on_zone_enter / on_zone_leave hooks, context.player and context.event.player identify the player entering or leaving the zone. Those zone hooks are not backed by a cancellable Bukkit event, so cancel() and uncancel() are not present there.
| Field / Method | Type | Notes |
|---|---|---|
is_cancelled | boolean | Cancellation state when the event subtable was first resolved. Calling cancel() or uncancel() does not refresh it. |
cancel() | method | Cancels the event; present only for cancellable events |
uncancel() | method | Un-cancels the event; present only for cancellable events |
player | entity table | The player involved in the event, if any |
on_right_click = function(context)
if context.event and context.event.cancel then
context.event:cancel() -- prevent the default right-click interaction
end
end
EliteMobs power scripts have a more detailed event table with damage amounts, damage causes, damager references, and damage modification methods. See Lua API Reference for the full EliteMobs event fields.
em Helper Namespace
The em table is available at file load time. Its constructors build data, without loading worlds, registering watches, or validating a complete shape. Returned setters mutate their table and retain supplied location tables by reference.
| Function | Purpose |
|---|---|
em.create_location(x, y, z [, world, yaw, pitch]) | Create a location table with optional world name, yaw, and pitch. The returned table also has an .add(dx, dy, dz) method that offsets the location in-place and returns itself for chaining. |
em.create_vector(x, y, z) | Create a vector table |
em.zone.create_sphere_zone(radius) | Create a sphere zone definition |
em.zone.create_dome_zone(radius) | Create a dome zone definition |
em.zone.create_cylinder_zone(radius, height) | Create a cylinder zone definition |
em.zone.create_cuboid_zone(x, y, z) | Create a cuboid zone definition |
em.zone.create_cone_zone(length, radius) | Create a cone zone definition |
em.zone.create_static_ray_zone(length, thickness) | Create a static ray zone definition |
em.zone.create_rotating_ray_zone(length, point_radius, animation_duration) | Create a rotating ray zone definition |
em.zone.create_translating_ray_zone(length, point_radius, animation_duration) | Create a translating ray zone definition |
em.location.is_in_dungeon(loc) | Check if a location is inside any registered dungeon region (any owner reporting the "dungeon" kind) |
em.location.is_protected(loc) | Check if a location is inside any protected region (WorldGuard, GriefPrevention, EM regions, etc.) |
em.location.owned_by(loc, namespace) | Check whether a specific plugin namespace owns the location (e.g. "EliteMobs") |
em.location.owners(loc) | Array of namespace strings that own the location |
em.location.kinds_at(loc) | Array of kind tags at the location ("elitemobs", "dungeon", "world_package", "open_world_dungeon", dungeon size category) |
em.location.has_kind(loc, kind) | Check if the location is tagged with the given kind |
MagmaCore ships WorldGuard and GriefPrevention adapters. Consumers initialize them during enable, and a protection query can initialize them lazily. Queries include registered providers, not every possible protection plugin. A failing registered region adapter reports protected. With no provider or protection ownership, the query returns false; warnings identify missing providers.
For em.location.*, supply a loaded world name or {name=...} in world. There is no owner-world fallback. A missing/unresolved world or nil input returns false or an empty array; missing coordinates default to zero, while malformed numeric fields can raise an error. Ownership namespaces are case-sensitive and kind tags are case-insensitive. Ownership alone does not imply dungeon membership or protection. Returned arrays have no guaranteed order.
Zone builders return chainable tables with :set_center(loc) (or :set_origin(loc) / :set_destination(loc) depending on zone type):
-- At file scope: create a reusable zone shape
local blast_zone = em.zone.create_sphere_zone(5)
return {
api_version = 1,
on_spawn = function(context)
-- Anchor the zone at call time
blast_zone:set_center(context.boss:get_location())
local entities = context.zones:get_entities_in_zone(blast_zone)
-- ...
end
}
The em namespace is particularly useful in EliteMobs where zone definitions are used with context.zones:get_entities_in_zone() and context.script. In FreeMinecraftModels, the context.zones:create_sphere(...) / create_cylinder(...) / create_cuboid(...) methods are more commonly used for simple watch-based zones.
Entity Tables
The tables below introduce the common methods. The complete Lua API Reference also covers projectile fields, movement-path checks, ownership, and protection helpers. They apply to generic Entity Tables; EliteMobs power-specific wrappers have their own section in that reference.
Entity tables are returned from world queries and event data. They provide a layered set of fields and methods depending on the entity type.
Entity Base Fields
| Field | Type | Notes |
|---|---|---|
uuid | string | The entity's UUID |
entity_type | string | The entity type (e.g. "player", "zombie", "skeleton", "villager") |
is_valid | boolean | Whether the entity reference is still valid |
is_dead | boolean | Whether the entity is dead |
is_player | boolean | Whether the entity is a player |
is_hostile | boolean | Whether the entity implements Bukkit Monster, not its current aggression |
is_passive | boolean | Whether the entity implements Bukkit Animals, not a guarantee of passive behavior |
current_location | location table | The entity's current position (x, y, z, world, yaw, pitch) |
world | string | The world name the entity is in |
Entity Base Methods
| Method | Returns | Notes |
|---|---|---|
teleport(location_table) | void | Requires x, y, z; an omitted or unresolved world falls back to the entity's world. yaw and pitch default to zero. No automatic movement-path or protection check. |
remove() | void | Removes the entity from the world. Only acts if the entity is still valid. |
set_silent(flag) | void | Suppresses or re-enables the entity's sounds. |
set_invulnerable(flag) | void | Makes the entity invulnerable or vulnerable to damage. |
set_gravity(flag) | void | Enables or disables gravity for the entity. |
set_glowing(flag) | void | Toggles the glowing outline effect on the entity. |
Living Entity Fields
Living entities (players, mobs, etc.) have all entity base fields plus:
| Field | Type | Notes |
|---|---|---|
health | number | Current health |
maximum_health | number | Maximum health |
name | string | Display name |
is_alive | boolean | Whether the entity is alive |
Living Entity Methods
| Method | Returns | Notes |
|---|---|---|
damage(amount) | void | Deals the given amount of damage to the entity |
push(x, y, z) | void | Immediately adds the vector to current velocity |
set_facing(x, y, z) | void | Teleports in place with a new facing direction |
add_potion_effect(effect, duration, amplifier) | void | Adds a potion effect. effect is a string (e.g. "speed", "slowness", "regeneration"). duration is in ticks. amplifier is the effect level minus 1 (0 = level I). |
remove_potion_effect(effect) | void | Removes a potion effect by name |
get_scale() | number | Returns the effective scale including modifiers, or 1.0 when unavailable |
set_scale(value) | void | Changes the scale attribute's base value when available |
Not all entities returned by get_nearby_entities() are living entities. You can use entity.is_player, entity.is_hostile, or entity.is_passive to filter by category, or check if entity.damage then before calling living-entity methods like damage(), push(), or add_potion_effect().
Plugin Integration Fields
Entity tables can include plugin integration fields. Without the relevant enricher, they are absent and read as nil. Tracking flags, model identity, operator state, and most nested data are snapshots taken when built; they do not automatically follow later tracking changes. The shared EliteMobs health fields are lazy, and model/permission methods query their backing objects when called.
EliteMobs Fields
Available when EliteMobs is installed.
| Field | Type | Notes |
|---|---|---|
is_elite | boolean | Whether the entity is an EliteMobs elite |
is_custom_boss | boolean | Whether the entity is an EliteMobs custom boss (also available inside the elite subtable) |
is_significant_boss | boolean | Whether the entity is a custom boss with a health multiplier over 1 (i.e. a designed encounter, not a filler elite) |
elite | table or nil | Elite info subtable (see below). nil if the entity is not an elite. |
The elite subtable contains:
| Field | Type | Notes |
|---|---|---|
elite.level | int | The elite's level |
elite.name | string or nil | The elite's display name |
elite.health | number | Current health |
elite.max_health | number | Maximum health |
elite.is_custom_boss | boolean | Whether this is a custom boss (as opposed to a natural elite) |
elite.health_multiplier | number | Config-defined health multiplier |
elite.damage_multiplier | number | Config-defined damage multiplier |
elite:remove() | void | Removes the elite through EliteMobs' removal pipeline; this is not a kill or a promise of loot |
Example: Damage elites differently
local entities = context.world:get_nearby_entities(x, y, z, 5)
for _, entity in ipairs(entities) do
if entity.is_elite then
-- Deal double damage to elites
entity:damage(20)
context.log:info("Hit elite: " .. entity.elite.name .. " (level " .. entity.elite.level .. ")")
elseif entity.is_hostile then
entity:damage(10)
end
end
FreeMinecraftModels Fields
Available when FreeMinecraftModels is installed.
| Field | Type | Notes |
|---|---|---|
is_modeled | boolean | Whether the entity has an FMM model attached (DynamicEntity or PropEntity) |
is_prop | boolean | Whether the entity is an FMM prop (static decorative entity) |
model | table or nil | Model info subtable (see below). nil if the entity is not modeled. |
The model subtable contains:
| Field | Type | Notes |
|---|---|---|
model.model_id | string or nil | The model blueprint ID (e.g. "torch_01") |
model.is_dynamic | boolean | Whether the modeled entity is a DynamicEntity (mob/boss model) rather than a static prop |
model:play_animation(name, [blend], [loop]) | boolean | Accepts a named animation request. blend and loop default to false; queueing or a suppressed attack can return true without immediate playback. Terminal death rejects playback. |
model:stop_animations() | void | Clears queued playback and returns to idle when available; terminal death is unaffected. |
model:remove() | void | Removes the modeled entity through FMM's proper removal pipeline. |
On player entity tables, the FreeMinecraftModels enricher also adds:
| Field / Method | Type | Notes |
|---|---|---|
is_op | boolean | Whether the player is a server operator. |
has_permission(node) | boolean | Whether the player has the permission node. |
These two player values exist only in FreeMinecraftModels scripts. On shared tables built inside EliteMobs, EliteMobs's own FreeMinecraftModels adapter adds is_modeled, is_prop, and model, but not is_op or has_permission.
FMM player tables also expose is_holding_elitemobs_item(filename). It checks the main-hand EliteMobs filename tag, including .yml, rather than the display name. Empty hands, other items, and an unavailable integration return false. EliteMobs's own shared enricher separately adds elite_player with the player's combat tier when its data is available; FMM's optional EliteMobs adapter does not add that subtable.
The model bridge (available on any entity) defaults blend and loop to false. The prop table play_animation defaults both to true for queued, looped prop playback. blend controls queueing; it does not cross-fade animations.
Example: Filter entities by type
local entities = context.world:get_nearby_entities(x, y, z, 10)
for _, entity in ipairs(entities) do
if entity.is_prop then
-- Skip props
elseif entity.is_player then
entity:damage(5)
elseif entity.entity_type == "villager" then
-- Don't hurt villagers
elseif entity.is_elite then
entity:damage(20)
elseif entity.is_hostile then
entity:damage(10)
end
end
Player-Specific Fields
Player entities have all entity and living entity fields plus:
| Field | Type | Notes |
|---|---|---|
game_mode | string | The player's game mode ("creative", "survival", "adventure", "spectator") |
Player-Specific Methods
Anywhere the API says "supports color codes", Magmacore runs the string through its built-in MiniMessage parser. Legacy & codes, &#RRGGBB hex, <#RRGGBB>, <gradient>, <rainbow> and the rest of the MiniMessage tag set all work and can be mixed freely. Kyori Adventure is not a dependency and no Adventure API is available to scripts -- you do not need it.
The Lua text methods convert down to a legacy color-coded string, so colors, gradients and decorations render correctly while interactive tags (<hover>, <click>, <insert>, <font>, <key>, <lang>) are parsed but have no effect. See the shared Color Codes guide for the common formats.
| Method | Returns | Notes |
|---|---|---|
send_message(msg) | void | Sends a chat message to the player. Supports color codes. |
chat(text) | void | Sends color-processed text through Bukkit player chat, including slash-prefixed commands as that player |
get_held_item() | table or nil | Returns {type, amount, display_name} for the item in the player's main hand, or nil if empty. type is the uppercase Bukkit Material name, such as "GOLD_INGOT". |
consume_held_item(amount?) | void | Queues consumption from the main hand as it exists when the task runs. Defaults to 1; it neither retains a checked item nor confirms payment. Supply a positive amount; negative values are not rejected. |
has_item(material, amount?) | boolean | Returns true if the player has at least amount (default 1) of the given material anywhere in their inventory |
get_target_entity([range]) | entity table or nil | Traces from the eye against blocks and entities, excluding the player, with entity ray size 0.5. Ignores fluids/passable blocks; default range 50. |
get_eye_location() | location table | Returns a location table at the player's eye height |
get_look_direction() | table | Returns a {x, y, z} direction vector for where the player is looking |
send_block_change(x, y, z, material, [ticks]) | boolean | Queues a client-only block, with reset after positive ticks. True means queued; invalid/non-block materials return false. |
reset_block(x, y, z) | boolean | Queues restoration of the real block for this player and returns true |
sleep(x, y, z) | void | Queues a real white bed and an attempt to sleep. On waking or disconnect, restores only BlockData, not block-entity contents. |
wake_up() | void | Wakes up a sleeping player. |
Queued block changes and resets read the player's world when executing; do not rely on them restoring the intended block after a world change. Sleep cleanup checks after 20 ticks, then every 10 ticks. These tasks are separate from context.scheduler ownership.
Player UI Methods
These are the shared Player Table methods. Boss-power player tables have different UI signatures, listed in the API reference. Shared bars are keyed by player within the host plugin's MagmaCore copy, not by script. An expiry timer removes only the bar or repeating action bar that started it, so an older expiry does not clear a newer one. Their Bukkit timers are not owned by context.scheduler.
player:show_boss_bar(text, [color], progress, [ticks])
Replaces the player's shared solid boss bar. Progress is clamped to 0..1; an invalid color becomes white. Pass nil for an omitted color so progress remains the third argument.
| Parameter | Type | Default | Notes |
|---|---|---|---|
text | string | required | The text to display. Supports & color codes. |
color | string | "WHITE" | Bar color. One of: "RED", "BLUE", "GREEN", "YELLOW", "PURPLE", "PINK", "WHITE" |
progress | number | required | Fill amount from 0.0 (empty) to 1.0 (full) |
ticks | int | -1 | Positive values schedule removal; otherwise the bar stays until hidden or replaced |
player:hide_boss_bar()
Removes the boss bar from the player's screen. Takes no parameters.
player:show_action_bar(text, [ticks])
Shows text in the action bar area (above the hotbar).
| Parameter | Type | Default | Notes |
|---|---|---|---|
text | string | required | The text to display. Supports & color codes. |
ticks | int | -1 | Positive values repeat every 40 ticks until expiry; otherwise sends once. A new call cancels the previous repeating task. |
player:show_title(title, [subtitle], fade_in, stay, fade_out)
Shows a title screen to the player.
| Parameter | Type | Default | Notes |
|---|---|---|---|
title | string | required | Main title text. Supports & color codes. |
subtitle | string | "" | Subtitle text below the main title. Optional. |
fade_in | int | required | Fade-in duration in ticks |
stay | int | required | How long the title stays on screen in ticks |
fade_out | int | required | Fade-out duration in ticks |
Next Steps
For plugin-specific hooks, APIs, and workflows:
- EliteMobs: Getting Started | NPC Scripts | Hooks & Lifecycle | Boss & Entities | World & Environment | Zones & Targeting
- FreeMinecraftModels: Getting Started | Prop API | Examples