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 😨?
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.
scenario_name
: Purely to make the.parkpatch
easily identifiable by humans, serves no purpose during the game4.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 ofif...else if
string comparisons with a simple file hashing followed by checking if<hash>.parkpatch
exists5.-
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.
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.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.rides
: Enables operating on rides themselves, such as to swap the aforementioned entrance and exit of Urban Park's Merry-Go-Round.
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:
- #21189: Only apply patches when starting scenarios, this will simplify the
JSON
and the loading process. - #22598: Double check patches for RCTC scenarios against the original RCT1 and RCT2 ones, to make sure they are patched only where needed7.
- #22653, #22654, #22655: Finish what drove all of this refactoring and apply the fixes originally proposed on #19740.
- Move
.parkpatch
files to another repository: We don't store most of the other collections of OpenRCT2 "binary" files in the main repository (i.e. objects, title sequences, music, and so on), to this extent we probably shouldn't for the patches either. - Use a
JSON
schema: The patch algorithm has plenty of guards to check against invalid keys, empty lists and other nuisances. We could instead just write aJSON
schema that could be used to validate each.parkpatch
file on a CI environment. It would serve as a self-documenting file, as well as a gating process to push new patches, which would then allow for the code to be simpler as theJSON
files would be valid by construction. - Introduce new patches: Since we are not so afraid of adding more fixes now, new patches can be submitted, including for scenarios we couldn't support in the old system, like ones on the UCES set.
- Allow coordinates to be specified as a range: The
.parkpatch
coordinates are provided on a per tile base, but if we have a contiguous sequence, we could just provide a range where it could iterate, so we would be able to simplify things like[ 34, 60 ], [ 34, 61 ], [ 35, 60 ], [ 35, 61 ]
to[[34, 60], [35, 61]]
8. - Actually document the syntax: The
.parkpatch
files are in, but there is not a great documentation on how to write them, aside from actually reading them, so it hasn't become a lot friendlier like I claimed. - Create a plugin that generates a patch file: What if you could load a scenario and inspect it with a plugin, marking places that need patching and what they need to be patched for, and then have the plugin spit a
JSON
ready to be pushed? It's crazy, but it also could be neat :D
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!
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.↩︎