understudy

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package understudy drives a Minecraft server connection far enough to be a real player: it completes the handshake, login and configuration states, enters play, and then keeps the connection healthy.

An understudy stands in for the lead. This is a headless client that takes a player's place on a server — it joins, walks, mines, places, crafts, fights and eats over the real wire protocol, so a server cannot tell it apart from somebody at a keyboard. That is the point: anything that measures players — statistics, advancements, anti-cheat, progression systems — sees a player.

Everything here is vanilla protocol behaviour and nothing above it: there is no scenario language, no assertions, no opinion about why you are driving a player. That layer belongs to whatever is using this.

Getting started

bot, err := understudy.New(understudy.Options{
	Addr:     "127.0.0.1:25565",
	Username: "Understudy",
})
if err != nil {
	return err
}
if err := bot.Connect(ctx); err != nil {
	return err
}
defer bot.Close()

go bot.Run(ctx)   // pump packets; everything below needs this running

if _, err := bot.Fall(ctx); err != nil {
	return err
}
return bot.DigBlock(ctx, 10, 63, -5, protocol.FaceTop, 400*time.Millisecond)

Connect and Run are deliberately separate: it lets a caller assert on having joined before anything starts pumping, which keeps "did the bot get in?" answerable apart from "what happened after it got in?".

The server must run with online-mode=false. This client implements no encryption and no Mojang authentication, and says so explicitly rather than hanging if a server asks for it.

What it decodes

Only what it needs to act and to know where it is: position, health, death, chunks, entities and inventory. Frames are length-prefixed, so everything else is skipped by its prefix — 26.1 has 141 clientbound play packets and this handles a couple of dozen.

Silence is the failure mode

A Minecraft server ignores a great deal without complaining. An out-of-reach dig, a placement into an occupied space, any action from a dead player — no rejection, no reply, nothing distinguishable from success.

So this client checks what it can itself and turns a silent miss into a real error, and confirms state changes by observing them rather than by assuming the packet worked. Where a check is impossible, the doc comment says what the symptom looks like instead.

Where things are

One package, because Client's methods cannot be split across directories — but the files are named for what they hold, and pure computation that needed none of this lives in its own packages under internal/.

client.go      lifecycle: New, Connect, Close, and the play-state dispatch
login.go       handshake -> login -> configuration
state.go       every mutex-guarded accessor, in one place
heartbeat.go   idle position reporting, ~20/s, like a real client
settle.go      the post-teleport gate on block interactions
detect.go      server-list ping and version auto-detection
versions.go    the blank import that registers the generated protocol tables

world.go       terrain: chunk storage, ground scans, block queries
entities.go    entity tracking, targeting, attacking, interacting
inventory.go   slots, item lookup, container clicks, pickups
craft.go       the player's own 2x2 grid
container.go   block and entity UIs: opening, clicking, trading
trades.go      merchant offers: decoding, availability, trading by item
recipes.go     the server's recipe book, so crafting can name what it wants
windows.go     window types and slot layouts, read off a live server
slots.go       moving items in and out of an open window
workstation.go furnaces, anvils, looms, grindstones, smithing, brewing
storage.go     chests of every kind, barrels, shulkers, hoppers, minecarts

look.go        aiming, by direction / point / block / entity / player
move.go        position packets and walking
fall.go        gravity-driven descent, water entry, auto-fall
dig.go         breaking blocks, and observing that they broke
place.go       placing blocks, and confirming they appeared
reach.go       the reach and liveness checks a server enforces silently
raytrace.go    what the crosshair is actually on
bow.go         drawing and loosing
verbs.go       input bits, sneaking, equipping, eating
geometry.go    re-exports of internal/geom, so callers need not import it

The state those files operate on lives in its own packages, because a mutex-guarded store that never needs a Client is testable without one: internal/world (chunks and block states), internal/entities (the tracker) and internal/inventory (slots and stacks). Client keeps thin delegating methods, and Entity and ItemStack are aliases, so no caller imports an internal package to name a type.

Concurrency

A Client is safe for concurrent use once Connect returns. Exactly one goroutine may call Run; every other method may be called from any goroutine, which is what lets a control API drive the bot while the read loop pumps.

Index

Constants

View Source
const (
	// DefaultDialTimeout bounds the TCP connect.
	DefaultDialTimeout = 10 * time.Second
	// DefaultReadTimeout bounds any single read once connected. It must
	// comfortably exceed the server's keep-alive interval (vanilla: 15s) or a
	// healthy idle connection is torn down as if it had stalled.
	DefaultReadTimeout = 60 * time.Second
)

Default timeouts, applied by New when Options leaves them zero.

View Source
const (
	// CraftingResultSlot is the output of a crafting table. Slots 1..9 are the
	// 3x3 grid, row-major from the top-left.
	CraftingResultSlot = 0
	// CraftingGridSlot is the first of the nine grid slots.
	CraftingGridSlot = 1
	// CraftingGridSize is how many slots the 3x3 grid covers.
	CraftingGridSize = 9

	// SmithingTemplateSlot, SmithingBaseSlot and SmithingAdditionSlot are the
	// three inputs of a smithing table; SmithingResultSlot is its output.
	SmithingTemplateSlot = 0
	SmithingBaseSlot     = 1
	SmithingAdditionSlot = 2
	SmithingResultSlot   = 3

	// StonecutterInputSlot and StonecutterResultSlot are a stonecutter's two
	// slots. The recipe is chosen with ClickContainerButton, not by clicking.
	StonecutterInputSlot  = 0
	StonecutterResultSlot = 1

	// MerchantResultSlot is where a villager's selected trade delivers.
	MerchantInputSlot1 = 0
	MerchantInputSlot2 = 1
	MerchantResultSlot = 2
)

Container slot layouts.

Slot numbering is per window type and there is no way to derive it from the packets — the server sends a type ID and a flat array. Getting it wrong is silent: a click lands on a different slot and the recipe simply does not craft, with no rejection anywhere. So the layouts a caller is likely to want are named here rather than left as literals at each call site.

View Source
const (
	SlotCraftGrid2x2Start = 1 // slots 1..4
	SlotCraftGrid2x2End   = 4
)

The player's own 2x2 crafting grid lives in window 0, so using it needs no container to be opened and no window bookkeeping — which makes it the cheapest way to craft anything with a 2x2 recipe.

View Source
const (
	// EyeHeight is how far a standing player's eyes sit above their feet.
	// Every interaction the server range-checks is measured from here.
	EyeHeight = geom.EyeHeight

	// ArrowEyeHeight is where a bow releases its arrow — slightly below the
	// eyes, which matters over distance.
	ArrowEyeHeight = geom.ArrowEyeHeight

	// MobAimHeight is a rough mid-body offset for aiming at a mob rather than
	// at the ground it stands on.
	MobAimHeight = geom.MobAimHeight

	// BlockCentreOffset moves a coordinate from a block's corner to its centre.
	BlockCentreOffset = geom.BlockCentreOffset
)

Player and world geometry, re-exported from internal/geom so callers of this package do not have to reach into an internal one for a constant.

View Source
const (
	SlotCraftOutput = inventory.SlotCraftOutput
	SlotCraftGridA  = inventory.SlotCraftGridA
	SlotArmorHead   = inventory.SlotArmorHead
	SlotMainStart   = inventory.SlotMainStart
	SlotMainEnd     = inventory.SlotMainEnd
	SlotHotbarStart = inventory.SlotHotbarStart
	SlotHotbarEnd   = inventory.SlotHotbarEnd
	SlotOffhand     = inventory.SlotOffhand
	PlayerWindowID  = inventory.PlayerWindowID

	// StorageSlots is a player's carrying capacity: the 27 main slots plus the
	// 9 hotbar slots, excluding armour and the offhand.
	StorageSlots = inventory.StorageSlots
)

Player inventory slot layout for window 0, re-exported so callers never reach into an internal package for a constant.

View Source
const (
	ClickModeNormal     int32 = 0
	ClickModeQuickMove  int32 = 1 // shift-click
	ClickModeHotbarSwap int32 = 2
	ClickModeDrop       int32 = 4
)

Container click modes. Mode 2 is the useful one here: it swaps a slot directly with a hotbar slot, which moves an item into the hand without ever picking it up onto the cursor.

