예제 #1
0
def build_world_graphs(settings, window=dummy_window()):
    logger = logging.getLogger('')
    worlds = []
    for i in range(0, settings.world_count):
        worlds.append(World(i, settings))

    window.update_status('Creating the Worlds')
    for id, world in enumerate(worlds):
        logger.info('Generating World %d.' % (id + 1))

        window.update_progress(0 + 1 * (id + 1) / settings.world_count)
        logger.info('Creating Overworld')

        if settings.logic_rules == 'glitched':
            overworld_data = os.path.join(data_path('Glitched World'),
                                          'Overworld.json')
        else:
            overworld_data = os.path.join(data_path('World'), 'Overworld.json')

        # Compile the json rules based on settings
        world.load_regions_from_json(overworld_data)
        create_dungeons(world)
        world.create_internal_locations()

        if settings.shopsanity != 'off':
            world.random_shop_prices()
        world.set_scrub_prices()

        window.update_progress(0 + 4 * (id + 1) / settings.world_count)
        logger.info('Calculating Access Rules.')
        set_rules(world)

        window.update_progress(0 + 5 * (id + 1) / settings.world_count)
        logger.info('Generating Item Pool.')
        generate_itempool(world)
        set_shop_rules(world)
        set_drop_location_names(world)
        world.fill_bosses()

    if settings.triforce_hunt:
        settings.distribution.configure_triforce_hunt(worlds)

    logger.info('Setting Entrances.')
    set_entrances(worlds)
    return worlds
예제 #2
0
    def copy(self):
        ret = World(self.settings)
        ret.skipped_trials = copy.copy(self.skipped_trials)
        ret.dungeon_mq = copy.copy(self.dungeon_mq)
        ret.big_poe_count = copy.copy(self.big_poe_count)
        ret.can_take_damage = self.can_take_damage
        ret.shop_prices = copy.copy(self.shop_prices)
        ret.keys_placed = self.keys_placed
        ret.id = self.id
        from Regions import create_regions
        from Dungeons import create_dungeons
        from Rules import set_rules, set_shop_rules
        create_regions(ret)
        create_dungeons(ret)
        set_rules(ret)

        # connect copied world
        for region in self.regions:
            copied_region = ret.get_region(region.name)
            for entrance in region.entrances:
                ret.get_entrance(entrance.name).connect(copied_region)

        # fill locations
        for location in self.get_locations():
            if location.item is not None:
                item = Item(location.item.name, location.item.advancement,
                            location.item.priority, location.item.type)
                item.world = location.item.world
                ret.get_location(location.name).item = item
                item.location = ret.get_location(location.name)
                item.location.locked = location.locked

        # copy remaining itempool. No item in itempool should have an assigned location
        for item in self.itempool:
            new_item = Item(item.name, item.advancement, item.priority,
                            item.type)
            new_item.world = item.world
            ret.itempool.append(new_item)

        # copy progress items in state
        ret.state.prog_items = copy.copy(self.state.prog_items)

        set_shop_rules(ret)

        return ret
