The Time I Cracked Two Commodore 64 Games

In 2014, I released a dual-game crack of Legacy of the Ancients and Legend of Blacksilver, two games that shared the same engine on the Commodore 64. In this post, I'll be discussing more about the process and why they're two of the best RPGs in existence. I warn you now, there's a lot of assembly code in here.

You Awake With a Start

For me, it all started in the very early 90s. My grandparents owned a couple different computers. My grandfather, who worked at a munitions plant at the time, was informed that the company would start utilizing microcomputer technology at the workplace. He didn't want to be left behind, so he purchased the Commodore 64 in order to learn more about them. He purchased manuals and all kinds of software.

I visited my Konzel grandparents very frequently and was absolutely mystified at these magical devices. My grandfather had a dual-drive Commodore 64 setup (which I later found out was somewhat uncommon because it required some soldering!) Part of their software library included some classic games and educational software, and I got to play around on it, on both the 386SX computer they owned and the Commodore 64.

Bringing It Home

Later, my father received the Commodore 64, and began using it quite a bit himself. Not so much for home productivity, but for gaming. He and I played on the machine quite a bit in my youth. He introduced me to some wonderful Electronic Arts titles such as Legacy of the Ancients, Racing Destruction Set, and M.U.L.E. I also discovered Ultima, Summer Games, and Legend of Blacksilver because he had quite a library of games.

Some of them, he'd gotten online. (This was back before the internet as we know it, of course!) And some of them had these wonderful intros. Those of us who grew up playing games on the Commodore 64 in the U.S. were no doubt introduced to Eagle Soft Incorporated at some point.

Electronic Arts and Epyx dominated my catalog. And respectively, they released Legacy of the Ancients and Legend of Blacksilver, two of the greatest RPGs of their time, in my opinion. They were both written by Chuck and John Dougherty and both released in 1987. Legacy got a release on the PC, Commodore 64 and Apple II, while Legend only got Commodore 64 and Apple II releases.

Captivation

These weren't like any games I'd played before. Maybe Ultima was similar enough— incidentally, the Questron series (also written by the Dougherty brothers) actually licensed that game design. But these two games take that design and expand on it so much that it can't even be considered an Ultima clone anymore.

There's still an open-world feel about the Legacy engine. It's primarily played in one of four screen types:

  • World
  • Town
  • Dungeon
  • Minigame

The World and Town screens both play the same way, which take place on a top-down view of the environment. The Dungeon screen (which includes the Archive) plays in a 3D perspective, where pressing left and right turns your character instead of moves them. The Minigame screen varies between the different arcade style games you are presented.

Getting Deeper into the Engine

The engine itself is a combination of BASIC and 6502 Assembly. Story elements are coded directly into the BASIC portion of the game, whereas the heavy lifting of loading files and processing graphics is done by the Assembly code. These two sections communicate back and forth very frequently. I suspect that BASIC is used for the story elements because it's far easier to proofread and modify, and Assembly code is there for the sheer speed it provides.

Copy protection for both of these games was added in after the fact, which is commonly the case for games written by a developer who isn't also the publisher. Electronic Arts used an earlier version of PirateSlayer. Epyx used a fastloader called Vorpal, which was in its second version.

More about the Commodore 64

There are a couple things that are specific to the 6502 processor that are important. In case you need a refresher...

The zero page is the first 256 bytes of RAM, which can be accessed in an accelerated manner by the processor. This memory is valuable for operations that are time-critical. In fact, certain addressing modes can only be used with an address in zero page RAM.

The KERNAL is an acronym for Keyboard Entry Read Network And Link. It is the operating system for the Commodore 64, which includes I/O operations for the disk and tape drives, functions for placing text on the screen, and many more useful functions. A lot of games relied on the KERNAL for saving games especially, since it meant that the developers didn't need to write their own disk routines which are outrageously timing sensitive (although some did, and copy protections relied on uploading code to the disk drive, which had its own 6502 processor.) This integral part of the system is documented at C64-wiki.

The stack page is located at $0100. It's a hardware-supported stash of memory that's 256 bytes long that keeps track of addresses to go back to when you jump to subroutines.

More about the crack

I started this project in the first place because VICE support for Vorpal protected games was very poor at the time, and the Eagle Soft Incorporated release used a kind of compression to make the game fit that caused load times to be almost unbearable. Plus, there's the hassle of having to switch disks, and keep a spare disk around just to save your game.

I originally wanted to make this into an Ocean format cartridge, but it became clear early on that it would be too large for the 512 kilobyte limit, plus there was no way to disable the cartridge ROM using the format. EasyFlash was the only possibility, I decided.