View Source
const (
	// Furnace, blast furnace and smoker share a layout.
	FurnaceInputSlot  = 0
	FurnaceFuelSlot   = 1
	FurnaceResultSlot = 2

	// Anvil: two inputs and the result. Renaming needs only the first.
	AnvilFirstSlot  = 0
	AnvilSecondSlot = 1
	AnvilResultSlot = 2

	// Loom: the banner, the dye, an optional banner-pattern item, and the
	// result. The pattern itself is chosen with ClickContainerButton.
	LoomBannerSlot  = 0
	LoomDyeSlot     = 1
	LoomPatternSlot = 2
	LoomResultSlot  = 3

	// Grindstone: two inputs, one result. Disenchanting uses the first only.
	GrindstoneFirstSlot  = 0
	GrindstoneSecondSlot = 1
	GrindstoneResultSlot = 2

	// Cartography: a map and the paper/glass/compass applied to it.
	CartographyMapSlot    = 0
	CartographyPaperSlot  = 1
	CartographyResultSlot = 2

	// Enchanting: the item and the lapis. The level is a container button.
	EnchantItemSlot  = 0
	EnchantLapisSlot = 1

	// Brewing: three bottle slots, the ingredient above them, and the fuel.
	BrewBottleSlot1    = 0
	BrewBottleSlot2    = 1
	BrewBottleSlot3    = 2
	BrewIngredientSlot = 3
	BrewFuelSlot       = 4

	// Beacon has a single payment slot.
	BeaconPaymentSlot = 0
)

Slot layouts, by window.

Slot numbering restarts per window type and cannot be derived from the packets — the server sends a flat array. Getting one wrong is silent: the click lands somewhere else and the operation simply does not happen.

View Source
const AttackCooldown = 600 * time.Millisecond

AttackCooldown is how long a sword takes to recharge. Attacking faster lands uncharged hits that do a fraction of the damage, so a fight never resolves.

View Source
const AttackReach = 3.0

AttackReach is how far a player can hit an entity, in blocks.

The server enforces this and simply *ignores* an attack on anything further away — no error, no feedback. A bot swinging at a target that has wandered two blocks too far looks identical to one landing every hit, so this is checked client-side to turn a silent miss into a real error.

View Source
const BlockReach = 4.5

BlockReach is how far a survival player can reach a block, in blocks.

This is the `block_interaction_range` attribute, and it is measured from the eyes to the nearest point of the block's box — not to its centre — so a bot can legitimately work a little further than centre-distance suggests. Creative raises it to 5.0; this client assumes survival.

View Source
const BowFullDraw = ballistics.FullDraw

BowFullDraw is how long the bow must be held for maximum power.

View Source
const ConsumeDuration = 32 * TickRate

ConsumeDuration is how long a normal eat or drink takes: 32 ticks.

View Source
const MaxFallBlocks = 512

MaxFallBlocks bounds a self-detecting fall. Without a floor beneath it a bot would otherwise descend into the void forever, so an unbounded search is reported as an error instead of a very slow death.

View Source
const PlayerWindowSlots = 36

PlayerWindowSlots is how many of the player's own slots the server appends to every container window: 27 storage plus the 9 hotbar.

Armour and the offhand are *not* included, which is why a container window is 36 larger than its own contents rather than 41.

View Source
const SprintSpeed = WalkSpeed * 1.3

SprintSpeed is a player's sprinting speed, which vanilla derives as walking times 1.3.

View Source
const TeleportSettle = 350 * time.Millisecond

TeleportSettle bounds how long a block interaction will wait for a teleport to finish settling.

A teleport is not finished when the client agrees it moved. The server keeps the player in an "awaiting position from client" state and, while that lasts, silently ignores use_item, use_item_on and player_action — the three packets behind eating, placing and digging. There is no rejection and no feedback: the action simply does not happen.

Mining never showed the problem because awaitBreak keeps swinging and re-sends the finish packet, so it retries through the window by accident. A one-shot place has no such luck and just vanishes.

Reported measurement, taken on fresh sessions against a client that sent no idle position packets at all: 2/4 placements succeeded with no pause, 4/4 with a 300ms pause, and 4/4 if any real block interaction had gone first. A bare arm swing did *not* help, which is what ruled out "the session is not warm yet" and pointed at the teleport.

This is a ceiling, not a cost

The name of the state says what clears it: the server wants a position from the client. So awaitTeleportSettle sends one, rather than sleeping until the heartbeat happens to come round, and then waits a single tick for the server to act on it. The usual cost is therefore one TickRate, and this constant only bounds the fallback path where the position could not be sent.

It used to sleep the whole window unconditionally, which was measured at 346ms mean across 11 waits — and mineField repositions about eleven times per field, through tp directly rather than through the driver's stand(), so nothing else was absorbing it. That came to 3.8s per field of pure sleeping, and it is the bulk of a 98s -> 154s regression across a full run.

A different failure with the same symptom

Worth ruling out first, because it is far easier to hit: a block action issued before the client has *processed* the teleport is rejected for being out of reach, because the client is still measuring from where it used to be. On a fresh session the read loop is busy absorbing a flood of chunk batches, so that window is hundreds of milliseconds. That one is loud — it returns an error naming the distance. This one is silent.

View Source
const TickRate = 50 * time.Millisecond

TickRate is the server tick, and the natural cadence for movement updates. A real client sends position roughly once per tick; sending far faster is wasted, and sending far slower makes movement look like teleporting and risks a "moved too quickly" rejection.

View Source
const WalkSpeed = 4.317

WalkSpeed is a player's normal walking speed in blocks per second.

Variables

View Source
var ErrNoContainer = errors.New("understudy: no container window is open")

ErrNoContainer reports that an operation needed an open container window and there was not one.

A sentinel because the alternative is worse than an error: a click with no window open is addressed to the player's own inventory, which the server accepts and applies somewhere unintended.

View Source
var ErrNoSuchEntity = errors.New("understudy: no tracked entity")

ErrNoSuchEntity reports that nothing of the requested type is being tracked.

It is a sentinel because "there is none" and "there is one but you cannot reach it" are different answers that callers act on differently — killing the last of something is a success, being out of range is not. See AttackTimes, which relies on telling them apart.

View Source
var ErrNotConnected = errors.New("understudy: not connected")

ErrNotConnected reports that something tried to put a packet on a wire that is not there. Only reachable before Connect or after Close; it exists so those paths return an error rather than panicking on a nil connection.

Functions

func BlockOffsetByFace

func BlockOffsetByFace(x, y, z, face int32) [3]int32

BlockOffsetByFace returns the coordinate a block placed against a face occupies — the neighbour, not the clicked block itself.

func BowPower

func BowPower(draw time.Duration) float64

BowPower converts a draw duration into vanilla's launch power, 0..1.

The curve is not linear: half a second of draw gives roughly 0.4 power, not 0.5, so a caller tuning for range needs the real curve rather than the intuition. See internal/ballistics for the arithmetic.

func DetectVersion

func DetectVersion(addr, host string, port uint16, timeout time.Duration) (*protocol.Version, error)

DetectVersion pings a server and resolves the matching protocol table.

It matches on the protocol number, not the version name: names are free text that proxies and forks rewrite ("Paper 1.21", "Velocity"), whereas the protocol number is what actually has to agree on the wire.

func DirectionNames

func DirectionNames() []string

DirectionNames lists the accepted direction names, sorted, for error messages and help text.

func LookDirection

func LookDirection(yaw, pitch float32) (dx, dy, dz float64)

LookDirection converts a yaw/pitch in degrees to a unit direction vector, in Minecraft's convention: yaw 0 faces +Z, and a negative pitch looks up.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a single bot connection. See the package comment for what may be called concurrently.

func New

func New(opts Options) (*Client, error)

New builds a Client. It does not perform any I/O.

func (*Client) ActivateBeacon

func (c *Client) ActivateBeacon(ctx context.Context, payment string, primary, secondary int32) error

ActivateBeacon pays a beacon and selects the effect it projects.

The payment goes in the beacon's single slot; the effect is a separate packet rather than a container button, which is why this needs its own verb where a loom or a stonecutter does not.

primary is a status-effect id. secondary is only accepted on a full five-layer pyramid and is ignored otherwise; pass 0 for none. A beacon whose pyramid is too small, or which cannot see the sky, takes the payment and projects nothing — silently, as ever.

func (*Client) AimBow

func (c *Client) AimBow(x, y, z float64, draw time.Duration) error

AimBow points the bot so an arrow at the given draw strength will land on a world coordinate, accounting for gravity and drag.

func (*Client) ApplyBannerPattern

func (c *Client) ApplyBannerPattern(ctx context.Context, banner, dye, patternItem string, pattern int32) (ItemStack, error)

ApplyBannerPattern dyes a pattern onto a banner at a loom.

pattern selects from the loom's list by index, the same numbering the buttons use. An optional patternItem is a banner-pattern item for the designs that need one; pass "" for the built-in patterns.

func (*Client) ApplyToMap

func (c *Client) ApplyToMap(ctx context.Context, mapItem, applied string) (ItemStack, error)

ApplyToMap runs a map through a cartography table — paper to expand it, glass to lock it, an empty map to copy it — and takes the result.

A plain three-slot container, so this is only naming which slot is which.