예제 #3
0
def distribute_items_restrictive(window, worlds, fill_locations=None):
    song_locations = [world.get_location(location) for world in worlds for location in
        ['Song from Composer Grave', 'Impa at Castle', 'Song from Malon', 'Song from Saria', 
        'Song from Ocarina of Time', 'Song at Windmill', 'Sheik Forest Song', 'Sheik at Temple', 
        'Sheik in Crater', 'Sheik in Ice Cavern', 'Sheik in Kakariko', 'Sheik at Colossus']]

    shop_locations = [location for world in worlds for location in world.get_unfilled_locations() if location.type == 'Shop' and location.price == None]

    # If not passed in, then get a shuffled list of locations to fill in
    if not fill_locations:
        fill_locations = [location for world in worlds for location in world.get_unfilled_locations() if location not in song_locations and location not in shop_locations]
    world_states = [world.state for world in worlds]

    window.locationcount = len(fill_locations) + len(song_locations)
    window.fillcount = 0

    # Generate the itempools
    shopitempool = [item for world in worlds for item in world.itempool if item.type == 'Shop']
    songitempool = [item for world in worlds for item in world.itempool if item.type == 'Song']
    itempool =     [item for world in worlds for item in world.itempool if item.type != 'Shop' and item.type != 'Song']
    if worlds[0].shuffle_song_items:
        itempool.extend(songitempool)
        fill_locations.extend(song_locations)

    # add unrestricted dungeon items to main item pool
    itempool.extend([item for world in worlds for item in world.get_unrestricted_dungeon_items()])
    dungeon_items = [item for world in worlds for item in world.get_restricted_dungeon_items()]

    random.shuffle(itempool) # randomize item placement order. this ordering can greatly affect the location accessibility bias
    progitempool = [item for item in itempool if item.advancement]
    prioitempool = [item for item in itempool if not item.advancement and item.priority]
    restitempool = [item for item in itempool if not item.advancement and not item.priority]


    # We place all the shop items first. Like songs, they have a more limited
    # set of locations that they can be placed in, so placing them first will
    # reduce the odds of creating unbeatable seeds. This also avoids needing
    # to create item rules for every location for whether they are a shop item
    # or not. This shouldn't have much affect on item bias.
    if shop_locations:
        random.shuffle(shop_locations)
        fill_shops(window, worlds, shop_locations, shopitempool, itempool + songitempool + dungeon_items)
    # Update the shop item access rules    
    for world in worlds:
        set_shop_rules(world)

    # If there are dungeon items that are restricted to their original dungeon,
    # we must place them first to make sure that there is always a location to
    # place them. This could probably be replaced for more intelligent item
    # placement, but will leave as is for now
    random.shuffle(fill_locations)
    fill_dungeons_restrictive(window, worlds, fill_locations, dungeon_items, itempool + songitempool)
    for world in worlds:
        world.keys_placed = True
        
    # I have no idea why the locations are reversed but this is how it was, 
    # so whatever. It can't hurt I guess
    random.shuffle(fill_locations)
    fill_locations.reverse()

    # places the songs into the world
    # Currently places songs only at song locations. if there's an option
    # to allow at other locations then they should be in the main pool.
    # Placing songs on their own since they have a relatively high chance
    # of failing compared to other item type. So this way we only have retry
    # the song locations only.
    if not worlds[0].shuffle_song_items:
        fill_songs(window, worlds, song_locations, songitempool, progitempool)

    # Put one item in every dungeon, needs to be done before other items are
    # placed to ensure there is a spot available for them
    if worlds[0].one_item_per_dungeon:
        fill_dungeon_unique_item(window, worlds, fill_locations, progitempool)

    # Place all progression items. This will include keys in keysanity.
    # Items in this group will check for reachability and will be placed
    # such that the game is guaranteed beatable.
    random.shuffle(fill_locations)
    fill_restrictive(window, worlds, [world.state for world in worlds], fill_locations, progitempool)

    # Place all priority items.
    # These items are items that only check if the item is allowed to be
    # placed in the location, not checking reachability. This is important
    # for things like Ice Traps that can't be found at some locations
    random.shuffle(fill_locations)
    fill_restrictive_fast(window, worlds, fill_locations, prioitempool)

    # Place the rest of the items.
    # No restrictions at all. Places them completely randomly. Since they
    # cannot affect the beatability, we don't need to check them
    random.shuffle(fill_locations)
    fast_fill(window, fill_locations, restitempool)

    # Log unplaced item/location warnings
    for item in progitempool + prioitempool + restitempool:
        logging.getLogger('').debug('Unplaced Items: %s [World %d]' % (item.name, item.world.id))
    if progitempool + prioitempool + restitempool:
        for item in progitempool + prioitempool + restitempool:
            print('Unplaced Items: %s [World %d]' % (item.name, item.world.id))

        raise FillError('Not all items are placed.')

    if fill_locations:
        for location in fill_locations:
            logging.getLogger('').debug('Unfilled Locations: %s [World %d]' % (location.name, location.world.id))
        raise FillError('Not all locations have an item.')
