def __init__(self, romFile, magic=None): self.romFile = romFile self.race = None # default to morph ball location self.nothingId = 0x1a self.nothingAddr = 0x786DE if magic is not None: from rom.race_mode import RaceModeReader self.race = RaceModeReader(self, magic)
class RomReader: nothings = ['0xbae9', '0xbaed'] # read the items in the rom items = { # vanilla '0xeed7': {'name': 'ETank'}, '0xeedb': {'name': 'Missile'}, '0xeedf': {'name': 'Super'}, '0xeee3': {'name': 'PowerBomb'}, '0xeee7': {'name': 'Bomb'}, '0xeeeb': {'name': 'Charge'}, '0xeeef': {'name': 'Ice'}, '0xeef3': {'name': 'HiJump'}, '0xeef7': {'name': 'SpeedBooster'}, '0xeefb': {'name': 'Wave'}, '0xeeff': {'name': 'Spazer'}, '0xef03': {'name': 'SpringBall'}, '0xef07': {'name': 'Varia'}, '0xef13': {'name': 'Plasma'}, '0xef17': {'name': 'Grapple'}, '0xef23': {'name': 'Morph'}, '0xef27': {'name': 'Reserve'}, '0xef0b': {'name': 'Gravity'}, '0xef0f': {'name': 'XRayScope'}, '0xef1b': {'name': 'SpaceJump'}, '0xef1f': {'name': 'ScrewAttack'}, # old rando "chozo" items '0xef2b': {'name': 'ETank'}, '0xef2f': {'name': 'Missile'}, '0xef33': {'name': 'Super'}, '0xef37': {'name': 'PowerBomb'}, '0xef3b': {'name': 'Bomb'}, '0xef3f': {'name': 'Charge'}, '0xef43': {'name': 'Ice'}, '0xef47': {'name': 'HiJump'}, '0xef4b': {'name': 'SpeedBooster'}, '0xef4f': {'name': 'Wave'}, '0xef53': {'name': 'Spazer'}, '0xef57': {'name': 'SpringBall'}, '0xef5b': {'name': 'Varia'}, '0xef5f': {'name': 'Gravity'}, '0xef63': {'name': 'XRayScope'}, '0xef67': {'name': 'Plasma'}, '0xef6b': {'name': 'Grapple'}, '0xef6f': {'name': 'SpaceJump'}, '0xef73': {'name': 'ScrewAttack'}, '0xef77': {'name': 'Morph'}, '0xef7b': {'name': 'Reserve'}, # old rando "hidden" items '0xef7f': {'name': 'ETank'}, '0xef83': {'name': 'Missile'}, '0xef87': {'name': 'Super'}, '0xef8b': {'name': 'PowerBomb'}, '0xef8f': {'name': 'Bomb'}, '0xef93': {'name': 'Charge'}, '0xef97': {'name': 'Ice'}, '0xef9b': {'name': 'HiJump'}, '0xef9f': {'name': 'SpeedBooster'}, '0xefa3': {'name': 'Wave'}, '0xefa7': {'name': 'Spazer'}, '0xefab': {'name': 'SpringBall'}, '0xefaf': {'name': 'Varia'}, '0xefb3': {'name': 'Gravity'}, '0xefb7': {'name': 'XRayScope'}, '0xefbb': {'name': 'Plasma'}, '0xefbf': {'name': 'Grapple'}, '0xefc3': {'name': 'SpaceJump'}, '0xefc7': {'name': 'ScrewAttack'}, '0xefcb': {'name': 'Morph'}, '0xefcf': {'name': 'Reserve'}, '0x0': {'name': 'Nothing'}, '0xbae9': {'name': 'Nothing'}, # new visible/chozo Nothing '0xbaed': {'name': 'Nothing'} # new hidden Nothing } patches = { 'startCeres': {'address': 0x7F1F, 'value': 0xB6, 'desc': "Blue Brinstar and Red Tower blue doors"}, 'startLS': {'address': 0x7F17, 'value': 0xB6, 'desc': "Blue Brinstar and Red Tower blue doors"}, 'layout': {'address': 0x21BD80, 'value': 0xD5, 'desc': "Anti soft lock layout modifications"}, 'casual': {'address': 0x22E879, 'value': 0xF8, 'desc': "Switch Blue Brinstar Etank and missile"}, 'gravityNoHeatProtection': {'address': 0x0869dd, 'value': 0x01, 'desc': "Gravity suit heat protection removed"}, 'progressiveSuits': {'address':0x869df, 'value': 0xF0, 'desc': "Progressive suits"}, 'nerfedCharge': {'address':0x83821, 'value': 0x80, 'desc': "Nerfed charge beam from the start of the game"}, # this value works for both DASH and VARIA variants 'variaTweaks': {'address': 0x7CC4D, 'value': 0x37, 'desc': "VARIA tweaks"}, 'area': {'address': 0x788A0, 'value': 0x2B, 'desc': "Area layout modifications"}, 'areaLayout': {'address': 0x252FA7, 'value': 0xF8, 'desc': "Area layout additional modifications"}, 'traverseWreckedShip': {'address': 0x219dbf, 'value': 0xFB, 'desc': "Area layout additional access to east Wrecked Ship"}, 'areaEscape': {'address': 0x20c91, 'value': 0x4C, 'desc': "Area escape randomization"}, 'newGame': {'address': 0x1001d, 'value': 0x22, 'desc': "Custom new game"}, 'nerfedRainbowBeam': {'address': 0x14BA2E, 'value': 0x13, 'desc': 'nerfed rainbow beam'}, 'croc_area': {'address': 0x78ba3, 'value': 0x8c, 'desc': "Crocomire in its own area"}, 'minimizer_bosses': {'address': 0x10F500, 'value': 0xAD, 'desc': "Minimizer"}, 'minimizer_tourian': {'address': 0x7F730, 'value': 0xA9, 'desc': "Fast Tourian"}, 'open_zebetites': {'address': 0x26DF22, 'value': 0xc3, 'desc': "Zebetites without morph"}, 'beam_doors': {'address': 0x226e5, 'value': 0x0D, 'desc': "Beam doors"}, 'red_doors': {'address':0x20560, 'value':0xbd, 'desc': "Red doors open with one Missile and do not react to Super"}, 'rotation': {'address': 0x44DF, 'value': 0xD0, 'desc': "Rotation hack"} } # FIXME shouldn't be here allPatches = { 'AimAnyButton': {'address': 0x175ca, 'value': 0x60, 'vanillaValue': 0xad}, 'animal_enemies': {'address': 0x78418, 'value': 0x3B, 'vanillaValue': 0x48}, # all the modifications from animals are included in animal_enemies... #'animals': {'address': 0x7841D, 'value': 0x8C, 'vanillaValue': 0x18}, 'area_rando_blue_doors': {'address': 0x7823E, 'value': 0x3B, 'vanillaValue': 0x66}, 'area_rando_door_transition': {'address': 0x852BA, 'value': 0x20, 'vanillaValue': 0xad}, 'rando_escape': {'address': 0x15F38, 'value': 0x5C, 'vanillaValue': 0xbd}, 'area_rando_layout_base': {'address': 0x788A0, 'value': 0x2B, 'vanillaValue': 0x26}, 'area_rando_layout': {'address': 0x78666, 'value': 0x62, 'vanillaValue': 0x64}, 'bomb_torizo': {'address': 0x208A7, 'value': 0x20, 'vanillaValue': 0x0d}, 'brinstar_map_room': {'address': 0x784EC, 'value': 0x3B, 'vanillaValue': 0x42}, 'credits_varia': {'address': 0x44B, 'value': 0x5C, 'vanillaValue': 0xa2}, 'dachora': {'address': 0x22A173, 'value': 0xC9, 'vanillaValue': 0xc8}, 'draygonimals': {'address': 0x1ADAC, 'value': 0x60, 'vanillaValue': 0xff}, 'early_super_bridge': {'address': 0x22976B, 'value': 0xC7, 'vanillaValue': 0x43}, 'elevators_doors_speed': {'address': 0x2E9D, 'value': 0x08, 'vanillaValue': 0x04}, 'endingtotals': {'address': 0x208A0, 'value': 0x30, 'vanillaValue': 0x8e}, 'escapimals': {'address': 0x1ADAC, 'value': 0x4D, 'vanillaValue': 0xff}, 'g4_skip': {'address': 0x18C5D, 'value': 0xFE, 'vanillaValue': 0x00}, 'gameend': {'address': 0x18BCC, 'value': 0x20, 'vanillaValue': 0x00}, 'grey_door_animals': {'address': 0x21E186, 'value': 0x06, 'vanillaValue': 0x07}, 'high_jump': {'address': 0x23AA77, 'value': 0x04, 'vanillaValue': 0x05}, 'itemsounds': {'address': 0x16126, 'value': 0x22, 'vanillaValue': 0xa9}, 'ln_chozo_platform': {'address': 0x7CEB0, 'value': 0xC9, 'vanillaValue': 0xc7}, 'ln_chozo_sj_check_disable': {'address': 0x2518F, 'value': 0xEA, 'vanillaValue': 0xad}, 'low_timer': {'address': 0x18BCC, 'value': 0x20, 'vanillaValue': 0x00}, 'max_ammo_display': {'address': 0x19E1, 'value': 0xEA, 'vanillaValue': 0xad}, 'metalimals': {'address': 0x1ADAC, 'value': 0x2B, 'vanillaValue': 0xff}, 'moat': {'address': 0x21BD80, 'value': 0xD5, 'vanillaValue': 0x54}, 'nova_boost_platform': {'address': 0x236CC3, 'value': 0xDF, 'vanillaValue': 0x5f}, 'phantoonimals': {'address': 0x1ADAC, 'value': 0x13, 'vanillaValue': 0xff}, 'rando_speed': {'address': 0x7FDC, 'value': 0xB4, 'vanillaValue': 0x20}, 'red_tower': {'address': 0x2304F7, 'value': 0xC4, 'vanillaValue': 0x44}, 'ridleyimals': {'address': 0x1ADAC, 'value': 0x2E, 'vanillaValue': 0xff}, 'ridley_platform': {'address': 0x246C09, 'value': 0x00, 'vanillaValue': 0x02}, 'seed_display': {'address': 0x16CBB, 'value': 0x20, 'vanillaValue': 0xa2}, 'skip_ceres': {'address': 0x7F17, 'value': 0xB6, 'vanillaValue': 0xff}, 'skip_intro': {'address': 0x7F1F, 'value': 0xB6, 'vanillaValue': 0xff}, 'spazer': {'address': 0x23392B, 'value': 0xC5, 'vanillaValue': 0x45}, 'spinjumprestart': {'address': 0x8763A, 'value': 0xAD, 'vanillaValue': 0xff}, 'spospo_save': {'address': 0x785FC, 'value': 0x3B, 'vanillaValue': 0x03}, 'supermetroid_msu1': {'address': 0xF27, 'value': 0x20, 'vanillaValue': 0x8d}, 'tracking': {'address': 0x10CEA, 'value': 0x4C, 'vanillaValue': 0xee}, 'tracking_buggy_arm_pump': {'address': 0x8EB05, 'value': 0x4C, 'vanillaValue': 0x4D}, 'wake_zebes': {'address': 0x18EB5, 'value': 0xFF, 'vanillaValue': 0x00}, 'ws_etank': {'address': 0x7CC4D, 'value': 0x37, 'vanillaValue': 0x8f}, 'ws_save': {'address': 0x7CEB0, 'value': 0xC9, 'vanillaValue': 0xc7}, 'Removes_Gravity_Suit_heat_protection': {'address': 0x0869dd, 'value': 0x01, 'vanillaValue': 0x20}, 'progressive_suits': {'address': 0x869df, 'value': 0xF0, 'vanillaValue': 0xd0}, 'nerfed_charge': {'address': 0x83821, 'value': 0x80, 'vanillaValue': 0xd0}, 'Mother_Brain_Cutscene_Edits': {'address': 0x148824, 'value': 0x01, 'vanillaValue': 0x40}, 'Suit_acquisition_animation_skip': {'address': 0x020717, 'value': 0xea, 'vanillaValue': 0x22}, 'Fix_Morph_and_Missiles_Room_State': {'address': 0x07e655, 'value': 0xea, 'vanillaValue': 0x89}, 'Fix_heat_damage_speed_echoes_bug': {'address': 0x08b629, 'value': 0x01, 'vanillaValue': 0x00}, 'Disable_GT_Code': {'address': 0x15491c, 'value': 0x80, 'vanillaValue': 0xd0}, 'Disable_Space_Time_select_in_menu': {'address': 0x013175, 'value': 0x01, 'vanillaValue': 0x08}, 'Fix_Morph_Ball_Hidden_Chozo_PLMs': {'address': 0x0268ce, 'value': 0x04, 'vanillaValue': 0x02}, 'Fix_Screw_Attack_selection_in_menu': {'address': 0x0134c5, 'value': 0x0c, 'vanillaValue': 0x0a}, 'No_Music': {'address': 0x278413, 'value': 0x6f, 'vanillaValue': 0xcd}, 'random_music': {'address': 0x10F320, 'value': 0x01, 'vanillaValue': 0xff}, 'fix_suits_selection_in_menu': {'address': 0x13000, 'value': 0x90, 'vanillaValue': 0x80}, 'traverseWreckedShip': {'address': 0x219dbf, 'value': 0xFB, 'vanillaValue': 0xeb}, 'Infinite_Space_Jump': {'address': 0x82493, 'value': 0xEA, 'vanillaValue': 0xf0}, 'refill_before_save': {'address': 0x270C2, 'value': 0x98, 'vanillaValue': 0xff}, 'nerfed_rainbow_beam': {'address': 0x14BA2E, 'value': 0x13, 'vanillaValue': 0x2b}, 'croc_area': {'address': 0x78ba3, 'value': 0x8c, 'vanillaValue': 0x4}, 'area_rando_warp_door': {'address': 0x26425E, 'value': 0x80, 'vanillaValue': 0x70}, 'minimizer_bosses': {'address': 0x10F500, 'value': 0xAD, 'vanillaValue': 0xff}, 'minimizer_tourian': {'address': 0x7F730, 'value': 0xA9, 'vanillaValue': 0xff}, 'open_zebetites': {'address': 0x26DF22, 'value': 0xc3, 'vanillaValue': 0x43}, 'beam_doors': {'address': 0x226e5, 'value': 0x0D, 'vanillaValue': 0xaf}, 'rotation': {'address': 0x44DF, 'value': 0xD0, 'vanillaValue': 0xe0}, 'no_demo': {'address': 0x59F2C, 'value': 0x80, 'vanillaValue': 0xf0}, 'varia_hud': {'address': 0x15EF7, 'value': 0x5C, 'vanillaValue': 0xAE}, 'nothing_item_plm': {'address': 0x23AD1, 'value': 0x24, 'vanillaValue': 0xb9} } @staticmethod def getDefaultPatches(): # called by the isolver in seedless mode # activate only layout patch (the most common one) and blue bt/red tower blue doors ret = {} for patch in RomReader.patches: if patch in ['layout', 'startLS']: ret[RomReader.patches[patch]['address']] = RomReader.patches[patch]['value'] else: ret[RomReader.patches[patch]['address']] = 0xFF # add phantoon door ptr used by boss rando detection doorPtr = getAccessPoint('PhantoonRoomOut').ExitInfo['DoorPtr'] doorPtr = (0x10000 | doorPtr) + 10 ret[doorPtr] = 0 ret[doorPtr+1] = 0 return ret def __init__(self, romFile, magic=None): self.romFile = romFile self.race = None # default to morph ball location self.nothingId = 0x1a self.nothingAddr = 0x786DE if magic is not None: from rom.race_mode import RaceModeReader self.race = RaceModeReader(self, magic) def getItemBytes(self): value1 = int.from_bytes(self.romFile.read(1), byteorder='little') value2 = int.from_bytes(self.romFile.read(1), byteorder='little') return (value1, value2) def getItem(self, address, visibility): # return the hex code of the object at the given address self.romFile.seek(address) # value is in two bytes if self.race is None: (value1, value2) = self.getItemBytes() else: (value1, value2) = self.race.getItemBytes(address) # match itemVisibility with # | Visible -> 0 # | Chozo -> 0x54 (84) # | Hidden -> 0xA8 (168) if visibility == 'Visible': itemCode = hex(value2*256+(value1-0)) elif visibility == 'Chozo': itemCode = hex(value2*256+(value1-84)) elif visibility == 'Hidden': itemCode = hex(value2*256+(value1-168)) else: raise Exception("RomReader: unknown visibility: {}".format(visibility)) # for the new nothing item plm the visibility is: # Visible/Chozo -> 0 # Hidden -> 4 if itemCode not in self.items: if visibility in ['Visible', 'Chozo']: nothingCode = hex(value2*256+(value1-0)) elif visibility == 'Hidden': nothingCode = hex(value2*256+(value1-4)) if nothingCode in self.nothings: itemCode = nothingCode # dessyreqt randomizer make some missiles non existant, detect it self.romFile.seek(address+4) value3 = int.from_bytes(self.romFile.read(1), byteorder='little') if (value3 == self.nothingId and int(itemCode, 16) == 0xeedb and address != self.nothingAddr): return hex(0) else: return itemCode def getMajorsSplit(self): address = 0x17B6C split = chr(self.romFile.readByte(address)) splits = { 'F': 'Full', 'Z': 'Chozo', 'M': 'Major', 'H': 'FullWithHUD', 'S': 'Scavenger' } # default to Full return splits.get(split, 'Full') def loadItems(self, locations): majorsSplit = self.getMajorsSplit() for loc in locations: if loc.isBoss(): # the boss item has the same name as its location, except for mother brain which has a space loc.itemName = loc.Name.replace(' ', '') continue item = self.getItem(loc.Address, loc.Visibility) try: loc.itemName = self.items[item]["name"] except: # race seeds loc.itemName = "Nothing" item = '0x0' return (majorsSplit if majorsSplit != 'FullWithHUD' else 'Full', majorsSplit) # used to read scavenger locs def genLocIdsDict(self, locations): locIdsDict = {} for loc in locations: if loc.isScavenger(): locIdsDict[loc.Id] = loc return locIdsDict def loadScavengerOrder(self, locations): order = [] locIdsDict = self.genLocIdsDict(locations) self.romFile.seek(snes_to_pc(0xA1F5D8)) while True: data = self.romFile.readWord() locId = (data & 0xFF00) >> 8 if locId == 0xFF: break loc = locIdsDict[locId] order.append(loc) # check that there's no nothing in the loc assert loc.itemName != "Nothing", "Nothing detected in scav loc {}".format(loc.Name) return order def loadTransitions(self): # return the transitions rooms = GraphUtils.getRooms() bossTransitions = {} areaTransitions = {} for accessPoint in Logic.accessPoints: if accessPoint.isInternal() == True: continue key = self.getTransition(accessPoint.ExitInfo['DoorPtr']) destAP = rooms[key] if accessPoint.Boss == True or destAP.Boss == True: bossTransitions[accessPoint.Name] = destAP.Name else: areaTransitions[accessPoint.Name] = destAP.Name def removeBiTrans(transitions): # remove bidirectionnal transitions # can't del keys in a dict while iterating it transitionsCopy = copy.copy(transitions) for src in transitionsCopy: if src in transitions: dest = transitions[src] if dest in transitions: if transitions[dest] == src: del transitions[dest] return [(t, transitions[t]) for t in transitions] # get escape transition escapeSrcAP = getAccessPoint('Tourian Escape Room 4 Top Right') key = self.getTransition(escapeSrcAP.ExitInfo['DoorPtr']) # may not be set in plandomizer if key in rooms: escapeDstAP = rooms[key] escapeTransition = [(escapeSrcAP.Name, escapeDstAP.Name)] else: escapeTransition = [] areaTransitions = removeBiTrans(areaTransitions) bossTransitions = removeBiTrans(bossTransitions) return (areaTransitions, bossTransitions, escapeTransition, GraphUtils.hasMixedTransitions(areaTransitions, bossTransitions)) def getTransition(self, doorPtr): # room ptr is in two bytes roomPtr = self.romFile.readWord(0x10000 | doorPtr) direction = self.romFile.readByte((0x10000 | doorPtr) + 3) sx = self.romFile.readByte((0x10000 | doorPtr) + 6) sy = self.romFile.readByte() distanceToSpawn = self.romFile.readWord() if distanceToSpawn == 0: # incompatible transition use samus X/Y instead of direction # as incompatible transition change the value of direction asmAddress = 0x70000 | self.romFile.readWord() offset = 0 b = self.romFile.readByte(asmAddress+3) if b == 0x20: # ignore original door asm ptr call offset += 3 b = self.romFile.readByte(asmAddress+6) if b == 0x20: # ignore exit asm ptr call offset += 3 x = self.romFile.readWord(asmAddress+4+offset) y = self.romFile.readWord(asmAddress+10+offset) return (roomPtr, (sx, sy), (x, y)) else: return (roomPtr, (sx, sy), direction) def patchPresent(self, patchName): value = self.romFile.readByte(self.patches[patchName]['address']) return value == self.patches[patchName]['value'] def getPatches(self): # for display in the solver result = [] for patch in self.patches: if self.patchPresent(patch) == True: result.append(self.patches[patch]['desc']) return result def getRawPatches(self): # for interactive solver result = {} for patchName in self.patches: value = self.romFile.readByte(self.patches[patchName]['address']) result[self.patches[patchName]['address']] = value # add boss detection bytes doorPtr = getAccessPoint('PhantoonRoomOut').ExitInfo['DoorPtr'] doorPtr = (0x10000 | doorPtr) + 10 result[doorPtr] = self.romFile.readByte(doorPtr) result[doorPtr+1] = self.romFile.readByte() return result def getAllPatches(self): # to display in cli solver (for debug use) ret = [] for patch in self.allPatches: value = self.romFile.readByte(self.allPatches[patch]['address']) if value == self.allPatches[patch]["value"]: ret.append(patch) return sorted(ret) def getPlandoAddresses(self): self.romFile.seek(0x2F6000) addresses = [] for i in range(128): address = self.romFile.readWord() if address == 0xFFFF: break else: addresses.append(address) return addresses def getPlandoTransitions(self, maxTransitions): self.romFile.seek(0x2F6100) addresses = [] for i in range(maxTransitions): srcDoorPtr = self.romFile.readWord() destDoorPtr = self.romFile.readWord() if srcDoorPtr == 0xFFFF and destDoorPtr == 0xFFFF: break else: addresses.append((srcDoorPtr, destDoorPtr)) return addresses def decompress(self, address): # return (size of compressed data, decompressed data) return Compressor().decompress(self.romFile, address) def getEscapeTimer(self): second = self.romFile.readByte(0x1E21) minute = self.romFile.readByte() second = int(second / 16)*10 + second%16 minute = int(minute / 16)*10 + minute%16 return "{:02d}:{:02d}".format(minute, second) def readNothingId(self): address = 0x17B6D value = self.romFile.readByte(address) if value != 0xff: self.nothingId = value # find the associated location to get its address for loc in Logic.locations: if loc.Id == self.nothingId: self.nothingAddr = 0x70000 | loc.Address break def readLogic(self): if self.patchPresent('rotation'): return 'rotation' else: return 'vanilla' def getStartAP(self): address = 0x10F200 value = self.romFile.readWord(address) startLocation = 'Landing Site' startArea = 'Crateria Landing Site' startPatches = [] for ap in Logic.accessPoints: if ap.Start is not None and 'spawn' in ap.Start and ap.Start['spawn'] == value: startLocation = ap.Name startArea = ap.Start['solveArea'] if 'patches' in ap.Start: startPatches = ap.Start['patches'] break return (startLocation, startArea, startPatches) # go read all location IDs for item split. used to get major/chozo locs in non standard start def getLocationsIds(self): ret = [] for area,addr in locIdsByAreaAddresses.items(): self.romFile.seek(addr) while True: idByte = self.romFile.readByte() if idByte == 0xff: break ret.append(idByte) return ret