Skip to content

Changelog

  • New event model. Built-in events have new names (player.join, block.break, entity.hurt, …) and pass one event table e instead of positional arguments; cancel with e:cancel(reason?). The old names still work with positional arguments and return false, and log a deprecation warning. See Events for the mapping.
  • Cancelled events skip later handlers unless they are registered with { receiveCancelled = true }. This also applies to handlers under the old names.
  • Errors cancel. A handler of a cancellable event that throws now cancels the event (a crashing whitelist used to let everyone in).
  • mc.emit refuses built-in event names. It returns false when a handler of the custom event returned false.
  • Region events: enter, leave, move, death with e.entity / e.player / e.region; the old entity_* / player_* names still work.
  • Handler options for mc.on and region:on: priority (a number, higher runs first; aliases lowest … highest), receiveCancelled, throttle. Options may also come before the handler, for Nova trailing blocks.
  • Unknown event names produce a warning with the closest name (“did you mean ‘player.join’?”).
  • Script errors in chat: errors in handlers, commands and scheduled tasks are sent to players with px.ignis.prompt_errors (operators by default), at most once per 10 seconds per error.
  • Editor support: config/ignis/.types/ (LuaLS type definitions, refreshed on start) and .luarc.json (created once). Event handlers are typed per event.
  • /ignis watch on|off: reload automatically when a .lua file is saved (-Dpxignis.watch=true to start on).
  • /ignis eval <lua>: run Lua in the scripts’ environment, me is the caller (px.ignis.eval).
  • /ignis status: loaded scripts, handlers per event, commands, scheduled tasks, regions.
  • player.login kick message: e:cancel("reason").
  • Region death / entity_death / player_death handlers were never called.
  • throttle on region handlers only counted down while the region had a tick handler; the global bus only while some tick handler existed.

0.17.1 — CI and bootstrap fixes (2026-08-01)

Section titled “0.17.1 — CI and bootstrap fixes (2026-08-01)”
  • Bootstrap crash on 1.21.10/1.21.11: PxIgnis no longer shadows its own copy of commons-lang3 (pulled in transitively by BCEL for LuaJC bytecode generation). The embedded 3.14.0 lacked the Mutable.get() method that Minecraft 1.21.11’s Spline codec expects, throwing NoSuchMethodError during registry bootstrap. The mod now uses Minecraft’s bundled commons-lang3 instead (3.17.0 on 1.21.10, 3.19.0 on 1.21.11).
  • Modrinth release publishing: Updated the publishing action to Node 24 and disabled optional version unfeaturing, avoiding a post-upload authentication failure while preserving artifact uploads.

0.17.0 — Async coroutines, region debug overlay (2026-08-01)

Section titled “0.17.0 — Async coroutines, region debug overlay (2026-08-01)”
  • mc.task, mc.run, mc.prun, mc.sleep, mc.fetch removed from the mc table. Use require "async" instead.
  • async.task / async.run now require an explicit executor — "main" or "threadpool".

require "async" — coroutine-based async module

Section titled “require "async" — coroutine-based async module”

Load with local async = require "async". Every task picks where its Lua code runs:

ExecutorIntended use
"main"Minecraft server thread — safe for players, worlds, entities, etc.
"threadpool"Bounded worker pool — expensive pure-Lua computation only

"main" runs on the server thread and must stay short. "threadpool" runs off-thread and must not touch Minecraft objects or shared globals. I/O (async.sleep, async.fetch) is already non-blocking and needs no thread pool.

APIDescription
async.task(executor, fn, ...)Runs fn(...) on an executor; returns a task (awaitable)
async.run(executor, fn, ...)Runs fn(...) and waits; returns raw values, throws on error
async.promise()Creates a manually-settleable promise
task:wait()Yields until done; returns the raw result values, throws LuaError on task error
task:try()Yields until done; returns true, result... or false, error (pcall-like)
task.donetrue once the task/promise has settled
task.state"pending", "resolved", or "rejected"
promise:resolve(...)Settles the promise with a value; returns true if it was the first settlement
promise:error(msg)Rejects the promise; returns true if it was the first settlement
async.all(t1, t2, ...)Waits for all tasks; throws the first error after all settle
async.allSettled(t1, ...)Waits for all tasks; never throws; returns { ok, value/error } per input
async.sleep(ticks)Yields the coroutine for N ticks (20 = 1s)
async.fetch(url)HTTP request, yields the coroutine; returns response table
async.fetch {...}Full request with { url, method, headers, body, json, timeout } options
async.mutex()Coroutine-safe mutex; mutex:with(fn, ...) runs with exclusive ownership

Tasks run as proper Lua coroutines — they can call async.sleep, async.fetch, and task:wait() internally.

