A Lua-scriptable roleplay command framework for Minecraft Fabric servers. Define custom chat commands and complex server logic using Lua scripts — no Java or Kotlin mod code required.
This project is developed with the assistance of AI. Humans were harmed (and included) during development too.
- Features
- Quick Start
- Examples
- Registering Commands
Vec(x, y, z)— vector constructormc.*API- Player API
- ItemStack API
- Inventory API
- Container API
- World API
- Entity Wrapper
- Structure Wrapper
- Bundled Lua Libraries
- Events Reference
- Built-in Lua Standard Libraries
- Storage
- License
- Lua-driven commands — Write
.luafiles to register Brigadier commands with tab completion, argument parsing, and permission checks. - Event system — React to 17 game events: player join/leave/death/chat/kill/damage/hurt, block break/place, item use, entity attack/interact, entity hurt/damage, and server lifecycle with Lua handlers (9 cancellable).
- Dynamic reload —
/pxrp reloadre-executes all Lua scripts instantly without restarting the server. All Lua state is torn down and rebuilt — persistent data must usemc.data/player.data. - Rich argument types — Supports
text,word,target/player,int,double,float,bool,block_pos, and custom choices (choice=a,b,c) with validation. - Minecraft API exposed to Lua — Trigger particles, sounds, global/range broadcasting, block manipulation, entity spawning, world time/weather control, and server time access.
- Persistent data storage — Key-value data per player (
ctx.player.data) and globally (mc.data), auto-persisted to JSON. - Permission system — Integrates with the Fabric Permissions API (supports both OP-based and permissions plugins like LuckPerms).
- Player context — Handlers receive a live
Playerwrapper object with readable properties (health, position, gamemode, etc.) and methods (sendMessage,teleport,kick,give). - Structure loading — Load and place Minecraft structure files with rotation, mirroring, and per-entity Lua callbacks.
- Vector API —
Vec(x, y, z)global constructor with arithmetic operators (+,-,*,/,unm,==,tostring). Component-wise forv1 * v2, scalar forv / n. Bothv * nandn * vwork. - Entity API —
entity:damage(amount, source?),entity:raycast(range),entity:addEffect/removeEffect/hasEffect,entity:setOnFireFor(ticks),entity:readNbt()/writeNbt(table). - Debug dumping —
mc.dump(obj, depth?)prints any Lua value as readable nested output with cycle detection. - Metatable extensions —
mc.getMetatable("player"/"entity"/"world"/"structure"/"vec")allows adding custom methods to all wrappers of that type. - Per-player sidebar —
player.sidebar = {title = "...", lines = {...}}for packet-based scoreboard display. - Lua libraries — Bundled
format.lua(f-string-like templating),simple.lua(concise command registration), andchestgui.lua(chest-based GUI with grid positioning).
- Minecraft 1.21.x, Fabric Loader ≥0.19.2, Fabric API ≥0.141.4, Fabric Language Kotlin ≥1.10.8
- Install the mod on your Fabric server.
- On first run,
config/pxrp/demo.luais created with example scripts. - Run
/pxrp reload(requires operator level 4 orpyxiion.pxrppermission) to apply changes.
register("fart", function(ctx)
local player = ctx.player
local pos = player.pos
local dir = player.bodyDir
broadcastFormat "*{p.name} farted*" {p = player}
player.world:particle("minecraft:gust", Vec(pos.x - dir.x * 0.5, pos.y + 0.6, pos.z - dir.z * 0.5))
player.world:playSound("minecraft:entity.slime.squish", pos.x, pos.y, pos.z, 10, 0.1)
end)Argument types can be given custom names with the name:type syntax:
register("rp kill", function(ctx, target)
broadcastFormat "*{p.name} killed {t.name}*" {p = ctx.player, t = target}
end, "rp.kill")
-- Custom arg names are useful when you have multiple args of the same type:
register("try <action:text>", function(ctx, action)
mc.broadcast(ctx.player.name .. " tries to " .. action)
end)Player data and global data persist to JSON automatically:
register("coins", function(ctx)
local bal = ctx.player.data.coins or 0
mc.broadcast("You have " .. bal .. " coins")
end)
register("rp pay <target:player>", function(ctx, target)
local bal = ctx.player.data.coins or 0
if bal < 10 then
mc.broadcast("Not enough coins! You have " .. bal)
return
end
ctx.player.data.coins = bal - 10
target.data.coins = (target.data.coins or 0) + 10
mc.broadcast(ctx.player.name .. " paid 10 coins to " .. target.name)
end)mc.on("player_join", function(player)
mc.broadcast("Welcome, " .. player.name .. "!")
end)
mc.on("player_chat", function(player, message)
if message:find("badword") then
player:sendMessage("NO BAD WORDS ON MY SERVER!")
return false -- suppress the message
end
end)
mc.on("server_start", function()
mc.data.startTime = mc.time()
end)register("cmd <name:type> [<name:type>]", handler, permission?)
| Part | Meaning |
|---|---|
cmd sub |
Literal path tokens |
<name:type> |
Required argument. Missing :type raises parse error |
[<name:type>] |
Optional trailing argument. Everything from first [...] onward is optional. Missing → nil |
<name:choice=x,y> |
Choice type — runtime validation, tab completions |
Types: text (multi-word), word (single word), player (or target alias), int, double, float, bool, block_pos (returns {x,y,z}), choice=opt1,opt2,...
Handler: function(ctx, arg1, arg2, ...) — ctx.player is a live wrapper. ctx.player.data is per-player DataTable.
Reserved commands — the following top-level command names cannot be overridden via register():
pxrp, stop, reload, op, deop, ban, ban-ip, pardon, pardon-ip, save-all, save-on, save-off, whitelist.
Optional arguments — everything from the first [<name:type>] onward is optional. Missing params become nil in Lua.
Examples:
register("msg <target:player> <msg:text>", handler)
register("mute <target:player>", handler, "pxrp.mod")
register("setblocks <pos:block_pos> <block:text>", handler, "pxrp.admin")
register("homelist", handler)
register("gamemode <mode:choice=creative,survival,adventure,spectator> [<target:player>]", handler, "pxrp.admin")
register("kick <target:player> [<reason:text>]", handler, "pxrp.mod")Every command handler receives a Context object as its first argument:
| Field | Type | Description |
|---|---|---|
ctx.player |
Player |
The player who executed the command |
A global Lua function available in all scripts. Creates a vector table with arithmetic operators. Vectors are used across the API — pos, dir, blockPos, hit, normal, size are all vector tables.
| Property | Type | Description |
|---|---|---|
x |
number | X component |
y |
number | Y component |
z |
number | Z component |
| Operator | Description |
|---|---|
v + w |
Component-wise addition (v and w can be vectors or scalars) |
v - w |
Component-wise subtraction |
v * w |
Component-wise multiplication (both v * n and n * v work) |
v / n |
Scalar division (vector must be first, denominator must be number) |
-v |
Negation |
v == w |
Equality (all three components must match) |
tostring(v) |
Returns "(x, y, z)" |
local v = Vec(1, 2, 3)
local w = Vec(4, 5, 6)
print(v + w) -- (5, 7, 9)
print(v - w) -- (-3, -3, -3)
print(v * 2) -- (2, 4, 6)
print(2 * v) -- (2, 4, 6)
print(v * w) -- (4, 10, 18) component-wise
print(v / 2) -- (0.5, 1, 1.5)
print(-v) -- (-1, -2, -3)
print(v == Vec(1,2,3)) -- true
print(tostring(v)) -- "(1, 2, 3)"Any {x, y, z} table in the API accepts a Vec — they're interchangeable.
The vector metatable is accessible via mc.getMetatable("vec") for custom extensions.
Sends a chat message to all players. If overlay is a number, sends a title overlay for that many ticks.
Returns the current server epoch time in seconds (as a double). Useful for cooldowns:
local last = ctx.player.data.lastFart or 0
if mc.time() - last < 10 then
ctx.player:sendMessage("Wait " .. (10 - (mc.time() - last)) .. " seconds!")
return
end
ctx.player.data.lastFart = mc.time()Prints any Lua value to console as formatted nested output. Supports tables, functions, cycles (detected and shown as {...}), and custom __pairs metamethods. Returns the string.
mc.dump({a = 1, b = {c = "hello"}})
-- {
-- a = 1,
-- b = {
-- c = "hello",
-- },
-- }
-- Custom depth (default 3)
mc.dump(ctx.player, 1)Runs a Lua function once after delay ticks (20 ticks = 1 second). Returns a task ID.
mc.schedule(40, function()
mc.broadcast("2 seconds have passed!")
end)Runs a Lua function repeatedly. First execution after delay ticks, then every interval ticks. Returns a task ID.
local id
id = mc.scheduleRepeating(0, 20, function()
local time = mc.data.gameTime or 0
time = time + 1
mc.data.gameTime = time
mc.broadcast(time .. " second(s) have passed!")
end)
-- THIS WON'T WORK (because it's Lua)
local id = mc.scheduleRepeating(0, 20, function()
mc.cancelTask(id) -- id will be nil
end)
-- FIX
local id
id = mc.scheduleRepeating(0, 20, function()
mc.cancelTask(id) -- Now works fine =D
end)Cancels a task by its ID. Returns true if found and cancelled, false otherwise.
local id = mc.schedule(100, function()
mc.broadcast("This will never run!")
end)
mc.cancelTask(id)- All tasks are automatically cancelled on
/pxrp reloadand server stop. - Callback errors are caught and logged per-task without affecting other tasks.
- Callbacks run on the server tick thread — do not perform blocking operations.
Returns an array of Player wrappers for all online players. Wrappers are cached per UUID — repeated calls reuse the same Lua objects.
for i, p in ipairs(mc.players()) do
print(p.name, p.health)
endThe current number of online players
if mc.onlineCount == 0 then
mc.broadcast("Server is empty!")
endLooks up an entity by UUID across all worlds. Returns an EntityWrapper or nil if not found.
local e = mc.getEntity("a1b2c3d4-...")
if e ~= nil then
e:damage(10)
endReturns a World wrapper for the given dimension name (e.g. "overworld", "the_nether", "the_end"). Also accessible via player.world.
local w = mc.world("overworld")
w.time = w.time - (w.time % 24000) + 6000 -- set to noonLoads a structure from the Minecraft structure block manager by registry ID (e.g. "minecraft:igloo/igloo_top"). Returns a Structure wrapper.
local s = mc.loadStructure("minecraft:igloo/igloo_top")
mc.broadcast("Size: " .. s.size.x .. "x" .. s.size.y .. "x" .. s.size.z)Loads a structure from an .nbt file on disk. Path is relative to the server root or absolute.
local s = mc.loadStructureFile("config/pxrp/mybuild.nbt")
s:place(player.world, {x = 0, y = 64, z = 0})Creates an ItemStack. See ItemStack API for the full reference.
local arrows = mc.createItem("minecraft:arrow", 64)
local sword = mc.createItem("minecraft:diamond_sword", {
name = "§cLegendary Sword",
lore = {"Wielded by champions"},
unbreakable = true,
count = 1
})
player:setItem(0, sword)
player:give(mc.createItem("minecraft:gold_ingot"))A server-wide persistent table (config/pxrp/storage/global.json). Data is written on server stop, player disconnect, and /pxrp reload.
mc.data.eventActive = true
mc.data.totalPlayers = (mc.data.totalPlayers or 0) + 1Returns a singleton LuaTable for one of 5 wrapper types. Functions set on these tables become available on all wrappers of that type via __index fallthrough.
| Name | Affects |
|---|---|
"player" |
Player wrappers (checked before entity) |
"entity" |
Entity wrappers (Player delegates here) |
"world" |
World wrappers |
"structure" |
Structure wrappers |
"vec" |
Vector tables (arithmetic metamethods) |
local meta = mc.getMetatable("entity")
meta.myFunc = function(self) return self.name end
-- Now `entity:myFunc()` works on ALL entitiesMethods are colon-callable (receive self as arg1).
Registers a Lua handler for a game event. See Events Reference for available events.
Fires a game event programmatically, triggering all registered Lua handlers for that event. Any extra arguments are passed through to the handlers.
mc.emit("server_start")
mc.emit("player_chat", somePlayer, "hello")The Player object is accessed via ctx.player inside a command handler, or as the first argument in events like player_join. It's a live wrapper around the Minecraft player — every property read fetches current state from the entity.
| Property | Type | Settable | Description |
|---|---|---|---|
name |
string | ❌ | Player name |
uuid |
string | ❌ | UUID string |
world |
World | ❌ | World wrapper (use world.name for the path string) |
pos |
{x, y, z} |
✅ | Position |
dir |
{x, y, z} |
❌ | Look direction |
bodyDir |
{x, z} |
❌ | Body yaw direction |
health |
number | ✅ | Current health |
maxHealth |
number | ✅ | Max health (via attribute) |
food |
number | ✅ | Food level |
saturation |
number | ❌ | Saturation |
gamemode |
string | ✅ | Gamemode ("survival", "creative", etc.) |
ping |
number | ❌ | Latency (ms) |
xpLevel |
number | ❌ | Experience level |
xpProgress |
number | ❌ | XP progress (0–1) |
isOp |
boolean | ❌ | Operator status |
displayName |
string | ❌ | Display name |
customName |
string | ✅ | Custom name tag (nil clears) |
isSneaking |
boolean | ✅ | Sneaking state |
isSprinting |
boolean | ✅ | Sprinting state |
selectedSlot |
number | ❌ | Hotbar slot |
fallDistance |
number | ✅ | Fall distance |
isFlying |
boolean | ❌ | Flying state |
air |
number | ✅ | Air ticks (max 300) |
removed |
boolean | ❌ | Entity removed from world |
tags |
table | ✅ | Command tags proxy (tags["foo"] = true) |
maxAir |
number | ❌ | Max air ticks |
armor |
number | ✅ | Armor attribute |
armorToughness |
number | ✅ | Armor toughness attribute |
attackDamage |
number | ✅ | Base attack damage attribute |
attackSpeed |
number | ✅ | Attack speed attribute |
blockBreakSpeed |
number | ✅ | Block break speed attribute |
flyingSpeed |
number | ✅ | Flying speed attribute |
gravity |
number | ✅ | Gravity attribute |
knockbackResistance |
number | ✅ | Knockback resistance attribute |
luck |
number | ✅ | Luck attribute |
safeFallDistance |
number | ✅ | Safe fall distance attribute |
scale |
number | ✅ | Scale attribute |
speed |
number | ✅ | Movement speed attribute |
stepHeight |
number | ✅ | Step height attribute |
mainhand |
ItemStack | ✅ | Active hotbar slot item |
offhand |
ItemStack | ✅ | Offhand item |
head |
ItemStack | ✅ | Helmet slot item |
chest |
ItemStack | ✅ | Chestplate slot item |
legs |
ItemStack | ✅ | Leggings slot item |
feet |
ItemStack | ✅ | Boots slot item |
Setting a read-only property logs a warning and does nothing.
Methods are called with : syntax:
ctx.player:sendMessage("Hello!")
ctx.player:teleport(100, 64, 200)| Method | Args | Description |
|---|---|---|
sendMessage |
text | Sends a chat message |
sendActionBar |
text | Action bar overlay |
sendTitle |
title, [subtitle=nil] | Title + optional subtitle (fade 20/60/20 ticks) |
kick |
[reason="Kicked"] | Disconnects the player |
teleport |
x, y, z, [worldName=nil] | Teleport (intra-world or cross-dimension) |
damage |
amount | Deal generic damage |
heal |
amount | Heal health |
playSound |
id, [volume=1], [pitch=1] | Play a sound to the player |
give |
item | Give item — either a string (e.g. "minecraft:diamond 5") or an ItemStack wrapper |
setItem |
slot, item | Set item in inventory slot (ItemStack or nil) |
getItem |
slot | Get item from slot → ItemStack or nil |
clear |
— | Clear entire inventory |
A packet-based per-player scoreboard sidebar that does not affect the global scoreboard.
-- Create sidebar
player.sidebar = {
title = "My Server",
lines = {"Line 1", "Line 2", "Line 3"}
}
-- Update parts
player.sidebar.title = "New Title"
player.sidebar.lines = {"Updated!"}
-- Remove
player.sidebar = nilThe sidebar persists across worlds and reconnects (restored 2 ticks after join).
A Lua table that persists to disk (config/pxrp/storage/players/<uuid>.json). Data is written on server stop, player disconnect, and /pxrp reload.
ctx.player.data.coins = (ctx.player.data.coins or 0) + 1
ctx.player.data.inventory = {sword = 1, shield = 1}
-- ❌ Nested sub-tables require re-assignment:
ctx.player.data.nested.key = "value"
-- ✅ Correct pattern:
local t = ctx.player.data.nested or {}
t.key = "value"
ctx.player.data.nested = tItemStack wrappers are returned by equipment properties, inventory methods, and mc.createItem(). Empty slots return nil.
All read-only:
| Property | Type | Description |
|---|---|---|
id |
string | Registry ID (e.g. "minecraft:diamond") |
count |
number | Stack count |
custom_model_data |
number | Custom model data value, if set |
Short form: mc.createItem(id, count). Extended form with a components table:
| Field | Type | Description |
|---|---|---|
count |
int | Stack count (default 1) |
name |
string | Custom item name |
lore |
string[] | Lore lines |
custom_model_data |
int | Custom model data value |
unbreakable |
bool | Makes item unbreakable |
attackDamage |
number | Sets base attack damage (adds attribute modifier) |
Virtual inventories backed by SimpleInventory. Sizes must be multiples of 9 between 9 and 54.
local inv = mc.createInventory(27) -- 3 rows × 9 cols| Property | Type | Description |
|---|---|---|
size |
number | Total slot count |
| Method | Args | Description |
|---|---|---|
getItem |
slot (1-based) | Returns ItemStack or nil |
setItem |
slot, item | Sets item (nil clears) |
fill |
item | Fills all slots with item (nil clears) |
clear |
— | Empties all slots |
open |
player, [title="Container"] | Opens the inventory as a chest screen → Container |
Returned by inv:open(player, title). Represents an open chest screen for a player.
| Property | Type | Description |
|---|---|---|
player |
Player | The player viewing this container |
inventory |
Inventory | The backing inventory |
| Method | Args | Description |
|---|---|---|
close |
— | Closes the screen |
onClick |
callback or nil | Registers/unregisters click handler |
container:onClick(function(player, slot, clickType, slotItem, cursorItem)
-- slot: 1-based slot index
-- clickType: "pickup", "quick_move", "swap", "throw", "quick_craft", "pickup_all"
-- slotItem: item in the clicked slot (nil if empty)
-- cursorItem: item on the player's cursor (nil if empty)
return false -- cancel the click (prevents item manipulation)
end)When a callback is registered, the inventory auto-locks — players can't remove or swap items. Setting onClick(nil) unlocks it for shared-inventory use.
All containers are force-closed on /pxrp reload and player disconnect.
local chestgui = require "chestgui"Wraps mc.createInventory + Container with grid positioning.
Creates a GUI with rows rows (1–6). Returns a GUI object.
local gui = chestgui.create(3, "My Shop")| Method | Args | Description |
|---|---|---|
set |
row, col, item, [callback] | Place item at grid position |
decorate |
row, col, item | Place decorative item (no callback) |
button |
slot, item, [callback] | Place item by raw slot number |
fill |
item | Fill all slots |
clear |
— | Empty all slots |
open |
player | Opens GUI for a player → Container |
close |
player | Closes GUI for that player |
row and col are both 1-based. For a 3-row GUI: rows 1–3, cols 1–9.
local shop = chestgui.create(3, "Shop")
-- Grid positioning
shop:set(2, 5, mc.createItem("diamond"), function(player, slot, clickType, slotItem, cursorItem)
player:sendMessage("Bought diamond!")
return false
end)
-- Decorative border (no interaction)
shop:decorate(1, 1, mc.createItem("black_stained_glass_pane"))
-- Raw slot still works
shop:button(15, mc.createItem("emerald"), function(player)
player:sendMessage("Bought emerald!")
end)
shop:open(somePlayer)The World object is returned by player.world or mc.world(name).
| Property | Type | Settable | Description |
|---|---|---|---|
name |
string | ❌ | World path (e.g. "overworld") |
time |
number | ✅ | Game time (ticks). Set to specific tick values |
raining |
boolean | ✅ | Whether rain/snow is falling |
thundering |
boolean | ✅ | Whether a thunderstorm is active |
players |
table | ❌ | Array of Player wrappers currently in this world |
local w = player.world
w.time = w.time - (w.time % 24000) + 1000 -- set to day
w.raining = true
w.thundering = falseCreates and spawns an entity. pos accepts {x, y, z} or {x=..., y=..., z=...}. entityId auto-prefixes minecraft: if no namespace.
| Override | Type | Description |
|---|---|---|
custom_name |
string | Custom name tag |
health |
number | Health (for LivingEntity). If exceeding default max, maxHealth is raised to match |
local mob = w:spawn("zombie", {x = 100, y = 64, z = 200}, {
health = 40,
})Places a block. blockId defaults to minecraft: if omitted. Triggers full neighbor updates (redstone, water flow, observers).
player.world:setBlock({x = 0, y = 64, z = 0}, "diamond_block")Returns the registry ID of the block at the given position, e.g. "minecraft:stone".
local block = player.world:getBlock({x = 0, y = 4, z = 0})
if block == "minecraft:air" then
mc.broadcast("The floor was broken!")
endFills a cuboid region. No neighbor updates — blocks appear instantly without observer/redstone cascades. Volume capped at 32,768 blocks.
player.world:fill({x = -10, y = 4, z = -10}, {x = 10, y = 4, z = 10}, "glass")Spawns a particle visible to all players in that world. pos is a vector table ({x,y,z} or Vec). An optional opts table can specify spread options and particle-specific data.
| Option | Type | Default | Description |
|---|---|---|---|
count |
int | 1 |
Particle count |
spread |
{x,y,z} or Vec |
0,0,0 |
Spread vector |
speed |
number | 0 |
Particle speed |
data |
table | nil |
Particle type-specific data (see below) |
If the opts table contains particle data keys (color, block, item, fromColor, toColor, angle, or any camelCase‑to‑snake_case key), the whole table is treated as both opts and data — no need for a data wrapper.
-- Simple particle (no data needed)
world:particle("minecraft:gust", Vec(0, 64, 0))
world:particle("minecraft:flame", {x=0, y=64, z=0}, {count=3, spread=Vec(1,1,1)})
-- Implicit form — opts IS the data (cleaner, preferred)
world:particle("minecraft:flash", Vec(0, 64, 0), {color={1, 0, 0}})
world:particle("minecraft:block", Vec(0, 64, 0), {block="stone"})
world:particle("minecraft:item", Vec(0, 64, 0), {item="diamond"})
world:particle("minecraft:dust", Vec(0, 64, 0), {color={1,1,0}, size=1})
world:particle("minecraft:dust_color_transition", Vec(0, 64, 0),
{fromColor={1,0,0}, toColor={0,0,1}, size=1})
world:particle("minecraft:sculk_charge", Vec(0, 64, 0), {angle=0.5})
world:particle("minecraft:shriek", Vec(0, 64, 0), {delay=20})
-- Explicit data= form (also works, same result)
world:particle("minecraft:flash", Vec(0, 64, 0), {data={color={1,0,0}}})Sugar keys accepted in the data table (or directly in opts):
| Key | Maps to NBT | Example value |
|---|---|---|
block |
block_state (string) |
"stone" (auto-prefixes minecraft:) |
item |
item (compound {id, count}) |
"diamond" |
color |
color (packed int 0xRRGGBB) |
{1, 0, 0} or {r=1, g=0, b=0} |
fromColor |
from_color (packed int) |
{1, 0, 0} |
toColor |
to_color (packed int) |
{0, 0, 1} |
angle |
roll (float) |
0.5 |
Color values accept both 0–255 and 0.0–1.0 ranges. Any other key is converted from camelCase to snake_case automatically (e.g. myField → my_field).`
Plays a sound at the given coordinates in this world.
player.world:playSound("minecraft:entity.slime.squish", 0, 64, 0, 10, 0.1)Broadcasts text to players within range in that world.
player.world:broadcastInRange("Someone is nearby!", 0, 64, 0, 10)Raycasts from an arbitrary start position in a given direction. Same return format as entity:raycast.
| Param | Default | Description |
|---|---|---|
startVec |
— | Start position ({x, y, z} or Vec) |
dirVec |
— | Direction vector ({x, y, z} or Vec) |
range |
— | Max distance in blocks |
includeFluids |
false |
Whether fluid bodies are checked |
includeEntities |
true |
Whether entity hits are checked |
-- Check line of sight (blocks + entities)
local r = world:raycast(Vec(0, 10, 0), Vec(0, -1, 0), 20)
-- Blocks only
local r = world:raycast(start, dir, 20, false, false)
if r and r.type == "block" then
print("Hit", r.hit, "on", r.side, "face")
endReturns an array of EntityWrapper for entities within a radius. Optionally filters by entity type ID.
local mobs = w:getEntities({x = 0, y = 64, z = 0}, 10, "minecraft:zombie")
for i, e in ipairs(mobs) do
e:damage(10)
endReturned by world:spawn() and also backs ctx.player internally (player-only keys + delegation).
| Property | Type | Settable | Description |
|---|---|---|---|
uuid |
string | ❌ | UUID string |
type |
string | ❌ | Entity type ID (e.g. "minecraft:zombie") |
name |
string | ❌ | Entity name |
displayName |
string | ❌ | Display name |
customName |
string | ✅ | Custom name tag |
world |
World | ❌ | Current world |
pos |
{x, y, z} |
✅ | Position |
dir |
{x, y, z} |
❌ | Look direction |
bodyDir |
{x, z} |
❌ | Body yaw direction |
health |
number | ✅ | Current health |
maxHealth |
number | ✅ | Max health (via attribute) |
air |
number | ✅ | Air ticks |
maxAir |
number | ❌ | Max air ticks |
fallDistance |
number | ✅ | Fall distance |
fireTicks |
number | ✅ | Fire ticks |
glowing |
boolean | ✅ | Glowing effect |
invulnerable |
boolean | ✅ | Invulnerability |
isSneaking |
boolean | ✅ | Sneaking state |
isSprinting |
boolean | ✅ | Sprinting state |
removed |
boolean | ❌ | Entity removed from world |
All read-write, return nil if the entity doesn't support that slot (e.g. pig → mainhand returns nil). For players, writes sync the inventory screen. For non-player entities, uses entity equipment API (tracker handles sync).
| Property | Type |
|---|---|
mainhand |
ItemStack or nil |
offhand |
ItemStack or nil |
head |
ItemStack or nil |
chest |
ItemStack or nil |
legs |
ItemStack or nil |
feet |
ItemStack or nil |
All read-write, number values. Modifies the attribute instance's baseValue.
speed, armor, armorToughness, attackDamage, attackSpeed, knockbackResistance, luck, stepHeight, blockBreakSpeed, gravity, scale, safeFallDistance, flyingSpeed
Command tags are exposed via a proxy table — entity.tags["tagName"] = true adds a tag, entity.tags["tagName"] = false removes it. Iterate via pairs(entity.tags).
entity.tags["quest_mob"] = trueDamages the entity. If sourceEntity (a table/EntityWrapper with a uuid field) is provided, enables knockback via player/mob attack source.
entity:damage(10) -- generic damage, no knockback
entity:damage(10, ctx.player) -- player attack with knockbackRaycasts from the entity's eyes. Returns a result table with hit details, or nil if nothing hit.
Block hit:
local r = entity:raycast(50)
if r and r.type == "block" then
print(r.blockPos) -- Vec(10, 5, 7) block position
print(r.hit) -- Vec(10.5, 5.3, 7.0) exact hit point
print(r.side) -- "north", "south", "up", "down", "east", "west"
print(r.normal) -- Vec(0, 0, -1) unit normal of the face
endEntity hit:
local r = entity:raycast(50)
if r and r.type == "entity" then
print(r.entity.name) -- entity wrapper
print(r.hit) -- Vec(...) hit point on bounding box
r.entity:damage(10)
endAdds a status effect. All params after duration are optional.
entity:addEffect("minecraft:speed", 100, 2) -- speed III for 5 secondsRemoves a status effect by ID.
Checks if the entity has a specific effect.
Sets the entity on fire. Instantly syncs the fire visual to clients (unlike setting fireTicks directly).
entity:setOnFireFor(100) -- 5 seconds of fireentity:readNbt()→ table — Returns a full NBT snapshot of the entity as a Lua table (recursive, handles all 12 NBT types).entity:writeNbt(table)— Applies a complete NBT snapshot from a Lua table. Full-snapshot replacement — partial tables will reset unmentioned fields to defaults. Read → modify → write pattern is safe.
-- Inspect entity NBT
local nbt = pig:readNbt()
mc.dump(nbt)
-- Clone entity data to another entity
local nbt = pig:readNbt()
nbt["CustomName"] = "Cloned Pig"
otherPig:writeNbt(nbt)local pig = w:spawn("pig", {x = 100, y = 64, z = 200})
pig.tags["quest_mob"] = true
pig.speed = 0.5
pig.mainhand = mc.createItem("minecraft:stick", {name = "§eMagic Wand"})The Structure object is returned by mc.loadStructure() and mc.loadStructureFile().
| Property | Type | Description |
|---|---|---|
size |
{x, y, z} |
Structure dimensions in blocks |
Places the structure at the given position.
| Param | Type | Default | Description |
|---|---|---|---|
rotation |
string | "none" |
"none"/"0", "clockwise_90"/"90", "clockwise_180"/"180", "counterclockwise_90"/"270" |
mirror |
string | "none" |
"none", "left_right", "front_back" |
on_entity |
function | nil |
Per-entity callback when placing entities |
local s = mc.loadStructure("minecraft:igloo/igloo_top")
s:place(player.world, {x = 0, y = 64, z = 0}, {
rotation = "90",
mirror = "left_right",
})When on_entity is provided, structure entities are placed individually. The callback receives an EntityWrapper for each entity. Return false to skip spawning that entity.
local s = mc.loadStructure("minecraft:igloo/igloo_top")
s:place(player.world, {x = 0, y = 64, z = 0}, {
on_entity = function(e)
if e.type == "minecraft:villager" then
e.customName = "Custom Villager"
return true
end
return false -- skip all other entities
end,
})Positions are transformed (rotated/mirrored) to match the structure's placement. Entity UUIDs are regenerated automatically.
Loaded via require at the top of any script in config/pxrp/:
require "format" -- provides format() and broadcastFormat()
require "simple" -- provides registerSimple()- Templates use
{expr}placeholders with dot-notation access format(pattern)(args)returns formatted stringbroadcastFormat(pattern)(args)formats and broadcasts in one call
registerSimple(syntax, template, range?, overlay?)— one-shot command registration
gui.create(rows, title)— creates a chest GUIgui:set(row, col, item, callback)— place item with click handlergui:decorate(row, col, item)— place decorative itemgui:open(player)/gui:close(player)— open/close for a player
Templates use {expr} placeholders with dot-notation access:
format(pattern)(args) -- returns formatted string
broadcastFormat(pattern)(args) -- formats and broadcasts in one call
broadcastFormat "*{p.name} throws a fireball at {t.name}*" {p = ctx.player, t = target}registerSimple(cmd, args, template, range?, overlay?) creates a command that formats and broadcasts a template, passing p = ctx.player automatically:
| Param | Type | Description |
|---|---|---|
cmd |
string |
Command path |
args |
table |
Argument type list (same as register) |
template |
string |
Format template ({p.name}, {argName}, etc.) |
range |
number? |
If > 0, uses broadcastInRange with this radius |
overlay |
`boolean | number?` |
registerSimple("wave", {}, "*{p.name} waves at everyone*", 15) -- range 15 blocks
registerSimple("bow", {}, "*{p.name} bows*", nil, true) -- title overlay
registerSimple("cheer", {}, "*{p.name} cheers*", 20, 60) -- bothmc.on(event, handler) registers a handler that fires when a game event occurs. Handlers are cleared on /pxrp reload.
mc.on("player_join", function(player)
mc.broadcast("Welcome, " .. player.name .. "!")
end)
mc.on("player_leave", function(player)
mc.broadcast(player.name .. " left the server.")
end)
mc.on("player_death", function(player, damageType)
mc.broadcast(player.name .. " died to " .. damageType)
end)
mc.on("player_chat", function(player, message)
if message == "hello" then
mc.broadcast(player.name .. " said hello!")
return false -- cancel the player message
end
end)
mc.on("server_start", function()
mc.data.startTime = mc.time()
end)
mc.on("server_stop", function()
local uptime = mc.time() - (mc.data.startTime or mc.time())
mc.data.lastUptime = uptime
end)
mc.on("player_block_break", function(player, pos, block)
if block == "minecraft:bedrock" then
return false -- prevent breaking bedrock
end
end)
mc.on("player_block_place", function(player, pos, block)
if block == "minecraft:tnt" then
return false -- prevent placing TNT
end
end)| Event | Handler args | Fires | Cancellable |
|---|---|---|---|
server_start |
— | Server finishes starting (after Lua reload) | ❌ |
server_stop |
— | Server is stopping (before save) | ❌ |
player_join |
player |
Player joins the server | ✅ |
player_leave |
player |
Player disconnects | ❌ |
player_death |
player, damageType |
Player dies ("fall", "player_attack", etc.) |
❌ |
player_chat |
player, message |
Player sends a chat message | ✅ |
player_block_break |
player, pos, block |
Player is about to break a block | ✅ |
player_block_place |
player, pos, block |
Player is about to place a block | ✅ |
player_use_item |
player, hand, itemStack, itemId |
Player right-clicks with item | ✅ |
player_attack_entity |
player, entity |
Left-click on entity | ✅ |
player_interact_entity |
player, entity |
Right-click on entity | ✅ |
player_hurt |
player, damageType, amount |
Before player takes damage | ✅ |
entity_hurt |
entity, damageType, amount |
Before entity (non-player) takes damage | ✅ |
player_damage |
player, damageType, damageTaken, blocked |
After player takes damage | ❌ |
entity_damage |
entity, damageType, damageTaken, blocked |
After entity (non-player) takes damage | ❌ |
player_kill |
player, killedEntity, damageType |
Player kills another entity | ❌ |
Notes:
handis"main"or"off"itemStackis an ItemStack wrapper ornildamageTypeis the last segment after.(e.g."fall","player_attack","generic")blockedis a boolean —trueif a shield was used
For cancellable events (marked ✅ above), returning false cancels the action:
-- Kick banned players on join
mc.on("player_join", function(player)
local bans = mc.data.bans or {}
if bans[player.name] then
return false -- player is disconnected
end
end)
-- Block messages containing swear words
mc.on("player_chat", function(player, message)
local blocked = {"badword1", "badword2"}
for _, word in ipairs(blocked) do
if message:find(word) then
return false -- message is suppressed
end
end
end)player_join: fires before the player fully connects. Returningfalsedisconnects them.player_chat: fires before the message is broadcast. Returningfalseblocks the message.player_block_break: fires before the block is removed. Returningfalsecancels the break.player_block_place: fires before the block is placed. Returningfalsecancels the placement.player_use_item: fires before item use. Returningfalseprevents using the item.player_attack_entity: fires before attack. Returningfalsecancels the attack.player_interact_entity: fires before interaction. Returningfalsecancels the interaction.player_hurt/entity_hurt: fires before damage is applied. Returningfalsemakes the entity immune to that damage.- Other events (
player_leave,player_death,player_damage,entity_damage,player_kill,server_start,server_stop) are observational only — return values are ignored. - Disconnecting a rejected player during
player_jointriggersplayer_leaveas well. Scripts that broadcast on leave may show a ghost message for rejected players.
PxRP loads the following Lua standard libraries via luaj (targeting Lua 5.2):
| Library | Globals | Reference |
|---|---|---|
| Base | type, tostring, tonumber, pairs, ipairs, pcall, error, assert, select, unpack, _G, etc. |
§2–6 |
| math | math.random, math.randomseed, math.floor, math.ceil, math.sin, math.cos, math.sqrt, math.min, math.max, math.pi, math.huge |
§5.6 |
| string | string.format, string.sub, string.find, string.match, string.gmatch, string.gsub, string.len, string.byte, string.char, string.rep, string.lower, string.upper |
§5.4 |
| table | table.insert, table.remove, table.sort, table.concat, table.maxn |
§5.5 |
| bit32 | bit32.band, bit32.bor, bit32.bxor, bit32.lshift, bit32.rshift, bit32.arshift, bit32.bnot |
luaj docs |
| package | require, package.path, package.loaded, package.preload |
§5.3 |
The following standard libraries are not loaded: io, os, coroutine, debug.
See the complete Lua 5.2 Reference Manual for detailed documentation.
Data is stored as JSON in config/pxrp/storage/:
config/pxrp/storage/global.json— Global data (mc.data)config/pxrp/storage/players/<uuid>.json— Per-player data (ctx.player.data)
Data is persisted to disk on server stop, player disconnect, and /pxrp reload.
The storage backend is abstract (DataBackend interface). Currently ships with JsonBackend (atomic writes via temp file + atomic move). The interface allows adding other backends later without changing Lua code.
GNU Lesser General Public License v3.0. See LICENSE.txt.