Read the player and the world

Read the local player, the world, the inventory, the hotbar, the armor and the blocks around with xiao.game.

This guide shows how to read the local player, the world state, the items the player carries and the blocks around it with xiao.game.

Where you can read

The client takes one snapshot per frame: every call during that frame returns the same values. Items are read the first time a frame asks for them, then reused until the next frame. Nothing waits on the game: a value that cannot be read right now is nil or absent, so always test before use.

The local player New

xiao.game.player() returns the full player state, or nil outside a world.

Field Meaning
id, name runtime id and name of the local player
x, y, z, position position at eye height
pitch, yaw degrees, negative pitch looks up
velocity blocks per tick, 20 ticks per second
on_ground standing on a block
health, max_health health points
hunger, saturation, level, experience food, saturation, level, progress to the next level (0 to 1)
selected_slot selected hotbar slot, 1 to 9
sprinting, sneaking, swimming, gliding, crawling, flying, jumping New movement states, see State to read and force them
dimension, dimension_name 0, 1, 2 and "overworld", "nether", "the_end"

Every field except id, name, x, y, z and position is absent when the client could not read it this frame.

xiao.signal.add("think", "player_once", function()
    local me = xiao.game.player()
    if me == nil then return end
    print(("hp %.0f/%.0f  food %s  level %s"):format(
        me.health or 0, me.max_health or 0, tostring(me.hunger), tostring(me.level)))
    xiao.signal.remove("think", "player_once")
end)

xiao.game.local_player() still exists and returns only the position as a vector3.

The world New

xiao.game.world() never returns nil: outside a world in_world is false.

Field Meaning
in_world the local player is bound
session network session counter, changes on StartGame, ChangeDimension, disconnect and server change
dimension, dimension_name absent when unknown

Use the world_change signal to react to changes instead of comparing session every frame, see Events.

Items New

All these functions return nil when unreadable.

Function Result
hotbar() 9 slots, index 1 is the leftmost
inventory() 36 slots, slots 1 to 9 are the hotbar
armor() 4 slots: head, chest, legs, feet
offhand() the off hand slot
selected_slot() 1 to 9
held_item() the selected slot with an extra slot field, nil when the hand is empty

An item has name ("minecraft:diamond_sword", "" for an empty slot), count (0 for an empty slot), enchanted, and when relevant durability (0 to 1, items that wear). Armor slots also have damage and max_damage. The aux value of a stack is not read.

local function describe(item)
    if item.name == "" then return "empty" end
    local text = ("%s x%d"):format(item.name:gsub("^minecraft:", ""), item.count)
    if item.enchanted then text = text .. " (enchanted)" end
    if item.durability then text = text .. (" %.0f%%"):format(item.durability * 100) end
    return text
end

xiao.signal.add("think", "items_once", function()
    local hotbar = xiao.game.hotbar()
    local armor = xiao.game.armor()
    if hotbar == nil or armor == nil then return end

    for slot, item in ipairs(hotbar) do
        print("hotbar", slot, describe(item))
    end

    local names = { "head", "chest", "legs", "feet" }
    for index, piece in ipairs(armor) do
        if piece.max_damage then
            print(names[index], piece.name, piece.max_damage - piece.damage, "uses left")
        end
    end
    xiao.signal.remove("think", "items_once")
end)

Blocks New

xiao.game.block(x, y, z) returns a block (name, x, y, z) with coordinates rounded down. On failure it returns nil and a reason.

Reason Meaning
"unloaded" the chunk or the height is not loaded on the client
"busy" the block scanner holds the chunk data right now, try again next frame
"no_world" no local player
"budget" more than 1024 calls in this frame
"failed" the chunk changed while it was read
"unavailable" called outside a frame

The player position is at eye height (feet plus 1.62), so the block under the feet of a standing player is xiao.game.block(me.x, me.y - 2, me.z).

local function block_under(me)
    local block, reason = xiao.game.block(me.x, me.y - 2, me.z)
    if block == nil then return "? (" .. tostring(reason) .. ")" end
    return block.name
end

Entities around you

xiao.game.players() and xiao.game.entities() return the visible players and mobs, each an entity with id, name, x, y, z and position.

New fields New : type ("minecraft:zombie", present when the client saw the spawn packet), is_player, and distance to the local player when known. health and max_health are present when known.

local function nearest_mob(max_distance)
    local best
    for _, mob in ipairs(xiao.game.entities()) do
        if mob.distance and mob.distance < max_distance and (best == nil or mob.distance < best.distance) then
            best = mob
        end
    end
    return best
end

Full example: status panel

local paint = xiao.paint
local white = paint.color.rgb(255, 255, 255)
local shadow = paint.color.rgb(0, 0, 0)
local cursor = paint.point.vector2(20, 0)

local function short(name)
    return (name:gsub("^minecraft:", ""))
end

paint.add("status", function(width, height)
    local me = xiao.game.player()
    if me == nil then return end

    local under = xiao.game.block(me.x, me.y - 2, me.z)
    local held = xiao.game.held_item()
    local mob = nil
    for _, entity in ipairs(xiao.game.entities()) do
        if entity.distance and (mob == nil or entity.distance < mob.distance) then mob = entity end
    end

    local lines = {
        ("hp %.0f / %.0f  food %s  lvl %s"):format(me.health or 0, me.max_health or 0,
            tostring(me.hunger), tostring(me.level)),
        ("yaw %.0f  pitch %.0f  %s"):format(me.yaw or 0, me.pitch or 0, me.on_ground and "ground" or "air"),
        ("under %s  hand %s"):format(under and short(under.name) or "?", held and short(held.name) or "empty"),
        ("dimension %s"):format(xiao.game.world().dimension_name or "?"),
        mob and ("nearest mob %s %.1fm"):format(short(mob.type or "?"), mob.distance) or "no mob in view",
    }

    for index, line in ipairs(lines) do
        cursor.y = 200 + index * 16
        paint.text.left(cursor, line, white, shadow)
    end
end)

Pitfalls and limits

  • Only id, name, x, y, z, position (and is_player on entities) are always present: use or fallbacks or nil checks for the rest.

  • Positions of player() are at eye height, positions of other entities are the values the client has; test with xiao.game.world_to_screen when you draw.

  • Do not keep entity tables across frames expecting them to update: read again each frame.

  • A packet hook cannot read xiao.game: copy what it needs into an upvalue from think, see Modify packets.

xiao.game reads the game. The settings of the client itself (ESP, aim, HUD positions...) are read and changed by key with xiao.config, which works everywhere except inside a packet hook or a cmd hook.

Related pages: xiao.game, xiao.config, Draw an overlay, React to events.