local async = require "async"
-- Parallel computation on the worker pool
local left = async.task("threadpool", function() return generate_chunk(1) end)
local right = async.task("threadpool", function() return generate_chunk(2) end)
local results = async.all(left, right)
-- Back on the server thread
async.run("main", function()
apply_chunk(results[1].value)
apply_chunk(results[2].value)
end)
-- Waiting for I/O is already non-blocking
local response = async.fetch("https://api.example.com/data")
print(response.ok and response.text or response.error)
-- Error handling
local ok, err = async.task("threadpool", function() error("boom") end):try()
if not ok then print("failed:", err) end
-- Promises
local p = async.promise()
mc.schedule(20, function() p:resolve("done") end)
print(p:wait())
-- Mutex
local mutex = async.mutex()
mutex:with(function() return update_shared_cache() end)
  • Server-side region sync: opted-in players receive the regions in their 4-chunk interest radius as incremental diffs (/ignis debug regions, ops only). Bounds changes re-sync as upserts; a one-time warning fires at the 256-region cap.
  • Client-side registry (src/client): thread-safe region box store toggled by the command. Wireframe rendering stays a documented no-op until the renderer API is available in the mod’s Fabric/Yarn set.
  • Async suspend bridge: luaSuspendFunction / luaSuspendFunctionNil let Lua coroutines call Kotlin suspend blocks without blocking the server thread. The coroutine yields, the block runs on a CoroutineScope, and the result resumes the coroutine via a per-thread LuaThread.ResumeHandler — set on the main thread and inherited by child coroutines, dispatching resumes back to the server. Calls from outside a coroutine or without a resume handler raise a clear LuaError instead of hanging. See docs/async-suspend-bridge.md.
  • lua_resume_sync: now sets and restores LuaState.current() so thread-local state lookups work on the server thread like they do in async coroutines.
  • EventBus: Lua closure handlers now run through a LuaThread instead of being invoked directly, so async.sleep, async.fetch, and suspend functions work inside event callbacks like they do in scheduled tasks and commands.
  • Executor registry + thread pool: AsyncExecutorRegistry with bounded PxIgnis-async-* worker pools, shut down on server stop. LuaThread.executionContext propagates the owning executor to child coroutines.
  • Serialized resumption: a per-coroutine resume gate guarantees one thread.resume() at a time, so asynchronous completions never interleave with a coroutine that is already running.
  • Scheduler: now thread-safe (lock-protected queue, volatile tick counter) for cross-thread task scheduling.
  • modScope: IgnisRuntime owns a mod-lifetime CoroutineScope (SupervisorJob, cancelled on server stop). LuaMcApi.suspendFunction wraps it for convenience. RegionManager gained a shared state provider so region events resolve the Lua state like the root event bus does.
  • Build: split client source set (loom.splitEnvironmentSourceSets); each region payload now has a unique CustomPayload.Id (they previously shared one identifier and collided at registration).

0.16.1 — Interop refactor, scheduler bounds, template fixes (2026-06-26)

Section titled “0.16.1 — Interop refactor, scheduler bounds, template fixes (2026-06-26)”
  • Scheduler: Added MAX_TASKS_PER_TICK = 1024 cap to prevent server hang from runaway scheduled tasks. clear() now also resets the task ID counter.
  • StorageManager: globalData is no more nullable.
  • Reload error handling: Wrapped reloadResources mixin in try/catch so a script crash on resource reload doesn’t break the server.
  • items.lua: Template now preserves all custom keys from opts instead of a whitelist. items.find now accepts a string (template name) in addition to item tables.
  • Lua interop refactor: Replaced anonymous object : VarArgFunction() { ... } with typed bridge classes helper functions (luaFunction, luaFunctionNil, luaVarFunction). Added Kotlin<->Lua value conversion helpers: .toLua(), .takeIfValid(), .asLuaString(), .asJString(), .asTable(), .asFunction(), .asObject(), .orNil().
  • EntityWrap: Moved shared player cache and tick provider into companion properties, removing per-instance coupling. All wrappers pass shared state through WorldWrap.wrap(world, playerCache, tickProvider).
  • Vector.toVec3d / toBlockPos: Moved from root Utils.kt into Vector.Companion.
  • AsyncLib: Added URL validation in mc.fetch; replaced anonymous functions with bridge helpers.
  • ScriptEnvironment / MetaTableBuilder / LuaMcApi / Utils.kt: Import cleanup and bridge type migration.
  • Docs: Added Russian translation for the documentation site.

0.16.0 — Items library, player_death rework, world expansion, boss bar

