PxIgnis: Lua Scripting Engine
for Minecraft Servers
Server-side framework (Fabric) for writing custom game logic, events,
interfaces, and mob AI in Lua with async support and hot
reloading (/pxignis reload) — no server restarts.
Built on PxLuaNova, a custom Lua 5.2 fork.
What You Get
Everything you need to script a server, built in.
Hot Reload
Edit scripts and run /pxignis reload — no server restarts, no lost connections. Persistent data (mc.data, player.data) is preserved across reloads.
Async Runtime
Coroutine-powered mc.sleep(ticks) and mc.fetch(url) with automatic JSON parsing. Yields the Lua coroutine, resumes on the server thread.
Mob AI Framework
MobEntity behaviour scripting. Register custom goals with native pathfinding, line-of-sight (canSee), and vanilla AI override. Built-in behaviours: guard, pet, orbiter, wander.
Dynamic UI
Per-player scoreboard sidebars, chest GUI menus with click interception (container:onClick), and virtual inventories. Packet-based — never touches the global scoreboard, other players never see it.
Real Examples
Real scripts you can try. Click tabs to switch.
-- Right-click blaze rod: raycast → knockback → fire → damage
mc.on("player_use_item", function(player, hand, _, id)
-- Early return if not the item we care about
if id ~= "minecraft:blaze_rod" then return end
-- Raycast from the player's eye position
local hit = player:raycast(15)
if not hit then return end
-- hit.type is "entity" (hit a mob) or "block" (hit terrain)
if hit.type == "entity" then
local target = hit.entity
-- Vector subtraction + normalisation for directional knockback
local d = target.pos - player.pos
local dist = math.sqrt(d.x^2 + d.y^2 + d.z^2)
if dist > 0 then
target.pos = target.pos + d / dist * 3
end
-- Entity methods: fire ticks, damage with source, status effect
target:setOnFireFor(80)
target:damage(6, player)
target:addEffect("minecraft:slowness", 60, 1)
end
end)-- Chest GUI shop: require library, build once, open for any player
local chestgui = require "chestgui"
-- Create a 3-row (27-slot) GUI
local gui = chestgui.create(3, "Shop")
local glass = mc.createItem("black_stained_glass_pane", { name = " " })
-- Decorate with glass border — no callback = stuck in place
for col = 1, 9 do
gui:decorate(1, col, glass)
gui:decorate(3, col, glass)
end
-- gui:set(row, col, item, callback) — clickable item
-- Returning false cancels the click (prevents item theft)
gui:set(2, 3, mc.createItem("diamond", { name = "Diamond", lore = { "10 coins" } }),
function(player)
-- player.data is per-player persistent storage (JSON, survives reloads)
local coins = player.data.coins or 0
if coins < 10 then
player:sendMessage("Not enough coins!")
return false
end
player.data.coins = coins - 10
player:give(mc.createItem("diamond"))
player:sendMessage("Bought a diamond! Coins: " .. (coins - 10))
return false
end)
register("shop", function(ctx)
gui:open(ctx.player)
end)-- Live per-player sidebar refreshing every 2s, self-cleaning on disconnect
mc.on("player_join", function(player)
-- Per-player data persists as JSON across reloads
local visits = player.data.visits or 0
visits = visits + 1
player.data.visits = visits
if visits == 1 then
player:give(mc.createItem("compass", 1))
end
-- Sidebar is packet-based — never touches the global scoreboard
player.sidebar = {
title = "Stats",
lines = {
"HP " .. math.floor(player.health),
"Visits " .. visits,
"Online " .. mc.onlineCount,
}
}
-- mc.scheduleRepeating(delay, interval, callback) — 20 ticks = 1 second
local id = mc.scheduleRepeating(40, 40, function()
local sb = player.sidebar
if sb then
sb.lines = {
"HP " .. math.floor(player.health),
"Visits " .. visits,
"Online " .. mc.onlineCount,
}
else
mc.cancelTask(id) -- sidebar destroyed, stop updating
end
end)
-- Clean up the repeating task when the player disconnects
mc.on("player_leave", function(p)
if p.uuid == player.uuid then
mc.cancelTask(id)
end
end)
end)-- Custom guard behaviour: patrol home, hunt players, return
mc.registerBehaviour("guard_beast", function(self, state)
-- self: the mob wrapper; state: persists across server ticks per mob
if not state.home then
state.home = self.pos -- remember spawn point on first tick
end
-- Throttle: only run every 10 ticks (0.5s) to avoid spam
state.timer = (state.timer or 0) + 1
if state.timer % 10 ~= 0 then return end
-- Search for players within 10 blocks
local players = self.world:getEntities(self.pos, 10, "minecraft:player")
if #players > 0 then
-- Navigate to nearest player, attack when in melee range
self:navigateTo(players[1])
if self:distanceTo(players[1]) < 2.5 then
self:tryAttack(players[1])
end
else
-- No threat — return to home position if drifted away
local d = self:distanceTo(state.home)
if d > 2 then
self:navigateTo(state.home.x, state.home.y, state.home.z)
end
end
end)
-- Spawn a zombie and assign the behaviour
local guard = world:spawn("minecraft:zombie", player.pos)
guard.customName = "Guard"
guard.health = 40
guard:setAI("guard_beast")-- Simple: no arguments, no permission — anyone can use it
register("fly", function(ctx)
ctx.player.isFlying = not ctx.player.isFlying
ctx.player:sendMessage("Flight: " .. tostring(ctx.player.isFlying))
end)
-- Complex: choice type (tab-completion), optional target, permission
register("kit <name:choice=warrior,archer,miner> [target:player]", function(ctx, name, target)
-- target is nil when omitted; fall back to the command sender
local player = target or ctx.player
player:clear()
if name == "warrior" then
-- mc.createItem(id, {components}) — name, unbreakable, lore, count...
player:give(mc.createItem("iron_sword", { name = "Warrior Blade", unbreakable = true }))
player:give(mc.createItem("iron_chestplate"))
elseif name == "archer" then
player:give(mc.createItem("bow", { name = "Longbow", unbreakable = true }))
player:give(mc.createItem("arrow", 64))
end
end, "px.ignis.admin") -- only operators or those with the permission nodeQuick Start
Place PxIgnis.jar in your Fabric server's mods/ folder.
Write scripts in config/pxignis/scripts/. The runtime loads all *.lua files alphabetically.
Run /pxignis reload in-game — no restart. Global data (mc.data) and per-player data auto-persist to JSON.