openrct2-staff-manager

plugin for openrct2 for managing staff

Staff Manager

CI Release Coverage Downloads

Staff Manager screenshot

Requires OpenRCT2 ≥ 0.5.5. Patrol-area zoning, nearest-zone assignment and mechanic exit detection rely on the PathNavigator/PathConnection plugin 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/PathConnection plugin 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 .ts build 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 plugin folder (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.slnx and build from the IDE instead of the command line (this runs the same npm run build script under the hood).
  1. Install dependencies: npm install.
  2. Build: npm run build.
    • Lints, runs the unit tests, type-checks, and compiles src/ (entry src/main.ts) to dist/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/
    • Override the destination with the OPENRCT2_PLUGIN_DIR environment variable if your OpenRCT2 user directory is somewhere else.
  3. Use npm run watch while developing to recompile on save (note: this only recompiles; run npm run build again, or press the game's plugin hot-reload, after a watch recompile to redeploy the file).
  4. For a fast inner loop, npm run dev bundles and deploys without the lint/test/typecheck pass, and npm run verify runs those checks on their own. npm run build is still the full verify + bundle + deploy pipeline.

Code quality

  • Fast linting. npm run build starts with npm 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. Run npm run lint on its own any time.
  • Consistent formatting. npm run build also runs npm run format:check (oxfmt --check ., configured in .oxfmtrc.ts), the matching Rust-based formatter for oxlint. Run npm run format to format the codebase in place, or npm run format:check to only verify it. A Husky pre-commit hook 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 in beforeAll/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 load src/ui.ts or open a real window.

Installation

  1. Run npm install then npm run build (see Building) — this compiles and deploys staff-manager.js directly into your OpenRCT2 plugin folder.
  2. Start OpenRCT2 (or, in single‑player, use the plugin hot‑reload).
  3. Open it from the map / red‑toolbox button → “Staff Manager”.

Usage

  1. Open the window from the toolbox menu.
  2. For each staff type, tick the Enabled checkbox to include it.
  3. 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.
  4. 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.
  5. 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

  1. Scan every tile for footpaths (incl. queue flag), surface ownership and the park entrance.
  2. Walk the real footpath graph from the park entrance using the engine's PathNavigator/PathConnection API (src/paths/path-graph.ts): two tiles are only treated as connected when the engine itself reports a PathConnection between 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.
  3. Keep only reachable tiles on owned land, split into paths and queues.
  4. Scan gardening tiles — tiles with mowable grass or waterable scenery — grouped into connected components.
  5. 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 PathNavigator query 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 PathNavigator reports a real PathConnection between 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 PathNavigator is 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 to en-GB, which is the canonical/fallback dictionary and always contains every translation key.
  • Adding a new language: copy src/i18n/en-gb.ts to src/i18n/<language-code>.ts (e.g. fr-FR.ts), translate every value (the Translations type in src/i18n/types.ts makes the TypeScript compiler fail the build if a key is missing or misspelled), then import and register the dictionary in the translations map in src/i18n/index.ts.
  • Build-time bundling: translations live in separate .ts source 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 bundled dist/staff-manager.js at 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