Skip to content

Undrawable track pieces build, rate, and render as nothing (wooden coaster steep transitions) #1

Description

@wseaton

Summary

A track piece the ride style has no artwork for can be placed, tests fine, and
receives ratings, but renders as nothing at all: no track, no supports. Runs
containing such pieces produced park screenshots with large stretches of the
ride simply absent, which initially looked like a painter or sprite-sorting
bug.

The classic case is the wooden roller coaster (ride type 52) and the one-tile
flat↔60° transitions (flatToUp60, up60ToFlat, flatToDown60,
down60ToFlat, track group flatToSteepSlope). RCT2 only ever drew the
three-tile *LongBase variants for wooden, so
GetTrackPaintFunctionWoodenAndClassicWoodenRC has no case for the short ones
and falls through to default: return TrackPaintFunctionDummy.

Example: https://wseaton.github.io/CoasterBench/run-20260723-v2-design-claude-opus-4-8.html

Why placement allows it

flatToSteepSlope is in neither the wooden RTD's enabledTrackGroups nor its
extraTrackGroups, so the in-game construction window will never offer these
pieces. But the enabled-group gate (IsTrackEnabled /
UpdateEnabledRideGroups, RideData.cpp) only feeds that window.
TrackPlaceAction never consults it, so any programmatic placement is
accepted. RideSetSettingAction (rideType) is the same story from the other
direction: it sets ride->type and calls updateRideTypeForAllPieces() with
no check that the existing pieces are drawable by the new style.

Ratings still compute, because the physics reads the track element descriptor,
which exists for these pieces regardless of artwork.

How it was diagnosed

  1. Instrumented the two height culls in PaintTileElementBase: 168 track
    tiles visited, 168 painted, none culled.
  2. Logged every ride paint struct at creation and at draw: 1166 made, 1166
    drawn — nothing dropped during quadrant arrangement or sorting.
  3. Ablated imageWithinRT creation-time culling entirely: 62 pixels changed,
    so culling was not involved.
  4. Logged calls to paintFunction (TrackPaint.cpp:2035) that left
    session.LastPS untouched: 32 tiles. 16 were legitimate empty filler
    sequences (banked five-tile turn sequences 1 and 4, large helix sequences
    1, 4, 8, 11 have no sprites by design). The other 16 were exactly the
    program's 16 steep-transition pieces.
  5. Minimal repro: the piece list flat, flat_to_up_60, up_60, up_60_to_flat, flat, flat, flat_to_down_60, down_60, down_60_to_flat, flat renders whole
    on ride type 51 and as disconnected floating fragments on 52.

Harness fix

CheckPieceDrawable in src/openrct2/rustbridge/RustBridge.cpp now asks the
same dispatch the renderer uses — GetTrackPaintFunction(style, type) compared
against TrackPaintFunctionDummy — before placing or querying a piece. The
ride's track groups are deliberately not used as the test, because they also
gate pieces that draw perfectly well. The check runs in both
orct2_host_track_place and orct2_host_track_query, so place_piece,
place_pieces, valid_next_pieces and the program executor all reject
undrawable pieces with an actionable message instead of building invisible
track.

Upstream

Worth reporting to OpenRCT2: the enabled-group check exists only in the UI
layer, and both TrackPlaceAction and the ride-type change bypass it. Any
plugin, imported track design, or park built through those paths can contain
track that renders as nothing for everyone who opens it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions