Writing a plugin
Write a plugin, and see how the server loads, enables, and runs it.
A plugin is a small package the server loads at startup. You write it against the Endstone API, not against Bedrock's binary - so when Minecraft updates, Endstone updates its hooks underneath and your plugin keeps working, no recompile required. That stability is the whole point of building here instead of patching BDS directly.
You can write plugins in Python or C++. Both load from the server's plugins/ folder and reach the exact same API surface; they differ only in how you build and ship them.
What a plugin is
Every plugin is a class that extends Plugin and overrides the lifecycle methods the server calls for you. This is a complete, loadable plugin - nothing is hidden:
from endstone.plugin import Plugin
class MyPlugin(Plugin):
def on_enable(self) -> None:
self.logger.info("Hello from MyPlugin!")#include <endstone/endstone.hpp>
class MyPlugin : public endstone::Plugin {
public:
void onEnable() override
{
getLogger().info("Hello from MyPlugin!");
}
};
ENDSTONE_PLUGIN("my_plugin", "0.1.0", MyPlugin) {}A little metadata - your plugin's name, version, and the API version it targets - tells the server how to load it. In Python that's an entry point in pyproject.toml; in C++ it's the ENDSTONE_PLUGIN macro. Each track's setup guide walks through it.
on_enable is one of several lifecycle methods the server calls as it hands your plugin control while it starts, runs, and stops.
How the server runs it
Once every plugin is built, the server drives each one through the same three states. These map exactly to the methods you override (on_load / on_enable / on_disable in Python; onLoad / onEnable / onDisable in C++):
- Loaded - read configuration and prepare internal state; the server isn't fully up, so don't touch the world yet.
- Enabled - register commands and event listeners, start scheduled tasks, anything that touches the now-live game.
- Disabled - on stop or reload, flush anything unsaved and release what you acquired.
The ordering is a guarantee worth leaning on: every plugin is loaded before any is enabled, so by the time on_enable runs you can safely assume the other plugins you depend on are present.
How it gets loaded
On startup the server makes sure a plugins/ folder exists (creating an empty one if it doesn't) and hands it to the plugin manager. The manager owns two loaders - one for C++, one for Python - and each recognises its own kind of plugin:
- the C++ loader claims compiled libraries:
.dllon Windows,.soon Linux; - the Python loader claims any
endstonepackage installed into the server's Python environment (shipped on PyPI and pip-installed, found through its entry point), plus.whlfiles dropped in the folder.
Whichever loader matches builds an instance of your plugin class and hands it back to the manager.
A plugin can even register a new loader as it loads - the hook that lets language support grow - but those two cover everything you'll write today.
How it responds to events
With everything enabled, the server settles into its main loop and your plugin mostly waits - until the event system wakes it up. When something happens in the game (a player joins, a block breaks, a command runs) the server raises an event, and the plugin manager delivers it to every listener registered for that event.
Many events are cancellable. Cancelling one tells the server not to do the thing it was about to - reject the block break, deny the join - which is how a plugin changes the game instead of just watching it. Listeners also run in priority order, so multiple plugins can cooperate on the same event.
That's the whole arc: found on disk, built by a loader, enabled into the running server, then fed events until shutdown. The only thing left is to choose how you'll build it.
Python or C++?
Python API
Iterate without a compile step and ship to PyPI - the quickest path to a running plugin.
C++ API
Native performance and direct access, built with CMake and FetchContent.
The two tracks expose the same capabilities - method names just shift between snake_case in Python and camelCase in C++. So choose on how you want to build and distribute, not on what you can do:
| Python | C++ | |
|---|---|---|
| Best for | Fast iteration, smaller plugins, the pip ecosystem | Native performance, direct low-level access |
| Build | No compile step | CMake, with Endstone pulled in via FetchContent |
| Ship as | A package on PyPI, or a .whl dropped into plugins/ | A compiled library dropped into plugins/ |
| Needs | Python 3.10+ | A C++20 compiler |
If you're unsure, start with the Python API. The mental model is identical, and porting a proven plugin to the C++ API later is mostly mechanical.