Lua Scripting: Prop API
This page covers every API available to FreeMinecraftModels prop scripts: context.prop, context.event, context.world, context.zones, context.scheduler, context.state, and context.log. If you are new to prop scripting, start with Getting Started first.
context.prop
The prop table provides information about the prop entity and methods to control its animations. This is rebuilt fresh for each hook call.
Fields
| Field | Type | Notes |
|---|---|---|
prop.model_id | string | The blueprint model name (e.g. "torch_01") |
prop.current_location | location table | The prop's position at the time the context was built |
The location table has the standard fields: x, y, z, world, yaw, pitch.
Example: reading prop info
return {
api_version = 1,
on_spawn = function(context)
context.log:info("Prop spawned: " .. (context.prop.model_id or "unknown"))
local loc = context.prop.current_location
if loc then
context.log:info("Location: " .. loc.x .. ", " .. loc.y .. ", " .. loc.z)
end
end
}
prop:play_animation(name, blend, loop)
Plays a named animation on the prop model.
| Parameter | Type | Default | Notes |
|---|---|---|---|
name | string | required | The animation name as defined in the model file |
blend | boolean | true | Whether to blend with the current animation |
loop | boolean | true | Whether the animation loops |
Returns true if the animation was found and started, false otherwise.
Example
return {
api_version = 1,
on_right_click = function(context)
local success = context.prop:play_animation("open", true, false)
if not success then
context.log:warn("Animation 'open' not found on this model!")
end
end
}
prop:stop_animation()
Stops all currently playing animations on the prop.
Takes no parameters.
Example
return {
api_version = 1,
on_right_click = function(context)
context.prop:stop_animation()
end
}
context.event
Event data for the current hook. Available in on_left_click, on_right_click, and on_projectile_hit. Returns nil in hooks that have no associated event (on_spawn, on_game_tick, on_destroy, on_zone_enter, on_zone_leave).
Fields and Methods
| Field or Method | Type | Notes |
|---|---|---|
event.is_cancelled | boolean | Whether the event is currently cancelled |
event.cancel() | function | Cancels the event (e.g. prevents damage or interaction) |
event.uncancel() | function | Un-cancels a previously cancelled event |
Not all events are cancellable. If the underlying Bukkit event does not implement Cancellable, event.cancel() and event.uncancel() will not be present and event.is_cancelled will always be false.
Example: Making a prop invulnerable
Example
return {
api_version = 1,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}
Example: Checking cancellation state
Example
return {
api_version = 1,
on_left_click = function(context)
if context.event and not context.event.is_cancelled then
context.event.cancel()
context.log:info("Damage cancelled!")
end
end
}
Inside scheduled callbacks (scheduler:run_later, scheduler:run_repeating), context.event is always nil. Event modification can only happen during the event hook itself.
context.world
The world table provides methods for querying and interacting with the Minecraft world. It is built from the prop's current world.
The context.world API is shared with EliteMobs. FMM props use the same Magmacore world table as EliteMobs bosses. The methods documented here are those available in the base Magmacore implementation. For the full reference, see the EliteMobs World & Environment page -- the methods listed there as part of the core context.world API are available in FMM as well.
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").
| Parameter | Type | Notes |
|---|---|---|
x | int | Block X coordinate |
y | int | Block Y coordinate |
z | int | Block Z coordinate |
Example
return {
api_version = 1,
on_spawn = function(context)
local loc = context.prop.current_location
if loc then
local block = context.world:get_block_at(
math.floor(loc.x),
math.floor(loc.y) - 1,
math.floor(loc.z)
)
context.log:info("Prop is standing on: " .. block)
end
end
}
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, UPPER_CASE (e.g. "FLAME", "DUST") |
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 |
Example
return {
api_version = 1,
on_right_click = function(context)
local loc = context.prop.current_location
if loc then
context.world:spawn_particle("HEART", loc.x, loc.y + 1, loc.z, 5, 0.3, 0.3, 0.3, 0)
end
end
}
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, UPPER_CASE (e.g. "ENTITY_EXPERIENCE_ORB_PICKUP") |
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 |
Example
return {
api_version = 1,
on_right_click = function(context)
local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_NOTE_BLOCK_PLING", loc.x, loc.y, loc.z, 1.0, 1.2)
end
end
}
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 |
Example
return {
api_version = 1,
on_projectile_hit = function(context)
local loc = context.prop.current_location
if loc then
context.world:strike_lightning(loc.x, loc.y, loc.z)
end
end
}
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 an array of entity wrapper tables for all living entities within a bounding box centered at the given coordinates.
| 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) |
Example
return {
api_version = 1,
on_right_click = function(context)
local loc = context.prop.current_location
if loc then
local entities = context.world:get_nearby_entities(loc.x, loc.y, loc.z, 10)
context.log:info("Found " .. #entities .. " entities nearby")
end
end
}
world:get_nearby_players(x, y, z, radius)
Returns an array of player wrapper tables for all players within a bounding box centered at the given coordinates.
| Parameter | Type | Notes |
|---|---|---|
x | number | Center X |
y | number | Center Y |
z | number | Center Z |
radius | number | Search radius |
Example
return {
api_version = 1,
on_right_click = function(context)
local loc = context.prop.current_location
if loc then
local players = context.world:get_nearby_players(loc.x, loc.y, loc.z, 20)
for i = 1, #players do
context.log:info("Nearby player: " .. (players[i].name or "unknown"))
end
end
end
}
context.zones
The zones table lets you create spatial zones and watch them for player enter/leave events. Zones are tied to the script instance and cleaned up automatically when the prop is removed.
zones:create_sphere(x, y, z, radius)
Creates a sphere zone centered at the given coordinates. Returns a numeric zone handle.
| 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 zone handle.
| 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 zone handle.
| 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)
Starts watching a zone for player enter/leave events. When a player enters or leaves the zone, the corresponding hook fires (on_zone_enter or on_zone_leave).
| Parameter | Type | Notes |
|---|---|---|
handle | int | Zone handle from a create_* call |
onEnterCallback | function or nil | Called when a player enters the zone |
onLeaveCallback | function or nil | Called when a player leaves the zone |
Returns true if the watch was set up successfully, nil if the zone handle was invalid.
Zone watches are checked every server tick against all players in the same world. Keep zone counts reasonable to avoid performance overhead.
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
Example
return {
api_version = 1,
on_spawn = function(context)
local loc = context.prop.current_location
if loc == nil then return end
-- Create a sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, 5)
-- Watch for player enter/leave
context.zones:watch(
handle,
function(player)
context.log:info("Player entered zone!")
end,
function(player)
context.log:info("Player left zone!")
end
)
-- Store handle so we can clean up later if needed
context.state.zone_handle = handle
end,
on_destroy = function(context)
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
end
end
}
context.scheduler
The scheduler lets you run delayed and repeating tasks. All tasks are owned by the script instance and cancelled automatically when the prop is removed.
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 |
Example
return {
api_version = 1,
on_right_click = function(context)
context.log:info("Something will happen in 2 seconds...")
context.scheduler:run_later(40, function(delayed_context)
local loc = delayed_context.prop.current_location
if loc then
delayed_context.world:spawn_particle("EXPLOSION_EMITTER", loc.x, loc.y, loc.z, 1)
end
end)
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 |
Example
return {
api_version = 1,
on_spawn = function(context)
-- Emit particles every second
context.state.particle_task = context.scheduler:run_repeating(0, 20, function(tick_context)
local loc = tick_context.prop.current_location
if loc then
tick_context.world:spawn_particle("FLAME", loc.x, loc.y + 1, loc.z, 3, 0.1, 0.1, 0.1, 0.02)
end
end)
end,
on_destroy = function(context)
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
end
end
}
scheduler:cancel(taskId)
Cancels a scheduled task by its ID.
| Parameter | Type | Notes |
|---|---|---|
taskId | int | The task ID returned by run_later or run_repeating |
If you start a repeating task, always cancel it in on_destroy (or when it is no longer needed). Forgetting to cancel leaves a background task running until the prop is removed from the server, which wastes performance and can cause errors.
context.state
A plain Lua table that persists for the prop's entire lifetime. Use it to store flags, counters, task IDs, toggle states, and any data you need to share between hooks.
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
Only context.state persists between hook calls. All other context tables (context.prop, context.world, context.event, etc.) are rebuilt fresh each time.
context.log
Console logging methods. Messages are prefixed with the script filename in the server console.
| Method | Notes |
|---|---|
log:info(message) | Informational message |
log:warn(message) | Warning message |
log:error(message) | Error-level message (functionally same as warn) |
Example
return {
api_version = 1,
on_spawn = function(context)
context.log:info("Script loaded!")
end,
on_right_click = function(context)
context.log:warn("Prop was clicked -- this is a debug warning")
end
}
Runtime Model
One Runtime Per Prop
Every prop entity that has scripts attached gets its own independent Lua runtime instance. When the prop spawns, FMM loads the Lua source, evaluates it in a fresh sandboxed environment, and stores the returned table. When the prop is removed, the runtime is shut down.
This means:
- Local variables declared at file scope are private to that prop instance.
context.stateis completely isolated between prop instances, even if they share the same script file.
Scheduled Task Ownership
All tasks created through context.scheduler are owned by the runtime that created them. When a prop is removed:
- The runtime shuts down.
- Every owned task -- both one-shot and repeating -- is automatically cancelled.
- All zone watches are cleared.
Execution Budget
Every hook invocation and every callback invocation is timed. If a single call takes longer than 50 milliseconds, the script is disabled with a console warning:
[Lua] my_script.lua took 73ms in 'on_game_tick' (limit: 50ms) -- script disabled to prevent lag.
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.
Next Steps
- Examples & Patterns -- complete working prop scripts with walkthroughs
- Troubleshooting -- common issues, debugging tips, and a QC checklist
- Getting Started -- file structure, hooks, first script walkthrough