Merge remote-tracking branch 'pepper/multishop-all' into multishop

# Conflicts:
#	EntranceRandomizer.py
#	Rom.py
#	WebHostLib/static/static/playerSettings.json
#	data/basepatch.bmbp
This commit is contained in:
Fabian Dill 2021-01-09 16:03:59 +01:00
commit e36c6e97c1
14 changed files with 508 additions and 33 deletions

View File

@ -134,6 +134,8 @@ class World(object):
set_player_attr('triforce_pieces_available', 30) set_player_attr('triforce_pieces_available', 30)
set_player_attr('triforce_pieces_required', 20) set_player_attr('triforce_pieces_required', 20)
set_player_attr('shop_shuffle', 'off') set_player_attr('shop_shuffle', 'off')
set_player_attr('shop_shuffle_slots', 0)
set_player_attr('potion_shop_shuffle', 'none')
set_player_attr('shuffle_prizes', "g") set_player_attr('shuffle_prizes', "g")
set_player_attr('sprite_pool', []) set_player_attr('sprite_pool', [])
set_player_attr('dark_room_logic', "lamp") set_player_attr('dark_room_logic', "lamp")
@ -1158,7 +1160,8 @@ class Shop():
'max': max, 'max': max,
'replacement': replacement, 'replacement': replacement,
'replacement_price': replacement_price, 'replacement_price': replacement_price,
'create_location': create_location 'create_location': create_location,
'player': 0
} }
def push_inventory(self, slot: int, item: str, price: int, max: int = 1): def push_inventory(self, slot: int, item: str, price: int, max: int = 1):
@ -1171,7 +1174,8 @@ class Shop():
'max': max, 'max': max,
'replacement': self.inventory[slot]["item"], 'replacement': self.inventory[slot]["item"],
'replacement_price': self.inventory[slot]["price"], 'replacement_price': self.inventory[slot]["price"],
'create_location': self.inventory[slot]["create_location"] 'create_location': self.inventory[slot]["create_location"],
'player': self.inventory[slot]["player"]
} }
@ -1257,6 +1261,10 @@ class Spoiler(object):
if item is None: if item is None:
continue continue
shopdata['item_{}'.format(index)] = "{}{}".format(item['item'], item['price']) if item['price'] else item['item'] shopdata['item_{}'.format(index)] = "{}{}".format(item['item'], item['price']) if item['price'] else item['item']
if item['player'] > 0:
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('', '(Player {}) — '.format(item['player']))
if item['max'] == 0: if item['max'] == 0:
continue continue
shopdata['item_{}'.format(index)] += " x {}".format(item['max']) shopdata['item_{}'.format(index)] += " x {}".format(item['max'])
@ -1330,6 +1338,8 @@ class Spoiler(object):
'triforce_pieces_available': self.world.triforce_pieces_available, 'triforce_pieces_available': self.world.triforce_pieces_available,
'triforce_pieces_required': self.world.triforce_pieces_required, 'triforce_pieces_required': self.world.triforce_pieces_required,
'shop_shuffle': self.world.shop_shuffle, 'shop_shuffle': self.world.shop_shuffle,
'shop_shuffle_slots': self.world.shop_shuffle_slots,
'potion_shop_shuffle': self.world.potion_shop_shuffle,
'shuffle_prizes': self.world.shuffle_prizes, 'shuffle_prizes': self.world.shuffle_prizes,
'sprite_pool': self.world.sprite_pool, 'sprite_pool': self.world.sprite_pool,
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss 'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss

View File

