Skip to content

feat(universal): migrate UOR + Hollywood to /resort-areas/*/places#191

Merged
cubehouse merged 14 commits into
mainfrom
feat/universal-places-migration
May 25, 2026
Merged

feat(universal): migrate UOR + Hollywood to /resort-areas/*/places#191
cubehouse merged 14 commits into
mainfrom
feat/universal-places-migration

Conversation

@cubehouse
Copy link
Copy Markdown
Member

Summary

Migrates UniversalOrlando and UniversalStudios (Hollywood) entity, live-data, and showtime sources from the deprecated services.universalorlando.com/api/pointsofinterest endpoint to the UDX-platform /resort-areas/{UOR,USH}/places endpoint + the CDN shows/show-list.json feed. Restores missing Epic Universe + Volcano Bay restaurants reported in the wiki, aligns with what the official Flutter app actually consumes (verified via captured device traffic), and adds per-show showtimes for the first time.

Every UOR + Hollywood entity ID changes — hard cut from legacy numeric IDs (10010, 24000, etc.) to sanitized place_ids (uor.usf, uor.eu, etc.). Park IDs likewise: 10010 → uor.usf, 10000 → uor.ioa, 24000 → uor.eu, 13801 → uor.vb, 13825 → ush.ush. Downstream wiki re-key handled post-merge via the existing npm run migrate -- universalorlando / ... -- universalstudios tool (matches by name + Haversine distance, browser UI confirms each pair, then pushes externalId rewrites preserving wiki UUIDs).

Entity counts (before → after)

Park DEST PARK ATTRACTION SHOW RESTAURANT Total
Universal Orlando 1 → 1 4 → 4 80 → 73 37 → 48 44 → 192 166 → 318
Universal Hollywood 1 → 1 1 → 1 16 → 15 26 → 29 9 → 58 53 → 104

Headline wins

  • Epic Universe restaurants: 0 → 30 (the reported issue — these were entirely absent from the legacy feed).
  • Volcano Bay restaurants: 0 → ~10 (legacy filter dropped CasualDining/FineDining-only).
  • Per-show showtimes now emitted on SHOW entities (was 0 — the legacy POI feed carried no per-day times). UOR: 27 shows with future-time slots.
  • USF restaurants: 2 → 54 (CityWalk, Loews hotels, Epic Universe dining now flow in).

Architecture

Single-file refactor of src/parks/universal/universal.ts, mirroring the pattern already shipping for src/parks/usj/universalstudiosjapan.ts (USJ). Existing UDX OAuth (getUdxToken + injectUdxToken) already authenticates the new endpoint — no new auth code. Park schedules stay on the legacy getVenueSchedule endpoint, with a hard-coded place_id ↔ legacy VenueId translation table (PARK_PLACE_ID_TO_LEGACY_VENUE_ID) for ID continuity until the schedule endpoint itself migrates in a follow-up.

UDX request headers brought in line with the captured Flutter session: user-agent: Dart/3.11 (dart:io), x-channel-type: Mobile, x-uniwebservice-appversion: 2026.5.1.

Two new pure helpers landed with unit tests (TDD): placeToEntity (8 cases) and parseShowTimes (3 cases). Plus an existing-test fix: schedule.test.ts now asserts on the new place_id-based park IDs.

Known caveats

  1. Hollywood Rip Ride Rockit™ (UOR USF) and Fast & Furious: Hollywood Drift (USH) are absent from /resort-areas/{UOR,USH}/places upstream — confirmed by searching the after-snapshot. The official Flutter app doesn't surface them either, so this matches user-facing behaviour, but worth flagging. A manual override list could be added as a follow-up if needed.
  2. ~10 Halloween Horror Nights houses (UOR) dropped — HHN is a paid separately-ticketed seasonal event; the Flutter app omits these from the main rides list too.
  3. USH Lower/Upper Lot orphan parentIds — non-park entities under ush.lower_lot / ush.upper_lot emit with parentIds that don't resolve to any emitted PARK entity (the Hollywood umbrella park is ush.ush). Same orphan-parent pattern as legacy hotel/CityWalk restaurants — not a regression, but a follow-up VENUE_ID_ALIAS: {ush.lower_lot → ush.ush, ush.upper_lot → ush.ush} map would clean this up. Filed out-of-scope here.
  4. Most "removed" attractions in a name-sorted diff are punctuation/trademark renames (em-dash → hyphen, ™ position shifts, colon dropped, etc.) — not real removals.

