2. Events and storage
In part 1 you built commands. Now let’s make your script react to the world and remember things between reloads.
In this guide you will learn how to:
- Listen to server events with
mc.on. - Cancel events by returning
false. - Fire your own events with
mc.emit. - Save data globally with
mc.dataand per-player withplayer.data.
All examples use Nova lambda syntax, so the first line of each file is:
--# nova syntax1. Reacting to events with mc.on
Section titled “1. Reacting to events with mc.on”Minecraft fires events constantly — when a player joins, breaks a block, chats, dies, and so on. mc.on lets you run code whenever one of those events happens.
Syntax:
mc.on(eventName, handler)eventNameis a string like"player_join".handleris a function that receives event-specific arguments.mc.onreturns a numeric ID. Save it if you want to stop listening later withmc.off(id).
Example:
--# nova syntaxmc.on("player_join") \{ player -> mc.broadcast("Welcome, " .. player.name .. "!")}The handler receives player, the player who joined. Each event sends its own arguments, so check the Events reference for details.
To unsubscribe later:
--# nova syntaxlocal joinId = mc.on("player_join") \{ player -> mc.broadcast("Welcome!")}
-- later...mc.off(joinId)2. Cancelling events
Section titled “2. Cancelling events”Some events are cancellable. If your handler returns false, Minecraft stops the action.
--# nova syntaxmc.on("player_block_break") \{ player, pos, blockId -> if not player:hasPermission("build") then player:sendMessage("You cannot break blocks here.") return false end}In this example, player:hasPermission("build") checks whether the player has the build permission. You can use any permission node you have configured on your server.
Cancellable events include player_block_break, player_block_place, player_chat, player_hurt, and several others. See the Events reference for the full list.
If the event is cancelable and you return nothing (or true) the event will proceed normally.
3. Custom events with mc.emit
Section titled “3. Custom events with mc.emit”You can fire your own events from Lua. This is useful when you want separate scripts to react to the same thing.
--# nova syntax-- rewards.luamc.on("my_mod:boss_killed") \{ player, bossName -> player:sendMessage("You defeated " .. bossName .. "!") player.data.bossKills = (player.data.bossKills or 0) + 1}Then elsewhere:
--# nova syntax-- bossfight.luamc.emit("my_mod:boss_killed", somePlayer, "Ender Dragon")4. Global persistent data with mc.data
Section titled “4. Global persistent data with mc.data”mc.data is a special table that is saved to config/ignis/storage/global.json. Anything you put in it survives /ignis reload and server restarts.
--# nova syntaxmc.data.joins = (mc.data.joins or 0) + 1
mc.on("player_join") \{ player -> mc.broadcast("Visitor #" .. mc.data.joins .. "!")}Allowed values: numbers, strings, booleans, tables, and nil (to delete a key).
Not allowed: functions, userdata, threads, and tables that reference themselves (cyclic tables). If you try to save those, the server logs an error.
5. Per-player data with player.data
Section titled “5. Per-player data with player.data”Every player wrapper has its own persistent table at player.data. It works exactly like mc.data, but each player gets a separate file on disk.
--# nova syntaxmc.on("player_join") \{ player -> local visits = (player.data.visits or 0) + 1 player.data.visits = visits player:sendMessage("This is visit #" .. visits)}Per-player data is saved when the player disconnects and when /ignis reload runs. It is removed from memory on disconnect, but stays on disk so it is available next time the player joins.
6. Putting it together: a tiny coin system
Section titled “6. Putting it together: a tiny coin system”This example combines commands, events, and per-player storage.
--# nova syntaxlocal function giveCoins(player, amount) player.data.coins = (player.data.coins or 0) + amount player:sendMessage("You have " .. player.data.coins .. " coins")end
-- /coins gives 10 coinsregister("coins") \{ ctx -> giveCoins(ctx.player, 10)}
-- Killing another player gives 50 coinsmc.on("player_kill") \{ player, target, damageSource -> giveCoins(player, 50)}
-- Dying makes you lose 10% of your coinsmc.on("player_death") \{ player, damageType -> local coins = player.data.coins or 0 local lost = math.floor(coins * 0.1) player.data.coins = coins - lost player:sendMessage("You lost " .. lost .. " coins")}How it works:
giveCoinsreads the player’s saved coin count, adds the amount, and saves it back.- The
coinscommand calls it for the player who ran the command. - The
player_killevent gives a reward. - The
player_deathevent takes a penalty.
Because player.data persists, your coin total survives disconnects and reloads.
7. Notes and limitations
Section titled “7. Notes and limitations”- Storage saves automatically. You do not need to call a save method.
- Deep nested tables work directly:
mc.data.guilds.mine.members.leader = player.name. - Some APIs, such as
mc.sleepandmc.fetch, are async and cannot be used directly inside event handlers. If you need them, defer the work to the scheduler:
--# nova syntaxmc.on("player_join") \{ player -> mc.schedule(0) \{ -- async work goes here mc.sleep(1000) player:sendMessage("Delayed hello!") }}Next steps
Section titled “Next steps”Keep going with the topics that match what you want to build:
More about events and data
- Events reference — complete event list and handler signatures
- Storage reference — allowed types and save behavior
- Examples: persistence — advanced data patterns
- Examples: events — larger event-driven scripts
Built-in helpers
Game systems
- Region API — protect areas and react to movement
- Sidebar API — show persistent on-screen info
- Hologram API — floating text displays
- Inventory API — custom menus and storage UIs
- Mob AI — script custom mob behavior