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",
"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. Do not use hyphens, leading or trailing underscores, or consecutive 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.yaml. This automatically validates your plugin structure, manifest, Python syntax, and initialize(rhapi) entry point.
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 validation fails, use the reported rule code, source location, and help text to fix the plugin structure, manifest, or Python entry point.
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!