예제 #4
0
def main(settings, window=dummy_window()):

    start = time.process_time()

    logger = logging.getLogger('')

    worlds = []

    allowed_tricks = {}
    for trick in logic_tricks.values():
        settings.__dict__[
            trick['name']] = trick['name'] in settings.allowed_tricks

    settings.load_distribution()

    # we load the rom before creating the seed so that error get caught early
    if settings.compress_rom == 'None' and not settings.create_spoiler:
        raise Exception(
            '`No Output` must have spoiler enabled to produce anything.')

    if settings.compress_rom != 'None':
        window.update_status('Loading ROM')
        rom = Rom(settings.rom)

    if not settings.world_count:
        settings.world_count = 1
    if settings.world_count < 1 or settings.world_count > 255:
        raise Exception('World Count must be between 1 and 255')
    if settings.player_num > settings.world_count or settings.player_num < 1:
        if settings.compress_rom not in ['None', 'Patch']:
            raise Exception('Player Num must be between 1 and %d' %
                            settings.world_count)
        else:
            settings.player_num = 1

    logger.info('OoT Randomizer Version %s  -  Seed: %s\n\n', __version__,
                settings.seed)
    settings.remove_disabled()
    random.seed(settings.numeric_seed)
    settings.resolve_random_settings()

    for i in range(0, settings.world_count):
        worlds.append(World(settings))

    window.update_status('Creating the Worlds')
    for id, world in enumerate(worlds):
        world.id = id
        world.distribution = settings.distribution.world_dists[id]
        logger.info('Generating World %d.' % id)

        window.update_progress(0 + 1 * (id + 1) / settings.world_count)
        logger.info('Creating Overworld')

        # Determine MQ Dungeons
        dungeon_pool = list(world.dungeon_mq)
        dist_num_mq = world.distribution.configure_dungeons(
            world, dungeon_pool)

        if world.mq_dungeons_random:
            for dungeon in dungeon_pool:
                world.dungeon_mq[dungeon] = random.choice([True, False])
            world.mq_dungeons = list(world.dungeon_mq.values()).count(True)
        else:
            mqd_picks = random.sample(dungeon_pool,
                                      world.mq_dungeons - dist_num_mq)
            for dung in mqd_picks:
                world.dungeon_mq[dung] = True

        if settings.logic_rules == 'glitched':
            overworld_data = os.path.join(data_path('Glitched World'),
                                          'Overworld.json')
        else:
            overworld_data = os.path.join(data_path('World'), 'Overworld.json')
        world.load_regions_from_json(overworld_data)

        create_dungeons(world)

        if settings.shopsanity != 'off':
            world.random_shop_prices()
        world.set_scrub_prices()

        window.update_progress(0 + 4 * (id + 1) / settings.world_count)
        logger.info('Calculating Access Rules.')
        set_rules(world)

        window.update_progress(0 + 5 * (id + 1) / settings.world_count)
        logger.info('Generating Item Pool.')
        generate_itempool(world)
        set_shop_rules(world)
        set_drop_location_names(world)

    logger.info('Setting Entrances.')
    set_entrances(worlds)

    window.update_status('Placing the Items')
    logger.info('Fill the world.')
    distribute_items_restrictive(window, worlds)
    window.update_progress(35)

    spoiler = Spoiler(worlds)
    cosmetics_log = None
    if settings.create_spoiler:
        window.update_status('Calculating Spoiler Data')
        logger.info('Calculating playthrough.')
        create_playthrough(spoiler)
        window.update_progress(50)
    if settings.create_spoiler or settings.hints != 'none':
        window.update_status('Calculating Hint Data')
        State.update_required_items(spoiler)
        for world in worlds:
            world.update_useless_areas(spoiler)
            buildGossipHints(spoiler, world)
        window.update_progress(55)
    spoiler.build_file_hash()

    logger.info('Patching ROM.')

    settings_string_hash = hashlib.sha1(
        settings.settings_string.encode('utf-8')).hexdigest().upper()[:5]
    if settings.output_file:
        outfilebase = settings.output_file
    elif settings.world_count > 1:
        outfilebase = 'OoT_%s_%s_W%d' % (settings_string_hash, settings.seed,
                                         settings.world_count)
    else:
        outfilebase = 'OoT_%s_%s' % (settings_string_hash, settings.seed)

    output_dir = default_output_path(settings.output_dir)

    if settings.compress_rom == 'Patch':
        rng_state = random.getstate()
        file_list = []
        window.update_progress(65)
        for world in worlds:
            if settings.world_count > 1:
                window.update_status('Patching ROM: Player %d' %
                                     (world.id + 1))
                patchfilename = '%sP%d.zpf' % (outfilebase, world.id + 1)
            else:
                window.update_status('Patching ROM')
                patchfilename = '%s.zpf' % outfilebase

            random.setstate(rng_state)
            patch_rom(spoiler, world, rom, outfilebase)
            cosmetics_log = patch_cosmetics(settings, rom)
            window.update_progress(65 + 20 *
                                   (world.id + 1) / settings.world_count)

            window.update_status('Creating Patch File')
            output_path = os.path.join(output_dir, patchfilename)
            file_list.append(patchfilename)
            create_patch_file(rom, output_path)
            rom.restore()
            window.update_progress(65 + 30 *
                                   (world.id + 1) / settings.world_count)

            if settings.create_cosmetics_log and cosmetics_log:
                window.update_status('Creating Cosmetics Log')
                if settings.world_count > 1:
                    cosmetics_log_filename = "%sP%d_Cosmetics.txt" % (
                        outfilebase, world.id + 1)
                else:
                    cosmetics_log_filename = '%s_Cosmetics.txt' % outfilebase
                cosmetics_log.to_file(
                    os.path.join(output_dir, cosmetics_log_filename))
                file_list.append(cosmetics_log_filename)
            cosmetics_log = None

        if settings.world_count > 1:
            window.update_status('Creating Patch Archive')
            output_path = os.path.join(output_dir, '%s.zpfz' % outfilebase)
            with zipfile.ZipFile(output_path, mode="w") as patch_archive:
                for file in file_list:
                    file_path = os.path.join(output_dir, file)
                    patch_archive.write(file_path,
                                        file.replace(outfilebase, ''),
                                        compress_type=zipfile.ZIP_DEFLATED)
            for file in file_list:
                os.remove(os.path.join(output_dir, file))
        logger.info("Created patchfile at: %s" % output_path)
        window.update_progress(95)

    elif settings.compress_rom != 'None':
        window.update_status('Patching ROM')
        patch_rom(spoiler, worlds[settings.player_num - 1], rom, outfilebase)
        cosmetics_log = patch_cosmetics(settings, rom)
        window.update_progress(65)

        window.update_status('Saving Uncompressed ROM')
        if settings.world_count > 1:
            filename = "%sP%d.z64" % (outfilebase, settings.player_num)
        else:
            filename = '%s.z64' % outfilebase
        output_path = os.path.join(output_dir, filename)
        rom.write_to_file(output_path)
        if settings.compress_rom == 'True':
            window.update_status('Compressing ROM')
            logger.info('Compressing ROM.')

            if is_bundled():
                compressor_path = "."
            else:
                compressor_path = "Compress"

            if platform.system() == 'Windows':
                if 8 * struct.calcsize("P") == 64:
                    compressor_path += "\\Compress.exe"
                else:
                    compressor_path += "\\Compress32.exe"
            elif platform.system() == 'Linux':
                if platform.uname()[4] == 'aarch64' or platform.uname(
                )[4] == 'arm64':
                    compressor_path += "/Compress_ARM64"
                else:
                    compressor_path += "/Compress"
            elif platform.system() == 'Darwin':
                compressor_path += "/Compress.out"
            else:
                compressor_path = ""
                logger.info('OS not supported for compression')

            output_compress_path = output_path[:output_path.
                                               rfind('.')] + '-comp.z64'
            if compressor_path != "":
                run_process(
                    window, logger,
                    [compressor_path, output_path, output_compress_path])
            os.remove(output_path)
            logger.info("Created compessed rom at: %s" % output_compress_path)
        else:
            logger.info("Created uncompessed rom at: %s" % output_path)
        window.update_progress(95)

    for world in worlds:
        for info in setting_infos:
            world.settings.__dict__[info.name] = world.__dict__[info.name]

    settings.distribution.update_spoiler(spoiler)
    if settings.create_spoiler:
        window.update_status('Creating Spoiler Log')
        spoiler_path = os.path.join(output_dir,
                                    '%s_Spoiler.json' % outfilebase)
        settings.distribution.to_file(spoiler_path)
        logger.info("Created spoiler log at: %s" %
                    ('%s_Spoiler.json' % outfilebase))
    else:
        window.update_status('Creating Settings Log')
        settings_path = os.path.join(output_dir,
                                     '%s_Settings.json' % outfilebase)
        settings.distribution.to_file(settings_path)
        logger.info("Created settings log at: %s" %
                    ('%s_Settings.json' % outfilebase))

    if settings.create_cosmetics_log and cosmetics_log:
        window.update_status('Creating Cosmetics Log')
        if settings.world_count > 1 and not settings.output_file:
            filename = "%sP%d_Cosmetics.txt" % (outfilebase,
                                                settings.player_num)
        else:
            filename = '%s_Cosmetics.txt' % outfilebase
        cosmetic_path = os.path.join(output_dir, filename)
        cosmetics_log.to_file(cosmetic_path)
        logger.info("Created cosmetic log at: %s" % cosmetic_path)

    window.update_progress(100)
    if cosmetics_log and cosmetics_log.error:
        window.update_status(
            'Success: Rom patched successfully. Some cosmetics could not be applied.'
        )
    else:
        window.update_status('Success: Rom patched successfully')
    logger.info('Done. Enjoy.')
    logger.debug('Total Time: %s', time.process_time() - start)

    return worlds[settings.player_num - 1]