Captured device traffic

Behavior verified against a fresh Flutter session (darkride session 16274, 2026-05-23). The official Flutter app no longer calls services.universalorlando.com/api/pointsofinterest at all — it's exclusively on /resort-areas/UOR/places + the CDN feeds. Header set (Bearer + x-channel-type: Mobile + Dart/3.11 user-agent) matched verbatim.

Test plan

  • npm run build clean
  • npm test1099 / 1099 pass (11 new tests in places.test.ts)
  • npm run dev -- universalorlando → 4/4
  • npm run dev -- universalstudios → 4/4
  • Pre-merge entity diff captured locally (/tmp/uor-diff/REPORT.md) — counts + name diffs + caveats
  • Post-merge: npm run migrate -- universalorlando then ... -- universalstudios to hand new IDs to the wiki via the existing browser-based migration tool
  • Post-merge: npm run audit:live -- --diff --only=universalorlando,universalstudios to confirm continuity

🤖 Generated with Claude Code

cubehouse and others added 12 commits May 23, 2026 07:59
The server already binds to 0.0.0.0 — only the printed URL was misleading
for users running parksapi on a separate host. Print os.hostname() (or
$MIGRATE_HOST override) so the LAN-reachable URL is clickable.
Update user-agent from Dart/3.6 to Dart/3.11 and add x-channel-type: Mobile
header to match the official Flutter app. This aligns our UDX inject chain
with the current device fingerprint for the new /resort-areas/*/places endpoint.
Mirrors the type set already shipping for USJ. Defines the wire shape
returned by /resort-areas/{UOR,USH}/places and the CDN show-list.json
feed, plus the PLACE_TYPE_TO_ENTITY map for the migration's
entity build. No behavior change yet — wired up in subsequent commits.
Wires up the UDX /resort-areas/{resortKey}/places endpoint plus the
CDN show-list.json feed. Cached behind @cache; no consumer yet — the
buildEntityList / buildLiveData refactors in the next commits use them.
Keeps buildSchedules on the working legacy /schedule endpoint while
exposing new place_id-based park IDs to consumers. Also serves as the
allow-list for which Park-type place records become PARK entities —
CityWalk + Hollywood Upper/Lower Lots are excluded (not theme parks).
Five entries: UOR (usf/ioa/eu/vb) + USH (ush). Easy to delete when the
schedule endpoint itself migrates to the UDX platform in a follow-up.
Maps a UniversalPlace to a wiki Entity. Returns null for place_types
we don't surface (Park handled separately, Shop/Amenity/etc. dropped).
Sanitizes place_id and venue_id via the shared helper so any future
upstream noise can't smuggle disallowed chars into entity IDs.
Replaces the legacy /api/pointsofinterest entity build. Parks emitted
first (filtered to the PARK_PLACE_ID_TO_LEGACY_VENUE_ID allow-list to
drop CityWalk + Hollywood sub-lots), then placeToEntity() handles
attractions / shows / restaurants. Dining filter removed — Epic Universe
and Volcano Bay restaurants are now exposed.

Live data + schedules still reference legacy POI state and break here;
fixed in the next two commits.
The USH umbrella park (ush.ush) carries only a GEOFENCE entry in
geometry.locations[], no map entry. The previous PARK emission code
required a map entry, so ush.ush emitted without location and the
base-class anchor-entity validation rejected it. Fall back to any
geometry entry with lat_lng when no map entry exists — preserves
the strict 'map preferred' behaviour for parks that have one, and
keeps base-class validation happy for those that don't.

Non-park entities (placeToEntity) intentionally do not get this
fallback — their location is best-effort, not validation-critical.
Filters show_times[] to ENABLED future slots and projects them onto
the wiki LiveData showtimes shape. Used by the buildLiveData refactor
in the next commit to expose showtimes for UOR + Hollywood shows for
the first time (the legacy POI feed didn't carry per-day times).
…CDN)

Wait-time CDN entries are now keyed straight to sanitizeId(wait_time_attraction_id),
matching the place_id scheme — no more POI numeric-ID lookups. Showtimes
come from the CDN show-list.json (new — the legacy POI feed had no per-day
times). Express Now offers look up entities by sanitized place_id directly.

The numeric-WaitTime fallback that read from poi.Rides/Shows is dropped
— the new endpoint doesn't carry one. If any ride loses status info as a
result, surface in the pre-merge diff and treat as a separate fix.
Schedule endpoint still hits the legacy /schedule URL by numeric VenueId
(unchanged — that endpoint hasn't migrated), but the emitted
EntitySchedule.id is now the new place_id so it joins up with the
PARK entities buildEntityList emits.
All entity, live-data, and showtime sources now flow through
/resort-areas/*/places + the CDN feeds. Drops fetchPOI / getPOI,
UniversalPOIResponse / UniversalPOIData types, WANTED_DINING_TYPES
+ IGNORE_SHOW_TYPES filters, getFilteredShows, getRideIDFromWaitTimeId,
getParks/fetchParks, and UniversalVenuesResponse. Also removes the
getUniversalAttractionType and shouldIncludeUniversalAttraction
helpers that exclusively served the old POI path. ~200 lines lighter.

