Skip to content

Commit

Permalink
SM: remove hard-coded ROM address writes
Browse files Browse the repository at this point in the history
  • Loading branch information
strotlog committed Jul 18, 2022
1 parent b215f31 commit 06285c5
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 58 deletions.
47 changes: 46 additions & 1 deletion worlds/sm/Rom.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hashlib
import os

import json
import Utils
from Patch import read_rom, APDeltaPatch

Expand All @@ -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:
Expand All @@ -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
168 changes: 111 additions & 57 deletions worlds/sm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -284,19 +284,26 @@ 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:
if itemLoc.item.type in ItemManager.Items:
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()):
Expand All @@ -307,96 +314,142 @@ 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
hasSpazer = False
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
Expand Down Expand Up @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions worlds/sm/data/sourceinfo.txt
Original file line number Diff line number Diff line change
@@ -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!

0 comments on commit 06285c5

Please sign in to comment.