예제 #5
0
def generate(settings, window):
    logger = logging.getLogger('')
    worlds = []
    for i in range(0, settings.world_count):
        worlds.append(World(i, settings))

    window.update_status('Creating the Worlds')
    for id, world in enumerate(worlds):
        logger.info('Generating World %d.' % (id + 1))

        window.update_progress(0 + 1 * (id + 1) / settings.world_count)
        logger.info('Creating Overworld')

        if settings.logic_rules == 'glitched':
            overworld_data = os.path.join(data_path('Glitched World'),
                                          'Overworld.json')
        else:
            overworld_data = os.path.join(data_path('World'), 'Overworld.json')

        # Compile the json rules based on settings
        world.load_regions_from_json(overworld_data)
        create_dungeons(world)
        world.create_internal_locations()

        if settings.shopsanity != 'off':
            world.random_shop_prices()
        world.set_scrub_prices()

        window.update_progress(0 + 4 * (id + 1) / settings.world_count)
        logger.info('Calculating Access Rules.')
        set_rules(world)

        window.update_progress(0 + 5 * (id + 1) / settings.world_count)
        logger.info('Generating Item Pool.')
        generate_itempool(world)
        set_shop_rules(world)
        set_drop_location_names(world)
        world.fill_bosses()

    if settings.triforce_hunt:
        settings.distribution.configure_triforce_hunt(worlds)

    logger.info('Setting Entrances.')
    set_entrances(worlds)

    window.update_status('Placing the Items')
    logger.info('Fill the world.')
    distribute_items_restrictive(window, worlds)
    window.update_progress(35)

    spoiler = Spoiler(worlds)
    if settings.create_spoiler:
        window.update_status('Calculating Spoiler Data')
        logger.info('Calculating playthrough.')
        create_playthrough(spoiler)
        window.update_progress(50)
    if settings.create_spoiler or settings.hints != 'none':
        window.update_status('Calculating Hint Data')
        logger.info('Calculating hint data.')
        State.update_required_items(spoiler)
        buildGossipHints(spoiler, worlds)
        window.update_progress(55)
    spoiler.build_file_hash()
    return spoiler
