#!/usr/bin/env python # # ****************************************************************************** # # COMMODORE PLUS/4 ELITE FLICKER-FREE MODIFICATION SCRIPT # # Written by Mark Moxon # # This script applies flicker-free ship-drawing to Commodore Plus/4 Elite, as # described here: # # https://elite.bbcelite.com/deep_dives/flicker-free_ship_drawing.html # # It does the following: # # * Modify the PRG file to draw flicker-free ships # # 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 following disk image, which is the # Pigmy Plus/4 version from Ian Bell's site, with the demo and packing removed: # # * elite_+4_unpacked.prg # # ****************************************************************************** from __future__ import print_function import os # 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)) # Print a progess message print() print("Modifying Commodore Plus/4 Elite") # Configuration variables load_address = 0x1100 - 2 # 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("elite_+4_unpacked.prg", "rb") data_block.extend(elite_file.read()) elite_file.close() print() print("[ Read ] elite_+4_unpacked.prg") # Set the addresses for the extra routines (LLX30, PATCH1, PATCH2) that we will # load into unused portiong of the main game code llx30 = 0x7200 patch1 = 0x723E patch2 = 0x7255 # 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://elite.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 + 0x900, "shppt-plus4.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 + 0x900, [ 0x20, patch1 % 256, patch1 // 256 # JSR PATCH1 ]) insert_nops(data_block, 0x9A8D + 0x900, 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 + 0x900, [ 0xA0, 0x09, # LDY #9 0xB1, 0x57, # LDA (XX0),Y 0x85, 0xAE # STA XX20 ]) insert_nops(data_block, 0x9F30 + 0x900, 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 + 0x900, [ 0xA0, 0x00, # LDY #0 0x84, 0x9F # STY XX17 ]) insert_nops(data_block, 0x9F3D + 0x900, 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 + 0x900, [ 0x20, llx30 % 256, llx30 // 256 # JSR LLX30 ]) insert_nops(data_block, 0x9F8A + 0x900, 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 + 0x900, [ 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 + 0x900, 1) # LL9 (Part 10) # # These 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 + 0x900, [ 0xC8, # INY 0xB1, 0x5B, # LDA (V),Y 0xAA # TAX ]) lda_sta_block = get_offset(0x9FDD + 0x900) for n in range(lda_sta_block, lda_sta_block + 4 * 5): data_block[n] = data_block[n + 4] insert_bytes(data_block, 0x9FF1 + 0x900, [ 0xC8, # INY 0xB1, 0x5B, # LDA (V),Y 0xAA # TAX ]) insert_nops(data_block, 0x9FF5 + 0x900, 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 + 0x900, [ 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 + 0x900, 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 + 0x900, "ll78-plus4.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 + 0x900, "ll155-plus4.bin") # We now load the three extra routines required by the modifications into the # memory used by the explosion and Trumble sprites, which are not used in the # Plus/4 version: # # LLX30 # PATCH1 # PATCH2 # DrawPlanetLine insert_binary_file(data_block, 0x7200, "extra-plus4.bin") # We now move on to the routines for drawing flicker-free planets # Set the addresses for the extra routines (EraseRestOfPlanet, PATCH4, PATCH5) # that we inserted above erasep = 0x89BC patch4 = 0x1EA0 patch5 = 0x1EA5 # PL9 (Part 1 of 3) # # We have already assembled the modified part 1 of PL9 in BeebAsm and saved # it as the binary file pl9.bin, so now we drop this over the top of the # existing routine (the new routine is slightly bigger, so it ends by jumping # to PATCH6, which contains the spill-over). insert_binary_file(data_block, 0x7D8C + 0x8F0, "pl9-plus4.bin") # PL9 (Part 2 of 3) # # The above modification moves PL20, so we need to modify the branch instruction # at the start of part 2 of PL9. insert_bytes(data_block, 0x7DA8 + 0x8F0, [ 0x90, 0xEB # BCC PL20 ]) # PL9 (Part 3 of 3) # # The above modification moves PL20, so we need to modify the branch instruction # at the start of part 3 of PL9. insert_bytes(data_block, 0x7DE2 + 0x8F0, [ 0x30, 0xB1 # BMI PL20 ]) # WPLS2 # # We have already assembled the modified WPLS2 in BeebAsm and saved it as # the binary file wpls2.bin, so now we drop this over the top of the # existing routine. This binary contains the following routines: # # WPLS2 # EraseRestOfPlanet # PATCH3 # PATCH6 insert_binary_file(data_block, 0x80BB + 0x8F0, "wpls2-plus4.bin") # PLS22 # # This is the modification on either side of PL40, with the label moving two # bytes backwards to accommodate the modified code. # # From: BCS PL40 # ... # STA CNT2 # JMP PLL4 # .PL40 # RTS # # To: BCS PL40 # ... # JMP PATCH4 # .PL40 # JMP EraseRestOfPlanet insert_bytes(data_block, 0x7F04 + 0x8F0, [ 0xB0, 0x0A # BCS PL40 ]) insert_bytes(data_block, 0x7F0D + 0x8F0, [ 0x4C, patch4 % 256, patch4 // 256, # JMP PATCH4 0x4C, erasep % 256, erasep // 256 # JMP EraseRestOfPlanet ]) # CIRCLE2 # # This is the modification at the start of the routine. # # From: LDX #&FF # STX FLAG # # To: JSR PATCH5 # NOP insert_bytes(data_block, 0x805E + 0x8F0, [ 0x20, patch5 % 256, patch5 // 256 # JSR PATCH5 ]) insert_nops(data_block, 0x8061 + 0x8F0, 1) # BLINE # # We have already assembled the modified BLINE in BeebAsm and saved it as the # binary file bline.bin, so now we drop this over the top of the existing # routine (the new routine is slightly bigger, so it ends by jumping to PATCH3, # which contains the spill-over). insert_binary_file(data_block, 0x2974, "bline-plus4.bin") # We now load the extra routines required by the modifications into the memory # used by the Trumble sprite-drawing routine, which contains a whole raft of # NOPs in the Plus/4 version. We can therefore slip the following routines into # this set of NOPs, adding a JMP beforehand to skip over the patches: # # DrawNewPlanetLine # PATCH4 # PATCH5 insert_binary_file(data_block, 0x1E6A, "trumble-plus4.bin") # All the modifications are done, so write the output file for the modified PRG output_file = open("elite_+4_modified.prg", "wb") output_file.write(data_block) output_file.close() print("[ Save ] elite_+4_modified.prg")