OpenRCT2: Handyman Repair Scenery Order
This tutorial walks through implementing a new handyman staff order in the OpenRCT2 codebase.
What you’ll build
Handymen will gain a fifth toggleable order — Repair vandalised scenery — that lets them autonomously find and fix broken path additions within their patrol area.
Steps overview
- Add the order bitmask flag in
Staff.h - Register a sprite ID and create the icon in
SpriteIds.h - Write the pathfinding detector in
Staff.cpp - Implement the repair action state
- Track and persist the statistic
- Update the UI window (orders + stats tabs)
- Add localisation strings in
en-GB.txt - Expose the stat in the plugin API
- Write a changelog entry
Prerequisites
- Basic C++ — structs, enums, pointers, bitwise operators
- Git and CMake installed
- The repo cloned and building locally
Before writing any code, search for STAFF_ORDERS_EMPTY_BINS and
trace every usage. The bin-emptying order is the closest existing analogue
to what we’re building — every step in this tutorial models its approach.
Step 1 — Add the order flag
Open src/openrct2/entity/Staff.h and add a new constant to the STAFF_ORDERS enum.
Each order is a power-of-two bit so multiple orders can be combined in a single byte.
enum STAFF_ORDERS
{
STAFF_ORDERS_SWEEPING = (1 << 0),
STAFF_ORDERS_WATER_FLOWERS = (1 << 1),
STAFF_ORDERS_EMPTY_BINS = (1 << 2),
STAFF_ORDERS_MOWING = (1 << 3),
STAFF_ORDERS_REPAIR_SCENERY = (1 << 4), // new
// ...
};
Step 2 — Register a sprite ID
Open src/openrct2/SpriteIds.h. Find the existing staff order icons and append a new ID
after the last registered sprite in the G2 block.
G2 is OpenRCT2’s packed sprite sheet. Every icon gets a sequential integer ID at
compile time. Run grep -n ‘SPR_STAFF_ORDERS’ src/openrct2/SpriteIds.h to
list the existing staff-order entries, note the value of the last one, and use
that number plus one as your <NEXT_ID>.
SPR_STAFF_ORDERS_SWEEPING = 5111,
SPR_STAFF_ORDERS_WATER_FLOWERS = 5112,
SPR_STAFF_ORDERS_EMPTY_BINS = 5113,
SPR_STAFF_ORDERS_MOWING = 5114,
// ...
SPR_STAFF_ORDERS_REPAIR_SCENERY = 5115, // new — use last value + 1
Create a 24×24 PNG in resources/g2/ and register it in resources/g2/sprites.json by
appending an entry that matches the filename — follow the existing entries as your pattern.
Step 3 — Pathfinding
Add UpdatePatrollingFindBrokenScenery() to Staff.cpp. It follows the same pattern
as UpdatePatrollingFindBin() — check the order flag, walk the tile element stack,
test the condition (IsBroken() instead of bin capacity), and transition state.
OpenRCT2 represents each map tile as a contiguous array of TileElement objects — one
for the ground surface, one for any path, one for each object placed on it, and so on.
MapGetFirstElementAt() returns a pointer to the first element; incrementing the pointer
walks through the stack, and IsLastForTile() signals the end.
GetNextIsSurface() returns true when the handyman’s queued next position is an open
ground tile with no path — there is nothing to repair there, so we return early.
bool Staff::UpdatePatrollingFindBrokenScenery()
{
if (!(StaffOrders & STAFF_ORDERS_REPAIR_SCENERY))
return false;
if (GetNextIsSurface())
return false;
TileElement* tileElement = MapGetFirstElementAt(NextLoc);
if (tileElement == nullptr)
return false;
for (;; tileElement++)
{
if (tileElement->GetType() == TileElementType::Path
&& tileElement->GetBaseZ() == NextLoc.z)
break;
if (tileElement->IsLastForTile())
return false;
}
if (!tileElement->AsPath()->HasAddition()) return false;
if (!tileElement->AsPath()->IsBroken()) return false;
if (tileElement->AsPath()->AdditionIsGhost()) return false;
SetState(PeepState::repairingScenery);
SubState = 0;
SetDestination(tileElement->AsPath()->GetLocation().ToTileStart(), 3); // 3 = destination tolerance in pixels; matches the bin-emptying value
return true;
}
IsBroken() reads a boolean flag on the path addition element that guests set when they
vandalize an object — there is no damage counter, just a flag.
OpenRCT2 runs each guest and staff member through a state machine every game tick. Each
PeepState value corresponds to an Update*() function that is called until the state
changes. SubState is a counter you use to sequence sub-steps within that state (walk to
target, play animation, apply fix). When SetState(PeepState::repairingScenery) fires
here, the game will call UpdateRepairingScenery() — implemented in Step 4 — on every
subsequent tick until the repair completes and the state reverts to Patrolling.
Then call it from the dispatch block at the bottom of UpdatePatrolling().
Step 4 — Add the peep state and implement the repair loop
Open src/openrct2/peep/Peep.h and find the PeepState enum. Search for EmptyingBin
and add RepairingScenery immediately after it:
enum class PeepState : uint8_t
{
// ...
EmptyingBin,
RepairingScenery, // new
// ...
};
In Staff.cpp, find UpdateEmptyingBin() and add UpdateRepairingScenery() directly
below it, using it as your model. The key differences from the bin version:
- The detection already happened in Step 3 — no second scan needed
- The fix action is
tileElement->AsPath()->SetBroken(false) - Increment
StaffSceneryRepaired(added in Step 5) when the repair completes - Return to
PeepState::Patrollingwhen done
Finally, add a case PeepState::RepairingScenery: branch to the main update switch in
Staff.cpp. Search for case PeepState::EmptyingBin: to find the right place.
Step 5 — Track and persist the statistic
In Staff.h, add the repair counter to the Staff struct next to StaffBinsEmptied:
uint16_t StaffBinsEmptied;
uint16_t StaffSceneryRepaired; // new
Find ResetStats() and zero the new field. Then find Serialise() — search for
StaffBinsEmptied in Staff.cpp — and add a serialisation call for the new field
immediately after it:
ds(StaffBinsEmptied);
ds(StaffSceneryRepaired); // new
The order of ds() calls must match between save and load, so position matters.
Step 6 — Update the UI window
Search for WIDX_STAFF_ORDERS_EMPTY_BINS to find the handyman window file under
src/openrct2/windows/.
Orders tab: Add a new widget ID for the repair checkbox, then add a checkbox entry
using the same layout as the existing four. Its label should reference the localisation
string STR_STAFF_ORDERS_REPAIR_SCENERY (added in Step 7). Wire the checkbox to the
STAFF_ORDERS_REPAIR_SCENERY bit flag from Step 1 in the toggle handler.
Stats tab: Search for StaffBinsEmptied in the same file — the stats tab renders
each counter as a row. Add an equivalent row for StaffSceneryRepaired using
STR_STAFF_SCENERY_REPAIRED (also from Step 7).
Step 7 — Add localisation strings
Open data/language/en-GB.txt. Search for STR_STAFF_ORDERS_EMPTY_BINS to find the
staff order strings block and add:
STR_STAFF_ORDERS_REPAIR_SCENERY :Repair vandalised scenery
STR_STAFF_SCENERY_REPAIRED :Scenery repaired: {INT32}
Step 8 — Expose the stat in the plugin API
Search for binsEmptied in src/openrct2/scripting/. The staff entity binding
exposes each stat as a named read-only property. Add a matching sceneryRepaired
property following the same pattern.
Step 9 — Add a changelog entry
Add a one-line entry to distribution/changelog.txt under the Feature heading:
- Feature: Handymen can now repair vandalised path additions when the 'Repair vandalised scenery' order is enabled