From 655f287d423cda46fa7a00671d0d6849550d3220 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Mon, 31 Oct 2022 22:42:11 -0700 Subject: [PATCH] SM: Fix unobtainable items in remote items+item links combo (#1151) * SM: fix using item links together with remote items * SM: write 0 index for excess player ids * some style and minor fixes (strotlog/Archipelago#1) * more typing in SM patching Co-authored-by: Doug Hoskisson --- worlds/sm/Client.py | 4 +- worlds/sm/Rom.py | 4 +- worlds/sm/__init__.py | 151 +-- .../multiworld-basepatch.ips | Bin 18193 -> 18658 bytes .../data/SMBasepatch_prebuilt/multiworld.sym | 924 ++++++++++-------- .../sm-basepatch-symbols.json | 83 +- 6 files changed, 663 insertions(+), 503 deletions(-) diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index 190ce29ecc..16aea935c0 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -4,7 +4,7 @@ import time from NetUtils import ClientStatus, color from worlds.AutoSNIClient import SNIClient -from .Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT +from .Rom import SM_ROM_MAX_PLAYERID snes_logger = logging.getLogger("SNES") @@ -143,7 +143,7 @@ class SMSNIClient(SNIClient): else: location_id = 0x00 #backward compat - player_id = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0 + player_id = item.player if item.player <= SM_ROM_MAX_PLAYERID else 0 snes_buffered_write(ctx, SM_RECV_QUEUE_START + item_out_ptr * 4, bytes( [player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, location_id & 0xFF])) item_out_ptr += 1 diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index e5f5bc7a37..67c0780dbf 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -7,8 +7,8 @@ from Utils import read_snes_rom from worlds.Files import APDeltaPatch SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' -ROM_PLAYER_LIMIT = 65535 # max archipelago player ID. note, SM ROM itself will only store 201 names+ids max - +SM_ROM_MAX_PLAYERID = 65535 +SM_ROM_PLAYERDATA_COUNT = 202 class SMDeltaPatch(APDeltaPatch): hash = SMJUHASH diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 76f39b75a7..1cb5932592 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -5,7 +5,7 @@ import copy import os import threading import base64 -from typing import Set, TextIO +from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils @@ -15,7 +15,7 @@ from .Regions import create_regions from .Rules import set_rules, add_entrance_rule from .Options import sm_options from .Client import SMSNIClient -from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch, get_sm_symbols +from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols import Utils from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, RegionType, CollectionState, Tutorial @@ -67,6 +67,13 @@ class SMWeb(WebWorld): ["Farrak Kilhn"] )] + +class ByteEdit(TypedDict): + sym: Dict[str, Any] + offset: int + values: Iterable[int] + + locations_start_id = 82000 items_start_id = 83000 @@ -201,7 +208,8 @@ class SMWorld(World): create_locations(self, self.player) create_regions(self, self.multiworld, self.player) - def getWordArray(self, w): # little-endian convert a 16-bit number to an array of numbers <= 255 each + def getWordArray(self, w: int) -> List[int]: + """ 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 @@ -281,48 +289,87 @@ class SMWorld(World): "data", "SMBasepatch_prebuilt", "variapatches.ips")) def APPostPatchRom(self, romPatcher): - symbols = get_sm_symbols(os.path.join(os.path.dirname(__file__), + symbols = get_sm_symbols(os.path.join(os.path.dirname(__file__), "data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json")) - multiWorldLocations = [] - multiWorldItems = [] + + # gather all player ids and names relevant to this rom, then write player name and player id data tables + playerIdSet: Set[int] = {0} # 0 is for "Archipelago" server + for itemLoc in self.multiworld.get_locations(): + assert itemLoc.item, f"World of player '{self.multiworld.player_name[itemLoc.player]}' has a loc.item " + \ + f"that is {itemLoc.item} during generate_output" + # add each playerid who has a location containing an item to send to us *or* to an item_link we're part of + if itemLoc.item.player == self.player or \ + (itemLoc.item.player in self.multiworld.groups and + self.player in self.multiworld.groups[itemLoc.item.player]['players']): + playerIdSet |= {itemLoc.player} + # add each playerid, including item link ids, that we'll be sending items to + if itemLoc.player == self.player: + playerIdSet |= {itemLoc.item.player} + if len(playerIdSet) > SM_ROM_PLAYERDATA_COUNT: + # max 202 entries, but it's possible for item links to add enough replacement items for us, that are placed + # in worlds that otherwise have no relation to us, that the 2*location count limit is exceeded + logger.warning("SM is interacting with too many players to fit in ROM. " + f"Removing the highest {len(playerIdSet) - SM_ROM_PLAYERDATA_COUNT} ids to fit") + playerIdSet = set(sorted(playerIdSet)[:SM_ROM_PLAYERDATA_COUNT]) + otherPlayerIndex: Dict[int, int] = {} # ap player id -> rom-local player index + playerNameData: List[ByteEdit] = [] + playerIdData: List[ByteEdit] = [] + # sort all player data by player id so that the game can look up a player's data reasonably quickly when + # the client sends an ap playerid to the game + for i, playerid in enumerate(sorted(playerIdSet)): + playername = self.multiworld.player_name[playerid] if playerid != 0 else "Archipelago" + playerIdForRom = playerid + if playerid > SM_ROM_MAX_PLAYERID: + # note, playerIdForRom = 0 is not unique so the game cannot look it up. + # instead it will display the player received-from as "Archipelago" + playerIdForRom = 0 + if playerid == self.player: + raise Exception(f"SM rom cannot fit enough bits to represent self player id {playerid}") + else: + logger.warning(f"SM rom cannot fit enough bits to represent player id {playerid}, setting to 0 in rom") + otherPlayerIndex[playerid] = i + playerNameData.append({"sym": symbols["rando_player_name_table"], + "offset": i * 16, + "values": playername[:16].upper().center(16).encode()}) + playerIdData.append({"sym": symbols["rando_player_id_table"], + "offset": i * 2, + "values": self.getWordArray(playerIdForRom)}) + + multiWorldLocations: List[ByteEdit] = [] + multiWorldItems: List[ByteEdit] = [] idx = 0 - self.playerIDMap = {} - playerIDCount = 0 # 0 is for "Archipelago" server; highest possible = 200 (201 entries) vanillaItemTypesCount = 21 for itemLoc in self.multiworld.get_locations(): 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 + # item to place in this SM world: write full item data to tables if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items: itemId = ItemManager.Items[itemLoc.item.type].Id else: - itemId = ItemManager.Items['ArchipelagoItem'].Id + idx + itemId = ItemManager.Items["ArchipelagoItem"].Id + idx 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 + if itemLoc.item.player == self.player: + itemDestinationType = 0 # dest type 0 means 'regular old SM item' per itemtable.asm + elif itemLoc.item.player in self.multiworld.groups and \ + self.player in self.multiworld.groups[itemLoc.item.player]['players']: + # dest type 2 means 'SM item link item that sends to the current player and others' + # per itemtable.asm (groups are synonymous with item_links, currently) + itemDestinationType = 2 + else: + itemDestinationType = 1 # dest type 1 means 'item for entirely someone else' per itemtable.asm - [w0, w1] = self.getWordArray(0 if itemLoc.item.player == self.player else 1) + [w0, w1] = self.getWordArray(itemDestinationType) [w2, w3] = self.getWordArray(itemId) - [w4, w5] = self.getWordArray(romPlayerID) + [w4, w5] = self.getWordArray(otherPlayerIndex[itemLoc.item.player] if itemLoc.item.player in + otherPlayerIndex else 0) [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]}) - 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 = [{"fileName": "off_world_prog_item.bin", "paletteSymbolName": "prog_item_eight_palette_indices", "dataSymbolName": "offworld_graphics_data_progression_item"}, @@ -331,7 +378,7 @@ class SMWorld(World): "paletteSymbolName": "nonprog_item_eight_palette_indices", "dataSymbolName": "offworld_graphics_data_item"}] idx = 0 - offworldSprites = [] + offworldSprites: List[ByteEdit] = [] 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()) @@ -343,31 +390,21 @@ class SMWorld(World): "values": buffer[8:264]}) idx += 1 - deathLink = [{"sym": symbols["config_deathlink"], - "offset": 0, - "values": [self.multiworld.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.append({"sym": symbols["rando_player_table"], - "offset": value * 16, - "values": self.multiworld.player_name[key][:16].upper().center(16).encode()}) - playerNameIDMap.append({"sym": symbols["rando_player_id_table"], - "offset": value * 2, - "values": self.getWordArray(key)}) + deathLink: List[ByteEdit] = [{ + "sym": symbols["config_deathlink"], + "offset": 0, + "values": [self.multiworld.death_link[self.player].value] + }] + remoteItem: List[ByteEdit] = [{ + "sym": symbols["config_remote_items"], + "offset": 0, + "values": self.getWordArray(0b001 + (0b010 if self.remote_items else 0b000)) + }] + ownPlayerId: List[ByteEdit] = [{ + "sym": symbols["config_player_id"], + "offset": 0, + "values": self.getWordArray(self.player) + }] patchDict = { 'MultiWorldLocations': multiWorldLocations, 'MultiWorldItems': multiWorldItems, @@ -375,15 +412,15 @@ class SMWorld(World): 'deathLink': deathLink, 'remoteItem': remoteItem, 'ownPlayerId': ownPlayerId, - 'PlayerName': playerNames, - 'PlayerNameIDMap': playerNameIDMap} + 'playerNameData': playerNameData, + 'playerIdData': playerIdData} # 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 = {} + def resolve_symbols_to_file_offset_based_dict(byte_edits_arr: List[ByteEdit]) -> Dict[int, Iterable[int]]: + this_patch_as_dict: Dict[int, Iterable[int]] = {} for byte_edit in byte_edits_arr: - offset_within_rom_file = byte_edit["sym"]["offset_within_rom_file"] + byte_edit["offset"] + offset_within_rom_file: int = 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 @@ -499,7 +536,7 @@ class SMWorld(World): itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.multiworld.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.multiworld.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.multiworld.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.multiworld.get_locations() if itemLoc.item.player == self.player and itemLoc.item.advancement == True] - # progItemLocs = [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 and itemLoc.item.player == self.player and itemLoc.item.advancement == True] + # progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.multiworld.get_locations() if itemLoc.player == self.player and itemLoc.item.player == self.player and itemLoc.item.advancement == True] # romPatcher.writeSplitLocs(self.variaRando.args.majorsSplit, itemLocs, progItemLocs) romPatcher.writeSpoiler(itemLocs, progItemLocs) diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips index f8fba9b0cfba91ed4adfc2c8d8032796683271d7..b44fa72656dea2a6b4c65798678d4ddc635fe094 100644 GIT binary patch delta 661 zcmYL^O=uHA6vyAp#u~SlCe_xow76MAGeyN5Y%K~!1hEwKB!9&m%`(b4j54tFEwmCS5d2fFI|C{&T+%)<$ zhi1>U%%@E-IH(ibKbYIwsNMjdIGKKRwG*SibR0(;lDSzF$En}I33`q`$6+kqHP-NX zfzE_OG#Va1xQ2Mj0_P|6jm@U}AIZZa=oCX_nj$b;arbf>c@=Rgc9SPS-qXq5RYdRRYg}KMZn}~**(s^Y?8<3EGM9VZM0v<>z3*Tv^7<0nY|)$BX|Q}1lP~MmF#FV9v~a}~ zFDT}ow%ZUNNVbbTZn8 g52?n(Xdgm7Mv2TGq*7PFkh&irDtG$~x93RUAGG}>Qvd(} delta 416 zcmaDfk#S-l;{-1J#%qD<)Sksq;?VHGwgK zsp%&Jv%;QTlh-gQGAXuiKF{Q!FqvOPMQ0HM!-GkT0=zF?-0!=y&olZ?zC zI5MobT1~hl5)`7EI25{eX=GR=ctGU2K=Q(qHB=QRhpT2WF+JRTSal`i#)ka` zH|q4Cv(|lJ$@tYElcDsYWd}%zA1E{*NdE%r`_IsNP+?`u`gJ$zr22tGLubmtl*7L= z5^hCt>_09G^raQha+U`>R_tebu;ZjCg8}nOQ3fR^ph*l5c6{IliLO%WZrRZwf>0pB pz_m)L?pMoXD@`e;A1#~1H0>A}l{Qb+n!vN!)O-@-=F=A90sx