Skip to content

Align MCP with merged OpenRCT2 #26675 plugin APIs - #2

Merged
BenDaSpur merged 6 commits into
mainfrom
feat/plugin-api-v115-mcp
Jul 20, 2026
Merged

Align MCP with merged OpenRCT2 #26675 plugin APIs#2
BenDaSpur merged 6 commits into
mainfrom
feat/plugin-api-v115-mcp

Conversation

@BenDaSpur

@BenDaSpur BenDaSpur commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Consume OpenRCT2 #26675 fields via ride-builder: reliability, guestCount/isEmpty, income/profit, station.queueTime, context.gameSpeed, and isAdditionFull.
  • Keep MCP map/guest rect tools working with tile-scan polyfills (native getElementsInRect / getGuestsInRect did not land in that PR).
  • Sync openrct2.d.ts to develop and update refurbish/list/status tooling to prefer the new metrics.

Test plan

  • pytest tests/test_bridge_fast_api115.py tests/test_map_region_api115.py tests/test_time_tools.py tests/test_ride_ops.py tests/test_scenery_tools.py
  • Against OpenRCT2 develop (post-#26675): reload ride-builder, confirm openrct2_status reports plugin_api_version and plugin game_speed
  • list_rides / list_refurbish_candidates_tool show reliability; refurbish_ride_tool waits using guestCount/isEmpty
  • get_map_elements_in_rect_tool and sample_guests_near_tile_tool return data via polyfill

Summary by CodeRabbit

  • New Features
    • Ride listings now include reliability, guest/empty state, income, profit, and queue time to improve refurbishment readiness and candidate ranking.
    • Added a bulk tool to export map elements in a rectangle (footpaths, tracks, entrances) with enforced region limits.
    • Guest-near-tile sampling uses a faster rectangle-based query when available (with fallback behavior preserved).
    • Game time status can now report plugin-derived game speed with clear source labeling.
  • Bug Fixes
    • Improved litter-bin “full” detection to prefer the newer “addition full” signal when available, with a safer fallback.

A bin slot is only full (visibly full / emptiable by a handyman) once its
2-bit edge slot reaches 0, not whenever the status byte is below 255. Update
the _is_litter_bin_full fallback to do a per-slot check, prefer the native
isAdditionFull getter when present, and sync the ride-builder API type stub.
Use ride reliability/occupancy/income, context.gameSpeed, and isAdditionFull; polyfill rect map/guest scans since those APIs did not land.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c199fbb-9c27-493b-b36a-45c97a9c2b3d

📥 Commits

Reviewing files that changed from the base of the PR and between 22696bd and 941ee6d.

📒 Files selected for processing (2)
  • mcp-server/openrct2_mcp/time_tools.py
  • mcp-server/tests/test_time_tools.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • mcp-server/tests/test_time_tools.py
  • mcp-server/openrct2_mcp/time_tools.py

📝 Walkthrough

Walkthrough

The update expands Ride Builder APIs and type definitions, enriches ride and time status data, adds spatial guest and map-element queries, changes refurbishment occupancy handling, improves litter-bin fullness detection, and wires these capabilities through MCP tools with focused tests.

Changes

OpenRCT2 integration

Layer / File(s) Summary
Plugin APIs and type contracts
plugins/ride-builder/lib/openrct2.d.ts, plugins/ride-builder/src/ride-builder.js
Adds game-speed, guest, map-element, queue, and ride-maintenance fields and endpoints, updates API compatibility to 116, and expands related type definitions.
Ride telemetry and maintenance workflows
mcp-server/openrct2_mcp/bridge_fast.py, mcp-server/openrct2_mcp/ride_ops.py, mcp-server/openrct2_mcp/time_tools.py, mcp-server/openrct2_mcp/server.py, mcp-server/tests/*
Merges plugin maintenance data into rides, reports plugin or session game-speed sources, and waits for plugin-confirmed ride occupancy before refurbishment.
Spatial query tools and fallbacks
mcp-server/openrct2_mcp/guest_intel.py, mcp-server/openrct2_mcp/map_region.py, mcp-server/openrct2_mcp/server.py, mcp-server/tests/test_map_region_api115.py
Adds plugin-backed guest sampling with bridge fallback and exposes bounded rectangle exports for footpaths, tracks, and entrances.
Litter-bin fullness detection
mcp-server/openrct2_mcp/scenery_tools.py, mcp-server/tests/test_scenery_tools.py
Uses isAdditionFull when available and decodes additionStatus otherwise, with tests for both detection paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPTool
  participant MapRegion
  participant RideBuilderClient
  participant RideBuilderPlugin
  MCPTool->>MapRegion: request element type and rectangle
  MapRegion->>RideBuilderClient: call getElementsInRect with bounded coordinates
  RideBuilderClient->>RideBuilderPlugin: execute rectangle scan
  RideBuilderPlugin-->>RideBuilderClient: return serialized elements
  RideBuilderClient-->>MapRegion: return elements payload
  MapRegion-->>MCPTool: return type, bounds, count, and elements
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: aligning MCP with the merged OpenRCT2 #26675 plugin APIs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/plugin-api-v115-mcp

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mcp-server/openrct2_mcp/ride_ops.py (1)

283-308: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

_merge_maintenance duplicates _merge_ride_builder_maintenance from bridge_fast.py.

Both functions implement identical merge logic (same key pairs, same is None guard, same activeBreakdown/stationQueueTimes handling). Consider extracting a shared helper to avoid drift as new fields are added.

Additionally, _load_ride_builder_maintenance (line 370) now delegates to _load_ride_builder_maintenance_index — a private function imported across module boundaries (line 12). If this helper is shared, consider making it public (dropping the underscore) or moving it to a shared utility module.

♻️ Proposed shared helper
 # In bridge_fast.py — rename to public:
-def _merge_ride_builder_maintenance(
+def merge_ride_builder_maintenance(
     summary: dict[str, Any],
     ride_builder_row: dict[str, Any] | None,
 ) -> dict[str, Any]:
     ...

-def _load_ride_builder_maintenance_index(ride_builder: RideBuilderClient) -> dict[int, dict[str, Any]]:
+def load_ride_builder_maintenance_index(ride_builder: RideBuilderClient) -> dict[int, dict[str, Any]]:
     ...
 # In ride_ops.py — reuse instead of duplicating:
-from openrct2_mcp.bridge_fast import ..., _load_ride_builder_maintenance_index
+from openrct2_mcp.bridge_fast import ..., load_ride_builder_maintenance_index, merge_ride_builder_maintenance

-def _merge_maintenance(
-    maintenance: dict[str, Any],
-    ride_builder_row: dict[str, Any] | None,
-) -> dict[str, Any]:
-    """Prefer ride-builder maintenance stats when the bridge omits reliability."""
-    merged = dict(maintenance)
-    if ride_builder_row is None:
-        return merged
-    for key, rb_key in (
-        ...
-    ):
-        if merged.get(key) is None and ride_builder_row.get(rb_key) is not None:
-            merged[key] = ride_builder_row.get(rb_key)
-    if ride_builder_row.get("activeBreakdown"):
-        merged["active_breakdown"] = True
-    station_times = ride_builder_row.get("stationQueueTimes")
-    if station_times and merged.get("station_queue_times") is None:
-        merged["station_queue_times"] = station_times
-    return merged
+# _merge_maintenance callers now use merge_ride_builder_maintenance directly

Also applies to: 370-370

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/openrct2_mcp/ride_ops.py` around lines 283 - 308, Extract the
duplicated maintenance merge logic from _merge_maintenance and
_merge_ride_builder_maintenance into one shared helper, preserving the existing
field mappings, None checks, activeBreakdown handling, and stationQueueTimes
behavior. Update both callers to use that helper, and avoid cross-module imports
of private _load_ride_builder_maintenance_index by renaming it to a public
symbol or relocating it to the shared utility module.
🧹 Nitpick comments (6)
mcp-server/tests/test_bridge_fast_api115.py (1)

1-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tests are correct; consider adding a None row edge case.

The two tests cover the main merge behaviors (fill gaps, preserve bridge values). A test for ride_builder_row=None would verify the early-return path and guard against future changes that break the None guard.

✅ Suggested additional test
+    def test_returns_summary_unchanged_when_row_is_none(self):
+        summary = {"id": 5, "reliability": 80.0, "guest_count": 3}
+        merged = _merge_ride_builder_maintenance(summary, None)
+        self.assertEqual(merged, summary)
+        self.assertIs(merged, summary)  # should return the same object
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/tests/test_bridge_fast_api115.py` around lines 1 - 31, Add a
unittest for _merge_ride_builder_maintenance that passes a None ride-builder
row, and assert it returns the original summary unchanged, covering the
early-return guard without altering the existing merge behavior tests.
mcp-server/tests/test_time_tools.py (1)

22-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a fallback-path test.

The happy-path test is solid, but there's no coverage for when ride_builder is None or when the plugin call raises an exception — both should fall back to known_speed with game_speed_source: "session". A regression in the fallback path could go undetected.

✅ Suggested additional test
+    def test_falls_back_to_session_when_plugin_fails(self):
+        from openrct2_mcp.time_tools import game_time_status
+        from openrct2_mcp.connection import GameSpeed
+
+        class _FailingRideBuilder:
+            def call(self, endpoint: str):
+                raise RuntimeError("plugin unavailable")
+
+        status = game_time_status(
+            self._FakeGame(),
+            known_speed=GameSpeed.FAST,
+            ride_builder=_FailingRideBuilder(),
+        )
+        self.assertEqual(status["game_speed"], int(GameSpeed.FAST))
+        self.assertEqual(status["game_speed_source"], "session")
+
+    def test_falls_back_when_no_ride_builder(self):
+        from openrct2_mcp.time_tools import game_time_status
+        from openrct2_mcp.connection import GameSpeed
+
+        status = game_time_status(
+            self._FakeGame(),
+            known_speed=GameSpeed.NORMAL,
+            ride_builder=None,
+        )
+        self.assertEqual(status["game_speed_source"], "session")
+        self.assertIsNone(status.get("game_speed_note"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/tests/test_time_tools.py` around lines 22 - 41, Add fallback-path
coverage to GameTimeStatusTests for game_time_status when ride_builder is absent
and when its plugin call raises an exception; assert both cases use known_speed
with game_speed_source set to "session", while preserving the existing
plugin-success test.
mcp-server/openrct2_mcp/ride_ops.py (1)

373-448: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Double-loading of maintenance index and ride data in list_refurbish_candidates.

list_rides_fast (called at line 387) internally calls _load_ride_builder_maintenance_index and fetches each ride's raw data via get_ride_raw. Then list_refurbish_candidates separately calls _load_ride_builder_maintenance (line 383, which delegates to the same index loader) and get_ride_raw again per ride (line 393). This doubles the network calls to the ride-builder plugin and bridge for every invocation.

Consider reusing the summaries already returned by list_rides_fast (which now contain merged maintenance fields) instead of re-fetching and re-merging.

♻️ Proposed refactor: reuse merged summaries
 def list_refurbish_candidates(
     game: RCT2,
     ride_builder: RideBuilderClient,
     *,
     limit: int = 20,
     reliability_threshold: float = DEFAULT_RELIABILITY_REFURBISH_THRESHOLD,
     downtime_threshold: float = DEFAULT_DOWNTIME_REFURBISH_THRESHOLD,
     rides_only: bool = True,
 ) -> dict[str, Any]:
     """List rides ranked for refurbish using downtime and reliability together."""
-    rb_rows = _load_ride_builder_maintenance(ride_builder)
-    reliability_available = any(row.get("reliability") is not None for row in rb_rows.values())
+    summaries = list_rides_fast(game, ride_builder)
+    reliability_available = any(s.get("reliability") is not None for s in summaries)

     candidates: list[dict[str, Any]] = []
-    for summary in list_rides_fast(game, ride_builder):
+    for summary in summaries:
         classification = str(summary.get("classification") or "").lower()
         if rides_only and classification in ("stall", "facility"):
             continue

         ride_id = summary["id"]
-        raw = get_ride_raw(game, ride_id) or {}
-        maintenance = _merge_maintenance(
-            ride_maintenance_from_raw(raw),
-            rb_rows.get(ride_id),
-        )
+        # summary already has merged maintenance fields from list_rides_fast
+        maintenance = {
+            "downtime": summary.get("downtime"),
+            "reliability": summary.get("reliability"),
+            "age_months": summary.get("age_months"),
+            "breakdown": summary.get("breakdown"),
+            "active_breakdown": summary.get("active_breakdown"),
+        }
         score, reasons = refurbish_need_score(
             maintenance,
             reliability_threshold=reliability_threshold,
             downtime_threshold=downtime_threshold,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/openrct2_mcp/ride_ops.py` around lines 373 - 448, Update
list_refurbish_candidates to reuse the maintenance fields already returned in
each summary by list_rides_fast, removing the separate
_load_ride_builder_maintenance call and per-ride get_ride_raw fetch. Derive
reliability_available from those summaries and pass each summary’s merged
maintenance data to refurbish_need_score and recommend_refurbish while
preserving candidate ranking, thresholds, and output behavior.
plugins/ride-builder/src/ride-builder.js (2)

199-211: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

readReliabilityPercent's early return skips the normalization applied to the fallback keys.

The fallback keys (reliabilityPercentage, reliability_percentage) are defensively clamped/converted (value <= 100 ? value : Math.round(value / 655.35)), but the new ride.reliability fast-path trusts the .d.ts contract ("0–100") unconditionally. If a future/variant OpenRCT2 build ever exposes reliability on the same raw 0–65535 scale as the legacy fields, this path would silently return an out-of-range percentage instead of correcting it like the fallback does.

♻️ Suggested fix: route through the same normalization
     function readReliabilityPercent(ride) {
-        if (typeof ride.reliability === "number" && Number.isFinite(ride.reliability)) {
-            return ride.reliability;
-        }
-        const keys = ["reliabilityPercentage", "reliability_percentage"];
+        const keys = ["reliability", "reliabilityPercentage", "reliability_percentage"];
         for (let i = 0; i < keys.length; i++) {
             const value = ride[keys[i]];
             if (typeof value === "number" && Number.isFinite(value)) {
                 return value <= 100 ? value : Math.round(value / 655.35);
             }
         }
         return null;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/ride-builder/src/ride-builder.js` around lines 199 - 211, Update
readReliabilityPercent so ride.reliability uses the same normalization as
reliabilityPercentage and reliability_percentage: return it unchanged when at
most 100, otherwise convert it with the existing 655.35 scaling and rounding.
Preserve the finite-number validation and fallback-key behavior.

365-395: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

handleGetElementsInRect's native branch bypasses the summarizers that the fallback and the guest-rect handler both use.

handleGetGuestsInRect maps both the native (map.getGuestsInRect) and fallback (scanGuestsInRect) paths through serializeGuestNearTile, so callers get a consistent shape either way. handleGetElementsInRect, however, returns raw map.getElementsInRect(type, bounds) output when that native function exists, but routes the fallback through summarizeFootpathElement/summarizeTrackElement/summarizeEntranceElement (which add tileX/tileY and filter fields like isAdditionFull). Today this is dead code since the native function was never merged per the PR description, but if it lands later with a different element shape, downstream consumers (map_region.py, the MCP tool) would silently receive a differently-shaped payload.

♻️ Suggested fix: normalize the native path too (adjust field extraction once the native shape is known)
         if (typeof map.getElementsInRect === "function") {
-            return map.getElementsInRect(type, bounds);
+            return map.getElementsInRect(type, bounds).map(({ tileX, tileY, element }) => {
+                if (type === "footpath") return summarizeFootpathElement(tileX, tileY, element);
+                if (type === "track") return summarizeTrackElement(tileX, tileY, element);
+                return summarizeEntranceElement(tileX, tileY, element);
+            });
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/ride-builder/src/ride-builder.js` around lines 365 - 395, Normalize
the native result in handleGetElementsInRect before returning it, using the same
type-specific summarizers as scanElementsInRect: summarizeFootpathElement,
summarizeTrackElement, or summarizeEntranceElement. Ensure both native and
fallback paths return the same summarized shape, including tile coordinates and
filtered fields.
mcp-server/openrct2_mcp/server.py (1)

203-235: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant getGameSpeed round trip.

openrct2_status calls ride_builder.call("getGameSpeed") to extract apiVersion, then immediately calls game_time_status(..., ride_builder=ride_builder), which (per time_tools.py) calls the same "getGameSpeed" endpoint again just to read gameSpeed from the same payload shape. This is a redundant plugin round trip on every status check, and introduces a small window where the two calls could theoretically observe different speeds if the speed changes between them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/openrct2_mcp/server.py` around lines 203 - 235, Update
openrct2_status to make a single getGameSpeed call, reuse its payload for both
plugin_api_version and game_time_status, and adjust the game_time_status
interface or invocation as needed so it does not issue a second ride_builder
request while preserving existing status values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mcp-server/openrct2_mcp/ride_ops.py`:
- Line 12: Eliminate the duplicated maintenance merge logic by renaming
bridge_fast’s _merge_ride_builder_maintenance to public
merge_ride_builder_maintenance and updating ride_ops’ _merge_maintenance callers
to use it. Rename _load_ride_builder_maintenance_index to public
load_ride_builder_maintenance_index and update the ride_ops import and
references accordingly, preserving existing merge behavior.

In `@plugins/ride-builder/src/ride-builder.js`:
- Around line 259-269: Update normalizeRectBounds to enforce a hard maximum
rectangle width and height before returning normalized bounds, rejecting
oversized requests with null. Ensure this validation applies to all callers,
including scanElementsInRect, scanGuestsInRect, and native rectangle scans, so
raw socket clients cannot trigger unbounded synchronous tile iteration.

---

Outside diff comments:
In `@mcp-server/openrct2_mcp/ride_ops.py`:
- Around line 283-308: Extract the duplicated maintenance merge logic from
_merge_maintenance and _merge_ride_builder_maintenance into one shared helper,
preserving the existing field mappings, None checks, activeBreakdown handling,
and stationQueueTimes behavior. Update both callers to use that helper, and
avoid cross-module imports of private _load_ride_builder_maintenance_index by
renaming it to a public symbol or relocating it to the shared utility module.

---

Nitpick comments:
In `@mcp-server/openrct2_mcp/ride_ops.py`:
- Around line 373-448: Update list_refurbish_candidates to reuse the maintenance
fields already returned in each summary by list_rides_fast, removing the
separate _load_ride_builder_maintenance call and per-ride get_ride_raw fetch.
Derive reliability_available from those summaries and pass each summary’s merged
maintenance data to refurbish_need_score and recommend_refurbish while
preserving candidate ranking, thresholds, and output behavior.

In `@mcp-server/openrct2_mcp/server.py`:
- Around line 203-235: Update openrct2_status to make a single getGameSpeed
call, reuse its payload for both plugin_api_version and game_time_status, and
adjust the game_time_status interface or invocation as needed so it does not
issue a second ride_builder request while preserving existing status values.

In `@mcp-server/tests/test_bridge_fast_api115.py`:
- Around line 1-31: Add a unittest for _merge_ride_builder_maintenance that
passes a None ride-builder row, and assert it returns the original summary
unchanged, covering the early-return guard without altering the existing merge
behavior tests.

In `@mcp-server/tests/test_time_tools.py`:
- Around line 22-41: Add fallback-path coverage to GameTimeStatusTests for
game_time_status when ride_builder is absent and when its plugin call raises an
exception; assert both cases use known_speed with game_speed_source set to
"session", while preserving the existing plugin-success test.

In `@plugins/ride-builder/src/ride-builder.js`:
- Around line 199-211: Update readReliabilityPercent so ride.reliability uses
the same normalization as reliabilityPercentage and reliability_percentage:
return it unchanged when at most 100, otherwise convert it with the existing
655.35 scaling and rounding. Preserve the finite-number validation and
fallback-key behavior.
- Around line 365-395: Normalize the native result in handleGetElementsInRect
before returning it, using the same type-specific summarizers as
scanElementsInRect: summarizeFootpathElement, summarizeTrackElement, or
summarizeEntranceElement. Ensure both native and fallback paths return the same
summarized shape, including tile coordinates and filtered fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b5579337-28bf-45c4-a5cc-e979866ec75e

📥 Commits

Reviewing files that changed from the base of the PR and between ed363c6 and b164207.

📒 Files selected for processing (13)
  • mcp-server/openrct2_mcp/bridge_fast.py
  • mcp-server/openrct2_mcp/guest_intel.py
  • mcp-server/openrct2_mcp/map_region.py
  • mcp-server/openrct2_mcp/ride_ops.py
  • mcp-server/openrct2_mcp/scenery_tools.py
  • mcp-server/openrct2_mcp/server.py
  • mcp-server/openrct2_mcp/time_tools.py
  • mcp-server/tests/test_bridge_fast_api115.py
  • mcp-server/tests/test_map_region_api115.py
  • mcp-server/tests/test_scenery_tools.py
  • mcp-server/tests/test_time_tools.py
  • plugins/ride-builder/lib/openrct2.d.ts
  • plugins/ride-builder/src/ride-builder.js

Comment thread mcp-server/openrct2_mcp/ride_ops.py Outdated
Comment thread plugins/ride-builder/src/ride-builder.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mcp-server/openrct2_mcp/ride_ops.py (1)

283-309: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate merge logic across ride_ops.py and bridge_fast.py.

_merge_maintenance here is functionally identical to bridge_fast._merge_ride_builder_maintenance (same field list, same "fill only when missing" semantics). This PR extended both copies in parallel with the same 6 new keys — exactly the drift risk duplication creates. _load_ride_builder_maintenance right below already shows the intended pattern (delegate to the bridge_fast helper); _merge_maintenance should do the same.

♻️ Proposed fix
-def _merge_maintenance(
-    maintenance: dict[str, Any],
-    ride_builder_row: dict[str, Any] | None,
-) -> dict[str, Any]:
-    """Prefer ride-builder maintenance stats when the bridge omits reliability."""
-    merged = dict(maintenance)
-    if ride_builder_row is None:
-        return merged
-    for key, rb_key in (
-        ("downtime", "downtime"),
-        ("reliability", "reliability"),
-        ("age_months", "age"),
-        ("breakdown", "breakdown"),
-        ("guest_count", "guestCount"),
-        ("is_empty", "isEmpty"),
-        ("income_per_hour", "incomePerHour"),
-        ("profit", "profit"),
-        ("queue_time", "queueTime"),
-    ):
-        if merged.get(key) is None and ride_builder_row.get(rb_key) is not None:
-            merged[key] = ride_builder_row.get(rb_key)
-    if ride_builder_row.get("activeBreakdown"):
-        merged["active_breakdown"] = True
-    station_times = ride_builder_row.get("stationQueueTimes")
-    if station_times and merged.get("station_queue_times") is None:
-        merged["station_queue_times"] = station_times
-    return merged
+def _merge_maintenance(
+    maintenance: dict[str, Any],
+    ride_builder_row: dict[str, Any] | None,
+) -> dict[str, Any]:
+    """Prefer ride-builder maintenance stats when the bridge omits reliability."""
+    return _merge_ride_builder_maintenance(maintenance, ride_builder_row)

(requires importing _merge_ride_builder_maintenance from bridge_fast, alongside _load_ride_builder_maintenance_index)

Also applies to: 369-370

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/openrct2_mcp/ride_ops.py` around lines 283 - 309, Replace the
duplicated field-by-field logic in _merge_maintenance with a delegation to
bridge_fast._merge_ride_builder_maintenance, importing that helper alongside
_load_ride_builder_maintenance_index. Preserve the existing None handling and
return behavior, including the unchanged result when ride_builder_row is absent.
🧹 Nitpick comments (4)
mcp-server/tests/test_map_region_api115.py (1)

24-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for width/height clamping behavior.

The clamping logic (max(1, min(width, MAX_REGION_SIDE))) in get_elements_in_rect is untested. Edge cases like width=0, width=-5, and width=100 should be covered to verify the bounds are correctly clamped to 1 and 40 respectively.

♻️ Suggested additional tests
    def test_clamps_oversized_width_and_height(self):
        rb = MagicMock()
        rb.call.return_value = []
        result = get_elements_in_rect(rb, "footpath", 0, 0, 100, 100)
        rb.call.assert_called_once_with(
            "getElementsInRect",
            {
                "type": "footpath",
                "bounds": {"minX": 0, "minY": 0, "maxX": 39, "maxY": 39},
            },
        )

    def test_clamps_zero_width_and_height(self):
        rb = MagicMock()
        rb.call.return_value = []
        result = get_elements_in_rect(rb, "track", 5, 5, 0, 0)
        rb.call.assert_called_once_with(
            "getElementsInRect",
            {
                "type": "track",
                "bounds": {"minX": 5, "minY": 5, "maxX": 5, "maxY": 5},
            },
        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/tests/test_map_region_api115.py` around lines 24 - 26, Add tests
alongside test_rejects_unknown_element_type for get_elements_in_rect covering
oversized dimensions and zero or negative dimensions, asserting the backend call
clamps each side to MAX_REGION_SIDE (40) and the minimum side length of 1,
including the expected bounds.
mcp-server/openrct2_mcp/guest_intel.py (1)

97-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the exception instead of silently swallowing it.

The broad except Exception: pass makes it impossible to distinguish between "plugin path legitimately unavailable" and "plugin path has a bug that always fails." If the plugin scan consistently errors, users will silently get the slower bridge-scan heuristic with no indication. At minimum, log the exception at debug or warning level before falling back.

♻️ Suggested refactor
         except Exception as exc:
-            pass
+            logger.debug("ride-builder getGuestsInRect failed, falling back to bridge scan: %s", exc)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/openrct2_mcp/guest_intel.py` around lines 97 - 98, Update the
broad exception handler in the plugin scan flow to log the caught exception at
debug or warning level before falling back, while preserving the existing
fallback behavior. Use the surrounding plugin path detection function or scan
symbol to locate this handler, and include enough context to distinguish a
missing plugin path from a scan failure.

Source: Linters/SAST tools

plugins/ride-builder/src/ride-builder.js (1)

365-381: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Native-API fallback for elements skips the summarization guests get.

handleGetGuestsInRect's native path maps results through serializeGuestNearTile to keep a consistent shape. handleGetElementsInRect's native path (map.getElementsInRect) returns the raw result unmodified — inconsistent with the tile-scan fallback's summarized shape (tileX, tileY, additionStatus, isAdditionFull, etc.). Currently unreachable (native map.getElementsInRect doesn't exist per the comment above), but will silently change the response contract once it lands.

♻️ Proposed fix: summarize the native result too
         if (typeof map.getElementsInRect === "function") {
-            return map.getElementsInRect(type, bounds);
+            const summarize = type === "footpath" ? summarizeFootpathElement
+                : type === "track" ? summarizeTrackElement
+                : summarizeEntranceElement;
+            return map.getElementsInRect(type, bounds).map(el => summarize(el.tileX ?? el.x, el.tileY ?? el.y, el));
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/ride-builder/src/ride-builder.js` around lines 365 - 381, Update the
native path in handleGetElementsInRect to map each map.getElementsInRect result
through the same element summarization used by scanElementsInRect, preserving
the consistent tileX, tileY, additionStatus, isAdditionFull, and related
response shape across both paths.
mcp-server/openrct2_mcp/ride_ops.py (1)

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cross-module import of a private helper.

_load_ride_builder_maintenance_index is imported here with its underscore intact, signalling module-private in bridge_fast.py yet now used cross-module. Consider dropping the leading underscore in bridge_fast.py (or re-exporting explicitly) to make the intended public surface clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/openrct2_mcp/ride_ops.py` at line 12, Make the maintenance-index
helper’s cross-module API intentional: rename
_load_ride_builder_maintenance_index in bridge_fast.py to a public name and
update its callers, including ride_ops.py, or explicitly re-export it through
bridge_fast.py while preserving the existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mcp-server/openrct2_mcp/map_region.py`:
- Around line 44-45: Normalize the response’s elements field to an empty list
whenever elements is not a list, matching the existing count behavior. Update
the elements assignment alongside the count calculation so non-list payloads
such as None produce count 0 and elements [].

In `@mcp-server/openrct2_mcp/ride_ops.py`:
- Around line 101-121: Update _ride_occupancy_from_plugin to validate that the
result returned by ride_builder.call is a dictionary before accessing guestCount
or isEmpty. If the payload is not a dict, return (None, None), preserving the
existing fallback behavior for plugin failures and malformed responses.
- Around line 214-233: Update the wait-timeout handling in the ride operation
loop so a ride that remains occupied records or reports a meaningful
occupancy-based failure before returning the timeout error. Ensure the final
diagnostic does not fall back to “unknown error” when
_ride_occupancy_from_plugin indicates guests remain, while preserving existing
_attempt() error reporting for other failure paths.

In `@mcp-server/openrct2_mcp/server.py`:
- Around line 1355-1373: Update the docstring for get_map_elements_in_rect_tool
to replace the Unicode multiplication sign in “40×40” with the ASCII “x”,
preserving the rest of the documentation unchanged.

In `@plugins/ride-builder/src/ride-builder.js`:
- Around line 259-269: Update normalizeRectBounds, used by scanElementsInRect
and scanGuestsInRect, to clamp each rectangle’s X and Y span to the plugin’s
established maximum before returning normalized bounds. Ensure direct
getElementsInRect/getGuestsInRect callers, including uncapped radius requests,
cannot trigger scans beyond that limit while preserving valid bounds and
existing min/max normalization.

---

Outside diff comments:
In `@mcp-server/openrct2_mcp/ride_ops.py`:
- Around line 283-309: Replace the duplicated field-by-field logic in
_merge_maintenance with a delegation to
bridge_fast._merge_ride_builder_maintenance, importing that helper alongside
_load_ride_builder_maintenance_index. Preserve the existing None handling and
return behavior, including the unchanged result when ride_builder_row is absent.

---

Nitpick comments:
In `@mcp-server/openrct2_mcp/guest_intel.py`:
- Around line 97-98: Update the broad exception handler in the plugin scan flow
to log the caught exception at debug or warning level before falling back, while
preserving the existing fallback behavior. Use the surrounding plugin path
detection function or scan symbol to locate this handler, and include enough
context to distinguish a missing plugin path from a scan failure.

In `@mcp-server/openrct2_mcp/ride_ops.py`:
- Line 12: Make the maintenance-index helper’s cross-module API intentional:
rename _load_ride_builder_maintenance_index in bridge_fast.py to a public name
and update its callers, including ride_ops.py, or explicitly re-export it
through bridge_fast.py while preserving the existing behavior.

In `@mcp-server/tests/test_map_region_api115.py`:
- Around line 24-26: Add tests alongside test_rejects_unknown_element_type for
get_elements_in_rect covering oversized dimensions and zero or negative
dimensions, asserting the backend call clamps each side to MAX_REGION_SIDE (40)
and the minimum side length of 1, including the expected bounds.

In `@plugins/ride-builder/src/ride-builder.js`:
- Around line 365-381: Update the native path in handleGetElementsInRect to map
each map.getElementsInRect result through the same element summarization used by
scanElementsInRect, preserving the consistent tileX, tileY, additionStatus,
isAdditionFull, and related response shape across both paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b5579337-28bf-45c4-a5cc-e979866ec75e

📥 Commits

Reviewing files that changed from the base of the PR and between ed363c6 and b164207.

📒 Files selected for processing (13)
  • mcp-server/openrct2_mcp/bridge_fast.py
  • mcp-server/openrct2_mcp/guest_intel.py
  • mcp-server/openrct2_mcp/map_region.py
  • mcp-server/openrct2_mcp/ride_ops.py
  • mcp-server/openrct2_mcp/scenery_tools.py
  • mcp-server/openrct2_mcp/server.py
  • mcp-server/openrct2_mcp/time_tools.py
  • mcp-server/tests/test_bridge_fast_api115.py
  • mcp-server/tests/test_map_region_api115.py
  • mcp-server/tests/test_scenery_tools.py
  • mcp-server/tests/test_time_tools.py
  • plugins/ride-builder/lib/openrct2.d.ts
  • plugins/ride-builder/src/ride-builder.js

Comment thread mcp-server/openrct2_mcp/map_region.py Outdated
Comment thread mcp-server/openrct2_mcp/ride_ops.py
Comment thread mcp-server/openrct2_mcp/ride_ops.py
Comment thread mcp-server/openrct2_mcp/server.py
Comment thread plugins/ride-builder/src/ride-builder.js
Dedupe maintenance merge helpers, reuse ride summaries for refurbish ranking, avoid double getGameSpeed, and harden ride-builder rect scans.
Normalize non-list map payloads, clamp plugin rect bounds, validate occupancy responses, and improve timeout diagnostics.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugins/ride-builder/src/ride-builder.js (1)

226-241: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve unknown occupancy as unknown.

When guestCount is unavailable and ride.isEmpty is not a boolean, Line [241] returns false because null === 0 is false. Downstream bridge/refurbishment code uses None to represent unknown occupancy, so this incorrectly reports an empty-state result as authoritative.

Suggested fix
-        const guestCount = typeof ride.guestCount === "number" ? ride.guestCount : null;
+        const guestCount = Number.isFinite(ride.guestCount) ? ride.guestCount : null;
...
-            isEmpty: typeof ride.isEmpty === "boolean" ? ride.isEmpty : guestCount === 0,
+            isEmpty: typeof ride.isEmpty === "boolean"
+                ? ride.isEmpty
+                : guestCount == null ? null : guestCount === 0,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/ride-builder/src/ride-builder.js` around lines 226 - 241, The
occupancy fallback in the ride mapping must preserve an unknown state: update
the isEmpty calculation in the returned object so it uses ride.isEmpty when
boolean, returns true or false only when guestCount is numeric, and otherwise
returns null/None rather than comparing null to zero.
🧹 Nitpick comments (1)
mcp-server/openrct2_mcp/time_tools.py (1)

102-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Catch ConnectionError here instead of Exception. RideBuilderClient.call() only raises ConnectionError on request failure, so this keeps the fallback working without swallowing unrelated bugs. The failure test should raise ConnectionError to match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/openrct2_mcp/time_tools.py` around lines 102 - 106, Update the
exception handler around ride_builder.call("getGameSpeed") to catch only
ConnectionError, allowing unrelated exceptions to propagate; also update the
associated failure test to raise ConnectionError instead of a generic exception.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mcp-server/openrct2_mcp/server.py`:
- Around line 212-218: In the getGameSpeed handling within openrct2_status,
catch only ConnectionError for transport failures and set speed_payload and
plugin_api to None there. Before accessing apiVersion, require speed_payload to
be a dict, while preserving the integer check; allow malformed payloads and
unrelated exceptions to propagate instead of silently suppressing them.

In `@plugins/ride-builder/src/ride-builder.js`:
- Around line 259-276: Update normalizeRectBounds to validate bounds.minX,
bounds.maxX, bounds.minY, and bounds.maxY as finite integers before applying
Math.min or Math.max. Return null for strings, booleans, null, fractional, or
otherwise invalid coordinates, while preserving the existing normalization and
maximum-side logic for valid integer bounds.

---

Outside diff comments:
In `@plugins/ride-builder/src/ride-builder.js`:
- Around line 226-241: The occupancy fallback in the ride mapping must preserve
an unknown state: update the isEmpty calculation in the returned object so it
uses ride.isEmpty when boolean, returns true or false only when guestCount is
numeric, and otherwise returns null/None rather than comparing null to zero.

---

Nitpick comments:
In `@mcp-server/openrct2_mcp/time_tools.py`:
- Around line 102-106: Update the exception handler around
ride_builder.call("getGameSpeed") to catch only ConnectionError, allowing
unrelated exceptions to propagate; also update the associated failure test to
raise ConnectionError instead of a generic exception.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 511f1c89-a7f1-4c74-bdb3-cef306382f90

📥 Commits

Reviewing files that changed from the base of the PR and between b164207 and eab9538.

📒 Files selected for processing (10)
  • mcp-server/openrct2_mcp/bridge_fast.py
  • mcp-server/openrct2_mcp/guest_intel.py
  • mcp-server/openrct2_mcp/map_region.py
  • mcp-server/openrct2_mcp/ride_ops.py
  • mcp-server/openrct2_mcp/server.py
  • mcp-server/openrct2_mcp/time_tools.py
  • mcp-server/tests/test_bridge_fast_api115.py
  • mcp-server/tests/test_map_region_api115.py
  • mcp-server/tests/test_time_tools.py
  • plugins/ride-builder/src/ride-builder.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • mcp-server/tests/test_map_region_api115.py
  • mcp-server/tests/test_bridge_fast_api115.py
  • mcp-server/openrct2_mcp/bridge_fast.py
  • mcp-server/openrct2_mcp/guest_intel.py
  • mcp-server/openrct2_mcp/map_region.py
  • mcp-server/openrct2_mcp/ride_ops.py

Comment thread mcp-server/openrct2_mcp/server.py
Comment thread plugins/ride-builder/src/ride-builder.js
Catch only ConnectionError for plugin speed reads, require integer rect bounds, and keep unknown isEmpty as null.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mcp-server/openrct2_mcp/time_tools.py`:
- Line 11: Alias the project-local ConnectionError as PluginConnectionError in
mcp-server/openrct2_mcp/time_tools.py at line 11, then update its related except
and raise sites to use the alias. Apply the same import alias and exception-site
updates in mcp-server/tests/test_time_tools.py at line 35.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44590e9f-110b-4eb8-9e32-0156217ee808

📥 Commits

Reviewing files that changed from the base of the PR and between eab9538 and 22696bd.

📒 Files selected for processing (4)
  • mcp-server/openrct2_mcp/server.py
  • mcp-server/openrct2_mcp/time_tools.py
  • mcp-server/tests/test_time_tools.py
  • plugins/ride-builder/src/ride-builder.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • mcp-server/openrct2_mcp/server.py
  • plugins/ride-builder/src/ride-builder.js

Comment thread mcp-server/openrct2_mcp/time_tools.py Outdated
from pyrct2.client import RCT2

from openrct2_mcp.connection import ensure_paused, ensure_unpaused
from openrct2_mcp.connection import ConnectionError, ensure_paused, ensure_unpaused

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## time_tools.py\n'
cat -n mcp-server/openrct2_mcp/time_tools.py | sed -n '1,220p'

printf '\n## test_time_tools.py\n'
cat -n mcp-server/tests/test_time_tools.py | sed -n '1,220p'

printf '\n## ConnectionError imports/usages\n'
rg -n --no-heading '\bConnectionError\b' mcp-server/openrct2_mcp mcp-server/tests

Repository: BenDaSpur/openrct2-mcp

Length of output: 10484


Alias the project-local ConnectionError. This shadows the builtin in both mcp-server/openrct2_mcp/time_tools.py and mcp-server/tests/test_time_tools.py; import it as PluginConnectionError and update the except / raise sites.

🧰 Tools
🪛 Ruff (0.15.21)

[error] 11-11: Import ConnectionError is shadowing a Python builtin

(A004)

📍 Affects 2 files
  • mcp-server/openrct2_mcp/time_tools.py#L11-L11 (this comment)
  • mcp-server/tests/test_time_tools.py#L35-L35
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp-server/openrct2_mcp/time_tools.py` at line 11, Alias the project-local
ConnectionError as PluginConnectionError in
mcp-server/openrct2_mcp/time_tools.py at line 11, then update its related except
and raise sites to use the alias. Apply the same import alias and exception-site
updates in mcp-server/tests/test_time_tools.py at line 35.

Source: Linters/SAST tools

@BenDaSpur
BenDaSpur merged commit 22b492f into main Jul 20, 2026
2 checks passed
@BenDaSpur
BenDaSpur deleted the feat/plugin-api-v115-mcp branch July 20, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant