def __init__(self, romFileName=None, magic=None, plando=False): self.romFileName = romFileName self.race = None if romFileName == None: self.romFile = FakeROM() else: self.romFile = RealROM(romFileName) if magic is not None: from rom.race_mode import RaceModePatcher self.race = RaceModePatcher(self, magic, plando) # IPS_Patch objects list self.ipsPatches = [] # loc name to alternate address. we still write to original # address to help the RomReader. self.altLocsAddresses = {} # specific fixes for area rando connections self.roomConnectionSpecific = { # fix scrolling sky when transitioning to west ocean 0x93fe: self.patchWestOcean } self.doorConnectionSpecific = { # get out of kraid room: reload CRE 0x91ce: self.forceRoomCRE, # get out of croc room: reload CRE 0x93ea: self.forceRoomCRE }
def getBlockHeaderOffsets(nspcFileName): offsets = [] f = RealROM(nspcFileName) addr = 0 while True: offsets.append(addr) blkSize = f.readWord(addr) if blkSize == 0: break addr += 4 + blkSize # 4 is the size of the header itself f.close() return offsets
#!/usr/bin/python3 import sys, os, argparse from shutil import copyfile # we're in directory 'tools/' we have to update sys.path sys.path.append(os.path.dirname(sys.path[0])) from rom.rom import RealROM, snes_to_pc, pc_to_snes vanillaRom = RealROM(sys.argv[1]) # if last two tiles are used we also have to copy them to escape tiles escapeTilesAddr = snes_to_pc(0x94C800) vanillaRom.seek(escapeTilesAddr) # a 16 8x8 4bpp tiles row size rowSize = 32 * 16 for _ in range(32 * 4): vanillaRom.writeByte(0) lastRow8Addr = escapeTilesAddr + rowSize vanillaRom.seek(lastRow8Addr) for _ in range(32 * 4): vanillaRom.writeByte(0)
} ) # check that we don't have errors if response.status_code != 200: print("An error {} occured when calling the randomizer webservice. Error: {}".format(response.status_code, response.content)) exit(-1) # the output is a dict data = eval(response.text) # generate randomized rom romFileName = args.rom outFileName = data["fileName"] shutil.copyfile(romFileName, outFileName) romFile = RealROM(outFileName) ipsData = data["ips"] ipsData = base64.b64decode(ipsData.encode('ascii')) # our ips patcher need a file (or a dict), not the content of the ips file (fd, ipsFileName) = tempfile.mkstemp() os.close(fd) with open(ipsFileName, 'wb+') as ipsFile: ipsFile.write(ipsData) romFile.ipsPatch([IPS_Patch.load(ipsFileName)]) os.remove(ipsFileName)
#!/usr/bin/python3 import sys, os # we're in directory 'tools/' we have to update sys.path sys.path.append(os.path.dirname(sys.path[0])) from rom.ips import IPS_Patch from rom.rom import RealROM from patches.common import patches as common_patches vanilla = sys.argv[1] ipsToReverse = sys.argv[2:] rom = RealROM(vanilla) for ips in ipsToReverse: if ips not in common_patches.patches: ipsPath = os.path.abspath(ips) destDir, destFile = os.path.split(ipsPath) patch = IPS_Patch.load(ips) patchDict = patch.toDict() else: patchDict = common_patches.patches[ips] destDir = sys.path[0] + "/../patches/common/ips" destFile = ips + ".ips" destFile = "remove_" + destFile destPath = os.path.join(destDir, destFile) reversePatchDict = {} for addr, bytez in patchDict.items(): sz = len(bytez)
default=False) parser.add_argument('--no-mode7', '-m', help="no mode7 update", dest="no_mode7", action='store_true', default=False) parser.add_argument('--no-ship', '-s', help="no ship update", dest="no_ship", action='store_true', default=False) args = parser.parse_args() vanillaRom = RealROM(args.vanilla) baseImg = Image.open(args.ship_template) # extract ship shipBox = (128, 144, 240, 224) shipImg = baseImg.crop(shipBox) shipAlphaImg = shipImg.getchannel('A') shipGlowCustomImg = baseImg.crop((176, 120, 184, 128)) (r, g, b, a) = shipGlowCustomImg.getpixel((0, 0)) shipGlowCustomColor = [r, g, b] enableShipGlowCustom = sum(shipGlowCustomColor) > 0 shipGlowCustomMinImg = baseImg.crop((176, 128, 184, 136)) (r, g, b, a) = shipGlowCustomMinImg.getpixel((0, 0)) shipGlowCustomMinColor = [r, g, b] enableShipGlowMinCustom = sum(shipGlowCustomMinColor) > 0 #shipImg.show()
} needCopy = { "tilesAddr": False, "palettesAddr": False, "glowSpritemapsInstructionList": False, "spritemapsInstructionList": False, "instructionList": False, "spritemaps": False, "enemyHeader": False, "enemySet": False, #"enemyGfxName": False, "enemyGfx": False } hackRom = RealROM(hack) vanillaRom = RealROM(tmpfile) # level data are compressed, extract tiles in ship screen based on spritemaps landingSiteAddr = snes_to_pc(0x8F91F8) vLandingSite = Room(vanillaRom, landingSiteAddr) vLevelDataAddr = vLandingSite.defaultRoomState.levelDataPtr vRoomScreenSize = (vLandingSite.width, vLandingSite.height) hLandingSite = Room(hackRom, landingSiteAddr) hLevelDataAddr = hLandingSite.defaultRoomState.levelDataPtr hRoomScreenSize = (hLandingSite.width, hLandingSite.height) print("hack landing site addr: {} size: {}".format(hex(hLevelDataAddr), hRoomScreenSize))
layout="area" if len(sys.argv) > 3: layout=sys.argv[3] from graph.graph_utils import graphAreas as areas from rooms import rooms as rooms_area from rooms import rooms_alt rooms = rooms_area if layout == "alt": rooms = rooms_alt from rom.rom import pc_to_snes,RealROM rom = RealROM(vanilla) statesChecksArgSize = { 0xe5eb: 2, 0xe612: 1, 0xe629: 1 } with open(asm, "w") as src: src.write("lorom\narch snes.cpu\n\n") for room in rooms: # print(room["Name"]) def processState(stateWordAddr): src.write("org $8f%04x\n\tdb $%02x\n" % (stateWordAddr+16, areas.index(room['GraphArea']))) address = room['Address']+11 # process additionnal states
#!/usr/bin/python3 import sys, os # now that we're in directory 'tools/' we have to update sys.path sys.path.append(os.path.dirname(sys.path[0])) from rom.rom import RealROM, snes_to_pc romFileName = sys.argv[1] romFile = RealROM(romFileName) # compute tiles for upper and lower letters # lower chars are made of only one tile char2tileLower = { '0': 96, 'a': 106, '!': 132, '?': 133, '+': 134, '-': 135, '.': 136, ',': 137, '(': 138, ')': 139, ':': 140, ' ': 15 } # add remaining letters/numbers for i in range(1, ord('z') - ord('a') + 1): char2tileLower[chr(ord('a') + i)] = char2tileLower['a'] + i for i in range(1, ord('9') - ord('0') + 1):
def readRooms(romFileName): romFile = RealROM(romFileName) for roomInfo in rooms: romFile.seek(roomInfo['Address']) data = romFile.read(RoomHeader.Size) roomInfo['RoomIndex'] = data[RoomHeader.RoomIndex] roomInfo['Area'] = data[RoomHeader.Area] roomInfo['MapX'] = data[RoomHeader.MapX] roomInfo['MapY'] = data[RoomHeader.MapY] roomInfo['Width'] = data[RoomHeader.Width] roomInfo['Height'] = data[RoomHeader.Height] roomInfo['UpScroller'] = data[RoomHeader.UpScroller] roomInfo['DownScroller'] = data[RoomHeader.DownScroller] roomInfo['SpecialGfxBitflag'] = data[RoomHeader.SpecialGfxBitflag] roomInfo['DoorsPtr'] = snes_to_pc( concatBytes(data[RoomHeader.DoorsPtr1], data[RoomHeader.DoorsPtr2], 0x8F)) #print("") #print("{} ({}) ({} x {}) in area: {}".format(roomInfo['Name'], hex(roomInfo['Address']), hex(roomInfo['Width']), hex(roomInfo['Height']), Areas.id2name[roomInfo['Area']])) readDoorsPtrs(romFile, roomInfo) readDoorsData(romFile, roomInfo) roomsGraph = {} for roomInfo in rooms: nodeName = removeChars(roomInfo['Name'], "][ '-") address = roomInfo['Address'] & 0xFFFF roomsGraph[address] = { 'Name': nodeName, 'Area': roomInfo['Area'], 'Width': roomInfo['Width'], 'Height': roomInfo['Height'], 'Doors': {} } for doorData in roomInfo["DoorData"]: roomsGraph[address]['Doors'][doorData['doorPtr']] = { 'roomPtr': doorData['roomPtr'], 'exitScreenX': doorData['screenX'], 'exitScreenY': doorData['screenY'], 'exitDirection': doorData['direction'] } # get screen data from corresponding door for (entryRoomAddress, entryRoom) in roomsGraph.items(): for entryDoorData in entryRoom["Doors"].values(): exitRoomAddress = entryDoorData['roomPtr'] exitRoom = roomsGraph[exitRoomAddress] found = False for exitDoorData in exitRoom['Doors'].values(): #if entryRoom['Name'] in ['GrappleTutorialRoom1', 'GrappleBeamRoom']: #print("entry doors count: {} exit doors count: {}".format(len(entryRoom["Doors"]), len(exitRoom['Doors']))) #print("{}/{} -> {}/{} ({})".format(entryRoom['Name'], hex(entryRoomAddress), exitRoom['Name'], hex(exitDoorData['roomPtr']), roomsGraph[exitDoorData['roomPtr']]['Name'])) if exitDoorData['roomPtr'] == entryRoomAddress: #if entryRoom['Name'] in ['GrappleTutorialRoom1', 'GrappleBeamRoom']: #print("exitDoorData['roomPtr'] {} == entryRoomAddress {}".format(hex(exitDoorData['roomPtr']), hex(entryRoomAddress))) for entryDoorData in entryRoom['Doors'].values(): if entryDoorData['roomPtr'] == exitRoomAddress: entryDoorData['entryScreenX'] = exitDoorData[ 'exitScreenX'] entryDoorData['entryScreenY'] = exitDoorData[ 'exitScreenY'] entryDoorData['entryDirection'] = exitDoorData[ 'exitDirection'] found = True #else: #if entryRoom['Name'] in ['GrappleTutorialRoom1', 'GrappleBeamRoom']: #print("exitDoorData['roomPtr'] {} != entryRoomAddress {}".format(hex(exitDoorData['roomPtr']), hex(entryRoomAddress))) #if found == False: #print("door not found ({} -> {})".format(entryRoom['Name'], exitRoom['Name'])) #print("-----------------------------------------------------------------------------") #print(roomsGraph) print("""digraph { size="30,30!"; graph [overlap=orthoxy, splines=false, nodesep="1"]; node [shape="plaintext",fontsize=30]; edge [color="#0025fa80"]; """) for (address, roomInfo) in roomsGraph.items(): if roomInfo['Area'] == Areas.Tourian: src = roomInfo['Name'] print("{} [label = {}];".format( roomInfo['Name'], genLabel(roomInfo['Name'], roomInfo["Width"], roomInfo["Height"]))) for doorData in roomInfo["Doors"].values(): dstInfo = roomsGraph[doorData['roomPtr']] dst = dstInfo['Name'] print("{}:x{}{}:{} -> {}:x{}{}:{};".format( src, doorData.get('entryScreenX'), doorData.get('entryScreenY'), getDir(doorData.get('entryDirection')), dst, doorData.get('exitScreenX'), doorData.get('exitScreenY'), getDir(doorData.get('exitDirection')))) print("}")
for i in range(1, ord('9') - ord('0') + 1): char2tile[chr(ord('0') + i)] = char2tile['0'] + i lineLength = 64 firstChar = 2 * 2 baseAddr = 0xB6F200 + lineLength * 8 + firstChar texts = ["1. {} kraid", "2. {} phantoon", "3. {} draygon", "4. {} ridley"] kill_synonyms = [ "massacre", "slaughter", "slay", "wipe out", "annihilate", "eradicate", "erase", "exterminate", "finish", "neutralize", "obliterate", "destroy", "wreck", "smash", "crush", "end", "eliminate", "terminate" ] romFile = RealROM(sys.argv[1]) alreadyUsed = [] for i, text in enumerate(texts): verb = random.choice(kill_synonyms) while verb in alreadyUsed: verb = random.choice(kill_synonyms) alreadyUsed.append(verb) text = text.format(verb) print(text) addr = baseAddr + i * lineLength * 4 romFile.seek(snes_to_pc(addr)) for c in text: romFile.writeWord(0x3800 + char2tile[c]) romFile.close()
Range(0x5828, 0x6818, " Song-specific tracker data", True), Range(0x6819, 0x6BFF, "Unused", allowUnused), Range(0x6C00, 0x6CE9, "Instrument table", True), Range(0x6C00, 0x6C8F, " Shared instruments (0..17h)", False), Range(0x6C90, 0x6CE9, " Song-specific instruments (18h..26h)", True), Range(0x6CEA, 0x6CFF, "Unused", allowUnused), Range(0x6D00, 0x6D9F, "Sample table", True), Range(0x6D00, 0x6D5F, " Shared sample table", False), Range(0x6D60, 0x6D9F, " Song-specific sample table", True), Range(0x6DA0, 0x6DFF, "Unused", allowUnused), Range(0x6E00, 0xFFFF, "Sample data", True), Range(0x6E00, 0xB20F, " Shared sample data", False), Range(0xB210, 0xFFFF, " Song-specific sample data", True) ] rom = RealROM(sys.argv[1]) dataBlocks = [] addr = 0 while size - addr > 4: dataBlock = DataBlock(addr, rom) if dataBlock.size == 0: break print("datablock addr: {} size: {} dest: {}".format(dataBlock.addr, dataBlock.size, hex(dataBlock.dest))) dataBlocks.append(dataBlock) addr = dataBlock.getNextBlockAddr() #print("next addr: {}".format(addr)) print("found {} data blocks".format(len(dataBlocks))) print("last addr: {} last bytes: {}".format(addr, data[addr:]))
class RomPatcher: # standard: # Instantly open G4 passage when all bosses are killed # g4_skip.ips # Wake up zebes when going right from morph # wake_zebes.ips # Seed display # seed_display.ips # Custom credits with stats # credits.ips # Custom credits with stats (tracking code) # tracking.ips # Removes Gravity Suit heat protection # Mother Brain Cutscene Edits # Suit acquisition animation skip # Fix Morph & Missiles Room State # Fix heat damage speed echoes bug # Disable GT Code # Disable Space/Time select in menu # Fix Morph Ball Hidden/Chozo PLM's # Fix Screw Attack selection in menu # # optional (Kejardon): # Allows the aim buttons to be assigned to any button # AimAnyButton.ips # # optional (Scyzer): # Remove fanfare when picking up an item # itemsounds.ips # Allows Samus to start spinning in mid air after jumping or falling # spinjumprestart.ips # # optional standard (imcompatible with MSU1 music): # Max Ammo Display # max_ammo_display.ips # # optional (DarkShock): # Play music with MSU1 chip on SD2SNES # supermetroid_msu1.ips # # layout: # Disable respawning blocks at dachora pit # dachora.ips # Make it possible to escape from below early super bridge without bombs # early_super_bridge.ips # Replace bomb blocks with shot blocks before Hi-Jump # high_jump.ips # Replace bomb blocks with shot blocks at Moat # moat.ips # Raise platform in first heated norfair room to not require hi-jump # nova_boost_platform.ip # Raise platforms in red tower bottom to always be able to get back up # red_tower.ips # Replace bomb blocks with shot blocks before Spazer # spazer.ips IPSPatches = { 'Standard': ['new_game.ips', 'plm_spawn.ips', 'load_enemies_fix.ips', 'credits_varia.ips', 'seed_display.ips', 'tracking.ips', 'wake_zebes.ips', 'g4_skip.ips', # XXX those are door ASMs 'Mother_Brain_Cutscene_Edits', "Allow_All_Saves", 'Suit_acquisition_animation_skip', 'Fix_heat_damage_speed_echoes_bug', 'Disable_GT_Code', 'Disable_Space_Time_select_in_menu', 'Fix_Morph_Ball_Hidden_Chozo_PLMs', 'Fix_Screw_Attack_selection_in_menu', 'fix_suits_selection_in_menu.ips', 'Removes_Gravity_Suit_heat_protection', 'AimAnyButton.ips', 'endingtotals.ips', 'supermetroid_msu1.ips', 'max_ammo_display.ips', 'varia_logo.ips'], 'VariaTweaks' : ['WS_Etank', 'LN_Chozo_SpaceJump_Check_Disable', 'ln_chozo_platform.ips', 'bomb_torizo.ips'], 'Layout': ['dachora.ips', 'early_super_bridge.ips', 'high_jump.ips', 'moat.ips', 'spospo_save.ips', 'nova_boost_platform.ips', 'red_tower.ips', 'spazer.ips', 'brinstar_map_room.ips', 'kraid_save.ips', 'mission_impossible.ips'], 'Optional': ['itemsounds.ips', 'rando_speed.ips', 'Infinite_Space_Jump', 'refill_before_save.ips', 'spinjumprestart.ips', 'elevators_doors_speed.ips', 'No_Music', 'random_music.ips', 'skip_intro.ips', 'skip_ceres.ips', 'animal_enemies.ips', 'animals.ips', 'draygonimals.ips', 'escapimals.ips', 'gameend.ips', 'grey_door_animals.ips', 'low_timer.ips', 'metalimals.ips', 'phantoonimals.ips', 'ridleyimals.ips', 'remove_elevators_doors_speed.ips', 'remove_itemsounds.ips', 'beam_doors.ips'], 'Area': ['area_rando_layout.ips', 'door_transition.ips', 'area_rando_doors.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'area_rando_warp_door.ips', 'crab_shaft.ips', 'Save_Crab_Shaft', 'Save_Main_Street' ], 'Escape' : ['rando_escape.ips', 'rando_escape_ws_fix.ips'], 'MinimizerTourian': ['minimizer_tourian.ips', 'nerfed_rainbow_beam.ips'] } def __init__(self, romFileName=None, magic=None, plando=False): self.romFileName = romFileName self.race = None if romFileName == None: self.romFile = FakeROM() else: self.romFile = RealROM(romFileName) if magic is not None: from rom.race_mode import RaceModePatcher self.race = RaceModePatcher(self, magic, plando) # IPS_Patch objects list self.ipsPatches = [] # loc name to alternate address. we still write to original # address to help the RomReader. self.altLocsAddresses = {} # specific fixes for area rando connections self.roomConnectionSpecific = { # fix scrolling sky when transitioning to west ocean 0x93fe: self.patchWestOcean } self.doorConnectionSpecific = { # get out of kraid room: reload CRE 0x91ce: self.forceRoomCRE, # get out of croc room: reload CRE 0x93ea: self.forceRoomCRE } def end(self): self.romFile.close() def writeItemCode(self, item, visibility, address): itemCode = ItemManager.getItemTypeCode(item, visibility) if self.race is None: self.romFile.writeWord(itemCode, address) else: self.race.writeItemCode(itemCode, address) def getLocAddresses(self, loc): ret = [loc.Address] if loc.Name in self.altLocsAddresses: ret.append(self.altLocsAddresses[loc.Name]) return ret def writeNothing(self, itemLoc): loc = itemLoc.Location if loc.isBoss(): return for addr in self.getLocAddresses(loc): self.writeItemCode(ItemManager.Items['Missile'], loc.Visibility, addr) # all Nothing not at this loc Id will disappear when loc # item is collected self.romFile.writeByte(self.nothingId, addr + 4) def writeItem(self, itemLoc): loc = itemLoc.Location if loc.isBoss(): raise ValueError('Cannot write Boss location') #print('write ' + itemLoc.Item.Type + ' at ' + loc.Name) for addr in self.getLocAddresses(loc): self.writeItemCode(itemLoc.Item, loc.Visibility, addr) # if nothing was written at this loc before (in plando), # then restore the vanilla value self.romFile.writeByte(loc.Id, addr + 4) def writeItemsLocs(self, itemLocs): self.nItems = 0 self.nothingMissile = False for itemLoc in itemLocs: loc = itemLoc.Location item = itemLoc.Item if loc.isBoss(): continue isMorph = loc.Name == 'Morphing Ball' if item.Category == 'Nothing': self.writeNothing(itemLoc) if loc.Id == self.nothingId and not loc.restricted and itemLoc.Accessible: # nothing at morph gives a missile pack self.nothingMissile = True self.nItems += 1 else: self.nItems += 1 self.writeItem(itemLoc) if isMorph: self.patchMorphBallEye(itemLoc.Item) # trigger morph eye enemy on whatever item we put there, # not just morph ball def patchMorphBallEye(self, item): # print('Eye item = ' + item.Type) # consider Nothing as missile, because if it is at morph ball it will actually be a missile if item.Category == 'Nothing': if self.nothingId == 0x1a: isNothingMissile = True else: return else: isNothingMissile = False isAmmo = item.Category == 'Ammo' or isNothingMissile isMissile = item.Type == 'Missile' or isNothingMissile # category to check if ItemManager.isBeam(item): cat = 0xA8 # collected beams elif item.Type == 'ETank': cat = 0xC4 # max health elif item.Type == 'Reserve': cat = 0xD4 # max reserves elif isMissile: cat = 0xC8 # max missiles elif item.Type == 'Super': cat = 0xCC # max supers elif item.Type == 'PowerBomb': cat = 0xD0 # max PBs else: cat = 0xA4 # collected items # comparison/branch instruction # the branch is taken if we did NOT collect item yet if item.Category == 'Energy' or isAmmo: comp = 0xC9 # CMP (immediate) branch = 0x30 # BMI else: comp = 0x89 # BIT (immediate) branch = 0xF0 # BEQ # what to compare to if item.Type == 'ETank': operand = 0x65 # < 100 elif item.Type == 'Reserve' or isAmmo: operand = 0x1 # < 1 elif ItemManager.isBeam(item): operand = ItemManager.BeamBits[item.Type] else: operand = ItemManager.ItemBits[item.Type] self.patchMorphBallCheck(0x1410E6, cat, comp, operand, branch) # eye main AI self.patchMorphBallCheck(0x1468B2, cat, comp, operand, branch) # head main AI def patchMorphBallCheck(self, offset, cat, comp, operand, branch): # actually patch enemy AI self.romFile.writeByte(cat, offset) self.romFile.writeByte(comp, offset+2) self.romFile.writeWord(operand) self.romFile.writeByte(branch) def writeItemsNumber(self): # write total number of actual items for item percentage patch (patch the patch) for addr in [0x5E64E, 0x5E6AB]: self.romFile.writeByte(self.nItems, addr) def addIPSPatches(self, patches): for patchName in patches: self.applyIPSPatch(patchName) def customShip(self, ship): self.applyIPSPatch(ship, ipsDir='rando/patches/ships') def customSprite(self, sprite, customNames): self.applyIPSPatch(sprite, ipsDir='rando/patches/sprites') if not customNames: return # custom sprite message boxes update messageBoxes = { 'marga.ips': { 'Morph': 'morphing doll', 'SpringBall': 'spring doll', }, 'super_controid.ips': { 'PowerBomb': 'm-80,000 helio bomb' }, 'alucard.ips': { 'HiJump': 'gravity boots', 'SpeedBooster': 'god speed shoes', 'Morph': 'soul of bat', 'XRayScope': 'holy glasses', 'Varia': 'fire mail', 'Gravity': 'holy symbol', 'ETank': 'life vessel', 'Reserve': 'heart vessel' }, 'samus_backwards.ips': { 'ETank': 'ygrene knat', 'Missile': 'elissim', 'Super': 'repus elissim', 'PowerBomb': 'rewop bmob', 'Grapple': 'gnilpparg maeb', 'XRayScope': 'yar-x epocs', 'Varia': 'airav uius', 'SpringBall': 'gnirps llab', 'Morph': 'gnihprom llab', 'ScrewAttack': 'wercs kcatta', 'HiJump': 'pmuj-ih stoob', 'SpaceJump': 'ecaps pmuj', 'SpeedBooster': 'deeps retsoob', 'Charge': 'egrahc maeb', 'Ice': 'eci maeb', 'Wave': 'evaw maeb', 'Spazer': 'rezaps', 'Plasma': 'amsalp maeb', 'Bomb': 'bmob', 'Reserve': 'evreser knat', 'Gravity': 'ytivarg tius' }, 'vanilla': { 'ETank': 'energy tank', 'Missile': 'misille', 'Super': 'super missile', 'PowerBomb': 'power bomb', 'Grapple': 'grappling beam', 'XRayScope': 'x-ray scope', 'Varia': 'varia suit', 'SpringBall': 'spring ball', 'Morph': 'morphing ball', 'ScrewAttack': 'screw attack', 'HiJump': 'hi-jump boots', 'SpaceJump': 'space jump', 'SpeedBooster': 'speed booster', 'Charge': 'charge beam', 'Ice': 'ice beam', 'Wave': 'wave beam', 'Spazer': 'spazer', 'Plasma': 'plasma beam', 'Bomb': 'bomb', 'Reserve': 'server tank', 'Gravity': 'gravity suit' } } messageBoxes['samus_upside_down_and_backwards.ips'] = messageBoxes['samus_backwards.ips'] vFlip = ['samus_upside_down.ips', 'samus_upside_down_and_backwards.ips'] hFlip = ['samus_backwards.ips'] doVFlip = sprite in vFlip doHFlip = sprite in hFlip if (doVFlip or doHFlip) and sprite not in messageBoxes: sprite = 'vanilla' if sprite in messageBoxes: messageBox = MessageBox(self.romFile) for (messageKey, newMessage) in messageBoxes[sprite].items(): messageBox.updateMessage(messageKey, newMessage, doVFlip, doHFlip) def writePlmTable(self, plms, area, bosses, startAP): # called when saving a plando try: if bosses == True or area == True: plms.append('WS_Save_Blinking_Door') doors = self.getStartDoors(plms, area, None) self.writeDoorsColor(doors) self.applyStartAP(startAP, plms, doors) self.applyPLMs(plms) except Exception as e: raise Exception("Error patching {}. ({})".format(self.romFileName, e)) def applyIPSPatches(self, startAP="Landing Site", optionalPatches=[], noLayout=False, suitsMode="Classic", area=False, bosses=False, areaLayoutBase=False, noVariaTweaks=False, nerfedCharge=False, nerfedRainbowBeam=False, escapeAttr=None, noRemoveEscapeEnemies=False, minimizerN=None, minimizerTourian=True, doorsColorsRando=False): try: # apply standard patches stdPatches = [] plms = [] # apply race mode first because it fills the rom with a bunch of crap if self.race is not None: stdPatches.append('race_mode.ips') stdPatches += RomPatcher.IPSPatches['Standard'][:] if self.race is not None: stdPatches.append('race_mode_credits.ips') if suitsMode != "Classic": stdPatches.remove('Removes_Gravity_Suit_heat_protection') if suitsMode == "Progressive": stdPatches.append('progressive_suits.ips') if nerfedCharge == True: stdPatches.append('nerfed_charge.ips') if nerfedRainbowBeam == True: stdPatches.append('nerfed_rainbow_beam.ips') if bosses == True or area == True: stdPatches += ["WS_Main_Open_Grey", "WS_Save_Active"] plms.append('WS_Save_Blinking_Door') if bosses == True: stdPatches.append("Phantoon_Eye_Door") for patchName in stdPatches: self.applyIPSPatch(patchName) if noLayout == False: # apply layout patches for patchName in RomPatcher.IPSPatches['Layout']: self.applyIPSPatch(patchName) if noVariaTweaks == False: # VARIA tweaks for patchName in RomPatcher.IPSPatches['VariaTweaks']: self.applyIPSPatch(patchName) # apply optional patches for patchName in optionalPatches: if patchName in RomPatcher.IPSPatches['Optional']: self.applyIPSPatch(patchName) # random escape if escapeAttr is not None: if noRemoveEscapeEnemies == True: RomPatcher.IPSPatches['Escape'].append('Escape_Rando_Enable_Enemies') for patchName in RomPatcher.IPSPatches['Escape']: self.applyIPSPatch(patchName) # handle incompatible doors transitions if area == False and bosses == False: self.applyIPSPatch('door_transition.ips') # animals and timer self.applyEscapeAttributes(escapeAttr, plms) # apply area patches if area == True: if areaLayoutBase == True: for p in ['area_rando_layout.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips']: RomPatcher.IPSPatches['Area'].remove(p) RomPatcher.IPSPatches['Area'].append('area_rando_layout_base.ips') for patchName in RomPatcher.IPSPatches['Area']: self.applyIPSPatch(patchName) elif bosses == True: self.applyIPSPatch('door_transition.ips') if minimizerN is not None: self.applyIPSPatch('minimizer_bosses.ips') if minimizerTourian == True: for patchName in RomPatcher.IPSPatches['MinimizerTourian']: self.applyIPSPatch(patchName) doors = self.getStartDoors(plms, area, minimizerN) if doorsColorsRando: self.writeDoorsColor(doors) self.applyStartAP(startAP, plms, doors) self.applyPLMs(plms) except Exception as e: raise Exception("Error patching {}. ({})".format(self.romFileName, e)) def applyIPSPatch(self, patchName, patchDict=None, ipsDir="rando/patches"): if patchDict is None: patchDict = patches print("Apply patch {}".format(patchName)) if patchName in patchDict: patch = IPS_Patch(patchDict[patchName]) else: # look for ips file if os.path.exists(patchName): patch = IPS_Patch.load(patchName) else: patch = IPS_Patch.load(os.path.join(appDir, ipsDir, patchName)) self.ipsPatches.append(patch) def getStartDoors(self, plms, area, minimizerN): doors = [0x10] # red brin elevator def addBlinking(name): key = 'Blinking[{}]'.format(name) if key in patches: self.applyIPSPatch(key) if key in additional_PLMs: plms.append(key) if area == True: plms += ['Maridia Sand Hall Seal', "Save_Main_Street", "Save_Crab_Shaft"] for accessPoint in accessPoints: if accessPoint.Internal == True or accessPoint.Boss == True: continue addBlinking(accessPoint.Name) addBlinking("West Sand Hall Left") addBlinking("Below Botwoon Energy Tank Right") if minimizerN is not None: # add blinking doors inside and outside boss rooms for accessPoint in accessPoints: if accessPoint.Boss == True: addBlinking(accessPoint.Name) return doors def applyStartAP(self, apName, plms, doors): ap = getAccessPoint(apName) if not GraphUtils.isStandardStart(apName): # not Ceres or Landing Site, so Zebes will be awake plms.append('Morph_Zebes_Awake') (w0, w1) = getWord(ap.Start['spawn']) if 'doors' in ap.Start: doors += ap.Start['doors'] doors.append(0x0) addr = 0x10F200 patch = [w0, w1] + doors assert (addr + len(patch)) < 0x10F210, "Stopped before new_game overwrite" patchDict = { 'StartAP': { addr: patch }, } self.applyIPSPatch('StartAP', patchDict) # handle custom saves if 'save' in ap.Start: self.applyIPSPatch(ap.Start['save']) plms.append(ap.Start['save']) # handle optional rom patches if 'rom_patches' in ap.Start: for patch in ap.Start['rom_patches']: self.applyIPSPatch(patch) def applyEscapeAttributes(self, escapeAttr, plms): # timer escapeTimer = escapeAttr['Timer'] if escapeTimer is not None: minute = int(escapeTimer / 60) second = escapeTimer % 60 minute = int(minute / 10) * 16 + minute % 10 second = int(second / 10) * 16 + second % 10 patchDict = {'Escape_Timer': {0x1E21:[second, minute]}} self.applyIPSPatch('Escape_Timer', patchDict) # animals door to open if escapeAttr['Animals'] is not None: escapeOpenPatches = { 'Green Brinstar Main Shaft Top Left':'Escape_Animals_Open_Brinstar', 'Business Center Mid Left':"Escape_Animals_Open_Norfair", 'Crab Hole Bottom Right':"Escape_Animals_Open_Maridia", } if escapeAttr['Animals'] in escapeOpenPatches: plms.append("WS_Map_Grey_Door") self.applyIPSPatch(escapeOpenPatches[escapeAttr['Animals']]) else: plms.append("WS_Map_Grey_Door_Openable") else: plms.append("WS_Map_Grey_Door") # adds ad-hoc "IPS patches" for additional PLM tables def applyPLMs(self, plms): # compose a dict (room, state, door) => PLM array # 'PLMs' being a 6 byte arrays plmDict = {} # we might need to update locations addresses on the fly plmLocs = {} # room key above => loc name for p in plms: plm = additional_PLMs[p] room = plm['room'] state = 0 if 'state' in plm: state = plm['state'] door = 0 if 'door' in plm: door = plm['door'] k = (room, state, door) if k not in plmDict: plmDict[k] = [] plmDict[k] += plm['plm_bytes_list'] if 'locations' in plm: locList = plm['locations'] for locName, locIndex in locList: plmLocs[(k, locIndex)] = locName # make two patches out of this dict plmTblAddr = 0x7E9A0 # moves downwards plmPatchData = [] roomTblAddr = 0x7EC00 # moves upwards roomPatchData = [] plmTblOffset = plmTblAddr def appendPlmBytes(bytez): nonlocal plmPatchData, plmTblOffset plmPatchData += bytez plmTblOffset += len(bytez) def addRoomPatchData(bytez): nonlocal roomPatchData, roomTblAddr roomPatchData = bytez + roomPatchData roomTblAddr -= len(bytez) for roomKey, plmList in plmDict.items(): entryAddr = plmTblOffset roomData = [] for i in range(len(plmList)): plmBytes = plmList[i] assert len(plmBytes) == 6, "Invalid PLM entry for roomKey " + str(roomKey) + ": PLM list len is " + str(len(plmBytes)) if (roomKey, i) in plmLocs: self.altLocsAddresses[plmLocs[(roomKey, i)]] = plmTblOffset appendPlmBytes(plmBytes) appendPlmBytes([0x0, 0x0]) # list terminator def appendRoomWord(w, data): (w0, w1) = getWord(w) data += [w0, w1] for i in range(3): appendRoomWord(roomKey[i], roomData) appendRoomWord(entryAddr, roomData) addRoomPatchData(roomData) # write room table terminator addRoomPatchData([0x0] * 8) assert plmTblOffset < roomTblAddr, "Spawn PLM table overlap" patchDict = { "PLM_Spawn_Tables" : { plmTblAddr: plmPatchData, roomTblAddr: roomPatchData } } self.applyIPSPatch("PLM_Spawn_Tables", patchDict) def commitIPS(self): self.romFile.ipsPatch(self.ipsPatches) def writeSeed(self, seed): random.seed(seed) seedInfo = random.randint(0, 0xFFFF) seedInfo2 = random.randint(0, 0xFFFF) self.romFile.writeWord(seedInfo, 0x2FFF00) self.romFile.writeWord(seedInfo2) def writeMagic(self): if self.race is not None: self.race.writeMagic() def writeMajorsSplit(self, majorsSplit): address = 0x17B6C if majorsSplit == 'Chozo': char = 'Z' elif majorsSplit == 'Full': char = 'F' else: char = 'M' self.romFile.writeByte(ord(char), address) def setNothingId(self, startAP, itemLocs): # morph ball loc by default self.nothingId = 0x1a # if not default start, use first loc with a nothing if not GraphUtils.isStandardStart(startAP): firstNothing = next((il.Location for il in itemLocs if il.Item.Category == 'Nothing' and 'Boss' not in il.Location.Class), None) if firstNothing is not None: self.nothingId = firstNothing.Id def writeNothingId(self): address = 0x17B6D self.romFile.writeByte(self.nothingId, address) def getItemQty(self, itemLocs, itemType): q = len([il for il in itemLocs if il.Accessible and il.Item.Type == itemType]) if itemType == 'Missile' and self.nothingMissile == True: q += 1 return q def getMinorsDistribution(self, itemLocs): dist = {} minQty = 100 minors = ['Missile', 'Super', 'PowerBomb'] for m in minors: # in vcr mode if the seed has stuck we may not have these items, return at least 1 q = float(max(self.getItemQty(itemLocs, m), 1)) dist[m] = {'Quantity' : q } if q < minQty: minQty = q for m in minors: dist[m]['Proportion'] = dist[m]['Quantity']/minQty return dist def getAmmoPct(self, minorsDist): q = 0 for m,v in minorsDist.items(): q += v['Quantity'] return 100*q/66 def writeRandoSettings(self, settings, itemLocs): dist = self.getMinorsDistribution(itemLocs) totalAmmo = sum(d['Quantity'] for ammo,d in dist.items()) totalItemLocs = sum(1 for il in itemLocs if il.Accessible and not il.Location.isBoss()) totalNothing = sum(1 for il in itemLocs if il.Accessible and il.Item.Category == 'Nothing') if self.nothingMissile == True: totalNothing -= 1 totalEnergy = self.getItemQty(itemLocs, 'ETank')+self.getItemQty(itemLocs, 'Reserve') totalMajors = max(totalItemLocs - totalEnergy - totalAmmo - totalNothing, 0) address = 0x2736C0 value = "{:>2}".format(totalItemLocs) line = " ITEM LOCATIONS %s " % value self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " item locations ............ %s " % value self.writeCreditsStringBig(address, line, top=False) address += 0x40 maj = "{:>2}".format(int(totalMajors)) htanks = "{:>2}".format(int(totalEnergy)) ammo = "{:>2}".format(int(totalAmmo)) blank = "{:>2}".format(int(totalNothing)) line = " MAJ %s EN %s AMMO %s BLANK %s " % (maj, htanks, ammo, blank) self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " maj %s en %s ammo %s blank %s " % (maj, htanks, ammo, blank) self.writeCreditsStringBig(address, line, top=False) address += 0x40 pbs = "{:>2}".format(int(dist['PowerBomb']['Quantity'])) miss = "{:>2}".format(int(dist['Missile']['Quantity'])) supers = "{:>2}".format(int(dist['Super']['Quantity'])) line = " AMMO PACKS MI %s SUP %s PB %s " % (miss, supers, pbs) self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " ammo packs mi %s sup %s pb %s " % (miss, supers, pbs) self.writeCreditsStringBig(address, line, top=False) address += 0x40 etanks = "{:>2}".format(int(self.getItemQty(itemLocs, 'ETank'))) reserves = "{:>2}".format(int(self.getItemQty(itemLocs, 'Reserve'))) line = " HEALTH TANKS E %s R %s " % (etanks, reserves) self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " health tanks ...... e %s r %s " % (etanks, reserves) self.writeCreditsStringBig(address, line, top=False) address += 0x80 value = " "+settings.progSpeed.upper() line = " PROGRESSION SPEED ....%s " % value.rjust(8, '.') self.writeCreditsString(address, 0x04, line) address += 0x40 line = " PROGRESSION DIFFICULTY %s " % settings.progDiff.upper() self.writeCreditsString(address, 0x04, line) address += 0x80 # skip item distrib title param = (' SUITS RESTRICTION ........%s', 'Suits') line = param[0] % ('. ON' if settings.restrictions[param[1]] == True else ' OFF') self.writeCreditsString(address, 0x04, line) address += 0x40 value = " "+settings.restrictions['Morph'].upper() line = " MORPH PLACEMENT .....%s" % value.rjust(9, '.') self.writeCreditsString(address, 0x04, line) address += 0x40 for superFun in [(' SUPER FUN COMBAT .........%s', 'Combat'), (' SUPER FUN MOVEMENT .......%s', 'Movement'), (' SUPER FUN SUITS ..........%s', 'Suits')]: line = superFun[0] % ('. ON' if superFun[1] in settings.superFun else ' OFF') self.writeCreditsString(address, 0x04, line) address += 0x40 value = "%.1f %.1f %.1f" % (dist['Missile']['Proportion'], dist['Super']['Proportion'], dist['PowerBomb']['Proportion']) line = " AMMO DISTRIBUTION %s " % value self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " ammo distribution %s " % value self.writeCreditsStringBig(address, line, top=False) address += 0x40 # write ammo/energy pct address = 0x273C40 (ammoPct, energyPct) = (int(self.getAmmoPct(dist)), int(100*totalEnergy/18)) line = " AVAILABLE AMMO {:>3}% ENERGY {:>3}%".format(ammoPct, energyPct) self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " available ammo {:>3}% energy {:>3}%".format(ammoPct, energyPct) self.writeCreditsStringBig(address, line, top=False) def writeSpoiler(self, itemLocs, progItemLocs=None): # keep only majors, filter out Etanks and Reserve fItemLocs = [il for il in itemLocs if il.Item.Category not in ['Ammo', 'Nothing', 'Energy', 'Boss']] # add location of the first instance of each minor for t in ['Missile', 'Super', 'PowerBomb']: itLoc = None if progItemLocs is not None: itLoc = next((il for il in progItemLocs if il.Item.Type == t), None) if itLoc is None: itLoc = next((il for il in itemLocs if il.Item.Type == t), None) if itLoc is not None: # in vcr mode if the seed has stucked we may not have these minors fItemLocs.append(itLoc) regex = re.compile(r"[^A-Z0-9\.,'!: ]+") itemLocs = {} for iL in fItemLocs: itemLocs[iL.Item.Name] = iL.Location.Name def prepareString(s, isItem=True): s = s.upper() # remove chars not displayable s = regex.sub('', s) # remove space before and after s = s.strip() # limit to 30 chars, add one space before # pad to 32 chars if isItem is True: s = " " + s[0:30] s = s.ljust(32) else: s = " " + s[0:30] + " " s = " " + s.rjust(31, '.') return s isRace = self.race is not None startCreditAddress = 0x2f5240 address = startCreditAddress if isRace: addr = address - 0x40 data = [0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x1008, 0x1013, 0x1004, 0x100c, 0x007f, 0x100b, 0x100e, 0x1002, 0x1000, 0x1013, 0x1008, 0x100e, 0x100d, 0x1012, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f] for i in range(0x20): w = data[i] self.romFile.seek(addr) self.race.writeWordMagic(w) addr += 0x2 # standard item order items = ["Missile", "Super Missile", "Power Bomb", "Charge Beam", "Ice Beam", "Wave Beam", "Spazer", "Plasma Beam", "Varia Suit", "Gravity Suit", "Morph Ball", "Bomb", "Spring Ball", "Screw Attack", "Hi-Jump Boots", "Space Jump", "Speed Booster", "Grappling Beam", "X-Ray Scope"] displayNames = {} if progItemLocs is not None: # reorder it with progression indices prog = ord('A') idx = 0 progNames = [il.Item.Name for il in progItemLocs if il.Item.Category != 'Boss'] for i in range(len(progNames)): item = progNames[i] if item in items and item not in displayNames: items.remove(item) items.insert(idx, item) displayNames[item] = chr(prog + i) + ": " + item idx += 1 for item in items: # super fun removes items if item not in itemLocs: continue display = item if item in displayNames: display = displayNames[item] itemName = prepareString(display) locationName = prepareString(itemLocs[item], isItem=False) self.writeCreditsString(address, 0x04, itemName, isRace) self.writeCreditsString((address + 0x40), 0x18, locationName, isRace) address += 0x80 # we need 19 items displayed, if we've removed majors, add some blank text while address < startCreditAddress + len(items)*0x80: self.writeCreditsString(address, 0x04, prepareString(""), isRace) self.writeCreditsString((address + 0x40), 0x18, prepareString(""), isRace) address += 0x80 self.patchBytes(address, [0, 0, 0, 0], isRace) def writeCreditsString(self, address, color, string, isRace=False): array = [self.convertCreditsChar(color, char) for char in string] self.patchBytes(address, array, isRace) def writeCreditsStringBig(self, address, string, top=True): array = [self.convertCreditsCharBig(char, top) for char in string] self.patchBytes(address, array) def convertCreditsChar(self, color, byte): if byte == ' ': ib = 0x7f elif byte == '!': ib = 0x1F elif byte == ':': ib = 0x1E elif byte == '\\': ib = 0x1D elif byte == '_': ib = 0x1C elif byte == ',': ib = 0x1B elif byte == '.': ib = 0x1A else: ib = ord(byte) - 0x41 if ib == 0x7F: return 0x007F else: return (color << 8) + ib def convertCreditsCharBig(self, byte, top=True): # from: https://jathys.zophar.net/supermetroid/kejardon/TextFormat.txt # 2-tile high characters: # A-P = $XX20-$XX2F(TOP) and $XX30-$XX3F(BOTTOM) # Q-Z = $XX40-$XX49(TOP) and $XX50-$XX59(BOTTOM) # ' = $XX4A, $XX7F # " = $XX4B, $XX7F # . = $XX7F, $XX5A # 0-9 = $XX60-$XX69(TOP) and $XX70-$XX79(BOTTOM) # % = $XX6A, $XX7A if byte == ' ': ib = 0x7F elif byte == "'": if top == True: ib = 0x4A else: ib = 0x7F elif byte == '"': if top == True: ib = 0x4B else: ib = 0x7F elif byte == '.': if top == True: ib = 0x7F else: ib = 0x5A elif byte == '%': if top == True: ib = 0x6A else: ib = 0x7A byte = ord(byte) if byte >= ord('A') and byte <= ord('P'): ib = byte - 0x21 elif byte >= ord('Q') and byte <= ord('Z'): ib = byte - 0x11 elif byte >= ord('a') and byte <= ord('p'): ib = byte - 0x31 elif byte >= ord('q') and byte <= ord('z'): ib = byte - 0x21 elif byte >= ord('0') and byte <= ord('9'): if top == True: ib = byte + 0x30 else: ib = byte + 0x40 return ib def patchBytes(self, address, array, isRace=False): self.romFile.seek(address) for w in array: if not isRace: self.romFile.writeWord(w) else: self.race.writeWordMagic(w) # write area randomizer transitions to ROM # doorConnections : a list of connections. each connection is a dictionary describing # - where to write in the ROM : # DoorPtr : door pointer to write to # - what to write in the ROM : # RoomPtr, direction, bitflag, cap, screen, distanceToSpawn : door properties # * if SamusX and SamusY are defined in the dict, custom ASM has to be written # to reposition samus, and call doorAsmPtr if non-zero. The written Door ASM # property shall point to this custom ASM. # * if not, just write doorAsmPtr as the door property directly. def writeDoorConnections(self, doorConnections): asmAddress = 0x7F800 for conn in doorConnections: # write door ASM for transition doors (code and pointers) # print('Writing door connection ' + conn['ID']) doorPtr = conn['DoorPtr'] roomPtr = conn['RoomPtr'] if doorPtr in self.doorConnectionSpecific: self.doorConnectionSpecific[doorPtr](roomPtr) if roomPtr in self.roomConnectionSpecific: self.roomConnectionSpecific[roomPtr](doorPtr) self.romFile.seek(0x10000 + doorPtr) # write room ptr self.romFile.writeWord(roomPtr & 0xFFFF) # write bitflag (if area switch we have to set bit 0x40, and remove it if same area) self.romFile.writeByte(conn['bitFlag']) # write direction self.romFile.writeByte(conn['direction']) # write door cap x self.romFile.writeByte(conn['cap'][0]) # write door cap y self.romFile.writeByte(conn['cap'][1]) # write screen x self.romFile.writeByte(conn['screen'][0]) # write screen y self.romFile.writeByte(conn['screen'][1]) # write distance to spawn self.romFile.writeWord(conn['distanceToSpawn'] & 0xFFFF) # write door asm asmPatch = [] # call original door asm ptr if needed if conn['doorAsmPtr'] != 0x0000: # endian convert (D0, D1) = (conn['doorAsmPtr'] & 0x00FF, (conn['doorAsmPtr'] & 0xFF00) >> 8) asmPatch += [ 0x20, D0, D1 ] # JSR $doorAsmPtr # special ASM hook point for VARIA needs when taking the door (used for animals) if 'exitAsmPtr' in conn: # endian convert (D0, D1) = (conn['exitAsmPtr'] & 0x00FF, (conn['exitAsmPtr'] & 0xFF00) >> 8) asmPatch += [ 0x20, D0, D1 ] # JSR $exitAsmPtr # incompatible transition if 'SamusX' in conn: # endian convert (X0, X1) = (conn['SamusX'] & 0x00FF, (conn['SamusX'] & 0xFF00) >> 8) (Y0, Y1) = (conn['SamusY'] & 0x00FF, (conn['SamusY'] & 0xFF00) >> 8) # force samus position # see door_transition.asm. assemble it to print routines SNES addresses. asmPatch += [ 0x20, 0x00, 0xF6 ] # JSR incompatible_doors asmPatch += [ 0xA9, X0, X1 ] # LDA #$SamusX ; fixed Samus X position asmPatch += [ 0x8D, 0xF6, 0x0A ] # STA $0AF6 ; update Samus X position in memory asmPatch += [ 0xA9, Y0, Y1 ] # LDA #$SamusY ; fixed Samus Y position asmPatch += [ 0x8D, 0xFA, 0x0A ] # STA $0AFA ; update Samus Y position in memory else: # still give I-frames asmPatch += [ 0x20, 0x40, 0xF6 ] # JSR giveiframes # return asmPatch += [ 0x60 ] # RTS self.romFile.writeWord(asmAddress & 0xFFFF) self.romFile.seek(asmAddress) for byte in asmPatch: self.romFile.writeByte(byte) # print("asmAddress=%x" % asmAddress) # print("asmPatch=" + str(["%02x" % b for b in asmPatch])) asmAddress += len(asmPatch) # update room state header with song changes # TODO just do an IPS patch for this as it is completely static # this would get rid of both 'song' and 'songs' fields # as well as this code if 'song' in conn: for addr in conn["songs"]: self.romFile.seek(0x70000 + addr) self.romFile.writeByte(conn['song']) self.romFile.writeByte(0x5) # change BG table to avoid scrolling sky bug when transitioning to west ocean def patchWestOcean(self, doorPtr): self.romFile.writeWord(doorPtr, 0x7B7BB) # forces CRE graphics refresh when exiting kraid's or croc room def forceRoomCRE(self, roomPtr, creFlag=0x2): # Room ptr in bank 8F + CRE flag offset offset = 0x70000 + roomPtr + 0x8 self.romFile.writeByte(creFlag, offset) buttons = { "Select" : [0x00, 0x20], "A" : [0x80, 0x00], "B" : [0x00, 0x80], "X" : [0x40, 0x00], "Y" : [0x00, 0x40], "L" : [0x20, 0x00], "R" : [0x10, 0x00], "None" : [0x00, 0x00] } controls = { "Shot" : [0xb331, 0x1722d], "Jump" : [0xb325, 0x17233], "Dash" : [0xb32b, 0x17239], "ItemSelect" : [0xb33d, 0x17245], "ItemCancel" : [0xb337, 0x1723f], "AngleUp" : [0xb343, 0x1724b], "AngleDown" : [0xb349, 0x17251] } # write custom contols to ROM. # controlsDict : possible keys are "Shot", "Jump", "Dash", "ItemSelect", "ItemCancel", "AngleUp", "AngleDown" # possible values are "A", "B", "X", "Y", "L", "R", "Select", "None" def writeControls(self, controlsDict): for ctrl, button in controlsDict.items(): if ctrl not in RomPatcher.controls: raise ValueError("Invalid control name : " + str(ctrl)) if button not in RomPatcher.buttons: raise ValueError("Invalid button name : " + str(button)) for addr in RomPatcher.controls[ctrl]: self.romFile.writeByte(RomPatcher.buttons[button][0], addr) self.romFile.writeByte(RomPatcher.buttons[button][1]) def writePlandoAddresses(self, locations): self.romFile.seek(0x2F6000) for loc in locations: self.romFile.writeWord(loc.Address & 0xFFFF) # fill remaining addresses with 0xFFFF maxLocsNumber = 128 for i in range(0, maxLocsNumber-len(locations)): self.romFile.writeWord(0xFFFF) def writePlandoTransitions(self, transitions, doorsPtrs, maxTransitions): self.romFile.seek(0x2F6100) for (src, dest) in transitions: self.romFile.writeWord(doorsPtrs[src]) self.romFile.writeWord(doorsPtrs[dest]) # fill remaining addresses with 0xFFFF for i in range(0, maxTransitions-len(transitions)): self.romFile.writeWord(0xFFFF) self.romFile.writeWord(0xFFFF) def enableMoonWalk(self): # replace STZ with STA since A is non-zero at this point self.romFile.writeByte(0x8D, 0xB35D) def compress(self, address, data): # data: [] of 256 int # address: the address where the compressed bytes will be written # return the size of the compressed data compressedData = Compressor().compress(data) self.romFile.seek(address) for byte in compressedData: self.romFile.writeByte(byte) return len(compressedData) def setOamTile(self, nth, middle, newTile): # an oam entry is made of five bytes: (s000000 xxxxxxxxx) (yyyyyyyy) (YXpp000t tttttttt) # after and before the middle of the screen is not handle the same if nth >= middle: x = (nth - middle) * 0x08 else: x = 0x200 - (0x08 * (middle - nth)) self.romFile.writeWord(x) self.romFile.writeByte(0xFC) self.romFile.writeWord(0x3100+newTile) def writeVersion(self, version): # max 32 chars # new oamlist address in free space at the end of bank 8C self.romFile.writeWord(0xF3E9, 0x5a0e3) self.romFile.writeWord(0xF3E9, 0x5a0e9) # string length length = len(version) self.romFile.writeWord(length, 0x0673e9) middle = int(length / 2) + length % 2 # oams for (i, char) in enumerate(version): self.setOamTile(i, middle, char2tile[char]) def writeDoorsColor(self, doors): DoorsManager.writeDoorsColor(self.romFile, doors)
# - vanilla ROM # - path to nspc directory. *has* to be one level deeper than music base dir # - path to JSON metadata file to write. # will also parse room state headers, and list pointers where # music data/track has to be written, ie track number >= 5 # also lists the extra pointers for area rando, all of this # stored in the JSON metadata from rom.rom import RealROM, snes_to_pc from rom.ips import IPS_Patch vanilla=sys.argv[1] nspc_dir=sys.argv[2] json_path=sys.argv[3] rom=RealROM(vanilla) musicDataTable = snes_to_pc(0x8FE7E4) musicDataEnd = snes_to_pc(0xDED1C0) # tracks pointed music table, in that order # array is songs in music data: (name, spc_path) vanillaMusicData = [ # Song 0: intro, # Song 1: menu theme [("Title sequence intro", "vanilla/title_menu.spc", "Vanilla Soundtrack"), ("Menu theme", None, "Vanilla Soundtrack")], # Song 0: thunder - zebes asleep, # Song 1: thunder - zebes awake, # Song 2: no thunder (morph room...) [("Crateria Landing - Thunder, Zebes asleep", "vanilla/crateria_arrival.spc", "Vanilla Soundtrack"), ("Crateria Landing - Thunder, Zebes awake", "vanilla/crateria_rainstorm.spc", "Vanilla Soundtrack"), ("Crateria Landing - No Thunder", "vanilla/crateria_underground.spc", "Vanilla Soundtrack")],
# we're in directory 'tools/' we have to update sys.path sys.path.append(os.path.dirname(sys.path[0])) # helper tool to debug written music data of seeds with customized music: # use the playlist to find original tracks nspc data and compare from rom.rom import RealROM, snes_to_pc from rom.rompatcher import MusicPatcher, RomTypeForMusic from utils.parameters import appDir seed = sys.argv[1] playlist_json = sys.argv[2] # load args rom = RealROM(seed) with open(playlist_json, 'r') as f: playlist = json.load(f) # use patcher ctor to load music metadata p = MusicPatcher(rom, RomTypeForMusic.VariaSeed, baseDir=os.path.join(appDir + '/..', 'varia_custom_sprites', 'music')) vanillaTracks = p.vanillaTracks allTracks = p.allTracks tableAddr = p.musicDataTableAddress - 3 baseDir = p.baseDir preserved = p.constraints['preserve'] nspcInfo = p.nspcInfo
class RomPatcher: # possible patches. see patches asm source if applicable and available for more information IPSPatches = { # applied on all seeds 'Standard': [ # faster MB cutscene transitions 'Mother_Brain_Cutscene_Edits', # "Balanced" suit mode 'Removes_Gravity_Suit_heat_protection', # door ASM to skip G4 cutscene when all 4 bosses are dead 'g4_skip.ips', # basepatch is generated from https://github.com/lordlou/SMBasepatch 'basepatch.ips' ], # VARIA tweaks 'VariaTweaks': [ 'WS_Etank', 'LN_Chozo_SpaceJump_Check_Disable', 'ln_chozo_platform.ips', 'bomb_torizo.ips' ], # anti-softlock/game opening layout patches 'Layout': [ 'dachora.ips', 'early_super_bridge.ips', 'high_jump.ips', 'moat.ips', 'spospo_save.ips', 'nova_boost_platform.ips', 'red_tower.ips', 'spazer.ips', 'brinstar_map_room.ips', 'kraid_save.ips', 'mission_impossible.ips' ], # comfort patches 'Optional': [ 'rando_speed.ips', 'Infinite_Space_Jump', 'refill_before_save.ips', 'spinjumprestart.ips', 'elevators_doors_speed.ips', 'No_Music', 'random_music.ips', # animals 'animal_enemies.ips', 'animals.ips', 'draygonimals.ips', 'escapimals.ips', 'gameend.ips', 'grey_door_animals.ips', 'low_timer.ips', 'metalimals.ips', 'phantoonimals.ips', 'ridleyimals.ips', 'Escape_Animals_Change_Event', # ...end animals # vanilla behaviour restore 'remove_elevators_doors_speed.ips', 'varia_hud.ips' ], # base patchset+optional layout for area rando 'Area': [ 'area_rando_layout.ips', 'door_transition.ips', 'area_rando_doors.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'area_rando_warp_door.ips', 'crab_shaft.ips', 'Save_Crab_Shaft', 'Save_Main_Street', 'no_demo.ips' ], # patches for boss rando 'Bosses': ['door_transition.ips', 'no_demo.ips'], # patches for escape rando 'Escape': ['rando_escape.ips', 'rando_escape_ws_fix.ips', 'door_transition.ips'], # patches for minimizer with fast Tourian 'MinimizerTourian': ['minimizer_tourian.ips', 'open_zebetites.ips'], # patches for door color rando 'DoorsColors': ['beam_doors_plms.ips', 'beam_doors_gfx.ips', 'red_doors.ips'] } def __init__(self, romFileName=None, magic=None, plando=False, player=0): self.log = utils.log.get('RomPatcher') self.romFileName = romFileName self.race = None self.romFile = RealROM(romFileName) #if magic is not None: # from rom.race_mode import RaceModePatcher # self.race = RaceModePatcher(self, magic, plando) # IPS_Patch objects list self.ipsPatches = [] # loc name to alternate address. we still write to original # address to help the RomReader. self.altLocsAddresses = {} # specific fixes for area rando connections self.roomConnectionSpecific = { # fix scrolling sky when transitioning to west ocean 0x93fe: self.patchWestOcean } self.doorConnectionSpecific = { # get out of kraid room: reload CRE 0x91ce: self.forceRoomCRE, # get out of croc room: reload CRE 0x93ea: self.forceRoomCRE } self.patchAccess = PatchAccess() self.player = player def end(self): self.romFile.close() def writeItemCode(self, item, visibility, address): itemCode = ItemManager.getItemTypeCode(item, visibility) if self.race is None: self.romFile.writeWord(itemCode, address) else: self.race.writeItemCode(itemCode, address) def getLocAddresses(self, loc): ret = [loc.Address] if loc.Name in self.altLocsAddresses: ret.append(self.altLocsAddresses[loc.Name]) return ret def writeItem(self, itemLoc): loc = itemLoc.Location if loc.isBoss(): raise ValueError('Cannot write Boss location') #print('write ' + itemLoc.Item.Type + ' at ' + loc.Name) for addr in self.getLocAddresses(loc): self.writeItemCode(itemLoc.Item, loc.Visibility, addr) def writeItemsLocs(self, itemLocs): self.nItems = 0 for itemLoc in itemLocs: loc = itemLoc.Location item = itemLoc.Item if loc.isBoss(): continue self.writeItem(itemLoc) if item.Category != 'Nothing': self.nItems += 1 if loc.Name == 'Morphing Ball': self.patchMorphBallEye(item) def writeSplitLocs(self, split, itemLocs, progItemLocs): majChozoCheck = lambda itemLoc: itemLoc.Item.Class == split and itemLoc.Location.isClass( split) fullCheck = lambda itemLoc: itemLoc.Location.Id is not None splitChecks = { 'Full': fullCheck, 'Scavenger': fullCheck, 'Major': majChozoCheck, 'Chozo': majChozoCheck, 'FullWithHUD': lambda itemLoc: itemLoc.Item.Category not in ['Energy', 'Ammo', 'Boss'] } itemLocCheck = lambda itemLoc: itemLoc.Item.Category != "Nothing" and splitChecks[ split](itemLoc) for area, addr in locIdsByAreaAddresses.items(): locs = [ il.Location for il in itemLocs if itemLocCheck(il) and il.Location.GraphArea == area ] self.log.debug("writeSplitLocs. area=" + area) self.log.debug(str([loc.Name for loc in locs])) self.romFile.seek(addr) for loc in locs: self.romFile.writeByte(loc.Id) self.romFile.writeByte(0xff) if split == "Scavenger": # write required major item order self.romFile.seek(snes_to_pc(0xA1F5D8)) for itemLoc in progItemLocs: self.romFile.writeWord((itemLoc.Location.Id << 8) | itemLoc.Location.HUD) # bogus loc ID | "HUNT OVER" index self.romFile.writeWord(0xff10) # trigger morph eye enemy on whatever item we put there, # not just morph ball def patchMorphBallEye(self, item): # print('Eye item = ' + item.Type) isAmmo = item.Category == 'Ammo' # category to check if ItemManager.isBeam(item): cat = 0xA8 # collected beams elif item.Type == 'ETank': cat = 0xC4 # max health elif item.Type == 'Reserve': cat = 0xD4 # max reserves elif item.Type == 'Missile': cat = 0xC8 # max missiles elif item.Type == 'Super': cat = 0xCC # max supers elif item.Type == 'PowerBomb': cat = 0xD0 # max PBs else: cat = 0xA4 # collected items # comparison/branch instruction # the branch is taken if we did NOT collect item yet if item.Category == 'Energy' or isAmmo: comp = 0xC9 # CMP (immediate) branch = 0x30 # BMI else: comp = 0x89 # BIT (immediate) branch = 0xF0 # BEQ # what to compare to if item.Type == 'ETank': operand = 0x65 # < 100 elif item.Type == 'Reserve' or isAmmo: operand = 0x1 # < 1 elif ItemManager.isBeam(item): operand = item.BeamBits else: operand = item.ItemBits self.patchMorphBallCheck(0x1410E6, cat, comp, operand, branch) # eye main AI self.patchMorphBallCheck(0x1468B2, cat, comp, operand, branch) # head main AI def patchMorphBallCheck(self, offset, cat, comp, operand, branch): # actually patch enemy AI self.romFile.writeByte(cat, offset) self.romFile.writeByte(comp, offset + 2) self.romFile.writeWord(operand) self.romFile.writeByte(branch) def writeItemsNumber(self): # write total number of actual items for item percentage patch (patch the patch) for addr in [0x5E64E, 0x5E6AB]: self.romFile.writeByte(self.nItems, addr) def addIPSPatches(self, patches): for patchName in patches: self.applyIPSPatch(patchName) def writePlmTable(self, plms, area, bosses, startLocation): # called when saving a plando try: if bosses == True or area == True: plms.append('WS_Save_Blinking_Door') doors = self.getStartDoors(plms, area, None) self.writeDoorsColor(doors, self.player) self.applyStartAP(startLocation, plms, doors) self.applyPLMs(plms) except Exception as e: raise Exception("Error patching {}. ({})".format( self.romFileName, e)) def applyIPSPatches(self, startLocation="Landing Site", optionalPatches=[], noLayout=False, suitsMode="Balanced", area=False, bosses=False, areaLayoutBase=False, noVariaTweaks=False, nerfedCharge=False, nerfedRainbowBeam=False, escapeAttr=None, minimizerN=None, minimizerTourian=True, doorsColorsRando=False): try: # apply standard patches stdPatches = [] plms = [] # apply race mode first because it fills the rom with a bunch of crap if self.race is not None: stdPatches.append('race_mode.ips') stdPatches += RomPatcher.IPSPatches['Standard'][:] if self.race is not None: stdPatches.append('race_mode_credits.ips') if suitsMode != "Balanced": stdPatches.remove('Removes_Gravity_Suit_heat_protection') if suitsMode == "Progressive": stdPatches.append('progressive_suits.ips') if nerfedCharge == True: stdPatches.append('nerfed_charge.ips') if nerfedRainbowBeam == True: stdPatches.append('nerfed_rainbow_beam.ips') if bosses == True or area == True: stdPatches += ["WS_Main_Open_Grey", "WS_Save_Active"] plms.append('WS_Save_Blinking_Door') if bosses == True: stdPatches.append("Phantoon_Eye_Door") if area == True or doorsColorsRando == True: stdPatches.append("Enable_Backup_Saves") if 'varia_hud.ips' in optionalPatches: # varia hud has its own variant of g4_skip for scavenger mode, # it can also make demos glitch out stdPatches.remove("g4_skip.ips") self.applyIPSPatch("no_demo.ips") for patchName in stdPatches: self.applyIPSPatch(patchName) if noLayout == False: # apply layout patches for patchName in RomPatcher.IPSPatches['Layout']: self.applyIPSPatch(patchName) if noVariaTweaks == False: # VARIA tweaks for patchName in RomPatcher.IPSPatches['VariaTweaks']: self.applyIPSPatch(patchName) # apply optional patches for patchName in optionalPatches: if patchName in RomPatcher.IPSPatches['Optional']: self.applyIPSPatch(patchName) # random escape if escapeAttr is not None: for patchName in RomPatcher.IPSPatches['Escape']: self.applyIPSPatch(patchName) # animals and timer self.applyEscapeAttributes(escapeAttr, plms) # apply area patches if area == True: for patchName in RomPatcher.IPSPatches['Area']: if areaLayoutBase == True and patchName in [ 'area_rando_layout.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips' ]: continue self.applyIPSPatch(patchName) if areaLayoutBase == True: self.applyIPSPatch('area_rando_layout_base.ips') else: self.applyIPSPatch('area_ids_alt.ips') if bosses == True: for patchName in RomPatcher.IPSPatches['Bosses']: self.applyIPSPatch(patchName) if minimizerN is not None: self.applyIPSPatch('minimizer_bosses.ips') if minimizerTourian == True: for patchName in RomPatcher.IPSPatches['MinimizerTourian']: self.applyIPSPatch(patchName) doors = self.getStartDoors(plms, area, minimizerN) if doorsColorsRando == True: for patchName in RomPatcher.IPSPatches['DoorsColors']: self.applyIPSPatch(patchName) self.writeDoorsColor(doors, self.player) self.applyStartAP(startLocation, plms, doors) self.applyPLMs(plms) except Exception as e: raise Exception("Error patching {}. ({})".format( self.romFileName, e)) def applyIPSPatch(self, patchName, patchDict=None, ipsDir=None): if patchDict is None: patchDict = self.patchAccess.getDictPatches() # print("Apply patch {}".format(patchName)) if patchName in patchDict: patch = IPS_Patch(patchDict[patchName]) else: # look for ips file if ipsDir is None: patch = IPS_Patch.load( self.patchAccess.getPatchPath(patchName)) else: patch = IPS_Patch.load(os.path.join(appDir, ipsDir, patchName)) self.ipsPatches.append(patch) def applyIPSPatchDict(self, patchDict): for patchName in patchDict.keys(): # print("Apply patch {}".format(patchName)) patch = IPS_Patch(patchDict[patchName]) self.ipsPatches.append(patch) def getStartDoors(self, plms, area, minimizerN): doors = [0x10] # red brin elevator def addBlinking(name): key = 'Blinking[{}]'.format(name) if key in self.patchAccess.getDictPatches(): self.applyIPSPatch(key) if key in self.patchAccess.getAdditionalPLMs(): plms.append(key) if area == True: plms += [ 'Maridia Sand Hall Seal', "Save_Main_Street", "Save_Crab_Shaft" ] for accessPoint in Logic.accessPoints: if accessPoint.Internal == True or accessPoint.Boss == True: continue addBlinking(accessPoint.Name) addBlinking("West Sand Hall Left") addBlinking("Below Botwoon Energy Tank Right") if minimizerN is not None: # add blinking doors inside and outside boss rooms for accessPoint in Logic.accessPoints: if accessPoint.Boss == True: addBlinking(accessPoint.Name) return doors def applyStartAP(self, apName, plms, doors): ap = getAccessPoint(apName) # if start loc is not Ceres or Landing Site, or the ceiling loc picked up before morph loc, # Zebes will be awake and morph loc item will disappear. # this PLM ensures the item will be here whenever zebes awakes plms.append('Morph_Zebes_Awake') (w0, w1) = getWord(ap.Start['spawn']) if 'doors' in ap.Start: doors += ap.Start['doors'] doors.append(0x0) addr = 0x10F200 patch = [w0, w1] + doors assert (addr + len(patch)) < 0x10F210, "Stopped before new_game overwrite" patchDict = { 'StartAP': { addr: patch }, } self.applyIPSPatch('StartAP', patchDict) # handle custom saves if 'save' in ap.Start: self.applyIPSPatch(ap.Start['save']) plms.append(ap.Start['save']) # handle optional rom patches if 'rom_patches' in ap.Start: for patch in ap.Start['rom_patches']: self.applyIPSPatch(patch) def applyEscapeAttributes(self, escapeAttr, plms): # timer escapeTimer = escapeAttr['Timer'] if escapeTimer is not None: minute = int(escapeTimer / 60) second = escapeTimer % 60 minute = int(minute / 10) * 16 + minute % 10 second = int(second / 10) * 16 + second % 10 patchDict = {'Escape_Timer': {0x1E21: [second, minute]}} self.applyIPSPatch('Escape_Timer', patchDict) # animals door to open if escapeAttr['Animals'] is not None: escapeOpenPatches = { 'Green Brinstar Main Shaft Top Left': 'Escape_Animals_Open_Brinstar', 'Business Center Mid Left': "Escape_Animals_Open_Norfair", 'Crab Hole Bottom Right': "Escape_Animals_Open_Maridia", } if escapeAttr['Animals'] in escapeOpenPatches: plms.append("WS_Map_Grey_Door") self.applyIPSPatch(escapeOpenPatches[escapeAttr['Animals']]) else: plms.append("WS_Map_Grey_Door_Openable") else: plms.append("WS_Map_Grey_Door") # optional patches (enemies, scavenger) for patch in escapeAttr['patches']: self.applyIPSPatch(patch) # adds ad-hoc "IPS patches" for additional PLM tables def applyPLMs(self, plms): # compose a dict (room, state, door) => PLM array # 'PLMs' being a 6 byte arrays plmDict = {} # we might need to update locations addresses on the fly plmLocs = {} # room key above => loc name additionalPLMs = self.patchAccess.getAdditionalPLMs() for p in plms: plm = additionalPLMs[p] room = plm['room'] state = 0 if 'state' in plm: state = plm['state'] door = 0 if 'door' in plm: door = plm['door'] k = (room, state, door) if k not in plmDict: plmDict[k] = [] plmDict[k] += plm['plm_bytes_list'] if 'locations' in plm: locList = plm['locations'] for locName, locIndex in locList: plmLocs[(k, locIndex)] = locName # make two patches out of this dict plmTblAddr = 0x7E9A0 # moves downwards plmPatchData = [] roomTblAddr = 0x7EC00 # moves upwards roomPatchData = [] plmTblOffset = plmTblAddr def appendPlmBytes(bytez): nonlocal plmPatchData, plmTblOffset plmPatchData += bytez plmTblOffset += len(bytez) def addRoomPatchData(bytez): nonlocal roomPatchData, roomTblAddr roomPatchData = bytez + roomPatchData roomTblAddr -= len(bytez) for roomKey, plmList in plmDict.items(): entryAddr = plmTblOffset roomData = [] for i in range(len(plmList)): plmBytes = plmList[i] assert len( plmBytes) == 6, "Invalid PLM entry for roomKey " + str( roomKey) + ": PLM list len is " + str(len(plmBytes)) if (roomKey, i) in plmLocs: self.altLocsAddresses[plmLocs[(roomKey, i)]] = plmTblOffset appendPlmBytes(plmBytes) appendPlmBytes([0x0, 0x0]) # list terminator def appendRoomWord(w, data): (w0, w1) = getWord(w) data += [w0, w1] for i in range(3): appendRoomWord(roomKey[i], roomData) appendRoomWord(entryAddr, roomData) addRoomPatchData(roomData) # write room table terminator addRoomPatchData([0x0] * 8) assert plmTblOffset < roomTblAddr, "Spawn PLM table overlap" patchDict = { "PLM_Spawn_Tables": { plmTblAddr: plmPatchData, roomTblAddr: roomPatchData } } self.applyIPSPatch("PLM_Spawn_Tables", patchDict) def commitIPS(self): self.romFile.ipsPatch(self.ipsPatches) def writeSeed(self, seed): random.seed(seed) seedInfo = random.randint(0, 0xFFFF) seedInfo2 = random.randint(0, 0xFFFF) self.romFile.writeWord(seedInfo, 0x2FFF00) self.romFile.writeWord(seedInfo2) def writeMagic(self): if self.race is not None: self.race.writeMagic() def writeMajorsSplit(self, majorsSplit): address = 0x17B6C splits = { 'Chozo': 'Z', 'Major': 'M', 'FullWithHUD': 'H', 'Scavenger': 'S' } char = splits.get(majorsSplit, 'F') self.romFile.writeByte(ord(char), address) def getItemQty(self, itemLocs, itemType): return len([ il for il in itemLocs if il.Accessible and il.Item.Type == itemType ]) def getMinorsDistribution(self, itemLocs): dist = {} minQty = 100 minors = ['Missile', 'Super', 'PowerBomb'] for m in minors: # in vcr mode if the seed has stuck we may not have these items, return at least 1 q = float(max(self.getItemQty(itemLocs, m), 1)) dist[m] = {'Quantity': q} if q < minQty: minQty = q for m in minors: dist[m]['Proportion'] = dist[m]['Quantity'] / minQty return dist def getAmmoPct(self, minorsDist): q = 0 for m, v in minorsDist.items(): q += v['Quantity'] return 100 * q / 66 def writeRandoSettings(self, settings, itemLocs): dist = self.getMinorsDistribution(itemLocs) totalAmmo = sum(d['Quantity'] for ammo, d in dist.items()) totalItemLocs = sum(1 for il in itemLocs if il.Accessible and not il.Location.isBoss()) totalNothing = sum(1 for il in itemLocs if il.Accessible and il.Item.Category == 'Nothing') totalEnergy = self.getItemQty(itemLocs, 'ETank') + self.getItemQty( itemLocs, 'Reserve') totalMajors = max( totalItemLocs - totalEnergy - totalAmmo - totalNothing, 0) address = 0x2736C0 value = "{:>2}".format(totalItemLocs) line = " ITEM LOCATIONS %s " % value self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " item locations ............ %s " % value self.writeCreditsStringBig(address, line, top=False) address += 0x40 maj = "{:>2}".format(int(totalMajors)) htanks = "{:>2}".format(int(totalEnergy)) ammo = "{:>2}".format(int(totalAmmo)) blank = "{:>2}".format(int(totalNothing)) line = " MAJ %s EN %s AMMO %s BLANK %s " % (maj, htanks, ammo, blank) self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " maj %s en %s ammo %s blank %s " % (maj, htanks, ammo, blank) self.writeCreditsStringBig(address, line, top=False) address += 0x40 pbs = "{:>2}".format(int(dist['PowerBomb']['Quantity'])) miss = "{:>2}".format(int(dist['Missile']['Quantity'])) supers = "{:>2}".format(int(dist['Super']['Quantity'])) line = " AMMO PACKS MI %s SUP %s PB %s " % (miss, supers, pbs) self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " ammo packs mi %s sup %s pb %s " % (miss, supers, pbs) self.writeCreditsStringBig(address, line, top=False) address += 0x40 etanks = "{:>2}".format(int(self.getItemQty(itemLocs, 'ETank'))) reserves = "{:>2}".format(int(self.getItemQty(itemLocs, 'Reserve'))) line = " HEALTH TANKS E %s R %s " % (etanks, reserves) self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " health tanks ...... e %s r %s " % (etanks, reserves) self.writeCreditsStringBig(address, line, top=False) address += 0x80 value = " " + "NA" # settings.progSpeed.upper() line = " PROGRESSION SPEED ....%s " % value.rjust(8, '.') self.writeCreditsString(address, 0x04, line) address += 0x40 line = " PROGRESSION DIFFICULTY %s " % value.rjust( 7, '.') # settings.progDiff.upper() self.writeCreditsString(address, 0x04, line) address += 0x80 # skip item distrib title param = (' SUITS RESTRICTION ........%s', 'Suits') line = param[0] % ('. ON' if settings.restrictions[param[1]] == True else ' OFF') self.writeCreditsString(address, 0x04, line) address += 0x40 value = " " + settings.restrictions['Morph'].upper() line = " MORPH PLACEMENT .....%s" % value.rjust(9, '.') self.writeCreditsString(address, 0x04, line) address += 0x40 for superFun in [(' SUPER FUN COMBAT .........%s', 'Combat'), (' SUPER FUN MOVEMENT .......%s', 'Movement'), (' SUPER FUN SUITS ..........%s', 'Suits')]: line = superFun[0] % ('. ON' if superFun[1] in settings.superFun else ' OFF') self.writeCreditsString(address, 0x04, line) address += 0x40 value = "%.1f %.1f %.1f" % (dist['Missile']['Proportion'], dist['Super']['Proportion'], dist['PowerBomb']['Proportion']) line = " AMMO DISTRIBUTION %s " % value self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " ammo distribution %s " % value self.writeCreditsStringBig(address, line, top=False) address += 0x40 # write ammo/energy pct address = 0x273C40 (ammoPct, energyPct) = (int(self.getAmmoPct(dist)), int(100 * totalEnergy / 18)) line = " AVAILABLE AMMO {:>3}% ENERGY {:>3}%".format( ammoPct, energyPct) self.writeCreditsStringBig(address, line, top=True) address += 0x40 line = " available ammo {:>3}% energy {:>3}%".format( ammoPct, energyPct) self.writeCreditsStringBig(address, line, top=False) def writeSpoiler(self, itemLocs, progItemLocs=None): # keep only majors fItemLocs = [ il for il in itemLocs if il.Item.Category not in ['Ammo', 'Nothing', 'Energy', 'Boss'] ] # add location of the first instance of each minor for t in ['Missile', 'Super', 'PowerBomb']: itLoc = None if progItemLocs is not None: itLoc = next((il for il in progItemLocs if il.Item.Type == t), None) if itLoc is None: itLoc = next((il for il in itemLocs if il.Item.Type == t), None) if itLoc is not None: # in vcr mode if the seed has stucked we may not have these minors fItemLocs.append(itLoc) regex = re.compile(r"[^A-Z0-9\.,'!: ]+") itemLocs = {} for iL in fItemLocs: itemLocs[iL.Item.Name] = iL.Location.Name def prepareString(s, isItem=True): s = s.upper() # remove chars not displayable s = regex.sub('', s) # remove space before and after s = s.strip() # limit to 30 chars, add one space before # pad to 32 chars if isItem is True: s = " " + s[0:30] s = s.ljust(32) else: s = " " + s[0:30] + " " s = " " + s.rjust(31, '.') return s isRace = self.race is not None startCreditAddress = 0x2f5240 address = startCreditAddress if isRace: addr = address - 0x40 data = [ 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x1008, 0x1013, 0x1004, 0x100c, 0x007f, 0x100b, 0x100e, 0x1002, 0x1000, 0x1013, 0x1008, 0x100e, 0x100d, 0x1012, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f ] for i in range(0x20): w = data[i] self.romFile.seek(addr) self.race.writeWordMagic(w) addr += 0x2 # standard item order items = [ "Missile", "Super Missile", "Power Bomb", "Charge Beam", "Ice Beam", "Wave Beam", "Spazer", "Plasma Beam", "Varia Suit", "Gravity Suit", "Morph Ball", "Bomb", "Spring Ball", "Screw Attack", "Hi-Jump Boots", "Space Jump", "Speed Booster", "Grappling Beam", "X-Ray Scope" ] displayNames = {} if progItemLocs is not None: # reorder it with progression indices prog = ord('A') idx = 0 progNames = [ il.Item.Name for il in progItemLocs if il.Item.Category != 'Boss' ] for i in range(len(progNames)): item = progNames[i] if item in items and item not in displayNames: items.remove(item) items.insert(idx, item) displayNames[item] = chr(prog + i) + ": " + item idx += 1 for item in items: # super fun removes items if item not in itemLocs: continue display = item if item in displayNames: display = displayNames[item] itemName = prepareString(display) locationName = prepareString(itemLocs[item], isItem=False) self.writeCreditsString(address, 0x04, itemName, isRace) self.writeCreditsString((address + 0x40), 0x18, locationName, isRace) address += 0x80 # we need 19 items displayed, if we've removed majors, add some blank text while address < startCreditAddress + len(items) * 0x80: self.writeCreditsString(address, 0x04, prepareString(""), isRace) self.writeCreditsString((address + 0x40), 0x18, prepareString(""), isRace) address += 0x80 self.patchBytes(address, [0, 0, 0, 0], isRace) def writeCreditsString(self, address, color, string, isRace=False): array = [self.convertCreditsChar(color, char) for char in string] self.patchBytes(address, array, isRace) def writeCreditsStringBig(self, address, string, top=True): array = [self.convertCreditsCharBig(char, top) for char in string] self.patchBytes(address, array) def convertCreditsChar(self, color, byte): if byte == ' ': ib = 0x7f elif byte == '!': ib = 0x1F elif byte == ':': ib = 0x1E elif byte == '\\': ib = 0x1D elif byte == '_': ib = 0x1C elif byte == ',': ib = 0x1B elif byte == '.': ib = 0x1A else: ib = ord(byte) - 0x41 if ib == 0x7F: return 0x007F else: return (color << 8) + ib def convertCreditsCharBig(self, byte, top=True): # from: https://jathys.zophar.net/supermetroid/kejardon/TextFormat.txt # 2-tile high characters: # A-P = $XX20-$XX2F(TOP) and $XX30-$XX3F(BOTTOM) # Q-Z = $XX40-$XX49(TOP) and $XX50-$XX59(BOTTOM) # ' = $XX4A, $XX7F # " = $XX4B, $XX7F # . = $XX7F, $XX5A # 0-9 = $XX60-$XX69(TOP) and $XX70-$XX79(BOTTOM) # % = $XX6A, $XX7A if byte == ' ': ib = 0x7F elif byte == "'": if top == True: ib = 0x4A else: ib = 0x7F elif byte == '"': if top == True: ib = 0x4B else: ib = 0x7F elif byte == '.': if top == True: ib = 0x7F else: ib = 0x5A elif byte == '%': if top == True: ib = 0x6A else: ib = 0x7A byte = ord(byte) if byte >= ord('A') and byte <= ord('P'): ib = byte - 0x21 elif byte >= ord('Q') and byte <= ord('Z'): ib = byte - 0x11 elif byte >= ord('a') and byte <= ord('p'): ib = byte - 0x31 elif byte >= ord('q') and byte <= ord('z'): ib = byte - 0x21 elif byte >= ord('0') and byte <= ord('9'): if top == True: ib = byte + 0x30 else: ib = byte + 0x40 return ib def patchBytes(self, address, array, isRace=False): self.romFile.seek(address) for w in array: if not isRace: self.romFile.writeWord(w) else: self.race.writeWordMagic(w) # write area randomizer transitions to ROM # doorConnections : a list of connections. each connection is a dictionary describing # - where to write in the ROM : # DoorPtr : door pointer to write to # - what to write in the ROM : # RoomPtr, direction, bitflag, cap, screen, distanceToSpawn : door properties # * if SamusX and SamusY are defined in the dict, custom ASM has to be written # to reposition samus, and call doorAsmPtr if non-zero. The written Door ASM # property shall point to this custom ASM. # * if not, just write doorAsmPtr as the door property directly. def writeDoorConnections(self, doorConnections): asmAddress = 0x7F800 for conn in doorConnections: # write door ASM for transition doors (code and pointers) # print('Writing door connection ' + conn['ID']) doorPtr = conn['DoorPtr'] roomPtr = conn['RoomPtr'] if doorPtr in self.doorConnectionSpecific: self.doorConnectionSpecific[doorPtr](roomPtr) if roomPtr in self.roomConnectionSpecific: self.roomConnectionSpecific[roomPtr](doorPtr) self.romFile.seek(0x10000 + doorPtr) # write room ptr self.romFile.writeWord(roomPtr & 0xFFFF) # write bitflag (if area switch we have to set bit 0x40, and remove it if same area) self.romFile.writeByte(conn['bitFlag']) # write direction self.romFile.writeByte(conn['direction']) # write door cap x self.romFile.writeByte(conn['cap'][0]) # write door cap y self.romFile.writeByte(conn['cap'][1]) # write screen x self.romFile.writeByte(conn['screen'][0]) # write screen y self.romFile.writeByte(conn['screen'][1]) # write distance to spawn self.romFile.writeWord(conn['distanceToSpawn'] & 0xFFFF) # write door asm asmPatch = [] # call original door asm ptr if needed if conn['doorAsmPtr'] != 0x0000: # endian convert (D0, D1) = (conn['doorAsmPtr'] & 0x00FF, (conn['doorAsmPtr'] & 0xFF00) >> 8) asmPatch += [0x20, D0, D1] # JSR $doorAsmPtr # special ASM hook point for VARIA needs when taking the door (used for animals) if 'exitAsmPtr' in conn: # endian convert (D0, D1) = (conn['exitAsmPtr'] & 0x00FF, (conn['exitAsmPtr'] & 0xFF00) >> 8) asmPatch += [0x20, D0, D1] # JSR $exitAsmPtr # incompatible transition if 'SamusX' in conn: # endian convert (X0, X1) = (conn['SamusX'] & 0x00FF, (conn['SamusX'] & 0xFF00) >> 8) (Y0, Y1) = (conn['SamusY'] & 0x00FF, (conn['SamusY'] & 0xFF00) >> 8) # force samus position # see door_transition.asm. assemble it to print routines SNES addresses. asmPatch += [0x20, 0x00, 0xF6] # JSR incompatible_doors asmPatch += [0xA9, X0, X1 ] # LDA #$SamusX ; fixed Samus X position asmPatch += [ 0x8D, 0xF6, 0x0A ] # STA $0AF6 ; update Samus X position in memory asmPatch += [0xA9, Y0, Y1 ] # LDA #$SamusY ; fixed Samus Y position asmPatch += [ 0x8D, 0xFA, 0x0A ] # STA $0AFA ; update Samus Y position in memory else: # still give I-frames asmPatch += [0x20, 0x40, 0xF6] # JSR giveiframes # return asmPatch += [0x60] # RTS self.romFile.writeWord(asmAddress & 0xFFFF) self.romFile.seek(asmAddress) for byte in asmPatch: self.romFile.writeByte(byte) # print("asmAddress=%x" % asmAddress) # print("asmPatch=" + str(["%02x" % b for b in asmPatch])) asmAddress += len(asmPatch) # update room state header with song changes # TODO just do an IPS patch for this as it is completely static # this would get rid of both 'song' and 'songs' fields # as well as this code if 'song' in conn: for addr in conn["songs"]: self.romFile.seek(0x70000 + addr) self.romFile.writeByte(conn['song']) self.romFile.writeByte(0x5) # change BG table to avoid scrolling sky bug when transitioning to west ocean def patchWestOcean(self, doorPtr): self.romFile.writeWord(doorPtr, 0x7B7BB) # forces CRE graphics refresh when exiting kraid's or croc room def forceRoomCRE(self, roomPtr, creFlag=0x2): # Room ptr in bank 8F + CRE flag offset offset = 0x70000 + roomPtr + 0x8 self.romFile.writeByte(creFlag, offset) buttons = { "Select": [0x00, 0x20], "A": [0x80, 0x00], "B": [0x00, 0x80], "X": [0x40, 0x00], "Y": [0x00, 0x40], "L": [0x20, 0x00], "R": [0x10, 0x00], "None": [0x00, 0x00] } controls = { "Shoot": [0xb331, 0x1722d], "Jump": [0xb325, 0x17233], "Dash": [0xb32b, 0x17239], "Item Select": [0xb33d, 0x17245], "Item Cancel": [0xb337, 0x1723f], "Angle Up": [0xb343, 0x1724b], "Angle Down": [0xb349, 0x17251] } # write custom contols to ROM. # controlsDict : possible keys are "Shot", "Jump", "Dash", "ItemSelect", "ItemCancel", "AngleUp", "AngleDown" # possible values are "A", "B", "X", "Y", "L", "R", "Select", "None" def writeControls(self, controlsDict): for ctrl, button in controlsDict.items(): if ctrl not in RomPatcher.controls: raise ValueError("Invalid control name : " + str(ctrl)) if button not in RomPatcher.buttons: raise ValueError("Invalid button name : " + str(button)) for addr in RomPatcher.controls[ctrl]: self.romFile.writeByte(RomPatcher.buttons[button][0], addr) self.romFile.writeByte(RomPatcher.buttons[button][1]) def writePlandoAddresses(self, locations): self.romFile.seek(0x2F6000) for loc in locations: self.romFile.writeWord(loc.Address & 0xFFFF) # fill remaining addresses with 0xFFFF maxLocsNumber = 128 for i in range(0, maxLocsNumber - len(locations)): self.romFile.writeWord(0xFFFF) def writePlandoTransitions(self, transitions, doorsPtrs, maxTransitions): self.romFile.seek(0x2F6100) for (src, dest) in transitions: self.romFile.writeWord(doorsPtrs[src]) self.romFile.writeWord(doorsPtrs[dest]) # fill remaining addresses with 0xFFFF for i in range(0, maxTransitions - len(transitions)): self.romFile.writeWord(0xFFFF) self.romFile.writeWord(0xFFFF) def enableMoonWalk(self): # replace STZ with STA since A is non-zero at this point self.romFile.writeByte(0x8D, 0xB35D) def setOamTile(self, nth, middle, newTile, y=0xFC): # an oam entry is made of five bytes: (s000000 xxxxxxxxx) (yyyyyyyy) (YXpp000t tttttttt) # after and before the middle of the screen is not handle the same if nth >= middle: x = (nth - middle) * 0x08 else: x = 0x200 - (0x08 * (middle - nth)) self.romFile.writeWord(x) self.romFile.writeByte(y) self.romFile.writeWord(0x3100 + newTile) def writeVersion(self, version, addRotation=False): # max 32 chars # new oamlist address in free space at the end of bank 8C self.romFile.writeWord(0xF3E9, 0x5a0e3) self.romFile.writeWord(0xF3E9, 0x5a0e9) # string length versionLength = len(version) if addRotation: rotationLength = len('rotation') length = versionLength + rotationLength else: length = versionLength self.romFile.writeWord(length, 0x0673e9) versionMiddle = int(versionLength / 2) + versionLength % 2 # oams for (i, char) in enumerate(version): self.setOamTile(i, versionMiddle, char2tile[char]) if addRotation: rotationMiddle = int(rotationLength / 2) + rotationLength % 2 for (i, char) in enumerate('rotation'): self.setOamTile(i, rotationMiddle, char2tile[char], y=0x8e) def writeDoorsColor(self, doors, player): DoorsManager.writeDoorsColor(self.romFile, doors, player)
] def move8tile(srcGfx, dstGfx, srcAddr, dstAddr): bytes = srcGfx.readBytes(0x20, srcAddr) dstGfx.writeBytes(bytes, 0x20, dstAddr) def move16tile(srcGfx, dstGfx, srcAddr, dstAddr): move8tile(srcGfx, dstGfx, srcAddr, dstAddr) move8tile(srcGfx, dstGfx, srcAddr + 0x20, dstAddr + 0x20) move8tile(srcGfx, dstGfx, srcAddr + 0x200, dstAddr + 0x200) move8tile(srcGfx, dstGfx, srcAddr + 0x200 + 0x20, dstAddr + 0x200 + 0x20) variagfx = RealROM(variagfx) vanillagfx = RealROM(vanillagfx) # move tiles to match vanilla layout for tile in tiles: if tile['size'] == 16: move16tile(variagfx, vanillagfx, tile['src'], tile['dst']) else: move8tile(variagfx, vanillagfx, tile['src'], tile['dst']) variagfx.close() vanillagfx.close() # update palette variapal = RealROM(variapal) vanillarom = RealROM(vanillarom)
import sys, os # we're in directory 'tools/' we have to update sys.path sys.path.append(os.path.dirname(sys.path[0])) # extract ship tiles & layout & palette from hack, generate an ips in the end. from rom.rom import RealROM, snes_to_pc from rom.compression import Compressor vanilla = sys.argv[1] tileAddr = snes_to_pc(0x95A82F) tilemapAddr = snes_to_pc(0x96FE69) vanillaRom = RealROM(vanilla) _, tileData = Compressor().decompress(vanillaRom, tileAddr) _, tilemapData = Compressor().decompress(vanillaRom, tilemapAddr) tileBytes = [b.to_bytes(1, byteorder='little') for b in tileData] with open('ship7.gfx', 'wb') as ship: for byte in tileBytes: ship.write(byte) tilemapBytes = [b.to_bytes(1, byteorder='little') for b in tilemapData] with open('ship7.tilemap', 'wb') as tilemap: for byte in tilemapBytes: tilemap.write(byte) print("ship7.gfx and ship7.tilemap extracted")
def __init__(self, romFileName, magic=None): super(RomLoaderSfc, self).__init__() realROM = RealROM(romFileName) self.romReader = RomReader(realROM, magic)
#!/usr/bin/python3 import sys, os, json, hashlib # we're in directory 'tools/' we have to update sys.path sys.path.append(os.path.dirname(sys.path[0])) from rom.rom import RealROM, snes_to_pc, pc_to_snes from rom.rompatcher import MusicPatcher, RomTypeForMusic from utils.parameters import appDir from utils.utils import removeChars seed = sys.argv[1] baseDir = os.path.join(appDir + '/..', 'varia_custom_sprites', 'music') rom = RealROM(seed) p = MusicPatcher(rom, RomTypeForMusic.VariaSeed, baseDir=baseDir) vanillaTracks = p.vanillaTracks allTracks = p.allTracks tableAddr = p.musicDataTableAddress - 3 nspcMetaPath = os.path.join(baseDir, "nspc_metadata.json") with open(nspcMetaPath, "r") as f: nspcMeta = json.load(f) def readNspcData(rom, addr): # songs can have two tracks nspcCount = 0 step = 0 data = [] maxSize = 64 * 1024 rom.seek(addr)