Skip to content

About

RaspberryJuice — modernized: a Bukkit/Spigot plugin implementing the Minecraft Pi socket API (mcpi) for Paper 1.21 / Java 21. Revived fork of zhuowei/RaspberryJuice.

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

NOTE: This is a modernized fork of the (end-of-life) upstream zhuowei/RaspberryJuice. It has been updated to build and run on the latest Paper 26.2 servers and Java 25, verified end-to-end. Existing Python mcpi scripts keep working unchanged: legacy numeric block/entity ids are bridged to the modern BlockData API via Bukkit's built-in legacy conversion. See docs/modernization-notes.md for details.

RaspberryJuice

A Bukkit/Paper plugin which implements the Minecraft Pi Socket API, letting Python (and other) mcpi clients drive Minecraft over a simple TCP protocol.

New: a friendly, typed Python client lives in client/ — pip install raspberryjuice, then from raspberryjuice import Minecraft. It covers blocks, the turtle agent, entity/mob control, and world/player commands. See client/README.md.

The plugin is listed as RaspberryJuice Reloaded on Modrinth and Hangar, and both Python clients are on PyPI.

Quickstart

  1. Install the plugin. Grab raspberryjuice-*.jar from Modrinth, Hangar, or the latest release (or build it — see Build) and drop it in your Paper 26.2 / Java 25 server's plugins/ folder, then start the server. It listens on localhost:4711 by default.

  2. Install the Python client.

    pip install raspberryjuice   # or, from a clone: pip install ./client
  3. Drive Minecraft from Python.

    from raspberryjuice import Minecraft
    
    mc = Minecraft.connect("localhost", 4711)
    mc.post_to_chat("Hello from Python!")
    mc.set_block(0, 10, 0, 41)        # place a gold block
    print(mc.get_block(0, 10, 0))     # -> 41

On a shared or networked server, read SECURITY.md first — set an auth-token and keep the port on localhost (or a tunnel). Also see CONTRIBUTING.md and the CHANGELOG.

Commands

Commands supported

  • world.get/setBlock
  • world.getBlockWithData
  • world.setBlocks
  • world.getPlayerIds
  • world.getBlocks
  • chat.post
  • events.clear
  • events.block.hits
  • player.getTile
  • player.setTile
  • player.getPos
  • player.setPos
  • world.getHeight
  • entity.getTile
  • entity.setTile
  • entity.getPos
  • entity.setPos

Commands that can't be supported

  • Camera angles

Extra commands

Beyond the core mcpi calls above, these are registered too (the definitive list is buildCommandRegistry() in RemoteSession.java). Note the namespaced names (world.spawnEntity, events.projectile.hits, …) — the old bare/poll* forms are gone.

Blocks & world

  • world.getBlocks(x1,y1,z1,x2,y2,z2) - read a whole cuboid at once (respects max-blocks and max-chunks-per-tick)
  • world.getHeight(x,z) - the highest block y at a column
  • world.setSign(x,y,z,blockTypeId,data,line1,line2,line3,line4) - place a sign
    • Wall signs (id 68) take facing data: 2=north, 3=south, 4=west, 5=east
    • Standing signs (id 63) take rotation data 0-15: 0=south, 4=west, 8=north, 12=east

Players & entities (mutation is owner-scoped — see Entity/mob control)

  • world.getPlayerId(name) - the entity id of an online player by name
  • entity.getName(id) - the name for an entity id (reverse of getPlayerId)
  • world.getEntities([typeId]) - loaded entities, optionally filtered by type id
  • world.getEntityTypes() - the entity types this server supports
  • world.spawnEntity(x,y,z,typeId) - spawn an entity and return its id (your session owns it). Respects max-entities-per-session; over the cap the command returns Fail and does not spawn.
  • world.removeEntity(id) / world.removeEntities([typeId]) - remove entities you spawned
  • player.getEntities(dist,typeId) / player.removeEntities(dist,typeId) - near the bound player
  • entity.getEntities(id,dist,typeId) / entity.removeEntities(id,dist,typeId) - near an entity
  • player.getAbsPos() / player.setAbsPos(x,y,z) - absolute position, ignoring the location config
  • {player,entity}.getDirection/setDirection, getRotation/setRotation, getPitch/setPitch - read/aim facing

Session identity

  • setPlayer(name[,token]) - bind this connection to an online player (scopes player.* and the event streams; Python: mc.set_player("Alice")). With player-tokens configured the bind is fail-closed and needs that player's token — see SECURITY.md.

