From 058436e47f9492278b95f9aff1186d0d20630421 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 11 Jan 2021 13:35:48 +0100 Subject: [PATCH] shop cleanup and correctly backreference swapped items' locations Also fixes a false reference in progression balancing from 2019 (swapped Location.item.location was not updated) --- Fill.py | 20 +++++++++-- Main.py | 110 +++++++++++++++++++++++++++++++------------------------- 2 files changed, 78 insertions(+), 52 deletions(-) diff --git a/Fill.py b/Fill.py index 0727e128..47a39bc5 100644 --- a/Fill.py +++ b/Fill.py @@ -1,7 +1,7 @@ import logging import typing -from BaseClasses import CollectionState, PlandoItem +from BaseClasses import CollectionState, PlandoItem, Location from Items import ItemFactory from Regions import key_drop_data @@ -336,7 +336,8 @@ def balance_multiworld_progression(world): replacement_locations.insert(0, new_location) new_location = replacement_locations.pop() - new_location.item, old_location.item = old_location.item, new_location.item + swap_location_item(old_location, new_location) + new_location.event, old_location.event = True, False logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " f"displacing {old_location.item} in {old_location}") @@ -361,6 +362,18 @@ def balance_multiworld_progression(world): raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') +def swap_location_item(location_1: Location, location_2: Location, check_locked=True): + """Swaps Items of locations. Does NOT swap flags like event, shop_slot or locked""" + if check_locked: + if location_1.locked: + logging.warning(f"Swapping {location_1}, which is marked as locked.") + if location_2.locked: + logging.warning(f"Swapping {location_2}, which is marked as locked.") + location_2.item, location_1.item = location_1.item, location_2.item + location_1.item.location = location_1 + location_2.item.location = location_2 + + def distribute_planned(world): world_name_lookup = {world.player_names[player_id][0]: player_id for player_id in world.player_ids} @@ -368,7 +381,8 @@ def distribute_planned(world): placement: PlandoItem for placement in world.plando_items[player]: if placement.location in key_drop_data: - placement.warn(f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.") + placement.warn( + f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.") continue item = ItemFactory(placement.item, player) target_world: int = placement.world diff --git a/Main.py b/Main.py index 583a5d0a..24484905 100644 --- a/Main.py +++ b/Main.py @@ -8,16 +8,19 @@ import random import time import zlib import concurrent.futures +import typing from BaseClasses import World, CollectionState, Item, Region, Location, Shop from Items import ItemFactory, item_table, item_name_groups -from Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance, SHOP_ID_START +from Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance, \ + SHOP_ID_START from InvertedRegions import create_inverted_regions, mark_dark_world_regions from EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string from Rules import set_rules from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive -from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned +from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned, \ + swap_location_item from ItemPool import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple import Patch @@ -210,56 +213,66 @@ def main(args, seed=None): if world.players > 1: balance_multiworld_progression(world) - blacklist_words = {"Rupee", "Pendant", "Crystal"} - blacklist_words = {item_name for item_name in item_table if any( - blacklist_word in item_name for blacklist_word in blacklist_words)} - blacklist_words.add("Bee") - candidates = [location for location in world.get_locations() if - not location.locked and - not location.shop_slot and - not location.item.name in blacklist_words] + shop_slots: typing.List[Location] = [location for shop_locations in (shop.region.locations for shop in world.shops) + for location in shop_locations if location.shop_slot] - world.random.shuffle(candidates) - shop_slots = [item for sublist in (shop.region.locations for shop in world.shops) for item in sublist if - item.shop_slot] + if shop_slots: + # TODO: allow each game to register a blacklist to be used here? + blacklist_words = {"Rupee", "Pendant", "Crystal"} + blacklist_words = {item_name for item_name in item_table if any( + blacklist_word in item_name for blacklist_word in blacklist_words)} + blacklist_words.add("Bee") + candidates: typing.List[Location] = [location for location in world.get_locations() if + not location.locked and + not location.shop_slot and + not location.item.name in blacklist_words] - for location in shop_slots: - slot_num = int(location.name[-1]) - 1 - shop: Shop = location.parent_region.shop - if shop.can_push_inventory(slot_num): - for c in candidates: # chosen item locations - 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: - # we use this candidate - candidates.remove(c) - break + world.random.shuffle(candidates) + + # currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory + # Potentially create Locations as needed and make inventory the only source, to prevent divergence + + for location in shop_slots: + slot_num = int(location.name[-1]) - 1 + shop: Shop = location.parent_region.shop + if shop.can_push_inventory(slot_num): + for c in candidates: # chosen item locations + if c.item_rule(location.item): # if rule is good... + logging.debug('Swapping {} with {}:: {} ||| {}'.format(c, location, c.item, location.item)) + swap_location_item(c, location, check_locked=False) + # TODO: should likely be (all_state-c.item).can_reach(shop.region) + # can still be can_beat_game when beatable-only for the item-owning player + # appears to be the main source of "progression items unreachable Exception" + # in door-rando + multishop + if not world.can_beat_game(): + # swap back + swap_location_item(c, location, check_locked=False) + else: + # we use this candidate + candidates.remove(c) + break + else: + # This *should* never happen. But let's fail safely just in case. + logging.warning("Ran out of ShopShuffle Item candidate locations.") + shop.region.locations.remove(location) + continue + + item_name = location.item.name + if any(x in item_name for x in ['Single Bomb', 'Single Arrow']): + price = world.random.randrange(1, 7) + elif any(x in item_name for x in ['Arrows', 'Bombs', 'Clock']): + price = world.random.randrange(4, 24) + elif any(x in item_name for x in ['Compass', 'Map', 'Small Key', 'Piece of Heart']): + price = world.random.randrange(10, 30) + else: + price = world.random.randrange(10, 60) + + price *= 5 + + shop.push_inventory(slot_num, item_name, price, 1, + location.item.player if location.item.player != location.player else 0) else: - # This *should* never happen. But let's fail safely just in case. - logging.warning("Ran out of ShopShuffle Item candidate locations.") shop.region.locations.remove(location) - continue - - # update table to location data - item_name = location.item.name - if any(x in item_name for x in ['Single Bomb', 'Single Arrow']): - price = world.random.randrange(1, 7) - elif any(x in item_name for x in ['Arrows', 'Bombs', 'Clock']): - price = world.random.randrange(4, 24) - elif any(x in item_name for x in ['Compass', 'Map', 'Small Key', 'Piece of Heart']): - price = world.random.randrange(10, 30) - else: - price = world.random.randrange(10, 60) - - price *= 5 - - shop.push_inventory(slot_num, item_name, price, 1, - location.item.player if location.item.player != location.player else 0) - else: - shop.region.locations.remove(location) logger.info('Patching ROM.') @@ -302,7 +315,6 @@ def main(args, seed=None): args.fastmenu[player], args.disablemusic[player], args.sprite[player], palettes_options, world, player, True) - mcsb_name = '' if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):