Transcription
Recently I listened to an episode of the podcast Stay Forever. It was about Rollercoaster Tycoon, a game from my youth, which I found very exciting. In the trivia episode, Christian, the podcaster, reported that the game in its original version had a quite nasty bug. Specifically, when switching from summer time to winter time or from winter time to summer time, the player lost their entire scenario and game progress. For this reason, I took a look at the binaries, disassembled them, and looked into why this error originally occurred. How did Chris S, the developer, fix this error in the short term, and what does it all have to do with the mysterious date of December 20, 1998? Let's first look at the problem. I have set up a virtual machine with Windows 98 here. It is important that we run the original version in order to reproduce the error. And we also need Windows 95 or 98 with the FAT file system. Only with that is it truly reproducible. [snort] I have installed two versions in the virtual machine here: once the patched version and once the original version. If we look at the metadata, this is Binary Ninja, a tool I used to examine the internals of the EXE. We see at the bottom, when I search for versions, that the patched version is version 108 183. So, we note the 183 at the end; it's from around early 2000. And the other version, that would be this one, that is 108164, which is essentially the version from February 26, 1999. So that is the version that still contains the bug. It was relatively hard to find, but via eBay I was eventually able to find a real original version where it is reproducible. How does this manifest? We start the original version of Rollercoaster Tycoon here and I load a save game where we are just before completion. So in principle, it's about the scenario selection. We initially have uh five scenarios here, and as soon as we complete a scenario, the next one, in this case Bumbly Beach, is unlocked. So, I load a save game here and we have already completed it. I can now select a chart name or scenario chart name and then say okay, I sometimes have problems with the mouse in the virtual machine. So, press, and with that Forest Frontiers is completed and I can exit it. As soon as I go to the Select Scenario or New Game overview, I see that the first game is completed, where the scenario is completed and as said Bumblebeach is unlocked. Okay. And the actual error manifests when we change the time. I'll show that briefly. We currently have today June 30, and if I go to, say, August, that is still summer time and then there is no problem showing that quickly. I go into the game again and we see nothing has changed. Scenario progress is still available. As soon as I now switch to winter time, for example to December 30 here, click Apply, okay, the game starts, then we see that the scenario progress is gone. Besides this original bug, there is another problem that players essentially had to deal with. For example, uh when they transferred their data from their current computer to another, it also happened that the scenario progress was lost. And if you search in the forums, you often find the mentioned date. Here I have e.g. an old thread from 2001 and here you can see e.g. that a user wrote that the file CSS0.D should be copied from the old to the new computer or overwritten on the new computer, then the system date should be set to 12/20/98 and then the game should be started, and then you have access and can use the save games again and the scenario progress is still available. We will now look at how the bug originally occurred and what this date is all about. Let's first look at which file the scenario progress is saved in. That is the installation directory of Roller Coaster Tycoon and in the Scenarios folder we find all 21 scenarios from SC0 to SC20. A file that is dynamically created at runtime by Roller Coaster Tycoon is the SC.idx file. That is the index file and Roller Coaster Tycoon tries to ensure the integrity of this file on several levels. The file is about 2 KB in size because it is compressed and also encrypted. When starting, the file is unpacked or freshly created if it does not exist yet, and then it is about 14.8 KB in size. I have a hex editor so you can get an idea of what is in this file. As said, here the file is in raw format, so nothing can be recognized. If you look at the character representation, the ASCII representation on the side, it doesn't make sense uh. If we switch to the decrypted variant. Based on the code I wrote a Python script that decompresses and decrypts the file accordingly. And then we see the content here. In the left area I have the decrypted ASCII representation of the content and I built a pattern template based on the code more or less, so that I can navigate through the file better. Yes, and I see e.g. The first area contains the index cache of the scenarios, then come the names and then comes the progress of the individual scenarios and at the end I have metadata uh like e.g. the date when the installation was made and so on. Here in the example you can see that we have the 20 or 21 scenario files accordingly. They were dynamically read in. That means if you install add-ons or similar, they are also listed here. In the second area we find, I can also click down here, then it goes directly to the corresponding address. In the second area are the names for the individual scenarios: Forest Frontiers, Dynamite Dunes and so on. And in the third area we have the progress. In principle, the file format or data format is such that we have four bytes that belong to one scenario. The last byte in these four-byte blocks here, which is hex 80, is the smallest minimal number that can be represented and means for Roller Coaster Tycoon that the progress is essentially locked, i.e. the scenario has not been solved yet. If a scenario is solved, like the first one here, then you see, it no longer says hex 80, but the four bytes represent the score. A technical detail would be e.g. that it is stored in little-endian format. That means the number must be read from right to left. So the score would be hex 016FB0. I think that's 1942 or something like that. That was the score I reached when I played through or completed the first scenario. If I scroll further down, I see next the chart, the scenario chart name, which I had named, and yes, it is now assigned to the first scenario and as said, at the very end there are still some metadata, which we will discuss later. Okay, let's briefly look at the process that this file goes through. When Roller Coaster Tycoon starts, it checks whether the file exists in principle. If not, it is created. In the second step, the checksum is checked. That, as said, is to ensure data integrity. That means if a byte changes, the file is immediately invalid. If we go back to the hex editor, then the checksum is essentially these last four bytes here. Next, the file is decompressed. During this, it grows to about ten times its size. After that, the content is decrypted. Next comes the part that is important for us: a timestamp is checked. The purpose is to ensure that the index file matches the current installation. What happens is that it stores the timestamp of a file in the index file. Specifically, if I go back to the Data folder, there is the file CSS1.d and it saves the creation date of this file in the index file. That means if we look at the properties of this file here, we see in this case it would be April 1 at 16:41. That's been a while. I had initially installed it in the VM and it takes exactly that date into the index file. If we look at the decrypted file again, we see a field Reference File Timestamp at the bottom. Yes, that is essentially the 2 bytes, i.e. 16 bits here, the 28 C2. That would be the creation date of CSS1.d. If the date has not changed, then it next checks whether the scenarios themselves are still correct. So, have additional scenarios been added or have scenarios been modified, and it does that by accumulating the total size of all scenarios and checking it against its stored value. If we go back to the file, we see that would be these 8 bytes that we see here. That is essentially two double words. We have a high part and a low part. In our case the low part is zero and the high part is this value here. We can determine that relatively easily. We can switch to the calculator and say the bytes divided by 1024 are kilobytes, divided by 1024 are megabytes. Then we see here that it is approximately 6.3 MB. That means, if I go back to the VM and back to the scenario folder, I select all scenarios and look at the properties, then I see the 6.3 MB. If this value matches, then the progress is preserved. As soon as one of these various verifications fails, the current index file is discarded. It scans for the scenario files again in the corresponding scenario folder and then creates a correspondingly empty new SC.idx file and yes, everything is then initially locked again. You can verify that relatively easily. I have unlocked the first scenario again and if I now exit the game and move or delete a scenario file here accordingly. Let's take e.g. file zero and simply move it to the desktop. Now if we restart, I would expect that the scenario file is recreated and the progress is lost. So, I start the game again. Yes, and the scenario progress is lost and you can also see that the scenario index file has been recreated. You see the current date here, today 4:29, so the index file was rewritten one minute ago. Let's look in the following at what caused the time zone bug. As you might have guessed, this timestamp check is to blame for the problem. Roller Coaster Tycoon checks at startup whether the date of the CSS1.d file and specifically the creation date is still identical to the date that was previously stored in the SC.idx file. And exactly this check can fail due to the time zone change. Here on the MSDN page, the Microsoft documentation page for the function that Roller Coaster Tycoon uses to determine the creation date of the CSS1.d file. Specifically, Find First File is used or Find Next File, which returns the same data structure. Find First File returns a UTC time for a file. UTC means in this context that the time contains time zone information. To be precise, you get a Get File Time data structure back. That would be this data structure here. We have creation date, last access, and yes, last write. And what interests us or what is evaluated in the code is the creation date of the file. This is a long pointer to a FileTime structure that is returned. And if we look at the data structure, FileTime, then it is described again what it looks like. Namely, it is a 64-bit value and the number represents the 100 nanoseconds that have passed since January 1, 1601. In that case we speak of a UTC time. That means you can convert it to a local time, depending on the time zone. That is possible because you have an absolute reference point. In this case it is January 1, 1601. There is also a or usually you know the well-known Linux UTC time, which is since January 1, 1970, but here under Windows there is a different reference point for this FileTime structure. But basically, because we know the distance to a fixed reference, it can be converted and we can say, okay, in Germany it would be a different time e.g. than in America, because the time zones are different. You can convert it back. In contrast, there are also times that are stored independently of time zones. There only the pure time value is stored. So e.g. today is 6/30/2026, currently 11:28 PM. The value will be stored as such and then of course the time zone information is completely missing. The whole problem is actually very well described in this paragraph. It says that the API used under FAT takes the local time from the FAT file system and converts it to UTC with consideration of the current time zone. That is exactly the problem. That means the FAT file system does not properly feed the overlying API. That is, in the FAT file system only the local time is stored, as I just said, e.g. again 6/30/2026 23:09, and this now has to be converted into this UTC time, i.e. the 100 nanoseconds since January 1, 1601, and that [clears throat] is only possible if it makes an assumption about which time zone to use. That means concretely that the time indication in the FAT file system is constant, but the value that the API returns from it, i.e. the Get File Time structure, contains a different value depending on whether it is winter or summer time or whether we are in a completely different time zone. To demonstrate that briefly, I went back to the Windows 98 VM. I compiled a small program that makes this information accessible to us. So, as said, it's about the file CSS1.D. If we look at that in the properties, we see here the creation date is, as we saw earlier, April 1, 16:41. I have now written a program that uses exactly the API. Find File and we'll see what it returns here. Interesting here is that the file CSS1.D is evaluated and we have the property FT Creation Time. We see the 64 bits here, the high and low double words, so essentially the four upper bytes and the four lower bytes, and they contain more or less the result. That's what matters. We can also see what time is behind it. I have entered this bit sequence on the right into my calculator. So here we have the 8 bytes, i.e. the high and low bit sequence one after the other. And I see that this corresponds to this time in decimal. And if I copy this time, it's the 100 nanoseconds since January 1, 1601. Yes, there is e.g. an online Windows Time Converter for this. If I paste the value here and convert, then I see that is April 1, 16:41. As mentioned, the problem is that this value, this 64-bit value changes depending on the time zone. We can prove that. I'll take a quick screenshot of this to be able to compare it later. Okay, so I have the screenshot here for comparison. I'll move it aside and now change to winter time. That means I go to the date symbol at the bottom, enter December, click Apply. Okay, run the program again. We now recognize here that the low byte has completely changed. Yes, we have 1254, here we have B090 and so on, and the high byte has changed in the last part. Instead of E5 we now have EE. I also entered the bit sequence again in my calculator. If I copy the value back to the timestamp Windows converter, paste it here, then we see here it is one hour later, 17:41. Yes, that is exactly the problem. That means the underlying value in the FAT file system does not change, but the interpretation during conversion into the FileTime data structure is time zone dependent and changes the value by exactly one hour. The simplest solution would of course be to say that we accept a tolerance of one hour, but there is another problem and yes, we will look at that next, how Chris Sawyer solved the whole thing. The most obvious solution is to make the date comparison more tolerant and accept a deviation of one hour. Unfortunately, that was not easily possible in the current implementation and Gray even had to write a small migration. But to understand that, it gets a little mathematical now. But we need the theory to understand the problem and also the solution that Chris Sawyer pragmatically chose. Let's first look again at how the original timestamp of the file is stored in the scenario IDX file. I have opened again in my calculator the representation of the date format. We see here we have, as said, the 64 bits, that means we have 8 x 8 bits and you can imagine it like this: a column of 8 bits results in a byte and that is always represented by two digits in hex representation. So, the last byte here corresponds to 40. The next more significant byte would be the 8 bits. That corresponds to 71 and so on all the way to the left. And here above we have the decimal representation. Chris Sawyer was very optimization-conscious and said to himself, okay, if I want to detect a change, I don't need to store 64 bits. I just pick 16 bits, i.e. two bytes. He said, I decide and only store two bytes in my file, but not all eight, simply to save space. Now the question naturally arises, which two bytes of this number do we take? To understand that, I will briefly illustrate this with a decimal number. Suppose we had a four-digit decimal number that represents e.g. seconds. Let's say, we have the numbers 105 and a change to, say, 110. And now we look at what limitations we have if we only store and compare one digit instead of all digits. Depending on which digit I choose, I always have a different resolution that essentially determines the smallest change I can detect. And there is a period, meaning when the field wraps around or the number starts again from the beginning, that is the maximum range I can detect. So that means e.g. if I remember the ones digit here, that would be the 5 in this case, then I have a resolution of one second. That is, I can detect a minimum change of one second, but e.g. half a second I cannot detect with that, at least one second, and I can or after a period of 10, i.e. modulo 10 in mathematics, that digit starts over again. So when we reach 9, the next higher digit would increment by one and the corresponding ones digit would start over again. That means I can only detect changes within a range of 10 seconds. So that wouldn't be a problem. I could detect the 5-second change from 105 seconds to 110, but if e.g. I had a change from 105 to 120, that's 15 seconds, if I only store the first digit, I would only detect the 5-second change. That means I can only detect changes within this range. If I look at the next higher digit, that would be the tens digit, then that has a resolution of 10 seconds. Every 10 seconds that digit increments, with a period of modulo 100, meaning up to 100 seconds I can detect. So in this case I would detect a change from 0 to 2 and would think, okay, that would be 20 seconds. Of course the information is somewhat imprecise because the other digits are missing. It could be 15 seconds. Next, if I look at the hundreds digit, then I recognize that I have a resolution of 100 seconds. That is, with that digit I can detect a minimum change of 100 seconds and I would cover a range of 1000 seconds. Yes, and here you can already see the pattern. That means the further to the right the digit is, the more precisely I can detect changes, but I am less robust in terms of range. The further I go to the left, the smaller the precision or accuracy I can remember. But I can detect larger ranges. If we go back to the hexadecimal domain, then as said we have a similar approach, but now we have to think in hexadecimal. Chris decided to only take 2 bytes, i.e. 16 bits out of these 64 bits into the data format. If we divide the 8 bytes by the two bytes we want to store, we get four sections. Usually you represent that by saying, okay, you have the high part on the left and the low part on the right, i.e. L for low, H for high, and the high part is further subdivided into high and low. And the low part similarly into high and low. Hence H, LH, HL, and L. That would be the four sections. Each section is 16 bits wide or those 2 bytes. If we were to take only the rightmost 16 bits of our 64-bit number, i.e. from section L, then I already mentioned that we have a resolution of 100 nanoseconds. That means every 100 nanoseconds this digit increments. Now the question is, how big is the period? That is always determined by the base. In the decimal domain it was 10. Here it is hexadecimal, that means we have 16, or if we calculate in bits, it would be base 2. So what we would calculate is 2 to the power of 16, i.e. the width times the 100 nanoseconds, and that would be about 6.5 milliseconds. Yes, that means if I choose this digit, then I detect a file change of 100 nanoseconds. The problem is I can hardly assess if a change is larger than these 6.5 milliseconds. One can now calculate all sections here. So, I have inserted that here. Uh and here you can read it off very well. That means if I take the L section, it wraps around every 6.5 milliseconds. The next section would be LH, which counts up every 6.5 milliseconds and after about 7 minutes a carry, a wrap would occur and that digit would start from scratch again. That is the section that Chris Sawyer decided on. So again, that means he currently stores these bits, these 16 bits or these 2 bytes from the timestamp, and compares these 2 bytes. That means if a file change is smaller than 6.5 milliseconds, he would not detect it, and every 7 minutes and 9 seconds the wrap starts again. So theoretically, if we compare two file changes and between them there are n times 7 minutes and 9 seconds, the change would also not be detected. So it can happen that there is a large file change in terms of time. But if it is exactly a multiple of 7 minutes 9 seconds, i.e. modulo N is zero, then he would not detect that change either. But this probability is very low, which is why he essentially accepted that and chose this section for optimization reasons. But you can already see the problem here. That means you can effectively only detect or track a change up to 7 minutes and 9 seconds. It is impossible to deduce an hour from this, because if he compares the two bytes and subtracts them from each other, a number always comes out that is smaller than 7 minutes 9 seconds, and thus he cannot incorporate a tolerance of one hour. But to also show this in the data, I have switched back to the output in the virtual machine for what File Time returns for the CSS1.D file. Then we see here FileTime high and low. That is essentially the 64 bits, 8 bytes above, 8 bytes below, and as I said, he decided on the HL section. That would be these B090, which is exactly that section. I also extracted that separately with a 16-bit shift. That doesn't matter. It just means that this section, this B090, is extracted from this pattern above. So the B090 is what ends up in the data format. The rest is essentially thrown away. So everything that comes before and the two bytes that come after are thrown away. Only this section is actually stored. We can see that again when I switch to the data format and look in the actual hex editor, that would be this one. Then we see here at Reference File Stamp, as said, again little-endian from right to left we have B090, what he extracted, only the two bytes, and the actual solution then looks like this: Chris Sawyer decided to switch to the next higher section. He switches to this section and writes a small migration in the code, and then uses the HL section. Here he can detect a minimum of 7 minutes 9 seconds, but has a large range, so within 325 days, almost a year, he can fully detect the change using these two bytes in the form of 7 minute 9 second increments. That means if he wants to incorporate a tolerance of one hour, that would be approximately eight units, because 8 times 7 minutes is approximately one hour. Next, we can also look at the code for how he implemented that. Before we look at the code next, I would like to make a small correction here. Unfortunately, the table header is inconsistent with the table below. We are going from the low part to the high part and that corresponds to the view from right to left at the top. That means on the far right we have L. Then comes LH and not HL. Then I got confused. So we'll correct that briefly. Next would then come HL and finally H. So we go from right to left, from the low part to the high part. Okay, perhaps a few brief words about the reverse engineering process. I have used the tool Binary Ninja here and I have open now the original version. We see here at the bottom, if I search through the strings, I find the version from February 26, 1999. That is the original version that still contains the bug accordingly. If you were to search for this bug, the best approach is to search e.g. for the string SC.idx and it is even present as a string here. If I mark it, I see the code reference at the bottom left and I relatively quickly find two functions that use this value. The function is or the code is disassembled by Binary Ninja. That means it looks approximately like this. You have assembly code with move and increment instructions, jump instructions and so on, and that is relatively difficult to solve. That is why many tools, including Binary Ninja, offer a decompilation process or a decompilation tool. That means I can display the whole thing with pseudo C or other languages, e.g. or the setting I sometimes use is High Level for Intermediate Language. That looks better already. But you have to remember that during this process information is lost, misinterpreted, and errors can occur in this view. That means you often have to switch back to assembler to check if it corresponds to the truth. And the next point is, you see here, it is still, so here is the use of SC.id from the string of the text. Uh, but if you go through again, you still don't see clearly what is happening. So you still have to go into the individual functions, think about them and by tracing the code you can draw conclusions about what really happens and make corresponding renaming here. I didn't do that in the original version, but only in the patched version because I had that first. That's why I will open the patched version and switch back to this version only for the differences shown. Okay, so now we are in the version that I edited. We see here, this is from January 13, 2000, that is the patched version, and we see here uh that I have already looked at what the code does and renamed corresponding things. The function Load or Rebuild Scenario Index is essentially the function that I presented to you earlier as a flowchart. I will now go into the section that is important to us. The important section would be this part here that validates the time. The code that has been added here in the patched version would be this section here. And for backward compatibility reasons, the original call is still there. Reference F Timestamp is the timestamp from the SC.idx file, i.e. the timestamp of the CSS1.d file, the installation date essentially, the stored one, and it compares it here with the reference date, i.e. the date of the CSS1.DAT file on disk. And I have already written low here. Newly added is a function that now performs the migration or stores and compares the whole thing using the high date. If we go into the function, how he determines the low date, I'll go into it, then we see the function essentially as I told you earlier or in the previous chapter, he uses Find First File from the Windows API. That is still a wrapper, but if I go into it, I see he uses Find First File to determine the date. He gets a handle back. If the file does not exist, he exits here. Otherwise he now extracts the low date section. The result is stored in a global variable. That is interesting. If I double-click to switch to this memory area where he stores the result, it looks approximately like this. And that also corresponds exactly to the data format, which
As we saw previously in the documentation. This means that we have, so to speak, the creation date, last access, last write access, and then we have 4 B for the lower-order part and 4 bytes for the higher-order part. These are now, so to speak, the 64 bits that are being discussed here, or the 8 bits. If we go back, we see that it accesses exactly that. Yes, so it takes this global area, goes to the creation date, and takes the lower-order part, Low Daytime, from there. In addition, it also performs a shift by 16 bits to the right. However, it only does this to extract, so to speak, the higher-order part of the Low Day Times. So the higher-order 16 bits, the lower-order 16 bits are, so to speak, discarded. That is, so to speak, what it means. Once again, the analogy here, or it is shown again in the calculator, if it is the 64 bits, then the lower-order part is, so to speak, these 32 bits, so this area here. What it does now is it takes this area and shifts it 16 positions to the right. This means that the low part of the lower area is shifted to the right and instead this area comes into its place. This way, we have, so to speak, omitted this area here, and that would then be, so to speak, the B090. This corresponds to the 40, this here corresponds to the 71, and we are interested in the B090, and it is shifted 16 bits to the right, so omitted. And the function that is now newly added is, so to speak, the one below. It did not exist before in the original version. It is exactly identical to the version above. Only with the difference, you see it here, that you take the High area and here there is no bit shift. Why not? We can switch back to the calculator. This means that this here is the High area. It is not shifted 16 bits to the right. Therefore, it is implicitly, one says, converted into a 16-bit pattern. This means that it effectively takes the two bits before the B90 into these 16 bits. This means that after the migration, it does not remember B090, but C1 E5. As mentioned, this is the next higher area, which no longer has such precise accuracy, but on the other hand, a larger area is then, so to speak, covered that can be recognized. The migration will then proceed in such a way that, in normal cases, this check will not fail. It goes to the label, it is okay. So it jumps down here. and then migrates to the High Date. So here it still checks the Low Date, but as soon as this patch is installed, it will migrate to the High Date at the next opportunity, and at the next check, when it runs through here again, it will fail. For this, however, it will then, so to speak, go into this branch. What it does is, it subtracts the whole thing from each other. This is the stored timestamp and the Reference Install High Date Timestamp. Next, it also takes the absolute value, because it can also be negative, depending on whether the date is before or after. And now comes the important aspect. It compares the units with the value 120. If we go back and look up the table, we see that one unit has a size of 429.5 seconds. This means I can calculate here 429.5 seconds, that is one unit, that was these 7 minutes 9 seconds times 120, that would be seconds divided by 60, to get back to minutes, divided by 60, to get to hours. This means that we have approximately 14.3 hours here, which we now have as tolerance. This means that Chris SAR has decided to build in more than an hour of tolerance, to be precise, this 14.3 hours of tolerance in both directions. This means that if the time difference of the CSS 1.D data is less than these 14.3 hours, then everything is okay and the index will not be deleted. If it is greater than these 14 hours, then the index will be reset. I have now placed the original version side by side, which is now on the right, which still contains the bug. And as you can see, the line that checks for 0x FFC. Yes, would now be this line. From this, you can see that the comparison comes after this, and then that's it. So, it goes to the Okay label. Otherwise, it stores this pattern FF0, which means, so to speak, that the scenarios are blocked. That would now be, so to speak, analogous on the left side, this line here. You can see that the section in between is new and does not exist here. Likewise, if I now go into the function, Sub 42 FF4C, which is our Get Reference Install Loadate function, so to speak. If I go there, I see here, I'll also go to the left into this function, that we can also compare them briefly. Yes, and then we see here, it also compares the file handle to FF again, whether it is valid, it does that at this point. We also have the bit shift 16 to the right. That is 10 hex. So that is exactly the same function, and the function here Gap Reference Install Highdate is new. It does not exist in the right version, but here comes the comparison with these three numbers. We will go down a bit. Exactly this function, and we also see a magic date, 20.12.98, 98, but I will come back to that later. Now I just wanted to clarify that this function is new and is part of the patch, so to speak. Next, I would like to check and show that the assumptions are correct. I have renamed the original file here and renamed the patched file as the current RCTXE. This means we are now working with the patched file, and when I start Roller Coaster Tycoon, we see that the first scenario is unlocked. Next, we set the date back to December to get to winter time. WD July 1st, we set it to December 1st, December 24th, and say apply. Okay, now I start the patched version and would expect that the scenario progress is still preserved or remains preserved. Yes, you can see that the scenario progress has not been lost. I set it back to July 1st. As the next point, I would like to check the 40-hour assumption. To do this, I go to the Data directory and look at the reference file. CSS1.d. And as mentioned, this was installed on April 1st at 4:41 PM. This means, for example, if we let 12 hours pass, so setting the creation date to April 2nd at 4:41 AM, then the index should not be recreated because it is over 14 hours, so something around 7 or 8 AM on April 2nd, it should then be recreated. To force this, I would set the date artificially back to April 2nd. As mentioned, to the morning, so 4:41 AM, then I would apply it. Okay. And if I now, so to speak, copy this file and paste it to the desktop, delete the file here in the folder, yes, and copy it back, then I should have set the date to this creation date. Let's check the properties. Yes, exactly. Now we have April 2nd, 4:42 AM, which means approximately 12 hours have passed. This means the index should not be deleted. Let's set it back to today's date, July 1st, 12:12 PM, apply. Okay, if I restart now, I would expect the index to be preserved. Yes, we see the index is still preserved because 12 hours is not enough deviation. Let's now try to force the 14 hours. Windows 95 just crashed in my virtual machine, and I have to restart it and run Scanisk and recover the virtual, what's it called, the Active Desktop, and so on. Anyway, it's working again, and the problem is that I have already implicitly performed the migration in my test. This means that by setting the date to April 2nd at 4:41 AM, it has copied this as the new reference date into its migration into the SC.idx and IDX file. However, since I want to have the test from the original date, I have reset it, reloaded the old scenario file, and also here, if we go to Data, have reset it to the original state, i.e., April 1st at 4:41 AM, so that we can test the 14 hours from this point. To show this again, if I start it, we now see here that the first scenario is still unlocked. We leave the whole thing and now force the 40-hour difference. Well, for that, we set it back to April 2nd. 4:41 was the 12 hours. 5 will be the 13, 6 will be the 14. Let's go to 7:41 AM. There we have, so to speak, a 15-hour difference, which is greater than the 14-hour tolerance. And I press apply and copy the file back to the desktop. Copy the file back to today's date. to July 1st, 12:49 PM, apply. Okay, let's look at the properties of the file. We see. Okay, April 2nd, 7:42 AM. This should now be greater than the 40-hour tolerance. Let's switch back to the game directory and start and go into the new new game. Yes, and here we see, it has been set. The tolerance has exceeded 14 hours. Let's now look at what the mysterious date, i.e., 20.12.1998, is all about. And that is, in this function, the Scenario Index file is checked and validated and verified, and so on, that exactly at this point here the bypass is checked. This means that this date is checked, and if it is this day, then the date check, which we looked at earlier, is bypassed here, and it proceeds directly to, so to speak, the current current date value being written back to the index file. This is done by the program, but the actual check and the invalidation in case of error are bypassed here, so to speak. The code itself is somewhat defectively decompiled here, because you see something like stack Jack Value. That is simply a decompilation error, because the function Death Bypass Date does not return the value via the AX register as is usual in assembler, but in this case controls it via a carry flag. That is, let's say, the subtleties of assembler, and exactly such errors can only be recognized when looking at the disassembler code. In principle, it simply means, hence the comment there as well, compiler bug. It would be the same as testing this condition here instead of the condition you see here, i.e., Death Bypass Date and then negating it. If we go into the function, we simply see that it relatively simply gets the current system date and checks the year against 98, the month against 12, and the current day against 20, and returns the result as a carry flag, as mentioned. Yes, why exactly this date was chosen is of course speculation. On the one hand, or one possibility is that Chris Seuer himself used the date so as not to lose his progress during development. However, the fact that he checks exactly one day speaks against this. This means he would have to constantly adjust the day. Normally, as a developer, one would rather set a flag somewhere else or do something like less than or equal to release or something, so that one doesn't always have to adjust the day. Therefore, my assumption is rather that it was probably either the release day itself or the release test day itself, meaning that the QA team tested on that day, or that is the more plausible variant. He has, so to speak, informed his QA team at the time that if they want to reinstall things and load save games and so on, they should reset their computer to this day beforehand so that their progress file or their progress through the scenario index file is not lost. Yes, I can imagine that the QA team performed the installation frequently and the progress was always set because, of course, the CSS1.d creation date changes, and then the save games no longer work, and so on. Therefore, he certainly chose this day and communicated it to his QA team. That is, let's say, the most plausible variant. In conclusion, I would like to report a few facts that I came across while reviewing the code. I would like to start with the visitors, and the data structure in the code is designed such that we have an address here that, so to speak, maps the entire visitor array. According to the accesses, a single visitor is allocated 256 bits in memory, and memory is allocated in advance, i.e., reserved for 5000 visitors. This means that if we look at this, it would be 256 bytes times 5000 visitors divided by 1024 would be kilobytes, divided by 1024 would be megabytes. This means we would have approximately 1.22 MB allocated or reserved for the visitors, which is already a considerable RAM usage for that time. The advantage is that the access is extremely fast. If one were to implement something like this today, it is usually done with dynamic allocation and it is allocated on the heap and reallocated or with a garbage collection and so on. So with a garbage collector. And we didn't do that back then. The advantage, however, is that there is no fragmentation. It is extremely fast. There are no lies and the like. And again, briefly, as a rule, one always accesses a single visitor. This means that the index is written into a register, so for example, if visitor 10 is to be accessed, the 10 is simply written into the Source Index register in this case. This register is shifted 8 bits to the left, which is the same trick as multiplying the whole thing by 256, and then the base address is added to it, so that, because it is, so to speak, the offset or rather the base where it starts, that would be, so to speak, this address that we see at the end, 00743b94, that's where the visitors start. And that is also the reason, if I mark this here, you can see many accesses to the visitors on the left, and you always see this left shift by 8 bits, which means index times x times 256, to access the corresponding visitor. Another thing that also revolves around the visitors is that there is a known Easter egg. This means that if you give the visitors certain names, such as Michael Schumacher or Mr. Bean, or also Chris Seer, then various flags are set that then, for example, cause the visitor to always take photos or be faster when driving a kart, and so on. The names themselves are not found in the code because they are very simply encrypted. There is a function here, which I have named Peep Name, so Peep are the visitors, the visitor name Matches, then Easter egg, and we see here on the left that when setting the guest name, it is called five times, 1 2 3 4 five times with different indices and here again. And if we look at the function now, we see that it is given the index here. 1 2 3 4 and a lookup is performed in a table. This is this one. And if we look at this now, I'll make it bigger, then we see the encrypted names here. The interesting thing is, these are simply the letters shifted by one letter. So, if we look at the first name, for example, LHBG and so on, then just count one letter further. L is the next letter. M H I B C G H. Yes. We already have Michael, yes, that was, for example, the encryption of Michael and so on. This means that all names are stored relatively simply, and the letter is simply counted one letter further, because it is relatively easy to decrypt in assembler, and these would be, in principle, all names that unlock a specific behavior in the code. Next, I am back in our function to recreate this scenario index, and it iterates over all scenarios that are in the directory, and each scenario file is loaded and compressed and decoded, and so on. This function does this for each respective scenario, and afterwards, the scenario index that is in the scenario file is accessed. You have already omitted certain indices, namely it goes from 0 to or from yes to 39. Yes, so the 40th index is omitted, and only from 100 onwards, exactly from 100 onwards, scenarios are included in the index again. This means that scenarios with indices 40 to 99 are not part of this index. So, if I have scenario files lying around in the directory, they will not be considered. And this is already the case in the original version. So, I have looked up what scenario IDs there are. You can see from 0 to 21 are the normal scenarios and the Megapark and so on, and then come the Loopy Landscape scenarios, they are also included in the index, but you can see here from Added attractions the DLC items, and then there are other Loopy Landens, they are completely ignored by this algorithm. This means that it was already clear beforehand that a slot was being reserved here for special scenarios for the add-ons or whatever. Yes, we have further 100 to 119 not occupied at all, and then from 110 onwards there are further scenarios, they are then processed by the index again. And last but not least, let's briefly look at how the Megapark is created. It is not present in the scenario folder by default. This is, so to speak, this function here, and what it basically does is the following. It checks that all 21 base scenarios are complete in our index Scenario Index file. It looks for the MPD file and then builds the scenario 21.sc SC4 file from it. We also see the string here for the name the file will receive. If we now look at the conversion again, we see exactly what it does here, or rather in the comment. It iterates over all bytes and performs an ROR4. This means it rolls the bytes around, i.e., it rolls the bits around four bits to the right for each bit. So what that basically means here as an example, if we have this byte sequence, then an ROR4 is performed. So ROR4 is an assembler command that simply causes the two to be swapped. Yes, they are then shifted out to the right and come back in from the left, so to speak. That's how you can imagine it. This means the part comes to the left and the part to the right. One then speaks of a so-called nibble switch, because a byte of four bits or half a byte is called a nibble, and this is a so-called nibble switch, and it does this for every bit, and then a valid scenario file is created from it. If we now switch back to the virtual machine, then in my case this MPD is not even on the disk. So if I look in the data directory, I don't see it. For me, it is still on the CD in the DD directory. If I go in here and look, then I see MPD here, and it is also, as usual, about 300 KB in size, which is a typical size for a scenario file. This means that one could also write a simple Python script here that implements the nibble switch, so to speak, and then decompresses and decodes the whole thing, and then one could, so to speak, copy this file as SC 21.Sc into the scenario folder and have the Megapark accessible, so to speak. And with that, I am done with my findings that I gained from reviewing the code with a focus on the DSTB. In conclusion, I think one can say that the bug was initiated by an API that behaved differently than the developer estimated. The fix that Chris Sar implemented here was very pragmatic. It adhered to the existing D format with the 16-bit excerpt of the timestamp, so to speak, and led to a migration to another format under the hood. Also interesting is the bypass date that is still present in the code here and which, in my opinion, was most likely intended for the QA department of Rollercoaster Tycoon. Feel free to write in the comments if you have any remarks, questions, or anything else on this topic. Thanks for watching and also thanks to Forever for the inspiration through the Rollercoaster Tycoon episode, which I recommend to everyone.