I wrote a program called CartridgeBuilder which would allow me to generate a CRT file which could be used with both the VICE Commodore 64 emulator and the EasyFlash software. This made things immensely easier because I didn't have to worry about manually placing things in the 1 megabyte space. Plus, I could auto-generate filesystem tables. I'm able to reserve certain banks within the cartridge (which I used for saved game storage), restrict a file to low or high banks (which was necessary for the documentation), and apply patches as I see fit.

I decided to use the CC65 suite of development tools for assembly, Regenerator for assisting in disassembly, C64List for converting BASIC, and Exomizer for compressing documentation. I did not apply compression to the data files because fast load times were the main goal. (To be honest, Vorpal gets pretty close to the load time of the cartridge, and I am very impressed by that fact.)

To make the cartridge release valuable in some way, I decided to add cracks to both versions under a combined title menu, which I wrote myself. I originally planned to have a title graphic, also:

I decided I didn't have enough knowledge to put something like this into the intro, though, and I'd been sitting on the release for a long time already. I was also going to include a 512 byte tune called Empty by 4mat. In the end, I decided I would just stick with a quiet, useful intro.

The docs were getting huge, and wouldn't fit in the space I had after I got halfway through the Legend of Blacksilver docs (which include a lot of story points!) So, I wrote a program unimaginitively called DocsTextFormatter which reduces the size of line breaks and spaces by limiting the text data to fewer bits. This meant I could decompress the text data all at once. A streaming text reader may have been a better choice here, but it worked! I managed to fit all of the docs in this encoding between $0800-$BFFF with very little space left.

I was also running out of room in the cartridge, so Exomizer was used to compress the docs.

After everything was finished, I had less than one page of space remaining. Less than 256 bytes out of over a million is pretty good usage.

Legacy of the Ancients

I wanted to crack this game from originals, but the original G64s found in the GameBase64 v9 torrent for this game were corrupted. I enlisted help from the IRC channel #c-64. (I don't remember who I got clean images from, but if it was you, please let me know!) These were all I needed in order to get the release done.

Breaking this game out of its PirateSlayer bonds was pretty easy. I wrote a program that would process the disk sectors. They only encrypted some sectors, and with a simple and easy to break XOR protection, and only for the first few bytes of a sector!

Getting the data was easy, but getting rid of the bootloader was a little harder. I cheated a little bit by freezing the 0/2/3 pages of memory because I didn't have the patience to see what needed to be initialized. (For those not in the know, freezing is like saving the state of memory, and is somewhat frowned upon in the crack scene.)

Codewheel Protection

In addition to the loader protection (in which the Electronic Arts loader adds minutes to the load time of the game on its own) there's also a codewheel protection that you must answer whenever you enter the Museum. This check is initiated by the BASIC code calling into assembly. After the assembly code runs and returns to BASIC, it performs a checksum on the codewheel code.

The codewheel code doesn't even need to be run, however! There's a part in the game where you can enter the museum without the codewheel, and that is when you are riding Pegasus back to the main continent. I found the BASIC code that allows you to do this, and made the game call that code instead. That's right: the author wrote the code that bypassed their own copy protection.

Filesystem

The driver for Legacy of the Ancients resides at $F700. This was a lot more difficult to replace than the Vorpal protection below because the filesystem has a lot more fine control over what gets loaded, and it even had the ability to stream bytes from a buffer (which is used for reading all the museum text.) I faked the stream reader by providing a function which would bank in EasyFlash, read a byte, then bank it out. A much more elegant solution would have been to read a whole page worth of the stream, but the performance difference wasn't much because banking in and out EasyFlash is a very inexpensive process.

This is what the jump table for the filesystem looks like:

$F700 (load direct)
$F703 (unknown)
$F706 (load file)
$F709 (load file w/ BASIC params)
$F70C (unknown)
$F70F (read single character to $FF)
$F712 (load standard data)
$F715 (get disk ID and store in $FF)
$F718 (save standard data)

Legend of Blacksilver

This game took quite a lot of patience. I didn't know anything about the special GCR used by Vorpal. I kinda cheated by using the game's own loader from the original G64 files to load each individual file, then used the VICE emulator to extract the data to individual files. I repeated this for each of the disks. This allowed me to get the data I wanted without having to know anything about the copy protection.

The VICE emulator didn't play nicely with the protection, and I often had to force the disk driver to retry loading files up to a half dozen times before they would load. The extraction process took a couple hours due to all the retries.

If you can manage to replace the Vorpal stub with your own custom loader, there is no further copy protection in the game. This reinforces what Epyx claims: they didn't write copy protection, they wrote a fastloader that just happens to also prevent casual copying. I'm inclined to believe them.

Filesystem

The driver for Legend of Blacksilver resides at $F300. This was a very straightforward filesystem to replace because it's very simple to begin with.

This is what the jump table for the filesystem looks like:

$F300 (init)
$F303 (load file)
$F306 (load sector)
$F309 (unknown)
$F30C (deinitialize)
$F30F (load directory)
$F312 (unknown)