Section titled “0.16.0 — Items library, player_death rework, world expansion, boss bar”
  • world:particle: the data field has been removed from the opts table. Particle-specific NBT fields (color, block, scale, power, etc.) now live directly in the same opts table alongside count, spread, and speed. Replace { data = { color = {1,0,0}, scale = 1 } } -> { color = {1,0,0}, scale = 1 }.
  • player_death: now cancellable and merged with entity_death handling. Signature changed from (player, damageType) to (entity, damageType, amount). Fires from ALLOW_DEATH instead of AFTER_DEATH.
  • world:broadcastInRange: arguments changed from (text, x, y, z, range) to (text, pos, range)
APIDescription
mc.execute(cmd, opts?)Executes a command with {as=entity, at=pos} options
mc.createBossBar(title, color?, style?)Creates a boss bar with addPlayer, removePlayer, destroy methods and progress/visible properties
mc.getMetatable("bossbar")Returns the boss bar shared metatable

world:* — block state, biome, explosions

Section titled “world:* — block state, biome, explosions”
APIDescription
world:getBlockState(pos)Returns { id, properties } table with typed property values
world:setBlockState(pos, { id, properties })Sets block with property overrides
world:getBiome(pos)Returns biome ID string
world:getBorder().center, .size, .damage, .warningTime, .warningBlocks, .damageThreshold
world:explode(pos, power, opts?)Creates explosion with {fire, destruction} options
world:strike(pos, opts?)Spawns lightning bolt with {effect} option
world:getEntitiesBySelector(selector, opts?)Entity selector queries — @a, @e, @e[type=...,distance=..N]
APIDescription
player:sendTitle({title, subtitle, fadeIn, stay, fadeOut})Table-based title API

Define custom item templates with scripted callbacks. Templates bundle a base item ID with custom_model_data, display properties, and optional callback functions:

local items = require "core:items"
local wand = items.define {
id = "stick", modelData = 1001,
name = "&5Magic Wand",
onUse = function(p, h, item, tpl) p:sendMessage("Poof!") end,
onTick = function(p, item, hand, tpl) p:sendActionBar("Wand ready") end,
}
FunctionDescription
items.define(opts)Creates an item template; returns template with :make(), :matches(), :has() methods
items.is(item, template)True if item matches by id + modelData
items.find(item)Returns matching template or nil
items.has(player, template)True if player has a matching item in inventory

Callbacks: onUse, onAttack, onConsume, onPickup, onInteractEntity (cancellable), onTick (held), onInventoryTick (any slot). See items library.

EventCancellableArgsDescription
player_consume_item✅player, itemFires when a living entity finishes consuming an item
player_pickup_item✅player, itemStack, countIntercepts item pickup from the ground
player_drop_item✅player, itemStack, countIntercepts Q-drop / inventory click-throw
player_move❌player, from, toFires on every position change
  • mc.players: Fixed stale wrapper after player death/respawn. The player wrapper cache was keyed by UUID and was never invalidated on respawn, causing mc.players to return a wrapper pointing at the removed ServerPlayerEntity.
  • player_pickup_item: Fixed injection point, now it fires at the correct getStack() call instead of remove(). Renamed mixin from PlayerEntityPickupMixin to ItemEntityPickup. Properly cancellable.
  • Vector metatable: Fixed __index so vector arithmetic and methods resolve correctly. (xD)
  • Lambda syntax: Implicit return was removed — \{ x -> expr } must now be \{ x -> return expr }. This fixes code generation issues. And just because LuaJ doens’t have a proper compiler with AST
  • core:items — 184-line Lua builtin registration library with onTick/onInventoryTick per-tick dispatch. Registers shared event listeners on require.
  • player_death — moved from AFTER_DEATH to ALLOW_DEATH, merged with entity_death for cancellability.
  • world:particle: now uses a custom builder for all particle types. Modded particles no more supported until Lua2Nbt parser appears.
  • WorldWrap: getBlockState, setBlockState, getBiome, getBorder, explode, strike, getEntitiesBySelector.
  • EntityWrap: readNbt/writeNbt removed.
  • PlayerWrap.sendTitle: Table argument support.
  • BossBarManager, BlockStateCodec, LuaPackage, PlayerMoveDispatcher.
  • PxLuaNova: Integer fast-paths (add/sub/mod), LuaThread.lastError clearing, sealed number hierarchy.
  • MetaTableRegistry: Backing-field refactor.
  • Utils.kt: asSequence()/toLuaTable() helpers; removed nbtToLua/luaToNbt.
  • Coroutine error logging: resumeOrLog now uses LuaThread.lastError for full stack traces instead of only the error message.

0.14.1 — MetaTableBuilder inheritance rework, EntityFactory, wrapper cleanup (2026-06-22)

Section titled “0.14.1 — MetaTableBuilder inheritance rework, EntityFactory, wrapper cleanup (2026-06-22)”

