OpenRCT2: Handyman Repair Scenery Order

beginner C++OpenRCT2game devopen source View repo →
0 / 0

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

Step 1Order FlagAdd the bitmask constant in Staff.h
Step 2Sprite & IconRegister a sprite ID and create the icon
Step 3PathfindingWrite the pathfinding detector in Staff.cpp
Step 4Action StateImplement the repair loop and peep state
Step 5StatisticsTrack and persist the repair counter
Step 6UI WindowUpdate the orders and stats tabs
Step 7LocalisationAdd display strings in en-GB.txt
Step 8Plugin APIExpose the stat to the scripting layer
Step 9ChangelogDocument the feature for release notes

Prerequisites

What you need to know:

Structs & enumsPointersBitwise operatorsBasic C++ syntaxTemplatesSmart pointersMove semantics

What you need installed:

GitClone the repo and manage branches
CMakeBuild system used by the project
C++ compilerGCC, Clang, or MSVCThe repo must build locally before you start
Reference pattern

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

Step 1 of 9

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.

src/openrct2/entity/Staff.hView on GitHub →
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

Step 2 of 9

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.

What is G2 and how do I find the next ID?

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>.

src/openrct2/SpriteIds.hView on GitHub →
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

Step 3 of 9

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.

src/openrct2/entity/Staff.cppView on GitHub →
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

Step 4 of 9

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:

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

Step 5 of 9

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

Step 6 of 9

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

Step 7 of 9

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

Step 8 of 9

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

Step 9 of 9

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