func (*Client) Attack

func (c *Client) Attack(entityID int32) error

Attack hits an entity once, at full charge.

Discrete attacks matter: holding the button down fires every tick, and every hit after the first is an uncharged swing doing a fraction of the damage. Space repeated calls by the weapon's cooldown (~600ms for a sword) to land full-power hits.

func (*Client) AttackNearest

func (c *Client) AttackNearest(typeName string) (Entity, error)

AttackNearest finds the closest entity of a type, faces it, and hits it once.

It fails rather than swinging if the nearest candidate is out of reach: "nearest" is not the same as "reachable", and mobs wander.

func (*Client) AttackTimes

func (c *Client) AttackTimes(ctx context.Context, typeName string, times int) (Entity, int, error)

AttackTimes hits the nearest entity of a type repeatedly, pausing for the weapon cooldown between swings.

The target is re-selected every swing: the previous one may have died, and hitting a corpse's stale ID does nothing.

It returns the number of hits that actually landed, which can be fewer than asked for. Running out of targets is not an error once something has been hit — a diamond pickaxe one-shots a chicken, so "attack it three times" legitimately lands one hit and finds nothing left to swing at. Reporting that as a failure blames the caller for succeeding. Finding nothing on the *first* swing is still an error, because then nothing was attacked at all.

func (*Client) AwaitSlot

func (c *Client) AwaitSlot(ctx context.Context, slot int, timeout time.Duration) (ItemStack, error)

AwaitSlot waits for a slot to hold something, and returns it.

Workstations produce their output a tick or many seconds later — a furnace takes ten seconds a piece — so this polls rather than sleeping a guess. A timeout means the operation did not happen, which for most workstations is silent otherwise: an invalid combination simply leaves the result slot empty.

func (*Client) BlockAt

func (c *Client) BlockAt(x, y, z int32) int32

BlockAt returns the block state at world coordinates.

func (*Client) BlockDistance

func (c *Client) BlockDistance(x, y, z int32) float64

BlockDistance returns the distance from the bot's eyes to the nearest point of a block, which is the measure the server actually enforces.

func (*Client) Brew

func (c *Client) Brew(ctx context.Context, bottle, ingredient, fuel string, count int) error

Brew loads a brewing stand and waits for the cycle to finish.

bottles go into the three lower slots, the ingredient above them, and blaze powder fuels it. The result replaces the bottles in place, so this waits for the first bottle slot to *change* rather than for a separate output.

func (*Client) CanReachBlock

func (c *Client) CanReachBlock(x, y, z int32) bool

CanReachBlock reports whether a block is within interaction range.

func (*Client) ChunkLoaded

func (c *Client) ChunkLoaded(x, z int32) bool

ChunkLoaded reports whether terrain covering a coordinate is known.

func (*Client) ClearContainerInputs

func (c *Client) ClearContainerInputs(ctx context.Context) error

ClearContainerInputs empties every slot the container owns, returning the contents to the player.

Worth doing between operations at a shared workstation: an ingredient left in a brewing stand or a loom changes what the next attempt makes, and the server reports that as a perfectly ordinary result rather than as a mistake.

func (*Client) ClearCraftingGrid

func (c *Client) ClearCraftingGrid(ctx context.Context) error

ClearCraftingGrid returns anything left in the 2x2 grid to the inventory.

Worth calling between crafts: leftovers in the grid change what the next recipe resolves to, and they are dropped on the floor when the inventory closes — where the bot may then walk over and collect them, changing an inventory something else is measuring.

func (*Client) ClickContainerButton

func (c *Client) ClickContainerButton(button int32) error

ClickContainerButton presses a numbered button in the open window.

This is how a stonecutter or loom selects a recipe, and how an enchanting table picks a level — despite the packet being named enchant_item, it is the generic container-button message.

func (*Client) ClickContainerSlot

func (c *Client) ClickContainerSlot(slot int, button int8, mode int32) error

ClickContainerSlot clicks a slot in the open window.

button and mode are the raw protocol values — see the ClickMode constants in inventory.go. The common cases have their own verbs below.

func (*Client) Close

func (c *Client) Close() error

Close tears down the connection and waits for the client's own goroutines to finish, so a caller that returns straight after does not leave an auto-fall writing into a closed socket.

func (*Client) CloseContainer

func (c *Client) CloseContainer() error

CloseContainer shuts the open window.

Worth doing rather than walking away: a crafting grid left dirty drops its contents on the floor when it eventually closes, and those drops then change any nearby pickup or hold count.

func (*Client) CombineInAnvil

func (c *Client) CombineInAnvil(ctx context.Context, first, second string) (ItemStack, error)

CombineInAnvil puts two items in an anvil — repairing, or applying a book — and takes the result.

func (*Client) Connect

func (c *Client) Connect(ctx context.Context) (err error)

Connect dials the server and drives handshake -> login -> configuration, returning once the client has entered the play state.

It does not read play-state packets; call Run for that. Splitting the two lets a caller connect, assert on having joined, and only then start pumping — which keeps "did the bot get in?" separable from "what happened after it got in?".

On failure the connection is closed before returning, so a caller that gives up after a failed Connect leaks nothing.

func (*Client) Consume

func (c *Client) Consume(ctx context.Context) error

Consume eats or drinks the held item.

This is a *held* action, not an instant one. Sending use_item alone starts the animation and nothing else; the server only applies the effect once the item has been held for its full use time and the use is then released. A client that skips the wait appears to eat and never actually does.

func (*Client) ConsumeItem

func (c *Client) ConsumeItem(ctx context.Context, name string) (ItemStack, error)

ConsumeItem holds a named item and consumes it.

func (*Client) ContainerContents

func (c *Client) ContainerContents() []ItemStack

ContainerContents lists what the container itself holds, excluding the player's rows — which is what "what is in this chest" means.

func (*Client) ContainerID

func (c *Client) ContainerID() int32

ContainerID returns the server-assigned window ID, or inventory.NoWindow.

func (*Client) ContainerKind

func (c *Client) ContainerKind() int32

ContainerKind returns the window type ID the server reported.

func (*Client) ContainerOpen

func (c *Client) ContainerOpen() bool

ContainerOpen reports whether a block or entity UI is currently open.

func (*Client) ContainerOwnSlots

func (c *Client) ContainerOwnSlots() int

ContainerOwnSlots returns how many slots belong to the container itself, excluding the player's inventory the server appends.

This is the number to trust rather than any constant: it is 27 for a single chest and 54 for a double, 5 for a hopper, 27 for a chest minecart. Derived from the window, so every variant works without a case for it.

func (*Client) ContainerSize

func (c *Client) ContainerSize() int

ContainerSize returns how many slots the open window covers.

func (*Client) ContainerSlot

func (c *Client) ContainerSlot(slot int) (ItemStack, bool)

ContainerSlot returns one slot of the open window.

func (*Client) ContainerSlots

func (c *Client) ContainerSlots() []ItemStack

ContainerSlots returns the open window's contents.

The array covers the container's own slots *and* the player's inventory appended after them, which is how the server addresses a click — so slot 9 of a crafting table window is the player's first storage slot, not a tenth grid cell.

func (*Client) ContainerTitle

func (c *Client) ContainerTitle() string

ContainerTitle returns the window's title as plain text.

func (*Client) ContainerTruncated

func (c *Client) ContainerTruncated() bool

ContainerTruncated reports whether the window's contents are incomplete because an item carried data components that could not be skipped.

func (*Client) ContainerType

func (c *Client) ContainerType() WindowType

ContainerType returns the open window's type.

func (*Client) Corrections

func (c *Client) Corrections() int

Corrections returns how many position corrections the server has issued. A rise in this value is how the client learns it has hit something.

func (*Client) CountInContainer

func (c *Client) CountInContainer(name string) int32

CountInContainer totals a named item across the open window.

func (*Client) CountInContainerOnly

func (c *Client) CountInContainerOnly(name string) int32

CountInContainerOnly totals a named item in the container's own slots, ignoring the player's inventory the window also covers.

The distinction matters: CountInContainer sees both, so a bot holding twenty diamonds while looking into an empty chest would read as twenty.

func (*Client) CountInPlayerRows

func (c *Client) CountInPlayerRows(name string) int32

CountInPlayerRows totals a named item in the player's own rows of the open window.

The third member of a set that needs all three. While a window is open the player's items live in *that window's* slots, and the separate inventory view is a different thing — so checking the player's side of a transfer through the wrong one reads as "nothing moved" when it plainly did.

CountInContainer      both sides of the window
CountInContainerOnly  the container's own slots
CountInPlayerRows     the player's rows

func (*Client) CountItem

func (c *Client) CountItem(name string) int32

CountItem totals an item across every slot the bot knows about, including the offhand and armour.

func (*Client) CountItemStorage