Updates schedule.test.ts to assert on new place IDs (uor.usf / ush.ush)
instead of legacy numeric venue IDs, and drops the stale getParks mock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 24, 2026 10:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Migrates Universal Orlando + Universal Studios Hollywood to consume entities/live data from the UDX /resort-areas/{UOR,USH}/places endpoint plus the CDN shows/show-list.json feed, aligning with the official app’s current data sources and enabling per-show showtimes.

Changes:

  • Refactors src/parks/universal/universal.ts to build entities from /places, map IDs to sanitized place_ids, and source showtimes from shows/show-list.json.
  • Updates Universal schedule emission to use place_id park IDs while still fetching hours from the legacy numeric venue schedule endpoint via a translation table.
  • Improves the migration tool server startup logging and adds unit tests for the new pure helpers (placeToEntity, parseShowTimes), plus updates schedule tests for the new park IDs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/tools/migrate/server.ts Adjusts migration server startup logging and host display for easier browser access.
src/parks/universal/universal.ts Core migration: entities via /places, showtimes via CDN feed, ID/key changes, schedule relabeling.
src/parks/universal/tests/schedule.test.ts Updates schedule tests to assert new place_id-based park IDs.
src/parks/universal/tests/places.test.ts Adds unit tests for placeToEntity and parseShowTimes.

