Plugin Structure & Setup
Building a Plugin
Every plugin is an isolated Python program with a standard structure. Benefits:
- Boilerplate code already prepared
- Core modules for common tasks (config, logging, paths)
- Automatic registration in PLUGIN_REGISTRY
Folder Structure
Automatically created via PowerShell script (create_plugin.ps1):
src/plugins/
└── my_plugin/
├── main.py ← Plugin core
├── README.md
└── version.txt
Creating a Plugin: 2 Steps
If you use the PowerShell script create_plugin.ps1, it will ask you for the name of your plugin. It then automatically creates the complete folder structure for you. This then looks like this:
.
├── your_plugin_name
│ ├── main.py
│ ├── README.md
│ └── version.txt
The new folder will be created at src/plugins/ with the name you specified during creation.
The Individual Files
main.py – The Heart of Your Plugin
This is the most important file! This is where you write the actual logic of your plugin. If you create a plugin with create_plugin.ps1, you automatically get base code inserted. It looks something like this:
from core import load_config, parse_args, get_root_dir, get_base_dir, get_base_file, register_plugin, AppConfig
import sys
BASE_DIR = get_base_dir()
ROOT_DIR = get_root_dir()
CONFIG_FILE = ROOT_DIR / "config" / "config.yaml"
DATA_DIR = ROOT_DIR / "data"
MAIN_FILE = get_base_file()
args = parse_args()
cfg = load_config(CONFIG_FILE)
gui_hidden = args.gui_hidden
register_only = args.register_only
if register_only:
register_plugin(AppConfig(
name="test",
path=MAIN_FILE,
enable=True,
level=4,
ics=False
))
sys.exit(0)
[!TIP] If you want to use a
config.yamlfile directly in the plugin folder, replace:CONFIG_FILE = ROOT_DIR / "config" / "config.yaml"With this code:
CONFIG_FILE = BASE_DIR / "config.yaml" CONFIG_FILE.touch(exist_ok=True) # Creates the file if you haven't already done it yourself.
What Exactly Is Happening?
Imports
You import functions and classes from the core module. This saves you a lot of writing work:
load_config: Loads the configuration fileparse_args: Reads command-line argumentsget_root_dir,get_base_dir,get_base_file: Determine important directories and file pathsregister_plugin: Registers your pluginAppConfig: A class that stores the plugin configuration
Setting Up Important Paths
BASE_DIR = get_base_dir() # The base folder of the application
ROOT_DIR = get_root_dir() # The root path, two levels above BASE_DIR
CONFIG_FILE = ROOT_DIR / "config" / "config.yaml" # Path to configuration
DATA_DIR = ROOT_DIR / "data" # Folder for user data
MAIN_FILE = get_base_file() # The path to main.exe (main.py in the dev folder)
You will need these variables later in your code — for example to save files or load the config.
Reading Startup Arguments
args = parse_args()
gui_hidden = args.gui_hidden # Was the --gui-hidden flag set?
register_only = args.register_only # Was the --register-only flag set?
The program can start your plugin with certain flags:
--gui-hidden: The GUI is started hidden--register-only: The plugin is only registered but not executed
Registering the Plugin (if --register-only is set)
if register_only:
register_plugin(AppConfig(
name="test",
path=MAIN_FILE,
enable=True,
level=4,
ics=False
))
sys.exit(0)
If the plugin just needs to be registered, the following happens:
-
name: The name of your plugin (e.g. "test") -
path: The path to the executable file -
enable:True= Plugin is active,False= Plugin is deactivated
Tip: Instead of hardcodingTrue/False, you can also use config values:enable=cfg.get("custom_name", {}).get("enable", True)This is how users can turn your plugin on and off in the
config.yaml! -
level: Determines when the terminal is visible (depending on thelog_levelin theconfig.yaml):- Level 0: Disables everything (should not be used)
- Level 1: Terminal visible at
log_level: 1 - Level 2: Main programs (
log_level: 2) - Level 3: Background services (e.g. checks, listeners)
- Level 4: Debug/Development
- Level 5: Overrides other settings (should not be used)
-
ics: Interface Control System – indicates whether the GUI is supportedTrue= GUI is supportedFalse= GUI is NOT supported (Direct Control System / DCS)
After registration, the program ends with sys.exit(0).
[!WARNING] Plugin registration: order and time limit
The call to
register_plugin(...)must occur as early as possible in the program. Before registration, no executable code may be present — except:
- Imports
- Configuration and path definitions
- Argument parsing (e.g.
parse_args())Not allowed before registration:
- Logic with side effects
- Network access or file access
- Initializations with external dependencies
Background: The registration routine runs in a strictly limited environment and may otherwise fail.
Immediate exit required
After successfully calling
register_plugin(...), the program must be terminated immediately withsys.exit(0).if register_only: register_plugin(AppConfig( name="test", path=MAIN_FILE, enable=True, level=4, ics=False )) sys.exit(0)Without this immediate termination, you risk executing downstream code, which can corrupt or invalidate the registry.
Note the time limit
The registration process has a hard time limit of 5 seconds. If this is exceeded, the program is terminated externally.
Loading Configuration
cfg = load_config(CONFIG_FILE)
Here the config.yaml is loaded. It contains all the settings for your plugin. cfg is now a dictionary you can access:
# Example: Read out config value with default value
enable = cfg.get("custom_name", {}).get("enable", True)
This is what it should look like in the config.yaml:
custom_name:
enable: True
README.md – Document Your Plugin
This file is your chance to show other developers what your plugin does. Write here:
- What does the plugin do? – A short description
- Requirements – What requirements does the user have to meet?
- Configuration – What options are available in the
config.yaml? - Usage – How is the plugin used?
A good README makes things easier for yourself and others later!
version.txt – The Version Number
In this file you save the current version of your plugin. By default, when you create a new plugin it says:
v1.0.0
Important: Stick to this format! It follows the Semantic Versioning standard:
- v1.0.0 = Major.Minor.Patch
- Major: Breaking changes (big changes)
- Minor: New features (backwards compatible)
- Patch: Bug fixes
Examples:
- v1.0.0 → v1.0.1 (small bug fix)
- v1.0.1 → v1.1.0 (new feature added)
- v1.1.0 → v2.0.0 (major conversion, no longer compatible)
Plugins in Other Programming Languages
Can I also write my plugin in Java, C++, JavaScript etc.? Yes, but...
When you leave Python, you have to do a lot of things yourself that Python modules do for you. Just to give you an idea:
- Load config
- Read startup arguments
- Determine paths
- Register plugin
- Error handling
The basic structure can quickly require several hundred lines of code depending on the language — significantly more than the ~20 lines of Python above.
Rule of thumb: Python is the best place to start. If you need more performance later, you can always optimize performance-critical parts or create them in a different language.
Next chapter: Webhook events and Minecraft integration