Lua Scripting: Troubleshooting
This page covers common problems you may encounter when writing or debugging FreeMinecraftModels prop scripts, plus tips for working with the lazy config generation system. If you are looking for working examples, see Examples & Patterns. If you are just getting started, see Getting Started.
FMM uses the MagmaCore ScriptInstance runtime. For documentation on the FMM/MagmaCore sandbox, scheduler, zones, world API, entity tables, and player UI methods, see the MagmaCore Lua Scripting Engine page. EliteMobs boss powers and NPC scripts reuse the Lua sandbox but expose their own context tables.
Common Issues
1. Config file not loading / Script not attached
Symptom: The prop spawns but does not respond to clicks or any hooks.
Causes and fixes:
-
No
.ymlconfig file exists yet. FMM attempts to create it asynchronously on first prop spawn, with defaults (enabled, no scripts), without overwriting an existing file. Add your script filenames, complete a successful/fmm reload, then place a fresh prop. Both YAML and Lua edits require a reload. -
The config has
isEnabled: false. Open the.ymlfile next to the model file and setisEnabled: true. -
The
scripts:list is empty. Add your script filenames:isEnabled: true
scripts:
- my_script.lua -
The
.ymlfilename does not match the model filename. The config must have the same base name as the model file. For example,torch_01.fmmodelneedstorch_01.ymlin the same directory.
2. Script file not found
Symptom: Console shows: [FMM Scripts] Script 'my_script.lua' not found in scripts/ folder
Causes and fixes:
-
Wrong directory. Script files must be in
plugins/FreeMinecraftModels/scripts/, not next to the model file. -
Filename mismatch. The name in the
.ymlconfig must match the filename in thescripts/folder, including case. The.luaextension is recommended in the config but optional; FMM appends.luainternally if you omit it. -
File not present. Double-check that the
.luafile actually exists in thescripts/folder. -
Cached definition or filename collision. FMM discovers scripts recursively at provider initialization, but binds them by filename, not relative path. Keep names unique across subfolders. After adding or editing a Lua file, run a successful
/fmm reload; simply respawning a prop does not reread cached Lua source.
3. Script does not load at all
Symptom: No console error, but no hooks fire.
Check the server console for Lua syntax errors during startup. The most common causes are:
- Missing
endto close a function orifblock - Unmatched parentheses
- Missing comma between hook definitions in the returned table
- File does not end with
.lua
Discovery failures appear as invalid/retired-script warnings or load errors. Hook and callback failures use a [Lua] error block with the filename and execution context; a line number appears only when it can be extracted from the exception.
4. Hook never fires
Symptom: Your own startup log appears, but a specific hook never triggers.
Verify the hook name is spelled exactly as listed in the hook reference. Common mistakes:
| Wrong name | Correct name |
|---|---|
on_click | on_right_click or on_left_click |
on_interact | on_right_click |
on_hit | on_left_click |
on_tick | on_game_tick |
on_remove | on_destroy (runs only when the prop is removed for good, not on chunk unload or reload) |
on_arrow_hit | on_projectile_hit |
Check that the model loaded, has a usable hitbox, and that no protection listener cancelled the interaction before the prop callback. A spawned prop uses a backing armor stand; changing a config flag is not a way to make an invalid or unloaded model clickable.
5. Timeout / execution budget exceeded
Symptom: Console shows a Lua error raised from inside the VM. A call that ran past the budget is aborted mid-execution with one of:
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)
Each hook, callback and file evaluation may execute at most 250,000 Lua instructions or burn 50 ms of current-thread CPU time, whichever comes first, and nested calls share a single allowance. (The third message only appears on a JVM where per-thread CPU timing is unavailable; MagmaCore then uses a more generous 250 ms elapsed-time limit instead.) The script was doing too much work in a single hook call. Common causes:
- Iterating over too many entities in
on_game_tick - Creating too many particles per tick
- Running expensive loops without spreading work across ticks
Fix: Move heavy work behind context.cooldowns or use scheduler:run_repeating() with a reasonable interval instead of on_game_tick.
The instruction limit bounds Lua loops. Time checks occur every 1,024 Lua instructions and on return; they do not interrupt a blocking Java/Bukkit method mid-call. Keep the work inside each callback bounded too.
6. context.event is nil in a click hook
This should not normally happen for on_left_click and on_right_click, but always guard with:
if context.event then
context.event.cancel()
end
Inside scheduler callbacks, context.event is always nil -- this is expected behavior. Event modification can only happen inside the event hook itself.
7. Animation does not play
Symptom: play_animation() returns false, or nothing visible happens.
Causes and fixes:
-
Wrong animation name. The name must exactly match what is defined in the model file. Check your
.bbmodelor.fmmodelfor the correct animation name. -
Model has no animations. Not all models have animations. Verify the model file actually contains animation data.
-
The viewer lacks the pack or is outside model view range. Players need both the current resource pack and a visible model.
-
The request is queued or suppressed. Passing
trueforblenduses a single queue slot, not a cross-fade. A later request can replace it, and an attack request during a custom animation can returntruewithout interrupting that animation. Use the actual positional arguments shown in Animations, and distinguish an accepted request from a rendered frame.
8. Particles do not appear
- Particle names accept either
FLAMEorflame; the API normalizes case before looking up the Bukkit particle. - Verify the location is in a loaded chunk. If no players are nearby, the chunk may be unloaded.
- Verify
countis at least 1. - Some particles (like
DUST) need specific extra data that the basespawn_particle()may not support. Use standard particles likeFLAME,HEART,HAPPY_VILLAGER,NOTE,ENCHANT, etc.
9. Sound does not play
- Sounds accept Bukkit enum names, regardless of case, or valid sound keys such as
minecraft:block.note_block.harpand custom resource-pack keys. The client must have the sound in its resource pack. - Verify the location coordinates are correct (not all zeros or NaN).
- Verify volume is greater than 0.
10. State resets unexpectedly
context.state belongs to one script instance. Separate entries in the same prop's scripts: list do not share it. It lasts while that instance is active. If state seems to reset:
- The prop may have been removed and respawned, or its chunk unloaded and loaded again. Each spawn or chunk load creates a new instance. Store values that must survive this with
prop:set_persistent_data. - A successful reload or a script error may have shut down the old instance. Inspect the console; fixing a file does not revive a closed instance without reloading and rebinding it.
- You may be reading state in a scheduler callback using the wrong context variable. Always use the callback's own context parameter.
11. os library is not available
Symptom: Script crashes with an error like attempt to index nil (os).
The os library is completely removed from the Lua sandbox. You cannot use os.time(), os.clock(), or any other os function.
Fix: Use context.world:get_time() for world time, or track elapsed time manually using state flags and on_game_tick tick counters. For cooldowns, prefer context.cooldowns:check_local(key, ticks).
12. Item script not activating
The equipped-item scripting format has been retired. A custom item with a nonempty scripts: list or old unnamespaced enchantments is skipped and logged. Replace it with a current custom item definition and namespaced enchantments. Prop scripts still use scripts:.
13. Item definition unavailable
Check the startup or reload warning for the exact filename. An unavailable material, such as a newer Minecraft item type on an older server, also causes that item to be skipped. Correct or replace the definition for your server version; FMM does not rewrite it or invent a replacement material.
14. Hook fires but script crashes on entity methods
Symptom: A hook like on_right_click works, but the script crashes inside a scheduled callback with errors like attempt to call nil when calling entity:damage() or entity:push().
Cause: context.world:get_nearby_entities() returns ALL entities in range, including non-living entities (dropped items, experience orbs, area effect clouds). These entities do not have living-entity methods like damage(), push(), or add_potion_effect().
Fix: Always guard with if entity.damage then before calling living-entity methods:
local entities = context.world:get_nearby_entities(x, y, z, radius)
for _, entity in ipairs(entities) do
if entity.damage then
-- Safe to call living entity methods
entity:damage(5.0)
entity:push(0, 0.5, 0)
end
end
15. Event-only data is missing in scheduled callbacks
Symptom: Data from the click/combat event is missing inside a scheduler:run_later() or scheduler:run_repeating() callback.
Cause: Scheduled callbacks receive a fresh context and do not keep the original Bukkit event or generic zone actor. context.event is always nil in callbacks. Prop scripts only receive top-level context.player during player-driven hooks such as clicks and zone enter/leave. Prop scripts should capture the player from context.player or context.event.player during the original hook when they need it later.
Fix: Use the callback's fresh context for normal world and state access. If you need the exact player or entity from the original event, capture it before scheduling and guard that it is still valid.
on_right_click = function(context)
local clicked_player = context.player or (context.event and context.event.player)
context.scheduler:run_later(20, function(later_context)
-- Prop scripts should use a captured player when they need the clicker.
local player = later_context.player or clicked_player
if player and player.is_valid then
player:send_message("&aDelayed message!")
end
end)
end
How Lazy Config Generation Works
Understanding the lazy config system helps avoid confusion when setting up new props:
-
First spawn: When a prop spawns and no sibling
.ymlfile exists, FMM creates the config asynchronously. This means the file is written in a background thread and is not immediately available. -
Default values: The generated config has
isEnabled: trueand an emptyscripts: []list. -
No scripts on first spawn: Because the config is created after the prop has already spawned (and has no scripts listed), the prop will have no scripts attached on its first spawn.
-
Edit, reload and respawn: Add your script filenames, then complete a successful
/fmm reload. Prop settings, placement checks and menu script lists use the same prepared catalog. A new prop uses that catalog; it does not reread edited YAML. Lua edits also require a reload. -
Config location: The
.ymlfile is created in the same directory as the model file, with the same base name. For example:- Model:
plugins/FreeMinecraftModels/models/fountain.fmmodel - Config:
plugins/FreeMinecraftModels/models/fountain.yml
- Model:
- Place model in
models/ - Place script in
scripts/ - Spawn the prop once (generates the
.yml) - Edit the
.ymlto addscripts: [my_script.lua] - Reload to discover the Lua file, then place a fresh prop and confirm its own startup log
Reading Error Messages
Hook and callback errors produce a [Lua] error block naming the file and execution context. The line number is included when the exception contains a recognizable source location. Discovery and configuration errors can use different warnings.
A typical error looks like this:
[Lua] Error in 'my_door.lua' at line 12 during 'on_right_click':
[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:
| Raw Lua error | What the console tells you |
|---|---|
attempt to call nil | You tried to call a method or function that doesn't exist. Check for typos and : vs . usage. |
index expected, got nil | You tried to access a field on something that is nil. Check that earlier code initialized it. |
attempt to index | You tried to access a property on a nil or invalid value. |
bad argument | A function received the wrong type of argument. The message shows the expected vs. actual type. |
| Budget exceeded | Lua instruction budget exceeded (250000 instruction limit) or Lua CPU-time budget exceeded (50ms current-thread CPU limit) -- the call was aborted mid-execution and the script instance disabled. |
Always read the full [Lua] error message before diving into the code. It usually points you straight to the fix.
Do Not Assume Undocumented Methods Exist
The FMM Lua API exposes a specific set of methods. Do not assume shorthand or alternative names exist. Common mistakes:
context.prop:get_location()-- does not exist. Usecontext.prop.current_location(a field, not a method).context.prop:set_animation("open")-- does not exist. Usecontext.prop:play_animation("open", true, true).context.event:setCancelled(true)-- does not exist. Usecontext.event.cancel().context.cooldowns:check_global(...)-- does not exist. Usecontext.cooldowns:global_ready()andcontext.cooldowns:set_global(ticks)for global cooldowns.context.player-- is only set for FMM prop hooks that involve a player, such as click hooks, watched-zone enter/leave hooks, andon_projectile_hitwhen a player shot the projectile. It is not a general prop-owner field and will benilin prop lifecycle hooks and scheduler callbacks.context.boss-- does not exist. That is EliteMobs. Usecontext.propin FMM.
Check the Prop API and its linked shared references for supported methods. A plausible method name is not evidence that the method exists.
Debug Tips
1. Use context.log:info() liberally
Add log messages at every step when debugging:
on_right_click = function(context)
context.log:info("Right click received!")
context.log:info("Event present: " .. tostring(context.event ~= nil))
context.log:info("State is_open: " .. tostring(context.state.is_open))
end
2. Check the console on startup
Add context.log:info("Prop script started") to on_spawn, then place a fresh prop after reloading. Check the console for your log and for script/configuration errors. FMM does not emit a success line for every binding.
3. Verify the model file path
The .yml config must be a sibling of the model file (same directory, same base name). Verify this by checking:
- Model file path:
plugins/FreeMinecraftModels/models/my_model.fmmodel - Config file path:
plugins/FreeMinecraftModels/models/my_model.yml
4. Test hooks individually
When building a complex script, start by testing each hook in isolation. Add one hook at a time and verify it fires before moving to the next.
5. Check for typos in hook names
The most common reason a hook does not fire is a misspelled hook name. An unknown hook rejects the script during validation; check the console error rather than expecting the other hooks in that file to run.
Beginner Progression Path
If you want to learn this system from scratch, this progression works well:
- Write a file with only
api_version = 1andon_spawnthat logs a message. - Add the script to a prop config and verify the log message appears.
- Add
on_right_clickand log when the prop is clicked. - Add
on_left_clickwithcontext.event.cancel()to suppress default click damage. - Play an animation on right-click.
- Add a sound on right-click.
- Add state and toggle behavior.
- Add a zone watch for proximity detection.
- Only then move into complex multi-hook scripts with schedulers.
Each step builds on the previous one, and you can test at every stage.
Next Steps
- Getting Started -- file structure, hooks, first script walkthrough, copy-paste templates
- Prop API -- full API reference
- Examples & Patterns -- complete working scripts you can study and adapt