Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion mcp-server/openrct2_mcp/bridge_fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def ride_summary_from_raw(raw: dict[str, Any]) -> dict[str, Any]:
intensity = raw.get("intensity")
nausea = raw.get("nausea")
maintenance = ride_maintenance_from_raw(raw)
guest_count = raw.get("guestCount")
return {
"id": raw.get("id"),
"name": raw.get("name"),
Expand All @@ -94,12 +95,18 @@ def ride_summary_from_raw(raw: dict[str, Any]) -> dict[str, Any]:
"price": primary_ride_price(raw.get("price")),
"satisfaction": raw.get("satisfaction"),
"breakdown": maintenance["breakdown"],
"active_breakdown": maintenance["active_breakdown"],
"downtime": maintenance["downtime"],
"reliability": maintenance["reliability"],
"age_months": maintenance["age_months"],
"guest_count": guest_count,
"is_empty": raw.get("isEmpty") if "isEmpty" in raw else (guest_count == 0 if guest_count is not None else None),
"income_per_hour": raw.get("incomePerHour"),
"profit": raw.get("profit"),
"inspection_interval": raw.get("inspectionInterval"),
"minimum_waiting_time": raw.get("minimumWaitingTime"),
"maximum_waiting_time": raw.get("maximumWaitingTime"),
"queue_time": raw.get("queueTime"),
}


Expand All @@ -115,14 +122,59 @@ def get_ride_raw(game: RCT2, ride_id: int) -> dict[str, Any] | None:
return resp["payload"]


def merge_ride_builder_maintenance(
summary: dict[str, Any],
ride_builder_row: dict[str, Any] | None,
) -> dict[str, Any]:
"""Fill bridge gaps from ride-builder (reliability/occupancy/income from OpenRCT2 #26675)."""
if ride_builder_row is None:
return summary
merged = dict(summary)
for summary_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(summary_key) is None and ride_builder_row.get(rb_key) is not None:
merged[summary_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 load_ride_builder_maintenance_index(ride_builder: RideBuilderClient) -> dict[int, dict[str, Any]]:
try:
rows = ride_builder.call("listRideMaintenance")
except Exception:
return {}
if not isinstance(rows, list):
return {}
by_id: dict[int, dict[str, Any]] = {}
for row in rows:
if isinstance(row, dict) and isinstance(row.get("rideId"), int):
by_id[row["rideId"]] = row
return by_id


def list_rides_fast(game: RCT2, ride_builder: RideBuilderClient) -> list[dict[str, Any]]:
"""List all rides using listAllRides + per-id queries (scales with ride count, not map size)."""
index = ride_builder.call("listAllRides")
rb_rows = load_ride_builder_maintenance_index(ride_builder)
summaries: list[dict[str, Any]] = []
for entry in index:
raw = get_ride_raw(game, entry["id"])
if raw is not None:
summaries.append(ride_summary_from_raw(raw))
summary = ride_summary_from_raw(raw)
summaries.append(merge_ride_builder_maintenance(summary, rb_rows.get(entry["id"])))
summaries.sort(key=lambda r: r.get("excitement") or 0, reverse=True)
return summaries

Expand Down
41 changes: 38 additions & 3 deletions mcp-server/openrct2_mcp/guest_intel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import logging
import re
from typing import Any

Expand All @@ -12,6 +13,8 @@
from openrct2_mcp.connection import RideBuilderClient
from openrct2_mcp.map_region import get_path_graph

logger = logging.getLogger(__name__)


def _ride_tile_map(game: RCT2, ride_builder: RideBuilderClient) -> dict[int, list[int]]:
mapping: dict[int, list[int]] = {}
Expand Down Expand Up @@ -69,8 +72,40 @@ def get_complaint_hotspots(game: RCT2, ride_builder: RideBuilderClient) -> dict[
return {"hotspot_count": len(hotspots), "by_category": by_category, "hotspots": hotspots[:30]}


def sample_guests_near_tile(game: RCT2, tile_x: int, tile_y: int, radius: int = 3, limit: int = 10) -> dict:
"""Sample guests by scanning entity ids near a tile (best-effort, no full scan)."""
def sample_guests_near_tile(
game: RCT2,
tile_x: int,
tile_y: int,
radius: int = 3,
limit: int = 10,
ride_builder: RideBuilderClient | None = None,
) -> dict:
"""Sample guests near a tile via ride-builder rect scan, with bridge fallback."""
if ride_builder is not None:
try:
bounds = {
"minX": tile_x - radius,
"minY": tile_y - radius,
"maxX": tile_x + radius,
"maxY": tile_y + radius,
}
guests = ride_builder.call("getGuestsInRect", {"bounds": bounds})
if isinstance(guests, list):
return {
"tile": [tile_x, tile_y],
"radius": radius,
"guests": guests[:limit],
"source": "plugin",
}
except Exception:
logger.debug(
"getGuestsInRect failed near tile (%s, %s) radius=%s; falling back to bridge scan",
tile_x,
tile_y,
radius,
exc_info=True,
)

# Bridge has no spatial guest query; sample low ids as heuristic peep pool.
found: list[dict] = []
center = Tile(tile_x, tile_y)
Expand All @@ -95,7 +130,7 @@ def sample_guests_near_tile(game: RCT2, tile_x: int, tile_y: int, radius: int =
)
except Exception:
continue
return {"tile": [tile_x, tile_y], "radius": radius, "guests": found}
return {"tile": [tile_x, tile_y], "radius": radius, "guests": found, "source": "bridge_scan"}


def guest_flow_summary(game: RCT2) -> dict[str, Any]:
Expand Down
37 changes: 37 additions & 0 deletions mcp-server/openrct2_mcp/map_region.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,44 @@
from pyrct2.client import RCT2
from pyrct2.world._tile import Tile

from openrct2_mcp.connection import RideBuilderClient

MAX_REGION_SIDE = 40
MAP_ELEMENT_TYPES = ("footpath", "track", "entrance")


def get_elements_in_rect(
ride_builder: RideBuilderClient,
element_type: str,
x: int,
y: int,
width: int,
height: int,
) -> dict[str, Any]:
"""Bulk-export footpath, track, or entrance elements via ride-builder rect scan.

Uses a tile-scan polyfill compatible with OpenRCT2 #26675 develop builds
(native map.getElementsInRect was not merged with that PR).
"""
if element_type not in MAP_ELEMENT_TYPES:
raise ValueError(f"element_type must be one of {MAP_ELEMENT_TYPES}")
width = max(1, min(width, MAX_REGION_SIDE))
height = max(1, min(height, MAX_REGION_SIDE))
bounds = {
"minX": x,
"minY": y,
"maxX": x + width - 1,
"maxY": y + height - 1,
}
elements = ride_builder.call("getElementsInRect", {"type": element_type, "bounds": bounds})
if not isinstance(elements, list):
elements = []
return {
"type": element_type,
"bounds": bounds,
"count": len(elements),
"elements": elements,
}


def get_map_bounds(game: RCT2) -> dict[str, int]:
Expand Down
Loading
Loading