Merge branch 'multishop'
This commit is contained in:
		
						commit
						a6dad66b59
					
				
							
								
								
									
										209
									
								
								BaseClasses.py
								
								
								
								
							
							
						
						
									
										209
									
								
								BaseClasses.py
								
								
								
								
							| 
						 | 
				
			
			@ -5,12 +5,11 @@ from enum import Enum, unique
 | 
			
		|||
import logging
 | 
			
		||||
import json
 | 
			
		||||
from collections import OrderedDict, Counter, deque
 | 
			
		||||
from typing import Union, Optional, List, Set, Dict, NamedTuple, Iterable
 | 
			
		||||
from typing import Union, Optional, List, Dict, NamedTuple, Iterable
 | 
			
		||||
import secrets
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from EntranceShuffle import door_addresses, indirect_connections
 | 
			
		||||
from Utils import int16_as_bytes
 | 
			
		||||
from EntranceShuffle import indirect_connections
 | 
			
		||||
from Items import item_name_groups
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -134,6 +133,7 @@ class World(object):
 | 
			
		|||
            set_player_attr('triforce_pieces_available', 30)
 | 
			
		||||
            set_player_attr('triforce_pieces_required', 20)
 | 
			
		||||
            set_player_attr('shop_shuffle', 'off')
 | 
			
		||||
            set_player_attr('shop_shuffle_slots', 0)
 | 
			
		||||
            set_player_attr('shuffle_prizes', "g")
 | 
			
		||||
            set_player_attr('sprite_pool', [])
 | 
			
		||||
            set_player_attr('dark_room_logic', "lamp")
 | 
			
		||||
| 
						 | 
				
			
			@ -415,7 +415,7 @@ class World(object):
 | 
			
		|||
        else:
 | 
			
		||||
            return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
 | 
			
		||||
 | 
			
		||||
    def can_beat_game(self, starting_state=None):
 | 
			
		||||
    def can_beat_game(self, starting_state : Optional[CollectionState]=None):
 | 
			
		||||
        if starting_state:
 | 
			
		||||
            if self.has_beaten_game(starting_state):
 | 
			
		||||
                return True
 | 
			
		||||
| 
						 | 
				
			
			@ -447,6 +447,87 @@ class World(object):
 | 
			
		|||
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def get_spheres(self):
 | 
			
		||||
        state = CollectionState(self)
 | 
			
		||||
 | 
			
		||||
        locations = {location for location in self.get_locations()}
 | 
			
		||||
 | 
			
		||||
        while locations:
 | 
			
		||||
            sphere = set()
 | 
			
		||||
 | 
			
		||||
            for location in locations:
 | 
			
		||||
                if location.can_reach(state):
 | 
			
		||||
                    sphere.add(location)
 | 
			
		||||
            yield sphere
 | 
			
		||||
            if not sphere:
 | 
			
		||||
                if locations:
 | 
			
		||||
                    yield locations  # unreachable locations
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            for location in sphere:
 | 
			
		||||
                state.collect(location.item, True, location)
 | 
			
		||||
            locations -= sphere
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def fulfills_accessibility(self, state: Optional[CollectionState] = None):
 | 
			
		||||
        """Check if accessibility rules are fulfilled with current or supplied state."""
 | 
			
		||||
        if not state:
 | 
			
		||||
            state = CollectionState(self)
 | 
			
		||||
        players = {"none" : set(),
 | 
			
		||||
                   "items": set(),
 | 
			
		||||
                   "locations": set()}
 | 
			
		||||
        for player, access in self.accessibility.items():
 | 
			
		||||
            players[access].add(player)
 | 
			
		||||
 | 
			
		||||
        beatable_fulfilled = False
 | 
			
		||||
 | 
			
		||||
        def location_conditition(location : Location):
 | 
			
		||||
            """Determine if this location has to be accessible, location is already filtered by location_relevant"""
 | 
			
		||||
            if location.player in players["none"]:
 | 
			
		||||
                return False
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        def location_relevant(location : Location):
 | 
			
		||||
            """Determine if this location is relevant to sweep."""
 | 
			
		||||
            if location.player in players["locations"] or location.event or \
 | 
			
		||||
                    (location.item and location.item.advancement):
 | 
			
		||||
                return True
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        def all_done():
 | 
			
		||||
            """Check if all access rules are fulfilled"""
 | 
			
		||||
            if beatable_fulfilled:
 | 
			
		||||
                if any(location_conditition(location) for location in locations):
 | 
			
		||||
                    return False  # still locations required to be collected
 | 
			
		||||
                return True
 | 
			
		||||
 | 
			
		||||
        locations = {location for location in self.get_locations() if location_relevant(location)}
 | 
			
		||||
 | 
			
		||||
        while locations:
 | 
			
		||||
            sphere = set()
 | 
			
		||||
            for location in locations:
 | 
			
		||||
                if location.can_reach(state):
 | 
			
		||||
                    sphere.add(location)
 | 
			
		||||
 | 
			
		||||
            if not sphere:
 | 
			
		||||
                # ran out of places and did not finish yet, quit
 | 
			
		||||
                logging.debug(f"Could not access required locations.")
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
            for location in sphere:
 | 
			
		||||
                locations.remove(location)
 | 
			
		||||
                state.collect(location.item, True, location)
 | 
			
		||||
 | 
			
		||||
            if self.has_beaten_game(state):
 | 
			
		||||
                beatable_fulfilled = True
 | 
			
		||||
 | 
			
		||||
            if all_done():
 | 
			
		||||
                return True
 | 
			
		||||
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CollectionState(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, parent: World):
 | 
			
		||||
| 
						 | 
				
			
			@ -980,7 +1061,7 @@ class Dungeon(object):
 | 
			
		|||
    def __str__(self):
 | 
			
		||||
        return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
 | 
			
		||||
 | 
			
		||||
class Boss(object):
 | 
			
		||||
class Boss():
 | 
			
		||||
    def __init__(self, name, enemizer_name, defeat_rule, player: int):
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.enemizer_name = enemizer_name
 | 
			
		||||
| 
						 | 
				
			
			@ -990,7 +1071,13 @@ class Boss(object):
 | 
			
		|||
    def can_defeat(self, state) -> bool:
 | 
			
		||||
        return self.defeat_rule(state, self.player)
 | 
			
		||||
 | 
			
		||||
class Location(object):
 | 
			
		||||
 | 
			
		||||
class Location():
 | 
			
		||||
    shop_slot: bool = False
 | 
			
		||||
    shop_slot_disabled: bool = False
 | 
			
		||||
    event: bool = False
 | 
			
		||||
    locked: bool = False
 | 
			
		||||
 | 
			
		||||
    def __init__(self, player: int, name: str = '', address=None, crystal: bool = False,
 | 
			
		||||
                 hint_text: Optional[str] = None, parent=None,
 | 
			
		||||
                 player_address=None):
 | 
			
		||||
| 
						 | 
				
			
			@ -1003,8 +1090,6 @@ class Location(object):
 | 
			
		|||
        self.spot_type = 'Location'
 | 
			
		||||
        self.hint_text: str = hint_text if hint_text else name
 | 
			
		||||
        self.recursion_count = 0
 | 
			
		||||
        self.event = False
 | 
			
		||||
        self.locked = False
 | 
			
		||||
        self.always_allow = lambda item, state: False
 | 
			
		||||
        self.access_rule = lambda state: True
 | 
			
		||||
        self.item_rule = lambda item: True
 | 
			
		||||