Text with commas and pipes

  • protocol.escape(1|0) - turn escaping of free text on or off for this connection. Replies 1; an older server replies Fail. Once on, \, \| and \\ stand for , | and \ inside a value, both in arguments (chat, sign lines, names) and in replies (chat events, projectile names, entity.getName). Both Python clients turn it on automatically. Without it, the classic protocol is unchanged.

Teacher commands (off unless classroom-teacher-token is set)

  • classroom.freeze(name,token) / classroom.unfreeze(name,token) / classroom.reset(name,token) - socket forms of /rj freeze, /rj unfreeze and /rj reset. Reply 1 when the work ran, else Fail. Three wrong tokens close the connection.

Event polls (each drains the events queued since the last poll)

  • events.block.hits / events.chat.posts / events.projectile.hits - global (subject to allow-global-events)
  • player.events.block.hits / .chat.posts / .projectile.hits - scoped to the bound player
  • entity.events.block.hits / .chat.posts / .projectile.hits - scoped to an entity
  • events.clear / player.events.clear / entity.events.clear - clear queued events

Entity/mob control

Drive spawned entities (use world.spawnEntity to create one and get its id). Only the connection that spawned an entity may control it (per-session ownership), so one client can't move or kill another client's mobs - reads are open, mutation is owner-only:

  • entity.moveTo(id,x,y,z) - walk a mob to a point using real pathfinding (mobs only)
  • entity.lookAt(id,x,y,z) - turn the entity to face a point
  • entity.getHealth(id) - the entity's current health (or Fail if not a living entity)
  • entity.setHealth(id,health) - set health, clamped to [0, max]
  • entity.setName(id,name) - set a visible name tag. Everything after the id is the name, commas included.
  • entity.setAI(id,0|1) - disable/enable the mob's AI (freeze or free it)

World & player control

  • world.setTime(ticks) / world.getTime() - set/get the time of day (0-24000; ticks forward)
  • world.setWeather(0|1|2) - 0 clear, 1 rain, 2 thunder
  • world.clone(x1,y1,z1,x2,y2,z2,dx,dy,dz) - copy a cuboid to a destination corner (respects max-blocks)
  • player.setGameMode(0|1|2|3) - survival / creative / adventure / spectator
  • player.give(blockId[,count]) - give the current player blocks (default 1)

Reactive events

Poll these (like the other events.* calls) to react to what players do - "when X, do Y":

  • events.player.moves - positions the player moved into since the last poll (x,y,z,name)
  • events.block.breaks - blocks players broke (x,y,z,blockId,name)
  • events.block.places - blocks players placed (x,y,z,blockId,name)
  • events.player.deaths - player deaths (x,y,z,name)

Agent commands (turtle)

A per-session, code-driven agent (a "turtle"): drive it with relative commands to move, turn, and place blocks - a hands-on way to teach sequencing, loops, and functions. Movement is grid-aligned teleport-step. Commands before agent.spawn answer Fail. While a classroom sandbox is on (plots non-empty), those fire-and-forget moves stay silent instead, so a Fail cannot desync the next query. agent.getPos still answers Fail until the agent exists.

  • agent.spawn() / agent.spawn(x,y,z) - create the agent at the player (or a given block); one per session
  • agent.despawn() - remove the agent
  • agent.getPos() - the agent's block position (relative coords)
  • agent.getRotation() - facing as a cardinal yaw: 0=south, 90=west, 180=north, 270=east
  • agent.forward(n) / agent.back(n) - move n blocks along/against facing (n optional, default 1)
  • agent.up(n) / agent.down(n) - move n blocks vertically
  • agent.turnLeft() / agent.turnRight() - rotate the facing 90°
  • agent.setBlock(id[,data]) - place a block at the agent's position (id 0 clears to air)

Example: a loop that draws a 4×4 square outline is just for _ in range(4): [conn.send(f"agent.setBlock(1)\nagent.forward()\n") ...]; conn.send("agent.turnRight()\n").

Note - extra features are NOT guaranteed to be maintained in future releases, particularly if updates are made to the original Pi API which replace the functionality

Config