Just bug fixes & refactoring.

  • MetaTableBuilder: inherit() now takes a lazy () -> LuaTable instead of BuiltMeta so it updates dynamically (basically fixed metatables bug).
  • EntityFactory: New centralized EntityFactory.wrap(entity) that detects the type & uses the best wrapper
  • PlayerListWrapper: Filters out removed players from the cached list (bugfix)
  • MetaTableBuilder test suite: Full coverage for inheritance, method fallthrough, lazy caching, setters, pairs merging, and multi-level chains.
  • Iterator<LuaValue>.toLuaArray helper (refactor).

0.14.0 — Entity pos simplification, new argument types, bugfixes (2026-06-21)

Section titled “0.14.0 — Entity pos simplification, new argument types, bugfixes (2026-06-21)”
  • entity.pos is now a plain vector — entity.pos.x = 5 no longer works. Use entity.pos = newPos
APIDescription
vec:length()Returns sqrt(x²+y²+z²)
<name:entity> arg typeBrigadier entity selector → wrapper
  • vec(x, y, z) validation inverted — the require(args.narg() == 3) check was incorrectly != 3, causing valid 3-arg calls to throw.
  • Raycast — fixed crash
  • Scheduler — LuaClosure paths use resumeOrLog with full stack traces; plain LuaFunction callbacks avoid coroutine overhead.
  • AsyncLib (mc.sleep, mc.fetch) — uses resumeOrLog helper to log coroutine errors instead of silently failing.
  • CommandRegistrar — command execution errors now include full stack traces.
  • MetaTableBuilder.__index — uses mt.get(key) instead of mt.rawget(key) so that __index on the metatable itself is properly followed.
  • EntityWrap.pos — simplified from lazy-cached proxy metatable to direct Vec3d read/write (removed livePosTable).
  • OperationHelper.checkNumber — fixed operators bug where all table + table turned into 0.
  • PxLuaNova regression test for __add on tables.

0.12.0 & 0.13.0 — Sync compiler, sandboxed require, item builder (2026-06-21)

Section titled “0.12.0 & 0.13.0 — Sync compiler, sandboxed require, item builder (2026-06-21)”

nova.sync() — JIT compile to JVM bytecode

Section titled “nova.sync() — JIT compile to JVM bytecode”

nova.sync(fn) JIT-compiles a Lua function for better performance. The compiled function is cached and reused. See Nova API.

local add = nova.sync(\{ a, b -> a + b })

require now only loads modules from config/ignis/. Built-in libraries use a core: prefix:

ModuleOldNew (core: prefix)
Formatrequire "format"require "core:format"
Simplerequire "simple"require "core:simple"
Chest GUIrequire "chestgui"require "core:chestgui"

Prefixes may become available later for user scripts.

mc.createItem now accepts a component table in addition to the positional form:

mc.createItem("minecraft:diamond_sword", {
name = "§bBlade",
lore = { "A legendary sword" },
count = 1,
unbreakable = true,
custom_model_data = 42,
})
-- Single-table form:
mc.createItem { id = "minecraft:stone", count = 64 }

New mc.serialise and mc.deserialise for ItemStacks:

local json = mc.serialise("item", item) -- ItemStack → JSON string
local item = mc.deserialise("item", json) -- JSON string → ItemStack

PxLuaNova’s lambda literal syntax is available in PxIgnis scripts. Opt in per file with --# nova syntax on line 1. See Language extensions for all four forms: named-arg (->), zero-arg expression, zero-arg chunk, and trailing-block sugar.

ChangeMigration
require "format" / "simple" / "chestgui" now require core: prefixrequire "core:format"
Built-in Lua libs io, os, debug not loadedRely on math, string, table, bit32, coroutine
collectgarbage, loadfile, dofile removed from globalsUse nova.sync() or require for loading code
world:playSound(id, x, y, z, volume?, pitch?) → world:playSound(id, pos, volume?, pitch?)Pass vec(x, y, z) as second argument
  • LuaCmdLoader.kt split into IgnisRuntime, ScriptEnvironment, ScriptLoader
  • New MetaTableBuilder DSL for cleaner metatable construction (used by all wrappers)
  • Vfs + LuaRequire — sandboxed module loading replacing PackageLib
  • Package reorganisation: commands/, api/wrapper/, api/manager/, api/util/ subpackages
  • ItemBuilder, ItemStackCodec — new utility classes
  • Test suite cleanup: removed /ignis reload-dependent tests (HologramManagerTest, RegionTest)
  • Built-in Lua libraries (demo.lua, format.lua, simple.lua, chestgui.lua) updated
  • All reference docs updated; new guide page Events & Storage

Spatial areas in a world with event subscriptions. Create with two corners (auto-normalized), subscribe to enter/leave/move/death/tick events with optional per-callback throttling.

