A client-side Fabric library that exposes a clean, high-level API for player interaction, world querying, and human-like automation. Designed to be a shared foundation so other mods don't have to reimplement the same low-level mixins.
PlayerAPI does nothing on its own. If you downloaded this, another mod you use requires it as a dependency. Simply place the .jar in your mods folder alongside that mod.
Version: 1.8.0
Minecraft: 1.21.11 (Fabric)
Maven coordinate: com.playerapi:playerapi:1.8.0
- Overview & Philosophy
- Architecture
- Adding PlayerAPI as a Dependency
- Threading Model
- Events — PlayerAPIEvents
- Scheduler
- Forced Actions
- Human Actions
- Info (Read-Only Queries)
- Data Types
- Key Names Reference
- Known Limitations
PlayerAPI is a client-side Fabric library that provides a clean, stable abstraction over everything a client-side automation or utility mod needs to do with the local player. It exists so that consumer mods never have to import Minecraft internals directly — all game interaction flows through PlayerAPI's public API classes.
Core design principles:
- Separation of concerns. Every API class has one clear responsibility: info classes only read state, action classes only perform actions. Nothing is mixed.
- Safe defaults everywhere. Every method that reads player state returns a safe default (
0,false,"", empty list,EMPTYsnapshot) if the player is not in a world. Consumer mods never need null checks. - Main-thread only. All action calls and event callbacks run on the main game thread. There is no concurrency to manage.
- Forced vs. Human. There are two parallel action systems. Forced actions are instant and computer-precise (good for precise positioning). Human actions add reaction delays, spring-physics look interpolation, and mouse noise (good for looking natural). Consumer mods choose which fits their use case.
- No internal state leaks. Snapshot types (
ItemSnapshot,EntitySnapshot,BlockSnapshot,EffectSnapshot) are immutable Java records. They can be stored and compared freely without the underlying game object becoming stale.
┌─────────────────────────────────────────────────────────┐
│ Consumer Mod │
│ (depends on PlayerAPI, never imports MC internals) │
└───────────────────┬─────────────────────────────────────┘
│ uses
┌───────────────────▼─────────────────────────────────────┐
│ PlayerAPI Public Surface │
│ │
│ Events Scheduler Actions (Forced) │
│ PlayerAPIEvents Scheduler MovementActions │
│ LookActions │
│ Info (read) InventoryActions │
│ PlayerInfo InteractionActions │
│ InventoryInfo ChatActions │
│ WorldInfo SoundActions │
│ TabListInfo DisplayActions │
│ │
│ Actions (Human) Data Types │
│ HumanActions ItemSnapshot │
│ HumanProfile EntitySnapshot │
│ BlockSnapshot │
│ EffectSnapshot │
└───────────────────┬─────────────────────────────────────┘
│ wraps
┌───────────────────▼─────────────────────────────────────┐
│ PlayerAPI Internals (not public) │
│ │
│ PlayerAPIMod — wires Fabric events, drives ticks │
│ KeyStateTracker — held-key set, mixin read target │
│ SchedulerState — tick-based task queue │
│ HumanLookState — spring-damper look interpolation │
│ MovementInputMixin — @Mixin(KeyboardInput) override │
│ PlayerInventoryAccessor — @Mixin accessor for slots │
└─────────────────────────────────────────────────────────┘
ClientTickEvents.END_CLIENT_TICK
│
├─ 1. SchedulerState.tick() — fires due scheduled tasks
├─ 2. HumanLookState.tick() — advances spring interpolation
├─ 3. diff-based event checks — health, position changes
└─ 4. PlayerAPIEvents.TICK.invoke() — consumer mod tick callbacks
MovementInputMixin is a @Mixin on KeyboardInput with a TAIL injection into its tick() method. At the end of every vanilla input tick, if KeyStateTracker.isActive() is true, the mixin overwrites input.pressingForward, input.pressingBack, input.pressingLeft, input.pressingRight, input.jumping, and input.sneaking with the values stored in KeyStateTracker. This means the override is completely transparent to the rest of the game — movement, sprint physics, and server packets all behave normally.
repositories {
mavenLocal()
}
dependencies {
modImplementation "com.playerapi:playerapi:1.7.0"
}playerapi_version=1.7.0"depends": {
"playerapi": ">=1.7.0"
}Build PlayerAPI first:
cd PlayerAPI
./gradlew publishToMavenLocal
Then build your mod normally.
All PlayerAPI calls must be made from the main game thread. This is the thread that runs ClientTickEvents, renders frames, and processes network packets. In practice this means:
- Inside
PlayerAPIEvents.TICKhandlers — ✅ always safe - Inside any Fabric event handler registered on the main thread — ✅ safe
- From a background thread or
CompletableFuture— ❌ never safe; useMinecraftClient.getInstance().execute(() -> ...)to marshal back
The Scheduler always fires callbacks on the main thread.
All events live on com.playerapi.PlayerAPIEvents. Register listeners from your mod's onInitializeClient().
PlayerAPIEvents.TICK.register(() -> {
// runs every tick
});All events use Fabric's EventFactory.createArrayBacked pattern — multiple mods can register independently and all listeners fire.
Event<PlayerAPIEvents.Tick> TICKFires once per game tick, after END_CLIENT_TICK, after the scheduler has processed due tasks.
Interface:
@FunctionalInterface
interface Tick {
void onTick();
}Notes:
- This is the primary entry point for any per-tick automation logic.
- Call
PlayerInfo.isInWorld()at the top if your logic requires a loaded world. - The scheduler runs before this event fires, so tasks scheduled from a previous tick are already complete when your
TICKhandler runs.
Event<PlayerAPIEvents.ChatReceived> CHAT_RECEIVEDFires when any chat message arrives — player chat, system messages, or game messages. Covers both signed player messages and unsigned server messages.
Interface:
@FunctionalInterface
interface ChatReceived {
void onChatReceived(String sender, String message);
}| Parameter | Description |
|---|---|
sender |
Display name of the sender, stripped of colour codes. Empty string for system/game messages. |
message |
Full message text, stripped of colour codes. |
Notes:
- Action bar messages (
overlay=true) are not forwarded. - Both
ClientReceiveMessageEvents.CHAT(player messages) andClientReceiveMessageEvents.GAME(system/game messages) feed into this single event.
Event<PlayerAPIEvents.HealthChanged> HEALTH_CHANGEDFires whenever the local player's health value changes by any amount.
Interface:
@FunctionalInterface
interface HealthChanged {
void onHealthChanged(float oldHealth, float newHealth);
}Notes:
PLAYER_DEATHfires first if health dropped to 0, thenHEALTH_CHANGEDfires immediately after.- Not fired on world join (initial health read). Only fires on changes after the first tick.
Event<PlayerAPIEvents.InventoryChanged> INVENTORY_CHANGEDFires when the player's main inventory changes.
Interface:
@FunctionalInterface
interface InventoryChanged {
void onInventoryChanged(int slot, String newItemId);
}| Parameter | Description |
|---|---|
slot |
Inventory slot that changed. -1 means a bulk/unknown change. |
newItemId |
Namespaced item ID of the new item in that slot, e.g. "minecraft:diamond_sword". "minecraft:air" for an empty slot. |
Event<PlayerAPIEvents.PositionChanged> POSITION_CHANGEDFires when the player moves more than 0.1 blocks from their position at the last firing. This is a threshold-based event — it does not fire every tick, only on meaningful movement.
Interface:
@FunctionalInterface
interface PositionChanged {
void onPositionChanged(double x, double y, double z);
}Notes:
- Coordinates are the player's feet position.
- 0.1 blocks is roughly the minimum visible movement at normal render scale.
- Not suitable for millimetre-precision tracking — use
PlayerInfo.getX/Y/Z()insideTICKfor that.
Event<PlayerAPIEvents.WorldJoin> WORLD_JOINFires once after the client connects to a server or loads a world and the player entity is available.
Interface:
@FunctionalInterface
interface WorldJoin {
void onWorldJoin();
}Notes:
- Health/position diff state is reset on world join, so
HEALTH_CHANGEDandPOSITION_CHANGEDwill not fire spuriously on the first tick. PlayerInfo.isInWorld()returns true at the time this fires.
Event<PlayerAPIEvents.WorldLeave> WORLD_LEAVEFires when the client disconnects or leaves a world. All held keys are released and any active human look interpolation is cancelled automatically before this event fires.
Interface:
@FunctionalInterface
interface WorldLeave {
void onWorldLeave();
}Event<PlayerAPIEvents.PlayerDeath> PLAYER_DEATHFires when the player's health reaches 0.
Interface:
@FunctionalInterface
interface PlayerDeath {
void onPlayerDeath();
}Notes:
HEALTH_CHANGEDalso fires immediately after withnewHealth = 0.- The player entity still exists at the time this fires (the death screen appears later).
Event<PlayerAPIEvents.TabListUpdated> TAB_LIST_UPDATEDFires when the server updates the tab-list player list. Throttled to at most once per tick.
Interface:
@FunctionalInterface
interface TabListUpdated {
void onTabListUpdated();
}Event<PlayerAPIEvents.BlockBroken> BLOCK_BROKENFires when the local player successfully breaks a block. Backed by Fabric's ClientPlayerBlockBreakEvents.AFTER.
Interface:
@FunctionalInterface
interface BlockBroken {
void onBlockBroken();
}Notes:
- Instant-break blocks (crops, tall grass, flowers) fire immediately client-side without waiting for server confirmation. This is the most important case for farming automation.
- Survival-mode blocks (stone, wood, etc.) fire after the server confirms the break.
- No position or block type information is passed. If you need that, read
WorldInfo.getBlockAtCrosshair()from theTICKevent just before.
com.playerapi.Scheduler — tick-based task scheduling on the main game thread.
1 tick ≈ 50 ms at 20 TPS.
All callbacks are Runnable and fire on the main game thread at the end of the tick they are due.
Schedule a task to run after delayTicks ticks. Returns a cancel token.
Scheduler.schedule(20, () -> doSomethingAfterOneSecond());
// token = return value, save it if you need to canceldelayTicks = 0fires on the very next tick.- Tasks are not guaranteed exact-tick timing if the game is lagging below 20 TPS, but they will never fire early.
Convenience overload. Converts milliseconds to ticks by dividing by 50, rounded up to a minimum of 1.
Scheduler.scheduleMs(500, () -> doSomethingAfterHalfSecond());Schedule a repeating task.
| Parameter | Description |
|---|---|
periodTicks |
Ticks between each invocation. |
times |
Number of times to fire. Pass 0 for infinite (runs until cancelled). |
task |
The callback to run. |
// Fire 5 times, once per second:
Scheduler.scheduleRepeating(20, 5, () -> BotLogger.info("tick!"));
// Fire forever (infinite loop — cancel manually):
int token = Scheduler.scheduleRepeating(20, 0, () -> doPeriodicCheck());
// later:
Scheduler.cancel(token);Important: Infinite repeating tasks self-reschedule after each firing. Cancelling the initial token only prevents the current pending invocation from firing — if the task has already fired once, use cancelAll() or track re-scheduled tokens yourself if you need hard cancellation.
Cancel a pending task by its token. Safe to call with an invalid/already-fired token.
Cancel every pending task across all mods using this scheduler. Useful in stopBot() to ensure no lingering delayed actions fire after stopping.
Returns the current global tick count as tracked by PlayerAPI. Starts at 0 on mod load and increments once per END_CLIENT_TICK. Useful for timestamps and rate-limiting.
long start = Scheduler.getCurrentTick();
// ...later...
long elapsed = Scheduler.getCurrentTick() - start; // in ticks
double seconds = elapsed / 20.0;Forced actions are instant and computer-precise. They take effect on the very next game tick with no delay, noise, or spring physics. Use these when precision matters more than appearance.
Controls which movement keys the player holds. The override only applies when setActive(true) has been called.
Valid key names: "forward", "back", "left", "right", "sprint", "sneak", "jump", "attack", "use"
Enable or disable the movement key override. When disabled, all keys are released and the player regains full manual control. Call setActive(true) before pressing any keys.
MovementActions.setActive(true);
MovementActions.pressKey("forward");
MovementActions.pressKey("sprint");
// player now walks forwardReturns whether the movement override is currently enabled.
Hold a key indefinitely. The key stays held on every subsequent tick until releaseKey or releaseAll is called.
MovementActions.pressKey("jump"); // holds space barRelease a single previously held key.
MovementActions.releaseKey("sprint");Release all currently held keys (forward, back, left, right, sprint, sneak, jump, attack, use). Does not disable the override — setActive remains unchanged.
Press a key for durationMs milliseconds then automatically release it. Internally converts to ticks.
MovementActions.tapKey("jump", 100); // tap space for 100 ms (2 ticks)Press multiple keys simultaneously. Equivalent to calling pressKey for each.
Release only the four directional keys (forward, back, left, right). Sprint, sneak, and jump states are unchanged. Useful for stopping movement while staying in sprint mode.
Returns an immutable snapshot of all currently held key names.
Returns true if the given key is currently being held by the override.
Returns a random delay between 50 ms and 260 ms. Useful as a timing jitter between programmatic key presses for anti-pattern detection.
Instantly set the player's camera direction. All changes take effect immediately — there is no animation.
Minecraft yaw convention:
0°= South (+Z)90°= West (−X)180°= North (−Z)270°= East (+X)
Pitch: −90° = straight up, 0° = horizontal, 90° = straight down.
Set the player's horizontal rotation to exactly yaw degrees.
Set the player's vertical rotation to exactly pitch degrees. Automatically clamped to [−90, 90].
Instantly rotate to look at an exact world position. Calculates the required yaw and pitch automatically from the player's current eye position.
LookActions.lookAt(100.0, 64.0, -200.0);
// For level aim: pass player.getY() + 1.62 as the y argumentInstantly look at the centre of a block (x+0.5, y+0.5, z+0.5).
Same as above, accepting a BlockPos.
Look at the centre of a specific face of a block. face is a Minecraft Direction enum value (UP, DOWN, NORTH, SOUTH, EAST, WEST).
LookActions.lookAtBlock(10, 64, -5, Direction.UP); // aim at top faceLook at a custom point within a block. Offsets are fractions in [0, 1] where 0.5, 0.5, 0.5 is the block centre.
LookActions.lookAtBlock(10, 64, -5, 0.5, 1.0, 0.5); // top-face centreSnap the player's yaw to the nearest multiple of 90°. Useful for aligning before a cardinal-facing path.
Returns the current yaw rounded to the nearest cardinal direction (0, 90, 180, or 270).
Direct inventory manipulation. All operations take effect instantly on the next tick.
Switch the selected hotbar slot to slot (0–8). Has no effect if slot is out of range.
Switch to the first hotbar slot whose item's display name matches exactly (case-insensitive). Returns true if found and switched, false if not in hotbar.
InventoryActions.switchToItem("Wheat Hoe");Switch to the first hotbar slot whose item display name starts with the given prefix (case-insensitive). Useful for tiered items like "Pest Repellent I" / "Pest Repellent MAX" where you only know the prefix.
InventoryActions.switchToItemStartingWith("Pest Repellent");Switch to the first hotbar slot whose item's namespaced ID contains idSubstring (case-insensitive). More stable than display name matching when item names are localised or can change.
InventoryActions.switchToItemId("diamond_hoe");Drop one item from the currently held stack (Q key equivalent).
Drop the entire currently held stack (Ctrl+Q equivalent).
Switch to each hotbar slot matching displayName and drop the entire stack. Restores the original selected slot afterward.
Player interactions with items, blocks, and entities.
Use (right-click) the item in the main hand. Sends the appropriate use-item packet to the server. Works for consumables, throwables, placing blocks, opening blocks, etc.
Same as useItem() but for the off-hand slot.
Continuously send useItem() every tick for durationMs milliseconds. Useful for items that require holding right-click (fishing rods, bows, charging crossbows).
InteractionActions.useItemHeld(1500); // hold right-click for 1.5 secondsAttack the entity currently under the player's crosshair (client.targetedEntity). Does nothing if no entity is targeted.
Send a left-click-block event for the block currently under the crosshair. Call every tick to simulate holding left-click to mine a block. Does nothing if the crosshair is not targeting a block.
Right-click interact with the entity currently under the crosshair (client.targetedEntity). Used for talking to NPCs, riding entities, etc.
Close any currently open screen (inventory, container, sign, etc.). Equivalent to pressing Escape.
Send a respawn request to the server. Call this when the player is on the death screen to respawn.
Send a regular chat message on behalf of the player. Do not include a leading /.
ChatActions.sendChat("Hello, world!");Send a command without the leading slash. PlayerAPI prefixes the slash internally.
ChatActions.sendCommand("tp 0 64 0"); // sends /tp 0 64 0
ChatActions.sendCommand("warp garden"); // sends /warp gardenPlay sounds from the built-in Minecraft sound library or from your own mod's .ogg files.
Browse all Minecraft sound IDs at: https://misode.github.io/sounds/
Register a new sound event. Call once during onInitializeClient(). The .ogg file must be at assets/<namespace>/sounds/<name>.ogg inside your mod's resources jar.
SoundActions.registerSound("mymod", "my_alert"); // assets/mymod/sounds/my_alert.oggPlay a sound by its Identifier. Works for both registered mod sounds and all built-in Minecraft sounds.
| Parameter | Range | Description |
|---|---|---|
volume |
0.0 – 2.0 | 1.0 = normal volume |
pitch |
0.5 – 2.0 | 1.0 = normal pitch/speed |
Convenience overload accepting namespace and name as separate strings.
SoundActions.play("minecraft", "entity.player.levelup", 1.0f, 1.0f);Play by a full string ID such as "minecraft:entity.player.levelup". If no namespace colon is present, defaults to "minecraft".
SoundActions.playById("entity.experience_orb.pickup", 1.0f, 1.5f);Play a sound times times with intervalTicks ticks between each play. Uses the Scheduler internally.
// Play level-up sound 3 times, 1 second apart:
Identifier id = Identifier.of("minecraft", "entity.player.levelup");
SoundActions.playRepeated(id, 1.0f, 1.0f, 3, 20);Same as above with a fixed 20-tick (1 second) interval.
String-ID convenience version of playRepeated.
SoundActions.playByIdRepeated("minecraft:entity.player.levelup", 1.0f, 1.0f, 5, 20);Stop all currently playing sounds.
Show in-game HUD text without relying on Minecraft internals directly.
Show a title and optional subtitle with the default timing (10 ticks fade-in / 70 ticks stay / 20 ticks fade-out).
Supports § colour codes. Pass "" for either parameter to leave it blank.
DisplayActions.showTitle("§aBot Started", "§7Primary path loaded");Same as above with custom timing.
Show a short message on the action bar — the line that appears above the hotbar. Disappears after approximately 2 seconds. Supports § colour codes.
DisplayActions.showActionBar("§ePaused — waiting for repellent");The Human API mirrors the Forced API but adds human-like imperfection: reaction delays, spring-physics camera movement with overshoot, random end-of-movement noise, and scroll-wheel slot switching. All timing parameters are tunable through HumanProfile.
All look methods start a spring-damper interpolation toward the target. There is a configurable reaction delay before movement begins, the camera accelerates toward the target, naturally overshoots, and settles with a small random noise offset. A 3-second watchdog forces completion if the spring is too slow.
| Method | Description |
|---|---|
lookAt(float yaw, float pitch) |
Smooth look to a yaw/pitch pair. |
lookAt(double x, double y, double z) |
Smooth look toward an exact world position. |
lookAt(int x, int y, int z) |
Smooth look at the centre of a block (integer coords). |
lookAt(BlockPos pos) |
Smooth look at a block position. |
lookAt(int x, int y, int z, Direction face) |
Smooth look at a specific face of a block. |
lookAt(BlockPos pos, Direction face) |
Same, with a BlockPos. |
lookAt(int x, int y, int z, double xOffset, double yOffset, double zOffset) |
Smooth look at a custom point within a block. |
lookYaw(float yaw) |
Change only yaw, keep current pitch. |
lookPitch(float pitch) |
Change only pitch, keep current yaw. |
cancelLook() |
Abort any in-progress look interpolation. View stays where it is. |
isLooking() → boolean |
Returns true while a look (including reaction delay) is active. |
| Method | Description |
|---|---|
pressKey(String key) |
Hold a key after a random [reactionMin, reactionMax] tick delay. |
releaseKey(String key) |
Release a key after a short [keyDelayMin, keyDelayMax] tick delay. |
tapKey(String key, int holdTicks) |
Tap a key for approximately holdTicks ticks, with jitter on both press and release. |
pressKeys(String... keys) |
Press multiple keys simultaneously, all after the same reaction delay. |
releaseAll() |
Release all keys after 1–2 ticks. |
| Method | Description |
|---|---|
switchToSlot(int targetSlot) |
Switch to hotbar slot, optionally scrolling through intermediate slots if HumanProfile.scrollThroughSlots is true. |
switchToItem(String displayName) |
Switch to first matching display name, with delay. |
switchToItemId(String idSubstring) |
Switch to first matching item ID, with delay. |
| Method | Description |
|---|---|
useItem() |
Use main-hand item after a [useItemDelayMin, useItemDelayMax] tick delay. |
useOffhandItem() |
Use off-hand item after the same delay. |
attackTargeted() |
Attack crosshair entity after a reaction delay. |
interactTargeted() |
Interact with crosshair entity after a reaction delay. |
Start a human look toward the world position, then execute callback once the look has fully settled. Internally polls isLooking() every tick until false, then fires the callback. A 3-second watchdog fires the callback anyway to prevent hangs.
HumanActions.lookThenDo(100.0, 64.0, -200.0, () -> {
HumanActions.useItem(); // fires only after camera settles
});Overloads:
lookThenDo(int x, int y, int z, Runnable callback)— block centrelookThenDo(BlockPos pos, Runnable callback)— BlockPoslookThenDo(BlockPos pos, Direction face, Runnable callback)— block facelookThenDo(float yaw, float pitch, Runnable callback)— raw yaw/pitch
Returns the shared HumanProfile instance for reading or changing timing parameters.
com.playerapi.HumanProfile — a singleton configuration object. All fields are public and can be modified at any time. Changes take effect on the next HumanActions call.
HumanProfile p = HumanProfile.getInstance();| Field | Type | Default | Description |
|---|---|---|---|
reactionMinTicks |
int |
2 (~100 ms) |
Minimum delay before any human action begins. |
reactionMaxTicks |
int |
5 (~250 ms) |
Maximum delay before any human action begins. A real human averages 200–300 ms. |
A random value in [reactionMin, reactionMax] is chosen each time.
| Field | Type | Default | Description |
|---|---|---|---|
lookSpringK |
float |
0.30 |
Spring constant. Fraction of angular error added to velocity per tick. Higher = faster approach. Range: 0.15 (slow) – 0.50 (fast). |
lookDamping |
float |
0.55 |
Damping multiplier per tick. Values below ~2·√K produce overshoot. Range: 0.4 (bouncy) – 0.85 (barely any overshoot). Critical damping for K=0.30 is ≈1.095. |
lookMaxDegPerTick |
float |
35° |
Hard cap on rotational speed per tick. Prevents instant flicks. |
lookNoiseMaxDeg |
float |
0.8° |
Random noise added once the look settles. Simulates imprecision. |
lookMaxSettleTicks |
int |
40 |
If the look hasn't settled after this many ticks, it is forced to the exact target. Watchdog against infinite loops. |
| Field | Type | Default | Description |
|---|---|---|---|
enableMicroLook |
boolean |
false |
When true, tiny random camera nudges apply every tick while no look is in progress. |
microLookChancePerTick |
float |
0.012 |
Probability per tick of a micro-movement firing (≈ once per 4 seconds). |
microLookMaxDeg |
float |
0.4° |
Maximum yaw change per micro-movement. Pitch is half this value. |
| Field | Type | Default | Description |
|---|---|---|---|
keyDelayMinTicks |
int |
1 |
Minimum delay between key command and key going down. |
keyDelayMaxTicks |
int |
4 |
Maximum delay. |
tapHoldJitterTicks |
int |
3 |
Extra random ticks added to a tap's hold duration. |
| Field | Type | Default | Description |
|---|---|---|---|
scrollThroughSlots |
boolean |
true |
When true, switchToSlot scrolls through intermediate slots rather than jumping directly. |
scrollIntervalTicks |
int |
2 |
Ticks between each scroll step when scrollThroughSlots is enabled. |
| Field | Type | Default | Description |
|---|---|---|---|
useItemDelayMinTicks |
int |
1 |
Minimum delay before a use-item action fires. |
useItemDelayMaxTicks |
int |
3 |
Maximum delay. |
HumanProfile.getInstance().applyFastPreset(); // near-instant, snappy look, minimal noise
HumanProfile.getInstance().applyCasualPreset(); // slow reactions, bouncy look, micro-movements
HumanProfile.getInstance().resetDefaults(); // restore all factory defaults| Preset | reactionMin | reactionMax | lookSpringK | lookDamping | enableMicroLook |
|---|---|---|---|---|---|
| Default | 2 | 5 | 0.30 | 0.55 | false |
| Fast | 1 | 2 | 0.48 | 0.58 | false |
| Casual | 3 | 7 | 0.20 | 0.50 | true |
Info classes provide read-only access to game state. They never modify anything. All return safe defaults if the player is not in a world.
Query the local player's state.
| Method | Returns | Description |
|---|---|---|
getX() |
double |
Player feet X coordinate. |
getY() |
double |
Player feet Y coordinate. |
getZ() |
double |
Player feet Z coordinate. |
getEyeY() |
double |
Player eye-level Y (getY() + eyeHeight). |
getYaw() |
float |
Horizontal rotation (degrees, Minecraft convention). |
getPitch() |
float |
Vertical rotation (degrees). |
getVelocityX/Y/Z() |
double |
Per-axis velocity in blocks per tick. |
isOnGround() |
boolean |
True if standing on a solid surface. |
isSprinting() |
boolean |
True if the sprint flag is set. |
isSneaking() |
boolean |
True if sneaking/crouching. |
isSwimming() |
boolean |
True if in swim animation. |
isFlying() |
boolean |
True if creative flight is active. |
isCreativeFlightEnabled() |
boolean |
True if the player has fly ability (creative/spectator). |
isSubmergedInWater() |
boolean |
True if fully submerged. |
isInLava() |
boolean |
True if touching lava. |
| Method | Returns | Description |
|---|---|---|
getHealth() |
float |
Current health points (typically 0–20). |
getMaxHealth() |
float |
Maximum health points. |
getAbsorptionAmount() |
float |
Absorption (yellow hearts) amount. |
getFoodLevel() |
int |
Hunger level (0–20). |
getSaturation() |
float |
Saturation level. |
getAir() |
int |
Remaining air ticks (max 300). |
getMaxAir() |
int |
Maximum air ticks (typically 300). |
| Method | Returns | Description |
|---|---|---|
getXpLevel() |
int |
Current XP level. |
getXpProgress() |
float |
Progress toward next level (0.0–1.0). |
getTotalXp() |
int |
Total accumulated XP points. |
| Method | Returns | Description |
|---|---|---|
getName() |
String |
Player's Minecraft username. |
getUuid() |
String |
Player's UUID as a string. |
getGameMode() |
String |
"SURVIVAL", "CREATIVE", "ADVENTURE", "SPECTATOR", or "UNKNOWN". |
getHeldItem() |
ItemSnapshot |
Snapshot of the main-hand item. |
getOffhandItem() |
ItemSnapshot |
Snapshot of the off-hand item. |
getActiveEffects() |
List<EffectSnapshot> |
All active status effects. |
hasEffect(String idSubstring) |
boolean |
True if any active effect's ID contains the substring (case-insensitive). |
isInWorld() |
boolean |
True if the player entity exists in a loaded world. Use this as a guard at the top of tick handlers. |
isScreenOpen() |
boolean |
True if any GUI screen is open (inventory, chest, chat, etc.). |
Read inventory contents. Never modifies anything.
Inventory slot layout:
| Range | Contents |
|---|---|
| 0–8 | Hotbar |
| 9–35 | Main storage |
| 36 | Head armour |
| 37 | Chest armour |
| 38 | Leg armour |
| 39 | Feet armour |
| 40 | Off-hand |
| Method | Returns | Description |
|---|---|---|
getSelectedSlot() |
int |
Currently selected hotbar slot (0–8). |
getHotbarSlot(int slot) |
ItemSnapshot |
Snapshot of one hotbar slot. |
getHotbar() |
List<ItemSnapshot> |
Snapshots of all 9 hotbar slots. |
getHotbarSnapshot() |
List<ItemSnapshot> |
Identical to getHotbar(). Intended for change-detection by comparing with .equals(). |
hasItemInHotbar(String displayName) |
boolean |
True if any hotbar slot has an item matching the display name (case-insensitive exact match). |
hasItemIdInHotbar(String idSubstring) |
boolean |
True if any hotbar slot has an item whose ID contains the substring. |
findHotbarSlotByName(String displayName) |
int |
First matching slot index, or -1. |
findHotbarSlotById(String idSubstring) |
int |
First matching slot index by item ID, or -1. |
| Method | Returns | Description |
|---|---|---|
getMainSlot(int slot) |
ItemSnapshot |
Snapshot of any slot 0–35. |
getMainInventory() |
List<ItemSnapshot> |
All 36 main inventory slots. |
countByName(String displayName) |
int |
Total item count matching display name across all 36 slots. |
countById(String idSubstring) |
int |
Total item count matching ID substring across all 36 slots. |
hasEmptySlot() |
boolean |
True if at least one main inventory slot is empty. |
emptySlotCount() |
int |
Number of empty slots in the main inventory. |
| Method | Returns | Description |
|---|---|---|
getArmorSlot(int armorIndex) |
ItemSnapshot |
0=head, 1=chest, 2=legs, 3=feet. |
getArmor() |
List<ItemSnapshot> |
All 4 armour slots in order (head → feet). |
getOffhandItem() |
ItemSnapshot |
Off-hand slot (slot 40). |
Query the current world/dimension.
| Method | Returns | Description |
|---|---|---|
getDimensionId() |
String |
e.g. "minecraft:overworld", "minecraft:the_nether". |
isInOverworld() |
boolean |
|
isInNether() |
boolean |
|
isInEnd() |
boolean |
|
getServerAddress() |
String |
Hostname:port of the server, "local" for singleplayer, "none" if not connected. |
isSinglePlayer() |
boolean |
True if singleplayer or LAN. |
isWorldLoaded() |
boolean |
True if a world is currently loaded client-side. |
| Method | Returns | Description |
|---|---|---|
getTimeOfDay() |
long |
Ticks into the current day (0–24000). Day starts at 0, night at ~13000. |
getWorldAge() |
long |
Total world age in ticks (never resets). |
isDay() |
boolean |
True if time < 13000. |
isNight() |
boolean |
Inverse of isDay(). |
| Method | Returns | Description |
|---|---|---|
isRaining() |
boolean |
|
isThundering() |
boolean |
| Method | Returns | Description |
|---|---|---|
getBlockId(int x, int y, int z) |
String |
Namespaced block ID, e.g. "minecraft:grass_block". Returns "minecraft:air" if unloaded. |
getBlockIdRelative(int dx, int dy, int dz) |
String |
Block ID at an offset from the player's feet. |
isAir(int x, int y, int z) |
boolean |
True if the block is air. |
getSkyLight(int x, int y, int z) |
int |
Sky light level 0–15. |
getBlockLight(int x, int y, int z) |
int |
Block light level 0–15. |
getLight(int x, int y, int z) |
int |
Max of sky and block light. |
isChunkLoaded(int x, int z) |
boolean |
True if the chunk at world coords x,z is loaded. |
getBiomeId() |
String |
Biome at the player's current position. |
| Method | Returns | Description |
|---|---|---|
getNearbyEntities(double radius) |
List<EntitySnapshot> |
All living entities within radius of the player. Excludes self. Uses a bounding-box search. |
getNearbyPlayers(double radius) |
List<EntitySnapshot> |
Other player entities within radius. |
getNearbyEntitiesMatching(double radius, Predicate<EntitySnapshot> filter) |
List<EntitySnapshot> |
Filtered version of getNearbyEntities. |
getEntityAtCrosshair() |
Optional<EntitySnapshot> |
Entity under the crosshair (Minecraft's raycast target), or empty. |
getBlockAtCrosshair() |
Optional<BlockSnapshot> |
Block under the crosshair, or empty. Includes all block-state properties. |
getEntityNbt(int entityId) |
Map<String, String> |
Entity NBT as a flat string map. Currently returns an empty map — see Known Limitations. |
// Find all wheat crops within 10 blocks:
List<BlockSnapshot> wheat = WorldInfo.getNearbyBlocks(10.0, "minecraft:wheat");
// Custom filter — mature crops only:
List<BlockSnapshot> matureWheat = WorldInfo.getNearbyBlocksMatching(10.0,
b -> b.blockId().equals("minecraft:wheat") && b.stateEquals("age", "7"));Performance: getNearbyBlocks and getNearbyBlocksMatching scan a full sphere. Keep radius ≤ 32 to avoid frame drops. The scan is O(r³).
Query the tab-list player list. Servers use display names in the tab list to show sidebar information (scores, status text, etc.).
All methods call MinecraftClient.getNetworkHandler().getPlayerList() and return safe defaults when not connected.
Note on text stripping: Display names in the tab list are Minecraft Text objects. getAllLines() and search methods strip all formatting codes (colours, bold, etc.) and return plain strings. To inspect formatting (e.g. detect strikethrough), use isLineStrikethrough().
Returns all tab-list display-name strings, stripped of colour codes. Ordered by score (descending) then by username.
Returns all tab-list display-name strings. Intended for raw inspection, though note that Text.getString() in Fabric already strips §-style codes. Useful primarily to iterate PlayerListEntry objects when combined with other methods.
Returns the first stripped tab-list line that starts with prefix, or null.
String line = TabListInfo.findLineWithPrefix("Area:");
// → "Area: Garden"Returns all stripped lines starting with prefix.
Returns the first stripped tab-list line that contains substring (case-insensitive), or null.
String line = TabListInfo.findLineContaining("Alive:");
// → " Alive: 3 pests"Returns all stripped lines containing substring (case-insensitive).
Returns true if the first tab-list line containing substring has strikethrough styling applied to any of its text segments. Uses Text.visit() to walk the Style tree — the only reliable way to detect strikethrough since getString() strips all formatting.
// Detect a disabled/crossed-out line:
if (TabListInfo.isLineStrikethrough("Bonus Pest Chance:")) {
// line is crossed out → bonus is disabled
}Find the first line starting with prefix and return the text after the prefix, trimmed. Returns null if not found.
String area = TabListInfo.extractValueAfterPrefix("Area:"); // → "Garden"Extract multiple key-value pairs in one call. Returns a map of prefix → value (or null if not found).
Map<String, String> data = TabListInfo.parseKeyValueLines("Area:", " Alive:", " Repellent:");
String area = data.get("Area:");
String alive = data.get(" Alive:");Find the first integer after the prefix. Returns -1 if not found or not parseable.
int pestCount = TabListInfo.extractInt(" Alive:"); // → 3Returns true if any stripped tab-list line matches the given Java regex.
| Method | Returns | Description |
|---|---|---|
getPlayerUsernames() |
List<String> |
Minecraft account names of all players in the tab list. |
isPlayerOnline(String username) |
boolean |
True if a player with that username is in the tab list (case-insensitive). |
getPlayerCount() |
int |
Total number of entries in the tab list. |
All types are immutable Java records. They are safe to store, pass between methods, and compare with .equals(). They hold no live references to Minecraft objects.
com.playerapi.types.ItemSnapshot
record ItemSnapshot(
String itemId, // namespaced ID, e.g. "minecraft:diamond_hoe"
String displayName, // localised display name, e.g. "Diamond Hoe"
int count, // stack size
int damage, // damage/durability value
boolean enchanted // true if any enchantments are present
)Static fields:
ItemSnapshot.EMPTY— represents an empty/air slot.itemId = "minecraft:air",count = 0.
Methods:
isEmpty()— true ifitemIdis"minecraft:air"orcount == 0.
Factory:
ItemSnapshot.from(ItemStack stack)— create from a live stack. ReturnsEMPTYfor null/empty stacks.
ItemSnapshot held = PlayerInfo.getHeldItem();
if (!held.isEmpty() && held.displayName().contains("Hoe")) {
System.out.println("Holding: " + held); // "Wheat Hoe x1 [skyblock:wheat_hoe]"
}com.playerapi.types.EntitySnapshot
record EntitySnapshot(
int entityId, // client-side entity ID (not UUID)
String typeId, // namespaced entity type, e.g. "minecraft:zombie"
String displayName, // display name (may include custom name tags)
Vec3d position, // world position (x, y, z)
float health, // current health (0 for non-LivingEntity)
float maxHealth // max health (0 for non-LivingEntity)
)Note: entityId is the client-side integer ID, not the UUID. It changes on every world join. Do not persist entity IDs across sessions.
List<EntitySnapshot> nearby = WorldInfo.getNearbyEntities(16.0);
for (EntitySnapshot e : nearby) {
System.out.printf("%s at %.1f/%.1f/%.1f (%.0f/%.0f HP)%n",
e.displayName(), e.position().x, e.position().y, e.position().z,
e.health(), e.maxHealth());
}com.playerapi.types.BlockSnapshot
record BlockSnapshot(
int x,
int y,
int z,
String blockId, // namespaced ID, e.g. "minecraft:wheat"
Map<String, String> blockState // property name → value, e.g. {"age": "7"}
)Methods:
| Method | Description |
|---|---|
getState(String key) |
Returns the value of a block-state property, or null. |
hasState(String key) |
True if the property exists. |
stateEquals(String key, String value) |
True if the property value equals value (case-insensitive). |
Optional<BlockSnapshot> block = WorldInfo.getBlockAtCrosshair();
block.ifPresent(b -> {
System.out.println(b.blockId()); // "minecraft:wheat"
System.out.println(b.getState("age")); // "7"
System.out.println(b.stateEquals("age", "7")); // true (fully grown)
});Common block state properties:
| Block | Key | Values |
|---|---|---|
| Wheat / Carrot / Potato | age |
"0" – "7" (7 = fully grown) |
| Sugarcane | (no state; check block above) | — |
| Pumpkin / Melon stem | age |
"0" – "7" |
| Nether Wart | age |
"0" – "3" |
| Any log/plank | axis |
"x", "y", "z" |
com.playerapi.types.EffectSnapshot
record EffectSnapshot(
String effectId, // namespaced ID, e.g. "minecraft:speed"
String displayName, // localised name, e.g. "Speed"
int amplifier, // 0-indexed (amplifier 0 = Level I)
int remainingTicks // ticks remaining
)Methods:
level()— returnsamplifier + 1(1-indexed for display).remainingSeconds()— returnsremainingTicks / 20.0f.
for (EffectSnapshot e : PlayerInfo.getActiveEffects()) {
System.out.printf("%s %d — %.1fs remaining%n",
e.displayName(), e.level(), e.remainingSeconds());
}
boolean hasSpeed = PlayerInfo.hasEffect("speed");Used by MovementActions, HumanActions.pressKey/releaseKey/tapKey:
| Key Name | Description |
|---|---|
"forward" |
W key (walk forward) |
"back" |
S key (walk backward) |
"left" |
A key (strafe left) |
"right" |
D key (strafe right) |
"sprint" |
Left Ctrl (sprint modifier) |
"sneak" |
Left Shift (sneak/crouch) |
"jump" |
Space bar |
"attack" |
Left mouse button (break block / attack entity) |
"use" |
Right mouse button (use item / place block / interact) |
getEntityNbt(int entityId) always returns an empty map. The underlying entity.saveSelfNbt() API changed in Minecraft 1.21.x to require a RegistryManager argument, and the correct method name needs verification via ./gradlew genSources. This is a stub pending a fix in a future version.
Despite the name, getAllLinesRaw() still uses Text.getString() which strips §-style codes. Use isLineStrikethrough() when you need formatting information.
Scheduler.cancelAll() cancels tasks from all mods using PlayerAPI, not just the caller. If multiple mods are running simultaneously and one calls cancelAll(), the other's scheduled tasks are also cancelled. Design mods accordingly — avoid cancelAll() if other mods may be relying on scheduled tasks.
Infinite repeating tasks self-reschedule after firing. The token returned by scheduleRepeating(period, 0, task) can only cancel the currently pending invocation. If the task fires before you cancel, it will reschedule itself and continue. Use a shared flag inside the callback to implement clean infinite-loop cancellation:
boolean[] running = {true};
Scheduler.scheduleRepeating(20, 0, () -> {
if (!running[0]) return; // effectively stops the loop
doSomething();
});
// To stop: running[0] = false;Calling pressKey() without calling setActive(true) first has no effect. The mixin ignores the key state tracker when not active. Always call setActive(true) in your bot's start method and setActive(false) / releaseAll() in the stop method.
PlayerAPIEvents.BLOCK_BROKEN fires via ClientPlayerBlockBreakEvents.AFTER. For survival-mode blocks that require hold-to-mine, this fires after server confirmation, not immediately on the client. For instant-break blocks (crops, flowers, tall grass), it fires immediately.