CREATING THE 2-SIDE 16-SECTOR VERSION OF PRINCE OF PERSIA ON THE APPLE II. (May 2014) from http://pferrie.host22.com/misc/lowlevel/lowlevel14.htm INTRODUCTION Just in time for the 25th anniversary of Price of Persia on the Apple II, the first ever two-sided 16-sector version! The funny thing is that I never played it on the real Apple II, only on the PC. Even after I acquired an Apple II .nib version in 2009, I didn't play it. Of course, the reason for that was because I was still using ApplePC as my Apple II emulator, and it has a fatal memory-corruption bug that crashed the game. Finally in 2014, I made the switch to AppleWin. AppleWin has its own bugs, but nothing that I couldn't work around. The retail version of the Apple II version of Prince of Persia came on two sides of a single disk. The sectors were stored in 18-sector format, and they were *full*. As a result, the 16-sector cracked versions all made use of an additional side to store those extra sectors. In 2013, about a year after the source code was recovered, Roland Gustaffson was interviewed, and expressed the opinion that the three-side version "was silly and really not impressive". I finally took the challenge to make a two-sided 16-sector version. I started with the "rebuilt from source" version. The first thing that you will notice is that it looks different in one particular place. The reason is that whoever built it used the 3.5" settings but placed it in the 5.25" format. It means that it never asks to turn over the disk when you reach level 3. It prompts to "insert" the disk instead, as though it is a single disk. IF YOU BUILD IT... So I decided to build it myself. I started AppleWin. I formatted a DOS 3.3 disk. I pre-saved some binary files the same size as the source files. I exited AppleWin. I used a hex editor to change the file types to text, to avoid the need to carry the load address and size. I converted the source code lines endings from LF to CR. I set the high bit on every character in the files. I inserted the files using my own tool. I really need to make a ProDOS version of that. I started AppleWin again. I ran Copy ][ Plus. I copied the files to a ProDOS disk. I ran Merlin. I loaded and assembled the source files, and then saved the object files. I ran Copy ][ Plus again. I copied the object files back to the DOS 3.3 disk. I exited AppleWin. I extracted the files using my own tool. I really need to make a ProDOS version of that one, too. I inserted the images in the appropriate locations in the track files. I used a hex editor to place the track files to the disk image. I tried to boot the new disk. TRY TRY AGAIN (AND AGAIN AND AGAIN) The first thing that I noticed is that it won't boot. The reason is that building the 5.25" version enables the copy-protection, which begins in the boot phase. I worked around that one by bypassing the failure check. The second thing that I noticed is that you can't play beyond level 2. The reason is another layer of copy-protection. The second-level copy protection relies on two variables, named "redherring" and "redherring2". The "redherring" variable is set indirectly during the boot-time copy protection check. However, the "redherring2" is never set in the source code version. Presumably someone removed the code (but did not notice that the declaration remains in the header file) because it's not used in the 3.5" version, because the 3.5" version is not copy-protected. Unfortunately, without that value in the 5.25" version, you can't start the later levels. It is set in the retail 5.25" version, and thus we also find out that the source code is only for the 3.5" version. I bypassed this problem by writing the proper value to the proper place manually. The third thing that I noticed is that the graphics become corrupted on level 4. The reason is yet another layer of copy-protection, which is executed before starting level 1, but the effect is delayed until after starting level 4. Nasty. :-) The end sequence is affected similarly. If the copy-protection fails, then the graphics become corrupted and the game hangs on level 14 (the reunion scene). This is an interesting design decision. If the protection was bypassed in the wrong way - by skipping the check on level 4, instead of fixing the variable that is being compared - then that second surprise awaits. I worked around that one in the correct way, by bypassing the failure check. The fourth thing that I noticed is that the graphics become corrupted and then game crashes into text mode when starting level 7. The reason is the final layer of copy-protection, which is executed after completing level 1, but the effect is delayed until the start of level 7. Very nasty. ;-) I worked around that one by bypassing the failure check. Finally, I checked the rest of the "rebuilt from source" version. The most important thing (depending on your point of view) is that all of the hidden parts are missing - the hidden routines (see below) and the hidden message (which is the decryption key for the original code). I also found that track $11 is completely missing from side B, so the side B '^' routine (see below) causes a hang. Some of the graphics data are truncated, too, when compared to the retail version which I acquired in the meantime. Even though I didn't notice any difference when I played it, I gave up on that idea, and just ripped the tracks from the 5.25" retail version instead. TURN DISK OVER Another interesting thing is how the game detects which side of the disk is in the drive. The protected version uses a unique value in the prologue data for the two sides ($A9 and $AD), and uses an API to specify which one to expect. Since a standard 16-sector disk also has a standard prologue, which is identical on both sides, that was no longer an option for me. Instead, I chose to find a "free" sector in a location that was common to both sides, and place the special byte there. When the prologue API is used, I redirected my read routine so that the next read request would first seek to the free sector and read the byte. If they matched, then the proper side was inserted already. Otherwise, the routine would read the sector periodically until that became true. The free sector that I chose actually contains data on side A. On track 0, sector F, is a funny message which is used as a decryption key. After the end of the message is some left-over code which comes from ProDOS. Presumably the disk was originally in ProDOS format, and the message was simply typed over the top, leaving some of the code behind. Within that code is an $A9 byte (part of a "LDA imm8" instruction sequence). This identifies side A. On side B, the corresponding sector is unused in my version, except for the $AD byte which I placed in the same location. This identifies side B. SIZE DOES MATTER At a high level, the solution to the size problem is one of compression - technically, further compression, since some of the data are compressed already. However, I required a compression algorithm that packs well, is fast to decompress, and most importantly, small. The size limitation was significant. The game requires 128kb of memory, and uses almost all of it. I was fortunate enough to find a small (4096 bytes) region at $d000 in main memory, in which to place my loader and the read buffer. This is the location of the original loader for the game. I simply replaced it with my own. I needed a read buffer within that region, because I had to load the compressed data somewhere before decompressing it into its final destination. I wanted the read buffer to be as large as possible, in order to reduce the number of read requests that I had to make. I managed to fit the loader code and data into under 1280 bytes (752 bytes of code, 202 bytes for the sector table (see below), the rest is dynamic data). That left me with 2816 bytes for the read buffer. That space is so small that the write routine (for saving the game after you reach side B) would not fit in memory at the same time. To work around that problem, I separated the write routine, and loaded and executed it dynamically when a save request is made. It is discarded after it has done its job. Back to the choice of compression. I have written Apple II implementations for two well-known algorithms: LZ4 and aPLib. I did not want to write another one, so I was forced to choose between them. LZ4 is both fast and small (my implementation is only 152 bytes long), but it did not pack well enough. It had to be aPLib. aPLib packs well (about 20kb smaller than LZ4), is fast enough when factoring in the reduced number of sectors to read, and small (my implementation is only 228 bytes long, so less than one sector). Some of the sectors are read only individually, some of them are read only as part of an entire track, and some of them are read using both methods, depending on the context. Once I determined how each of the sectors was loaded, I grouped them according to the size of the read, and then compressed the resulting block. I gave myself only two days total for the project, but it ended up taking me about two weeks. Most of that time was spent on finding an appropriate data structure. I finally chose a variable length region set to describe the placement of the sectors within a track. This yielded a huge advantage for the sectors which were read only in track mode, when the packed size of the single region was too large for the read buffer. In that case, the file could be split into two smaller "virtual" regions, and compressed separately to fit. The split point was determined by splitting into all 17 pairs (1 and 17, 2 and 16, 3 and 15 ...), compressing the pairs, then identifying the smallest pair. The smallest pair was chosen by the minimum number of sectors and then the minimum number of bytes. The assumption is that it costs more to decompress fewer bytes in more sectors, than to decompress more bytes in fewer sectors, even if the decompression is faster in the first case, because of the time to read and decode the additional sector. However, the flexibility of the region technique allows the alternative case to be used without any change to the code. The support for the sector reads is flexible, too. Since the regions are defined only by their start and length, I could erase the individual addresses from the 18-sector requests. This allows me to move sectors within a track, and make the corresponding change in the 18-sector request packet. This was actually needed for track 4. For track 4, the region that begins at sector $0a did not fit into 6 sectors even after compression. Fortunately, the region that begins at sector 0 needed only 7 sectors, so the region at sector $0a could move to sector 9. This was enough to get it to fit. For track $13, the first two sectors are never accessed, so I could have moved sector 2 to sector 0, but there was no benefit to it. Overall, my technique saved over 11 tracks on the first side, and over 16 tracks on the second side. Not enough for a single-side version, though. ;-) As a point of interest, I experimented with concatenating the entire data together, and including the sector offset in the table. That decreased the space quite significantly, but at a cost of increasing the size of the code, and making updating the data extremely difficult. That version saved over 13 tracks on the first side, and over 18 tracks on the second side. However, this is still not enough for a single-side version. It was not worth the effort, and it will not be released. EASTER EGGS While digging through the game code, I found several hidden routines. They are listed here. Before booting, hold both Apple keys, then press: DEL only on //GS, displays an oscilloscope use arrow keys to increase/decrease amplitude, space to change orientation, escape to quit ! displays a message, and then a lores animation RETURN continally draws a fractal, press 'c' to change colours @ displays a bouncing, spinning cube ^ pulse the drive head, move joystick to change tone, sounds like a motorcycle When playing side B, press '^' after completing a level to see an animation of Jordan waving, press a key at the end to view it again (never exits=game over) Copyright (c) 2014 Peter Ferrie All rights reserved