예제 #6
0
def distribute_items_restrictive(window, worlds, fill_locations=None):
    song_locations = [
        world.get_location(location) for world in worlds for location in [
            'Song from Composers Grave', 'Song from Impa', 'Song from Malon',
            'Song from Saria', 'Song from Ocarina of Time',
            'Song from Windmill', 'Sheik in Forest', 'Sheik at Temple',
            'Sheik in Crater', 'Sheik in Ice Cavern', 'Sheik in Kakariko',
            'Sheik at Colossus'
        ]
    ]

    shop_locations = [
        location for world in worlds
        for location in world.get_unfilled_locations()
        if location.type == 'Shop' and location.price == None
    ]

    # If not passed in, then get a shuffled list of locations to fill in
    if not fill_locations:
        fill_locations = [location for world in worlds for location in world.get_unfilled_locations() \
            if location not in song_locations and \
               location not in shop_locations and \
               location.type != 'GossipStone']
    world_states = [world.state for world in worlds]

    window.locationcount = len(fill_locations) + len(song_locations) + len(
        shop_locations)
    window.fillcount = 0

    # Generate the itempools
    shopitempool = [
        item for world in worlds for item in world.itempool
        if item.type == 'Shop'
    ]
    songitempool = [
        item for world in worlds for item in world.itempool
        if item.type == 'Song'
    ]
    itempool = [
        item for world in worlds for item in world.itempool
        if item.type != 'Shop' and item.type != 'Song'
    ]

    if worlds[0].shuffle_song_items:
        itempool.extend(songitempool)
        fill_locations.extend(song_locations)
        songitempool = []
        song_locations = []

    # add unrestricted dungeon items to main item pool
    itempool.extend([
        item for world in worlds
        for item in world.get_unrestricted_dungeon_items()
    ])
    dungeon_items = [
        item for world in worlds
        for item in world.get_restricted_dungeon_items()
    ]

    random.shuffle(
        itempool
    )  # randomize item placement order. this ordering can greatly affect the location accessibility bias
    progitempool = [item for item in itempool if item.advancement]
    prioitempool = [
        item for item in itempool if not item.advancement and item.priority
    ]
    restitempool = [
        item for item in itempool if not item.advancement and not item.priority
    ]

    cloakable_locations = shop_locations + song_locations + fill_locations
    all_models = shopitempool + dungeon_items + songitempool + itempool
    worlds[0].settings.distribution.fill(
        window, worlds, [shop_locations, song_locations, fill_locations], [
            shopitempool, dungeon_items, songitempool, progitempool,
            prioitempool, restitempool
        ])
    itempool = progitempool + prioitempool + restitempool

    # set ice traps to have the appearance of other random items in the item pool
    ice_traps = [item for item in itempool if item.name == 'Ice Trap']
    # Extend with ice traps manually placed in plandomizer
    ice_traps.extend(
        location.item for location in cloakable_locations
        if (location.name in location_groups['CanSee']
            and location.item is not None and location.item.name == 'Ice Trap'
            and location.item.looks_like_item is None))
    junk_items = remove_junk_items.copy()
    junk_items.remove('Ice Trap')
    major_items = [
        item for (item, data) in item_table.items()
        if data[0] == 'Item' and data[1] and data[2] is not None
    ]
    fake_items = []
    if worlds[0].settings.ice_trap_appearance == 'major_only':
        model_items = [item for item in itempool if item.majoritem]
        if len(
                model_items
        ) == 0:  # All major items were somehow removed from the pool (can happen in plando)
            model_items = ItemFactory(major_items)
    elif worlds[0].settings.ice_trap_appearance == 'junk_only':
        model_items = [item for item in itempool if item.name in junk_items]
        if len(model_items) == 0:  # All junk was removed
            model_items = ItemFactory(junk_items)
    else:  # world[0].settings.ice_trap_appearance == 'anything':
        model_items = [item for item in itempool if item.name != 'Ice Trap']
        if len(
                model_items
        ) == 0:  # All major items and junk were somehow removed from the pool (can happen in plando)
            model_items = ItemFactory(major_items) + ItemFactory(junk_items)
    while len(ice_traps) > len(fake_items):
        # if there are more ice traps than model items, then double up on model items
        fake_items.extend(model_items)
    for random_item in random.sample(fake_items, len(ice_traps)):
        ice_trap = ice_traps.pop(0)
        ice_trap.looks_like_item = random_item

    # Start a search cache here.
    search = Search([world.state for world in worlds])

    # We place all the shop items first. Like songs, they have a more limited
    # set of locations that they can be placed in, so placing them first will
    # reduce the odds of creating unbeatable seeds. This also avoids needing
    # to create item rules for every location for whether they are a shop item
    # or not. This shouldn't have much affect on item bias.
    if shop_locations:
        logger.info('Placing shop items.')
        fill_ownworld_restrictive(window, worlds, search, shop_locations,
                                  shopitempool,
                                  itempool + songitempool + dungeon_items,
                                  "shop")
    # Update the shop item access rules
    for world in worlds:
        set_shop_rules(world)

    search.collect_locations()

    # If there are dungeon items that are restricted to their original dungeon,
    # we must place them first to make sure that there is always a location to
    # place them. This could probably be replaced for more intelligent item
    # placement, but will leave as is for now
    if dungeon_items:
        logger.info('Placing dungeon items.')
        fill_dungeons_restrictive(window, worlds, search, fill_locations,
                                  dungeon_items, itempool + songitempool)
        search.collect_locations()

    # places the songs into the world
    # Currently places songs only at song locations. if there's an option
    # to allow at other locations then they should be in the main pool.
    # Placing songs on their own since they have a relatively high chance
    # of failing compared to other item type. So this way we only have retry
    # the song locations only.
    if not worlds[0].shuffle_song_items:
        logger.info('Placing song items.')
        fill_ownworld_restrictive(window, worlds, search, song_locations,
                                  songitempool, progitempool, "song")
        search.collect_locations()
        fill_locations += [
            location for location in song_locations if location.item is None
        ]

    # Put one item in every dungeon, needs to be done before other items are
    # placed to ensure there is a spot available for them
    if worlds[0].one_item_per_dungeon:
        logger.info('Placing one major item per dungeon.')
        fill_dungeon_unique_item(window, worlds, search, fill_locations,
                                 progitempool)
        search.collect_locations()

    # Place all progression items. This will include keys in keysanity.
    # Items in this group will check for reachability and will be placed
    # such that the game is guaranteed beatable.
    logger.info('Placing progression items.')
    fill_restrictive(window, worlds, search, fill_locations, progitempool)
    search.collect_locations()

    # Place all priority items.
    # These items are items that only check if the item is allowed to be
    # placed in the location, not checking reachability. This is important
    # for things like Ice Traps that can't be found at some locations
    logger.info('Placing priority items.')
    fill_restrictive_fast(window, worlds, fill_locations, prioitempool)

    # Place the rest of the items.
    # No restrictions at all. Places them completely randomly. Since they
    # cannot affect the beatability, we don't need to check them
    logger.info('Placing the rest of the items.')
    fast_fill(window, fill_locations, restitempool)

    # Log unplaced item/location warnings
    for item in progitempool + prioitempool + restitempool:
        logger.error('Unplaced Items: %s [World %d]' %
                     (item.name, item.world.id))
    for location in fill_locations:
        logger.error('Unfilled Locations: %s [World %d]' %
                     (location.name, location.world.id))

    if progitempool + prioitempool + restitempool:
        raise FillError('Not all items are placed.')

    if fill_locations:
        raise FillError('Not all locations have an item.')

    if not search.can_beat_game():
        raise FillError('Cannot beat game!')

    worlds[0].settings.distribution.cloak(worlds, [cloakable_locations],
                                          [all_models])

    for world in worlds:
        for location in world.get_filled_locations():
            # Get the maximum amount of wallets required to purchase an advancement item.
            if world.maximum_wallets < 3 and location.price and location.item.advancement:
                if location.price > 500:
                    world.maximum_wallets = 3
                elif world.maximum_wallets < 2 and location.price > 200:
                    world.maximum_wallets = 2
                elif world.maximum_wallets < 1 and location.price > 99:
                    world.maximum_wallets = 1

            # Get Light Arrow location for later usage.
            if location.item and location.item.name == 'Light Arrows':
                location.item.world.light_arrow_location = location