func (c *Client) CountItemStorage(name string) int32

CountItemStorage totals an item across the 36 *storage* slots only. See inventory.CountStorage for why both numbers are worth having.

func (*Client) CraftIn2x2

func (c *Client) CraftIn2x2(ctx context.Context, layout map[int]string) (ItemStack, error)

CraftIn2x2 crafts using the player's own 2x2 grid.

layout maps a grid slot (1..4) to the item that belongs there, so a caller expresses a recipe positionally rather than this package carrying a recipe table. Shapeless recipes simply use whichever slots are convenient.

The crafted stack is pulled out with a quick-move, which is what makes the crafted/* statistic tick — taking the result is the act that counts, not assembling the ingredients.

func (*Client) CraftInGrid

func (c *Client) CraftInGrid(ctx context.Context, layout map[int]string, repeat int) (ItemStack, error)

CraftInGrid lays a recipe out in the open crafting table and takes the result.

layout maps grid slot to item name, with CraftingGridSlot..CraftingGridSlot+8 as the 3x3 read row-major from the top-left — so a banner is six wool in slots 1..6 and a stick in slot 7.

This is CraftIn2x2's approach applied to a real table, and it is deliberately preferred over CraftRecipe for anything a caller writes by hand: a recipe request needs a numeric recipe ID from the server's registry, which nothing here decodes, whereas a layout is something a person can write down and read back. CraftRecipe remains the better option when the ID is known, because the server then repeats the craft for as long as the ingredients last.

repeat crafts the recipe more than once, re-laying the grid each time — "craft fifty banners" without needing a recipe ID.

func (*Client) CraftRecipe

func (c *Client) CraftRecipe(recipeID int32, all bool) error

CraftRecipe asks the server to lay out a recipe in the open crafting window.

The server populates the grid from the player's inventory using its own recipe book, so the caller does not have to encode recipes or place ingredients slot by slot — which is both fewer packets and one less thing to get wrong per recipe. With all true it repeats until the ingredients run out, which is what "craft fifty of these" wants.

The result still has to be collected: see TakeFromContainer with CraftingResultSlot.

func (*Client) CraftRecipeFor

func (c *Client) CraftRecipeFor(ctx context.Context, name string, all bool) error

CraftRecipeFor asks the server to craft a named item in the open window.

This is the cheap path: the server lays the grid out from its own recipe book, so a caller neither encodes the recipe nor places ingredients slot by slot, and `all` repeats until the ingredients run out. CraftInGrid does the same job by clicking, which works without the recipe book but costs about twenty clicks a craft.

The result still has to be collected — see TakeFromContainer with CraftingResultSlot.

func (*Client) Dead

func (c *Client) Dead() bool

Dead reports whether the bot is currently on the death screen. A dead bot silently ignores actions, so anything driving it should check this rather than trusting that a healthy connection means a usable player.

func (*Client) Deaths

func (c *Client) Deaths() int

Deaths returns how many times the bot has died this session.

func (*Client) Deposit

func (c *Client) Deposit(ctx context.Context, name string, count int32) (moved int32, err error)

Deposit moves a named item from the player's inventory into the container.

count is how many to move; 0 or less moves everything. The number actually moved is returned, which can be short when the container fills up — the server accepts the click and silently keeps the remainder.

func (*Client) DepositAll

func (c *Client) DepositAll(ctx context.Context) (stacks int, err error)

DepositAll empties the player's storage rows into the container, and reports how many stacks moved.

Useful for setting a scenario up: fill a chest, or clear a bot before measuring what it picks up.

func (*Client) DigBlock

func (c *Client) DigBlock(ctx context.Context, x, y, z, face int32, hold time.Duration) error

DigBlock aims at a block and breaks it, holding for the given duration.

The duration has to cover the server's expected break time for that block with the currently held tool. Too short and the server rejects the finish and leaves the block standing, silently. Callers that care about speed should measure per block type rather than guess a single global value.

func (*Client) DigBlocks

func (c *Client) DigBlocks(ctx context.Context, blocks [][3]int32, face int32, hold time.Duration) (dug int, err error)

DigBlocks breaks several blocks from where the bot is standing.

The point is not to move: everything inside BlockReach can be worked from a single position, so a field is cleared in one pass instead of a teleport per block. Anything out of range is reported rather than swung at, and the remaining blocks are still attempted — one unreachable corner should not abandon the rest of the field.

func (*Client) DigLookingAt

func (c *Client) DigLookingAt(ctx context.Context, hold time.Duration) (RayHit, error)

DigLookingAt breaks whatever the crosshair is on.

This is the game's own model — aim, then mine what you are pointing at — and it is the safer primitive: the face comes from the ray rather than being guessed, and there is no way to name a block the bot cannot actually hit.

func (*Client) Disenchant

func (c *Client) Disenchant(ctx context.Context, item string) (ItemStack, error)

Disenchant strips enchantments off an item at a grindstone, returning the cleaned item. Also how a grindstone repairs two of the same tool.

func (*Client) DistanceTo

func (c *Client) DistanceTo(e Entity) float64

DistanceTo returns how far the bot is from an entity, in blocks.

func (*Client) DrawBow

func (c *Client) DrawBow(ctx context.Context, draw time.Duration) error

DrawBow holds the bow for a duration and looses the arrow.

Like eating, this is a held action: the use has to be started, held, and then explicitly released. Sending only the start leaves the bot standing there at full draw forever, having fired nothing.

func (*Client) DropHeld

func (c *Client) DropHeld(ctx context.Context, all bool) error

DropHeld drops the held item. With all true it throws the whole stack, otherwise a single item.

Dropping rides on the block_dig packet with a status that means "drop" rather than "break" — the position and face are ignored, which is why they are sent as zeroes.

func (*Client) Effect

func (c *Client) Effect(name string) (Effect, bool)

Effect looks up one active effect by name.

func (*Client) Effects

func (c *Client) Effects() []Effect

Effects returns the player's active status effects.

func (*Client) EmptySlot

func (c *Client) EmptySlot(ctx context.Context, slot int) error

EmptySlot moves whatever is in a slot back to the player's inventory, if anything is. Used between operations so a leftover input does not change what the next one produces.

func (*Client) Enchant

func (c *Client) Enchant(ctx context.Context, item string, level int32) (ItemStack, error)

Enchant applies an enchantment at a table.

level is the button index — 0, 1 or 2 for the three offers, not the experience level. The bot needs the levels and the lapis, and an enchanting table with neither simply offers nothing.

func (*Client) EnsureGrounded

func (c *Client) EnsureGrounded(ctx context.Context) (fell float64, err error)

EnsureGrounded makes the bot fall if it is hovering, and settles almost immediately if it is already standing on something.

Worth calling after anything that can leave a bot in mid-air — a teleport to an unverified spot, or mining the block it was standing on. Vanilla kicks a floating player after about four seconds ("floating too long"), and that kick lands with no warning.

func (*Client) Entities

func (c *Client) Entities() []Entity

Entities returns every tracked entity, nearest first.

func (*Client) EntitiesOfType

func (c *Client) EntitiesOfType(typeName string) []Entity

EntitiesOfType returns tracked entities whose type name matches, nearest first. The match accepts a bare name ("zombie") as well as a namespaced one ("minecraft:zombie").

func (*Client) EntityID

func (c *Client) EntityID() int32

EntityID returns the player's entity ID, valid once joined.

func (*Client) EquipArmour

func (c *Client) EquipArmour(name string) (ItemStack, error)

EquipArmour moves a wearable item onto the armour slot it belongs in.

It leans on the server's own shift-click behaviour: quick-moving a helmet from the inventory puts it on the head, because that is where the server decides such an item goes. Doing it by hand would mean replicating those placement rules client-side.

func (*Client) Fall

func (c *Client) Fall(ctx context.Context) (blocks float64, err error)

Fall drops the bot until it lands, detecting the ground by itself.

The client decodes chunks, so it usually knows where the floor is — but it does not need to, because the *server* is the authority on solid ground. A move that would put the player inside terrain is rejected and the server snaps them back with a position correction, so a correction arriving mid-descent means "you have landed, and here is exactly where".

This is why the fall is simulated tick by tick at real gravity rather than jumped in one packet: a plausible descent lands on the floor, whereas a single huge step is corrected as "moved too quickly" and tells you nothing about where the ground was.

func (*Client) FallTo

func (c *Client) FallTo(ctx context.Context, groundY float64) (blocks float64, err error)

FallTo drops the bot to a known ground height.

It still stops early if the server corrects the descent, so a groundY that is too low lands correctly rather than leaving the bot hovering. Prefer Fall unless the floor height is genuinely known and the extra precision matters.

func (*Client) FindGround

func (c *Client) FindGround(x, y, z int32) Support

FindGround scans downward for the first thing that would stop a fall.

Water counts as a stop and is reported separately, because it stops a fall *differently*: it cancels fall damage entirely, and then drowns anything that stays under. A bot that treats water as empty space falls through it like a stone and dies at the bottom.

Sets Known == false when the terrain is not loaded, and Found == false when it is loaded and nothing was hit. Callers must not treat the first as the second: an unloaded chunk reads as air everywhere, so "no data" would otherwise mean "the void".

func (*Client) FindInContainer

func (c *Client) FindInContainer(name string) (ItemStack, bool)

FindInContainer returns the slot holding an item in the open window.

func (*Client) FindItem

func (c *Client) FindItem(name string) (ItemStack, bool)

FindItem returns the slot holding an item, preferring an exact name match and otherwise the lowest-numbered fuzzy one.

func (*Client) FinishDig

func (c *Client) FinishDig(ctx context.Context, x, y, z, face int32) error

FinishDig completes breaking a block.

func (*Client) FreeStorageSlots

func (c *Client) FreeStorageSlots() int

FreeStorageSlots reports how many of the 36 storage slots are empty.

func (*Client) GameMode

func (c *Client) GameMode() GameMode

GameMode returns the player's mode, or GameModeUnknown if the server has not said.

func (*Client) GroundBelow

func (c *Client) GroundBelow() Support

GroundBelow finds what the bot is currently standing over.

func (*Client) HasLineOfSight

func (c *Client) HasLineOfSight(x, y, z int32) bool

HasLineOfSight reports whether the crosshair would land on a block, for callers that only need the yes/no answer.

func (*Client) Health

func (c *Client) Health() (health float32, food int32)

Health returns the last known health and food level.

func (*Client) HeldItem

func (c *Client) HeldItem() (ItemStack, bool)

HeldItem returns what is currently in the bot's hand.

func (*Client) HeldSlot

func (c *Client) HeldSlot() int

HeldSlot returns the selected hotbar slot.

func (*Client) HoldItem

func (c *Client) HoldItem(name string) (ItemStack, error)

HoldItem puts a named item into the bot's hand, wherever it currently is.

If the item is already on the hotbar this is just a slot selection. If it is in the main inventory it is swapped onto the hotbar first — which is the whole point, since a bot given a tool by `/give` has no control over where it lands.

func (*Client) Input

func (c *Client) Input() uint8

Input returns the current movement-input bits.

func (*Client) InteractAt

func (c *Client) InteractAt(entityID int32, dx, dy, dz float64) error

InteractAt right-clicks a specific point on an entity, relative to its own position.

Where you click usually does not matter — a villager opens its trades wherever it is poked. It matters for entities whose parts do different things: a chest boat carries the chest and the seat in separate hitboxes, so aiming at the middle boards it and only the rear opens the chest.

The offset is in blocks from the entity's position, in world axes.

func (*Client) InteractEntity

func (c *Client) InteractEntity(entityID int32) error

InteractEntity right-clicks an entity — taming, breeding, trading, leashing, shearing.

In 26.1 this is interact only: attacking moved to its own packet. The hit location is sent as a zero vector, which the variable-length encoding expresses in a single zero byte, since interacting with an entity does not depend on where on its body it was clicked.

func (*Client) InteractNearest

func (c *Client) InteractNearest(typeName string) (Entity, error)

InteractNearest right-clicks the closest entity of a type.

func (*Client) Inventory

func (c *Client) Inventory() []ItemStack

Inventory returns every non-empty slot the bot knows about, ordered by slot.

func (*Client) InventoryTruncated

func (c *Client) InventoryTruncated() bool

InventoryTruncated reports whether the last full-window snapshot could not be decoded to the end. See the comment on inventory.truncated.

func (*Client) IsSolidAt

func (c *Client) IsSolidAt(x, y, z int32) bool

IsSolidAt reports whether the block at a coordinate blocks movement.

func (*Client) IsTargetableAt

func (c *Client) IsTargetableAt(x, y, z int32) bool

IsTargetableAt reports whether the crosshair would stop on this block — true for cobweb, crops and torches, which IsSolidAt reports as empty.

func (*Client) Joined

func (c *Client) Joined() bool

Joined reports whether the play login packet has been seen.

func (*Client) KnownRecipes

func (c *Client) KnownRecipes() int

KnownRecipes returns how many recipes were learned from the server.

Worth checking before relying on RecipeFor: a server that sends its book late, or a decode that stopped early on an unknown component, leaves this short.

func (*Client) LineOfSightTo

func (c *Client) LineOfSightTo(x, y, z int32) (RayHit, sight)

LineOfSightTo reports whether the bot could actually hit a block from where it stands, and what is in the way if not.

The check is deliberately "aim at it and see what the ray hits first", because that is the question the game answers.

Terrain that is not loaded reads as air everywhere, which would wrongly look like a clear path, so an unloaded chunk reports sightClear — the caller has nothing better to go on and the server will arbitrate.

func (*Client) LoadedChunks

func (c *Client) LoadedChunks() int

LoadedChunks returns how many chunk columns are currently held.

func (*Client) Look

func (c *Client) Look(yaw, pitch float32) error

Look points the bot at an absolute yaw/pitch, in degrees.

func (*Client) LookAt

func (c *Client) LookAt(x, y, z float64) error

LookAt points the bot at an exact world coordinate.

func (*Client) LookAtBlock

func (c *Client) LookAtBlock(x, y, z int32) error

LookAtBlock points the bot at the centre of a block.

Block coordinates name a corner, so aiming at the raw integer targets the seam between four blocks and the ray can land on a neighbour. The classic symptom is a mining loop that stalls one block short of its target.

func (*Client) LookAtEntity

func (c *Client) LookAtEntity(e Entity) error

LookAtEntity faces a tracked entity, aiming at roughly body height rather than at its feet.

func (*Client) LookAtNearest

func (c *Client) LookAtNearest(typeName string) (Entity, error)

LookAtNearest faces the closest tracked entity of a type.

func (*Client) LookAtPlayer

func (c *Client) LookAtPlayer(name string) (Entity, error)

LookAtPlayer faces a named player, aiming at their head rather than feet.

func (*Client) LookDirection

func (c *Client) LookDirection(name string) error

LookDirection points the bot along a named direction.

func (*Client) LookYawPitch

func (c *Client) LookYawPitch(yaw, pitch *float32) error

LookYawPitch updates either axis independently. A nil component is left unchanged, so a caller can pan without re-deriving the current tilt.

func (*Client) LookingAt

func (c *Client) LookingAt() (RayHit, bool)

LookingAt returns the block the bot's crosshair is currently on, if any is within reach.

func (*Client) MissingRecipes

func (c *Client) MissingRecipes() int

MissingRecipes returns how many entries the server sent that could not be decoded.

Nonzero means RecipeFor's answers are incomplete rather than authoritative, and that is a distinction worth having: a partially decoded book answers "no recipe for that" in exactly the same words as a complete one, so without this a version whose book half-decodes looks like a version where half the recipes do not exist.

func (*Client) MoveTo

func (c *Client) MoveTo(x, y, z float64) error

MoveTo sets the bot's position directly in a single packet.

The server validates how far a player moved in one update and rejects implausible jumps, so this is only safe for short hops. Use WalkTo to cover distance, or a server-side teleport to cross the world.

func (*Client) NearestEntity

func (c *Client) NearestEntity(typeName string) (Entity, error)

NearestEntity returns the closest entity of the given type. An empty typeName matches any type. It returns an error wrapping ErrNoSuchEntity if nothing of that type is tracked.

func (*Client) OnGround

func (c *Client) OnGround() bool

OnGround reports the bot's last known support state.

func (*Client) OpenContainer

func (c *Client) OpenContainer(ctx context.Context, x, y, z, face int32) error

OpenContainer right-clicks a block and waits for the server to open its UI.

This is a real interaction, not a command: the block has to be in reach and the bot has to be able to see it, exactly as for placing. A block that is not a container simply never opens one, which is why this reports a timeout rather than waiting forever.

func (*Client) OpenContainerOnEntity

func (c *Client) OpenContainerOnEntity(ctx context.Context, entityID int32) error

OpenContainerOnEntity right-clicks an entity and waits for its UI — a villager's trades, a horse's inventory.

func (*Client) OpenContainerOnNearest

func (c *Client) OpenContainerOnNearest(ctx context.Context, typeName string) (Entity, error)

OpenContainerOnNearest right-clicks the closest entity of a type and waits for its UI.

func (*Client) PickupsSeen

func (c *Client) PickupsSeen() (total int32, byItem map[string]int32)

PickupsSeen returns how many items the bot has collected off the ground, and the per-item tally.

Worth watching in both directions. Sometimes collecting things is the point; but a bot that wanders over its own mining drops also inflates counts nobody meant it to touch, and quietly changes the inventory something else is measuring. Either way it is better observed than inferred afterwards.

func (*Client) PlaceBlock

func (c *Client) PlaceBlock(ctx context.Context, x, y, z, face int32) error

PlaceBlock right-clicks a block face, which places the held item against it.

The cursor position is the hit point within the face, in 0..1. Centre is a safe default; it only matters for blocks whose placement depends on where they were clicked, such as slabs and stairs.

func (*Client) PlaceBlockVerified

func (c *Client) PlaceBlockVerified(ctx context.Context, x, y, z, face int32) error

PlaceBlockVerified places a block and confirms it actually appeared, re-sending once if it didn't.

Use this when the placement is the point. PlaceBlock is left unverified because it doubles as "right-click this block" for opening UIs, where nothing is expected to change.

func (*Client) PlayerEntity

func (c *Client) PlayerEntity(name string) (Entity, error)

PlayerEntity finds a tracked player by name.

The spawn packet carries a UUID but no name, and names only arrive in the player_info packet — a bitmask-driven structure with a lot of decoding surface. This client requires online-mode=false anyway (it implements no encryption), and on such a server a player's UUID is *derived* from their name, so the name can be resolved without decoding player_info at all.

That equivalence is exactly what makes this shortcut safe here and unsafe in general: against an online-mode server these UUIDs come from Mojang and this lookup would never match.

func (*Client) PlayerRowsEmptyOf

func (c *Client) PlayerRowsEmptyOf(name string) bool

PlayerRowsEmptyOf reports whether the player's rows hold none of an item.

Reads better than comparing a count to zero at a call site, and says which side of the window it means — which is the mistake it exists to prevent.

func (*Client) PlayerSlotsStart

func (c *Client) PlayerSlotsStart() int

PlayerSlotsStart returns the first slot of the player's own inventory within the open window. Ingredients must be taken from at or above this.

func (*Client) Position

func (c *Client) Position() Position

Position returns the last position the server told us about.

func (*Client) PutIntoSlot

func (c *Client) PutIntoSlot(ctx context.Context, name string, slot int) (ItemStack, error)

PutIntoSlot moves a named item from the player's rows into a container slot.

The whole stack goes: pick it up, put it down. For a single item use PutOneIntoSlot — the difference matters for a furnace, where the fuel slot taking a whole stack of coal is fine, and for an anvil, where it is not.

The item is only ever taken from the player's own rows. Searching the whole window would find whatever is already in the container — including the item this call just placed — which is how a loop ends up shuffling one item back and forth forever.

func (*Client) PutOneIntoSlot

func (c *Client) PutOneIntoSlot(ctx context.Context, name string, slot int) (ItemStack, error)

PutOneIntoSlot moves a single item from the player's rows into a slot, leaving the rest of the stack where it was.

func (*Client) RayTrace

func (c *Client) RayTrace(ox, oy, oz, dx, dy, dz, maxDist float64) (RayHit, bool)

RayTrace walks the voxel grid from a point along a direction and returns the first targetable block within maxDist.

The traversal itself is geom.Raycast; what this adds is the only thing that needs a client — deciding whether a voxel stops the ray, which depends on terrain the bot has been sent and on the version's block tables.

The whole walk runs under a single terrain lock, so it cannot observe a half-applied block update partway along the ray.

func (*Client) RecipeFor

func (c *Client) RecipeFor(name string) (RecipeID, bool)

RecipeFor returns the recipe id that produces a named item.

The lookup is by what the recipe *makes*, which is the question a caller actually has. Names match the same way they do elsewhere: namespaced or bare.

func (*Client) RenameItem

func (c *Client) RenameItem(ctx context.Context, item, newName string) (ItemStack, error)

RenameItem renames the item in an anvil and takes the result.

The rename itself is a separate packet: putting an item in an anvil and clicking the output without sending a name gives back an unchanged item, so the name has to go first.

Anvils cost levels. In survival with no experience the result slot fills but the take is refused, which looks exactly like a rename that did not happen — so a caller testing this needs the bot to have levels.

func (*Client) ResetPickups

func (c *Client) ResetPickups()

ResetPickups clears the pickup tally, so a caller can measure a window rather than a session.

func (*Client) Run

func (c *Client) Run(ctx context.Context) error

Run pumps play-state packets until ctx is cancelled, the server disconnects, or a protocol error occurs. Exactly one goroutine may call it.

Only the packets required to stay connected, to know where we are and to model the world get decoded; everything else is skipped by its length prefix. That is what keeps this client small — there are 141 clientbound play packets in 26.1 and this handles a couple of dozen.

func (*Client) SelectTrade

func (c *Client) SelectTrade(index int32) error

SelectTrade chooses a villager's trade by its index in the offer list.

The offers are decoded, so a caller can pick by index or read Trades() to choose one. See TradeFor and TradeForItem to select by what a trade produces rather than by its position, which survives a villager whose offer order differs.

func (*Client) SetHeldSlot

func (c *Client) SetHeldSlot(slot int) error

SetHeldSlot selects a hotbar slot, 0-8.

func (*Client) SetInput

func (c *Client) SetInput(flags uint8) error

SetInput sets the player's movement-input bits (sneak, sprint, jump…).

These live in player_input as of 26.1. Older clients put sneaking in entity_action, which in 26.1 no longer has the action at all — so code written against the old shape sneaks silently never.

func (*Client) SetItemName

func (c *Client) SetItemName(name string) error

SetItemName sends the new name for the item in an open anvil.

Separate from RenameItem because the packet is separate: the server applies the name to whatever is in the anvil's first slot when it arrives, so a caller doing something unusual can send it directly.

func (*Client) SetSneaking

func (c *Client) SetSneaking(on bool) error

SetSneaking starts or stops sneaking, leaving the other input bits alone.

func (*Client) SetSprinting

func (c *Client) SetSprinting(on bool) error

SetSprinting starts or stops sprinting, leaving the other input bits alone.

func (*Client) ShootAt

func (c *Client) ShootAt(ctx context.Context, x, y, z float64, draw time.Duration) error

ShootAt aims at a point and looses an arrow at the given draw strength.

The bow must already be in hand and arrows in the inventory; neither is checked here because the server's refusal is silent and a caller wanting certainty should verify the outcome (a target_hit stat, a dead mob) rather than trust the shot.

func (*Client) ShootBlock

func (c *Client) ShootBlock(ctx context.Context, x, y, z int32, draw time.Duration) error

ShootBlock aims at the centre of a block face and shoots it.

func (*Client) ShootNearest

func (c *Client) ShootNearest(ctx context.Context, typeName string, draw time.Duration) (Entity, error)

ShootNearest shoots the closest entity of a type, aiming at its body.

func (*Client) SlotAt

func (c *Client) SlotAt(slot int) (ItemStack, bool)

SlotAt returns the contents of a specific slot.

func (*Client) SlotsNeeded

func (c *Client) SlotsNeeded(name string, count int32) (slots int, fits bool)

SlotsNeeded reports how many slots a quantity of an item would occupy, and whether it can fit in a player's storage at all.

Worth asking before setting something up: "hold 2304 dirt" needs all 36 slots, which leaves no room for the tool the bot might otherwise be carrying.

func (*Client) Smelt

func (c *Client) Smelt(ctx context.Context, input, fuel string, count int) (ItemStack, error)

Smelt puts an input and a fuel into an open furnace, blast furnace or smoker and collects what comes out.

It returns the smelted stack. count asks for more than one, which works because the furnace keeps smelting while both slots hold something — so a stack of 8 iron and a stack of coal yields eight ingots from one call.

A furnace given something it cannot smelt does not complain: the input sits there and no result ever appears. That is what the timeout reports.

func (*Client) Sneak

func (c *Client) Sneak(ctx context.Context, d time.Duration) error

Sneak holds sneak for a duration, then releases it. Useful for the sneak_time statistic, which accrues only while actually sneaking.

The release is unconditional: a cancelled context must still stand the bot back up, or it stays crouched for the rest of the session.

func (*Client) SprintTo added in v0.2.0

func (c *Client) SprintTo(ctx context.Context, x, y, z float64) error

SprintTo is WalkTo at sprinting speed, with the sprint input held for the journey so the server sees a sprinting player rather than a walking one covering ground too fast.

Sprinting costs hunger and needs food above six to start, so this refuses below that rather than moving at a speed the server will not accept.

func (*Client) StartDig

func (c *Client) StartDig(ctx context.Context, x, y, z, face int32) error

StartDig begins breaking a block.

func (*Client) State

func (c *Client) State() protocol.State

State returns the current protocol state.

func (*Client) Submerged

func (c *Client) Submerged() bool

Submerged reports whether the bot's head is inside water, which is the condition that drowns it.

func (*Client) Swing

func (c *Client) Swing() error

Swing plays the arm animation. Purely cosmetic on its own — it does not hit anything. Attack and Dig are the packets that do work.

func (*Client) TakeFromContainer

func (c *Client) TakeFromContainer(slot int) error

TakeFromContainer shift-clicks a slot, moving its whole stack to the other half of the window.

Shift-clicking rather than pick-up-then-put-down is deliberate: it is one packet, the server decides where the items land, and it is what actually empties a crafting result including every repeat the ingredients allow.

func (*Client) TakeSlot

func (c *Client) TakeSlot(ctx context.Context, slot int, timeout time.Duration) (ItemStack, error)

TakeSlot waits for a slot to fill and then shift-clicks it into the player's inventory, returning what was taken.

Shift-clicking rather than pick-up-and-place because that is the click that credits crafting and smelting statistics, and because for a result slot it takes every repeat the inputs allow rather than one.

func (*Client) Trade

func (c *Client) Trade(ctx context.Context, index int32) (ItemStack, error)

Trade selects a villager's trade and confirms it actually produced something.

Worth doing rather than firing SelectTrade and hoping. A trade that is locked out — a villager sells the same offer only so many times before it needs to restock, and a wandering trader's stock runs out for good — is *refused silently*: the packet is accepted, no result appears, and nothing anywhere says why. So is a trade the player cannot afford. Both look identical to success from the sending side.

The offer list itself is not decoded (its input items carry a component matcher this client cannot skip), so "is this trade available" cannot be read ahead of time. What can be observed is the result slot, which is the server's own answer — so this selects, waits, and reports what actually landed.

This is half of a trade, and most callers want TradeAndTake instead. Selecting an offer makes the result stack appear, which reads as success and is not one: the server counts nothing, grants no traded_with_villager, and emits no trade event until the result is *taken*. A caller that stops here leaves the villager holding the goods.

So: it returns the result stack, and collecting it with TakeFromContainer(MerchantResultSlot) is a required second step. Confirm that take by the player's stock going up, not by the result slot changing — vanilla re-offers the same trade immediately and refills the slot with an identical stack, so watching the slot answers "no" on a trade that worked. TradeAndTake does all of that.

func (*Client) TradeAndTake

func (c *Client) TradeAndTake(ctx context.Context, index int32, times int) (done int, err error)

TradeAndTake performs a trade and collects the result, repeating while the villager keeps supplying one. It returns how many trades actually completed.

Counting is done from the stock gained, not from the number of clicks. Shift-clicking a merchant result is a *batch* in vanilla: one click repeats the trade for as many uses as the villager has left and the player can afford. So a single take can be four trades, and counting clicks reports one — which is how this first read "traded 1" while the server had handed over twenty-four bread.

It stops when a trade produces nothing rather than treating that as a failure, which is what tells a test the difference between "locked out after four" and "never worked at all".

func (*Client) TradeFor

func (c *Client) TradeFor(output string) (TradeOffer, bool)

TradeFor finds the first available offer producing a named item.

This is what makes trading testable by intent rather than by index: "trade for bread" survives a villager whose offer order differs, where "trade 0" does not.

func (*Client) TradeForItem

func (c *Client) TradeForItem(ctx context.Context, output string, times int) (int, error)

TradeForItem selects and performs the first available offer producing a named item, and collects the result.

Returns how many trades completed. Unlike selecting by index this can say why nothing happened: whether the villager has no such offer at all, or has one that is locked out until it restocks.

func (*Client) Trades

func (c *Client) Trades() []TradeOffer

Trades returns the open merchant window's offers.

Empty unless a merchant window is open — the list arrives with the window and is discarded when it closes.

func (*Client) UUID

func (c *Client) UUID() protocol.UUID

UUID returns the player's UUID: derived from the username for an offline-mode server, or whatever the server assigned if it disagreed.

On an offline-mode server this is the player's identity — every statistic and advancement the server records is keyed by it — so anything checking up on the bot afterwards must use this value.

func (*Client) UpgradeInSmithingTable

func (c *Client) UpgradeInSmithingTable(ctx context.Context, template, base, addition string) (ItemStack, error)

UpgradeInSmithingTable applies a smithing template and an addition to a base item — the netherite upgrade, and armour trims.

All three slots are required. A smithing table with two of the three filled produces nothing and says nothing about which one is missing.

func (*Client) UseItem

func (c *Client) UseItem(ctx context.Context) error

UseItem right-clicks with the held item without targeting a block — eating, drinking, throwing, and using a bucket in mid-air.

func (*Client) UseOnBlock

func (c *Client) UseOnBlock(ctx context.Context, x, y, z, face int32) error

UseOnBlock right-clicks a block without placing anything — opening a crafting table, anvil, furnace or chest, or using a bed.

It is the same packet as PlaceBlock; what differs is intent and what is in hand. Several statistics (interact_with_anvil and friends) count the GUI opening rather than any item being used, so an empty hand is usually right.

func (*Client) Username

func (c *Client) Username() string

Username returns the bot's player name.

func (*Client) Version

func (c *Client) Version() *protocol.Version

Version returns the protocol version this client is speaking. It is nil until Connect has resolved it, which matters when auto-detecting.

func (*Client) WalkTo

func (c *Client) WalkTo(ctx context.Context, x, y, z float64) error

WalkTo moves the bot to a target at walking speed, one step per tick.

This is dead reckoning, not pathfinding: it walks a straight line and does not know about walls, drops or water. That is deliberate rather than missing. Callers typically position a bot in terrain they control, so the movement that has to look believable is short and local.

func (*Client) WaterSurfaceAbove

func (c *Client) WaterSurfaceAbove() (surfaceY float64, found bool)

WaterSurfaceAbove finds the Y at which the bot's head would clear the water, scanning up from its current position. Returns false if no surface is found.

func (*Client) WhyNotDamageable

func (c *Client) WhyNotDamageable() error

WhyNotDamageable explains what would stop damage reaching the player, or returns nil if nothing would.

Written as a question about the *precondition* rather than a check on the result, because checking the result is what goes wrong: health read after a hit cannot tell "survived" from "died and respawned to full", and an intact totem cannot tell "did not fire" from "was never needed".

func (*Client) Withdraw

func (c *Client) Withdraw(ctx context.Context, name string, count int32) (moved int32, err error)

Withdraw moves a named item out of the container into the player's inventory.

Same caveat in reverse: a full inventory means fewer items move than asked for, and the server does not say so.

type Direction

type Direction struct {
	// contains filtered or unexported fields
}

Direction is a named heading, as a rotation the bot can be pointed along.

A direction need not name both axes: "up" and "down" tilt without throwing away the current heading, which is what makes them usable mid-task. Apply is where that "leave this axis alone" rule lives, so no caller re-implements it.

Values, not a pair of *float32. The pointer form meant every entry in the table below allocated, callers could reach through and mutate a shared float, and "unset" and "zero" looked identical at a glance — for a type whose whole job is to distinguish them.

func LookupDirection

func LookupDirection(name string) (Direction, bool)

LookupDirection resolves a direction name, case- and space-insensitively.

func (Direction) Apply

func (d Direction) Apply(yaw, pitch float32) (float32, float32)

Apply returns the rotation this direction produces from a current one, leaving untouched whichever axis it does not name.

func (Direction) Pitch

func (d Direction) Pitch() (float32, bool)

Pitch returns the tilt this direction names, and whether it names one.

func (Direction) Yaw

func (d Direction) Yaw() (float32, bool)

Yaw returns the heading this direction names, and whether it names one.

type Effect

type Effect struct {
	ID        int32  `json:"id"`
	Name      string `json:"name"`
	Amplifier int32  `json:"amplifier"`
	// Duration in ticks. -1 means infinite, which the server sends for effects
	// given with an infinite duration.
	Duration int32 `json:"duration"`
}

Effect is one active status effect.

func (Effect) Level

func (e Effect) Level() int32

Level is the effect's level as a player would say it: amplifier 0 is I.

type Entity

type Entity = entities.Entity

Entity is a tracked entity in the bot's view of the world.

An alias rather than a wrapper: the tracker owns the type, and callers of this package should not have to import an internal one to name it.

type GameMode

type GameMode int32

GameMode is the player's own mode, as the server last reported it.

const (
	GameModeUnknown  GameMode = -1
	GameModeSurvival GameMode = iota - 1
	GameModeCreative
	GameModeAdventure
	GameModeSpectator
)

The vanilla modes. Unknown is the zero value rather than survival, so a caller cannot mistake "never told" for "told it was survival".

func (GameMode) Damageable

func (g GameMode) Damageable() bool

Damageable reports whether ordinary damage can reach a player in this mode.

Creative and spectator absorb everything, which is what makes an unresettable scenario look like a broken one.

func (GameMode) String

func (g GameMode) String() string

type ItemStack

type ItemStack = inventory.ItemStack

ItemStack is one inventory slot's contents.

An alias rather than a wrapper: the slot store owns the type, and callers of this package should not have to import an internal one to name it.

type Options

type Options struct {
	// Addr is the server's host:port.
	Addr string
	// Host and Port are what the handshake advertises. Servers may route on
	// these (virtual hosts, BungeeCord), so they are kept separate from the
	// address actually dialled. Defaults are derived from Addr.
	Host string
	Port uint16
	// Username is the offline-mode player name. The UUID is derived from it.
	Username string
	// Version is the Minecraft version to speak, e.g. "26.1". Empty means
	// auto-detect: the client pings the server first and adopts whatever
	// protocol it reports, which is what makes one binary usable against a
	// fleet of servers on different versions.
	Version string
	// DialTimeout bounds the TCP connect. Defaults to DefaultDialTimeout.
	DialTimeout time.Duration
	// ReadTimeout bounds any single read. Defaults to DefaultReadTimeout.
	ReadTimeout time.Duration

	// DisableAutoFall stops the bot falling by itself after a teleport.
	//
	// Auto-fall is on by default because teleporting is constant during tests
	// and vanilla kicks a floating player after about four seconds. A bot
	// teleported into mid-air and left there dies mid-run with no warning,
	// which is a failure that has nothing to do with what was being tested.
	//
	// Falling after a teleport is also simply what a real client does. When the
	// bot is already on solid ground the fall detects that within a tick or two
	// and costs ~100ms, so leaving it on is nearly free.
	DisableAutoFall bool

	// DisableAutoRespawn leaves a killed bot on the death screen. Auto-respawn
	// is on by default (the zero value) because without it a dead bot is a
	// *silent* dead end: the connection stays healthy and keep-alives keep
	// flowing, but the server will not position a player who has not
	// respawned, so every later action is quietly ignored and the session
	// simply stops making progress. Bots die routinely — lava, falls, mobs —
	// so this is on by default. It is switchable because anything asserting on
	// death itself needs to observe the corpse.
	DisableAutoRespawn bool

	// DisableIdlePosition stops the bot reporting its position while standing
	// still. See Client.startPositionLoop for why that is almost never what
	// you want.
	DisableIdlePosition bool

	// Logger receives connection lifecycle events. Defaults to slog.Default().
	Logger *slog.Logger

	// OnPacket, if set, observes every decoded clientbound packet.
	//
	// It is called synchronously on the read loop, so a slow callback delays
	// keep-alive replies and can get the bot kicked. Intended for tracing.
	OnPacket func(state protocol.State, p protocol.Packet)
}

Options configures a Client.

type Position

type Position struct {
	X, Y, Z    float64
	Yaw, Pitch float32
}

Position is the player's location and facing, as last known from the server.

type RayHit

type RayHit struct {
	X        int32   `json:"x"`
	Y        int32   `json:"y"`
	Z        int32   `json:"z"`
	Face     int32   `json:"face"`
	Distance float64 `json:"distance"`
	State    int32   `json:"state"`
}

RayHit is a block the crosshair landed on.

type RecipeID

type RecipeID int32

RecipeID is the server-assigned id that craft_recipe_request takes.

type ServerStatus

type ServerStatus struct {
	// VersionName is the server's own label, e.g. "Fabric 26.1.2". It is free
	// text and servers rewrite it freely, so it is informational only.
	VersionName string
	// Protocol is the wire protocol number. This is the authoritative value —
	// it is what the handshake must match.
	Protocol int32
}

ServerStatus is what a server-list ping reports.

func PingServer

func PingServer(addr, host string, port uint16, timeout time.Duration) (ServerStatus, error)

PingServer performs a server-list ping and reports the version.

The status handshake is deliberately version-agnostic: the protocol number sent in it is ignored by the server for a status request, so this works against any version without knowing it in advance. That is what makes auto-detection possible at all.

type Support

type Support struct {
	// GroundY is the Y coordinate the bot's feet rest at.
	GroundY float64
	// Found is false when no floor was located within the search depth.
	Found bool
	// InWater is true when the landing point is water rather than solid ground.
	InWater bool
	// InLava is true when the landing point is lava.
	InLava bool
	// Known is false when the column holding the search is not loaded, which
	// is a different answer from "searched and found nothing" and must not be
	// collapsed into it.
	//
	// The two were the same value for a long time, and the comment on
	// FindGround told callers not to confuse them without giving them any way
	// to tell. Fall then read the shared "not found" as "no floor here" and
	// descended into terrain it simply had not been sent yet, which a server
	// answers by refusing every move and then kicking for floating.
	Known bool
}

Support describes what a bot is standing in or on.

type TradeOffer

type TradeOffer struct {
	// Index is the offer's position in the list, which is what SelectTrade takes.
	Index int32

	// Input and Input2 are what the trade costs. Input2 is empty for the many
	// trades that take a single item.
	Input  ItemStack
	Input2 ItemStack
	// Output is what the trade produces.
	Output ItemStack

	// Disabled is the server's own "this offer is not available right now",
	// which is what a villager sets once it has run out of uses and needs to
	// restock. A wandering trader sets it and never clears it.
	Disabled bool

	// Uses and MaxUses are how many times this offer has been taken and how
	// many it allows. Uses >= MaxUses is the same condition as Disabled,
	// arriving a moment earlier.
	Uses    int32
	MaxUses int32

	// XP is the villager experience the trade grants; Demand and SpecialPrice
	// feed the price adjustment a villager makes for popular trades.
	XP           int32
	SpecialPrice int32
	Demand       int32
	PriceMult    float32
}

TradeOffer is one entry in a villager's or wandering trader's offer list.

func (TradeOffer) Available

func (t TradeOffer) Available() bool

Available reports whether the offer can be taken right now.

Both conditions are checked because they are not quite the same moment: the server sets Disabled when it decides the offer is spent, and Uses reaching MaxUses is the arithmetic that leads there. Taking a spent trade is accepted and silently does nothing, which is the failure this exists to prevent.

func (TradeOffer) String

func (t TradeOffer) String() string

String describes the offer the way a person would read it.

type WindowType

type WindowType int32

WindowType identifies the kind of UI the server opened. The file also carries the slot layouts each of those windows uses.

Where these numbers come from

They are not in minecraft-data — there is no windows.json — so every value here was read off a live 26.1.2 server by opening the block and recording what it reported. They are documentation of an observation, not a guess, and the layout tests re-derive the sizes from the same source.

Do not branch on WindowType

It is here so a caller can *report* what it opened, not so the client can decide behaviour from it. Sizes and layouts are read from the window itself: a double chest is 54 own-slots where a single is 27, a copper chest is the same type ID as an oak one, and a chest minecart is a chest that happens to be an entity. Deriving from the window covers all of that for free, and hard-coding per type would need a new case for each variant Mojang adds.

Blocks that look like containers and are not

fletching_table, composter and an *empty* lectern open nothing at all — they are decorative or interacted with directly — so OpenContainer times out on them. That is the correct answer, not a bug. A lectern with a book opens type 17 with zero slots, being a reader rather than a container.

The layout rule

Every container window is [the container's own slots][the player's 36]. So the player's inventory always begins at Size()-PlayerWindowSlots, and a container's own slot count is Size()-PlayerWindowSlots. Nothing else about a window's shape is knowable from the protocol.

const (
	WindowGeneric9x1   WindowType = 0
	WindowGeneric9x3   WindowType = 2 // chest, barrel, ender chest, copper chest, chest minecart
	WindowGeneric3x3   WindowType = 6 // dispenser, dropper
	WindowCrafter      WindowType = 7 // the 3x3 auto-crafter
	WindowAnvil        WindowType = 8 // "container.repair"
	WindowBeacon       WindowType = 9
	WindowBlastFurnace WindowType = 10
	WindowBrewingStand WindowType = 11
	WindowCrafting     WindowType = 12 // crafting table
	WindowEnchantment  WindowType = 13
	WindowFurnace      WindowType = 14
	WindowGrindstone   WindowType = 15
	WindowHopper       WindowType = 16 // hopper, hopper minecart
	WindowLectern      WindowType = 17 // a lectern holding a book; carries no slots
	WindowLoom         WindowType = 18
	WindowMerchant     WindowType = 19 // villager, wandering trader
	WindowShulkerBox   WindowType = 20
	WindowSmithing     WindowType = 21 // "container.upgrade"
	WindowSmoker       WindowType = 22
	WindowCartography  WindowType = 23
	WindowStonecutter  WindowType = 24
)

Window type IDs, as reported by 26.1.2.

func (WindowType) String

func (w WindowType) String() string

String names the window type for logs and errors.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL