Plugin Development Tutorial¶
This tutorial will guide you through creating a simple RotorHazard plugin from scratch. We'll build a "Race Counter" plugin that displays the total number of races completed.
Prerequisites¶
Before starting, make sure you have:
- Basic Python knowledge
- A GitHub account
- Git installed on your computer
- A RotorHazard instance for testing
Step 1: Create Your Repository¶
- Go to the plugin template repository
- Click "Use this template" → "Create a new repository"
- Name your repository (e.g.,
rh-race-counter) - Set it to Public
- Click "Create repository"
Step 2: Clone Your Repository¶
Step 3: Set Up the Plugin Structure¶
The template provides a basic structure with a folder named rh_template. First, rename this folder to match your plugin domain:
You should now see:
Step 4: Update the Manifest¶
Edit manifest.json with your plugin details:
{
"domain": "race_counter",
"name": "Race Counter",
"description": "Displays the total number of races completed",
"version": "1.0.0",
"required_rhapi_version": "1.0.0",
"author": "Your Name",
"author_uri": "https://github.com/YOUR_USERNAME",
"documentation_uri": "https://github.com/YOUR_USERNAME/rh-race-counter",
"license": "MIT",
"license_uri": "https://github.com/YOUR_USERNAME/rh-race-counter/blob/main/LICENSE"
}
Domain naming
The domain field must match your directory name exactly and use only lowercase letters, numbers, and underscores.
Step 5: Create the Plugin Code¶
Edit __init__.py to implement your plugin:
"""Race Counter Plugin - Displays the total number of races completed."""
from eventmanager import Evt
def initialize(rhapi):
"""Called when the plugin is loaded."""
# Initialize race counter
rhapi.db.option_set("race_counter_total", 0)
# Register event handlers
rhapi.events.on(Evt.RACE_FINISH, on_race_finish)
# Add UI panel
rhapi.ui.register_panel(
"race_counter",
"Race Counter",
"stats",
order=0,
)
# Add UI field to display count
rhapi.ui.register_quickbutton(
"race_counter",
"race_counter_display",
"Total Races",
get_race_count,
[],
)
def on_race_finish(args):
"""Called when a race finishes - Increments the race counter."""
rhapi = args["rhapi"]
# Get current count
current_count = int(rhapi.db.option("race_counter_total", 0))
# Increment
new_count = current_count + 1
# Save
rhapi.db.option_set("race_counter_total", new_count)
# Log
rhapi.ui.message_notify(f"Race #{new_count} completed!")
def get_race_count(rhapi):
"""Return the current race count."""
count = rhapi.db.option("race_counter_total", 0)
return f"{count} races completed"
Step 6: Understanding the Code¶
Let's break down what each part does:
The initialize() Function¶
This is the entry point of your plugin. It's called when RotorHazard loads your plugin.
Event Handlers¶
This registers a function to be called when a race finishes. Available events include:
Evt.RACE_START- Race has startedEvt.RACE_FINISH- Race has finishedEvt.LAPS_SAVE- Laps have been savedEvt.LAPS_CLEAR- Laps have been clearedEvt.DATABASE_INIT- Database initialized
Database Options¶
Store persistent data that survives restarts.
UI Elements¶
Creates a panel in the RotorHazard interface where your plugin's UI elements will appear.
Step 7: Test Your Plugin Locally¶
-
Create a symbolic link to your plugin in the RotorHazard data directory:
Why use a symbolic link?
Using a symbolic link instead of copying allows you to:
- Edit your plugin files directly in your repository
- See changes immediately after restarting RotorHazard
- Keep your development workflow clean with git
- Avoid syncing issues between copies
Finding your rh-data folder
The
rh-datafolder is typically located outside your RotorHazard installation directory. Common locations:- Linux:
~/rh-data/plugins/ - Custom installs: Check your RotorHazard configuration for the data directory path
-
Restart RotorHazard to load your plugin:
Restart required
RotorHazard loads plugins at startup, so you need to restart the server whenever you make changes to your plugin code.
-
Verify your plugin is loaded:
- Open the RotorHazard web interface in your browser
- Navigate to Settings → Plugins
- Your "Race Counter" plugin should be listed
- Check that it shows the correct version and description
-
Test the functionality:
- Go to the race interface
- Start and complete a test race
- You should see a notification: "Race #1 completed!"
- Check the Stats panel for the race counter display
- Run another race and verify the counter increments to 2
-
Development workflow:
During development, follow this cycle:
- Edit your plugin files in the repository
- Restart RotorHazard (
Ctrl+Cthen restartpython server.py) - Test your changes in the web interface
- Check the server logs for any errors
- When satisfied, commit your changes with git to GitHub
Step 8: Verify RHFest Validation¶
The template repository already includes RHFest validation in .github/workflows/rhfest.yml. This automatically validates your plugin structure and manifest.
After you push your changes to GitHub, check that the validation passes:
- Go to your repository on GitHub
- Click the Actions tab
- You should see the RHFest workflow running
- Wait for it to complete and verify it shows a green checkmark
If the validation fails, check the error messages and fix any issues with your manifest.json or plugin structure.
Step 9: Create Your First Release¶
-
Update
manifest.jsonif needed (version should be1.0.0) -
Commit your changes:
-
Create a release on GitHub:
- Go to your repository → Releases → Create a new release
- Tag version:
v1.0.0 - Release title:
v1.0.0 - Click Generate release notes to automatically create a description from your commits
- Click Publish release
Step 10: Add to Community Database¶
Follow the Include repository guide to submit your plugin to the community database.
Enhancements¶
Once you have the basics working, try adding these features:
Add a Reset Button¶
def reset_counter(rhapi, args): # noqa: ARG001
"""Reset the race counter."""
rhapi.db.option_set("race_counter_total", 0)
rhapi.ui.message_notify("Race counter reset!")
# In initialize():
rhapi.ui.register_quickbutton(
"race_counter",
"race_counter_reset",
"Reset Counter",
reset_counter,
[],
)
Display Statistics¶
def get_race_stats(rhapi):
"""Calculate race statistics."""
total_races = int(rhapi.db.option("race_counter_total", 0))
# Get all saved races from database
races = rhapi.db.races
if races:
avg_pilots = sum(len(race.pilots) for race in races) / len(races)
return f"{total_races} races, avg {avg_pilots:.1f} pilots per race"
return f"{total_races} races completed"
Add Configuration Options¶
from RHUI import UIField, UIFieldType
# In initialize():
rhapi.ui.register_option(
rhapi.ui.UIField(
name="race_counter_show_notifications",
label="Show Notifications",
field_type=UIFieldType.CHECKBOX,
value=True,
),
"race_counter",
)
# In on_race_finish():
show_notifications = rhapi.db.option("race_counter_show_notifications", True)
if show_notifications:
rhapi.ui.message_notify(f"Race #{new_count} completed!")
Common Issues¶
Plugin Doesn't Load¶
- Check that
domaininmanifest.jsonmatches the folder name - Verify
required_rhapi_versionis compatible with your RotorHazard version - Check RotorHazard logs for error messages
Events Not Firing¶
- Ensure you're using the correct event name (e.g.,
Evt.RACE_FINISH, notRACE_FINISH) - Verify the event handler is registered in
initialize() - Check that the event you're using exists in your RotorHazard version
Database Values Not Persisting¶
- Use
rhapi.db.option_set()to save values - Don't use regular Python variables for persistence
- Values are stored as strings, convert when needed
Next Steps¶
- Explore the RotorHazard API documentation
- Study existing plugins in the community database
- Join the Discord community for help and ideas
- Read the FAQ for common questions
Resources¶
Happy coding!