| 
						 | 
				
			
			@ -1029,6 +1114,9 @@ class Location(object):
 | 
			
		|||
    def __hash__(self):
 | 
			
		||||
        return hash((self.name, self.player))
 | 
			
		||||
 | 
			
		||||
    def __lt__(self, other):
 | 
			
		||||
        return (self.player, self.name) < (other.player, other.name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Item(object):
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1086,105 +1174,6 @@ class Item(object):
 | 
			
		|||
class Crystal(Item):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
@unique
 | 
			
		||||
class ShopType(Enum):
 | 
			
		||||
    Shop = 0
 | 
			
		||||
    TakeAny = 1
 | 
			
		||||
    UpgradeShop = 2
 | 
			
		||||
 | 
			
		||||
class Shop():
 | 
			
		||||
    slots = 3  # slot count is not dynamic in asm, however inventory can have None as empty slots
 | 
			
		||||
    blacklist = set()  # items that don't work, todo: actually check against this
 | 
			
		||||
    type = ShopType.Shop
 | 
			
		||||
 | 
			
		||||
    def __init__(self, region: Region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool):
 | 
			
		||||
        self.region = region
 | 
			
		||||
        self.room_id = room_id
 | 
			
		||||
        self.inventory: List[Union[None, dict]] = [None] * self.slots
 | 
			
		||||
        self.shopkeeper_config = shopkeeper_config
 | 
			
		||||
        self.custom = custom
 | 
			
		||||
        self.locked = locked
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def item_count(self) -> int:
 | 
			
		||||
        for x in range(self.slots - 1, -1, -1):  # last x is 0
 | 
			
		||||
            if self.inventory[x]:
 | 
			
		||||
                return x + 1
 | 
			
		||||
        return 0
 | 
			
		||||
 | 
			
		||||
    def get_bytes(self) -> List[int]:
 | 
			
		||||
        # [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index]
 | 
			
		||||
        entrances = self.region.entrances
 | 
			
		||||
        config = self.item_count
 | 
			
		||||
        if len(entrances) == 1 and entrances[0].name in door_addresses:
 | 
			
		||||
            door_id = door_addresses[entrances[0].name][0] + 1
 | 
			
		||||
        else:
 | 
			
		||||
            door_id = 0
 | 
			
		||||
            config |= 0x40  # ignore door id
 | 
			
		||||
        if self.type == ShopType.TakeAny:
 | 
			
		||||
            config |= 0x80
 | 
			
		||||
        elif self.type == ShopType.UpgradeShop:
 | 
			
		||||
            config |= 0x10  # Alt. VRAM
 | 
			
		||||
        return [0x00]+int16_as_bytes(self.room_id)+[door_id, 0x00, config, self.shopkeeper_config, 0x00]
 | 
			
		||||
 | 
			
		||||
    def has_unlimited(self, item: str) -> bool:
 | 
			
		||||
        for inv in self.inventory:
 | 
			
		||||
            if inv is None:
 | 
			
		||||
                continue
 | 
			
		||||
            if inv['item'] == item:
 | 
			
		||||
                return True
 | 
			
		||||
            if inv['max'] != 0 and inv['replacement'] is not None and inv['replacement'] == item:
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def has(self, item: str) -> bool:
 | 
			
		||||
        for inv in self.inventory:
 | 
			
		||||
            if inv is None:
 | 
			
		||||
                continue
 | 
			
		||||
            if inv['item'] == item:
 | 
			
		||||
                return True
 | 
			
		||||
            if inv['max'] != 0 and inv['replacement'] == item:
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def clear_inventory(self):
 | 
			
		||||
        self.inventory = [None] * self.slots
 | 
			
		||||
 | 
			
		||||
    def add_inventory(self, slot: int, item: str, price: int, max: int = 0,
 | 
			
		||||
                      replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False):
 | 
			
		||||
        self.inventory[slot] = {
 | 
			
		||||
            'item': item,
 | 
			
		||||
            'price': price,
 | 
			
		||||
            'max': max,
 | 
			
		||||
            'replacement': replacement,
 | 
			
		||||
            'replacement_price': replacement_price,
 | 
			
		||||
            'create_location': create_location
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def push_inventory(self, slot: int, item: str, price: int, max: int = 1):
 | 
			
		||||
        if not self.inventory[slot]:
 | 
			
		||||
            raise ValueError("Inventory can't be pushed back if it doesn't exist")
 | 
			
		||||
 | 
			
		||||
        self.inventory[slot] = {
 | 
			
		||||
            'item': item,
 | 
			
		||||
            'price': price,
 | 
			
		||||
            'max': max,
 | 
			
		||||
            'replacement': self.inventory[slot]["item"],
 | 
			
		||||
            'replacement_price': self.inventory[slot]["price"],
 | 
			
		||||
            'create_location': self.inventory[slot]["create_location"]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TakeAny(Shop):
 | 
			
		||||
    type = ShopType.TakeAny
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpgradeShop(Shop):
 | 
			
		||||
    type = ShopType.UpgradeShop
 | 
			
		||||
    # Potions break due to VRAM flags set in UpgradeShop.
 | 
			
		||||
    # Didn't check for more things breaking as not much else can be shuffled here currently
 | 
			
		||||
    blacklist = item_name_groups["Potions"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Spoiler(object):
 | 
			
		||||
    world: World
 | 
			
		||||
| 
						 | 
				
			
			@ -1247,6 +1236,7 @@ class Spoiler(object):
 | 
			
		|||
            listed_locations.update(other_locations)
 | 
			
		||||
 | 
			
		||||
        self.shops = []
 | 
			
		||||
        from Shops import ShopType
 | 
			
		||||
        for shop in self.world.shops:
 | 
			
		||||
            if not shop.custom:
 | 
			
		||||
                continue
 | 
			
		||||
| 
						 | 
				
			
			@ -1257,6 +1247,10 @@ class Spoiler(object):
 | 
			
		|||
                if item is None:
 | 
			
		||||
                    continue
 | 
			
		||||
                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:
 | 
			
		||||
                    continue
 | 
			
		||||
                shopdata['item_{}'.format(index)] += " x {}".format(item['max'])
 | 
			
		||||
| 
						 | 
				
			
			@ -1330,6 +1324,7 @@ class Spoiler(object):
 | 
			
		|||
                         'triforce_pieces_available': self.world.triforce_pieces_available,
 | 
			
		||||
                         'triforce_pieces_required': self.world.triforce_pieces_required,
 | 
			
		||||
                         'shop_shuffle': self.world.shop_shuffle,
 | 
			
		||||
                         'shop_shuffle_slots': self.world.shop_shuffle_slots,
 | 
			
		||||
                         'shuffle_prizes': self.world.shuffle_prizes,
 | 
			
		||||
                         'sprite_pool': self.world.sprite_pool,
 | 
			
		||||
                         'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -144,7 +144,7 @@ def fill_dungeons_restrictive(world):
 | 
			
		|||
        # sort in the order Big Key, Small Key, Other before placing dungeon items
 | 
			
		||||
        sort_order = {"BigKey": 3, "SmallKey": 2}
 | 
			
		||||
        dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
 | 
			
		||||
        fill_restrictive(world, all_state_base, locations, dungeon_items, True)
 | 
			
		||||
        fill_restrictive(world, all_state_base, locations, dungeon_items, True, True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -326,9 +326,17 @@ 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('--shop_shuffle', default='', help='''\
 | 
			
		||||
    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
 | 
			
		||||
    u: shuffle capacity upgrades into the item pool
 | 
			
		||||
    w: consider witch's hut like any other shop and shuffle/randomize it too
 | 
			
		||||
    ''')
 | 
			
		||||
    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('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb'])
 | 
			
		||||
    parser.add_argument('--sprite_pool', help='''\
 | 
			
		||||
| 
						 | 
				
			
			@ -390,7 +398,8 @@ def parse_arguments(argv, no_defaults=False):
 | 
			
		|||
                         'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
 | 
			
		||||
                         'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
 | 
			
		||||
                         'heartbeep', "skip_progression_balancing", "triforce_pieces_available",
 | 
			
		||||
                         "triforce_pieces_required", "shop_shuffle", "required_medallions",
 | 
			
		||||
                         "triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots",
 | 
			
		||||
                         "required_medallions",
 | 
			
		||||
                         "plando_items", "plando_texts", "plando_connections",
 | 
			
		||||
                         'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
 | 
			
		||||
                         'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										27
									
								
								Fill.py
								
								
								
								
							
							
						
						
									
										27
									
								
								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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,8 @@ class FillError(RuntimeError):
 | 
			
		|||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fill_restrictive(world, base_state: CollectionState, locations, itempool, single_player_placement=False):
 | 
			
		||||
def fill_restrictive(world, base_state: CollectionState, locations, itempool, single_player_placement=False,
 | 
			
		||||
                     lock=False):
 | 
			
		||||
    def sweep_from_pool():
 | 
			
		||||
        new_state = base_state.copy()
 | 
			
		||||
        for item in itempool:
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +60,8 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
 | 
			
		|||
                                    f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
 | 
			
		||||
 | 
			
		||||
                world.push_item(spot_to_fill, item_to_place, False)
 | 
			
		||||
                if lock:
 | 
			
		||||
                    spot_to_fill.locked = True
 | 
			
		||||
                locations.remove(spot_to_fill)
 | 
			
		||||
                placements.append(spot_to_fill)
 | 
			
		||||
                spot_to_fill.event = True
 | 
			
		||||
| 
						 | 
				
			
			@ -168,6 +171,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
 | 
			
		|||
                fill_locations.remove(spot_to_fill)
 | 
			
		||||
 | 
			
		||||
    world.random.shuffle(fill_locations)
 | 
			
		||||
 | 
			
		||||
    prioitempool, fill_locations = fast_fill(world, prioitempool, fill_locations)
 | 
			
		||||
 | 
			
		||||
    restitempool, fill_locations = fast_fill(world, restitempool, fill_locations)
 | 
			
		||||
| 
						 | 
				
			
			@ -244,6 +248,7 @@ def flood_items(world):
 | 
			
		|||
                itempool.remove(item_to_place)
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def balance_multiworld_progression(world):
 | 
			
		||||
    balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]}
 | 
			
		||||
    if not balanceable_players:
 | 
			
		||||
| 
						 | 
				
			
			@ -331,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}")
 | 
			
		||||
| 
						 | 
				
			
			@ -355,6 +361,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}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -362,7 +380,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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										36
									
								
								ItemPool.py
								
								
								
								
							
							
						
						
									
										36
									
								
								ItemPool.py
								
								
								
								
							| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
from collections import namedtuple
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from BaseClasses import Region, RegionType, ShopType, Location, TakeAny
 | 
			
		||||
from BaseClasses import Region, RegionType, Location
 | 
			
		||||
from Shops import ShopType, Shop, TakeAny, total_shop_slots
 | 
			
		||||
from Bosses import place_bosses
 | 
			
		||||
from Dungeons import get_dungeon_item_pool
 | 
			
		||||
from EntranceShuffle import connect_entrance
 | 
			
		||||
| 
						 | 
				
			
			@ -460,10 +461,12 @@ def shuffle_shops(world, items, player: int):
 | 
			
		|||
 | 
			
		||||
        world.random.shuffle(new_items)  # Decide what gets tossed randomly if it can't insert everything.
 | 
			
		||||
 | 
			
		||||
        capacityshop: Shop = None
 | 
			
		||||
        for shop in world.shops:
 | 
			
		||||
            if shop.type == ShopType.UpgradeShop and shop.region.player == player and \
 | 
			
		||||
                    shop.region.name == "Capacity Upgrade":
 | 
			
		||||
                shop.clear_inventory()
 | 
			
		||||
                capacityshop = shop
 | 
			
		||||
 | 
			
		||||
        if world.goal[player] != 'icerodhunt':
 | 
			
		||||
            for i, item in enumerate(items):
 | 
			
		||||
| 
						 | 
				
			
			@ -472,7 +475,13 @@ def shuffle_shops(world, items, player: int):
 | 
			
		|||
                    if not new_items:
 | 
			
		||||
                        break
 | 
			
		||||
            else:
 | 
			
		||||
                logging.warning(f"Not all upgrades put into Player{player}' item pool. Still missing: {new_items}")
 | 
			
		||||
                logging.warning(f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
 | 
			
		||||
                bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item)
 | 
			
		||||
                arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
 | 
			
		||||
                if bombupgrades:
 | 
			
		||||
                    capacityshop.add_inventory(1, 'Bomb Upgrade (+5)', 100, bombupgrades)
 | 
			
		||||
                if arrowupgrades:
 | 
			
		||||
                    capacityshop.add_inventory(1, 'Arrow Upgrade (+5)', 100, arrowupgrades)
 | 
			
		||||
        else:
 | 
			
		||||
            for item in new_items:
 | 
			
		||||
                world.push_precollected(ItemFactory(item, player))
 | 
			
		||||
| 
						 | 
				
			
			@ -485,14 +494,19 @@ def shuffle_shops(world, items, player: int):
 | 
			
		|||
            if shop.region.player == player:
 | 
			
		||||
                if shop.type == ShopType.UpgradeShop:
 | 
			
		||||
                    upgrade_shops.append(shop)
 | 
			
		||||
                elif shop.type == ShopType.Shop and shop.region.name != 'Potion Shop':
 | 
			
		||||
                    shops.append(shop)
 | 
			
		||||
                    total_inventory.extend(shop.inventory)
 | 
			
		||||
                elif shop.type == ShopType.Shop:
 | 
			
		||||
                    if shop.region.name == 'Potion Shop' and not 'w' in option:
 | 
			
		||||
                        # don't modify potion shop
 | 
			
		||||
                        pass
 | 
			
		||||
                    else:
 | 
			
		||||
                        shops.append(shop)
 | 
			
		||||
                        total_inventory.extend(shop.inventory)
 | 
			
		||||
 | 
			
		||||
        if 'p' in option:
 | 
			
		||||
            def price_adjust(price: int) -> int:
 | 
			
		||||
                # 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):
 | 
			
		||||
                if item:
 | 
			
		||||
| 
						 | 
				
			
			@ -507,6 +521,7 @@ def shuffle_shops(world, items, player: int):
 | 
			
		|||
 | 
			
		||||
        if 'i' in option:
 | 
			
		||||
            world.random.shuffle(total_inventory)
 | 
			
		||||
            
 | 
			
		||||
            i = 0
 | 
			
		||||
            for shop in shops:
 | 
			
		||||
                slots = shop.slots
 | 
			
		||||
| 
						 | 
				
			
			@ -548,7 +563,7 @@ def set_up_take_anys(world, player):
 | 
			
		|||
    entrance = world.get_region(reg, player).entrances[0]
 | 
			
		||||
    connect_entrance(world, entrance.name, old_man_take_any.name, player)
 | 
			
		||||
    entrance.target = 0x58
 | 
			
		||||
    old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True)
 | 
			
		||||
    old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
 | 
			
		||||
    world.shops.append(old_man_take_any.shop)
 | 
			
		||||
 | 
			
		||||
    swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
 | 
			
		||||
| 
						 | 
				
			
			@ -570,7 +585,7 @@ def set_up_take_anys(world, player):
 | 
			
		|||
        entrance = world.get_region(reg, player).entrances[0]
 | 
			
		||||
        connect_entrance(world, entrance.name, take_any.name, player)
 | 
			
		||||
        entrance.target = target
 | 
			
		||||
        take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True)
 | 
			
		||||
        take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1)
 | 
			
		||||
        world.shops.append(take_any.shop)
 | 
			
		||||
        take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
 | 
			
		||||
        take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -584,13 +599,14 @@ def create_dynamic_shop_locations(world, player):
 | 
			
		|||
                if item is None:
 | 
			
		||||
                    continue
 | 
			
		||||
                if item['create_location']:
 | 
			
		||||
                    loc = Location(player, "{} Item {}".format(shop.region.name, i+1), parent=shop.region)
 | 
			
		||||
                    loc = Location(player, "{} Slot {}".format(shop.region.name, i + 1), parent=shop.region)
 | 
			
		||||
                    shop.region.locations.append(loc)
 | 
			
		||||
                    world.dynamic_locations.append(loc)
 | 
			
		||||
 | 
			
		||||
                    world.clear_location_cache()
 | 
			
		||||
 | 
			
		||||
                    world.push_item(loc, ItemFactory(item['item'], player), False)
 | 
			
		||||
                    loc.shop_slot = True
 | 
			
		||||
                    loc.event = True
 | 
			
		||||
                    loc.locked = True
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -611,7 +627,7 @@ def fill_prizes(world, attempts=15):
 | 
			
		|||
                prize_locs = list(empty_crystal_locations)
 | 
			
		||||
                world.random.shuffle(prizepool)
 | 
			
		||||
                world.random.shuffle(prize_locs)
 | 
			
		||||
                fill_restrictive(world, all_state, prize_locs, prizepool, True)
 | 
			
		||||
                fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
 | 
			
		||||
            except FillError as e:
 | 
			
		||||
                logging.getLogger('').exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
 | 
			
		||||
                                                attempts - attempt)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								Items.py
								
								
								
								
							
							
						
						
									
										6
									
								
								Items.py
								
								
								
								
							| 
						 | 
				
			
			@ -169,6 +169,12 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
 | 
			
		|||
              'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
 | 
			
		||||
              'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'),
 | 
			
		||||
              'Bee Trap': (False, False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'Friendship'),
 | 
			
		||||
              'Faerie': (False, False, None, 0xB1, 'Save me and I will revive you', 'and the captive', 'the tingle kid','hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a faerie'),
 | 
			
		||||
              'Good Bee': (False, False, None, 0xB2, 'Save me and I will sting you (sometimes)', 'and the captive', 'the tingle kid','hostage for sale', 'good dust and shrooms', 'bottle boy has friend again', 'a bee'),
 | 
			
		||||
              'Magic Jar': (False, False, None, 0xB3, '', '', '','', '', '', ''),
 | 
			
		||||
              'Apple': (False, False, None, 0xB4, '', '', '','', '', '', ''),
 | 
			
		||||
               #   'Hint': (False, False, None, 0xB5, '', '', '','', '', '', ''),
 | 
			
		||||
               #   'Bomb Trap': (False, False, None, 0xB6, '', '', '','', '', '', ''),
 | 
			
		||||
              'Red Potion': (False, False, None, 0x2E, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
 | 
			
		||||
              'Green Potion': (False, False, None, 0x2F, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
 | 
			
		||||
              'Blue Potion': (False, False, None, 0x30, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										54
									
								
								Main.py
								
								
								
								
							
							
						
						
									
										54
									
								
								Main.py
								
								
								
								
							| 
						 | 
				
			
			@ -9,9 +9,10 @@ import time
 | 
			
		|||
import zlib
 | 
			
		||||
import concurrent.futures
 | 
			
		||||
 | 
			
		||||
from BaseClasses import World, CollectionState, Item, Region, Location, PlandoItem
 | 
			
		||||
from BaseClasses import World, CollectionState, Item, Region, Location
 | 
			
		||||
from Shops import ShopSlotFill, create_shops, SHOP_ID_START, FillDisabledShopSlots
 | 
			
		||||
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
 | 
			
		||||
from Regions import create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -83,6 +84,7 @@ def main(args, seed=None):
 | 
			
		|||
    world.triforce_pieces_available = args.triforce_pieces_available.copy()
 | 
			
		||||
    world.triforce_pieces_required = args.triforce_pieces_required.copy()
 | 
			
		||||
    world.shop_shuffle = args.shop_shuffle.copy()
 | 
			
		||||
    world.shop_shuffle_slots = args.shop_shuffle_slots.copy()
 | 
			
		||||
    world.progression_balancing = {player: not balance for player, balance in args.skip_progression_balancing.items()}
 | 
			
		||||
    world.shuffle_prizes = args.shuffle_prizes.copy()
 | 
			
		||||
    world.sprite_pool = args.sprite_pool.copy()
 | 
			
		||||
| 
						 | 
				
			
			@ -209,8 +211,14 @@ def main(args, seed=None):
 | 
			
		|||
    if world.players > 1:
 | 
			
		||||
        balance_multiworld_progression(world)
 | 
			
		||||
 | 
			
		||||
    logger.info("Filling Shop Slots")
 | 
			
		||||
 | 
			
		||||
    ShopSlotFill(world)
 | 
			
		||||
 | 
			
		||||
    logger.info('Patching ROM.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    outfilebase = 'BM_%s' % (args.outputname if args.outputname else world.seed)
 | 
			
		||||
 | 
			
		||||
    rom_names = []
 | 
			
		||||
| 
						 | 
				
			
			@ -245,7 +253,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]]):
 | 
			
		||||
| 
						 | 
				
			
			@ -281,7 +288,7 @@ def main(args, seed=None):
 | 
			
		|||
          "progressive": world.progressive,                                # A
 | 
			
		||||
          "hints": 'True' if world.hints[player] else 'False'              # B
 | 
			
		||||
        }
 | 
			
		||||
        #                  0  1  2  3  4 5  6  7 8 9 A B 
 | 
			
		||||
        #                  0  1  2  3  4 5  6  7 8 9 A B
 | 
			
		||||
        outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (
 | 
			
		||||
          #  0          1      2      3    4     5    6      7     8        9         A     B           C
 | 
			
		||||
          # _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints
 | 
			
		||||
| 
						 | 
				
			
			@ -312,7 +319,7 @@ def main(args, seed=None):
 | 
			
		|||
 | 
			
		||||
    pool = concurrent.futures.ThreadPoolExecutor()
 | 
			
		||||
    multidata_task = None
 | 
			
		||||
    check_beatability_task = pool.submit(world.can_beat_game)
 | 
			
		||||
    check_accessibility_task = pool.submit(world.fulfills_accessibility)
 | 
			
		||||
    if not args.suppress_rom:
 | 
			
		||||
 | 
			
		||||
        rom_futures = []
 | 
			
		||||
| 
						 | 
				
			
			@ -329,13 +336,14 @@ def main(args, seed=None):
 | 
			
		|||
                return get_entrance_to_region(entrance.parent_region)
 | 
			
		||||
 | 
			
		||||
        # collect ER hint info
 | 
			
		||||
        er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla"}
 | 
			
		||||
        er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla" or world.retro[player]}
 | 
			
		||||
        from Regions import RegionType
 | 
			
		||||
        for region in world.regions:
 | 
			
		||||
            if region.player in er_hint_data and region.locations:
 | 
			
		||||
                main_entrance = get_entrance_to_region(region)
 | 
			
		||||
                for location in region.locations:
 | 
			
		||||
                    if type(location.address) == int:  # skips events and crystals
 | 
			
		||||
                        if location.address >= SHOP_ID_START + 33:  continue
 | 
			
		||||
                        if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
 | 
			
		||||
                            er_hint_data[region.player][location.address] = main_entrance.name
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -362,11 +370,27 @@ def main(args, seed=None):
 | 
			
		|||
                checks_in_area[location.player]["Dark World"].append(location.address)
 | 
			
		||||
            checks_in_area[location.player]["Total"] += 1
 | 
			
		||||
 | 
			
		||||
        oldmancaves = []
 | 
			
		||||
        for region in [world.get_region("Old Man Sword Cave", player) for player in range(1, world.players + 1) if world.retro[player]]:
 | 
			
		||||
            item = ItemFactory(region.shop.inventory[0]['item'], region.player)
 | 
			
		||||
            player = region.player
 | 
			
		||||
            location_id = SHOP_ID_START + 33
 | 
			
		||||
 | 
			
		||||
            if region.type == RegionType.LightWorld:
 | 
			
		||||
                checks_in_area[player]["Light World"].append(location_id)
 | 
			
		||||
            else:
 | 
			
		||||
                checks_in_area[player]["Dark World"].append(location_id)
 | 
			
		||||
            checks_in_area[player]["Total"] += 1
 | 
			
		||||
 | 
			
		||||
            er_hint_data[player][location_id] = get_entrance_to_region(region).name
 | 
			
		||||
            oldmancaves.append(((location_id, player), (item.code, player)))
 | 
			
		||||
 | 
			
		||||
        precollected_items = [[] for player in range(world.players)]
 | 
			
		||||
        for item in world.precollected_items:
 | 
			
		||||
            precollected_items[item.player - 1].append(item.code)
 | 
			
		||||
 | 
			
		||||
        FillDisabledShopSlots(world)
 | 
			
		||||
 | 
			
		||||
        def write_multidata(roms):
 | 
			
		||||
            for future in roms:
 | 
			
		||||
                rom_name = future.result()
 | 
			
		||||
| 
						 | 
				
			
			@ -378,7 +402,11 @@ def main(args, seed=None):
 | 
			
		|||
                multidatatags.append("Spoiler")
 | 
			
		||||
                if not args.skip_playthrough:
 | 
			
		||||
                    multidatatags.append("Play through")
 | 
			
		||||
            minimum_versions = {"server": (1,0,0)}
 | 
			
		||||
            minimum_versions = {"server": (1, 0, 0)}
 | 
			
		||||
            minimum_versions["clients"] = client_versions = []
 | 
			
		||||
            for (slot, team, name) in rom_names:
 | 
			
		||||
                if world.shop_shuffle_slots[slot]:
 | 
			
		||||
                    client_versions.append([team, slot, [3, 6, 1]])
 | 
			
		||||
            multidata = zlib.compress(json.dumps({"names": parsed_names,
 | 
			
		||||
                                                  # backwards compat for < 2.4.1
 | 
			
		||||
                                                  "roms": [(slot, team, list(name.encode()))
 | 
			
		||||
| 
						 | 
				
			
			@ -389,7 +417,7 @@ def main(args, seed=None):
 | 
			
		|||
                                                  "locations": [((location.address, location.player),
 | 
			
		||||
                                                                 (location.item.code, location.item.player))
 | 
			
		||||
                                                                for location in world.get_filled_locations() if
 | 
			
		||||
                                                                type(location.address) is int],
 | 
			
		||||
                                                                type(location.address) is int] + oldmancaves,
 | 
			
		||||
                                                  "checks_in_area": checks_in_area,
 | 
			
		||||
                                                  "server_options": get_options()["server_options"],
 | 
			
		||||
                                                  "er_hint_data": er_hint_data,
 | 
			
		||||
| 
						 | 
				
			
			@ -403,8 +431,11 @@ def main(args, seed=None):
 | 
			
		|||
                f.write(multidata)
 | 
			
		||||
 | 
			
		||||
        multidata_task = pool.submit(write_multidata, rom_futures)
 | 
			
		||||
    if not check_beatability_task.result():
 | 
			
		||||
        raise Exception("Game appears unbeatable. Aborting.")
 | 
			
		||||
    if not check_accessibility_task.result():
 | 
			
		||||
        if not world.can_beat_game():
 | 
			
		||||
            raise Exception("Game appears is unbeatable. Aborting.")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.warning("Location Accessibility requirements not fulfilled.")
 | 
			
		||||
    if not args.skip_playthrough:
 | 
			
		||||
        logger.info('Calculating playthrough.')
 | 
			
		||||
        create_playthrough(world)
 | 
			
		||||
| 
						 | 
				
			
			@ -458,6 +489,7 @@ def copy_world(world):
 | 
			
		|||
    ret.shufflepots = world.shufflepots.copy()
 | 
			
		||||
    ret.shuffle_prizes = world.shuffle_prizes.copy()
 | 
			
		||||
    ret.shop_shuffle =  world.shop_shuffle.copy()
 | 
			
		||||
    ret.shop_shuffle_slots = world.shop_shuffle_slots.copy()
 | 
			
		||||
    ret.dark_room_logic = world.dark_room_logic.copy()
 | 
			
		||||
    ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -530,7 +562,7 @@ def copy_dynamic_regions_and_locations(world, ret):
 | 
			
		|||
 | 
			
		||||
        if region.shop:
 | 
			
		||||
            new_reg.shop = region.shop.__class__(new_reg, region.shop.room_id, region.shop.shopkeeper_config,
 | 
			
		||||
                                                 region.shop.custom, region.shop.locked)
 | 
			
		||||
                                                 region.shop.custom, region.shop.locked, region.shop.sram_offset)
 | 
			
		||||
            ret.shops.append(new_reg.shop)
 | 
			
		||||
 | 
			
		||||
    for location in world.dynamic_locations:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ import shutil
 | 
			
		|||
 | 
			
		||||
from random import randrange
 | 
			
		||||
 | 
			
		||||
import Shops
 | 
			
		||||
from Utils import get_item_name_from_id, get_location_name_from_address, ReceivedItem
 | 
			
		||||
 | 
			
		||||
exit_func = atexit.register(input, "Press enter to close.")
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +94,6 @@ class Context():
 | 
			
		|||
        self.player_names: typing.Dict[int: str] = {}
 | 
			
		||||
        self.locations_recognized = set()
 | 
			
		||||
        self.locations_checked = set()
 | 
			
		||||
        self.unsafe_locations_checked = set()
 | 
			
		||||
        self.locations_scouted = set()
 | 
			
		||||
        self.items_received = []
 | 
			
		||||
        self.items_missing = []
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +104,6 @@ class Context():
 | 
			
		|||
        self.prev_rom = None
 | 
			
		||||
        self.auth = None
 | 
			
		||||
        self.found_items = found_items
 | 
			
		||||
        self.send_unsafe = False
 | 
			
		||||
        self.finished_game = False
 | 
			
		||||
        self.slow_mode = False
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -168,6 +167,11 @@ SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7        # 1 byte
 | 
			
		|||
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8   # 1 byte
 | 
			
		||||
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9       # 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 Shops.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 Shops.shop_table.items()])
 | 
			
		||||
 | 
			
		||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
 | 
			
		||||
                     "Blind's Hideout - Left": (0x11d, 0x20),
 | 
			
		||||
| 
						 | 
				
			
			@ -878,9 +882,6 @@ async def process_server_cmd(ctx: Context, cmd, args):
 | 
			
		|||
        raise Exception('Connection refused by the multiworld host, no reason provided')
 | 
			
		||||
 | 
			
		||||
    elif cmd == 'Connected':
 | 
			
		||||
        if ctx.send_unsafe:
 | 
			
		||||
            ctx.send_unsafe = False
 | 
			
		||||
            logger.info(f'Turning off sending of ALL location checks not declared as missing.  If you want it on, please use /send_unsafe true')
 | 
			
		||||
        Utils.persistent_store("servers", "default", ctx.server_address)
 | 
			
		||||
        Utils.persistent_store("servers", ctx.rom, ctx.server_address)
 | 
			
		||||
        ctx.team, ctx.slot = args[0]
 | 
			
		||||
| 
						 | 
				
			
			@ -1131,15 +1132,6 @@ class ClientCommandProcessor(CommandProcessor):
 | 
			
		|||
        else:
 | 
			
		||||
            self.output("Web UI was never started.")
 | 
			
		||||
 | 
			
		||||
    def _cmd_send_unsafe(self, toggle: str = ""):
 | 
			
		||||
        """Force sending of locations the server did not specify was actually missing. WARNING: This may brick online trackers. Turned off on reconnect."""
 | 
			
		||||
        if toggle:
 | 
			
		||||
            self.ctx.send_unsafe = toggle.lower() in {"1", "true", "on"}
 | 
			
		||||
            logger.info(f'Turning {("on" if self.ctx.send_unsafe else "off")} the option to send ALL location checks to the multiserver.')
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info("You must specify /send_unsafe true explicitly.")
 | 
			
		||||
            self.ctx.send_unsafe = False
 | 
			
		||||
 | 
			
		||||
    def default(self, raw: str):
 | 
			
		||||
        asyncio.create_task(self.ctx.send_msgs([['Say', raw]]))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1169,11 +1161,11 @@ async def track_locations(ctx : Context, roomid, roomdata):
 | 
			
		|||
    new_locations = []
 | 
			
		||||
 | 
			
		||||
    def new_check(location):
 | 
			
		||||
        ctx.unsafe_locations_checked.add(location)
 | 
			
		||||
        ctx.locations_checked.add(location)
 | 
			
		||||
 | 
			
		||||
        check = None
 | 
			
		||||
        if ctx.items_checked is None:
 | 
			
		||||
            check = f'New Check: {location} ({len(ctx.unsafe_locations_checked)}/{len(Regions.lookup_name_to_id)})'
 | 
			
		||||
            check = f'New Check: {location} ({len(ctx.locations_checked)}/{len(Regions.lookup_name_to_id)})'
 | 
			
		||||
        else:
 | 
			
		||||
            items_total = len(ctx.items_missing) + len(ctx.items_checked)
 | 
			
		||||
            if location in ctx.items_missing or location in ctx.items_checked:
 | 
			
		||||
| 
						 | 
				
			
			@ -1184,9 +1176,21 @@ async def track_locations(ctx : Context, roomid, roomdata):
 | 
			
		|||
            logger.info(check)
 | 
			
		||||
        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)+5)
 | 
			
		||||
            for cnt, b in enumerate(misc_data):
 | 
			
		||||
                my_check = Shops.shop_table_by_location_id[Shops.SHOP_ID_START + cnt]
 | 
			
		||||
                if int(b) > 0 and my_check not in ctx.locations_checked:
 | 
			
		||||
                    new_check(my_check)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(e)
 | 
			
		||||
        logger.info(f"Exception: {e}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    for location, (loc_roomid, loc_mask) in location_table_uw.items():
 | 
			
		||||
        try:
 | 
			
		||||
            if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
 | 
			
		||||
            if location not in ctx.locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
 | 
			
		||||
                new_check(location)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.exception(f"Exception: {e}")
 | 
			
		||||
| 
						 | 
				
			
			@ -1195,7 +1199,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
 | 
			
		|||
    uw_end = 0
 | 
			
		||||
    uw_unchecked = {}
 | 
			
		||||
    for location, (roomid, mask) in location_table_uw.items():
 | 
			
		||||
        if location not in ctx.unsafe_locations_checked:
 | 
			
		||||
        if location not in ctx.locations_checked:
 | 
			
		||||
            uw_unchecked[location] = (roomid, mask)
 | 
			
		||||
            uw_begin = min(uw_begin, roomid)
 | 
			
		||||
            uw_end = max(uw_end, roomid + 1)
 | 
			
		||||
| 
						 | 
				
			
			@ -1212,7 +1216,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
 | 
			
		|||
    ow_end = 0
 | 
			
		||||
    ow_unchecked = {}
 | 
			
		||||
    for location, screenid in location_table_ow.items():
 | 
			
		||||
        if location not in ctx.unsafe_locations_checked:
 | 
			
		||||
        if location not in ctx.locations_checked:
 | 
			
		||||
            ow_unchecked[location] = screenid
 | 
			
		||||
            ow_begin = min(ow_begin, screenid)
 | 
			
		||||
            ow_end = max(ow_end, screenid + 1)
 | 
			
		||||
| 
						 | 
				
			
			@ -1223,26 +1227,30 @@ async def track_locations(ctx : Context, roomid, roomdata):
 | 
			
		|||
                if ow_data[screenid - ow_begin] & 0x40 != 0:
 | 
			
		||||
                    new_check(location)
 | 
			
		||||
 | 
			
		||||
    if not all([location in ctx.unsafe_locations_checked for location in location_table_npc.keys()]):
 | 
			
		||||
    if not all([location in ctx.locations_checked for location in location_table_npc.keys()]):
 | 
			
		||||
        npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
 | 
			
		||||
        if npc_data is not None:
 | 
			
		||||
            npc_value = npc_data[0] | (npc_data[1] << 8)
 | 
			
		||||
            for location, mask in location_table_npc.items():
 | 
			
		||||
                if npc_value & mask != 0 and location not in ctx.unsafe_locations_checked:
 | 
			
		||||
                if npc_value & mask != 0 and location not in ctx.locations_checked:
 | 
			
		||||
                    new_check(location)
 | 
			
		||||
 | 
			
		||||
    if not all([location in ctx.unsafe_locations_checked for location in location_table_misc.keys()]):
 | 
			
		||||
    if not all([location in ctx.locations_checked for location in location_table_misc.keys()]):
 | 
			
		||||
        misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
 | 
			
		||||
        if misc_data is not None:
 | 
			
		||||
            for location, (offset, mask) in location_table_misc.items():
 | 
			
		||||
                assert(0x3c6 <= offset <= 0x3c9)
 | 
			
		||||
                if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.unsafe_locations_checked:
 | 
			
		||||
                if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked:
 | 
			
		||||
                    new_check(location)
 | 
			
		||||
 | 
			
		||||
    for location in ctx.unsafe_locations_checked:
 | 
			
		||||
        if (location in ctx.items_missing and location not in ctx.locations_checked) or ctx.send_unsafe:
 | 
			
		||||
            ctx.locations_checked.add(location)
 | 
			
		||||
            new_locations.append(Regions.lookup_name_to_id[location])
 | 
			
		||||
    for location in ctx.locations_checked:
 | 
			
		||||
        try:
 | 
			
		||||
            my_id = Regions.lookup_name_to_id.get(location, Shops.shop_table_by_location.get(location, -1))
 | 
			
		||||
            new_locations.append(my_id)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(e)
 | 
			
		||||
            logger.info(f"Exception: {e}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    await ctx.send_msgs([['LocationChecks', new_locations]])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1274,7 +1282,6 @@ async def game_watcher(ctx : Context):
 | 
			
		|||
            ctx.rom = rom.decode()
 | 
			
		||||
            if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
 | 
			
		||||
                ctx.locations_checked = set()
 | 
			
		||||
                ctx.unsafe_locations_checked = set()
 | 
			
		||||
                ctx.locations_scouted = set()
 | 
			
		||||
            ctx.prev_rom = ctx.rom
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -405,6 +405,8 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"
 | 
			
		|||
    # 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.shop_shuffle_slots = int(get_choice('shop_shuffle_slots', weights, '0'))
 | 
			
		||||
 | 
			
		||||
    ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
 | 
			
		||||
    if not ret.shop_shuffle:
 | 
			
		||||
        ret.shop_shuffle = ''
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										51
									
								
								Regions.py
								
								
								
								
							
							
						
						
									
										51
									
								
								Regions.py
								
								
								
								
							| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
import collections
 | 
			
		||||
import typing
 | 
			
		||||
 | 
			
		||||
from BaseClasses import Region, Location, Entrance, RegionType, Shop, TakeAny, UpgradeShop, ShopType
 | 
			
		||||
from BaseClasses import Region, Location, Entrance, RegionType
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_regions(world, player):
 | 
			
		||||
| 
						 | 
				
			
			@ -365,38 +366,6 @@ def mark_light_world_regions(world, player: int):
 | 
			
		|||
                queue.append(exit.connected_region)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_shops(world, player: int):
 | 
			
		||||
    cls_mapping = {ShopType.UpgradeShop: UpgradeShop,
 | 
			
		||||
                   ShopType.Shop: Shop,
 | 
			
		||||
                   ShopType.TakeAny: TakeAny}
 | 
			
		||||
    for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items():
 | 
			
		||||
        if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
 | 
			
		||||
            locked = True
 | 
			
		||||
            inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
 | 
			
		||||
        region = world.get_region(region_name, player)
 | 
			
		||||
        shop = cls_mapping[type](region, room_id, shopkeeper, custom, locked)
 | 
			
		||||
        region.shop = shop
 | 
			
		||||
        world.shops.append(shop)
 | 
			
		||||
        for index, item in enumerate(inventory):
 | 
			
		||||
            shop.add_inventory(index, *item)
 | 
			
		||||
 | 
			
		||||
# (type, room_id, shopkeeper, custom, locked, [items])
 | 
			
		||||
# item = (item, price, max=0, replacement=None, replacement_price=0)
 | 
			
		||||
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
 | 
			
		||||
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
 | 
			
		||||
shop_table = {
 | 
			
		||||
    'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults),
 | 
			
		||||
    'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]),
 | 
			
		||||
    'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
 | 
			
		||||
    'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
 | 
			
		||||
    'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
 | 
			
		||||
    'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_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),
 | 
			
		||||
    '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)]),
 | 
			
		||||
    'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
old_location_address_to_new_location_address = {
 | 
			
		||||
    0x2eb18: 0x18001b,   # Bottle Merchant
 | 
			
		||||
| 
						 | 
				
			
			@ -703,10 +672,13 @@ location_table: typing.Dict[str,
 | 
			
		|||
     'Turtle Rock - Prize': (
 | 
			
		||||
         [0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
 | 
			
		||||
 | 
			
		||||
from Shops import shop_table_by_location_id, shop_table_by_location
 | 
			
		||||
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.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 = {**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',
 | 
			
		||||
                                       1573194: 'Kings Grave Inner Rocks', 1573189: 'Kings Grave Inner Rocks',
 | 
			
		||||
| 
						 | 
				
			
			@ -832,7 +804,18 @@ lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 19125
 | 
			
		|||
                                       0x140064: 'Misery Mire',
 | 
			
		||||
                                       0x140058: 'Turtle Rock', 0x140007: 'Dark Death Mountain Ledge (West)',
 | 
			
		||||
                                       0x140040: 'Ganons Tower', 0x140043: 'Ganons Tower',
 | 
			
		||||
                                       0x14003a: 'Ganons Tower', 0x14001f: 'Ganons Tower'}
 | 
			
		||||
                                       0x14003a: 'Ganons Tower', 0x14001f: 'Ganons Tower',
 | 
			
		||||
                                       0x400000: 'Cave Shop (Dark Death Mountain)', 0x400001: 'Cave Shop (Dark Death Mountain)', 0x400002: 'Cave Shop (Dark Death Mountain)',
 | 
			
		||||
                                       0x400003: 'Red Shield Shop', 0x400004: 'Red Shield Shop', 0x400005: 'Red Shield Shop',
 | 
			
		||||
                                       0x400006: 'Dark Lake Hylia Shop', 0x400007: 'Dark Lake Hylia Shop', 0x400008: 'Dark Lake Hylia Shop',
 | 
			
		||||
                                       0x400009: 'Dark World Lumberjack Shop', 0x40000a: 'Dark World Lumberjack Shop', 0x40000b: 'Dark World Lumberjack Shop',
 | 
			
		||||
                                       0x40000c: 'Village of Outcasts Shop', 0x40000d: 'Village of Outcasts Shop', 0x40000e: 'Village of Outcasts Shop',
 | 
			
		||||
                                       0x40000f: 'Dark World Potion Shop', 0x400010: 'Dark World Potion Shop', 0x400011: 'Dark World Potion Shop',
 | 
			
		||||
                                       0x400012: 'Light World Death Mountain Shop', 0x400013: 'Light World Death Mountain Shop', 0x400014: 'Light World Death Mountain Shop',
 | 
			
		||||
                                       0x400015: 'Kakariko Shop', 0x400016: 'Kakariko Shop', 0x400017: 'Kakariko Shop',
 | 
			
		||||
                                       0x400018: 'Cave Shop (Lake Hylia)', 0x400019: 'Cave Shop (Lake Hylia)', 0x40001a: 'Cave Shop (Lake Hylia)',
 | 
			
		||||
                                       0x40001b: 'Potion Shop', 0x40001c: 'Potion Shop', 0x40001d: 'Potion Shop',
 | 
			
		||||
                                       0x40001e: 'Capacity Upgrade', 0x40001f: 'Capacity Upgrade', 0x400020: 'Capacity Upgrade'}
 | 
			
		||||
 | 
			
		||||
lookup_prizes = {location for location in location_table if location.endswith(" - Prize")}
 | 
			
		||||
lookup_boss_drops = {location for location in location_table if location.endswith(" - Boss")}
 | 
			
		||||
							
								
								
									
										35
									
								
								Rom.py
								
								
								
								
							
							
						
						
									
										35
									
								
								Rom.py
								
								
								
								
							| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
JAP10HASH = '03a63945398191337e896e5771f77173'
 | 
			
		||||
RANDOMIZERBASEHASH = '5a607e36a82bbd14180536c8ec3ae49b'
 | 
			
		||||
RANDOMIZERBASEHASH = '93538d51eb018955a90181600e3384ba'
 | 
			
		||||
 | 
			
		||||
import io
 | 
			
		||||
import json
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,8 @@ import xxtea
 | 
			
		|||
import concurrent.futures
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from BaseClasses import CollectionState, ShopType, Region, Location
 | 
			
		||||
from BaseClasses import CollectionState, Region, Location
 | 
			
		||||
from Shops import ShopType
 | 
			
		||||
from Dungeons import dungeon_music_addresses
 | 
			
		||||
from Regions import location_table, old_location_address_to_new_location_address
 | 
			
		||||
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
 | 
			
		||||
| 
						 | 
				
			
			@ -123,6 +124,9 @@ class LocalRom(object):
 | 
			
		|||
                    Patch.create_patch_file(local_path('basepatch.sfc'))
 | 
			
		||||
                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')):
 | 
			
		||||
            _, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.bmbp'))
 | 
			
		||||
            if self.verify(buffer):
 | 
			
		||||
| 
						 | 
				
			
			@ -130,6 +134,7 @@ class LocalRom(object):
 | 
			
		|||
                with open(local_path('basepatch.sfc'), 'wb') as stream:
 | 
			
		||||
                    stream.write(buffer)
 | 
			
		||||
                return
 | 
			
		||||
            raise RuntimeError('Base patch unverified.  Unable to continue.')
 | 
			
		||||
 | 
			
		||||
        raise RuntimeError('Could not find Base Patch. Unable to continue.')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -677,15 +682,13 @@ def patch_rom(world, rom, player, team, enemized):
 | 
			
		|||
        distinguished_prog_bow_loc.item.code = 0x65
 | 
			
		||||
 | 
			
		||||
    # patch items
 | 
			
		||||
 | 
			
		||||
    for location in world.get_locations():
 | 
			
		||||
        if location.player != player:
 | 
			
		||||
        if location.player != player or location.address is None or location.shop_slot:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        itemid = location.item.code if location.item is not None else 0x5A
 | 
			
		||||
 | 
			
		||||
        if location.address is None:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        if not location.crystal:
 | 
			
		||||
            if location.item is not None:
 | 
			
		||||
                # Keys in their native dungeon should use the orignal item code for keys
 | 
			
		||||
| 
						 | 
				
			
			@ -724,6 +727,7 @@ def patch_rom(world, rom, player, team, enemized):
 | 
			
		|||
            for music_address in music_addresses:
 | 
			
		||||
                rom.write_byte(music_address, music)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    if world.mapshuffle[player]:
 | 
			
		||||
        rom.write_byte(0x155C9, local_random.choice([0x11, 0x16]))  # Randomize GT music too with map shuffle
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1547,31 +1551,26 @@ def patch_race_rom(rom, world, player):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def write_custom_shops(rom, world, player):
 | 
			
		||||
    shops = [shop for shop in world.shops if shop.custom and shop.region.player == player]
 | 
			
		||||
    shops = sorted([shop for shop in world.shops if shop.custom and shop.region.player == player],
 | 
			
		||||
                   key=lambda shop: shop.sram_offset)
 | 
			
		||||
 | 
			
		||||
    shop_data = bytearray()
 | 
			
		||||
    items_data = bytearray()
 | 
			
		||||
    sram_offset = 0
 | 
			
		||||
 | 
			
		||||
    for shop_id, shop in enumerate(shops):
 | 
			
		||||
        if shop_id == len(shops) - 1:
 | 
			
		||||
            shop_id = 0xFF
 | 
			
		||||
        bytes = shop.get_bytes()
 | 
			
		||||
        bytes[0] = shop_id
 | 
			
		||||
        bytes[-1] = sram_offset
 | 
			
		||||
        if shop.type == ShopType.TakeAny:
 | 
			
		||||
            sram_offset += 1
 | 
			
		||||
        else:
 | 
			
		||||
            sram_offset += shop.item_count
 | 
			
		||||
        bytes[-1] = shop.sram_offset
 | 
			
		||||
        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:
 | 
			
		||||
            if item is None:
 | 
			
		||||
                break
 | 
			
		||||
            item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + [
 | 
			
		||||
                item['max'],
 | 
			
		||||
                ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + int16_as_bytes(
 | 
			
		||||
                item['replacement_price'])
 | 
			
		||||
            item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + \
 | 
			
		||||
                        [item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \
 | 
			
		||||
                        int16_as_bytes(item['replacement_price']) + [0 if item['player'] == player else item['player']]
 | 
			
		||||
            items_data.extend(item_data)
 | 
			
		||||
 | 
			
		||||
    rom.write_bytes(0x184800, shop_data)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								Rules.py
								
								
								
								
							
							
						
						
									
										2
									
								
								Rules.py
								
								
								
								
							| 
						 | 
				
			
			@ -85,7 +85,7 @@ def set_rules(world, player):
 | 
			
		|||
        add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
 | 
			
		||||
 | 
			
		||||
    set_bunny_rules(world, player, world.mode[player] == 'inverted')
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
def mirrorless_path_to_castle_courtyard(world, player):
 | 
			
		||||
    # If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,321 @@
 | 
			
		|||
from __future__ import annotations
 | 
			
		||||
from enum import unique, Enum
 | 
			
		||||
from typing import List, Union, Optional, Set, NamedTuple, Dict
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from BaseClasses import Location
 | 
			
		||||
from EntranceShuffle import door_addresses
 | 
			
		||||
from Items import item_name_groups, item_table, ItemFactory
 | 
			
		||||
from Utils import int16_as_bytes
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("Shops")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@unique
 | 
			
		||||
class ShopType(Enum):
 | 
			
		||||
    Shop = 0
 | 
			
		||||
    TakeAny = 1
 | 
			
		||||
    UpgradeShop = 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Shop():
 | 
			
		||||
    slots: int = 3  # slot count is not dynamic in asm, however inventory can have None as empty slots
 | 
			
		||||
    blacklist: Set[str] = set()  # items that don't work, todo: actually check against this
 | 
			
		||||
    type = ShopType.Shop
 | 
			
		||||
 | 
			
		||||
    def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool, sram_offset: int):
 | 
			
		||||
        self.region = region
 | 
			
		||||
        self.room_id = room_id
 | 
			
		||||
        self.inventory: List[Optional[dict]] = [None] * self.slots
 | 
			
		||||
        self.shopkeeper_config = shopkeeper_config
 | 
			
		||||
        self.custom = custom
 | 
			
		||||
        self.locked = locked
 | 
			
		||||
        self.sram_offset = sram_offset
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def item_count(self) -> int:
 | 
			
		||||
        for x in range(self.slots - 1, -1, -1):  # last x is 0
 | 
			
		||||
            if self.inventory[x]:
 | 
			
		||||
                return x + 1
 | 
			
		||||
        return 0
 | 
			
		||||
 | 
			
		||||
    def get_bytes(self) -> List[int]:
 | 
			
		||||
        # [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index]
 | 
			
		||||
        entrances = self.region.entrances
 | 
			
		||||
        config = self.item_count
 | 
			
		||||
        if len(entrances) == 1 and entrances[0].name in door_addresses:
 | 
			
		||||
            door_id = door_addresses[entrances[0].name][0] + 1
 | 
			
		||||
        else:
 | 
			
		||||
            door_id = 0
 | 
			
		||||
            config |= 0x40  # ignore door id
 | 
			
		||||
        if self.type == ShopType.TakeAny:
 | 
			
		||||
            config |= 0x80
 | 
			
		||||
        elif self.type == ShopType.UpgradeShop:
 | 
			
		||||
            config |= 0x10  # Alt. VRAM
 | 
			
		||||
        return [0x00] + int16_as_bytes(self.room_id) + [door_id, 0x00, config, self.shopkeeper_config, 0x00]
 | 
			
		||||
 | 
			
		||||
    def has_unlimited(self, item: str) -> bool:
 | 
			
		||||
        for inv in self.inventory:
 | 
			
		||||
            if inv is None:
 | 
			
		||||
                continue
 | 
			
		||||
            if inv['max']:
 | 
			
		||||
                if inv['replacement'] == item:
 | 
			
		||||
                    return True
 | 
			
		||||
            elif inv['item'] == item:
 | 
			
		||||
                return True
 | 
			
		||||
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def has(self, item: str) -> bool:
 | 
			
		||||
        for inv in self.inventory:
 | 
			
		||||
            if inv is None:
 | 
			
		||||
                continue
 | 
			
		||||
            if inv['item'] == item:
 | 
			
		||||
                return True
 | 
			
		||||
            if inv['replacement'] == item:
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def clear_inventory(self):
 | 
			
		||||
        self.inventory = [None] * self.slots
 | 
			
		||||
 | 
			
		||||
    def add_inventory(self, slot: int, item: str, price: int, max: int = 0,
 | 
			
		||||
                      replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False,
 | 
			
		||||
                      player: int = 0):
 | 
			
		||||
        self.inventory[slot] = {
 | 
			
		||||
            'item': item,
 | 
			
		||||
            'price': price,
 | 
			
		||||
            'max': max,
 | 
			
		||||
            'replacement': replacement,
 | 
			
		||||
            'replacement_price': replacement_price,
 | 
			
		||||
            'create_location': create_location,
 | 
			
		||||
            'player': player
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0):
 | 
			
		||||
        if not self.inventory[slot]:
 | 
			
		||||
            raise ValueError("Inventory can't be pushed back if it doesn't exist")
 | 
			
		||||
 | 
			
		||||
        self.inventory[slot] = {
 | 
			
		||||
            'item': item,
 | 
			
		||||
            'price': price,
 | 
			
		||||
            'max': max,
 | 
			
		||||
            'replacement': self.inventory[slot]["item"],
 | 
			
		||||
            'replacement_price': self.inventory[slot]["price"],
 | 
			
		||||
            'create_location': self.inventory[slot]["create_location"],
 | 
			
		||||
            'player': player
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def can_push_inventory(self, slot: int):
 | 
			
		||||
        return self.inventory[slot] and not self.inventory[slot]["replacement"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TakeAny(Shop):
 | 
			
		||||
    type = ShopType.TakeAny
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpgradeShop(Shop):
 | 
			
		||||
    type = ShopType.UpgradeShop
 | 
			
		||||
    # Potions break due to VRAM flags set in UpgradeShop.
 | 
			
		||||
    # Didn't check for more things breaking as not much else can be shuffled here currently
 | 
			
		||||
    blacklist = item_name_groups["Potions"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
 | 
			
		||||
                      ShopType.Shop: Shop,
 | 
			
		||||
                      ShopType.TakeAny: TakeAny}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def FillDisabledShopSlots(world):
 | 
			
		||||
    shop_slots: Set[Location] = {location for shop_locations in (shop.region.locations for shop in world.shops)
 | 
			
		||||
                                 for location in shop_locations if location.shop_slot and location.shop_slot_disabled}
 | 
			
		||||
    for location in shop_slots:
 | 
			
		||||
        location.shop_slot_disabled = True
 | 
			
		||||
        slot_num = int(location.name[-1]) - 1
 | 
			
		||||
        shop: Shop = location.parent_region.shop
 | 
			
		||||
        location.item = ItemFactory(shop.inventory[slot_num]['item'], location.player)
 | 
			
		||||
        location.item_rule = lambda item: item.name == location.item.name and item.player == location.player
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def ShopSlotFill(world):
 | 
			
		||||
    shop_slots: Set[Location] = {location for shop_locations in (shop.region.locations for shop in world.shops)
 | 
			
		||||
                                 for location in shop_locations if location.shop_slot}
 | 
			
		||||
    removed = set()
 | 
			
		||||
    for location in shop_slots:
 | 
			
		||||
        slot_num = int(location.name[-1]) - 1
 | 
			
		||||
        shop: Shop = location.parent_region.shop
 | 
			
		||||
        if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled:
 | 
			
		||||
            removed.add(location)
 | 
			
		||||
 | 
			
		||||
    if removed:
 | 
			
		||||
        shop_slots -= removed
 | 
			
		||||
 | 
			
		||||
    if shop_slots:
 | 
			
		||||
        from Fill import swap_location_item
 | 
			
		||||
        # TODO: allow each game to register a blacklist to be used here?
 | 
			
		||||
        blacklist_words = {"Rupee"}
 | 
			
		||||
        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_per_sphere = list(list(sphere) for sphere in world.get_spheres())
 | 
			
		||||
 | 
			
		||||
        candidate_condition = lambda location: not location.locked and \
 | 
			
		||||
                                               not location.shop_slot and \
 | 
			
		||||
                                               not location.item.name in blacklist_words
 | 
			
		||||
 | 
			
		||||
        # 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
 | 
			
		||||
        cumu_weights = []
 | 
			
		||||
 | 
			
		||||
        for sphere in candidates_per_sphere:
 | 
			
		||||
            if cumu_weights:
 | 
			
		||||
                x = cumu_weights[-1]
 | 
			
		||||
            else:
 | 
			
		||||
                x = 0
 | 
			
		||||
            cumu_weights.append(len(sphere) + x)
 | 
			
		||||
            world.random.shuffle(sphere)
 | 
			
		||||
 | 
			
		||||
        for i, sphere in enumerate(candidates_per_sphere):
 | 
			
		||||
            current_shop_slots = [location for location in sphere if location.shop_slot and not location.shop_slot_disabled]
 | 
			
		||||
            if current_shop_slots:
 | 
			
		||||
 | 
			
		||||
                for location in current_shop_slots:
 | 
			
		||||
                    shop: Shop = location.parent_region.shop
 | 
			
		||||
                    swapping_sphere = world.random.choices(candidates_per_sphere[i:], cum_weights=cumu_weights[i:])[0]
 | 
			
		||||
                    for c in swapping_sphere:  # chosen item locations
 | 
			
		||||
                        if candidate_condition(c) and c.item_rule(location.item) and location.item_rule(c.item):
 | 
			
		||||
                            swap_location_item(c, location, check_locked=False)
 | 
			
		||||
                            logger.debug(f'Swapping {c} into {location}:: {location.item}')
 | 
			
		||||
                            break
 | 
			
		||||
 | 
			
		||||
                    else:
 | 
			
		||||
                        # This *should* never happen. But let's fail safely just in case.
 | 
			
		||||
                        logger.warning("Ran out of ShopShuffle Item candidate locations.")
 | 
			
		||||
                        location.shop_slot_disabled = True
 | 
			
		||||
                        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(int(location.name[-1]) - 1, item_name, price, 1,
 | 
			
		||||
                                        location.item.player if location.item.player != location.player else 0)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_shops(world, player: int):
 | 
			
		||||
    option = world.shop_shuffle[player]
 | 
			
		||||
 | 
			
		||||
    player_shop_table = shop_table.copy()
 | 
			
		||||
    if "w" in option:
 | 
			
		||||
        player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False)
 | 
			
		||||
        dynamic_shop_slots = total_dynamic_shop_slots + 3
 | 
			
		||||
    else:
 | 
			
		||||
        dynamic_shop_slots = total_dynamic_shop_slots
 | 
			
		||||
 | 
			
		||||
    num_slots = min(dynamic_shop_slots, max(0, int(world.shop_shuffle_slots[player])))  # 0 to 30
 | 
			
		||||
    single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots)
 | 
			
		||||
    world.random.shuffle(single_purchase_slots)
 | 
			
		||||
 | 
			
		||||
    if 'g' in option or 'f' in option:
 | 
			
		||||
        default_shop_table = [i for l in [shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if not world.retro[player] or x != 'arrows'] for i in l]
 | 
			
		||||
        new_basic_shop = world.random.sample(default_shop_table, k=3)
 | 
			
		||||
        new_dark_shop = world.random.sample(default_shop_table, k=3)
 | 
			
		||||
        for name, shop in player_shop_table.items():
 | 
			
		||||
            typ, shop_id, keeper, custom, locked, items, sram_offset = shop
 | 
			
		||||
            if not locked:
 | 
			
		||||
                new_items = world.random.sample(default_shop_table, 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
 | 
			
		||||
                keeper = world.random.choice([0xA0, 0xC1, 0xFF])
 | 
			
		||||
                player_shop_table[name] = ShopData(typ, shop_id, keeper, custom, locked, new_items, sram_offset)
 | 
			
		||||
    if world.mode[player] == "inverted":
 | 
			
		||||
        player_shop_table["Dark Lake Hylia Shop"] = \
 | 
			
		||||
            player_shop_table["Dark Lake Hylia Shop"]._replace(locked=True, items=_inverted_hylia_shop_defaults)
 | 
			
		||||
    for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
 | 
			
		||||
        region = world.get_region(region_name, player)
 | 
			
		||||
        shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
 | 
			
		||||
        region.shop = shop
 | 
			
		||||
        world.shops.append(shop)
 | 
			
		||||
        for index, item in enumerate(inventory):
 | 
			
		||||
            shop.add_inventory(index, *item)
 | 
			
		||||
            if not locked and num_slots:
 | 
			
		||||
                slot_name = "{} Slot {}".format(region.name, index + 1)
 | 
			
		||||
                loc = Location(player, slot_name, address=shop_table_by_location[slot_name],
 | 
			
		||||
                               parent=region, hint_text="for sale")
 | 
			
		||||
                loc.shop_slot = True
 | 
			
		||||
                loc.locked = True
 | 
			
		||||
                if single_purchase_slots.pop():
 | 
			
		||||
                    additional_item = 'Rupees (50)'  # world.random.choice(['Rupees (50)', 'Rupees (100)', 'Rupees (300)'])
 | 
			
		||||
                    loc.item = ItemFactory(additional_item, player)
 | 
			
		||||
                else:
 | 
			
		||||
                    loc.item = ItemFactory('Nothing', player)
 | 
			
		||||
                    loc.shop_slot_disabled = True
 | 
			
		||||
                shop.region.locations.append(loc)
 | 
			
		||||
                world.dynamic_locations.append(loc)
 | 
			
		||||
                world.clear_location_cache()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShopData(NamedTuple):
 | 
			
		||||
    room: int
 | 
			
		||||
    type: ShopType
 | 
			
		||||
    shopkeeper: int
 | 
			
		||||
    custom: bool
 | 
			
		||||
    locked: bool
 | 
			
		||||
    items: List
 | 
			
		||||
    sram_offset: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# (type, room_id, shopkeeper, custom, locked, [items], sram_offset)
 | 
			
		||||
# item = (item, price, max=0, replacement=None, replacement_price=0)
 | 
			
		||||
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
 | 
			
		||||
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
 | 
			
		||||
_inverted_hylia_shop_defaults = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
 | 
			
		||||
shop_table: Dict[str, ShopData] = {
 | 
			
		||||
    'Cave Shop (Dark Death Mountain)': ShopData(0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults, 0),
 | 
			
		||||
    'Red Shield Shop': ShopData(0x0110, ShopType.Shop, 0xC1, True, False,
 | 
			
		||||
                                [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)], 3),
 | 
			
		||||
    'Dark Lake Hylia Shop': ShopData(0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults, 6),
 | 
			
		||||
    'Dark World Lumberjack Shop': ShopData(0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults, 9),
 | 
			
		||||
    'Village of Outcasts Shop': ShopData(0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults, 12),
 | 
			
		||||
    'Dark World Potion Shop': ShopData(0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults, 15),
 | 
			
		||||
    'Light World Death Mountain Shop': ShopData(0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults, 18),
 | 
			
		||||
    'Kakariko Shop': ShopData(0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults, 21),
 | 
			
		||||
    'Cave Shop (Lake Hylia)': ShopData(0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults, 24),
 | 
			
		||||
    'Potion Shop': ShopData(0x0109, ShopType.Shop, 0xA0, True, True,
 | 
			
		||||
                            [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], 27),
 | 
			
		||||
    'Capacity Upgrade': ShopData(0x0115, ShopType.UpgradeShop, 0x04, True, True,
 | 
			
		||||
                                 [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)], 30)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
total_shop_slots = len(shop_table) * 3
 | 
			
		||||
total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not data[4])  # data[4] -> locked
 | 
			
		||||
 | 
			
		||||
SHOP_ID_START = 0x400000
 | 
			
		||||
shop_table_by_location_id = {cnt: s for cnt, s in enumerate(
 | 
			
		||||
    (f"{name} Slot {num}" for name in [key for key, value in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)]
 | 
			
		||||
     for num in range(1, 4)), start=SHOP_ID_START)}
 | 
			
		||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave"
 | 
			
		||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 1)] = "Take-Any #1"
 | 
			
		||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 2)] = "Take-Any #2"
 | 
			
		||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 3)] = "Take-Any #3"
 | 
			
		||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 4)] = "Take-Any #4"
 | 
			
		||||
shop_table_by_location = {y: x for x, y in shop_table_by_location_id.items()}
 | 
			
		||||
 | 
			
		||||
shop_generation_types = {
 | 
			
		||||
    'arrows': [('Single Arrow', 5), ('Arrows (10)', 50)],
 | 
			
		||||
    'bombs': [('Single Bomb', 10), ('Bombs (3)', 30), ('Bombs (10)', 50)],
 | 
			
		||||
    'shields': [('Red Shield', 500), ('Blue Shield', 50)],
 | 
			
		||||
    'potions': [('Red Potion', 150), ('Green Potion', 90), ('Blue Potion', 190)],
 | 
			
		||||
    'discount_potions': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
 | 
			
		||||
    'bottle': [('Small Heart', 10), ('Apple', 50), ('Bee', 10), ('Good Bee', 100), ('Faerie', 100), ('Magic Jar', 100)],
 | 
			
		||||
    'time': [('Red Clock', 100), ('Blue Clock', 200), ('Green Clock', 300)],
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								Utils.py
								
								
								
								
							
							
						
						
									
										2
									
								
								Utils.py
								
								
								
								
							| 
						 | 
				
			
			@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
 | 
			
		|||
    micro: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__version__ = "3.6.0"
 | 
			
		||||
__version__ = "3.6.1"
 | 
			
		||||
_version_tuple = tuplize_version(__version__)
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -477,9 +477,23 @@
 | 
			
		|||
          "name": "None",
 | 
			
		||||
          "value": "none"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "Inventory",
 | 
			
		||||
          "value": "i"
 | 
			
		||||
        "g": {
 | 
			
		||||
          "keyString": "shop_shuffle.g",
 | 
			
		||||
          "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",
 | 
			
		||||
| 
						 | 
				
			
			@ -493,9 +507,269 @@
 | 
			
		|||
          "name": "Inventory and Prices",
 | 
			
		||||
          "value": "ip"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "Inventory, Prices, and Upgrades",
 | 
			
		||||
          "value": "ipu"
 | 
			
		||||
        "uip": {
 | 
			
		||||
          "keyString": "shop_shuffle.uip",
 | 
			
		||||
          "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.
										
									
								
							| 
						 | 
				
			
			@ -211,14 +211,25 @@ 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
 | 
			
		||||
  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
 | 
			
		||||
### Shop Settings ###
 | 
			
		||||
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
 | 
			
		||||
  0: 50
 | 
			
		||||
  5: 0
 | 
			
		||||
  15: 0
 | 
			
		||||
  30: 0
 | 
			
		||||
shop_shuffle:
 | 
			
		||||
  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
 | 
			
		||||
  u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
 | 
			
		||||
  w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
 | 
			
		||||
  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
 | 
			
		||||
  # You can add more combos
 | 
			
		||||
### End of Shop Section ###
 | 
			
		||||
shuffle_prizes: # aka drops
 | 
			
		||||
  none: 0 # do not shuffle prize packs
 | 
			
		||||
  g: 50 # shuffle "general" price packs, as in enemy, tree pull, dig etc.
 | 
			
		||||
| 
						 | 
				
			
			@ -253,11 +264,6 @@ green_clock_time: # For all timer modes, the amount of time in minutes to gain o
 | 
			
		|||
#  - "Small Keys"
 | 
			
		||||
#  - "Big Keys"
 | 
			
		||||
# 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
 | 
			
		||||
  # Pegasus Boots: on
 | 
			
		||||
  # Bomb Upgrade (+10): 4
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,8 @@ from Dungeons import create_dungeons, get_dungeon_item_pool
 | 
			
		|||
from EntranceShuffle import mandatory_connections, connect_simple
 | 
			
		||||
from ItemPool import difficulties, generate_itempool
 | 
			
		||||
from Items import ItemFactory
 | 
			
		||||
from Regions import create_regions, create_shops
 | 
			
		||||
from Regions import create_regions
 | 
			
		||||
from Shops import create_shops
 | 
			
		||||
from Rules import set_rules
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,8 @@ from EntranceShuffle import link_inverted_entrances
 | 
			
		|||
from InvertedRegions import create_inverted_regions
 | 
			
		||||
from ItemPool import generate_itempool, difficulties
 | 
			
		||||
from Items import ItemFactory
 | 
			
		||||
from Regions import mark_light_world_regions, create_shops
 | 
			
		||||
from Regions import mark_light_world_regions
 | 
			
		||||
from Shops import create_shops
 | 
			
		||||
from Rules import set_rules
 | 
			
		||||
from test.TestBase import TestBase
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,8 @@ from EntranceShuffle import link_inverted_entrances
 | 
			
		|||
from InvertedRegions import create_inverted_regions
 | 
			
		||||
from ItemPool import generate_itempool, difficulties
 | 
			
		||||
from Items import ItemFactory
 | 
			
		||||
from Regions import mark_light_world_regions, create_shops
 | 
			
		||||
from Regions import mark_light_world_regions
 | 
			
		||||
from Shops import create_shops
 | 
			
		||||
from Rules import set_rules
 | 
			
		||||
from test.TestBase import TestBase
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,8 @@ from EntranceShuffle import link_inverted_entrances
 | 
			
		|||
from InvertedRegions import create_inverted_regions
 | 
			
		||||
from ItemPool import generate_itempool, difficulties
 | 
			
		||||
from Items import ItemFactory
 | 
			
		||||
from Regions import mark_light_world_regions, create_shops
 | 
			
		||||
from Regions import mark_light_world_regions
 | 
			
		||||
from Shops import create_shops
 | 
			
		||||
from Rules import set_rules
 | 
			
		||||
from test.TestBase import TestBase
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,8 @@ from EntranceShuffle import link_entrances
 | 
			
		|||
from InvertedRegions import mark_dark_world_regions
 | 
			
		||||
from ItemPool import difficulties, generate_itempool
 | 
			
		||||
from Items import ItemFactory
 | 
			
		||||
from Regions import create_regions, create_shops
 | 
			
		||||
from Regions import create_regions
 | 
			
		||||
from Shops import create_shops
 | 
			
		||||
from Rules import set_rules
 | 
			
		||||
from test.TestBase import TestBase
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,8 @@ from EntranceShuffle import link_entrances
 | 
			
		|||
from InvertedRegions import mark_dark_world_regions
 | 
			
		||||
from ItemPool import difficulties, generate_itempool
 | 
			
		||||
from Items import ItemFactory
 | 
			
		||||
from Regions import create_regions, create_shops
 | 
			
		||||
from Regions import create_regions
 | 
			
		||||
from Shops import create_shops
 | 
			
		||||
from Rules import set_rules
 | 
			
		||||
from test.TestBase import TestBase
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
from Shops import shop_table
 | 
			
		||||
from test.TestBase import TestBase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSram(TestBase):
 | 
			
		||||
    def testUniqueOffset(self):
 | 
			
		||||
        sram_ids = set()
 | 
			
		||||
        for shop_name, shopdata in shop_table.items():
 | 
			
		||||
            for x in range(3):
 | 
			
		||||
                new = shopdata.sram_offset + x
 | 
			
		||||
                with self.subTest(shop_name, slot=x + 1, offset=new):
 | 
			
		||||
                    self.assertNotIn(new, sram_ids)
 | 
			
		||||
                sram_ids.add(new)
 | 
			
		||||
| 
						 | 
				
			
			@ -4,7 +4,8 @@ from EntranceShuffle import link_entrances
 | 
			
		|||
from InvertedRegions import mark_dark_world_regions
 | 
			
		||||
from ItemPool import difficulties, generate_itempool
 | 
			
		||||
from Items import ItemFactory
 | 
			
		||||
from Regions import create_regions, create_shops
 | 
			
		||||
from Regions import create_regions
 | 
			
		||||
from Shops import create_shops
 | 
			
		||||
from Rules import set_rules
 | 
			
		||||
from test.TestBase import TestBase
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue