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 <beauxq@users.noreply.github.com>
This commit is contained in:
strotlog 2022-10-31 22:42:11 -07:00 committed by GitHub
parent 802119502d
commit 655f287d42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 663 additions and 503 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"CLIPLEN_end": "85:990F",
"CLIPLEN_no_multi": "85:990C",
"CLIPSET": "85:FF1D",
"COLLECTTANK": "B8:84E8",
"COLLECTTANK": "B8:8503",
"MISCFX": "85:FF45",
"NORMAL": "84:8BF2",
"SETFX": "85:FF4E",
@ -12,6 +12,11 @@
"SOUNDFX_84": "84:F9E0",
"SPECIALFX": "85:FF3C",
"ammo_loop_table": "84:F896",
"ap_playerid_to_rom_other_player_index": "B8:85BA",
"ap_playerid_to_rom_other_player_index_checklastrow": "B8:85DD",
"ap_playerid_to_rom_other_player_index_correctindex": "B8:85F8",
"ap_playerid_to_rom_other_player_index_do_search_stage_1": "B8:85C0",
"ap_playerid_to_rom_other_player_index_notfound": "B8:85F5",
"archipelago_chozo_item_plm": "84:F874",
"archipelago_hidden_item_plm": "84:F878",
"archipelago_visible_item_plm": "84:F870",
@ -35,11 +40,13 @@
"i_item_setup_shared_all_items": "B8:8878",
"i_item_setup_shared_alwaysloaded": "B8:8883",
"i_live_pickup": "84:FA79",
"i_live_pickup_multiworld": "B8:8578",
"i_live_pickup_multiworld_end": "B8:85BD",
"i_live_pickup_multiworld_local_item_or_offworld": "B8:8594",
"i_live_pickup_multiworld_own_item": "B8:85A9",
"i_live_pickup_multiworld_own_item1": "B8:85B5",
"i_live_pickup_multiworld": "B8:85FD",
"i_live_pickup_multiworld_end": "B8:8679",
"i_live_pickup_multiworld_item_link_item": "B8:8659",
"i_live_pickup_multiworld_otherplayers_item": "B8:8649",
"i_live_pickup_multiworld_own_item": "B8:8635",
"i_live_pickup_multiworld_own_item1": "B8:8641",
"i_live_pickup_multiworld_send_network": "B8:8620",
"i_load_custom_graphics": "84:FA1E",
"i_load_custom_graphics_all_items": "84:FA39",
"i_load_custom_graphics_alwaysloaded": "84:FA49",
@ -52,36 +59,41 @@
"i_start_draw_loop_visible_or_chozo": "84:F9E5",
"i_visible_item": "84:F8A6",
"i_visible_item_setup": "84:FA53",
"message_PlaceholderBig": "85:BA8A",
"message_char_table": "85:BA0A",
"message_hook_tilemap_calc": "85:BABC",
"message_hook_tilemap_calc_msgbox_mwrecv": "85:BADC",
"message_hook_tilemap_calc_msgbox_mwsend": "85:BACE",
"message_PlaceholderBig": "85:BB73",
"message_char_table": "85:BAF3",
"message_hook_tilemap_calc": "85:BBAA",
"message_hook_tilemap_calc_msgbox_mw_item_link": "85:BBDD",
"message_hook_tilemap_calc_msgbox_mwrecv": "85:BBCF",
"message_hook_tilemap_calc_msgbox_mwsend": "85:BBC1",
"message_hook_tilemap_calc_normal": "85:824C",
"message_hook_tilemap_calc_vanilla": "85:BAC9",
"message_hook_tilemap_calc_vanilla": "85:BBBC",
"message_item_link_distributed": "85:B9A3",
"message_item_link_distributed_end": "85:BAA3",
"message_item_names": "85:9963",
"message_item_received": "85:B8A3",
"message_item_received_end": "85:B9A3",
"message_item_sent": "85:B7A3",
"message_item_sent_end": "85:B8A3",
"message_multiworld_init_new_messagebox_if_needed": "85:BA95",
"message_multiworld_init_new_messagebox_if_needed_msgbox_mwrecv": "85:BAB1",
"message_multiworld_init_new_messagebox_if_needed_msgbox_mwsend": "85:BAB1",
"message_multiworld_init_new_messagebox_if_needed_vanilla": "85:BAA9",
"message_write_placeholders": "85:B9A3",
"message_write_placeholders_adjust": "85:B9A5",
"message_write_placeholders_end": "85:BA04",
"message_write_placeholders_loop": "85:B9CA",
"message_write_placeholders_notfound": "85:B9DC",
"message_write_placeholders_value_ok": "85:B9DF",
"message_multiworld_init_new_messagebox_if_needed": "85:BB7E",
"message_multiworld_init_new_messagebox_if_needed_msgbox_mw_item_link": "85:BB9F",
"message_multiworld_init_new_messagebox_if_needed_msgbox_mwrecv": "85:BB9F",
"message_multiworld_init_new_messagebox_if_needed_msgbox_mwsend": "85:BB9F",
"message_multiworld_init_new_messagebox_if_needed_vanilla": "85:BB97",
"message_write_placeholders": "85:BAA3",
"message_write_placeholders_adjust": "85:BAA5",
"message_write_placeholders_end": "85:BAED",
"mw_cleanup_item_link_messagebox": "B8:84BB",
"mw_display_item_sent": "B8:848B",
"mw_handle_queue": "B8:84F8",
"mw_handle_queue_end": "B8:8571",
"mw_handle_queue_loop": "B8:84FA",
"mw_handle_queue_new_remote_item": "B8:854A",
"mw_handle_queue_next": "B8:8566",
"mw_handle_queue_perform_receive": "B8:855C",
"mw_hook_main_game": "B8:85C1",
"mw_handle_queue": "B8:8513",
"mw_handle_queue_collect_item_if_present": "B8:8562",
"mw_handle_queue_end": "B8:85B3",
"mw_handle_queue_found": "B8:859B",
"mw_handle_queue_lookup_player": "B8:8522",
"mw_handle_queue_loop": "B8:8515",
"mw_handle_queue_new_remote_item": "B8:857C",
"mw_handle_queue_next": "B8:85A7",
"mw_handle_queue_perform_receive": "B8:858E",
"mw_hook_main_game": "B8:867D",
"mw_init": "B8:8311",
"mw_init_continuereset": "B8:8366",
"mw_init_end": "B8:83EA",
@ -91,8 +103,9 @@
"mw_load_sram": "B8:8474",
"mw_load_sram_done": "B8:8482",
"mw_load_sram_setnewgame": "B8:8485",
"mw_receive_item": "B8:84A9",
"mw_receive_item_end": "B8:84E1",
"mw_prep_item_link_messagebox": "B8:84A9",
"mw_receive_item": "B8:84C4",
"mw_receive_item_end": "B8:84FC",
"mw_save_sram": "B8:8469",
"mw_write_message": "B8:8442",
"nonprog_item_eight_palette_indices": "84:F888",
@ -119,16 +132,16 @@
"p_visible_item_end": "84:F96E",
"p_visible_item_loop": "84:F95B",
"p_visible_item_trigger": "84:F967",
"patch_load_multiworld": "B8:85D8",
"patch_load_multiworld": "B8:8694",
"perform_item_pickup": "84:FA7E",
"plm_graphics_entry_offworld_item": "84:F886",
"plm_graphics_entry_offworld_progression_item": "84:F87C",
"plm_sequence_generic_item_0_bitmask": "84:FA90",
"prog_item_eight_palette_indices": "84:F87E",
"rando_item_table": "B8:E000",
"rando_player_id_table": "B8:DC90",
"rando_player_id_table_end": "B8:DE22",
"rando_player_table": "B8:D000",
"rando_player_id_table": "B8:DCA0",
"rando_player_id_table_end": "B8:DE34",
"rando_player_name_table": "B8:D000",
"rando_seed_data": "B8:CF00",
"sm_item_graphics": "B8:8800",
"sm_item_plm_pickup_sequence_pointers": "B8:882E",