OpenRCT2's system for patching scenario bugs

Introduction

It's the year 2000 and I just got home from school. I turn my computer on and sit still staring at the big CRT monitor, while I wait for the three minutes it takes for the PC booting sequence to finish. I open RollerCoaster Tycoon on the Leafy Lakes scenario, which I had been frantically playing on the day prior, and head to construct the new attraction I had in mind. I have to acquire more of the available land to do so, and I start doing it, until I notice there is a single tile in between all of them that is not purchasable. I freeze in horror and wonder: why 😨?

Screenshot of the RollerCoaster Tycoon's Leafy Lakes scenario showing a park with a single tile of unowned land in between the purchased ones.

The original RollerCoaster Tycoon games had some scenario configuration bugs and among them the land ownership was the most disruptive: land or construction rights that should be purchasable or available not being. Not that I was ever experienced enough to notice, so much so that the short story above is purely fictional, but that understandably did bother some people, who eventually made sure to patch these scenarios when loaded into OpenRCT2, and that's where this story really begins.

OpenRCT2 Scenario Patches to the rescue!

There were patches for the vanilla scenarios making their way through the OpenRCT2 code as early as OpenRCT2 v0.0.6, 8 years ago, and over the years they kept appearing whenever a sharp pair of eyes from one of our players spotted them and decided to make things right1. Most of these changes were functional, like fixing the above missing available land on Leafy Lakes or swapping the entrance and exit of the Urban Park's Merry-Go-Round, but there were some cosmetic ones too and they were starting to make the code look really messy2. Nevertheless the system was working for years on end, and it sounded like nothing would change, which is when the pull request #19740 dropped.

That PR alone was adding over 200 lines of code, mostly to fix the water height of lake shores that were not properly set (and thus didn't look like a proper body of water settling at uniform height). It was great and meticulous work done by @HtotheTML, but we were afraid of growing our patching monster even more, thus we decided that we needed to either introduce a new way to patch the scenarios or we wouldn't be taking any more individual tile fixes, as it was becoming difficult to maintain. That was the end of the story for another 4 months or so, until I decided to flex my fingers and give this challenge a go.

The birth of .parkpatch files

The main goal of creating a new patch system for the scenarios in OpenRCT2 was to take the ugly and verbose code away from the importers and just have generic code that was able to deal with patch files. That was not the only objective, though, we also wanted to make it easier for people to contribute fixes, since any player can spot for errors and it shouldn't take mad programming skills to be able to patch them3. With that in mind, we settled for storing each scenario patch on a different JSON file, and this is how it looks like for one of them6, Urban Park:

{
    "scenario_name": "Urban Park",
    "sha256": "835ec8bdba3dc4086906126907c022cf42fa0f9cd6ee06221f36aac526ac4ec4",
    "land_ownership": {
        "available": {
            "coordinates": [
                [ 34, 60 ], [ 34, 61 ], [ 35, 60 ], [ 35, 61 ],
                [ 48, 19 ], [ 48, 20 ],
                [ 52, 15 ], [ 53, 15 ], [ 54, 15 ], [ 55, 15 ],
                [ 52, 16 ], [ 53, 16 ], [ 54, 16 ], [ 55, 16 ],
                [ 52, 17 ], [ 52, 18 ], [ 52, 19 ],
                [ 64, 77 ], [ 61, 66 ], [ 61, 67 ], [ 39, 20 ]
            ]
        },
        "construction_rights_available": {
            "coordinates": [
                [ 46, 47 ]
            ]
        }
    },
    "rides": [
        {
            "id": 0,
            "operation": "swap_entrance_exit"
        }
    ]
}

It might not look less verbose, but remember that all of this was pulled out of the code and is now a collection of individual files. Let's walk through each JSON key we currently support and explain their intent.

  1. scenario_name : Purely to make the .parkpatch easily identifiable by humans, serves no purpose during the game4.
  2. sha256: The hash of the scenario file using SHA256. The old patch system was error prone, it relied on a combination of a scenario's internal metadata and its filename to find the patch for a given park, since some scenarios had "copy + paste" misconfigurations and were all internally set as the same "Six Flags Magic Mountain". Using the hash gives us a unified way of identifying a scenario and its corresponding patch, which simplifies the code by swapping a lengthy series of if...else if string comparisons with a simple file hashing followed by checking if <hash>.parkpatch exists5.
  3. land_ownership: Allows one to control whether a tile should be:

    • unowned: not part of the park and not purchasable in any way.
    • owned: already part of the park.
    • construction rights owned: Allows player to build above/under the land, but not on the land itself or to modify the land.
    • available to buy: can be purchased for full control.
    • construction rights available to buy: can be purchased as construction rights.

    Most of the scenarios covered in the old patch system were only there to fix land ownership, so this was the bulk of the migration. There were interesting cases of scenarios that were partially fixed on their RCTC versions, such as Katie's Dreamland, which favoured our model of one patch per scenario, since despite being the same park, they needed different mending.

  4. water: Allows changing the water height of a given tile, given that it sometimes just didn't look right, as depicted on the screenshot below.
  5. operations: Lets track tiles be fixed, such as to fix Ayers Adventure's "Splashster" not ever going past the first descent due to a misplaced water channel.
  6. rides: Enables operating on rides themselves, such as to swap the aforementioned entrance and exit of Urban Park's Merry-Go-Round.

Screenshot of the RollerCoaster Tycoon2's Six Flags Holland scenario showing a tile where the water uncannily does not rest leveled with the land shore.

It might seem we had everything planned from the beginning so it would be a straight forward and quick series of patches to get to the final form, but I assure you it was not. It took me around 2 months and 30 commits to get everything working as expected and a whole semester to get it merged, as our team and community helped me with input and cautious reviews to not break anything in the process. I won't dive into the details of the patch algorithm itself, because JSON handling is not very exciting, but you are welcome to take a look!

The future of .parkpatch

Along the way of porting these patches to the JSON files, I noticed there was room for improvement in the code, some of which I did already, others which could be improved later, to not drag the pull request forever. Here are a few ideas, in case anyone wants to have a go at them, keep in mind the ones not tracked by issues should be discussed first with the team:

Wrapping-up

Starting on v0.4.14 your OpenRCT2 will be patching files in a different way, but our goal is for you not to notice it at all! The way it was designed, you might see upcoming changelog entries boasting a lot more claims of fixed scenarios and we hope this encourages some of you to contribute and that it makes it for an even funnier experience for the whole community 🥳.

Happy patching!

~Tupaschoal


1: I'll leave as an exercise to the reader to define whether this effort was: led by anger of finding a tile not configured as it should, OCD to have things symmetric/aligned or heroism. â†©ï¸Ž

2: This is what it looked like to patch RCT2 scenario's ownership, 600 lines!↩︎

3: Still not a walk in the park due to the hashes, but it does look friendlier.↩︎

4: Actually it is used for printing debug messages, but that's a minor developer detail.↩︎

5: We use just the first 7 characters of the SHA sequence for the filename, to prevent them getting too long, but we double check that the full sequence matches once we find and open the file. Collisions are very unlikely, nevertheless it doesn't hurt to play safe, right?↩︎

6: Please don't mind the inconsistent choice of square brackets and curly braces.↩︎

7: I'm actually doing this myself and have developed a plugin to help, more on that on an upcoming post!↩︎

8: I know it might not look a lot simpler in this example, but imagine you have a rectangle of 20 coordinates and instead of listing them all you just list the start and end of the rectangle, 2 coordinates. It's a big simplification on the JSON, at some cost of complexity in the C++ itself.↩︎