@ -326,10 +326,21 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4)) parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--shop_shuffle', default='', help='''\ parser.add_argument('--shop_shuffle', default='', help='''\
combine letters for options: combine letters for options:
i: shuffle the inventories of the shops around g: generate default inventories for light and dark world shops, and unique shops
f: generate default inventories for each shop individually
i: shuffle the default inventories of the shops around
p: randomize the prices of the items in shop inventories p: randomize the prices of the items in shop inventories
u: shuffle capacity upgrades into the item pool u: shuffle capacity upgrades into the item pool
''') ''')
parser.add_argument('--shop_shuffle_slots', default=defval(0),
type=lambda value: min(max(int(value), 1), 96),
help='''
Maximum amount of shop slots able to be filled by items from the item pool.
''')
parser.add_argument('--potion_shop_shuffle', default=defval('none'), choices=['none', 'a'], help='''\
Determine if potion shop shuffle items should be affected by the rules of shop shuffle.
Value `none` will only allow prices to be shuffled, `a` will allow any items to be shuffled.
''')
parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb']) parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb'])
parser.add_argument('--sprite_pool', help='''\ parser.add_argument('--sprite_pool', help='''\
Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''') Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''')
@ -390,7 +401,8 @@ def parse_arguments(argv, no_defaults=False):
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
'heartbeep', "skip_progression_balancing", "triforce_pieces_available", 'heartbeep', "skip_progression_balancing", "triforce_pieces_available",
"triforce_pieces_required", "shop_shuffle", "required_medallions", "triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots", "potion_shop_shuffle",
"required_medallions",
"plando_items", "plando_texts", "plando_connections", "plando_items", "plando_texts", "plando_connections",
'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', 'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', 'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',

View File

@ -171,6 +171,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
fill_locations.remove(spot_to_fill) fill_locations.remove(spot_to_fill)
world.random.shuffle(fill_locations) world.random.shuffle(fill_locations)
prioitempool, fill_locations = fast_fill(world, prioitempool, fill_locations) prioitempool, fill_locations = fast_fill(world, prioitempool, fill_locations)
restitempool, fill_locations = fast_fill(world, restitempool, fill_locations) restitempool, fill_locations = fast_fill(world, restitempool, fill_locations)
@ -247,6 +248,7 @@ def flood_items(world):
itempool.remove(item_to_place) itempool.remove(item_to_place)
break break
def balance_multiworld_progression(world): def balance_multiworld_progression(world):
balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]} balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]}
if not balanceable_players: if not balanceable_players:

View File

@ -481,18 +481,23 @@ def shuffle_shops(world, items, player: int):
shops = [] shops = []
upgrade_shops = [] upgrade_shops = []
total_inventory = [] total_inventory = []
potion_option = world.potion_shop_shuffle[player]
for shop in world.shops: for shop in world.shops:
if shop.region.player == player: if shop.region.player == player:
if shop.type == ShopType.UpgradeShop: if shop.type == ShopType.UpgradeShop:
upgrade_shops.append(shop) upgrade_shops.append(shop)
elif shop.type == ShopType.Shop and shop.region.name != 'Potion Shop': elif shop.type == ShopType.Shop:
shops.append(shop) if shop.region.name == 'Potion Shop' and potion_option in [None, '', 'none']:
total_inventory.extend(shop.inventory) upgrade_shops.append(shop) # just put it with the upgrade shops/caves so we don't shuffle the items, just prices
else:
shops.append(shop)
total_inventory.extend(shop.inventory)
if 'p' in option: if 'p' in option:
def price_adjust(price: int) -> int: def price_adjust(price: int) -> int:
# it is important that a base price of 0 always returns 0 as new price! # it is important that a base price of 0 always returns 0 as new price!
return int(price * (0.5 + world.random.random() * 1.5)) adjust = 2 if price < 100 else 5
return int((price / adjust) * (0.5 + world.random.random() * 1.5)) * adjust
def adjust_item(item): def adjust_item(item):
if item: if item:
@ -507,6 +512,7 @@ def shuffle_shops(world, items, player: int):
if 'i' in option: if 'i' in option:
world.random.shuffle(total_inventory) world.random.shuffle(total_inventory)
i = 0 i = 0
for shop in shops: for shop in shops:
slots = shop.slots slots = shop.slots
@ -577,7 +583,7 @@ def create_dynamic_shop_locations(world, player):
if item is None: if item is None:
continue continue
if item['create_location']: if item['create_location']:
loc = Location(player, "{} Item {}".format(shop.region.name, i+1), parent=shop.region) loc = Location(player, "{} Shop Slot {}".format(shop.region.name, i+1), parent=shop.region)
shop.region.locations.append(loc) shop.region.locations.append(loc)
world.dynamic_locations.append(loc) world.dynamic_locations.append(loc)

67
Main.py
View File

@ -83,6 +83,8 @@ def main(args, seed=None):
world.triforce_pieces_available = args.triforce_pieces_available.copy() world.triforce_pieces_available = args.triforce_pieces_available.copy()
world.triforce_pieces_required = args.triforce_pieces_required.copy() world.triforce_pieces_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy() world.shop_shuffle = args.shop_shuffle.copy()
world.shop_shuffle_slots = args.shop_shuffle_slots.copy()
world.potion_shop_shuffle = args.potion_shop_shuffle.copy()
world.progression_balancing = {player: not balance for player, balance in args.skip_progression_balancing.items()} world.progression_balancing = {player: not balance for player, balance in args.skip_progression_balancing.items()}
world.shuffle_prizes = args.shuffle_prizes.copy() world.shuffle_prizes = args.shuffle_prizes.copy()
world.sprite_pool = args.sprite_pool.copy() world.sprite_pool = args.sprite_pool.copy()
@ -209,6 +211,70 @@ def main(args, seed=None):
if world.players > 1: if world.players > 1:
balance_multiworld_progression(world) balance_multiworld_progression(world)
candidates = [l for l in world.get_locations() if l.item.name in ['Bee Trap', 'Shovel', 'Bug Catching Net', 'Cane of Byrna', 'Triforce Piece'] or
any([x in l.item.name for x in ['Key', 'Map', 'Compass', 'Clock', 'Heart', 'Sword', 'Shield', 'Bomb', 'Arrow', 'Mail']])]
world.random.shuffle(candidates)
shop_slots = [item for sublist in [shop.region.locations for shop in world.shops] for item in sublist if item.name != 'Potion Shop']
shop_slots_adjusted = []
shop_items = []
for location in shop_slots:
slot_num = int(location.name[-1]) - 1
shop_item = location.parent_region.shop.inventory[slot_num]
item = location.item
# if item is a rupee or single bee, or identical, swap it out
if (shop_item is not None and shop_item['item'] == item.name) or 'Rupee' in item.name or (item.name in ['Bee']):
for c in candidates: # chosen item locations
if 'Rupee' in c.item.name or c.item.name in 'Bee': continue
if (shop_item is not None and shop_item['item'] == c.item.name): continue
if c.item_rule(location.item): # if rule is good...
logging.debug('Swapping {} with {}:: {} ||| {}'.format(c, location, c.item, location.item))
c.item, location.item = location.item, c.item
if not world.can_beat_game():
c.item, location.item = location.item, c.item
else:
shop_slots_adjusted.append(location)
break
# update table to location data
item = location.item
if (shop_item is not None and shop_item['item'] == item.name) or 'Rupee' in item.name or (item.name in ['Bee']):
# this will filter items that match the item in the shop or Rupees, or single bees
# really not a way for the player to know a renewable item from a player pool item
# bombs can be sitting on top of arrows or a potion refill, but dunno if that's a big deal
# this should rarely happen with the above code in place, and could be an option in config if necessary
logging.debug('skipping item shop {}'.format(item.name))
else:
if shop_item is None:
location.parent_region.shop.add_inventory(slot_num, 'None', 0)
shop_item = location.parent_region.shop.inventory[slot_num]
else:
shop_item['replacement'] = shop_item['item']
shop_item['replacement_price'] = shop_item['price']
shop_item['item'] = item.name
if any([x in shop_item['item'] for x in ['Single Bomb', 'Single Arrow']]):
shop_item['price'] = world.random.randrange(5,35)
elif any([x in shop_item['item'] for x in ['Arrows', 'Bombs', 'Clock']]):
shop_item['price'] = world.random.randrange(20,120)
elif any([x in shop_item['item'] for x in ['Universal Key', 'Compass', 'Map', 'Small Key', 'Piece of Heart']]):
shop_item['price'] = world.random.randrange(50,150)
else:
shop_item['price'] = world.random.randrange(50,300)
shop_item['max'] = 1
shop_item['player'] = item.player if item.player != location.player else 0
shop_items.append(shop_item)
if len(shop_items) > 0:
my_prices = [my_item['price'] for my_item in shop_items]
price_scale = (80*max(8, len(my_prices)+2))/sum(my_prices)
for i in shop_items:
i['price'] *= price_scale
if i['price'] < 5: i['price'] = 5
else: i['price'] = int((i['price']//5)*5)
logging.debug('Adjusting {} of {} shop slots'.format(len(shop_slots_adjusted), len(shop_slots)))
logging.debug('Adjusted {} into shops'.format([x.item.name for x in shop_slots_adjusted]))
logger.info('Patching ROM.') logger.info('Patching ROM.')
outfilebase = 'BM_%s' % (args.outputname if args.outputname else world.seed) outfilebase = 'BM_%s' % (args.outputname if args.outputname else world.seed)
@ -336,6 +402,7 @@ def main(args, seed=None):
main_entrance = get_entrance_to_region(region) main_entrance = get_entrance_to_region(region)
for location in region.locations: for location in region.locations:
if type(location.address) == int: # skips events and crystals if type(location.address) == int: # skips events and crystals
if location.address >= 0x400000: continue
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name er_hint_data[region.player][location.address] = main_entrance.name

View File

@ -158,6 +158,11 @@ SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
location_shop_order = [ name for name, info in Regions.shop_table.items() ] # probably don't leave this here. This relies on python 3.6+ dictionary keys having defined order
location_shop_ids = set([info[0] for name, info in Regions.shop_table.items()])
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
"Blind's Hideout - Left": (0x11d, 0x20), "Blind's Hideout - Left": (0x11d, 0x20),
@ -1159,6 +1164,18 @@ async def track_locations(ctx : Context, roomid, roomdata):
ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.unsafe_locations_checked))) ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.unsafe_locations_checked)))
ctx.ui_node.send_location_check(ctx, location) ctx.ui_node.send_location_check(ctx, location)
try:
if roomid in location_shop_ids:
misc_data = await snes_read(ctx, SHOP_ADDR, len(location_shop_order)*3)
for cnt, b in enumerate(misc_data):
my_check = Regions.shop_table_by_location_id[0x400000 + cnt]
if int(b) > 0 and my_check not in ctx.unsafe_locations_checked:
new_check(my_check)
except Exception as e:
print(e)
ctx.ui_node.log_info(f"Exception: {e}")
for location, (loc_roomid, loc_mask) in location_table_uw.items(): for location, (loc_roomid, loc_mask) in location_table_uw.items():
try: try:
if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0: if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
@ -1217,7 +1234,13 @@ async def track_locations(ctx : Context, roomid, roomdata):
for location in ctx.unsafe_locations_checked: for location in ctx.unsafe_locations_checked:
if (location in ctx.items_missing and location not in ctx.locations_checked) or ctx.send_unsafe: if (location in ctx.items_missing and location not in ctx.locations_checked) or ctx.send_unsafe:
ctx.locations_checked.add(location) ctx.locations_checked.add(location)
new_locations.append(Regions.lookup_name_to_id[location]) try:
my_id = Regions.lookup_name_to_id.get(location, Regions.shop_table_by_location.get(location, -1))
new_locations.append(my_id)
except Exception as e:
print(e)
ctx.ui_node.log_info(f"Exception: {e}")
await ctx.send_msgs([['LocationChecks', new_locations]]) await ctx.send_msgs([['LocationChecks', new_locations]])

View File

@ -969,7 +969,7 @@ def get_missing_checks(ctx: Context, client: Client) -> list:
#for location_id in [k[0] for k, v in ctx.locations if k[1] == client.slot]: #for location_id in [k[0] for k, v in ctx.locations if k[1] == client.slot]:
# if location_id not in ctx.location_checks[client.team, client.slot]: # if location_id not in ctx.location_checks[client.team, client.slot]:
# locations.append(Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}')) # locations.append(Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}'))
for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind for location_id, location_name in {**Regions.lookup_id_to_name, **Regions.shop_table_by_location_id}.items(): # cheat console is -1, keep in mind
if location_id != -1 and location_id not in ctx.location_checks[client.team, client.slot] and (location_id, client.slot) in ctx.locations: if location_id != -1 and location_id not in ctx.location_checks[client.team, client.slot] and (location_id, client.slot) in ctx.locations:
locations.append(location_name) locations.append(location_name)
return locations return locations

View File

@ -405,10 +405,16 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"
# change minimum to required pieces to avoid problems # change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90) ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
ret.shop_shuffle_slots = int(get_choice('shop_shuffle_slots', weights, '0'))
ret.shop_shuffle = get_choice('shop_shuffle', weights, '') ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
if not ret.shop_shuffle: if not ret.shop_shuffle:
ret.shop_shuffle = '' ret.shop_shuffle = ''
ret.potion_shop_shuffle = get_choice('potion_shop_shuffle', weights, '')
if not ret.potion_shop_shuffle:
ret.potion_shop_shuffle = ''
ret.mode = get_choice('world_state', weights, None) # legacy support ret.mode = get_choice('world_state', weights, None) # legacy support
if ret.mode == 'retro': if ret.mode == 'retro':
ret.mode = 'open' ret.mode = 'open'

View File

@ -369,7 +369,39 @@ def create_shops(world, player: int):
cls_mapping = {ShopType.UpgradeShop: UpgradeShop, cls_mapping = {ShopType.UpgradeShop: UpgradeShop,
ShopType.Shop: Shop, ShopType.Shop: Shop,
ShopType.TakeAny: TakeAny} ShopType.TakeAny: TakeAny}
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items(): option = world.shop_shuffle[player]
potion_option = world.potion_shop_shuffle[player]
my_shop_table = dict(shop_table)
num_slots = int(world.shop_shuffle_slots[player])
my_shop_slots = ([True] * num_slots + [False] * (len(shop_table) * 3))[:len(shop_table)*3 - 2]
world.random.shuffle(my_shop_slots)
from Items import ItemFactory
if 'g' in option or 'f' in option:
new_basic_shop = world.random.sample(shop_generation_types['default'], k=3)
new_dark_shop = world.random.sample(shop_generation_types['default'], k=3)
for name, shop in my_shop_table.items():
typ, shop_id, keeper, custom, locked, items = shop
new_items = world.random.sample(shop_generation_types['default'], k=3)
if 'f' not in option:
if items == _basic_shop_defaults:
new_items = new_basic_shop
elif items == _dark_world_shop_defaults:
new_items = new_dark_shop
if name == 'Capacity Upgrade':
continue
if name == 'Potion Shop':
if 'b' in potion_option:
new_items = world.random.sample(shop_generation_types['potion_discount'] + shop_generation_types['bottle'], k=3)
elif 'a' not in potion_option:
new_items = items
keeper = world.random.choice([0xA0, 0xC1, 0xFF])
my_shop_table[name] = (typ, shop_id, keeper, custom, locked, new_items)
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in my_shop_table.items():
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop': if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
locked = True locked = True
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)] inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
@ -379,6 +411,21 @@ def create_shops(world, player: int):
world.shops.append(shop) world.shops.append(shop)
for index, item in enumerate(inventory): for index, item in enumerate(inventory):
shop.add_inventory(index, *item) shop.add_inventory(index, *item)
if region_name == 'Potion Shop' and 'a' not in potion_option:
pass
elif region_name == 'Capacity Upgrade':
pass
else:
if my_shop_slots.pop():
additional_item = world.random.choice(['Rupees (50)', 'Rupees (100)', 'Rupees (300)'])
world.itempool.append(ItemFactory(additional_item, player))
slot_name = "{} Shop Slot {}".format(shop.region.name, index+1)
loc = Location(player, slot_name, address=shop_table_by_location[slot_name], parent=shop.region)
shop.region.locations.append(loc)
world.dynamic_locations.append(loc)
world.clear_location_cache()
# (type, room_id, shopkeeper, custom, locked, [items]) # (type, room_id, shopkeeper, custom, locked, [items])
# item = (item, price, max=0, replacement=None, replacement_price=0) # item = (item, price, max=0, replacement=None, replacement_price=0)
@ -394,10 +441,21 @@ shop_table = {
'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults), 'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults), 'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults), 'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]), 'Potion Shop': (0x0109, ShopType.Shop, 0xA0, True, False, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]),
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)]) 'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
} }
shop_table_by_location_id = {0x400000 + cnt: s for cnt, s in enumerate( [item for sublist in [ ["{} Shop Slot {}".format(name, num + 1) for num in range(3)] for name in shop_table ] for item in sublist])}
shop_table_by_location = {y:x for x,y in shop_table_by_location_id.items()}
shop_generation_types = {
'default': _basic_shop_defaults + [('Bombs (3)', 20), ('Green Potion', 90), ('Blue Potion', 190), ('Bee', 10), ('Single Arrow', 5), ('Single Bomb', 10)] + [('Red Shield', 500), ('Blue Shield', 50)],
'potion': [('Red Potion', 150), ('Green Potion', 90), ('Blue Potion', 190)],
'discount_potion': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
'bottle': [('Bee', 10)],
'time': [('Red Clock', 100), ('Blue Clock', 200), ('Green Clock', 300)],
}
old_location_address_to_new_location_address = { old_location_address_to_new_location_address = {
0x2eb18: 0x18001b, # Bottle Merchant 0x2eb18: 0x18001b, # Bottle Merchant
0x33d68: 0x18001a, # Purple Chest 0x33d68: 0x18001a, # Purple Chest
@ -705,8 +763,10 @@ location_table: typing.Dict[str,
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}, -1: "cheat console"} lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}, -1: "cheat console"}
lookup_id_to_name.update(shop_table_by_location_id)
lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int} lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int}
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}, "cheat console": -1} lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}, "cheat console": -1}
lookup_name_to_id.update(shop_table_by_location)
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks', lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',
1573194: 'Kings Grave Inner Rocks', 1573189: 'Kings Grave Inner Rocks', 1573194: 'Kings Grave Inner Rocks', 1573189: 'Kings Grave Inner Rocks',

18
Rom.py
View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '5a607e36a82bbd14180536c8ec3ae49b' RANDOMIZERBASEHASH = '0954a778832b76ba4f96b10eb527ee83'
import io import io
import json import json
@ -123,6 +123,9 @@ class LocalRom(object):
Patch.create_patch_file(local_path('basepatch.sfc')) Patch.create_patch_file(local_path('basepatch.sfc'))
return return
if not os.path.isfile(local_path('data', 'basepatch.bmbp')):
raise RuntimeError('Base patch unverified. Unable to continue.')
if os.path.isfile(local_path('data', 'basepatch.bmbp')): if os.path.isfile(local_path('data', 'basepatch.bmbp')):
_, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.bmbp')) _, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.bmbp'))
if self.verify(buffer): if self.verify(buffer):
@ -130,6 +133,7 @@ class LocalRom(object):
with open(local_path('basepatch.sfc'), 'wb') as stream: with open(local_path('basepatch.sfc'), 'wb') as stream:
stream.write(buffer) stream.write(buffer)
return return
raise RuntimeError('Base patch unverified. Unable to continue.')
raise RuntimeError('Could not find Base Patch. Unable to continue.') raise RuntimeError('Could not find Base Patch. Unable to continue.')
@ -677,6 +681,7 @@ def patch_rom(world, rom, player, team, enemized):
distinguished_prog_bow_loc.item.code = 0x65 distinguished_prog_bow_loc.item.code = 0x65
# patch items # patch items
for location in world.get_locations(): for location in world.get_locations():
if location.player != player: if location.player != player:
continue continue
@ -686,6 +691,9 @@ def patch_rom(world, rom, player, team, enemized):
if location.address is None: if location.address is None:
continue continue
if 'Shop Slot' in location.name and location.parent_region.shop is not None:
continue
if not location.crystal: if not location.crystal:
if location.item is not None: if location.item is not None:
# Keys in their native dungeon should use the orignal item code for keys # Keys in their native dungeon should use the orignal item code for keys
@ -724,6 +732,7 @@ def patch_rom(world, rom, player, team, enemized):
for music_address in music_addresses: for music_address in music_addresses:
rom.write_byte(music_address, music) rom.write_byte(music_address, music)
if world.mapshuffle[player]: if world.mapshuffle[player]:
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
@ -1564,14 +1573,13 @@ def write_custom_shops(rom, world, player):
else: else:
sram_offset += shop.item_count sram_offset += shop.item_count
shop_data.extend(bytes) shop_data.extend(bytes)
# [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high] # [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player]
for item in shop.inventory: for item in shop.inventory:
if item is None: if item is None:
break break
item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + [ item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + [ item['max'],\
item['max'],
ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + int16_as_bytes( ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + int16_as_bytes(
item['replacement_price']) item['replacement_price']) + [item['player']]
items_data.extend(item_data) items_data.extend(item_data)
rom.write_bytes(0x184800, shop_data) rom.write_bytes(0x184800, shop_data)

View File

@ -477,9 +477,23 @@
"name": "None", "name": "None",
"value": "none" "value": "none"
}, },
{ "g": {
"name": "Inventory", "keyString": "shop_shuffle.g",
"value": "i" "friendlyName": "Inventory Generate",
"description": "Generates new default base inventories of overworld and underworld shops.",
"defaultValue": 0
},
"f": {
"keyString": "shop_shuffle.f",
"friendlyName": "Full Inventory Generate",
"description": "Generates new base inventories of each individual shop.",
"defaultValue": 0
},
"i": {
"keyString": "shop_shuffle.i",
"friendlyName": "Inventory Shuffle",
"description": "Shuffles the inventories of shops between each other.",
"defaultValue": 0
}, },
{ {
"name": "Prices", "name": "Prices",
@ -493,9 +507,269 @@
"name": "Inventory and Prices", "name": "Inventory and Prices",
"value": "ip" "value": "ip"
}, },
{ "uip": {
"name": "Inventory, Prices, and Upgrades", "keyString": "shop_shuffle.uip",
"value": "ipu" "friendlyName": "Full Shuffle",
"description": "Shuffles the inventory and randomizes the prices of items in shops. Also distributes capacity upgrades throughout the world.",
"defaultValue": 0
}
}
},
"shop_shuffle_slots": {
"keyString": "shop_shuffle_slots",
"friendlyName": "Shop Shuffle Slots",
"description": "How Many Slots in Shops are dedicated to items from the item pool",
"inputType": "range",
"subOptions": {
"0": {
"keyString": "shop_shuffle_slots.0",
"friendlyName": 0,
"description": "0 slots",
"defaultValue": 50
},
"15": {
"keyString": "shop_shuffle_slots.3",
"friendlyName": 3,
"description": "3 slots",
"defaultValue": 0
},
"20": {
"keyString": "shop_shuffle_slots.6",
"friendlyName": 6,
"description": "6 slots",
"defaultValue": 0
},
"30": {
"keyString": "shop_shuffle_slots.12",
"friendlyName": 12,
"description": "12 slots",
"defaultValue": 0
},
"40": {
"keyString": "shop_shuffle_slots.96",
"friendlyName": 96,
"description": "96 slots",
"defaultValue": 0
}
}
},
"potion_shop_shuffle": {
"keyString": "potion_shop_shuffle",
"friendlyName": "Potion Shop Shuffle Rules",
"description": "Influence on potion shop by shop shuffle options",
"inputType": "range",
"subOptions": {
"none": {
"keyString": "potion_shop_shuffle.none",
"friendlyName": "Vanilla Shops",
"description": "Shop contents are left unchanged, only prices.",
"defaultValue": 50
},
"a": {
"keyString": "potion_shop_shuffle.a",
"friendlyName": "Any Items can be shuffled in and out of the shop",
"description": "",
"defaultValue": 0
}
}
},
"shuffle_prizes": {
"keyString": "shuffle_prizes",
"friendlyName": "Prize Shuffle",
"description": "Alters the Prizes from pulling, bonking, enemy kills, digging, and hoarders",
"inputType": "range",
"subOptions": {
"none": {
"keyString": "shuffle_prizes.none",
"friendlyName": "None",
"description": "All prizes from pulling, bonking, enemy kills, digging, hoarders are vanilla.",
"defaultValue": 0
},
"g": {
"keyString": "shuffle_prizes.g",
"friendlyName": "\"General\" prize shuffle",
"description": "Shuffles the prizes from pulling, enemy kills, digging, hoarders",
"defaultValue": 50
},
"b": {
"keyString": "shuffle_prizes.b",
"friendlyName": "Bonk prize shuffle",
"description": "Shuffles the prizes from bonking into trees.",
"defaultValue": 0
},
"bg": {
"keyString": "shuffle_prizes.bg",
"friendlyName": "Both",
"description": "Shuffles both of the options.",
"defaultValue": 0
}
}
},
"timer": {
"keyString": "timer",
"friendlyName": "Timed Modes",
"description": "Add a timer to the game UI, and cause it to have various effects.",
"inputType": "range",
"subOptions": {
"none": {
"keyString": "timer.none",
"friendlyName": "Disabled",
"description": "No timed mode is applied to the game.",
"defaultValue": 50
},
"timed": {
"keyString": "timer.timed",
"friendlyName": "Timed Mode",
"description": "Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.",
"defaultValue": 0
},
"timed_ohko": {
"keyString": "timer.timed_ohko",
"friendlyName": "Timed OHKO",
"description": "Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.",
"defaultValue": 0
},
"ohko": {
"keyString": "timer.ohko",
"friendlyName": "One-Hit KO",
"description": "Timer always at zero. Permanent OHKO.",
"defaultValue": 0
},
"timed_countdown": {
"keyString": "timer.timed_countdown",
"friendlyName": "Timed Countdown",
"description": "Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.",
"defaultValue": 0
},
"display": {
"keyString": "timer.display",
"friendlyName": "Timer Only",
"description": "Displays a timer, but otherwise does not affect gameplay or the item pool.",
"defaultValue": 0
}
}
},
"countdown_start_time": {
"keyString": "countdown_start_time",
"friendlyName": "Countdown Starting Time",
"description": "The amount of time, in minutes, to start with in Timed Countdown and Timed OHKO modes.",
"inputType": "range",
"subOptions": {
"0": {
"keyString": "countdown_start_time.0",
"friendlyName": 0,
"description": "Start with no time on the timer. In Timed OHKO mode, start in OHKO mode.",
"defaultValue": 0
},
"10": {
"keyString": "countdown_start_time.10",
"friendlyName": 10,
"description": "Start with 10 minutes on the timer.",
"defaultValue": 50
},
"20": {
"keyString": "countdown_start_time.20",
"friendlyName": 20,
"description": "Start with 20 minutes on the timer.",
"defaultValue": 0
},
"30": {
"keyString": "countdown_start_time.30",
"friendlyName": 30,
"description": "Start with 30 minutes on the timer.",
"defaultValue": 0
},
"60": {
"keyString": "countdown_start_time.60",
"friendlyName": 60,
"description": "Start with an hour on the timer.",
"defaultValue": 0
}
}
},
"red_clock_time": {
"keyString": "red_clock_time",
"friendlyName": "Red Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a red clock.",
"inputType": "range",
"subOptions": {
"-2": {
"keyString": "red_clock_time.-2",
"friendlyName": -2,
"description": "Subtract 2 minutes from the timer upon picking up a red clock.",
"defaultValue": 0
},
"1": {
"keyString": "red_clock_time.1",
"friendlyName": 1,
"description": "Add a minute to the timer upon picking up a red clock.",
"defaultValue": 50
}
}
},
"blue_clock_time": {
"keyString": "blue_clock_time",
"friendlyName": "Blue Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a blue clock.",
"inputType": "range",
"subOptions": {
"1": {
"keyString": "blue_clock_time.1",
"friendlyName": 1,
"description": "Add a minute to the timer upon picking up a blue clock.",
"defaultValue": 0
},
"2": {
"keyString": "blue_clock_time.2",
"friendlyName": 2,
"description": "Add 2 minutes to the timer upon picking up a blue clock.",
"defaultValue": 50
}
}
},
"green_clock_time": {
"keyString": "green_clock_time",
"friendlyName": "Green Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a green clock.",
"inputType": "range",
"subOptions": {
"4": {
"keyString": "green_clock_time.4",
"friendlyName": 4,
"description": "Add 4 minutes to the timer upon picking up a green clock.",
"defaultValue": 50
},
"10": {
"keyString": "green_clock_time.10",
"friendlyName": 10,
"description": "Add 10 minutes to the timer upon picking up a green clock.",
"defaultValue": 0
},
"15": {
"keyString": "green_clock_time.15",
"friendlyName": 15,
"description": "Add 15 minutes to the timer upon picking up a green clock.",
"defaultValue": 0
}
}
},
"glitch_boots": {
"keyString": "glitch_boots",
"friendlyName": "Glitch Boots",
"description": "Start with Pegasus Boots in any glitched logic mode that makes use of them.",
"inputType": "range",
"subOptions": {
"on": {
"keyString": "glitch_boots.on",
"friendlyName": "On",
"description": "Enable glitch boots.",
"defaultValue": 50
},
"off": {
"keyString": "glitch_boots.off",
"friendlyName": "Off",
"description": "Disable glitch boots.",
"defaultValue": 0
} }
] ]
} }

Binary file not shown.

View File

@ -211,12 +211,24 @@ beemizer: # Remove items from the global item pool and replace them with single
2: 0 # 60% of the non-essential item pool is replaced with bee traps, of which 20% could be single bees 2: 0 # 60% of the non-essential item pool is replaced with bee traps, of which 20% could be single bees
3: 0 # 100% of the non-essential item pool is replaced with bee traps, of which 50% could be single bees 3: 0 # 100% of the non-essential item pool is replaced with bee traps, of which 50% could be single bees
4: 0 # 100% of the non-essential item pool is replaced with bee traps 4: 0 # 100% of the non-essential item pool is replaced with bee traps
### Item Shuffle (shop)
shop_shuffle_slots: # Maximum amount of allowed shop slots to place item pool items
0: 50
5: 0
15: 0
999: 0
potion_shop_shuffle: # influence of potion shop by shop shuffle
none: 50 # only shuffle price
a: 0 # generate/shuffle in any items
shop_shuffle: shop_shuffle:
none: 50 none: 50
i: 0 # Shuffle the inventories of the shops around g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
f: 0 # Generate new default inventories for every shop independently
i: 0 # Shuffle default inventories of the shops around
p: 0 # Randomize the prices of the items in shop inventories p: 0 # Randomize the prices of the items in shop inventories
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld) u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
ip: 0 # Shuffle inventories and randomize prices ip: 0 # Shuffle inventories and randomize prices
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
# You can add more combos # You can add more combos
shuffle_prizes: # aka drops shuffle_prizes: # aka drops
@ -253,11 +265,6 @@ green_clock_time: # For all timer modes, the amount of time in minutes to gain o
# - "Small Keys" # - "Small Keys"
# - "Big Keys" # - "Big Keys"
# Can be uncommented to use it # Can be uncommented to use it
# non_local_items: # Force certain items to appear outside your world only, always across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl"
# - "Small Keys"
# - "Big Keys"
# Can be uncommented to use it
# startinventory: # Begin the file with the listed items/upgrades # startinventory: # Begin the file with the listed items/upgrades
# Pegasus Boots: on # Pegasus Boots: on
# Bomb Upgrade (+10): 4 # Bomb Upgrade (+10): 4