APIDescription
world:createRegion(posA, posB)Creates a region between two corners
r:getBounds()Returns {A, B} — the two bounding corners (A = min, B = max)
r:setBounds(posA, posB)Updates region bounds; re-evaluates entity membership
r:on(event, fn, opts?)Subscribes to a region event; returns handler ID
r:off(id)Unsubscribes a handler by ID; returns true if removed
r:contains(pos)true if position is inside the bounds
r:destroy()Destroys region, fires destroy event, clears handlers
r.playersSequence of players currently inside
r.entitiesSequence of entities currently inside
r.idSession-unique integer ID
r.worldWorld the region belongs to
world.regionsSequence of all live regions in this world
mc.getRegion(id)Looks up a region by ID across all worlds; returns Region or nil
world:getRegion(id)Looks up a region by ID in this world; returns Region or nil
world:getRegionsAt(pos)Returns a sequence of regions in this world containing pos

Events: entity_enter, entity_leave, entity_move, player_enter, player_leave, player_move, entity_death, player_death, tick, destroy.

local r = world:createRegion({x=0,y=0,z=0}, {x=100,y=64,z=100})
r:on("entity_enter", function(e) e:sendMessage("Welcome!") end)
r:on("entity_move", function(e, from, to) print((to - from):length()) end, { throttle = 5 })
r:on("tick", function() end) -- auto-subscribes, no enableTick needed
-- Look up a region later by ID (e.g. one stored in mc.data)
local saved = mc.getRegion(r.id)
-- Find all regions at a position
for _, reg in ipairs(world:getRegionsAt({ x = 50, y = 64, z = 50 })) do
print("In region", reg.id)
end

The event system was refactored into a single EventBus class backing both mc.on and region:on. Changes:

  • mc.on now returns a handler ID (integer). Pass it to mc.off(id) to unsubscribe.
  • mc.off(id) — new global API. Returns true if the handler was removed.
  • region:on now returns a handler ID. Pass it to region:off(id) to unsubscribe.
  • mc.on("tick", fn) — now works globally, same auto-subscribe semantics as regions. Fires every server tick.
  • mc.emit / mc.on signatures unchanged.
local id = mc.on("player_join", function(p) print(p.name) end)
mc.off(id) -- remove handler
local rid = r:on("entity_enter", function(e) e:sendMessage("Hi!") end)
r:off(rid)
  • mc.players() → mc.players — now a strict read-only list property, not a function. Callers must use mc.players without parentheses.
  • world.players refactored. It’s now cached per-tick.
  • mc.getWorld(name) now also caches.
FunctionDescription
world:spawnHologram(pos, text, opts?)Spawns a minecraft:text_display entity; returns hologram wrapper
mc.hologramsList of all live holograms (property)
mc.getHologram(uuid)Returns hologram by UUID, or nil

Hologram wrapper exposes r/w properties: text, lines, alignment, billboard, lineWidth, background, opacity, shadow, seeThrough, glowing, pos, plus :setLine(n, text) and :destroy() methods. Delegates to entity metatable for uuid, world, removed, pos live proxy, tags, NBT read/write, etc.

local h = world:spawnHologram({x=0,y=64,z=0}, "Hello\nWorld", {billboard="center"})
h.lines = {"Line 1", "Line 2"}
h:setLine(1, "Updated")
mc.schedule(40, function() h:destroy() end)
  • All holograms are destroyed on /ignis reload.
  • Per-player holograms deferred as future work (would need custom packet dispatch).

0.9.0 — Removed ByteBuddy, 6 new events (23 total)

Section titled “0.9.0 — Removed ByteBuddy, 6 new events (23 total)”
EventCancellableArgsFires
entity_spawn❌entityAny entity loads into a world
entity_despawn❌entityAny entity unloads from a world
entity_death✅entity, source, amountAny living entity is about to die (players + mobs)
player_join_init✅playerEarly join, player not fully loaded (renamed from player_join)
player_join❌playerPost-load join, player fully ready in world
player_respawn❌player, wasDeathPlayer respawns after death or end portal

Migration: player_join was renamed to player_join_init (still cancellable, early). The new player_join fires later when the player is fully in-world.

Removed: mc.observeHook / mc.removeHook / mc.clearHooks

Section titled “Removed: mc.observeHook / mc.removeHook / mc.clearHooks”

ByteBuddy (byte-buddy + byte-buddy-agent) was pulling ~4.3 MB into the output jar for a single unstable API (mc.observeHook). Use mc.on() events instead.

  • Deleted LuaMixinManager.kt (ByteBuddy — 152 lines)
  • Output jar reduced from ~4.9 MB to ~839 KB

0.8.0 — Async API, PxLuaNova embedded, documentation site

