A Minecraft: Pocket Edition 0.15.10 (protocol 84) server written in plain Java, with no external dependencies. It implements the MCPE network protocol and game logic in Java and includes a plugin API. Some mechanics may still differ from the original game.
Beta: the server is already playable, but some features are still missing or unfinished (for example, skeletons do not raise their bow arm yet).
Two jars are published:
| Jar | Purpose |
|---|---|
server.jar |
The server itself (java -jar server.jar) |
plugin-api.jar |
Compile-only jar used to build your plugins. Do not put it in plugins/ |
- Java 11 or newer (on Termux:
pkg install openjdk-17) - A Minecraft PE 0.15.10 client
java -jar server.jarRun it from the folder where you want the server files to live.
- A short wizard asks for a language (
eng,spa,por,fra,deu,ita). Press Enter for English. The choice is saved aslanguage=inserver.properties. - The wizard asks
Do you want the guided setup and tutorial? [Y/n](in the language you picked). Y (or Enter) walks you through 14 steps with a progress bar and an explanation for each: server name (MOTD), port, game mode, difficulty, max players, world name, world type, seed, view distance, PvP, mob spawns, whitelist, spawn protection and your OP name. Enter keeps the default shown in[brackets]. It ends with a summary and quick usage tips. N skips it and keeps the factory settings. server.propertiesis created with default values.- The world is generated (
Preparing start region...) and the console showsDone.
The console prints the name and version on startup, for example:
Starting BlockNes 1.5.0 beta version 0.15.10 (MCPE protocol 84)
Stop the server with /stop or Ctrl+C (both save players, worlds and items).
| Path | Purpose |
|---|---|
server.properties |
Server configuration |
ops.txt |
Operators (one name per line) |
players/ |
One file per player (<name>.dat): position, health, inventory. Also enderchests.dat (ender chests) |
banned-players.txt |
Banned player names, one per line. Created at startup; managed with ban and pardon |
banned-ips.txt |
Banned IP addresses, one per line. Managed with ban-ip and pardon-ip |
white-list.txt |
White list names, one per line. Used when white-list=on |
worlds/world/ |
Overworld: level.dat, region/ (chunks), items.dat, mobs.dat, xp.dat, tiles.dat, seed.txt, time.txt. Worlds from older versions (world/, world_nether/) are moved here automatically on the first start |
worlds/nether/ |
Nether: level.dat, region/ (chunks) |
plugins/ |
Plugin .jar files, plugin data folders and permissions.properties |
server.log |
Log file (rotated to server.log.old at about 5 MB) |
Missing keys are added automatically with their default value the next time the server starts.
| Key | Default | Description |
|---|---|---|
server-name |
BlockNes Server |
Internal name, used when motd is empty |
motd |
My Minecraft server |
Text shown in the server list |
server-port |
19132 |
UDP port (1-65535) |
gamemode |
survival |
survival or creative (0 / 1) |
difficulty |
1 |
0 peaceful, 1 easy, 2 normal, 3 hard (names work too) |
max-players |
20 |
1-1000 |
spawn-protection |
0 |
Protected radius around spawn in blocks (OPs ignore it) |
view-distance |
8 |
Chunks sent to each player (2-24) |
level-name |
world |
World folder name |
level-seed |
(empty) | Seed; empty means random. Once seed.txt exists in the world folder, it wins |
level-type |
normal |
normal, flat or nether |
language |
(set by wizard) | eng, spa, por, fra, deu, ita |
pvp |
on |
off: players cannot hit other players |
spawn-mobs |
on |
off: no natural mob spawns (spawn eggs and /summon still work) |
white-list |
off |
on: only the names in white-list.txt can join |
pregenerate-radius |
0 |
Chunks around spawn generated at startup, before players join (0-16). 0 only prepares the spawn area. New chunks may get animals on grass |
Invalid or out-of-range values fall back to a safe default and a warning is printed.
The server only writes default values into a new
server.properties. If you already have one, editmotdthere yourself.
Commands work from the console (with or without /) and in-game chat (starting with /). Most commands are OP-only; add yourself with /op <name> in the console or by editing ops.txt.
| Command | Description |
|---|---|
/help |
List commands (includes plugin commands) |
/version |
Server version |
/list |
Online players |
/say <message> |
Broadcast a message |
/me <action> |
Action message |
/tell <player> <message> |
Private message |
/kick <player> [reason] |
Kick a player |
/gamemode <0|1> [player] |
Change game mode |
/tp [player] <x y z | player> |
Teleport |
/time <set|add|query|stop|start> [value] |
Time control (set accepts day, noon, sunset, night, midnight, sunrise) |
/setblock <x> <y> <z> <id> [meta] |
Place a block |
/give <player> <id> [amount] [meta] |
Give items |
/enchant <player> <enchantment> [level] |
Enchant the item in hand, e.g. /enchant Steve sharpness 5 |
/weather <clear|rain|thunder|storm> [ticks] |
Change the overworld weather (/weather rain 600); without ticks it lasts a random time |
/summon <mob> [player] |
Spawn a mob next to a player |
/kill [player] / /heal [player] |
Kill / heal |
/op <player> / /deop <player> |
Manage operators |
/save-all |
Save everything now |
/status |
Server status |
/debug [on|off] |
Show every damage / hunger change in your chat |
/plugins |
List loaded plugins |
/plugins reload |
Reload plugins (OP only) |
ban <player> / pardon <player> |
Ban or pardon a player name (banned players are kicked) |
ban-ip <ip> / pardon-ip <ip> |
Ban or pardon an IP address |
banlist |
List banned players and IP addresses |
whitelist <on\|off\|add\|remove\|list\|reload> [player] |
Turn the white list on or off, edit it, or reload it |
/stop |
Save and shut down |
On Linux, macOS, or Termux with JDK 11 or newer, run:
sh test.sh # plugin API, lifecycle, HUD, menus, recipes, and examples
sh test-network.sh # longer UDP/RakNet integration testTest plugin JARs are built in temporary directories; the scripts do not overwrite or remove installed plugins. Detailed logs are saved in test-results/.
Plugins are .jar files placed in plugins/. They are loaded on startup and on /plugins reload. A plugin that crashes never takes the server down.
The API version is 1.5 (server().getApiVersion()). Plugins built for earlier versions of this API keep loading, because the API only adds to what was there. Version 1.2 adds p.getPing() and the text above the head (p.setNameTag(), see Player); a plugin that uses them declares api=1.2.
Version 1.3 adds the access-control and server-setting helpers listed below (banPlayer, whitelistAdd, isPvpEnabled, ...). A plugin that declares api=1.3 needs this version or newer; plugins for older versions keep loading.
Version 1.5 adds menus with buttons, recipes added by plugins, regions, per-player data, item names and lore, and argument helpers for commands (see API 1.5 additions). A plugin that uses them declares api=1.5; plugins for older versions keep loading.
Limits. A handler that throws 100 times, or takes more than 100 ms in 1000 calls, disables its plugin; the server keeps running. A plugin can have up to 5000 pending tasks; more runLater or runRepeating calls throw an IllegalStateException.
Compile your plugin against plugin-api.jar; the finished plugin jar goes in plugins/. A plugin jar needs a plugin.properties file in its root and a main class that extends mcpe.plugin.Plugin and has a public no-argument constructor.
name=HelloWorld
main=example.HelloWorld
version=1.0
author=You
description=What the plugin does
# everything below is optional
website=https://example.com
prefix=HW
api=1.1
depend=RequiredPlugin,AnotherOne
softdepend=OptionalPlugin
loadbefore=SomePluginauthor/authors:authors=You,Analists several people;author()returns them joined with,.depend: these plugins must be loaded first, otherwise your plugin is not loaded.softdepend: loaded before yours if they exist.loadbefore: your plugin is loaded before these, when they are present.prefix: the text in brackets at the start of your log lines (default: the plugin name).api: the minimum API version your plugin needs. This server has API1.5; a plugin that asks for a newer one is not loaded and a warning is printed.permissions.<node>.…andcommands.<name>.…declare permissions and commands (see below).- An optional
config.propertiesin the jar root is copied toplugins/<Name>/config.propertieson first run.
public class HelloWorld extends Plugin {
@Override public void onLoad() { } // jar loaded, nothing enabled yet
@Override public void onEnable() { } // register events, commands and tasks here
@Override public void onDisable() { } // server stopping or /plugins reload
}Events, commands, tasks, permissions and attachments registered by a plugin are removed automatically after onDisable(). Its menus are closed (their onClose runs), its recipes are removed and its player data is saved.
Plugins can be switched individually without restarting: /plugins disable <name> (also disables plugins that depend on it) and /plugins enable <name> (OP only; re-reads the jar). /plugins reload still reloads everything.
Register with on(Events.X.class, e -> ...). Handlers run on the server thread.
on(Events.PlayerJoin.class, e -> message(e.player, "Welcome " + e.player.name));
on(Events.BlockPlace.class, Priority.HIGH, true, e -> { if (e.id == 46) e.setCancelled(true); });Overloads: on(type, handler), on(type, priority, handler), on(type, priority, ignoreCancelled, handler).
Priority order: LOWEST, LOW, NORMAL, HIGH, HIGHEST, MONITOR (last; observe only).
| Event | Cancelable | Fields / notes |
|---|---|---|
PlayerJoin |
no | player |
PlayerQuit |
no | player |
PlayerChat |
yes | message (editable), format (optional String.format pattern taking the name and the message, e.g. "<%s> %s"; null = normal format), recipients (editable list; everyone logged in by default) |
PlayerCommand |
yes | line (text starting with /) |
PlayerMove |
yes | fromX/Y/Z, toX/Y/Z; fires when the block changes |
PlayerInteract |
yes | air, x, y, z, face, item |
PlayerDamage |
yes (not forced damage) | amount (editable), cause, forced, attacker |
PlayerDeath |
no | message (null = none), keepInventory |
PlayerRespawn |
no | player |
PlayerGamemode |
yes | from, to |
BlockBreak |
yes | x, y, z, id, meta |
BlockPlace |
yes | x, y, z, id, meta |
MobDamage |
yes | mob, by (may be null), amount (editable) |
MobDeath |
no | mob, killer, drops (editable list) |
MobSpawn |
yes | mob, natural |
ItemPickup |
yes | item |
ServerTick |
no | tick (20 per second) |
PlayerPreLogin |
yes | kickMessage (editable); cancelling kicks the player before joining |
PlayerKick |
yes | reason (editable) |
EntityCombust |
yes | eid, player (null for mobs), cause, seconds. Cancelled = it does not catch fire |
PlayerDropItem |
yes | item; cancelling gives the item back |
PlayerItemConsume |
yes | item (eating / drinking) |
PlayerBedEnter / PlayerBedLeave |
yes / no | x, y, z |
PlayerHeal |
yes | amount (editable) |
PlayerExhaust |
yes | amount (editable): how much food and saturation the exertion costs |
PlayerAnimation |
yes | action (1 = arm swing); cancelling hides it from the other players |
PlayerItemHeld |
no | item, slot, hotbarSlot; fires after the player changes the item in hand |
CraftItem |
yes | input (ingredients about to be used), result; cancelling uses no ingredients and gives no result |
PlayerToggleSneak / PlayerToggleSprint |
yes | sneaking / sprinting |
InventoryOpen |
yes | type (chest, enderchest, workbench, furnace), x, y, z |
InventoryClose |
no | player |
MenuClick |
yes | menu, slot, item (what the slot held), proposed (what the client tried to put there). The change is always reverted; cancelling skips the slot's handler (API 1.5) |
SignChange |
yes | x, y, z, lines (4, editable) |
Explosion |
yes | dimension, x, y, z, size, blocks ({x,y,z,id} list; remove entries to save blocks) |
ServerCommand |
yes | console only: sender, line (editable) |
PacketReceive |
yes | id, data (raw packet from a player, before the server handles it) |
PacketSend |
yes | data (raw packet sent through Server.send) |
LevelSave |
no | fired after every world save |
PluginEnable / PluginDisable |
no | plugin |
Instead of one on(...) per event you can group handlers in a class and register them all at once:
public class MyListener {
@EventHandler(priority = Priority.HIGH, ignoreCancelled = true)
public void onBreak(Events.BlockBreak e) { e.setCancelled(true); }
@EventHandler
public void onJoin(Events.PlayerJoin e) { /* ... */ }
}
// in onEnable():
registerEvents(new MyListener()); // returns how many handlers were registeredEach handler method takes exactly one parameter (an Event). Methods that don't are skipped with a warning.
unregisterEvents(listener) removes the handlers of that object and returns how many were removed; unregisterAllEvents() removes every handler this plugin registered.
You can listen to a base class (Events.PlayerEvent or Event) to receive every event of that kind, and fire your own events with fire(new MyEvent()) (any subclass of Event).
// opOnly = true: only OPs and the console
command("hello", "greets you", "", false, (sender, args) -> sender.send("Hello " + sender.name()));
// protected by a permission node (OPs always pass)
command("announce", "message to all", "<text ...>", "hello.announce", (s, a) -> broadcast(String.join(" ", a)));sender has name(), isOp() and send(String). Check a node manually with hasPermission(sender, "node").
Aliases: alias("hello", "hi", "hey") makes /hi and /hey run the same command (/help lists it once).
Run commands from code: dispatchCommand(sender, "give Steve 1 1"); console() returns an OP sender that writes to the log.
plugins/permissions.properties:
# player=groups and/or permissions
Steve=vip,hello.use
# group.<name>=permissions; "-x" denies; "@group" includes another group
group.vip=pos.others,-tnt.place,@default
# the "default" group applies to everybody
group.default=hello.useWildcards: * and a.*. OPs have every permission.
For a command with its own state, extend Command. It handles the usage line, the aliases and the permission:
import mcpe.command.CommandSender;
import mcpe.plugin.Command;
// in onEnable():
registerCommand(new Command("greet", "greets someone", "<name>", "hi") {
@Override public boolean execute(CommandSender sender, String label, String[] args) {
if (args.length == 0) return false; // false = show the usage line
sender.send("Hello " + args[0]);
return true;
}
});setPermission("hello.use") ("" = everyone, "op" = operators only), setPermissionMessage(...), setDescription(...), setUsage(...) and setAliases(...) change the command after it is created. getCommand("greet") returns it and unregisterCommand("greet") removes it.
Commands, aliases and permissions can also be declared in plugin.properties. Declared descriptions, usages and permissions are used for whatever the code leaves empty, and declared aliases are added to the ones the code gives:
commands.greet.description=greets someone
commands.greet.usage=<name>
commands.greet.aliases=hi,hey
commands.greet.permission=hello.use
commands.greet.permission-message=You cannot use that
permissions.hello.use.description=use /greet
permissions.hello.use.default=true
permissions.vip.default=op
permissions.vip.children=hello.use,-hello.adminpermissions.<node>.default:op(only operators, the default),notop(everyone except operators),true(everyone) orfalse(nobody).childrenapply to whoever has the parent permission. Above, anyone withvipgetshello.useand is deniedhello.admin(-denies).
The same thing in code:
registerPermission("hello.use", "use /greet", PermissionDefault.TRUE);
registerPermission("vip", "VIP perks", PermissionDefault.OP)
.addChild("hello.use", true)
.addChild("hello.admin", false);Players who are not operators get a permission from the first of these that says something about it:
- attachments made with
addAttachment(the last matching one wins); plugins/permissions.properties(the player's own line and their groups);- the children of parent permissions the player has;
- the default of the registered permission. Unregistered nodes are denied.
Operators and the console have every permission. hasPermission(player, "node") checks all of the above.
PermissionAttachment a = addAttachment(player); // or addAttachment(player, "hello.admin", true)
a.setPermission("hello.admin", true).setPermission("hello.use", false);
a.unsetPermission("hello.admin"); // back to the lower levels
removeAttachment(a); // or a.remove()Attachments are removed when the player leaves and when the plugin is disabled or reloaded.
Task t = runLater(100, () -> broadcast("5 seconds later")); // 20 ticks = 1 second
runRepeating(0, 6000, () -> broadcast("Every 5 minutes"));
t.cancel();
runAsync(() -> { /* HTTP, files... never touch players or worlds here */ });
runAsync(() -> fetchSomething(), result -> message(player, result)); // result arrives on the server thread
runAsync(() -> { String data = download(); runSync(() -> broadcast(data)); }); // runSync: back to the server thread from ANY thread
cancelTasks(); // cancels every task of this plugin
t.id(); t.isRepeating(); t.owner(); // Task infoFor work that must not block the server, extend AsyncTask. onRun() runs on another thread (never touch players or worlds there), then onCompletion() runs on the server thread:
int id = runAsync(new AsyncTask() {
@Override public void onRun() {
setResult(download("https://example.com/data.txt")); // your own code, off the server thread
}
@Override public void onCompletion() {
broadcast("Got: " + getResult()); // back on the server thread
}
});
isTaskQueued(id); // still pending?
cancelTask(id); // see the rules below- A cancelled task never runs
onCompletion(). If it is cancelled before it starts,onRun()does not run either. - If
onRun()throws, the error is logged,isCrashed()becomestrueandonCompletion()still runs. cancelTask(id)andisTaskQueued(id)work for both kinds of task;getTask(id)returns a scheduledTask. Pending tasks of a plugin are cancelled when it is disabled.
config() returns plugins/<Name>/config.properties (a java.util.Properties subclass):
Config cfg = config();
String s = cfg.getString("welcome", "Hi");
int n = cfg.getInt("limit", 10); // also getLong, getDouble, getBoolean
cfg.set("limit", 20).save();
reloadConfig(); // re-read from disk
Path file = saveResource("data.txt"); // copy a resource from the jar if missing
Path folder = dataFolder(); // plugins/<Name>/
cfg.setDefault("limit", 10).save(); // only writes the key if it doesn't exist yet
cfg.has("limit");
cfg.setList("worlds", List.of("a", "b")); // stored as "a,b"
List<String> w = cfg.getStringList("worlds"); // [a, b]
List<String> kits = cfg.keys("kits."); // kits.vip, kits.basic ... (sorted)Config also has remove(key), getAll() and getKeys(). saveDefaultConfig() copies the jar's config.properties if none exists, getConfig() is the same as config(), and getResource("name") opens a file from the plugin's own jar (close it when you are done).
For structured data, jsonConfig("data.json") gives a JsonConfig stored in the plugin folder, addressed with dotted paths:
JsonConfig json = jsonConfig("data.json");
json.set("homes.steve.x", 10).set("homes.steve.z", -4).save(); // creates the nested objects
int x = json.getInt("homes.steve.x", 0); // also getString, getLong, getDouble, getBoolean, getList
json.setDefault("maxHomes", 3).save(); // only if the key is missing
json.remove("homes.steve");
json.reload(); // read the file from disk againMCPE 0.15.10 has no title, boss bar or scoreboard packets, so the API emulates them on the two text channels the client does have: tip (scoreboard and titles) and popup (above the hotbar). It redraws every 10 ticks, only resends when something changed, and removes everything a plugin registered when that plugin is disabled.
// scoreboard: one line per key, sorted by order; a global line is evaluated for every player
hudLine("hp", 1, p -> "\u00a7cHP: " + p.health);
hudLine(player, "money", 2, "\u00a76$" + balance); // per-player line; text = null removes it
hudLine(player, "combat", 3, "\u00a7cIn combat", 200); // NEW: disappears by itself after 200 ticks
hudRemove("hp"); hudRemove(player, "money"); hudClear(player);
List<String> shown = hudLines(player); // NEW: scoreboard lines the player sees now (no padding)
title(player, "\u00a7aWelcome", "\u00a77enjoy", 60); // center, 60 ticks (covers the scoreboard while it lasts)
actionBar(player, "\u00a7eSaved!", 40); // above the hotbar (covers boss bars while it lasts)
bossBar(player, "dragon", "\u00a7cEnder Dragon", 0.65); // text + 24-segment progress bar; same key = update
removeBossBar(player, "dragon");
broadcastTitle("\u00a76Round 2", "fight!", 40); // NEW: to everybody
broadcastActionBar("\u00a7eServer restarts in 5 min", 100);
clearTitle(player); // NEW: remove the active title and action bar
String[] onScreen = hudView(player); // {popup, tip} currently shown (logical text, no padding)Events.PlayerHud fires per player on every redraw (only if someone listens): edit lines (the scoreboard) or set popup, so several plugins can add to the same HUD without knowing each other.
The client centers every line of the tip channel, so the scoreboard used to be in the middle of the screen. Now each line is padded with spaces (4 px each) so the whole block sits at the right edge by default; all lines start at the same x. The width of each line is computed with the default Minecraft font widths (color codes ignored, bold counts +1 px per letter).
Setting (server.properties) |
Default | Meaning |
|---|---|---|
hud-position |
right |
right, left or center (the old behaviour) |
hud-screen-width |
427 |
Screen width in GUI pixels used to find the edge. If the text is cut off or too far from the edge, change it (480, 640...) |
hud-offset-y |
0 |
Move the scoreboard up (+) or down (-) this many lines |
hud-max-lines |
15 |
Lines beyond this are dropped |
Every player can tune it live with /hud (no OP needed), because GUI size differs per device:
| Command | What it does |
|---|---|
/hud |
Hide / show the scoreboard |
/hud right | left | center |
Position |
/hud width <px> |
Screen width used for the alignment (160..2000) |
/hud y <n> |
Move up (+) or down (-), -20..20 |
/hud reset |
Back to the server defaults |
For plugins: hudPosition(p, HudPosition.LEFT), hudVisible(p, false), hudScreenWidth(p, 640), hudOffsetY(p, 2) (and the matching getters). Titles are always centered. These per-player settings are not saved to disk (they reset when the player leaves).
It is still an emulation: the tests check the text the server sends (including the padding math), not how a real 0.15.10 client draws it. Padding is based on assumed font widths and screen size, so check it on a real client and adjust hud-screen-width / /hud y once. The left position relies on trailing spaces being kept by the client (they are followed by \u00a7r just in case); right only uses leading spaces. The boss bar is text, not the real bar.
api=1.4 in plugin.properties is only needed if you use these. Everything is additive: plugins compiled against 1.3 keep working.
| Method | Description |
|---|---|
color("&aHi <here") / stripColor(s) |
& codes to \u00a7 codes / remove them (also Text.color, Text.strip) |
messagef(p, "&aYou have %d coins", n) / broadcastf(...) |
String.format + colors, to one player / everybody |
runRepeating(delay, period, task -> ...) |
Like runRepeating but the body receives its Task so it can task.cancel() itself |
runTimes(delay, period, times, i -> ...) |
Runs times times (index 0..times-1) and cancels itself |
countdown(5, s -> broadcast("Starting in " + s), () -> start()) |
One call per second with the remaining seconds, then done |
once(Events.X.class, e -> ...) |
Listen to an event only once (also with a Priority) |
onlineCount() / playerNames() / isOnline(name) |
Quick player queries |
playersNear(dim, x, y, z, r) / nearestPlayer(dim, x, y, z, max) / mobsNear(dim, x, y, z, r) |
Entities in a radius, nearest first |
teleportChecked(p, x, y, z) |
Teleport through Events.PlayerTeleport (returns false if another plugin cancelled it) |
setWeather(type, ticks) |
Weather.SUNNY / RAINY / RAINY_THUNDER / THUNDER; goes through Events.WeatherChange |
strikeLightning(x, y, z) / giveXp(p, n) / setTimeStopped(b) |
Small server helpers |
kickAll(reason, exceptPermission) |
Kick everyone except players with that permission |
hudLine(p, key, order, text, ttlTicks), hudLines, hudPosition, hudVisible, hudScreenWidth, hudOffsetY, broadcastTitle, broadcastActionBar, clearTitle |
See the HUD section |
New events: Events.PlayerTeleport (cancelable, toX/toY/toZ editable; fired by /tp and teleportChecked, not by the server's internal teleports) and Events.WeatherChange (cancelable, type/ticks editable; fired by /weather and setWeather, not by random weather).
New helper classes: Text (color, strip, width, bar(progress, length, "&a", "&8"), ticks, seconds, number, truncate) and Cooldowns (tryUse(key, ms), remainingSeconds(key), reset).
api=1.5 in plugin.properties is only needed if you use these. Everything is additive, so plugins compiled against 1.4 keep working. plugins-ejemplo/Tienda uses most of them.
A menu is a chest window with 3 or 6 rows (27 or 54 slots). Each slot holds an item and, optionally, a click handler. Items in a menu are buttons: when a player tries to take or move an item, the change is reverted and the handler runs.
Menu shop = menu(3); // 3 rows (27 slots); menu(6) gives 54
shop.border(new Item(160, 7, 1)); // glass around the edge
shop.setItem(11, new Item(297, 0, 1).withName(color("&ePan")), e -> {
give(e.player, new Item(297, 0, 1)); // the event has player, menu, slot, item and proposed
});
shop.setItem(15, new Item(35, 14, 1).withName(color("&cClose")), e -> closeMenu(e.player));
shop.onClose(p -> message(p, "Thanks for visiting"));
openMenu(player, shop);- Every change a client makes to a menu slot is reverted, so an item never ends up in the wrong place. A slot without a handler just reverts.
Events.MenuClickcan be cancelled. Cancelling it skips the slot's handler; the change is still reverted.- While a menu is open the player's inventory is frozen: the client cannot move, drop or craft items. That is what makes it impossible to lose or duplicate an item through a menu. Closing the menu unfreezes it.
onCloseruns when the player closes the menu, whencloseMenuis called, when the player disconnects, and when the plugin that created the menu is disabled.- Opening a menu (or a chest) closes the window the player had open before.
setItemand the other changes update the screen of every player who has the menu open. Call them from the server thread.- The client shows no title for these menus.
- To show the window, the client needs a chest where the server says it is. So while a menu is open, that player alone sees a chest two blocks above their head; it is removed when the menu closes. The world on the server is not changed.
| Method | Description |
|---|---|
menu(rows) |
New menu with 3 or 6 rows (other values throw IllegalArgumentException) |
openMenu(p, menu) / closeMenu(p) / isMenuOpen(p) |
Open it for a connected player / close it (runs onClose) / check |
setItem(slot, item) / setItem(slot, item, handler) |
Put an item (air empties the slot), with an optional click handler |
getItem(slot) / rows() / size() / owner() |
Read the menu |
fill(item) / border(item) / clear() |
Bulk changes; the slots they change lose their handlers |
onClose(handler) / viewers() / refresh() |
Close callback, players who have it open, resend the whole content |
Item sword = new Item(276, 0, 1).withName(color("&bSword of the cave"))
.withLore(Arrays.asList(color("&7Found deep underground"), color("&7Sharp")));
sword.name(); // the name with its § codes, or null when the item has no name
sword.lore(); // the lines, or an empty listNames and lore are saved with the item and sent to the client as NBT (display). Limits: 128 characters per name or line, and 16 lore lines. They are part of the item's identity: items with different names do not stack, and the duplicate protection counts them as different items.
Map<Character, Item> ing = new HashMap<>();
ing.put('T', new Item(5, 0, 1)); // planks
ing.put('P', new Item(280, 0, 1)); // stick
addShapedRecipe(new Item(278, 0, 1).withName(color("&bShop pickaxe")), new String[]{"TTT", " P ", " P "}, ing);
addShapelessRecipe(new Item(1, 0, 4), Arrays.asList(new Item(3, 0, 2), new Item(4, 0, 2)));- Shaped rows have up to 3 characters, and
' 'is an empty cell. Empty borders are trimmed, so{"AA ", "A "}is a 2x2 recipe. Every character must be in the ingredients map. - Shapeless recipes use up to 9 units in total.
- An ingredient with damage
-1matches any damage value, for example every kind of log. - Recipes added by a plugin are removed when it is disabled.
removeRecipe(recipe)removes any recipe, built-in ones included (server().recipes.alllists them). When a built-in recipe and a plugin recipe give the same result, the built-in one is tried first. - Furnace recipes are not part of this API yet.
- Each recipe added while players are online sends the whole recipe list to each of them again. When you add many, do it in
onEnable, before players join.
Region r = region(0, 10, 60, 10, 12, 62, 12); // corners in any order, inclusive; dimension 0 = overworld, 1 = nether
r.fill(1, 0); // stone; returns how many blocks were written
r.replace(1, 4, 0); // stone -> cobblestone; returns how many were replaced
int cobble = r.count(4);
Region.Snapshot copy = r.copy(); // ids and metas kept in memory
region(0, 40, 60, 10, 42, 62, 12).paste(copy, 40, 60, 10, true); // true = skip air: keep what is there where the copy is airA region holds up to 1,000,000 blocks; larger ones throw IllegalArgumentException. Blocks outside the world height are skipped. Other methods: contains(x, y, z), volume(), width(), height(), depth(), and minX() to maxZ(). Snapshots have width(), height(), depth(), idAt(dx, dy, dz) and metaAt(dx, dy, dz).
JsonConfig stats = playerData().of(player); // or of("Steve")
stats.set("kills", stats.getInt("kills", 0) + 1);Each player gets a JSON file at plugins/<Name>/players/<name>.json, with the same methods as jsonConfig (dotted paths, getInt, setDefault, ...). Names are lowercased, so "Steve" and "steve" share one file. The data is saved when the player disconnects and when the plugin is disabled. saveAll() saves now, exists(name) checks for the file, and unload(name) saves the player's data and drops it from memory.
mcpe.plugin.Args reads command arguments. A missing or invalid value returns the default, so a bad number never breaks a command:
| Method | Description |
|---|---|
Args.getInt(args, i, def) / Args.getInt(args, i, def, min, max) |
Integer; with a range, values outside it return def |
Args.getDouble(args, i, def) / Args.getBoolean(args, i, def) |
Finite decimal / true, on, si, yes, 1 and false, off, no, 0 |
Args.get(args, i, def) / Args.has(args, i) / Args.join(args, from) |
Text / whether it exists / the words from a position, joined with spaces |
| Method | Description |
|---|---|
server() |
The Server instance (server().isOp(player) etc.) |
players() / player(name) |
Connected players / find one by name |
broadcast(msg) / message(player, msg) |
Chat messages |
popup(player, msg) / tip(player, msg) |
Text above the hotbar / small text at screen center |
world(dim) |
World by dimension (0 overworld, 1 nether) |
getBlock(dim, x, y, z) / setBlock(dim, x, y, z, id, meta) |
Read / place blocks (clients are updated) |
spawnMob(dim, netId, x, y, z) |
Spawn a mob (Mob.ZOMBIE, Mob.COW, ...); null if unknown |
registerService(Type.class, impl) / service(Type.class) |
Share an API between plugins |
plugin(name) / isEnabled() |
Get another loaded plugin / is this plugin still active |
kick(p, reason) / teleport(p, x, y, z) |
Kick (goes through PlayerKick) / move a player |
heal(p, n) / damage(p, n, cause) / kill(p) |
Health (go through PlayerHeal / PlayerDamage) |
setGamemode(p, gm) / give(p, item) |
Game mode (PlayerGamemode) / give an item (returns what didn't fit) |
isOp(p) / setOp(name, bool) |
OP list |
time() / setTime(t) / saveAll() / maxPlayers() |
World time, save to disk, slot count |
getLogger() |
info, notice, warning, error, critical, alert, emergency, debug (only with -Dblocknes.debug=true) and logException(t); lines start with [prefix] |
log(msg) / logWarn(msg) |
Console output prefixed with your plugin name |
playerExact(name) / playersIn(dim) |
Exact-name lookup / players in one dimension |
broadcast(msg, permission) / broadcastTip(msg) / broadcastPopup(msg) |
Chat message only to players with the permission / tip or popup to everyone |
hasPermission(player, node) |
Permission check (see Who has which permission) |
banPlayer(name) / pardonPlayer(name) / isBanned(name) |
Ban (kicks the player) / pardon / check a name (banned-players.txt) |
banIp(ip) / pardonIp(ip) / isBannedIp(ip) |
Ban (kicks players from that IP) / pardon / check an IP (banned-ips.txt) |
whitelistAdd(name) / whitelistRemove(name) / isWhitelisted(name) |
Edit / check the white list (white-list.txt) |
setWhitelist(on) / isWhitelistOn() |
Turn the white list on or off / check it |
isPvpEnabled() / isSpawnMobsEnabled() |
The pvp and spawn-mobs settings from server.properties |
mobsIn(dim) / highestBlockAt(dim, x, z) |
Mobs in a dimension / top block at x, z |
dropItem(dim, x, y, z, item) / explode(dim, x, y, z, size) / playLevelEvent(dim, x, y, z, id, data) |
Drop an item / explosion / a level event (sound or particle effect) |
getFile() / dataFolder() / pluginManager() / plugins() |
Your jar / your folder / the plugin manager / loaded plugins |
server().getServerVersion() / getGameVersion() / getProtocolVersion() / getApiVersion() |
Name and build (e.g. BlockNes 1.5.0 beta), 0.15.10, 84, 1.5 |
server().onlinePlayers() / getOps() / getConfigString(key, def) / getConfigInt / getConfigBoolean |
Players, operator names, values from server.properties |
name(), version(), author(), description() |
Plugin metadata |
Colors use the Minecraft § codes (\u00a7a in Java strings).
Player objects come from events, commands and players():
| Call | Description |
|---|---|
p.getName() / getDisplayName() / setDisplayName(n) |
Login name / name shown in chat |
p.getHealth() / getMaxHealth() / getFoodLevel() / getSaturation() |
Health (0-20), hunger |
p.getX() / getY() / getZ() / getYaw() / getPitch() |
Position and look direction |
p.getDimension() / getWorld() / getGamemode() |
Dimension, world, game mode |
p.getItemInHand() / getInventory() |
Item in hand, inventory |
p.isOnline() / isDead() / isSleeping() / isOp() / setOp(true) |
State and operator flag |
p.getAddress() / getUniqueId() |
IP address and UUID |
p.sendMessage(m) / sendPopup(m) / sendTip(m) |
Messages |
p.kick(reason) / teleport(x, y, z) / setGamemode(gm) / heal(n) / damage(n, cause) / kill() / setHealth(v) |
Same as the helpers above. Health changes go through PlayerHeal / PlayerDamage, so they can be cancelled |
p.getPing() |
Round-trip latency in ms (RakNet ping every second). -1 until the first answer, and again after 5 s without one |
p.getNameTag() / setNameTag(text) |
Text above the head, seen by the players who see p. null = the real name; at most 256 characters |
p.isNameTagVisible() / setNameTagVisible(bool) |
Show or hide that text |
p.getPing() is measured with RakNet ping/pong: the server sends a ping every second and times the answer. It stays -1 until an answer arrives, and goes back to -1 if nothing comes back for 5 seconds.
The text above the head is the nametag. This example shows each player's name and latency, refreshed every second:
runRepeating(0, 20, () -> {
for (Player p : server().onlinePlayers()) {
p.setNameTag(p.getName() + " - " + (p.getPing() < 0 ? "?" : p.getPing() + " ms"));
}
});setNameTag sends the change only to the players who see p, and only when the text really changes. The tests of this release check what the server sends, not how a real client draws it; check the nametag on a real 0.15.10 client before relying on its layout.
Compile against plugin-api.jar, add plugin.properties (and an optional config.properties) to the jar, and put the result in plugins/:
mkdir -p out plugins
javac -source 11 -target 11 -encoding UTF-8 -cp plugin-api.jar -d out $(find src -name '*.java')
cp plugin.properties config.properties out/
jar --create --file plugins/MyPlugin.jar -C out .Restart the server or run /plugins reload. If javac / jar are not available, use java -m jdk.compiler/com.sun.tools.javac.Main and java -m jdk.jartool/sun.tools.jar.Main instead.
package example;
import mcpe.plugin.Events;
import mcpe.plugin.Plugin;
public class HelloWorld extends Plugin {
@Override public void onEnable() {
on(Events.PlayerJoin.class, e -> message(e.player, "Welcome, " + e.player.name + "!"));
command("hello", "greets you", "", false, (s, a) -> s.send("Hello " + s.name() + "!"));
log("ready");
}
}plugin.properties:
name=HelloWorld
main=example.HelloWorld
version=1.0
author=You
description=Greets players