openrct2-staff-manager
plugin for openrct2 for managing staff
Staff Manager

Requires OpenRCT2 ≥ 0.5.5. Patrol-area zoning, nearest-zone assignment and mechanic exit detection rely on the
PathNavigator/PathConnectionplugin API introduced in that release, so the plugin will not run on older OpenRCT2 builds.
An OpenRCT2 plugin that automates park staff management: it splits your paths (and ride queues) among handymen, security guards and entertainers, assigns mechanics to ride exits, and can hire, fire and (re)assign staff so the right number always patrol the right places — sending each staff member to their nearest free zone.
Features
🧹 Path staff — Handymen, Guards, Entertainers
- One area per staff member. The park's pathways are split into contiguous patrol areas and one is assigned to each staff member.
- Nearest‑zone assignment. Every staff member is matched to the closest free zone to where they currently stand, minimising walking.
- Only relevant paths. Path counting walks the real footpath graph from the park entrance and keeps only tiles on park‑owned land — so unusable public streets (e.g. Bumbly Beach) are ignored.
- Configurable density. A spinner sets how many tiles each staff member should cover; the stats table shows Hired · Needed · Difference per type.
- Handymen split in two. Handymen are classified by their orders:
- Cleanup handymen patrol the path and queue network.
- Gardening handymen water and mow the park's grass/scenery tiles, with each connected garden area guaranteed at least one gardener.
- Staff are placed in their area. Each assigned staff member is moved into the centre of its area (onto a real, safely‑placeable path/queue tile).
🎭 Entertainers — dedicated options
Entertainers have their own controls instead of the generic density spinner:
- Tiles / Staff — how many tiles each entertainer covers.
- Staff / Area — how many entertainers share each area; > 1 = overlapping (the area grows so the tiles‑per‑staff density is preserved). Defaults to 2.
- Queue checkbox — include ride queue tiles in the patrol set, so entertainers can keep queuing guests happy instead of only patrolling plain paths.
🔧 Mechanics
- One mechanic per ride exit. Each mechanic patrols the exit tile plus the path tile directly in front of it (chosen by checking every cardinal neighbour for an actual footpath, rather than trusting the exit's stored facing direction).
- Busy mechanics are protected. A mechanic currently servicing a ride is never teleported or interrupted; they keep their patrol area but finish their job first.
🤖 Automatic management
- Optional auto mode. A toggle at the bottom of the window keeps staffing right-sized as the park changes, without pressing the buttons. New staff are only hired (or an existing area extended) when appropriate — see Automatic staffing below.
- Hovers don't hire. A freshly placed path only triggers staff once it's a real (non-ghost) footpath; merely hovering the path tool never hires anyone.
- Areas grow to their cap before hiring. Auto mode tracks each type's assigned areas synchronously, so consecutive connected tiles extend one area up to the configured tiles/staff limit before hiring a new member — no more one hire per tile. Gardening handymen are created only for newly bought land with mowable/waterable grass that isn't already assigned.
- Gated until first run. The auto toggle is only usable after "Adjust and assign" has been run once.
👥 Hire / Fire
- Adjust and assign button hires or fires the right number of every type at once to match the calculated Needed counts — hiring when understaffed, firing staff when overstaffed — and then rebuilds the patrol areas.
- Oldest staff first. Surplus staff are fired oldest‑first for a stable, consistent result.
🖥️ UI & behaviour
- One resizable window with a scan summary at the top — Paths, Queues, Garden, Ride exits and Owned tiles — refreshed by every run.
- Four staff groups (Handymen, Guards, Mechanics, Entertainers), each with an Enabled toggle, its own spinners and a Hired / Needed / Difference stat table. Handymen have separate Cleanup and Gardening spinners; mechanics need no spinner because they are derived from the ride exits.
- Adjust and assign builds every type's patrol areas from the most recently scanned tiles and teleports one staff member to the start of each new area.
- A segmented progress bar with a status line below it reports the current stage, e.g. Adjusting staff count..., Assigning guards..., Done.
- A toggle at the bottom shows Automatic assignment is ON/OFF next to a red/green indicator.
- Staff teleports are serialised through a single queue, because OpenRCT2 only supports one peep being “picked up” at a time — overlapping pickups would otherwise clobber each other.
Building
This plugin is written in TypeScript against the community-maintained
@openrct2/types package, which provides
official OpenRCT2 plugin API typings and is installed as a dev dependency via npm install
(no vendored .d.ts file is checked into this repository).
Minimum OpenRCT2 version: 0.5.5. Patrol-area zoning, nearest-zone assignment and mechanic exit detection are built on the
PathNavigator/PathConnectionplugin API introduced in that release (see "How it decides which tiles count"), so the plugin no longer runs on older OpenRCT2 builds.
Prerequisites
- Node.js 24+ with npm — required to install
dependencies, run the TypeScript compiler, and run the
.tsbuild tooling (deploy.ts,vitest.config.ts) through Node's native TypeScript support. Verify with:node -v npm -v - OpenRCT2 installed at least once, so the local
pluginfolder (or your own custom location) exists to deploy into. - Optional: Visual Studio 2022/2026 with the Node.js/JavaScript workload if you want to open
openrct2-staff-manager.slnxand build from the IDE instead of the command line (this runs the samenpm run buildscript under the hood).
- Install dependencies:
npm install. - Build:
npm run build.- Lints, runs the unit tests, type-checks, and compiles
src/(entrysrc/main.ts) todist/staff-manager.js, then automatically copies (deploy.ts) that file straight into your local OpenRCT2 plugin folder:- Windows:
Documents\OpenRCT2\plugin\ - macOS:
~/Library/Application Support/OpenRCT2/plugin/ - Linux:
~/.config/OpenRCT2/plugin/
- Windows:
- Override the destination with the
OPENRCT2_PLUGIN_DIRenvironment variable if your OpenRCT2 user directory is somewhere else.
- Lints, runs the unit tests, type-checks, and compiles
- Use
npm run watchwhile developing to recompile on save (note: this only recompiles; runnpm run buildagain, or press the game's plugin hot-reload, after awatchrecompile to redeploy the file). - For a fast inner loop,
npm run devbundles and deploys without the lint/test/typecheck pass, andnpm run verifyruns those checks on their own.npm run buildis still the fullverify+ bundle + deploy pipeline.
Code quality
- Fast linting.
npm run buildstarts withnpm run lint(oxlint ., configured in.oxlintrc.json), a Rust-based linter that covers plugin source, tests and build tooling (deploy.ts) without being tied to a specific TypeScript compiler version. Runnpm run linton its own any time. - Consistent formatting.
npm run buildalso runsnpm run format:check(oxfmt --check ., configured in.oxfmtrc.ts), the matching Rust-based formatter for oxlint. Runnpm run formatto format the codebase in place, ornpm run format:checkto only verify it. A Huskypre-commithook runs the check automatically before every commit.
Tests
Unit tests are written with Vitest and cover the
testable, pure logic in src/ (they don't touch OpenRCT2's live map/UI).
They run automatically as part of every build (npm run build = lint → test →
typecheck → bundle → deploy), or alone via npm run test.
- Tests live in
test/. The OpenRCT2 globals that functions read at call time (e.g.map.size) are stubbed inbeforeAll/afterAll. - Because the bundled plugin runs in QuickJS-NG with no module loader, tests
only exercise exported pure helpers (
computeNeeded,tileKey,isValidStationExit,classifyHandyman, config defaults, …) — they do not loadsrc/ui.tsor open a real window.
Installation
- Run
npm installthennpm run build(see Building) — this compiles and deploysstaff-manager.jsdirectly into your OpenRCT2 plugin folder. - Start OpenRCT2 (or, in single‑player, use the plugin hot‑reload).
- Open it from the map / red‑toolbox button → “Staff Manager”.
Usage
- Open the window from the toolbox menu.
- For each staff type, tick the Enabled checkbox to include it.
- Click Adjust and assign to hire/fire the right number of each type based on your settings (this is what fills the Needed column) and to build the patrol areas and move staff into them. The progress bar and status line show how far the pass has got.
- Tune the per-type spinners (handyman cleanup/gardening density, guard and entertainer tiles/staff, entertainer staff/area, entertainer Queue checkbox) and run it again as your park changes.
- Optionally enable automatic management (the Automatic assignment is ON/OFF toggle at the bottom of the window) so the plugin keeps hiring/firing and assigning staff as you connect new paths or buy new land — see Automatic staffing.
How it decides which tiles count
- Scan every tile for footpaths (incl. queue flag), surface ownership and the park entrance.
- Walk the real footpath graph from the park entrance using the engine's
PathNavigator/PathConnectionAPI (src/paths/path-graph.ts): two tiles are only treated as connected when the engine itself reports aPathConnectionbetween them, which already accounts for slopes, height offsets and queue/regular separation — a bridge path and the path passing underneath it are never merged just because they share the same x/y. - Keep only reachable tiles on owned land, split into paths and queues.
- Scan gardening tiles — tiles with mowable grass or waterable scenery — grouped into connected components.
- Match staff to the nearest zone (paths, or paths+queues for
entertainers with the Queue checkbox on), or into fixed‑size overlapping areas for
entertainers. Mechanics get the exit tile plus the path tile a
PathNavigatorquery from the exit reports as connected, rather than a plain cardinal-neighbour probe, so this also resolves correctly across slopes.
If the entrance can't be found, it falls back to seeding from owned path tiles.
Automatic staffing
Auto mode (the toggle at the bottom of the window) reacts to two kinds of map edits:
- New footpath / queue tiles. When a real (non-ghost) path is created, a cleanup handyman, guard or entertainer is hired only if no area of that type already covers the tile; otherwise the adjacent area is extended up to its tiles/staff cap. Hover previews (ghost paths) are ignored.
- Newly bought land. A gardening handyman is created only for newly bought land whose tile is actually mowable/waterable grass and is not yet assigned to a gardening handyman; adjacent garden areas are extended up to the mower-tiles/staff cap first.
Auto mode bases these decisions on a synchronous in-memory record of each staff type's assigned areas (independent of the async staff-hire/patrol-area game actions), which is why a burst of connected tiles extends one area rather than hiring one member per tile. It's disabled until "Adjust and assign" has been run once.
Notes & limitations
- Auto mode never performs full map scans. It makes incremental, minimal changes only for the tiles you actually place or buy, so it stays cheap during long path drags.
- Mechanic dispatch: dispatched to, so “busy” is inferred from the mechanic not standing on a footpath tile.
- Patrol areas are height‑ and water‑aware. Path tiles are only linked into the
same patrol area when the engine's
PathNavigatorreports a realPathConnectionbetween them, and gardening tiles when actually walkable between each other (no unclimbable height differences, never through water), so an area is always one contiguous, fully reachable region. If there are more disconnected pockets than staff to cover them, the largest pockets are covered first rather than merging areas a staff member couldn't actually walk across. - Path graph caching. The footpath graph built from
PathNavigatoris cached and reused across zoning, nearest-zone assignment and mechanic detection within a single "Adjust and assign" pass, and is invalidated automatically whenever a game action that can change it runs (placing/removing paths or banners, changing land rights). - Teleport vs. patrol: a staff member's patrol area is always built in full, but the physical teleport target is the nearest safely‑placeable path tile, since the game rejects placement on obstructed tiles (benches, scenery, queue TV, embedded ride elements, etc.).
- Single‑player focus: actions are skipped on network clients.
Configuration (in‑file)
Near the top of src/config.ts the default tunables are defined (and the
initial values are assigned to the stores in src/store.ts). They're exposed
directly in the UI, but their initial defaults live in code:
| Store (default) | Purpose |
|---|---|
handymenTilesPerStaffStore (8) |
Path/queue tiles per cleanup handyman |
handymenMowerTilesPerStaffStore (256) |
Garden tiles per gardening handyman |
guardsTilesPerStaffStore (16) |
Plain path tiles per guard |
entertainersTilesPerStaffStore (16) |
Tiles per entertainer |
entertainersPerAreaStore (2) |
Entertainers assigned per area |
entertainersIncludeQueueStore (true) |
Whether entertainers patrol queues |
*EnabledStore (true) |
Whether each staff type is managed |
TODO
- Fix patrol‑area edge cases — continue shaking out issues in area assignment (gardening, mechanics, handymen) and the hire/fire flows.
Localization
The plugin's UI text is localized through a small, self-contained TypeScript
i18n layer in src/i18n/ — OpenRCT2's built-in StringId/
data/language/*.txt localization system is part of the C++ core and isn't
exposed to plugins, so this layer replaces it entirely.
- Language detection: on startup, the plugin reads
context.configuration.get("general.language", "en-GB")inside a try/catch (the API can vary between OpenRCT2 versions). If the read fails, or the detected language has no matching dictionary, the plugin falls back toen-GB, which is the canonical/fallback dictionary and always contains every translation key. - Adding a new language: copy
src/i18n/en-gb.tstosrc/i18n/<language-code>.ts(e.g.fr-FR.ts), translate every value (theTranslationstype insrc/i18n/types.tsmakes the TypeScript compiler fail the build if a key is missing or misspelled), then import and register the dictionary in thetranslationsmap insrc/i18n/index.ts. - Build-time bundling: translations live in separate
.tssource files purely for maintainability. QuickJS-NG (the engine the bundled plugin runs in) has no runtime module loader or file-system access, so translations are never read from disk while the game is running — esbuild inlines every language file into the single bundleddist/staff-manager.jsat build time.
Contributing & Releases
Contributions go through pull requests only (no direct pushes to main), commit
messages follow Conventional Commits and
are enforced via commitlint/husky, and releases (versioning, changelog, GitHub
Releases) are fully automated with release-please —
no npm publish step involved. See CONTRIBUTING.md for the
full workflow.
Licence
MIT © Johannes Holzhäuer