Section titled “0.8.0 — Async API, PxLuaNova embedded, documentation site”
  • Added mc.fetch(url) — coroutine-yielding HTTP requests via HttpClient.sendAsync
  • Added mc.sleep(ticks) — coroutine-yielding timed delay via Scheduler.schedule
  • Embedded PxLuaNova as a Gradle composite build (pxluanova/) — no more Maven Local dependency
  • Dependency auto-substitution via includeBuild 'pxluanova' in settings.gradle
  • New Astro + Starlight documentation site at site/ (brand: PxIgnis)
  • 28 documentation pages covering all APIs
  • .gitignore patterns fixed to match nested directories (trailing-slash form)
  • General cleanup and minor improvements

0.7.0 — Mob AI, ByteBuddy hooks, sidebar rewrite, PxLuaNova migration

Section titled “0.7.0 — Mob AI, ByteBuddy hooks, sidebar rewrite, PxLuaNova migration”
  • Using a custom fork of LuaJ with fixes & performance improvements.
  • Sidebar API enhanced — player.sidebar property now returns a sidebar object with title/lines (r/w), show()/hide()/destroy()/setLine(n, text), and visible/lineCount properties. Assignment with {title, lines, visible} creates or updates the sidebar. Sidebar no longer auto-restores on reconnect — script must re-create it.
APIDescription
mc.registerBehaviour(id, fn)Registers a named AI behaviour function
mob:setAI("id") / mob:setAI(fn)Assigns behaviour by name or directly with a function. Persists on reload if ID is used
mob:clearAI()Removes behaviour, restores vanilla AI
mob.aiActiveWhether a behaviour is currently active

Mob properties (delegates to entity metatable for entity-level props):

PropertyTypeNotes
mob.isMobboolAlways true
mob.targetentity or nilAttack target (r/w by UUID)
mob.speednumberMovement speed attribute
mob.pathRemaining0–1Navigation progress fraction
mob.pathFoundboolWhether a path is active

Mob methods:

MethodSignature
mob:navigateTo(x, y, z) / mob:navigateTo(entity)Pathfind to position or entity
mob:stopNavigation()Cancel current path
mob:lookAt(x, y, z) / mob:lookAt(entity)Turn toward position or entity
mob:moveToward(vec, speed)Direct movement toward position
mob:jump()Trigger jump
mob:canSee(entity)Line-of-sight check
mob:distanceTo(entity) / mob:distanceTo(vec)Euclidean distance

world:spawn() and mc.getEntity() automatically return MobWrapper for MobEntity subtypes.

Built-in behaviours: guard, pet, orbiter, statue, wander (check demo_ai.lua).

ByteBuddy runtime method hooks (experimental, unstable)

Section titled “ByteBuddy runtime method hooks (experimental, unstable)”
FunctionDescription
mc.observeHook(class, method, callback)Hooks a Java method at runtime via ByteBuddy
mc.removeHook(class, method) → boolRemoves a hook
mc.clearHooks()Removes all hooks

Hooks are cleared on /ignis reload and server stop. Callback receives (instance, args). Only single-overload methods supported. May be removed in future versions.

FunctionDescription
mc.runtimeNamespaceCurrent mapping namespace (e.g. "named" or "intermediary")
mc.mapped(className)Map named class → runtime class name
mc.getMetatable("sidebar")Sidebar shared metatable
mc.getMetatable("mob")Mob shared metatable

mc.runtimeNamespace & mc.mapped(className) have no purpose btw, hooks map names automatically.

player.sidebar = { title = "Title", lines = {"Line 1", "Line 2"} }
local sb = player.sidebar
sb.title = "New Title" -- update title
sb.lines = {"Line 1", "Line 2"} -- replace all lines
sb:setLine(1, "Updated") -- update specific line
sb:show() / sb:hide() / sb:destroy()
sb.visible / sb.lineCount -- read-only properties

Coroutines are now supported!

FunctionDescription
mc.fetch(url)Simple HTTP GET, yields coroutine
mc.fetch({url, method?, headers?, body?, json?, timeout?})Full request table. json auto-encodes Lua table as JSON body. body and json are mutually exclusive
mc.sleep(ticks)Yields the coroutine for ticks ticks (20 = 1s), resumes on server thread

mc.fetch returns a response table with a shared metatable:

FieldDescription
res.oktrue for 2xx
res.statusHTTP status code
res.textRaw body string
res.headersResponse headers table
res.jsonParsed JSON. Throws LuaError on invalid JSON
res.errorSet only on network failures

0.6.0 — Vector arithmetic, Inventory/Container API, Raycast, shared metatables

