From 06285c551dac97532357193345d035922390877b Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Fri, 15 Jul 2022 04:03:21 +0000 Subject: [PATCH] SM: remove hard-coded ROM address writes --- worlds/sm/Rom.py | 47 +++++++++- worlds/sm/__init__.py | 168 ++++++++++++++++++++++------------ worlds/sm/data/sourceinfo.txt | 3 + 3 files changed, 160 insertions(+), 58 deletions(-) create mode 100644 worlds/sm/data/sourceinfo.txt diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index a01fcbe3a89f..02622ee20197 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -1,6 +1,7 @@ import hashlib import os +import json import Utils from Patch import read_rom, APDeltaPatch @@ -17,7 +18,6 @@ class SMDeltaPatch(APDeltaPatch): def get_source_data(cls) -> bytes: return get_base_rom_bytes() - def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: @@ -40,3 +40,48 @@ def get_base_rom_path(file_name: str = "") -> str: if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name + +def get_sm_symbols(sym_json_path) -> dict: + with open(sym_json_path, "r") as stream: + symbols = json.load(stream) + symboltable = {} + for name, sixdigitaddr in symbols.items(): + (bank, addr_within_bank) = sixdigitaddr.split(":") + bank = int(bank, 16) + addr_within_bank = int(addr_within_bank, 16) + # categorize addresses using snes lorom mapping: + # (reference: https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map) + if (bank >= 0x70 and bank <= 0x7d): + offset_within_rom_file = None + # SRAM is not continuous, but callers may want it in continuous terms + # SRAM @ data bank $70-$7D, addr_within_bank $0000-$7FFF + # + # symbol aka snes offestwithincontinuousSRAM + # --------------- -------------------------- + # $70:0000-7FFF -> 0x0000- 7FFF + # $71:0000-7FFF -> 0x8000- FFFF + # $72:0000-7FFF -> 0x10000-17FFF + # etc... + offset_within_continuous_sram = (bank - 0x70) * 0x8000 + addr_within_bank + offset_within_wram = None + elif bank == 0x7e or bank == 0x7f or (bank == 0x00 and addr_within_bank <= 0x1fff): + offset_within_rom_file = None + offset_within_continuous_sram = None + offset_within_wram = addr_within_bank + if bank == 0x7f: + offset_within_wram += 0x10000 + elif bank >= 0x80: + offset_within_rom_file = ((bank - 0x80) * 0x8000) + (addr_within_bank % 0x8000) + offset_within_continuous_sram = None + offset_within_wram = None + else: + offset_within_rom_file = None + offset_within_continuous_sram = None + offset_within_wram = None + symboltable[name] = {"bank": bank, + "addr_within_bank": addr_within_bank, + "offset_within_rom_file": offset_within_rom_file, + "offset_within_continuous_sram": offset_within_continuous_sram, + "offset_within_wram": offset_within_wram + } + return symboltable diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 899f0278f28d..5f4a8dfa09e6 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -16,7 +16,7 @@ from .Regions import create_regions from .Rules import set_rules, add_entrance_rule from .Options import sm_options -from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch +from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch, get_sm_symbols import Utils from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, RegionType, CollectionState, Tutorial @@ -284,11 +284,16 @@ def APPatchRom(self, romPatcher): Utils.is_frozen() else Utils.local_path( "worlds", "sm", "data", "SMBasepatch_prebuilt", "variapatches.ips")) - multiWorldLocations = {} - multiWorldItems = {} + symbols = get_sm_symbols( + Utils.local_path("lib", "worlds", "sm", "data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json") if + Utils.is_frozen() else + Utils.local_path( "worlds", "sm", "data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json")) + multiWorldLocations = [] + multiWorldItems = [] idx = 0 self.playerIDMap = {} playerIDCount = 0 # 0 is for "Archipelago" server + vanillaItemTypesCount = 21 for itemLoc in self.world.get_locations(): romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0 if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None: @@ -296,7 +301,9 @@ def APPatchRom(self, romPatcher): itemId = ItemManager.Items[itemLoc.item.type].Id else: itemId = ItemManager.Items['ArchipelagoItem'].Id + idx - multiWorldItems[0x029EA3 + idx*64] = self.convertToROMItemName(itemLoc.item.name) + multiWorldItems.append({"sym": symbols["message_item_names"], + "offset": (vanillaItemTypesCount + idx)*64, + "values": self.convertToROMItemName(itemLoc.item.name)}) idx += 1 if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): @@ -307,85 +314,131 @@ def APPatchRom(self, romPatcher): [w2, w3] = self.getWordArray(itemId) [w4, w5] = self.getWordArray(romPlayerID) [w6, w7] = self.getWordArray(0 if itemLoc.item.advancement else 1) - multiWorldLocations[0x1C6000 + locationsDict[itemLoc.name].Id*8] = [w0, w1, w2, w3, w4, w5, w6, w7] + multiWorldLocations.append({"sym": symbols["rando_item_table"], + "offset": locationsDict[itemLoc.name].Id*8, + "values": [w0, w1, w2, w3, w4, w5, w6, w7]}) if itemLoc.item.player == self.player: if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): playerIDCount += 1 self.playerIDMap[romPlayerID] = playerIDCount - itemSprites = ["off_world_prog_item.bin", "off_world_item.bin"] + itemSprites = [{"fileName": "off_world_prog_item.bin", + "paletteSymbolName": "prog_item_eight_palette_indices", + "dataSymbolName": "offworld_graphics_data_progression_item"}, + + {"fileName": "off_world_item.bin", + "paletteSymbolName": "nonprog_item_eight_palette_indices", + "dataSymbolName": "offworld_graphics_data_item"}] idx = 0 - offworldSprites = {} - for fileName in itemSprites: - with open(Utils.local_path("lib", "worlds", "sm", "data", "custom_sprite", fileName) if Utils.is_frozen() else Utils.local_path("worlds", "sm", "data", "custom_sprite", fileName), 'rb') as stream: + offworldSprites = [] + for itemSprite in itemSprites: + with open(Utils.local_path("lib", "worlds", "sm", "data", "custom_sprite", itemSprite["fileName"]) if + Utils.is_frozen() else + Utils.local_path( "worlds", "sm", "data", "custom_sprite", itemSprite["fileName"]), "rb") as stream: buffer = bytearray(stream.read()) - offworldSprites[0x02787c + 10*(idx) + 2] = buffer[0:8] - offworldSprites[0x049100 + idx*256] = buffer[8:264] + offworldSprites.append({"sym": symbols[itemSprite["paletteSymbolName"]], + "offset": 0, + "values": buffer[0:8]}) + offworldSprites.append({"sym": symbols[itemSprite["dataSymbolName"]], + "offset": idx*256, + "values": buffer[8:264]}) idx += 1 - - openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]} - deathLink = {0x277f04: [self.world.death_link[self.player].value]} - remoteItem = {0x277f06: self.getWordArray(0b001 + (0b010 if self.remote_items else 0b000))} - ownPlayerId = {0x277f08: self.getWordArray(self.player)} - - playerNames = {} - playerNameIDMap = {} - playerNames[0x1C5000] = "Archipelago".upper().center(16).encode() - playerNameIDMap[0x1C5800] = self.getWordArray(0) + deathLink = [{"sym": symbols["config_deathlink"], + "offset": 0, + "values": [self.world.death_link[self.player].value]}] + remoteItem = [{"sym": symbols["config_remote_items"], + "offset": 0, + "values": self.getWordArray(0b001 + (0b010 if self.remote_items else 0b000))}] + ownPlayerId = [{"sym": symbols["config_player_id"], + "offset": 0, + "values": self.getWordArray(self.player)}] + + playerNames = [] + playerNameIDMap = [] + playerNames.append({"sym": symbols["rando_player_table"], + "offset": 0, + "values": "Archipelago".upper().center(16).encode()}) + playerNameIDMap.append({"sym": symbols["rando_player_id_table"], + "offset": 0, + "values": self.getWordArray(0)}) for key,value in self.playerIDMap.items(): - playerNames[0x1C5000 + value * 16] = self.world.player_name[key][:16].upper().center(16).encode() - playerNameIDMap[0x1C5800 + value * 2] = self.getWordArray(key) + playerNames.append({"sym": symbols["rando_player_table"], + "offset": value * 16, + "values": self.world.player_name[key][:16].upper().center(16).encode()}) + playerNameIDMap.append({"sym": symbols["rando_player_id_table"], + "offset": value * 2, + "values": self.getWordArray(key)}) patchDict = { 'MultiWorldLocations': multiWorldLocations, 'MultiWorldItems': multiWorldItems, 'offworldSprites': offworldSprites, - 'openTourianGreyDoors': openTourianGreyDoors, 'deathLink': deathLink, 'remoteItem': remoteItem, 'ownPlayerId': ownPlayerId, 'PlayerName': playerNames, 'PlayerNameIDMap': playerNameIDMap} + + # convert an array of symbolic byte_edit dicts like {"sym": symobj, "offset": 0, "values": [1, 0]} + # to a single rom patch dict like {0x438c: [1, 0], 0xa4a5: [0, 0, 0]} which varia will understand and apply + def resolve_symbols_to_file_offset_based_dict(byte_edits_arr) -> dict: + this_patch_as_dict = {} + for byte_edit in byte_edits_arr: + offset_within_rom_file = byte_edit["sym"]["offset_within_rom_file"] + byte_edit["offset"] + this_patch_as_dict[offset_within_rom_file] = byte_edit["values"] + return this_patch_as_dict + + for patchname, byte_edits_arr in patchDict.items(): + patchDict[patchname] = resolve_symbols_to_file_offset_based_dict(byte_edits_arr) + romPatcher.applyIPSPatchDict(patchDict) + openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]} + romPatcher.applyIPSPatchDict({'openTourianGreyDoors': openTourianGreyDoors}) + + # set rom name # 21 bytes from Main import __version__ - self.romName = bytearray(f'SM{__version__.replace(".", "")[0:3]}_{self.player}_{self.world.seed:11}\0', 'utf8')[:21] + self.romName = bytearray(f'SM{__version__.replace(".", "")[0:3]}_{self.player}_{self.world.seed:11}', 'utf8')[:21] self.romName.extend([0] * (21 - len(self.romName))) # clients should read from 0x7FC0, the location of the rom title in the SNES header. # duplicative ROM name at 0x1C4F00 is still written here for now, since people with archipelago pre-0.3.0 client installed will still be depending on this location for connecting to SM romPatcher.applyIPSPatch('ROMName', { 'ROMName': {0x1C4F00 : self.romName, 0x007FC0 : self.romName} }) - startItemROMAddressBase = 0x1C4800 + startItemROMAddressBase = symbols["start_item_data_major"]["offset_within_rom_file"] # array for each item: - # offset within ROM table 'start_item_data_major' of this item's info (starting status) - # item bitmask or amount per pickup, - # offset within ROM table 'start_item_data_major' of this item's info (starting maximum/starting collected items) - startItemROMDict = {'ETank': [0x8, 0x64, 0xA], - 'Missile': [0xC, 0x5, 0xE], - 'Super': [0x10, 0x5, 0x12], - 'PowerBomb': [0x14, 0x5, 0x16], - 'Reserve': [0x1A, 0x64, 0x18], - 'Morph': [0x2, 0x4, 0x0], - 'Bomb': [0x3, 0x10, 0x1], - 'SpringBall': [0x2, 0x2, 0x0], - 'HiJump': [0x3, 0x1, 0x1], - 'Varia': [0x2, 0x1, 0x0], - 'Gravity': [0x2, 0x20, 0x0], - 'SpeedBooster': [0x3, 0x20, 0x1], - 'SpaceJump': [0x3, 0x2, 0x1], - 'ScrewAttack': [0x2, 0x8, 0x0], - 'Charge': [0x7, 0x10, 0x5], - 'Ice': [0x6, 0x2, 0x4], - 'Wave': [0x6, 0x1, 0x4], - 'Spazer': [0x6, 0x4, 0x4], - 'Plasma': [0x6, 0x8, 0x4], - 'Grapple': [0x3, 0x40, 0x1], - 'XRayScope': [0x3, 0x80, 0x1] + # offset within ROM table "start_item_data_major" of this item"s info (starting status) + # item bitmask or amount per pickup (BVOB = base value or bitmask), + # offset within ROM table "start_item_data_major" of this item"s info (starting maximum/starting collected items) + # current BVOB max + # ------- ---- --- + startItemROMDict = {"ETank": [ 0x8, 0x64, 0xA], + "Missile": [ 0xC, 0x5, 0xE], + "Super": [0x10, 0x5, 0x12], + "PowerBomb": [0x14, 0x5, 0x16], + "Reserve": [0x1A, 0x64, 0x18], + "Morph": [ 0x2, 0x4, 0x0], + "Bomb": [ 0x3, 0x10, 0x1], + "SpringBall": [ 0x2, 0x2, 0x0], + "HiJump": [ 0x3, 0x1, 0x1], + "Varia": [ 0x2, 0x1, 0x0], + "Gravity": [ 0x2, 0x20, 0x0], + "SpeedBooster": [ 0x3, 0x20, 0x1], + "SpaceJump": [ 0x3, 0x2, 0x1], + "ScrewAttack": [ 0x2, 0x8, 0x0], + "Charge": [ 0x7, 0x10, 0x5], + "Ice": [ 0x6, 0x2, 0x4], + "Wave": [ 0x6, 0x1, 0x4], + "Spazer": [ 0x6, 0x4, 0x4], + "Plasma": [ 0x6, 0x8, 0x4], + "Grapple": [ 0x3, 0x40, 0x1], + "XRayScope": [ 0x3, 0x80, 0x1] + + # BVOB = base value or bitmask } mergedData = {} hasETank = False @@ -393,10 +446,10 @@ def APPatchRom(self, romPatcher): hasPlasma = False for startItem in self.startItems: item = startItem.Type - if item == 'ETank': hasETank = True - if item == 'Spazer': hasSpazer = True - if item == 'Plasma': hasPlasma = True - if (item in ['ETank', 'Missile', 'Super', 'PowerBomb', 'Reserve']): + if item == "ETank": hasETank = True + if item == "Spazer": hasSpazer = True + if item == "Plasma": hasPlasma = True + if (item in ["ETank", "Missile", "Super", "PowerBomb", "Reserve"]): (currentValue, amountPerItem, maxValue) = startItemROMDict[item] if (startItemROMAddressBase + currentValue) in mergedData: mergedData[startItemROMAddressBase + currentValue] += amountPerItem @@ -431,13 +484,14 @@ def APPatchRom(self, romPatcher): mergedData[key] = [value] - startItemPatch = { 'startItemPatch': mergedData } - romPatcher.applyIPSPatch('startItemPatch', startItemPatch) + startItemPatch = { "startItemPatch": mergedData } + romPatcher.applyIPSPatch("startItemPatch", startItemPatch) + # commit all the changes we've made here to the ROM romPatcher.commitIPS() itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.world.get_locations() if itemLoc.player == self.player] - romPatcher.writeItemsLocs(itemLocs) + romPatcher.writeItemsLocs(itemLocs) itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player] progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player and itemLoc.item.advancement == True] diff --git a/worlds/sm/data/sourceinfo.txt b/worlds/sm/data/sourceinfo.txt new file mode 100644 index 000000000000..8facb6b249cc --- /dev/null +++ b/worlds/sm/data/sourceinfo.txt @@ -0,0 +1,3 @@ +SMBasepatch_prebuilt: +- comes exactly from build/vanilla/ directory of https://github.com/lordlou/SMBasepatch +- keep it in sync with the basepatch repo; do not modify the contents in this repo alone!