SM: smoother co-op, basepatch internal improvements (#793)

* SM: remote touch instantly, pull ips refactor and symbols

* SM: remove hard-coded ROM address writes

* SM: Full length player table, incl. receive-only player ids

+ apply PR feedback (correct graphic offset, readable data file paths)
This commit is contained in:
strotlog
2022-07-26 00:43:39 -07:00
committed by GitHub
parent a0482cf27e
commit 1e366ff66f
9 changed files with 1027 additions and 81 deletions

View File

@@ -16,7 +16,7 @@ from .Items import lookup_name_to_id as items_lookup_name_to_id
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
@@ -201,10 +201,7 @@ class SMWorld(World):
create_locations(self, self.player)
create_regions(self, self.world, self.player)
def getWord(self, w):
return (w & 0x00FF, (w & 0xFF00) >> 8)
def getWordArray(self, w):
def getWordArray(self, w): # little-endian convert a 16-bit number to an array of numbers <= 255 each
return [w & 0x00FF, (w & 0xFF00) >> 8]
# used for remote location Credits Spoiler of local items
@@ -269,109 +266,175 @@ class SMWorld(World):
itemName = "___" + itemName + "___"
for char in itemName:
(w0, w1) = self.getWord(charMap.get(char, 0x3C4E))
[w0, w1] = self.getWordArray(charMap.get(char, 0x3C4E))
data.append(w0)
data.append(w1)
return data
def APPatchRom(self, romPatcher):
multiWorldLocations = {}
multiWorldItems = {}
# first apply the sm multiworld code patch named 'basepatch' (also has empty tables that we'll overwrite),
# + apply some patches from varia that we want to be always-on.
# basepatch and variapatches are both generated from https://github.com/lordlou/SMBasepatch
romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__),
"data", "SMBasepatch_prebuilt", "multiworld-basepatch.ips"))
romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__),
"data", "SMBasepatch_prebuilt", "variapatches.ips"))
symbols = get_sm_symbols(os.path.join(os.path.dirname(__file__),
"data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json"))
multiWorldLocations = []
multiWorldItems = []
idx = 0
self.playerIDMap = {}
playerIDCount = 0 # 0 is for "Archipelago" server
playerIDCount = 0 # 0 is for "Archipelago" server; highest possible = 200 (201 entries)
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:
# this SM world can find this item: write full item data to tables and assign player data for writing
romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0
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()):
playerIDCount += 1
self.playerIDMap[romPlayerID] = playerIDCount
(w0, w1) = self.getWord(0 if itemLoc.item.player == self.player else 1)
(w2, w3) = self.getWord(itemId)
(w4, w5) = self.getWord(romPlayerID)
(w6, w7) = self.getWord(0 if itemLoc.item.advancement else 1)
multiWorldLocations[0x1C6000 + locationsDict[itemLoc.name].Id*8] = [w0, w1, w2, w3, w4, w5, w6, w7]
[w0, w1] = self.getWordArray(0 if itemLoc.item.player == self.player else 1)
[w2, w3] = self.getWordArray(itemId)
[w4, w5] = self.getWordArray(romPlayerID)
[w6, w7] = self.getWordArray(0 if itemLoc.item.advancement else 1)
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:
elif itemLoc.item.player == self.player:
# this SM world owns the item: so in case the sending player might not have anything placed in this
# world to receive from it, assign them space in playerIDMap so that the ROM can display their name
# (SM item name not needed, as SM item type id will be in the message they send to this world live)
romPlayerID = itemLoc.player if itemLoc.player <= ROM_PLAYER_LIMIT else 0
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(os.path.join(os.path.dirname(__file__), "data", "custom_sprite", itemSprite["fileName"]), 'rb') as stream:
buffer = bytearray(stream.read())
offworldSprites[0x027882 + 10*(21 + 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": 0,
"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))}
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[0x1C5000] = "Archipelago".upper().center(16).encode()
playerNameIDMap[0x1C5800] = self.getWordArray(0)
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 = 0x2FD8B9
startItemROMAddressBase = symbols["start_item_data_major"]["offset_within_rom_file"]
# current, base value or bitmask, max, base value or bitmask
startItemROMDict = {'ETank': [0x8, 0x64, 0xA, 0x64],
'Missile': [0xC, 0x5, 0xE, 0x5],
'Super': [0x10, 0x5, 0x12, 0x5],
'PowerBomb': [0x14, 0x5, 0x16, 0x5],
'Reserve': [0x1A, 0x64, 0x18, 0x64],
'Morph': [0x2, 0x4, 0x0, 0x4],
'Bomb': [0x3, 0x10, 0x1, 0x10],
'SpringBall': [0x2, 0x2, 0x0, 0x2],
'HiJump': [0x3, 0x1, 0x1, 0x1],
'Varia': [0x2, 0x1, 0x0, 0x1],
'Gravity': [0x2, 0x20, 0x0, 0x20],
'SpeedBooster': [0x3, 0x20, 0x1, 0x20],
'SpaceJump': [0x3, 0x2, 0x1, 0x2],
'ScrewAttack': [0x2, 0x8, 0x0, 0x8],
'Charge': [0x7, 0x10, 0x5, 0x10],
'Ice': [0x6, 0x2, 0x4, 0x2],
'Wave': [0x6, 0x1, 0x4, 0x1],
'Spazer': [0x6, 0x4, 0x4, 0x4],
'Plasma': [0x6, 0x8, 0x4, 0x8],
'Grapple': [0x3, 0x40, 0x1, 0x40],
'XRayScope': [0x3, 0x80, 0x1, 0x80]
# 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 (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
@@ -379,48 +442,52 @@ class SMWorld(World):
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']):
(currentValue, currentBase, maxValue, maxBase) = startItemROMDict[item]
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] += currentBase
mergedData[startItemROMAddressBase + maxValue] += maxBase
mergedData[startItemROMAddressBase + currentValue] += amountPerItem
mergedData[startItemROMAddressBase + maxValue] += amountPerItem
else:
mergedData[startItemROMAddressBase + currentValue] = currentBase
mergedData[startItemROMAddressBase + maxValue] = maxBase
mergedData[startItemROMAddressBase + currentValue] = amountPerItem
mergedData[startItemROMAddressBase + maxValue] = amountPerItem
else:
(collected, currentBitmask, equipped, maxBitmask) = startItemROMDict[item]
(collected, bitmask, equipped) = startItemROMDict[item]
if (startItemROMAddressBase + collected) in mergedData:
mergedData[startItemROMAddressBase + collected] |= currentBitmask
mergedData[startItemROMAddressBase + equipped] |= maxBitmask
mergedData[startItemROMAddressBase + collected] |= bitmask
mergedData[startItemROMAddressBase + equipped] |= bitmask
else:
mergedData[startItemROMAddressBase + collected] = currentBitmask
mergedData[startItemROMAddressBase + equipped] = maxBitmask
mergedData[startItemROMAddressBase + collected] = bitmask
mergedData[startItemROMAddressBase + equipped] = bitmask
if hasETank:
# we are overwriting the starting energy, so add up the E from 99 (normal starting energy) rather than from 0
mergedData[startItemROMAddressBase + 0x8] += 99
mergedData[startItemROMAddressBase + 0xA] += 99
if hasSpazer and hasPlasma:
# de-equip spazer.
# otherwise, firing the unintended spazer+plasma combo would cause massive game glitches and crashes
mergedData[startItemROMAddressBase + 0x4] &= ~0x4
for key, value in mergedData.items():
if (key - startItemROMAddressBase > 7):
(w0, w1) = self.getWord(value)
[w0, w1] = self.getWordArray(value)
mergedData[key] = [w0, w1]
else:
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]