Section titled “0.6.0 — Vector arithmetic, Inventory/Container API, Raycast, shared metatables”
  • Player.kt → PlayerWrapper.kt, World.kt → WorldWrapper.kt (internal refactor, no Lua API change)
  • player.world:particle(id, x, y, z) → player.world:particle(id, vec(x, y, z), opts?) (positional args replaced by vector + options table)
  • Removed dead coerce/KotlinToLua.kt
  • vec(x, y, z) global constructor with +, -, *, /, unm, ==, tostring operators
  • Component-wise for v1 * v2, scalar for v / n, both v * n and n * v
  • Vector metatable accessible via mc.getMetatable("vec")
APIDescription
mc.createInventory(size)Creates a virtual SimpleInventory (9–54, multiple of 9)
inv:getItem(slot), inv:setItem(slot, item)Slot access (1-based)
inv:fill(item), inv:clear()Bulk ops
inv:open(player, title?)Opens chest screen → Container
container:onClick(callback)Registers click handler (auto-locks inventory)
container:onClick(nil)Unlocks inventory
container:close()Closes the screen

Both entity:raycast(range) and world:raycast(start, dir, range) now return a result table:

-- Block hit:
{ type = "block", blockPos = Vec(...), hit = Vec(...), side = "north", normal = Vec(...) }
-- Entity hit:
{ type = "entity", entity = EntityWrapper, hit = Vec(...) }
  • world:raycast(startVec, dirVec, range, includeFluids?, includeEntities?)
  • world:playSound(id, x, y, z, volume?, pitch?)
  • world:particle(id, pos, opts?) — vector position, options table with count, spread, speed, data
  • Item wrappers now use shared metatable (mc.getMetatable("item"))
  • mc.createItem full component table (name, lore, unbreakable, attackDamage, etc.)
  • ItemStackWrapper.toJson/fromJson for serialization
  • nbtToLua / luaToNbt utility functions in Utils.kt
  • chestgui.lua — chest GUI library with grid positioning
  • All wrappers now use shared metatables (one per type) — __index, __newindex, __pairs on the metatable, methods via rawset
  • player:damage(amount), player:heal(amount), player:give(id/count or ItemStack), player:setItem(slot, item), player:getItem(slot), player:clear()

No API changes. CI fix for Modrinth publish.


0.5.0 — Entity/Structure/Sidebar/Metatable APIs, 10 new events

Section titled “0.5.0 — Entity/Structure/Sidebar/Metatable APIs, 10 new events”
FunctionSignature
mc.getMetatable(name)"entity"|"player"|"world"|"structure" → shared metatable
mc.loadStructure(id)string → structure table
mc.loadStructureFile(path)file path → structure table
mc.getEntity(uuid)string → entity table or nil
mc.dump(value, maxDepth?)recursive table debug dump
mc.emit(event, ...)programmatic event firing
mc.playersproperty — list of online player tables (cached)
mc.onlineCountproperty — number of online players
MethodSignatureNotes
entity:readNbt()→ tableDump entity NBT to Lua table
entity:writeNbt(t)← tableWrite NBT back to entity
entity:raycast(range, includeFluids?)→ entity or {x,y,z} or nilHits entities before blocks
entity:damage(amount, sourceEntity?)—With optional damage source
entity:addEffect(id, duration, amplifier?, particles?, icon?)→ boolStatus effect
entity:removeEffect(id)→ bool
entity:hasEffect(id)→ bool
entity:setOnFireFor(ticks)—
entity.removedr/o boolWhether entity is removed
entity.pos.x/y/zr/w now livePreviously snapshot; now reads/writes current pos
MethodSignature
player:sendActionBar(text)
player:sendTitle(title, subtitle?, fadeIn?, stay?, fadeOut?)
player:playSound(id, volume?, pitch?)
player:getItem(slot) → item table or nil
player:setItem(slot, item)item from mc.createItem or nil
player:clear()Clear inventory
player.sidebarr/w property — set to {title="...", lines={...}} or nil to clear
player.sidebar.titler/w string
player.sidebar[i]r/w lines (1-indexed)
MethodSignatureNotes
world:particle(id, pos, opts)Moved from mc.particleParticle visible to all in that world
world:broadcastInRange(text, pos, range, overlay?)Moved from mc.broadcastInRange
world:getEntities(pos, radius, typeFilter?)→ table of entity tablesSpatial entity query
  • mc.particle(...) → use world:particle(...)
  • mc.broadcastInRange(...) → use world:broadcastInRange(...)

Cancellable:

  • player_block_break(player, pos, blockId)
  • player_block_place(player, pos, blockId)
  • player_use_item(player, hand, item, itemId)
  • player_attack_entity(player, entity)
  • player_interact_entity(player, entity)
  • player_hurt(player, source, amount)
  • entity_hurt(entity, source, amount)