EasyFlash

I had to write a bootloader for the EasyFlash cartridge. A lot of this initialization code comes from the EasyFlash SDK. One of the guidelines for creating an EasyFlash cartrdige according to the SDK documentation is to make it so you can boot right to the standard Commodore 64 KERNAL if you just hold down the Commodore key on boot. It also copies a bootstrap into the zero page which will commence loading everything.

EasyFlash File Driver in 128 Bytes

Because I wanted to keep the EasyFlash functionality loaded in EF-RAM at all times, I had to fit a driver that would perform these functions into just 128 bytes:

  • Switch EasyFlash banks
  • Swap 256 byte blocks of memory
  • Enable and disable EasyFlash ROM
  • Copy data from ROM to C64 RAM of any size
    • Data could start from any position in the ROM, not just a page boundary
    • Data could cross page boundaries and even bank boundaries

Here's the 128 byte stub I wrote at $DF03. Why there? Because I originally wanted to support the XBANK specification which must leave $DF00 unaltered. I ultimately didn't use it in the final product. I believe that some of the non-EF-RAM might use the memory at $DF01-$DF02.

This stub is identical for both the games in the release. It'll also be the only huge block of assembly code I show here (but at the end, you'll get a link to the entire project folder.)

;------------------------------------------------------------------------------
;    [ efram chunk ]
;         I wrote this because I needed something that could bank in/out the
;         EasyFlash code, perform copies and memory swaps, and not occupy RAM
;         where the game exists, all while fitting in the lower half of the
;         page (because EasyAPI uses the upper half.)
;         This starts at $DF03 to give us XBANK address at $DF00 and two
;         variables to do whatever with at $DF01 and $DF02. Plus it fits nicely
;         if you think of this as a jumptable entry address or something.
;         (Don't mind my rants, these are notes to help ME think.)
;------------------------------------------------------------------------------

.scope    EFRAM
BASE = $DF03-START

;------------------------------------------------------------------------------
;    [ rom call jump ]
;         A = low address to jump. If high address is needed too, write it
;         externally or something. I use $A000 because it fits my driver.
;------------------------------------------------------------------------------

START:
          sta ROMADDR+1+BASE
          jsr ENABLE+BASE
ROMADDR:  jsr $A000

;------------------------------------------------------------------------------
;    [ disable ef-rom ]
;         Turn off the EasyFlash LED and banking.
;         The "bne" instruction is used at the end to save space.
;------------------------------------------------------------------------------

DISABLE:
ROMCONF:  lda #$F5
          pha
          lda #%00000100
          bne SWCOMMON

;------------------------------------------------------------------------------
;    [ swap ]
;         note: source and destination must be written externally.
;         Use SWAPSRC1, SWAPSRC2, SWAPDST1, and SWAPDST2.
;         $FFFF is used as a temp address because it's all one bits and this
;         somehow benefits the life of the EasyFlash.
;         This only does up to one page at a time (the length is determined by
;         the X register) and banks out EasyFlash during the operation. It will
;         bank EasyFlash back in after it is finished.
;         Also note that due to the way the indexing is done, the swapping will
;         start at ADDRESS+1. Not a problem if X is zero, it will do a whole
;         page. But keep this in mind for all other values.
;------------------------------------------------------------------------------

SWAP:
.scope    SWAPSEC
          jsr DISABLE+BASE
LOOP:
SRC1:     lda $FFFF,x
DST1:     ldy $FFFF,x
DST2:     sta $FFFF,x
          tya
SRC2:     sta $FFFF,x
          dex
          bne LOOP
.endscope

;------------------------------------------------------------------------------
;    [ enable ef-rom ]
;         All other roms are enabled as a side-effect & EasyFlash LED is on.
;------------------------------------------------------------------------------

ENABLE:
          lda $01
          sta ROMCONF+1+BASE
          ora #$07
          pha
          lda #%10000111

;------------------------------------------------------------------------------
;    [ set ef-rom state ]
;         A = EasyFlash $DE02 state
;------------------------------------------------------------------------------

SWCOMMON:
          sta $DE02
          pla
          sta $01

;------------------------------------------------------------------------------
;    [ retore bank ]
;         This is the ONLY return point from the loader.
;         All functions lead here. Functions may also JSR here just to set
;         the EasyFlash bank.
;------------------------------------------------------------------------------

SETBANK:
BNK:      lda #$01
RESTBNK:
          ;php
          ;clc
          ;adc $DF00
          sta $DE00
          ;plp
          rts

;------------------------------------------------------------------------------
;    [ copy ]
;         note: source and destination must be written externally to
;         SRC+1/SRC+2 and DST+1/DST+2.
;         X = highbyte length (two's complement)
;         Y = lowbyte length (two's complement)
;         The "bne RESTBNK" line is the *actual* exit point in this function.
;         I have tried a number of methods and this hybrid self-modifying and
;         absolute-indexing method is the fastest I could get. It does require
;         a bit of setup due to the file length needing to be two's complement.
;         Even using zeropage was slower because of the indirect addressing.
;         BIT+BVC is used to determine if we are crossing EasyFlash banks.
;         Due to this, you CANNOT tell it not to use the high bank. However,
;         by writing different values to COPYWRP you can at least tell it you
;         don't want the low bank (the value $A0 will work OK for this.) The
;         default wrap value is $80.
;------------------------------------------------------------------------------

COPY:
.scope    COPYSEC
          jsr SETBANK+BASE
LOOP:
SRC:      lda $FFFF,y
DST:      sta $FFFF,y
          iny
          beq BANKCHK
CTROK:
          inx
          bne LOOP
          inc LEN+BASE
          bne LOOP
          lda #$01
          sta BNK+1+BASE
          bne RESTBNK
BANKCHK:          
          inc SRC+2+BASE
          inc DST+2+BASE
          bit SRC+2+BASE
          bvc CTROK
WRP:      lda #$80
          sta SRC+2+BASE
          inc BNK+1+BASE
          jsr SETBANK+BASE
          jmp CTROK+BASE
LEN:      .byte $00
.endscope

;------------------------------------------------------------------------------
;    [ footer ]
;         I used to put padding here, but now that this routine is pretty much
;         finished I removed it.
;------------------------------------------------------------------------------

END:
.endscope
EFRAMSIZE = EFRAM::END-EFRAM::START

;------------------------------------------------------------------------------
;    [ exports ]
;         To be used when writing internal variables externally.
;------------------------------------------------------------------------------

EFMEMCONF := EFRAM::ROMCONF+1+EFRAM::BASE
EFDISABLE := EFRAM::DISABLE+EFRAM::BASE
EFENABLE  := EFRAM::ENABLE+EFRAM::BASE
EFCOPY    := EFRAM::COPY+EFRAM::BASE
COPYSRC   := EFRAM::COPYSEC::SRC+1+EFRAM::BASE
COPYDST   := EFRAM::COPYSEC::DST+1+EFRAM::BASE
COPYBNK   := EFRAM::BNK+1+EFRAM::BASE
COPYWRP   := EFRAM::COPYSEC::WRP+1+EFRAM::BASE
COPYLEN   := EFRAM::COPYSEC::LEN+EFRAM::BASE
EFSWAP    := EFRAM::SWAP+EFRAM::BASE
SWAPSRC1  := EFRAM::SWAPSEC::SRC1+1+EFRAM::BASE
SWAPSRC2  := EFRAM::SWAPSEC::SRC2+1+EFRAM::BASE
SWAPDST1  := EFRAM::SWAPSEC::DST1+1+EFRAM::BASE
SWAPDST2  := EFRAM::SWAPSEC::DST2+1+EFRAM::BASE

Project Conclusion

I learned a lot while writing this crack. Some of what I learned I also applied in the Bizhawk Commodore 64 core, which was already used for one tool assisted speedrun (despite my best efforts to emphasize that the core is really immature.)

I learned that I don't really know that much about writing assembly, and I have a lot more to learn about the Commodore 64 system itself. I don't know much about the copy protections from the inside out and I took a lot of shortcuts.

I learned not to trust the emulator when writing for hardware you don't have. In fact, two VICE bugs came out of the development of this crack. One was for the initial state of EasyFlash 3 RAM (which did not match the emulation) and the other was an obscure debugger problem when you set Memory Store breakpoints. After this blunder, I got a discount on the hardware on the condition I would release my crack. A couple weeks later, I received the hardware and was able to finally verify what I was doing!

I learned that the community is immensely supportive, but if and only if you show you're actually putting effort forward. I suppose the scene's done being burned by people who are all talk. I certainly wasn't taken seriously until I had a demo cartridge image to share.

Overall, I really struggled with this release. But it was immensely enjoyable to do, and was worth all the trouble. I got the recognition I wanted, and the satisfaction of making playing these two wonderful games that much greater. Legend of Blacksilver wasn't even trained before my release, just broken. So at least I got to be the first at something.

I'd like to thank the folks over at the Lemon64 Forum post where I announced this project, and the folks at the Commodore 64 Scene Database Forum. Users from both communities put in some testing time which really helped.

What's Next?

I've taken apart the Questron and Questron II games and plan to do a full cartridge conversion for those games. These two games are much closer to the Ultima series and were specifically requested by Moloch in the responses to my first ever EasyFlash release on CSDB.

I spent almost two years on this release, but a lot of the hard work is done already. CartridgeBuilder will make short work of assembling the final product.

Thanks for reading! Hope to release the next Commodore 64 project soon~!