commit 3049dc938c36585415c30482bc61fc47660c21ea Author: Mark Moxon Date: Thu Sep 1 10:14:30 2022 +0100 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..af6f93d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acff8d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Project files +.vscode/tasks.json +work/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bccf4a --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# Flicker-free Elite on the Commodore 64 + +This repository contains a patch for Commodore 64 Elite that drastically improves the quality of the graphics. It does this by removing flicker from the ship-drawing routines. + +To play the flicker-free version, see the section on [playing flicker-free Commodore 64 Elite](playing-flicker-free-commodore-64-elite]). + +To read about how the patch weaves its magic, see the section on [how the patch works](#how-the-patch-works). + +If you are interested in building and applying the patch yourself, see the section on [building the patch](#building-the-patch). + +## Contents + +* [Acknowledgements](#acknowledgements) + + * [A note on licences, copyright etc.](#user-content-a-note-on-licences-copyright-etc) + +* [Playing flicker-free Commodore 64 Elite](playing-flicker-free-commodore-64-elite]) + +* [How the patch works](#how-the-patch-works) + + * [A better algorithm](a-better-algorithm) + * [The patching process](the-patching-process) + +* [Building the patch](#building-the-patch) + + * [Requirements](#requirements) + * [Applying the patch](#applying-the-patch) + +## Acknowledgements + +Commodore 64 Elite was written by Ian Bell and David Braben and published by Firebird, and is copyright © D. Braben and I. Bell 1985. + +The game disks in this repository are similar to those released on [Ian Bell's personal website](http://www.elitehomepage.org/), but to ensure accuracy to the released versions, they're actually the versions from the [Commodore 64 Preservation Project on archive.org](https://archive.org/download/C64_Preservation_Project_10th_Anniversary_Collection) (as it turns out that the disk images on Ian Bell's site differ from the official versions). + +The commentary is copyright © Mark Moxon. Any misunderstandings or mistakes in the documentation are entirely my fault. + +Huge thanks are due to the original authors for not only creating such an important piece of my childhood, but also for releasing the source code for us to play with. You can find more information about my Elite project in the [accompanying website's project page](https://www.bbcelite.com/about_site/about_this_project.html). + +### A note on licences, copyright etc. + +This repository is _not_ provided with a licence, and there is intentionally no `LICENSE` file provided. + +According to [GitHub's licensing documentation](https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/licensing-a-repository), this means that "the default copyright laws apply, meaning that you retain all rights to your source code and no one may reproduce, distribute, or create derivative works from your work". + +The reason for this is that my patch is intertwined with the original Elite game code, and the original game code is copyright. The whole repository is therefore covered by default copyright law, to ensure that this copyright is respected. + +Under GitHub's rules, you have the right to read and fork this repository... but that's it. No other use is permitted, I'm afraid. + +My hope is that the educational and non-profit intentions of this repository will enable it to stay hosted and available, but the original copyright holders do have the right to ask for it to be taken down, in which case I will comply without hesitation. I do hope, though, that along with the various other disassemblies, commentaries and disk images of Elite, it will remain viable. + +## Playing flicker-free Commodore 64 Elite + +The flicker-free version of Commodore 64 Elite is available in two versions: NTSC and PAL. These are both based on the GMA86 release of Elite from 1986. + +To play the patched game in an emulator or on a real machine, you can download disk images for both versions from the [flicker-free-disks](flicker-free-disks) folder. These have been tested in the VICE emulator, but should also work in other emulators and on real machines. If you don't know which one to use, try the PAL version first, as that seems to be the default for most emulators. + +Loading commander files should work in exactly the same way as in the unpatched GMA86 version; the only changes in the patch are graphical. + +## How the patch works + +### A better algorithm + +The 1986 releases of the BBC Master and Apple II versions of Elite saw a marked improvement in the ship-drawing algorithm that seriously reduced flicker without slowing down the game. + +In the original versions of Elite, such as those for the BBC Micro, Acorn Electron and Commodore 64, ships were animated on-screen by first erasing them entirely, and then redrawing them in their new positions. The improved algorithm in the BBC Master and Apple II versions is similar, but instead of erasing the entire ship and then redrawing a whole new ship, it erases one line of the old ship and immediately redraws one line of the new ship, repeating the process until the whole ship gets redrawn, one line at a time. This interleaving of the line-drawing process results in much smoother ship graphics, and without adding any extra steps, so it doesn't affect the game speed. + +Unfortunately planets are unaffected by the patch, as they use a completely different routine, so they still flicker. However ships, stations, asteroids and missiles are much improved. + +For more information on the flicker-free algorithm, see these deep dives on [flicker-free ship drawing](https://www.bbcelite.com/deep_dives/flicker-free_ship_drawing.html) and [backporting the flicker-free algorithm](https://www.bbcelite.com/deep_dives/backporting_the_flicker-free_algorithm.html) in my BBC Micro Elite project. + +### The patching process + +In order to patch Elite to use the new algorithm, we have to do the following: + +* Use c1451 to extract the game binaries from the original disk image + +* Use BeebAsm to assemble the additional code that's required for flicker-free ships + +* Use Python to inject this new code into the game binaries and adjust the code in a number of places, and to disable any copy protection in the original binaries + +* Use c1451 to create a new disk image containing the flicker-free binaries + +To find out more about the exact steps in this process, check out the following files, which contain lots of comments about how the process works. + +* The [build.sh](build.sh) script controls the build. Read this for an overview of the patching process, which is described above. + +* The [elite-flicker-free.asm](src/elite-flicker-free.asm) file assembles and saves out a number of code binaries. These contain larger blocks of code that implements the flicker-free algorithm, which are saved as binary files that are ready to be injected into the game binary to implement the patch. + +* The [elite-modify.py](src/elite-modify.py) modifies the game binary. It does this by loading the binary into memory, patching it by injecting the output from BeebAsm, and making a number of modifications to the code before saving out the modified version. It also disables any copy protection so the resulting + +## Building the patch + +### Requirements + +If you want to patch Commodore 64 Elite to the flicker-free version yourself, or you want to explore the patching process in more detail, then you will need the following: + +* A Mac or Linux box. The process may work on the Windows Subsystem for Linux, but I haven't tested it. + +* BeebAsm, which can be downloaded from the [BeebAsm repository](https://github.com/stardot/beebasm). You will have to build your own executable with `make code`. + +* Python. Both versions 2.7 and 3.x should work. + +* c1652 from the VICE emulator, which can be downloaded from the [VICE site](https://vice-emu.sourceforge.io). + +Let's look at how to patch Commodore 64 Elite to get those flicker-free ships. + +### Applying the patch + +The patching process is implemented by a bash script called `build.sh` in the root folder of the repository. If any of BeebAsm, Python or c1541 are not on your path, then you can either fix this, or you can edit the `$beebasm`, `$python` or `$c1541` variables in the first three lines of `build.sh` to point to their locations. + +You also need to change directory to the repository folder (i.e. the same folder as `build.sh`), and make the script executable. + +All being well, doing the following: + +``` +cd /path/to/c64-elite-flicker-free +chmod a+x build.sh +./build.sh +``` + +will produce two disk images in the `compiled-game-disks` folder that contain the patched game, which you can then load into an emulator or real machine. + +The build process also verifies the build against binaries that are known to be correct, and BeebAsm log files and interim binaries are saved in the `work` folder. + +--- + +Right on, Commanders! + +_Mark Moxon_ \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..46f99b3 --- /dev/null +++ b/build.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +# Set up program locations (change these if they are not aleady in the path) +beebasm="beebasm" +c1541="c1541" +python="python" + +# Create an empty work folder +rm -fr work +mkdir work + +# First, we build the NTSC version +cd work + +# Extract the files from the original disk image +$c1541 -attach "../original-disks/elite[firebird_1986](ntsc)(v060186)(!).g64" \ + -extract + +# Assemble the additional code required for flicker-free ships +$beebasm -i ../src/elite-flicker-free.asm -v > compile.txt + +# Modify the main game code +$python ../src/elite-modify.py ntsc + +# Rebuild the game disk +$c1541 \ + -format "no-flicker elite,1" \ + d64 \ + ../flicker-free-disks/c64-elite-flicker-free-ntsc.d64 \ + -attach ../flicker-free-disks/c64-elite-flicker-free-ntsc.d64 \ + -write firebird \ + -write gma1.modified gma1 \ + -write gma3 \ + -write gma4 \ + -write gma5 \ + -write gma6.encrypted gma6 + +# Report checksums +cd .. +$python src/crc32.py reference-binaries/ntsc work + +# And now, we build the PAL version +cd work + +# Extract the files from the original disk image +$c1541 -attach "../original-disks/elite[firebird_1986](pal)(v040486).g64" \ + -extract + +# Assemble the additional code required for flicker-free ships +$beebasm -i ../src/elite-flicker-free.asm -v >> compile.txt + +# Modify the main game code +$python ../src/elite-modify.py pal + +# Rebuild the game disk +$c1541 \ + -format "no-flicker elite,1" \ + d64 \ + ../flicker-free-disks/c64-elite-flicker-free-pal.d64 \ + -attach ../flicker-free-disks/c64-elite-flicker-free-pal.d64 \ + -write firebird \ + -write byebyejulie \ + -write gma1.modified gma1 \ + -write gma3 \ + -write gma4 \ + -write gma5 \ + -write gma6.encrypted gma6 + +# Report checksums +cd .. +$python src/crc32.py reference-binaries/pal work diff --git a/flicker-free-disks/c64-elite-flicker-free-ntsc.d64 b/flicker-free-disks/c64-elite-flicker-free-ntsc.d64 new file mode 100644 index 0000000..b2fd2e4 Binary files /dev/null and b/flicker-free-disks/c64-elite-flicker-free-ntsc.d64 differ diff --git a/flicker-free-disks/c64-elite-flicker-free-pal.d64 b/flicker-free-disks/c64-elite-flicker-free-pal.d64 new file mode 100644 index 0000000..16fa06c Binary files /dev/null and b/flicker-free-disks/c64-elite-flicker-free-pal.d64 differ diff --git a/original-disks/elite[firebird_1986](ntsc)(v060186)(!).g64 b/original-disks/elite[firebird_1986](ntsc)(v060186)(!).g64 new file mode 100644 index 0000000..12e3a0f Binary files /dev/null and b/original-disks/elite[firebird_1986](ntsc)(v060186)(!).g64 differ diff --git a/original-disks/elite[firebird_1986](pal)(v040486).g64 b/original-disks/elite[firebird_1986](pal)(v040486).g64 new file mode 100644 index 0000000..3b22f8b Binary files /dev/null and b/original-disks/elite[firebird_1986](pal)(v040486).g64 differ diff --git a/reference-binaries/ntsc/extra.bin b/reference-binaries/ntsc/extra.bin new file mode 100644 index 0000000..e61fe1c Binary files /dev/null and b/reference-binaries/ntsc/extra.bin differ diff --git a/reference-binaries/ntsc/firebird b/reference-binaries/ntsc/firebird new file mode 100644 index 0000000..03585c0 Binary files /dev/null and b/reference-binaries/ntsc/firebird differ diff --git a/reference-binaries/ntsc/gma1 b/reference-binaries/ntsc/gma1 new file mode 100644 index 0000000..bac9255 Binary files /dev/null and b/reference-binaries/ntsc/gma1 differ diff --git a/reference-binaries/ntsc/gma1.modified b/reference-binaries/ntsc/gma1.modified new file mode 100644 index 0000000..e11afe5 Binary files /dev/null and b/reference-binaries/ntsc/gma1.modified differ diff --git a/reference-binaries/ntsc/gma3 b/reference-binaries/ntsc/gma3 new file mode 100644 index 0000000..07f0050 Binary files /dev/null and b/reference-binaries/ntsc/gma3 differ diff --git a/reference-binaries/ntsc/gma4 b/reference-binaries/ntsc/gma4 new file mode 100644 index 0000000..d916d52 Binary files /dev/null and b/reference-binaries/ntsc/gma4 differ diff --git a/reference-binaries/ntsc/gma5 b/reference-binaries/ntsc/gma5 new file mode 100644 index 0000000..0ae5b39 Binary files /dev/null and b/reference-binaries/ntsc/gma5 differ diff --git a/reference-binaries/ntsc/gma6 b/reference-binaries/ntsc/gma6 new file mode 100644 index 0000000..3b098fa Binary files /dev/null and b/reference-binaries/ntsc/gma6 differ diff --git a/reference-binaries/ntsc/gma6.decrypted b/reference-binaries/ntsc/gma6.decrypted new file mode 100644 index 0000000..659ef5e Binary files /dev/null and b/reference-binaries/ntsc/gma6.decrypted differ diff --git a/reference-binaries/ntsc/gma6.encrypted b/reference-binaries/ntsc/gma6.encrypted new file mode 100644 index 0000000..8fbbb05 Binary files /dev/null and b/reference-binaries/ntsc/gma6.encrypted differ diff --git a/reference-binaries/ntsc/gma6.modified b/reference-binaries/ntsc/gma6.modified new file mode 100644 index 0000000..ea72d19 Binary files /dev/null and b/reference-binaries/ntsc/gma6.modified differ diff --git a/reference-binaries/ntsc/ll155.bin b/reference-binaries/ntsc/ll155.bin new file mode 100644 index 0000000..b2e15e9 Binary files /dev/null and b/reference-binaries/ntsc/ll155.bin differ diff --git a/reference-binaries/ntsc/shppt.bin b/reference-binaries/ntsc/shppt.bin new file mode 100644 index 0000000..4e3f6dc --- /dev/null +++ b/reference-binaries/ntsc/shppt.bin @@ -0,0 +1 @@ + }6CɎ \Ci \((Lx%((Lxln5kimL \ No newline at end of file diff --git a/reference-binaries/pal/byebyejulie b/reference-binaries/pal/byebyejulie new file mode 100644 index 0000000..9429336 Binary files /dev/null and b/reference-binaries/pal/byebyejulie differ diff --git a/reference-binaries/pal/extra.bin b/reference-binaries/pal/extra.bin new file mode 100644 index 0000000..e61fe1c Binary files /dev/null and b/reference-binaries/pal/extra.bin differ diff --git a/reference-binaries/pal/firebird b/reference-binaries/pal/firebird new file mode 100644 index 0000000..03585c0 Binary files /dev/null and b/reference-binaries/pal/firebird differ diff --git a/reference-binaries/pal/gma1 b/reference-binaries/pal/gma1 new file mode 100644 index 0000000..2b23527 Binary files /dev/null and b/reference-binaries/pal/gma1 differ diff --git a/reference-binaries/pal/gma1.modified b/reference-binaries/pal/gma1.modified new file mode 100644 index 0000000..c915859 Binary files /dev/null and b/reference-binaries/pal/gma1.modified differ diff --git a/reference-binaries/pal/gma3 b/reference-binaries/pal/gma3 new file mode 100644 index 0000000..bc6a2b5 Binary files /dev/null and b/reference-binaries/pal/gma3 differ diff --git a/reference-binaries/pal/gma4 b/reference-binaries/pal/gma4 new file mode 100644 index 0000000..d916d52 Binary files /dev/null and b/reference-binaries/pal/gma4 differ diff --git a/reference-binaries/pal/gma5 b/reference-binaries/pal/gma5 new file mode 100644 index 0000000..0ae5b39 Binary files /dev/null and b/reference-binaries/pal/gma5 differ diff --git a/reference-binaries/pal/gma6 b/reference-binaries/pal/gma6 new file mode 100644 index 0000000..3b098fa Binary files /dev/null and b/reference-binaries/pal/gma6 differ diff --git a/reference-binaries/pal/gma6.decrypted b/reference-binaries/pal/gma6.decrypted new file mode 100644 index 0000000..659ef5e Binary files /dev/null and b/reference-binaries/pal/gma6.decrypted differ diff --git a/reference-binaries/pal/gma6.encrypted b/reference-binaries/pal/gma6.encrypted new file mode 100644 index 0000000..8fbbb05 Binary files /dev/null and b/reference-binaries/pal/gma6.encrypted differ diff --git a/reference-binaries/pal/gma6.modified b/reference-binaries/pal/gma6.modified new file mode 100644 index 0000000..ea72d19 Binary files /dev/null and b/reference-binaries/pal/gma6.modified differ diff --git a/reference-binaries/pal/ll155.bin b/reference-binaries/pal/ll155.bin new file mode 100644 index 0000000..b2e15e9 Binary files /dev/null and b/reference-binaries/pal/ll155.bin differ diff --git a/reference-binaries/pal/shppt.bin b/reference-binaries/pal/shppt.bin new file mode 100644 index 0000000..4e3f6dc --- /dev/null +++ b/reference-binaries/pal/shppt.bin @@ -0,0 +1 @@ + }6CɎ \Ci \((Lx%((Lxln5kimL \ No newline at end of file diff --git a/src/crc32.py b/src/crc32.py new file mode 100644 index 0000000..eccb5e9 --- /dev/null +++ b/src/crc32.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# +# ****************************************************************************** +# +# ELITE VERIFICATION SCRIPT +# +# Written by Kieran Connell, extended by Mark Moxon +# +# This script performs checksums on the compiled files from the build process, +# and checks them against the extracted files from the original source disc +# +# ****************************************************************************** + +from __future__ import print_function +import sys +import os +import os.path +import zlib + + +def main(): + if len(sys.argv) <= 2: + # Do CRC on single folder + folder = sys.argv[1] if len(sys.argv) == 2 else "." + names = sorted(os.listdir(folder)) + + print() + print('Checksum Size Filename') + print('------------------------------------------') + + for name in names: + if not name.startswith("."): + full_name = os.path.join(folder, name) + if not os.path.isfile(full_name): + continue + with open(full_name, 'rb') as f: + data = f.read() + print('%08x %5d %s' % ( + zlib.crc32(data) & 0xffffffff, + len(data), + full_name) + ) + print() + else: + # Do CRC on two folders + folder1 = sys.argv[1] + names1 = sorted(os.listdir(folder1)) + folder2 = sys.argv[2] + names2 = sorted(os.listdir(folder2)) + names = list(names1) + names.extend(x for x in names2 if x not in names) + + if 'reference-binaries' in folder1: + src = '[--originals--]' + elif 'output' in folder1: + src = '[---output----]' + else: + src = '[{0: ^13}]'.format(folder1[0:13]).replace(' ', '-') + + if 'reference-binaries' in folder2: + dest = '[--originals--]' + elif 'output' in folder2: + dest = '[---output----]' + else: + dest = '[{0: ^13}]'.format(folder2[0:13]).replace(' ', '-') + + print() + print(src + ' ' + dest) + print('Checksum Size Checksum Size Match Filename') + print('-----------------------------------------------------------') + + for name in names: + if not name.startswith("."): + full_name1 = os.path.join(folder1, name) + full_name2 = os.path.join(folder2, name) + + if name in names1 and name in names2 and os.path.isfile(full_name1) and os.path.isfile(full_name2): + with open(full_name1, 'rb') as f: + data1 = f.read() + with open(full_name2, 'rb') as f: + data2 = f.read() + crc1 = zlib.crc32(data1) & 0xffffffff + crc2 = zlib.crc32(data2) & 0xffffffff + match = ' Yes ' if crc1 == crc2 and len(data1) == len(data2) else ' No ' + print('%08x %5d %08x %5d %s %s' % ( + crc1, + len(data1), + crc2, + len(data2), + match, + name) + ) + elif name in names1 and os.path.isfile(full_name1): + with open(full_name1, 'rb') as f: + data = f.read() + print('%08x %5d %s %s %s %s' % ( + zlib.crc32(data) & 0xffffffff, + len(data), + '- ', + ' -', + ' - ', + name) + ) + print() + + +if __name__ == '__main__': + main() diff --git a/src/elite-flicker-free.asm b/src/elite-flicker-free.asm new file mode 100644 index 0000000..b9c2510 --- /dev/null +++ b/src/elite-flicker-free.asm @@ -0,0 +1,429 @@ +\ ****************************************************************************** +\ +\ BBC MASTER ELITE GAME SOURCE (FLICKER-FREE ROUTINES) +\ +\ BBC Master Elite was written by Ian Bell and David Braben and is copyright +\ Acornsoft 1986 +\ +\ The code on this site has been reconstructed from a disassembly of the version +\ released on Ian Bell's personal website at http://www.elitehomepage.org/ +\ +\ The commentary is copyright Mark Moxon, and any misunderstandings or mistakes +\ in the documentation are entirely my fault +\ +\ The terminology and notations used in this commentary are explained at +\ https://www.bbcelite.com/about_site/terminology_used_in_this_commentary.html +\ +\ The deep dive articles referred to in this commentary can be found at +\ https://www.bbcelite.com/deep_dives +\ +\ ****************************************************************************** + +GUARD &CE00 \ Guard against assembling over memory used by game + +\ ****************************************************************************** +\ +\ Configuration variables +\ +\ The addresses in the following are from when the game binary is loaded into +\ memory. They were calculated by analysing a memory dump of the running game, +\ searching for patterns in the bytes to match them with the corrsponding code +\ from the BBC Micro version (which is very similar, if you ignore any different +\ addresses). +\ +\ XX14 is an unused variable in BBC Micro Elite, and the corresponding address +\ in Commodore 64 Elite is used by something else. Luckily locations $FB to $FE +\ are unused by Elite and the OS, so we can sneak XX14 in there instead. +\ +\ Also, the Y variable contains the height of the space view in pixels, divided +\ by 2. This is 96 on the BBC Micro, but the space view is 144 pixels on the +\ Commodore 64, so Y needs to be set to 72 instead. +\ +\ ****************************************************************************** + +XX1 = $0009 +INWK = $0009 +XX19 = $002A +CNT = $0030 +K3 = $0035 +K4 = $0043 +XX0 = $0057 +V = $005B +XX15 = $006B +X1 = $006B +Y1 = $006C +X2 = $006D +Y2 = $006E +XX12 = $0071 +XX17 = $009F +XX4 = $00AD +XX20 = $00AE +XX14 = $00FB +PROJ = $7D1F +LL75 = $9FB8 +LL80 = $A13F +LL30 = $AB91 +Y = 72 + +\ ****************************************************************************** +\ +\ Name: SHPPT +\ Type: Subroutine +\ Category: Drawing ships +\ Summary: Draw a distant ship as a point rather than a full wireframe +\ +\ ****************************************************************************** + +ORG $9932 + +.SHPPT + + JSR PROJ \ Project the ship onto the screen, returning: + \ + \ * K3(1 0) = the screen x-coordinate + \ * K4(1 0) = the screen y-coordinate + \ * A = K4+1 + + ORA K3+1 \ If either of the high bytes of the screen coordinates + BNE nono \ are non-zero, jump to nono as the ship is off-screen + + LDA K4 \ Set A = the y-coordinate of the dot + + CMP #Y*2-2 \ If the y-coordinate is bigger than the y-coordinate of + BCS nono \ the bottom of the screen, jump to nono as the ship's + \ dot is off the bottom of the space view + + JSR Shpt \ Call Shpt to draw a horizontal 4-pixel dash for the + \ first row of the dot (i.e. a four-pixel dash) + + LDA K4 \ Set A = y-coordinate of dot + 1 (so this is the second + CLC \ row of the two-pixel-high dot) + ADC #1 + + JSR Shpt \ Call Shpt to draw a horizontal 4-pixel dash for the + \ first row of the dot (i.e. a four-pixel dash) + + LDA #%00001000 \ Set bit 3 of the ship's byte #31 to record that we + ORA XX1+31 \ have now drawn something on-screen for this ship + STA XX1+31 + + JMP LL155 \ Jump to LL155 to draw any remaining lines that are + \ still in the ship line heap and return from the + \ subroutine using a tail call + +.nono + + LDA #%11110111 \ Clear bit 3 of the ship's byte #31 to record that + AND XX1+31 \ nothing is being drawn on-screen for this ship + STA XX1+31 + + JMP LL155 \ Jump to LL155 to draw any remaining lines that are + \ still in the ship line heap and return from the + \ subroutine using a tail call + +.Shpt + + \ This routine draws a horizontal 4-pixel dash, for + \ either the top or the bottom of the ship's dot + + STA Y1 \ Store A in both y-coordinates, as this is a horizontal + STA Y2 \ dash at y-coordinate A + + LDA K3 \ Set A = screen x-coordinate of the ship dot + + STA X1 \ Store the x-coordinate of the ship dot in X1, as this + \ is where the dash starts + + CLC \ Set A = screen x-coordinate of the ship dot + 3 + ADC #3 + + BCC P%+4 \ If the addition overflowed, set A = 255, the + LDA #255 \ x-coordinate of the right edge of the screen + + STA X2 \ Store the x-coordinate of the ship dot in X1, as this + \ is where the dash starts + + JMP LLX30 \ Draw this edge using flicker-free animation, by first + \ drawing the ship's new line and then erasing the + \ corresponding old line from the screen, and return + \ from the subroutine using a tail call + +SAVE "shppt.bin", SHPPT, P% + +\ ****************************************************************************** +\ +\ Name: LL9 (Part 11 of 12) +\ Type: Subroutine +\ Category: Drawing ships +\ Summary: Draw ship: Loop back for the next edge +\ Deep dive: Drawing ships +\ +\ ****************************************************************************** + +ORG $A15B + +.LL78 + + LDA XX14 \ If XX14 >= CNT, skip to LL81 so we don't loop back for + CMP CNT \ the next edge (CNT was set to the maximum heap size + BCS LL81 \ for this ship in part 10, so this checks whether we + \ have just run out of space in the ship line heap, and + \ stops drawing edges if we have) + + LDA V \ Increment V by 4 so V(1 0) points to the data for the + CLC \ next edge + ADC #4 + STA V + + BCC ll81 \ If the above addition didn't overflow, jump to ll81 + + INC V+1 \ Otherwise increment the high byte of V(1 0), as we + \ just moved the V(1 0) pointer past a page boundary + +.ll81 + + INC XX17 \ Increment the edge counter to point to the next edge + + LDY XX17 \ If Y >= XX20, which contains the number of edges in + CPY XX20 \ the blueprint, skip the following + BCS P%+5 + + JMP LL75-2 \ Loop back to LL75-2 to process the next edge (we jump + \ to LL75-2 as we have modified the code around LL75, + \ which moves the jump point back by two bytes) + +.LL81 + + NOP + +SAVE "ll78.bin", LL78, P% + +\ ****************************************************************************** +\ +\ Name: LL9 (Part 12 of 12) +\ Type: Subroutine +\ Category: Drawing ships +\ Summary: Draw ship: Draw all the visible edges from the ship line heap +\ Deep dive: Drawing ships +\ +\ ****************************************************************************** + +ORG $A178 + +.LL155 + + LDY XX14 \ Set Y to the offset in the line heap XX14 + +.LL27 + + CPY XX14+1 \ If Y >= XX14+1, jump to LLEX to return from the ship + BCS LLEX \ drawing routine, because the index in Y is greater + \ than the size of the existing ship line heap, which + \ means we have alrady erased all the old ships lines + \ when drawing the new ship + + \ If we get here then Y < XX14+1, which means Y is + \ pointing to an on-screen line from the old ship that + \ we need to erase + + LDA (XX19),Y \ Fetch the X1 line coordinate from the heap and store + STA XX15 \ it in XX15 + + INY \ Increment the heap pointer + + LDA (XX19),Y \ Fetch the Y1 line coordinate from the heap and store + STA XX15+1 \ it in XX15+1 + + INY \ Increment the heap pointer + + LDA (XX19),Y \ Fetch the X2 line coordinate from the heap and store + STA XX15+2 \ it in XX15+2 + + INY \ Increment the heap pointer + + LDA (XX19),Y \ Fetch the Y2 line coordinate from the heap and store + STA XX15+3 \ it in XX15+3 + + JSR LL30 \ Draw a line from (X1, Y1) to (X2, Y2) to erase it from + \ the screen + + INY \ Increment the heap pointer + + JMP LL27 \ Loop back to LL27 to draw (i.e. erase) the next line + \ from the heap + +.LLEX + + LDA XX14 \ Store XX14 in the first byte of the ship line heap + LDY #0 + STA (XX19),Y + +.LL82 + + RTS \ Return from the subroutine + +SAVE "ll155.bin", LL155, P% + +ORG $CCE0 + +\ ****************************************************************************** +\ +\ Name: LLX30 +\ Type: Subroutine +\ Category: Drawing lines +\ Summary: Draw a ship line using flicker-free animation +\ +\ ****************************************************************************** + +.LLX30 + + LDY XX14 \ Set Y = XX14, to get the offset within the ship line + \ heap where we want to insert our new line + + CPY XX14+1 \ Compare XX14 and XX14+1 and store the flags on the + PHP \ stack so we can retrieve them later + + LDX #3 \ We now want to copy the line coordinates (X1, Y1) and + \ (X2, Y2) to XX12...XX12+3, so set a counter to copy + \ 4 bytes + +.LLXL + + LDA X1,X \ Copy the X-th byte of X1/Y1/X2/Y2 to the X-th byte of + STA XX12,X \ XX12 + + DEX \ Decrement the loop counter + + BPL LLXL \ Loop back until we have copied all four bytes + + JSR LL30 \ Draw a line from (X1, Y1) to (X2, Y2) + + LDA (XX19),Y \ Set X1 to the Y-th coordinate on the ship line heap, + STA X1 \ i.e. one we are replacing in the heap + + LDA XX12 \ Replace it with the X1 coordinate in XX12 + STA (XX19),Y + + INY \ Increment the index to point to the Y1 coordinate + + LDA (XX19),Y \ Set Y1 to the Y-th coordinate on the ship line heap, + STA Y1 \ i.e. one we are replacing in the heap + + LDA XX12+1 \ Replace it with the Y1 coordinate in XX12+1 + STA (XX19),Y + + INY \ Increment the index to point to the X2 coordinate + + LDA (XX19),Y \ Set X1 to the Y-th coordinate on the ship line heap, + STA X2 + + LDA XX12+2 \ Replace it with the X2 coordinate in XX12+2 + STA (XX19),Y + + INY \ Increment the index to point to the Y2 coordinate + + LDA (XX19),Y \ Set Y2 to the Y-th coordinate on the ship line heap, + STA Y2 + + LDA XX12+3 \ Replace it with the Y2 coordinate in XX12+3 + STA (XX19),Y + + INY \ Increment the index to point to the next coordinate + STY XX14 \ and store the updated index in XX14 + + PLP \ Restore the result of the comparison above, so if the + BCS LL82a \ original value of XX14 >= XX14+1, then we have already + \ redrawn all the lines from the old ship's line heap, + \ so return from the subroutine (as LL82 contains an + \ RTS) + + JMP LL30 \ Otherwise there are still more lines to erase from the + \ old ship on-screen, so the coordinates in (X1, Y1) and + \ (X2, Y2) that we just pulled from the ship line heap + \ point to a line that is still on-screen, so call LL30 + \ to draw this line and erase it from the screen, + \ returning from the subroutine using a tail call + +.LL82a + + RTS \ Return from the subroutine + +\ ****************************************************************************** +\ +\ Name: LL9 (Part 1 of 12) +\ Type: Subroutine +\ Category: Drawing ships +\ Summary: Draw ship: Check if ship is exploding, check if ship is in front +\ Deep dive: Drawing ships +\ +\ ****************************************************************************** + +.PATCH1 + + \ We replace the following two instructions in part 1 of + \ LL9 with JSR PATCH1, so we start with those two + \ instructions to ensure that they still get done + + LDA #31 \ Set XX4 = 31 to store the ship's distance for later + STA XX4 \ comparison with the visibility distance. We will + \ update this value below with the actual ship's + \ distance if it turns out to be visible on-screen + + \ We now set things up for flicker-free ship plotting, + \ by setting the following: + \ + \ XX14 = offset to the first coordinate in the ship's + \ line heap + \ + \ XX14+1 = the number of bytes in the heap for the + \ ship that's currently on-screen (or 0 if + \ there is no ship currently on-screen) + + LDY #1 \ Set XX14 = 1, the offset of the first set of line + STY XX14 \ coordinates in the ship line heap + + DEY \ Decrement Y to 0 + + LDA #%00001000 \ If bit 3 of the ship's byte #31 is set, then the ship + BIT INWK+31 \ is currently being drawn on-screen, so skip the + BNE P%+5 \ following two instructions + + LDA #0 \ The ship is not being drawn on screen, so set A = 0 + \ so that XX14+1 gets set to 0 below (as there are no + \ existing coordinates on the ship line heap for this + \ ship) + + EQUB $2C \ Skip the next instruction by turning it into + \ $2C $B1 $BD, or BIT $BDB1 which does nothing apart + \ from affect the flags + + LDA (XX19),Y \ Set XX14+1 to the first byte of the ship's line heap, + STA XX14+1 \ which contains the number of bytes in the heap + + RTS + +\ ****************************************************************************** +\ +\ Name: LL9 (Part 10 of 12) +\ Type: Subroutine +\ Category: Drawing ships +\ Summary: Draw ship: Check if ship is exploding, check if ship is in front +\ Deep dive: Drawing ships +\ +\ ****************************************************************************** + +.PATCH2 + + \ We replace the JMP LL78 instruction at the end of part + \ 10 of LL9 with JSR PATCH2, so this effectively inserts + \ the call to LLX30 at the end of part 10, as required + + JSR LLX30 \ Draw the laser line using flicker-free animation, by + \ first drawing the new laser line and then erasing the + \ corresponding old line from the screen + + JMP LL78 \ Jump down to part 11 + + +SAVE "extra.bin", LLX30, P% + diff --git a/src/elite-modify.py b/src/elite-modify.py new file mode 100644 index 0000000..13b3ab8 --- /dev/null +++ b/src/elite-modify.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python +# +# ****************************************************************************** +# +# COMMODORE 64 ELITE FLICKER-FREE MODIFICATION SCRIPT +# +# Written by Mark Moxon +# +# This script applies flicker-free ship-drawing to Commodore 64 Elite, as +# described here: +# +# https://www.bbcelite.com/deep_dives/flicker-free_ship_drawing.html +# +# It does the following: +# +# * Decrypt the gma6 file +# * Modify the gma6 file to draw flicker-free ships +# * Encrypt the gma6 file +# * Modify the gma1 file to remove disk protection +# +# Run this script by changing directory to the folder containing the disk files +# and running the script with "python elite-modify.py" +# +# This modification script works with the elite[firebird_1986](pal)(v040486).g64 +# disk image +# +# ****************************************************************************** + +from __future__ import print_function +import os +import sys + + +# Convert a C64 address into the corresponding offset within the gma6 file + +def get_offset(addr): + return addr - load_address + + +# Insert a binary file into the game code, overwriting what's there + +def insert_binary_file(data_block, addr, filename): + file = open(filename, "rb") + file_size = os.path.getsize(filename) + insert_from = get_offset(addr) + insert_to = insert_from + file_size + data_block[insert_from:insert_to] = file.read() + file.close() + print("[ Modify ] insert file {} at 0x{:02X}".format(filename, addr)) + + +# Insert an array of bytes into the game code, overwriting what's there + +def insert_bytes(data_block, addr, insert): + insert_from = get_offset(addr) + insert_to = insert_from + len(insert) + data_block[insert_from:insert_to] = insert + print("[ Modify ] insert {} bytes at 0x{:02X}".format(len(insert), addr)) + + +# Insert a block of NOPs into the game code, overwriting what's there + +def insert_nops(data_block, addr, count): + insert = [0xEA] * count + insert_bytes(data_block, addr, insert) + print("[ Modify ] insert {} NOPs at 0x{:02X}".format(count, addr)) + + +# Fetch the platform (NTSC or PAL) from the command line arguments + +if len(sys.argv) >= 2: + platform = sys.argv[1] +else: + platform = "pal" + +# Print a progess message + +print() +print("Modifying Commodore 64 Elite") +print("Platform: {}".format(platform.upper())) + +# Configuration variables + +load_address = 0x6A00 - 2 +seed = 0x49 +scramble_from = 0x6A00 +scramble_to = 0x6A00 + 0x62D6 + +# Set up an array to hold the game binary, so we can modify it + +data_block = bytearray() + +# Load the main code file into data_block + +elite_file = open("gma6", "rb") +data_block.extend(elite_file.read()) +elite_file.close() + +print() +print("[ Read ] gma6") + +# Decrypt the main code file + +updated_seed = seed + +for n in range(scramble_to, scramble_from - 1, -1): + new = (data_block[n - load_address] - updated_seed) % 256 + data_block[n - load_address] = new + updated_seed = new + +print("[ Decrypt ] gma6") + +# Write an output file containing the decrypted but unmodified game code, which +# we can use for debugging + +output_file = open("gma6.decrypted", "wb") +output_file.write(data_block) +output_file.close() + +print("[ Save ] gma6.decrypted") + +# Set the addresses for the extra routines (LLX30, PATCH1, PATCH2) that we will +# append to the end of the main game code (where there is a bit of free space) + +llx30 = 0xCCE0 +patch1 = 0xCD1E +patch2 = 0xCD35 + +# We now modify the code to implement flicker-free ship drawing. The code +# changes are described here, which can be read alongside the following: +# +# https://www.bbcelite.com/deep_dives/backporting_the_flicker-free_algorithm.html +# +# The addresses in the following are from when the game binary is loaded into +# memory. They were calculated by analysing a memory dump of the running game, +# searching for patterns in the bytes to match them with the corrsponding code +# from the BBC Micro version (which is very similar, if you ignore any different +# addresses). + +# SHPPT +# +# We start with the new version of SHPPT, which we have already assembled in +# BeebAsm and saved as the binary file shppt.bin, so we simply drop this over +# the top of the existing routine (which is slightly longer, so there is room). + +insert_binary_file(data_block, 0x9932, "shppt.bin") + +# LL9 (Part 1) +# +# This is the modification just after LL9. We insert the extra code with a call +# to the new PATCH1 routine, which implements the original instructions before +# moving on to the new code. +# +# From: LDA #31 +# STA XX4 +# +# To: JSR PATCH1 +# NOP + +insert_bytes(data_block, 0x9A8A, [ + 0x20, patch1 % 256, patch1 // 256 # JSR PATCH1 +]) +insert_nops(data_block, 0x9A8D, 1) + +# LL9 (Part 9) +# +# This is the modification at EE31. +# +# From: LDA #%00001000 +# BIT XX1+31 +# BEQ LL74 +# JSR LL155 +# +# To: LDY #9 +# LDA (XX0),Y +# STA XX20 +# NOP +# NOP +# NOP + +insert_bytes(data_block, 0x9F2A, [ + 0xA0, 0x09, # LDY #9 + 0xB1, 0x57, # LDA (XX0),Y + 0x85, 0xAE # STA XX20 +]) +insert_nops(data_block, 0x9F30, 3) + +# LL9 (Part 9) +# +# This is the modification just after LL74. +# +# From: LDY #9 +# LDA (XX0),Y +# STA XX20 +# LDY #0 +# STY U +# STY XX17 +# INC U +# +# To: LDY #0 +# STY XX17 +# NOP x10 + +insert_bytes(data_block, 0x9F39, [ + 0xA0, 0x00, # LDY #0 + 0x84, 0x9F # STY XX17 +]) +insert_nops(data_block, 0x9F3D, 10) + +# LL9 (Part 9) +# +# This is the modification at the end of the routine. +# +# From: LDA XX15 +# STA (XX19),Y +# INY +# LDA XX15+1 +# STA (XX19),Y +# INY : +# LDA XX15+2 +# STA (XX19),Y +# INY +# LDA XX15+3 +# STA (XX19),Y +# INY +# STY U +# +# To: JSR LLX30 +# NOP x21 + +insert_bytes(data_block, 0x9F87, [ + 0x20, llx30 % 256, llx30 // 256 # JSR LLX30 +]) +insert_nops(data_block, 0x9F8A, 21) + +# LL9 (Part 10) +# +# This is the modification around LL75. +# +# From: STA T1 +# LDY XX17 +# +# To: STA CNT +# LDY #0 + +insert_bytes(data_block, 0x9FB4, [ + 0x85, 0x30, # STA CNT + 0xA0, 0x00 # LDY #0 +]) + +# LL9 (Part 10) +# +# This is the second INY after LL75. +# +# From: INY +# +# To: NOP + +insert_nops(data_block, 0x9FC1, 1) + +# LL9 (Part 10) +# +# This are the two modifications at LL79. +# +# From: LDA (V),Y +# TAX +# INY +# LDA (V),Y +# STA Q +# ... four lots of unchanged LDA/STA, 5 bytes each ... +# LDX Q +# +# To: INY +# LDA (V),Y +# TAX +# ... shuffle the LDA/STA block down by 4 bytes ... +# INY +# LDA (V),Y +# TAX +# NOP +# NOP + +insert_bytes(data_block, 0x9FD9, [ + 0xC8, # INY + 0xB1, 0x5B, # LDA (V),Y + 0xAA # TAX +]) + +lda_sta_block = get_offset(0x9FDD) +for n in range(lda_sta_block, lda_sta_block + 4 * 5): + data_block[n] = data_block[n + 4] + +insert_bytes(data_block, 0x9FF1, [ + 0xC8, # INY + 0xB1, 0x5B, # LDA (V),Y + 0xAA # TAX +]) +insert_nops(data_block, 0x9FF5, 2) + +# LL9 (Part 10) +# +# This is the modification at the end of the routine. The C64 version has an +# extra JMP LL80 instruction at this point that we can modify to jump to a +# new routine PATCH2, which lets us insert the extra JSR LLX30 without taking +# up any more bytes. +# +# From: JMP LL80 +# +# To: JMP PATCH2 + +insert_bytes(data_block, 0xA010, [ + 0x4C, patch2 % 256, patch2 // 256 # JMP PATCH2 +]) + +# LL9 (Part 11) +# +# This is the modification at LL80. +# +# We blank out the .LL80 section with 28 NOPs + +insert_nops(data_block, 0xA13F, 28) + +# LL9 (Part 11) +# +# We have already assembled the modified part 11 in BeebAsm and saved it as +# the binary file ll78.bin, so now we drop this over the top of the existing +# routine (which is exactly the same size). + +insert_binary_file(data_block, 0xA15B, "ll78.bin") + +# LL9 (Part 12) +# +# We have already assembled the modified part 11 in BeebAsm and saved it as +# the binary file ll115.bin, so now we drop this over the top of the existing +# routine (which is slightly longer, so there is room). + +insert_binary_file(data_block, 0xA178, "ll155.bin") + +# We now append the three extra routines required by the modifications to the +# end of the main binary (where there is enough free space for them): +# +# LLX30 +# PATCH1 +# PATCH2 +# +# We have already assembled these in BeebAsm and saved them as the binary file +# extra.bin, so we simply append this file to the end. + +elite_file = open("extra.bin", "rb") +data_block.extend(elite_file.read()) +elite_file.close() + +print("[ Modify ] append file extra.bin") + +# All the modifications are done, so write the output file for gma6.modified, +# which we can use for debugging + +output_file = open("gma6.modified", "wb") +output_file.write(data_block) +output_file.close() + +print("[ Save ] gma6.modified") + +# Encrypt the main code file + +for n in range(scramble_from, scramble_to): + data_block[n - load_address] = (data_block[n - load_address] + data_block[n + 1 - load_address]) % 256 + +data_block[scramble_to - load_address] = (data_block[scramble_to - load_address] + seed) % 256 + +print("[ Encrypt ] gma6.modified") + +# Write the output file for gma6.encrypted, which contains our modified game +# binary with the flicker-free code + +output_file = open("gma6.encrypted", "wb") +output_file.write(data_block) +output_file.close() + +print("[ Save ] gma6.encrypted") + +# Finally, we need to remove the disk protection from gma1, as described here: +# https://www.lemon64.com/forum/viewtopic.php?t=67762&start=90 + +data_block = bytearray() + +elite_file = open("gma1", "rb") +data_block.extend(elite_file.read()) +elite_file.close() + +print("[ Read ] gma1") + +if platform == "pal": + # For elite[firebird_1986](pal)(v040486).g64 + data_block[0x25] = 0xEA + data_block[0x26] = 0xEA + data_block[0x27] = 0xEA + data_block[0x2C] = 0xD0 +else: + # For elite[firebird_1986](ntsc)(v060186)(!).g64 + data_block[0x14] = 0xEA + data_block[0x16] = 0xEA + data_block[0x15] = 0xEA + +print("[ Modify ] gma1") + +output_file = open("gma1.modified", "wb") +output_file.write(data_block) +output_file.close() + +print("[ Save ] gma1.modified") +print()