Non-cancellable:

  • player_damage(player, source, damageTaken, blocked)
  • entity_damage(entity, source, damageTaken, blocked)
  • player_kill(player, killedEntity, source)
  • Structure table — .size (vector), :place(world, pos, opts?) (rotation, mirror, on_entity)
  • Shared metatables — mc.getMetatable("entity"|"player"|"world"|"structure")
  • item.custom_model_data — now readable on item tables
  • mc.createItem(id, {attackDamage = N}) — new option to set attack damage
  • world:spawn(id, pos, overrides?) now returns entity with all new methods (raycast, damage, effects, NBT)
  • entity.dir, entity.bodyDir now return {x,y,z} vector tables (previously Java userdata)
  • Player cache: mc.players and world.players reuse wrappers across lookups
  • Sidebar persists across world changes and reconnects (restored 2 ticks after join)
  • player.block_break/player.block_place migrated from mixins to Fabric API events

0.4.0 — World/Entity API, tags, item creation

Section titled “0.4.0 — World/Entity API, tags, item creation”
FunctionSignature
mc.world(name)string → world table
mc.createItem(id, countOrTable?)→ item table
mc.setBlock(x,y,z,block,world)Removed in 0.5.0 — use world:setBlock
mc.getBlock(x,y,z,world)Removed in 0.5.0 — use world:getBlock
mc.fill(x1,y1,x2,y2,z1,z2,block,world)Removed in 0.5.0 — use world:fill
MethodNotes
world:spawn(id, pos, overrides?)→ entity table
world:setBlock(pos, blockId)
world:getBlock(pos)→ block id string
world:fill(pos1, pos2, blockId)
world.name, world.time, world.raining, world.thunderingr/w properties

Entity table returned by world:spawn() and player delegation:

PropertyTypeNotes
uuidstring
typestringe.g. "minecraft:zombie"
name, displayName, customNamestring
worldtableWorld wrapper (was string in 0.3.0)
pos{x,y,z} vectorr/w (snapshot in 0.4.0; live proxy in 0.5.0)
dir, bodyDir{x,y,z} vector
health, maxHealthnumber
fallDistance, fireTicksnumber
glowing, invulnerableboolean
isSneaking, isSprintingboolean
air, maxAirnumber
tagsboolean proxypairs(entity.tags), entity.tags["tag"] = true/false
speed, armor, attackDamage, maxHealth_attr, followRange, knockbackResistance, luck, horseJump, flyingSpeed, armorToughness, movementEfficiency, scale, stepHeightnumberAttribute accessors (r/w where vanilla permits)
mainhand, offhand, head, chest, legs, feetitem table or nilEquipment slots (writable)
  • player.world now returns a world table (was a world name string)
  • player:give(id, count) also accepts item table from mc.createItem
  • All entity properties delegated to entity metatable (player inherits pos, health, tags, equipment, etc.)
  • word — single-word string (StringArgumentType.word())
  • Block IDs auto-prefixed with minecraft: if no namespace given
  • demo.lua no longer requires return {...} at file end
  • Shadow relocates org.luaj → ru.pyxiion.lib.luaj and Permissions API

0.3.0 — Scheduler, teleport fix, Lua arg fallback

Section titled “0.3.0 — Scheduler, teleport fix, Lua arg fallback”
FunctionSignature
mc.schedule(delayTicks, callback)→ task id
mc.scheduleRepeating(delayTicks, intervalTicks, callback)→ task id
mc.cancelTask(id)→ bool
mc.data, player.dataPersistent key-value storage
  • server_start, server_stop
  • player_join(player) — cancellable
  • player_leave(player), player_death(player, source, message)
  • player_chat(player, message) — cancellable
  • player:send(msg), player:teleport(x,y,z), player:kick(reason?)
  • player:give(id, count), player:damage(amount)
  • player:getInventory() → table of item stacks
  • player:getBlockPos() → {x,y,z}
  • player.name, player.uuid, player.world, player.pos (read-only snapshot)
  • player.health (r/w), player.displayName (r/w)
  • register(syntax, handler, permission?) — literal/arg syntax with <name:type> and [optional]
  • Types: text, player (alias target), int, double, float, bool, block_pos, choice=a,b,c
  • Reserved commands: stop, reload, op, deop, ban, ban-ip, pardon, pardon-ip, save-all, save-on, save-off, whitelist, pxrp
  • JSON storage: mc.data → global, player.data → per-player
  • Saved on server stop, player disconnect, /ignis reload
  • Nested tables require re-assignment (data.nested = t)
  • require "format" — format(template), broadcastFormat(template)
  • require "simple" — registerSimple(syntax, template, range?, overlay?)
  • Vec(x, y, z) constructor with arithmetic metamethods