Modify config.yml:

  • hostname: - address to bind to. Defaults to localhost (loopback only) because the API socket is unauthenticated. Set to 0.0.0.0 to accept connections from any host on the network - only do this on a trusted/firewalled network (a security warning is logged when you do).
  • port: 4711 - the default tcp port can be changed in config.yml
  • location: RELATIVE - determine whether locations are RELATIVE to the spawn point (default like pi) or ABSOLUTE
  • hitclick: RIGHT - determine whether hit events are triggered by LEFT clicks, RIGHT clicks or BOTH
  • max-blocks: 1000000 - maximum blocks a single getBlocks/setBlocks may span; oversized requests are rejected. 0 disables the cap.
  • max-blocks-per-tick: 10000000 - cumulative blocks all cuboid ops (getBlocks/setBlocks/clone) may touch in one server tick; bounds a flood of near-cap requests that max-blocks alone can't. 0 disables the per-tick budget (not recommended).
  • max-chunks-per-tick: 256 - distinct chunk columns one session may touch in a single tick (getBlock/setBlock/getBlocks/getHeight/spawnEntity/teleport/…). Stops a scatter of far coordinates from force-generating thousands of chunks on the main thread. Already-touched chunks in the same tick are free. This rates generation; it does not cap eventual world size. 0 disables the cap.
  • max-sessions: 100 - maximum concurrent socket sessions; connections beyond this are refused. Raise it for a large classroom; 0 disables the cap.
  • max-entities-per-session: 1000 - maximum entities one socket session may spawn via world.spawnEntity over the life of the connection (a lifetime count: removeEntity does not free a slot). Over the cap the command returns Fail. Raise it for a lesson that spawns many mobs; lower it on a crowded classroom server. 0 disables the cap.
  • max-connections-per-minute: 60 - maximum new connections one remote IP may open per minute. Bounds connection-flood churn and backs the auth/setPlayer brute-force lockouts (an attacker can't just reconnect for more guesses). 0 disables per-IP rate limiting.
  • welcome-message: true - broadcast a "Welcome <player>" message on join. Set false to stay silent.
  • enable-op-commands: true - allow the power commands player.setGameMode / player.give. Set false on a shared/survival server so a socket client can't self-grant creative mode or items.
  • allow-global-events: false - by default the reactive event streams (events.player.moves / block.breaks / block.places / player.deaths) report only the session's OWN player's activity. A connection picks its player with setPlayer(<name>) (mc.set_player("Alice")); on a multi-player server an unbound connection receives no events at all (fail closed), so no client is handed an arbitrary player's feed. Set true to instead broadcast every player's events to every socket (a live tracking feed) - only on a trusted single-user server where you want whole-world/region triggers. See SECURITY.md.
  • auth-token: '' - optional shared secret. When set, clients must send auth(<token>) before any other command (Minecraft.connect(host, port, token="…") in the Python client). Empty = no auth. The socket is unencrypted, so tunnel the port for confidentiality - see SECURITY.md.
  • player-tokens: - optional per-player bind secrets (a name: secret map). Empty (default) = setPlayer(<name>) binds by name, unchanged. Non-empty = fail closed: setPlayer(<name>,<token>) (Python: mc.set_player("Alice", token="…")) succeeds only for a listed player whose token matches, so on a multi-user server a client can bind to (and observe) only the player it holds a token for. Repeated wrong tokens close the connection. See SECURITY.md.
  • plots: {} - per-student build plots, six inclusive integers in the same space as location (minX,minY,minZ,maxX,maxY,maxZ), keyed by the setPlayer name. Empty (default) = no spatial sandbox. A non-empty map is fail-closed: the socket may read and write only inside that player's plot, and an unbound session no longer falls through to the host player. Cross-plot getBlocks / spawnEntity return Fail; setBlock, setBlocks, clone, and teleports stay silent. By hand, breaking, placing, or bucketing a block that sits in another student's plot is cancelled; gaps and a shared overlap stay allowed. Flint and steel and spawn eggs are not stopped. Overlapping plots are shared. This is not max-chunks-per-tick (that only rates chunk generation). Set enable-op-commands: false when you turn plots on. See SECURITY.md.
  • sandbox-lock-world-rules: true - while plots is on, world.setTime and world.setWeather are rejected (they change the world for every student). Ignored when plots is empty. Set false for a lesson about the clock or the weather.
  • sandbox-max-blocks: 0 - tighter single-cuboid cap while plots is on (min with max-blocks when both are positive). 0 = no extra cap. Ignored when plots is empty.
  • sandbox-max-blocks-per-tick: 0 - tighter per-tick cuboid budget while plots is on. 0 = no extra cap. A class of 30 should set this well under the 10000000 default (for example 20000).
  • sandbox-max-commands-per-tick: 0 - commands drained per tick per session while plots is on. 0 keeps the built-in 9000. Read when the socket connects. 500 logs Over 500 commands when a tick defers the rest.
  • /rj freeze <player> and /rj unfreeze <player> - teacher command (raspberryjuice.classroom.teacher, default op; console is allowed). Freezes that online player's socket: reads, event polls, and agent turns still run, and every other command keeps its usual reply without changing the world. The player can still walk and build by hand. Freeze survives a reconnect and clears on restart. An offline or unknown name is refused. See SECURITY.md.
  • /rj reset <player> - same permission. Sets that configured plot to air, removes non-player entities inside it (including a shared overlap), and discards that student's agent. The student may be offline. One name per call. A plot over 100000 blocks, or over a tighter positive max-blocks / sandbox-max-blocks, is refused and changes nothing. There is no undo, and the entity cap is not refunded. Back up the world first. See SECURITY.md.
  • classroom-teacher-token: - shared secret for the socket teacher commands (classroom.freeze / unfreeze / reset). Empty (default) = they answer Fail. The socket is not encrypted, so most classrooms should leave it empty and use /rj. No commas or pipes in the token. See SECURITY.md.

Libraries

Two Python clients ship with this repo:

  • client/ — a new, typed raspberryjuice package with a clean modern API (see Quickstart). Best for writing new lessons.
  • mcpi-client/ — a modern, drop-in mcpi package so existing Minecraft-Pi curriculum (Adventures in Minecraft, Raspberry Pi tutorials) runs unchanged: from mcpi.minecraft import Minecraft. Best for running material that's already written.

For reference, the original classic mcpi Java/Python libraries (as modded for RaspberryJuice) are also bundled at src/main/resources/mcpi.

Build

Requires JDK 25 (Paper 26.2 is compiled to Java 25 bytecode). A Maven Wrapper is included, so you don't need a system Maven install.

git clone https://github.com/sakebomb/RaspberryJuice
cd RaspberryJuice
./mvnw package        # use mvnw.cmd on Windows

The plugin jar is produced at target/raspberryjuice-*.jar; drop it in your Paper server's plugins/ directory. Run the test suite with ./mvnw test. There's also an end-to-end scripts/smoke_test.py you can run against a live server.

Version history

  • 2.1.1 - first release on Modrinth, Hangar and PyPI; opt-in protocol.escape keeps commas and pipes in chat, signs and names (entity.setName keeps commas for every client); typed event records in the Python client; classroom sandbox state logged at startup. See CHANGELOG.
  • 2.1.0 - programmable education platform: turtle agent, entity/mob control, world/player control, reactive events, and a typed Python client (client/); security hardening (per-session entity ownership incl. bulk removal, auth-token handshake, per-player setPlayer tokens + brute-force lockout, enable-op-commands, per-tick block budget, bounded socket I/O). See CHANGELOG.
  • 2.0.0 - modernized fork: runs on Paper 26.2 / Java 25 (down through Paper 1.21); block layer ported off pre-1.13 numeric-ID APIs to Material/BlockData via a legacy-ID bridge (protocol unchanged); Adventure chat/sign APIs; security & concurrency hardening; JUnit 5 + MockBukkit test suite and CI
  • 1.12.1 - hostname specified in config.yml
  • 1.12 - getEntities, removeEntities, pollProjectileHits, events calls by player and entity
  • 1.11 - spawnEntity, setDirection, setRotation, setPitch
  • 1.10.1 - bug fixes
  • 1.10 - left, right, both hit clicks added to config.yml & fixed minor hit events bug
  • 1.9.1 - minor change to improve connection reset
  • 1.9 - relative and absolute positions added to config.yml
  • 1.8 - minecraft version 1.9.2 compatibility
  • 1.7 - added pollChatPosts() & block update performance improvements
  • 1.6 - added getPlayerId(playerName), getDirection, getRotation, getPitch
  • 1.5 - entity functions
  • 1.4.2 - bug fixes
  • 1.4 - bug fixes, port specified in config.yml
  • 1.3 - getHeight, multiplayer, getBlocks
  • 1.2 - added world.getBlockWithData
  • 1.1.1 - block hit events
  • 1.1 - Initial release

Contributors

About

RaspberryJuice — modernized: a Bukkit/Spigot plugin implementing the Minecraft Pi socket API (mcpi) for Paper 1.21 / Java 21. Revived fork of zhuowei/RaspberryJuice.

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages