Services
Bridges to the client's built-in services: smooth rotations, freelook, inventory, the vanilla interaction manager and movement prediction.
rotation
Server-side rotations with smoothing, priorities and automatic freelook (your camera stays put while the "server-side" facing rotates).
| Function | What it does |
|---|---|
rotation.set(yaw, pitch[, priority]) | asks for a rotation; returns true if it was accepted |
rotation.look_at(x, y, z[, priority]) | rotates to face a point (measured from the player's eyes) |
rotation.camera() | rotates towards where the camera looks (priority 100) |
rotation.active() | returns true while a managed rotation is running |
rotation.lock_item_use() | blocks new rotations until the end of the tick |
A request lasts one tick — to keep facing a direction, request it every tick (usually
from pre_interaction or tick). A request with a lower priority than the active one
is rejected.
module:event("pre_interaction", function()
local target = world.player("Notch")
if target then
rotation.look_at(target.x, target.y + target.height / 2, target.z, 10)
end
end)
freelook
| Function | What it does |
|---|---|
freelook.active() | returns true while freelook is on |
freelook.activate() / deactivate() | turns freelook on / off |
freelook.lock() / unlock() | freezes the camera (the mouse stops rotating it) |
freelook.locked() | returns true while the camera is frozen |
freelook.yaw() / pitch() | current camera angles |
inventory
Actions are scheduled to a safe point of the tick and play nicely with movement and packets.
| Function | What it does |
|---|---|
inventory.swap(a, b) | swaps the items in two slots |
inventory.drop(slot[, all]) | drops from a slot (all defaults to true — the whole stack) |
inventory.merge(from, to) | moves a stack onto another one |
inventory.use(slot[, yaw, pitch]) | uses the item in a slot, optionally facing a direction first |
inventory.item(slot) | returns the item table {id, count, name}, or nil |
inventory.find(item_id) | returns the first slot holding that item, or nil |
inventory.count(item_id) | returns how many of that item you carry in total |
inventory.slot() | returns the selected hotbar slot (0–8) |
inventory.set_slot(i) | selects a hotbar slot (0–8) |
Slot numbering
| Range | Meaning |
|---|---|
0 | crafting result |
1–4 | crafting grid |
5–8 | armor (helmet → boots) |
9–35 | main inventory |
36–44 | hotbar |
45 | offhand |
module:event("tick", function()
local totem = inventory.find("minecraft:totem_of_undying")
if totem and inventory.item(45).id ~= "minecraft:totem_of_undying" then
inventory.swap(totem, 45)
end
end)
interaction
The vanilla interaction manager: attacks, block/entity/item interactions, block breaking and screen-handler clicks. Everything goes through the vanilla client paths — correct packets, swing timers and sequence numbers. For raw packet control see Packets.
Hands are "main" / "off" (default "main"); sides are
"up" | "down" | "north" | "south" | "east" | "west" (default "up"). Interactions
return "success" | "fail" | "pass".
| Function | What it does |
|---|---|
interaction.gamemode() | current game mode, e.g. "survival" |
interaction.attack(entity_id) | attacks an entity (with a main-hand swing) |
interaction.interact(entity_id[, hand]) | right-clicks an entity |
interaction.use_item([hand]) | uses the held item |
interaction.attack_with_piercing_weapon([hand]) | spear stab/lunge attack; false when not holding one |
interaction.use_block(x, y, z[, side[, hand]]) | right-clicks a block (aims at its center) |
interaction.attack_block(x, y, z[, side]) | starts breaking a block (the first hit) |
interaction.update_breaking(x, y, z[, side]) | continues breaking; call every tick while "holding the button" |
interaction.cancel_breaking() | aborts the current breaking |
interaction.break_block(x, y, z) | finishes breaking instantly (creative / fully cracked) |
interaction.breaking() | true while a block is being broken |
interaction.breaking_progress() | breaking stage 0–10, -1 when idle |
interaction.stop_using() | releases bow / stops eating / lowers shield |
interaction.swing([hand]) | swings an arm (animation + packet) |
interaction.sync_id() | sync id of the open screen handler (0 = inventory) |
interaction.click_slot(sync_id, slot, button[, action]) | clicks a slot like a mouse click |
interaction.click_button(sync_id, button) | clicks a screen-handler button (stonecutter, loom, ...) |
interaction.pick_block(x, y, z[, include_data]) | pick-block |
interaction.pick_entity(entity_id[, include_data]) | pick-block on an entity |
interaction.close_screen() | closes the open screen (and tells the server) |
click_slot actions:
"pickup" (default), "quick_move" (shift-click), "swap" (button = hotbar slot
0–8, 40 = offhand), "clone", "throw", "quick_craft", "pickup_all".
module:event("pre_interaction", function()
for _, p in ipairs(world.players() or {}) do
if not p.is_self and p.distance < 3 then
interaction.attack(p.id)
break
end
end
end)
prediction
Vanilla-accurate movement physics simulation: "where will this player be in N ticks?". The local player is simulated from the real input; other players from input guessed from their observed motion. Simulations are cached per client tick and stepped lazily — asking for tick 5 and then tick 8 only simulates three more ticks. Max 1200 ticks.
| Function | What it does |
|---|---|
prediction.self(ticks) | your snapshot ticks ticks ahead (0 = now) |
prediction.self_path(from, to) | array of your snapshots for [from, to] |
prediction.self_with_input(input, ticks) | one-off simulation with a custom held input |
prediction.player(entity_id, ticks) | another player's snapshot |
prediction.player_path(entity_id, from, to) | array of another player's snapshots |
prediction.velocity(entity_id) | de-smeared per-tick velocity → vx, vy, vz |
A snapshot is a table: {x, y, z, vel_x, vel_y, vel_z, min_x, min_y, min_z, max_x, max_y, max_z, on_ground, fall_distance, horizontal_collision} (min_*/max_* are the
bounding box). input for self_with_input is a table of booleans:
{forward, backward, left, right, jump, sneak, sprint} — omitted keys are false.
Remote players' network positions are interpolated, so their raw per-tick delta is
unreliable — prediction.velocity returns the recovered true motion and is what the
simulation itself seeds from.
module:event("render_3d", function(render)
for _, p in ipairs(world.players() or {}) do
if not p.is_self and p.distance < 20 then
local s = prediction.player(p.id, 10)
if s then
render.box(s.min_x, s.min_y, s.min_z, s.max_x, s.max_y, s.max_z, 0x80FF4040, 2)
end
end
end
end)