Comment on lines 860 to 861
// Process virtual queues
for (const vQueue of vQueueStates) {
Comment on lines 901 to +905
for (const queue of attraction.queues) {
let rideId: string | null = null;

const poiId = queue.alternate_ids.find((x) => x.system_name === 'POI');
if (poiId) {
rideId = poiId.system_id;
} else if (attraction.wait_time_attraction_id) {
rideId = this.getRideIDFromWaitTimeId(poi, attraction.wait_time_attraction_id);
if (attraction.wait_time_attraction_id) {
rideId = sanitizeId(attraction.wait_time_attraction_id);
… QueueEntityId

Post place_id migration, the entity list emits sanitized place_ids
(e.g. `uor.ioa.rides.the_amazing_adventures_of_spider_man`) instead of
the legacy numeric ids. The VQ loop was still keying RETURN_TIME by
`vQueue.QueueEntityId` — a numeric id that no longer matches any
emitted entity. With 22+ active VQ entries observed on the current
feed, the orphan-attached RETURN_TIME data was a real live-data
regression vs main: the data went to phantom numeric ids while the
real ride entities received nothing.

The VQ feed already carries a `PlaceId` that matches the new entity
scheme (40 of 45 entries observed). Switch the key to
`sanitizeId(vQueue.PlaceId)` and skip entries that lack one (rather
than silently attach to a non-existent id). Caught by Copilot review
and confirmed against a fresh pre/post snapshot.
@DylanWatson
Copy link
Copy Markdown

DylanWatson commented May 25, 2026

Makes sense that Hollywood Rip Ride Rockit isn't present - this ride closed on August 18, 2025. Fast & Furious: Hollywood Drift isn't open yet, should be opening this summer so I bet that it will appear soon/when it opens.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comment thread src/tools/migrate/server.ts Outdated
Comment on lines +256 to +260
const host = process.env.MIGRATE_HOST || os.hostname();
console.log(`\nMigration review server running on :${config.port} (bound 0.0.0.0)`);
console.log(` ${config.parkName}`);
console.log(` ${mappings.length} mappings to review`);
console.log(`\nOpen http://${host}:${config.port}/ in your browser to review (or http://localhost:${config.port}/ when running on the same machine).\n`);
Comment thread src/parks/universal/universal.ts Outdated
Comment on lines +816 to +819
// Park location: prefer `map`, fall back to any geometry entry (USH's
// `ush.ush` umbrella has only a GEOFENCE entry — base class validation
// requires the PARK to carry a location, so prefer-map-else-first beats
// dropping coords entirely).
Comment on lines 1105 to 1107
schedules.push({
id: park.Id.toString(),
id: placeId,
schedule,
@@ -10,8 +10,7 @@
import {describe, test, expect, beforeEach} from 'vitest';
Four cosmetic / defensive fixes, no behaviour change:

- universal.ts (buildEntityList): clarify the park-location-fallback
  comment — the anchor-entity location check lives in src/testRunner.ts,
  not the base destination class. Previous comment was misleading.
- universal.ts (buildSchedules): apply sanitizeId() to placeId before
  emitting EntitySchedule.id, for symmetry with the PARK entity
  emission. The table keys are clean today (uor.usf etc.), but the
  symmetry future-proofs the join if a key ever needs sanitising.
- migrate/server.ts: bracket IPv6 hosts when printing the browser URL
  (\`http://[::1]:9900/\`) so MIGRATE_HOST=::1 produces a clickable URL.
- schedule.test.ts: drop unused \`beforeEach\` import left over from the
  earlier mock-rewrite.
@cubehouse cubehouse merged commit adf6ee5 into main May 25, 2026
4 checks passed
@cubehouse cubehouse deleted the feat/universal-places-migration branch May 25, 2026 07:29
cubehouse added a commit that referenced this pull request May 25, 2026
…192)

* fix(universal): restore CHILD_SWAP + MINIMUM_HEIGHT attraction tags

The /places migration in #191 silently dropped the attraction tags the
legacy POI build emitted (HasChildSwap → CHILD_SWAP, MinHeightInInches
→ MINIMUM_HEIGHT). Reporter noticed Harry Potter and the Forbidden
Journey losing its 48" minimum height in Orlando.

The new feed exposes the same data under place_type.attributes[]:
- has_child_swap (string "true"/"false")
- minimum_rider_height_inches (string)

Read both in placeToEntity for ATTRACTION-type emissions only (matches
the legacy surface — only rides had these), emit via TagBuilder.

Verified live against the real /places feed:
- Harry Potter and the Forbidden Journey: 48 in + CHILD_SWAP ✓
- Despicable Me Minion Mayhem: 40 in + CHILD_SWAP ✓

Added 6 unit tests covering present/absent/zero/non-numeric height
values, has_child_swap true/false, and that non-ATTRACTION types
(Show / Dining) don't pick up these tags even if the upstream feed
ever includes them on a non-ride.

* test(universal): strengthen attribute-tag tests per Copilot review

- 'both tags' test now asserts the exact tags (CHILD_SWAP + MINIMUM_HEIGHT
  with height/unit), and explicitly verifies the unrelated express_pass /
  mfdo_enabled attributes do not produce extra tags.
- 'Non-Ride entities' test now covers BOTH Show and Dining (matches the
  test name's claim — it previously only covered Show).
cubehouse added a commit that referenced this pull request May 25, 2026
The CDN /shows/show-list.json feed emits UTC ISO strings (e.g.
2026-05-25T20:30:00.000Z). The /places migration in #191 passed those
through unchanged, but downstream displays were rendering the Z string
in local time and showing all showtimes as "after park close" — e.g.
Shrek's Swamp Meet (UOR USF) displayed as 8:30 PM instead of 4:30 PM
EDT.

The legacy pre-/places code emitted park-local ISO with offset via
constructDateTime(date, time, this.timezone). Restore that behaviour
by re-projecting each ENABLED show_time through formatInTimezone.

parseShowTimes now takes a timezone parameter. Updated the
buildLiveData call site (uses this.timezone — UOR: America/New_York,
USH: America/Los_Angeles) and the unit tests, which now assert against
the expected -04:00 / -07:00 offsets and cover both timezones.

Verified live against /places + /shows feeds: Shrek's Swamp Meet
emits 2026-05-25T18:20:00-04:00 / 19:20:00-04:00 (6:20 PM / 7:20 PM
EDT) instead of the bare-UTC 22:20:00.000Z / 23:20:00.000Z.
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.

3 participants