Skip to main content

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.

MagmaCore Lua Runtime

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 .yml config 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 .yml file next to the model file and set isEnabled: true.

  • The scripts: list is empty. Add your script filenames:

    isEnabled: true
    scripts:
    - my_script.lua
  • The .yml filename does not match the model filename. The config must have the same base name as the model file. For example, torch_01.fmmodel needs torch_01.yml in 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 .yml config must match the filename in the scripts/ folder, including case. The .lua extension is recommended in the config but optional; FMM appends .lua internally if you omit it.

  • File not present. Double-check that the .lua file actually exists in the scripts/ 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 end to close a function or if block
  • 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 nameCorrect name
on_clickon_right_click or on_left_click
on_interacton_right_click
on_hiton_left_click
on_tickon_game_tick
on_removeon_destroy (runs only when the prop is removed for good, not on chunk unload or reload)
on_arrow_hiton_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 .bbmodel or .fmmodel for 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 true for blend uses a single queue slot, not a cross-fade. A later request can replace it, and an attack request during a custom animation can return true without 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 FLAME or flame; 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 count is at least 1.
  • Some particles (like DUST) need specific extra data that the base spawn_particle() may not support. Use standard particles like FLAME, 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.harp and 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:

  1. First spawn: When a prop spawns and no sibling .yml file exists, FMM creates the config asynchronously. This means the file is written in a background thread and is not immediately available.

  2. Default values: The generated config has isEnabled: true and an empty scripts: [] list.

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

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

  5. Config location: The .yml file 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
Quick setup workflow
  1. Place model in models/
  2. Place script in scripts/
  3. Spawn the prop once (generates the .yml)
  4. Edit the .yml to add scripts: [my_script.lua]
  5. 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 errorWhat the console tells you
attempt to call nilYou tried to call a method or function that doesn't exist. Check for typos and : vs . usage.
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 argumentA function received the wrong type of argument. The message shows the expected vs. actual type.
Budget exceededLua 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.
tip

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. Use context.prop.current_location (a field, not a method).
  • context.prop:set_animation("open") -- does not exist. Use context.prop:play_animation("open", true, true).
  • context.event:setCancelled(true) -- does not exist. Use context.event.cancel().
  • context.cooldowns:check_global(...) -- does not exist. Use context.cooldowns:global_ready() and context.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, and on_projectile_hit when a player shot the projectile. It is not a general prop-owner field and will be nil in prop lifecycle hooks and scheduler callbacks.
  • context.boss -- does not exist. That is EliteMobs. Use context.prop in 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:

  1. Write a file with only api_version = 1 and on_spawn that logs a message.
  2. Add the script to a prop config and verify the log message appears.
  3. Add on_right_click and log when the prop is clicked.
  4. Add on_left_click with context.event.cancel() to suppress default click damage.
  5. Play an animation on right-click.
  6. Add a sound on right-click.
  7. Add state and toggle behavior.
  8. Add a zone watch for proximity detection.
  9. 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​