- Introduction to PocketMine-MPz
- Setting Up Your Development Environment
- Creating Your First Plugin
- Plugin Structure
- Plugin.yml File
- Main Plugin Class
- Events and Event Handling
- Commands
- Permissions
- Config Files
- Forms API
- Database Integration
- Best Practices
- Publishing Your Plugin
- Advanced Topics
PocketMine-MP (PMMP) is a server software for Minecraft: Bedrock Edition written in PHP. It allows developers to create custom plugins to extend the functionality of Minecraft servers.
A plugin is a package of code that adds new features or modifies existing ones in the PMMP server. Plugins can:
- Add new commands
- Create custom items and blocks
- Modify game mechanics
- Add mini-games
- Implement economy systems
- And much more!
- PHP 8.0 or newer
- Composer (PHP package manager)
- A code editor (VS Code, PHPStorm, etc.)
- PocketMine-MP server for testing
-
Install PHP:
- Windows: Download from windows.php.net
- Linux:
sudo apt install php8.0-cli php8.0-xml php8.0-mbstring php8.0-gd php8.0-curl
- macOS:
brew install php
-
Install Composer:
- Download and install from getcomposer.org
-
Download PocketMine-MP:
- Get the latest release from pmmp.io
- Extract the files to a directory
-
Setup IDE:
- Install a PHP-compatible IDE like Visual Studio Code or PHPStorm
- Install PHP extensions/plugins for your IDE
Let's create a simple "Hello World" plugin that welcomes players when they join.
MyFirstPlugin/
├── src/
│ └── YourName/
│ └── MyFirstPlugin/
│ └── Main.php
└── plugin.yml
The plugin.yml
file contains metadata about your plugin:
name: MyFirstPlugin
main: YourName\MyFirstPlugin\Main
version: 1.0.0
api: 5.0.0
author: YourName
description: My first PocketMine-MP plugin
<?php
declare(strict_types=1);
namespace YourName\MyFirstPlugin;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\plugin\PluginBase;
use pocketmine\utils\TextFormat;
class Main extends PluginBase implements Listener {
protected function onEnable(): void {
$this->getServer()->getPluginManager()->registerEvents($this, $this);
$this->getLogger()->info("MyFirstPlugin has been enabled!");
}
protected function onDisable(): void {
$this->getLogger()->info("MyFirstPlugin has been disabled!");
}
public function onPlayerJoin(PlayerJoinEvent $event): void {
$player = $event->getPlayer();
$player->sendMessage(TextFormat::GREEN . "Welcome to the server, " . $player->getName() . "!");
}
}
A typical PMMP plugin follows this structure:
PluginName/
├── resources/ # Config files, assets, etc.
│ └── config.yml
├── src/
│ └── Author/
│ └── PluginName/
│ ├── Main.php
│ ├── commands/ # Command classes
│ ├── listeners/ # Event listener classes
│ └── utils/ # Utility classes
├── plugin.yml # Plugin metadata
└── README.md # Documentation
The plugin.yml
file is required for all plugins and contains essential metadata:
name: PluginName # Plugin name
main: Author\PluginName\Main # Main class (namespace\class)
version: 1.0.0 # Plugin version
api: 5.0.0 # PMMP API version
author: YourName # Author name
description: Plugin description # Short description
website: https://example.com # Optional website
depend: [AnotherPlugin] # Required plugins
softdepend: [OptionalPlugin] # Optional plugins
# Commands (optional)
commands:
examplecmd:
description: Example command
usage: "/examplecmd <argument>"
permission: pluginname.command.example
aliases: [excmd, ecmd]
# Permissions (optional)
permissions:
pluginname.command.example:
description: Allows using the example command
default: op
The main class of your plugin must extend PluginBase
and is the entry point for your plugin:
<?php
declare(strict_types=1);
namespace Author\PluginName;
use pocketmine\plugin\PluginBase;
class Main extends PluginBase {
protected function onEnable(): void {
// Called when the plugin is enabled
$this->saveDefaultConfig(); // Saves default config.yml if it doesn't exist
// Register commands
$this->getServer()->getCommandMap()->register("pluginname", new ExampleCommand($this));
// Register event listeners
$this->getServer()->getPluginManager()->registerEvents(new EventListener($this), $this);
$this->getLogger()->info("Plugin has been enabled!");
}
protected function onDisable(): void {
// Called when the plugin is disabled
$this->getLogger()->info("Plugin has been disabled!");
}
}
Events are triggered when something happens in the game. Your plugin can listen for these events and respond accordingly.
<?php
declare(strict_types=1);
namespace Author\PluginName\listeners;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\event\player\PlayerQuitEvent;
use pocketmine\event\block\BlockBreakEvent;
use Author\PluginName\Main;
class EventListener implements Listener {
private Main $plugin;
public function __construct(Main $plugin) {
$this->plugin = $plugin;
}
/**
* @param PlayerJoinEvent $event
* @priority NORMAL
*/
public function onPlayerJoin(PlayerJoinEvent $event): void {
$player = $event->getPlayer();
// Do something when a player joins
}
/**
* @param PlayerQuitEvent $event
*/
public function onPlayerQuit(PlayerQuitEvent $event): void {
$player = $event->getPlayer();
// Do something when a player leaves
}
/**
* @param BlockBreakEvent $event
* @priority HIGH
* @ignoreCancelled true
*/
public function onBlockBreak(BlockBreakEvent $event): void {
// This will only be called if the event wasn't cancelled by another plugin
// And it will run after NORMAL priority listeners but before MONITOR
$player = $event->getPlayer();
$block = $event->getBlock();
// Cancel the event to prevent the block from breaking
// $event->cancel();
}
}
PlayerJoinEvent
- When a player joins the serverPlayerQuitEvent
- When a player leaves the serverPlayerChatEvent
- When a player sends a chat messageBlockBreakEvent
- When a player breaks a blockBlockPlaceEvent
- When a player places a blockEntityDamageEvent
- When an entity takes damagePlayerInteractEvent
- When a player interacts with a block or air
Commands allow players to interact with your plugin through text input.
<?php
declare(strict_types=1);
namespace Author\PluginName\commands;
use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\player\Player;
use pocketmine\plugin\Plugin;
use pocketmine\plugin\PluginOwned;
use pocketmine\utils\TextFormat;
use Author\PluginName\Main;
class ExampleCommand extends Command implements PluginOwned {
private Main $plugin;
public function __construct(Main $plugin) {
parent::__construct("example", "An example command", "/example <argument>", ["ex", "examplecmd"]);
$this->setPermission("pluginname.command.example");
$this->plugin = $plugin;
}
public function execute(CommandSender $sender, string $commandLabel, array $args): bool {
if (!$this->testPermission($sender)) {
return false;
}
if (!$sender instanceof Player) {
$sender->sendMessage(TextFormat::RED . "This command can only be used in-game");
return false;
}
if (count($args) < 1) {
$sender->sendMessage(TextFormat::RED . "Usage: " . $this->getUsage());
return false;
}
// Command logic here
$sender->sendMessage(TextFormat::GREEN . "You used the example command with argument: " . $args[0]);
return true;
}
public function getOwningPlugin(): Plugin {
return $this->plugin;
}
}
Permissions control what players can and cannot do on your server.
permissions:
pluginname:
default: false
description: Root permission for all plugin features
children:
pluginname.command:
default: false
description: Permission for all commands
children:
pluginname.command.example:
default: op
description: Permission for the example command
pluginname.feature:
default: true
description: Permission to use a specific feature
op
: Only operators have this permission by defaulttrue
: Everyone has this permission by defaultfalse
: No one has this permission by defaultnot op
: Everyone except operators have this permission by default
if ($player->hasPermission("pluginname.command.example")) {
// Player has permission
} else {
// Player doesn't have permission
$player->sendMessage(TextFormat::RED . "You don't have permission to use this command!");
}
Config files allow you to make your plugin customizable without changing the code.
Create a file named config.yml
in the resources
directory:
# Example config.yml
settings:
enable-feature: true
cooldown: 10
messages:
welcome: "Welcome to the server!"
goodbye: "Goodbye!"
protected function onEnable(): void {
// Save default config if it doesn't exist
$this->saveDefaultConfig();
// Get values from config
$enableFeature = $this->getConfig()->get("settings.enable-feature", true);
$welcomeMessage = $this->getConfig()->get("messages.welcome", "Welcome!");
// You can also reload the config
$this->reloadConfig();
}
// Save a custom config file from resources
$this->saveResource("custom.yml");
// Load a custom config file
$customConfig = new Config($this->getDataFolder() . "custom.yml", Config::YAML);
$value = $customConfig->get("key", "default");
// Save changes to custom config
$customConfig->set("key", "new value");
$customConfig->save();
Forms provide a graphical interface for players to interact with your plugin.
To use forms, you'll need the FormAPI library. You can include it in your plugin using Composer:
{
"require": {
"jojoe77777/formapi": "^2.1"
}
}
use jojoe77777\FormAPI\SimpleForm;
public function sendSimpleForm(Player $player): void {
$form = new SimpleForm(function (Player $player, ?int $data) {
if ($data === null) {
// Form was closed without selecting an option
return;
}
switch ($data) {
case 0:
$player->sendMessage("You selected Option 1");
break;
case 1:
$player->sendMessage("You selected Option 2");
break;
}
});
$form->setTitle("Example Form");
$form->setContent("Please select an option:");
$form->addButton("Option 1", 0, "textures/ui/icon1");
$form->addButton("Option 2", 0, "textures/ui/icon2");
$form->sendToPlayer($player);
}
use jojoe77777\FormAPI\CustomForm;
public function sendCustomForm(Player $player): void {
$form = new CustomForm(function (Player $player, ?array $data) {
if ($data === null) {
// Form was closed
return;
}
// $data[0] is the first input (name)
// $data[1] is the dropdown selection
// $data[2] is the slider value
// $data[3] is the toggle state
$player->sendMessage("Name: " . $data[0]);
$player->sendMessage("Option: " . $data[1]);
$player->sendMessage("Value: " . $data[2]);
$player->sendMessage("Enabled: " . ($data[3] ? "Yes" : "No"));
});
$form->setTitle("Settings Form");
$form->addInput("Name", "Enter your name", "Steve");
$form->addDropdown("Select Option", ["Option A", "Option B", "Option C"]);
$form->addSlider("Select Value", 1, 100, 5, 50);
$form->addToggle("Enable Feature", true);
$form->sendToPlayer($player);
}
PMMP plugins can use various database systems to store data.
private \SQLite3 $db;
protected function onEnable(): void {
// Create database directory if it doesn't exist
@mkdir($this->getDataFolder() . "data/");
// Connect to SQLite database
$this->db = new \SQLite3($this->getDataFolder() . "data/database.db");
// Create tables if they don't exist
$this->db->exec("CREATE TABLE IF NOT EXISTS players (
uuid TEXT PRIMARY KEY,
name TEXT,
coins INTEGER DEFAULT 0,
last_seen INTEGER
)");
}
public function savePlayer(Player $player, int $coins): void {
$stmt = $this->db->prepare("INSERT OR REPLACE INTO players (uuid, name, coins, last_seen) VALUES (:uuid, :name, :coins, :time)");
$stmt->bindValue(":uuid", $player->getUniqueId()->toString(), SQLITE3_TEXT);
$stmt->bindValue(":name", $player->getName(), SQLITE3_TEXT);
$stmt->bindValue(":coins", $coins, SQLITE3_INTEGER);
$stmt->bindValue(":time", time(), SQLITE3_INTEGER);
$stmt->execute();
}
public function getPlayerCoins(string $uuid): int {
$stmt = $this->db->prepare("SELECT coins FROM players WHERE uuid = :uuid");
$stmt->bindValue(":uuid", $uuid, SQLITE3_TEXT);
$result = $stmt->execute();
if ($row = $result->fetchArray(SQLITE3_ASSOC)) {
return (int) $row["coins"];
}
return 0;
}
protected function onDisable(): void {
// Close database connection
if (isset($this->db)) {
$this->db->close();
}
}
private ?\mysqli $db = null;
protected function onEnable(): void {
// Load database config
$config = $this->getConfig();
$host = $config->get("mysql.host", "localhost");
$user = $config->get("mysql.user", "root");
$pass = $config->get("mysql.password", "");
$dbname = $config->get("mysql.database", "minecraft");
$port = $config->get("mysql.port", 3306);
// Connect to MySQL database
try {
$this->db = new \mysqli($host, $user, $pass, $dbname, $port);
if ($this->db->connect_error) {
throw new \Exception("Connection failed: " . $this->db->connect_error);
}
// Create tables if they don't exist
$this->db->query("CREATE TABLE IF NOT EXISTS players (
uuid VARCHAR(36) PRIMARY KEY,
name VARCHAR(16) NOT NULL,
coins INT DEFAULT 0,
last_seen INT
)");
$this->getLogger()->info("Database connection established");
} catch (\Exception $e) {
$this->getLogger()->error("Database connection failed: " . $e->getMessage());
$this->getServer()->getPluginManager()->disablePlugin($this);
}
}
protected function onDisable(): void {
// Close database connection
if ($this->db !== null) {
$this->db->close();
}
}
-
Separate concerns: Use different classes for different functionalities
- Commands in a
commands
namespace - Event listeners in a
listeners
namespace - Data models in a
models
namespace
- Commands in a
-
Use namespaces properly: Follow PSR-4 autoloading standards
-
Document your code: Use PHPDoc comments to document methods and classes
-
Avoid heavy operations in event handlers: Events can be called frequently
-
Use async tasks for database operations:
$this->getServer()->getAsyncPool()->submitTask(new DatabaseTask($data));
-
Cache frequently accessed data: Avoid repeated database queries
-
Use scheduled tasks for periodic operations:
$this->getScheduler()->scheduleRepeatingTask(new MyTask($this), 20 * 60); // Run every minute (20 ticks * 60)
-
Check if a plugin exists before using it:
if ($this->getServer()->getPluginManager()->getPlugin("EconomyAPI") !== null) { // EconomyAPI is loaded }
-
Use proper dependency management in plugin.yml:
depend: [EconomyAPI] # Plugin won't load without EconomyAPI softdepend: [WorldGuard] # Plugin will load after WorldGuard if available
-
Create documentation:
- README.md with installation and usage instructions
- Wiki for detailed documentation (optional)
-
License your plugin:
- Choose an appropriate license (MIT, GPL, etc.)
- Include a LICENSE file
-
Create a release:
- Package your plugin as a .phar file
- Use tools like DevTools or PharBuilder
-
Share your plugin:
- Poggit - Official PMMP plugin repository
- GitHub - Source code hosting
- PMMP Forums - Community forums
Virions are reusable libraries that can be included in your plugins.
-
Using virions:
- Include the virion in your plugin using a tool like Poggit
- Use the classes from the virion in your code
-
Creating virions:
- Create a separate project with reusable code
- Follow the virion structure guidelines
use pocketmine\item\ItemFactory;
use pocketmine\item\ItemIdentifier;
use pocketmine\item\ItemIds;
// Register a custom item
$factory = ItemFactory::getInstance();
$factory->register(new CustomItem(new ItemIdentifier(ItemIds::DIAMOND_SWORD, 100), "Custom Sword"), true);
use pocketmine\scheduler\Task;
class MyTask extends Task {
private Main $plugin;
public function __construct(Main $plugin) {
$this->plugin = $plugin;
}
public function onRun(): void {
// This code runs when the task executes
$this->plugin->getLogger()->info("Task executed!");
}
}
// Schedule a task to run once after 20 ticks (1 second)
$this->getScheduler()->scheduleDelayedTask(new MyTask($this), 20);
// Schedule a task to run every 20 ticks (1 second)
$this->getScheduler()->scheduleRepeatingTask(new MyTask($this), 20);
use pocketmine\scheduler\AsyncTask;
use pocketmine\Server;
class MyAsyncTask extends AsyncTask {
private string $data;
public function __construct(string $data) {
$this->data = $data;
}
public function onRun(): void {
// This runs in a separate thread
$result = $this->performHeavyOperation($this->data);
$this->setResult($result);
}
public function onCompletion(): void {
// This runs in the main thread after the task completes
$result = $this->getResult();
Server::getInstance()->getLogger()->info("Async task completed with result: " . $result);
}
private function performHeavyOperation(string $data): string {
// Simulate a heavy operation
sleep(5);
return "Processed: " . $data;
}
}
// Submit an async task
$this->getServer()->getAsyncPool()->submitTask(new MyAsyncTask("test data"));
This tutorial has covered the basics of PocketMine-MP plugin development. As you become more comfortable with these concepts, you can create increasingly complex and feature-rich plugins.
Remember to:
- Test your plugins thoroughly
- Keep up with PMMP API changes
- Join the PMMP community for support and collaboration
- Share your plugins with others
Happy coding!
- PocketMine-MP Documentation
- PocketMine-MP GitHub
- Poggit - Plugin repository and CI service
- PMMP Forums - Community forums
- Discord Server - Official PMMP Discord server