2021-10-10 14:50:01 +00:00
|
|
|
from itertools import zip_longest, chain
|
2017-12-17 05:25:46 +00:00
|
|
|
import logging
|
2019-05-29 23:10:16 +00:00
|
|
|
import os
|
2017-12-17 05:25:46 +00:00
|
|
|
import time
|
2020-01-04 21:08:13 +00:00
|
|
|
import zlib
|
2020-08-21 16:35:48 +00:00
|
|
|
import concurrent.futures
|
2021-01-03 13:32:32 +00:00
|
|
|
import pickle
|
2021-07-21 16:08:15 +00:00
|
|
|
import tempfile
|
|
|
|
import zipfile
|
2021-10-10 22:46:18 +00:00
|
|
|
from typing import Dict, Tuple, Optional
|
2017-12-17 05:25:46 +00:00
|
|
|
|
2022-02-01 15:36:14 +00:00
|
|
|
from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType
|
2021-07-12 11:54:47 +00:00
|
|
|
from worlds.alttp.Items import item_name_groups
|
2021-07-22 13:51:50 +00:00
|
|
|
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
|
2021-01-04 14:14:20 +00:00
|
|
|
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
2021-08-29 23:16:04 +00:00
|
|
|
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
2021-08-09 07:15:41 +00:00
|
|
|
from Utils import output_path, get_options, __version__, version_tuple
|
2021-07-14 13:24:34 +00:00
|
|
|
from worlds.generic.Rules import locality_rules, exclusion_rules
|
2021-07-12 12:32:39 +00:00
|
|
|
from worlds import AutoWorld
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2019-04-29 21:11:23 +00:00
|
|
|
|
2021-10-10 22:46:18 +00:00
|
|
|
ordered_areas = (
|
|
|
|
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
|
|
|
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
|
|
|
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
|
|
|
|
if not baked_server_options:
|
|
|
|
baked_server_options = get_options()["server_options"]
|
2019-12-28 16:12:27 +00:00
|
|
|
if args.outputpath:
|
2020-01-10 06:25:16 +00:00
|
|
|
os.makedirs(args.outputpath, exist_ok=True)
|
2019-12-28 16:12:27 +00:00
|
|
|
output_path.cached_path = args.outputpath
|
|
|
|
|
2020-01-02 01:38:26 +00:00
|
|
|
start = time.perf_counter()
|
2017-05-15 18:28:04 +00:00
|
|
|
# initialize the world
|
2021-03-14 07:38:02 +00:00
|
|
|
world = MultiWorld(args.multi)
|
2020-07-14 05:01:51 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
logger = logging.getLogger()
|
2021-10-09 00:30:46 +00:00
|
|
|
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2021-03-14 07:38:02 +00:00
|
|
|
world.shuffle = args.shuffle.copy()
|
|
|
|
world.logic = args.logic.copy()
|
|
|
|
world.mode = args.mode.copy()
|
|
|
|
world.difficulty = args.difficulty.copy()
|
|
|
|
world.item_functionality = args.item_functionality.copy()
|
|
|
|
world.timer = args.timer.copy()
|
|
|
|
world.goal = args.goal.copy()
|
2020-09-11 01:23:00 +00:00
|
|
|
world.open_pyramid = args.open_pyramid.copy()
|
2019-12-17 14:55:53 +00:00
|
|
|
world.boss_shuffle = args.shufflebosses.copy()
|
|
|
|
world.enemy_health = args.enemy_health.copy()
|
|
|
|
world.enemy_damage = args.enemy_damage.copy()
|
2021-11-03 05:34:11 +00:00
|
|
|
world.beemizer_total_chance = args.beemizer_total_chance.copy()
|
|
|
|
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
2020-02-03 01:10:56 +00:00
|
|
|
world.timer = args.timer.copy()
|
2020-10-28 23:20:59 +00:00
|
|
|
world.countdown_start_time = args.countdown_start_time.copy()
|
|
|
|
world.red_clock_time = args.red_clock_time.copy()
|
|
|
|
world.blue_clock_time = args.blue_clock_time.copy()
|
|
|
|
world.green_clock_time = args.green_clock_time.copy()
|
2020-04-12 22:46:32 +00:00
|
|
|
world.dungeon_counters = args.dungeon_counters.copy()
|
2020-06-17 08:02:54 +00:00
|
|
|
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
2020-06-07 13:22:24 +00:00
|
|
|
world.triforce_pieces_required = args.triforce_pieces_required.copy()
|
2020-08-23 13:03:06 +00:00
|
|
|
world.shop_shuffle = args.shop_shuffle.copy()
|
2020-09-20 02:35:45 +00:00
|
|
|
world.shuffle_prizes = args.shuffle_prizes.copy()
|
2020-10-06 20:22:03 +00:00
|
|
|
world.sprite_pool = args.sprite_pool.copy()
|
2020-10-07 17:51:46 +00:00
|
|
|
world.dark_room_logic = args.dark_room_logic.copy()
|
2021-01-02 11:49:43 +00:00
|
|
|
world.plando_items = args.plando_items.copy()
|
2021-01-02 15:44:58 +00:00
|
|
|
world.plando_texts = args.plando_texts.copy()
|
2021-01-02 21:41:03 +00:00
|
|
|
world.plando_connections = args.plando_connections.copy()
|
2021-01-02 22:00:14 +00:00
|
|
|
world.required_medallions = args.required_medallions.copy()
|
2021-02-21 19:17:24 +00:00
|
|
|
world.game = args.game.copy()
|
2021-06-11 16:02:48 +00:00
|
|
|
world.set_options(args)
|
2021-08-09 07:15:41 +00:00
|
|
|
world.player_name = args.name.copy()
|
|
|
|
world.enemizer = args.enemizercli
|
|
|
|
world.sprite = args.sprite.copy()
|
2021-03-22 20:14:19 +00:00
|
|
|
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
2019-08-11 12:55:38 +00:00
|
|
|
|
2021-01-03 13:32:32 +00:00
|
|
|
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
2020-01-14 09:42:27 +00:00
|
|
|
|
2021-06-11 12:22:44 +00:00
|
|
|
logger.info("Found World Types:")
|
2021-07-12 13:11:48 +00:00
|
|
|
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
2021-08-16 16:40:26 +00:00
|
|
|
numlength = 8
|
2021-06-11 12:22:44 +00:00
|
|
|
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
2021-08-27 18:46:23 +00:00
|
|
|
if not cls.hidden:
|
2021-08-27 12:52:33 +00:00
|
|
|
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | "
|
|
|
|
f"{len(cls.location_names):3} Locations")
|
2021-10-17 18:53:06 +00:00
|
|
|
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
|
2021-08-27 12:52:33 +00:00
|
|
|
f"{max(cls.item_id_to_name):{numlength}} | "
|
|
|
|
f"Location IDs: {min(cls.location_id_to_name):{numlength}} - "
|
|
|
|
f"{max(cls.location_id_to_name):{numlength}}")
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2021-08-27 22:26:02 +00:00
|
|
|
AutoWorld.call_all(world, "generate_early")
|
|
|
|
|
2020-01-14 09:42:27 +00:00
|
|
|
logger.info('')
|
2021-05-22 04:27:22 +00:00
|
|
|
|
2021-05-09 19:22:21 +00:00
|
|
|
for player in world.player_ids:
|
2021-09-23 01:53:16 +00:00
|
|
|
for item_name, count in world.start_inventory[player].value.items():
|
|
|
|
for _ in range(count):
|
|
|
|
world.push_precollected(world.create_item(item_name, player))
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
for player in world.player_ids:
|
2021-07-21 16:08:15 +00:00
|
|
|
if player in world.get_game_players("A Link to the Past"):
|
2021-07-12 13:11:48 +00:00
|
|
|
# enforce pre-defined local items.
|
|
|
|
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
2021-09-16 22:17:54 +00:00
|
|
|
world.local_items[player].value.add('Triforce Piece')
|
2021-01-05 17:56:20 +00:00
|
|
|
|
2021-07-12 13:11:48 +00:00
|
|
|
# Not possible to place pendants/crystals out side of boss prizes yet.
|
2021-09-16 22:17:54 +00:00
|
|
|
world.non_local_items[player].value -= item_name_groups['Pendants']
|
|
|
|
world.non_local_items[player].value -= item_name_groups['Crystals']
|
2020-12-19 23:36:29 +00:00
|
|
|
|
2021-07-12 13:11:48 +00:00
|
|
|
# items can't be both local and non-local, prefer local
|
2021-09-16 22:17:54 +00:00
|
|
|
world.non_local_items[player].value -= world.local_items[player].value
|
2020-01-06 18:13:42 +00:00
|
|
|
|
2021-07-22 13:51:50 +00:00
|
|
|
logger.info('Creating World.')
|
2021-06-11 12:22:44 +00:00
|
|
|
AutoWorld.call_all(world, "create_regions")
|
2021-04-01 09:40:58 +00:00
|
|
|
|
2021-07-22 13:51:50 +00:00
|
|
|
logger.info('Creating Items.')
|
|
|
|
AutoWorld.call_all(world, "create_items")
|
2019-04-18 21:11:11 +00:00
|
|
|
|
2017-05-20 12:07:40 +00:00
|
|
|
logger.info('Calculating Access Rules.')
|
2021-02-23 23:36:37 +00:00
|
|
|
if world.players > 1:
|
|
|
|
for player in world.player_ids:
|
|
|
|
locality_rules(world, player)
|
2021-09-14 23:02:06 +00:00
|
|
|
else:
|
2021-09-16 22:17:54 +00:00
|
|
|
world.non_local_items[1].value = set()
|
|
|
|
world.local_items[1].value = set()
|
2021-04-01 09:40:58 +00:00
|
|
|
|
2021-06-11 16:02:48 +00:00
|
|
|
AutoWorld.call_all(world, "set_rules")
|
|
|
|
|
2021-07-14 13:24:34 +00:00
|
|
|
for player in world.player_ids:
|
2021-09-16 22:17:54 +00:00
|
|
|
exclusion_rules(world, player, world.exclude_locations[player].value)
|
2022-02-01 15:36:14 +00:00
|
|
|
world.priority_locations[player].value -= world.exclude_locations[player].value
|
|
|
|
for location_name in world.priority_locations[player].value:
|
|
|
|
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
|
2021-07-14 13:24:34 +00:00
|
|
|
|
2021-06-11 12:22:44 +00:00
|
|
|
AutoWorld.call_all(world, "generate_basic")
|
2021-04-01 09:40:58 +00:00
|
|
|
|
2021-01-02 11:49:43 +00:00
|
|
|
logger.info("Running Item Plando")
|
|
|
|
|
2021-03-20 23:47:17 +00:00
|
|
|
for item in world.itempool:
|
|
|
|
item.world = world
|
|
|
|
|
2021-01-04 14:14:20 +00:00
|
|
|
distribute_planned(world)
|
2021-01-02 11:49:43 +00:00
|
|
|
|
2021-08-09 04:50:11 +00:00
|
|
|
logger.info('Running Pre Main Fill.')
|
2021-01-24 07:26:39 +00:00
|
|
|
|
2021-08-09 04:50:11 +00:00
|
|
|
AutoWorld.call_all(world, "pre_fill")
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2021-10-17 18:53:06 +00:00
|
|
|
logger.info(f'Filling the world with {len(world.itempool)} items.')
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2021-03-14 07:38:02 +00:00
|
|
|
if world.algorithm == 'flood':
|
2017-05-20 12:07:40 +00:00
|
|
|
flood_items(world) # different algo, biased towards early game progress items
|
2021-03-14 07:38:02 +00:00
|
|
|
elif world.algorithm == 'balanced':
|
2021-08-10 07:03:44 +00:00
|
|
|
distribute_items_restrictive(world)
|
2017-06-03 19:28:02 +00:00
|
|
|
|
2021-08-29 23:16:04 +00:00
|
|
|
AutoWorld.call_all(world, 'post_fill')
|
2020-12-23 21:30:21 +00:00
|
|
|
|
2021-02-05 07:07:12 +00:00
|
|
|
if world.players > 1:
|
|
|
|
balance_multiworld_progression(world)
|
|
|
|
|
2021-09-03 15:30:10 +00:00
|
|
|
logger.info(f'Beginning output...')
|
2021-05-15 22:21:00 +00:00
|
|
|
outfilebase = 'AP_' + world.seed_name
|
2020-03-06 22:08:46 +00:00
|
|
|
|
2021-07-21 16:08:15 +00:00
|
|
|
output = tempfile.TemporaryDirectory()
|
|
|
|
with output as temp_dir:
|
2021-10-10 22:46:18 +00:00
|
|
|
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
|
2021-09-03 18:35:40 +00:00
|
|
|
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
|
|
|
|
2021-10-10 14:50:01 +00:00
|
|
|
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
|
2021-09-03 18:35:40 +00:00
|
|
|
for player in world.player_ids:
|
|
|
|
# skip starting a thread for methods that say "pass".
|
|
|
|
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
|
2021-10-10 22:46:18 +00:00
|
|
|
output_file_futures.append(
|
|
|
|
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
2021-09-03 18:35:40 +00:00
|
|
|
|
|
|
|
def get_entrance_to_region(region: Region):
|
|
|
|
for entrance in region.entrances:
|
|
|
|
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
|
|
|
|
return entrance
|
|
|
|
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
|
|
|
|
return get_entrance_to_region(entrance.parent_region)
|
|
|
|
|
|
|
|
# collect ER hint info
|
|
|
|
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
|
|
|
world.shuffle[player] != "vanilla" or world.retro[player]}
|
|
|
|
|
|
|
|
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 lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
|
|
|
er_hint_data[region.player][location.address] = main_entrance.name
|
|
|
|
|
|
|
|
checks_in_area = {player: {area: list() for area in ordered_areas}
|
|
|
|
for player in range(1, world.players + 1)}
|
|
|
|
|
|
|
|
for player in range(1, world.players + 1):
|
|
|
|
checks_in_area[player]["Total"] = 0
|
2021-08-09 07:15:41 +00:00
|
|
|
|
2021-07-21 16:08:15 +00:00
|
|
|
for location in world.get_filled_locations():
|
2021-09-03 18:35:40 +00:00
|
|
|
if type(location.address) is int:
|
|
|
|
main_entrance = get_entrance_to_region(location.parent_region)
|
|
|
|
if location.game != "A Link to the Past":
|
|
|
|
checks_in_area[location.player]["Light World"].append(location.address)
|
|
|
|
elif location.parent_region.dungeon:
|
|
|
|
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
|
|
|
'Inverted Ganons Tower': 'Ganons Tower'} \
|
|
|
|
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
|
|
|
checks_in_area[location.player][dungeonname].append(location.address)
|
2021-11-23 21:47:41 +00:00
|
|
|
elif location.parent_region.type == RegionType.LightWorld:
|
|
|
|
checks_in_area[location.player]["Light World"].append(location.address)
|
|
|
|
elif location.parent_region.type == RegionType.DarkWorld:
|
|
|
|
checks_in_area[location.player]["Dark World"].append(location.address)
|
2021-09-03 18:35:40 +00:00
|
|
|
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
|
|
|
checks_in_area[location.player]["Light World"].append(location.address)
|
|
|
|
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
|
|
|
checks_in_area[location.player]["Dark World"].append(location.address)
|
|
|
|
checks_in_area[location.player]["Total"] += 1
|
|
|
|
|
|
|
|
oldmancaves = []
|
|
|
|
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
|
|
|
for index, take_any in enumerate(takeanyregions):
|
2021-09-13 01:38:18 +00:00
|
|
|
for region in [world.get_region(take_any, player) for player in
|
|
|
|
world.get_game_players("A Link to the Past") if world.retro[player]]:
|
2021-10-10 22:46:18 +00:00
|
|
|
item = world.create_item(
|
|
|
|
region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
|
|
|
region.player)
|
2021-09-03 18:35:40 +00:00
|
|
|
player = region.player
|
|
|
|
location_id = SHOP_ID_START + total_shop_slots + index
|
|
|
|
|
|
|
|
main_entrance = get_entrance_to_region(region)
|
|
|
|
if main_entrance.parent_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] = main_entrance.name
|
|
|
|
oldmancaves.append(((location_id, player), (item.code, player)))
|
|
|
|
|
|
|
|
FillDisabledShopSlots(world)
|
|
|
|
|
|
|
|
def write_multidata():
|
|
|
|
import NetUtils
|
|
|
|
slot_data = {}
|
|
|
|
client_versions = {}
|
|
|
|
games = {}
|
2022-01-30 12:57:12 +00:00
|
|
|
minimum_versions = {"server": (0, 2, 4), "clients": client_versions}
|
|
|
|
slot_info = {}
|
|
|
|
names = [[name for player, name in sorted(world.player_name.items())]]
|
2021-09-03 18:35:40 +00:00
|
|
|
for slot in world.player_ids:
|
|
|
|
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
|
|
|
games[slot] = world.game[slot]
|
2022-01-30 15:55:55 +00:00
|
|
|
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot-1], world.game[slot], world.player_types[slot])
|
2021-10-10 14:50:01 +00:00
|
|
|
precollected_items = {player: [item.code for item in world_precollected]
|
|
|
|
for player, world_precollected in world.precollected_items.items()}
|
2021-09-03 18:35:40 +00:00
|
|
|
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
2022-01-14 18:27:44 +00:00
|
|
|
|
2021-09-03 18:35:40 +00:00
|
|
|
sending_visible_players = set()
|
|
|
|
|
|
|
|
for slot in world.player_ids:
|
|
|
|
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
2021-10-10 03:38:53 +00:00
|
|
|
if world.worlds[slot].sending_visible:
|
|
|
|
sending_visible_players.add(slot)
|
2021-09-03 18:35:40 +00:00
|
|
|
|
2021-10-03 12:40:25 +00:00
|
|
|
def precollect_hint(location):
|
|
|
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
2022-01-18 05:16:16 +00:00
|
|
|
location.item.code, False, "", location.item.flags)
|
2021-10-03 12:40:25 +00:00
|
|
|
precollected_hints[location.player].add(hint)
|
|
|
|
precollected_hints[location.item.player].add(hint)
|
|
|
|
|
2022-01-18 04:52:29 +00:00
|
|
|
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids}
|
2021-09-03 18:35:40 +00:00
|
|
|
for location in world.get_filled_locations():
|
|
|
|
if type(location.address) == int:
|
|
|
|
# item code None should be event, location.address should then also be None
|
|
|
|
assert location.item.code is not None
|
2022-01-18 04:52:29 +00:00
|
|
|
locations_data[location.player][location.address] = \
|
2022-01-18 05:16:16 +00:00
|
|
|
location.item.code, location.item.player, location.item.flags
|
2021-10-10 03:48:13 +00:00
|
|
|
if location.player in sending_visible_players:
|
2021-10-03 12:40:25 +00:00
|
|
|
precollect_hint(location)
|
|
|
|
elif location.name in world.start_location_hints[location.player]:
|
|
|
|
precollect_hint(location)
|
2021-09-30 17:49:36 +00:00
|
|
|
elif location.item.name in world.start_hints[location.item.player]:
|
2021-10-03 12:40:25 +00:00
|
|
|
precollect_hint(location)
|
2021-09-03 18:35:40 +00:00
|
|
|
|
|
|
|
multidata = {
|
|
|
|
"slot_data": slot_data,
|
2022-01-30 12:57:12 +00:00
|
|
|
"slot_info": slot_info,
|
|
|
|
"names": names, # TODO: remove around 0.2.5 in favor of slot_info
|
|
|
|
"games": games, # TODO: remove around 0.2.5 in favor of slot_info
|
2021-09-03 18:35:40 +00:00
|
|
|
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
|
|
|
"remote_items": {player for player in world.player_ids if
|
|
|
|
world.worlds[player].remote_items},
|
2021-09-23 01:48:37 +00:00
|
|
|
"remote_start_inventory": {player for player in world.player_ids if
|
|
|
|
world.worlds[player].remote_start_inventory},
|
2021-09-03 18:35:40 +00:00
|
|
|
"locations": locations_data,
|
|
|
|
"checks_in_area": checks_in_area,
|
2021-10-10 22:46:18 +00:00
|
|
|
"server_options": baked_server_options,
|
2021-09-03 18:35:40 +00:00
|
|
|
"er_hint_data": er_hint_data,
|
|
|
|
"precollected_items": precollected_items,
|
|
|
|
"precollected_hints": precollected_hints,
|
|
|
|
"version": tuple(version_tuple),
|
|
|
|
"tags": ["AP"],
|
|
|
|
"minimum_versions": minimum_versions,
|
|
|
|
"seed_name": world.seed_name
|
|
|
|
}
|
|
|
|
AutoWorld.call_all(world, "modify_multidata", multidata)
|
|
|
|
|
|
|
|
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
|
|
|
|
|
|
|
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
|
2022-02-02 15:29:29 +00:00
|
|
|
f.write(bytes([3])) # version of format
|
2021-09-03 18:35:40 +00:00
|
|
|
f.write(multidata)
|
|
|
|
|
|
|
|
multidata_task = pool.submit(write_multidata)
|
|
|
|
if not check_accessibility_task.result():
|
|
|
|
if not world.can_beat_game():
|
|
|
|
raise Exception("Game appears as unbeatable. Aborting.")
|
|
|
|
else:
|
|
|
|
logger.warning("Location Accessibility requirements not fulfilled.")
|
|
|
|
|
|
|
|
# retrieve exceptions via .result() if they occured.
|
2021-11-30 04:33:56 +00:00
|
|
|
multidata_task.result()
|
2021-09-13 01:38:18 +00:00
|
|
|
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
|
|
|
|
if i % 10 == 0 or i == len(output_file_futures):
|
2021-09-03 18:35:40 +00:00
|
|
|
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
|
|
|
|
future.result()
|
2021-08-27 12:52:33 +00:00
|
|
|
|
2021-09-12 23:32:32 +00:00
|
|
|
if args.spoiler > 1:
|
2021-07-21 16:08:15 +00:00
|
|
|
logger.info('Calculating playthrough.')
|
|
|
|
create_playthrough(world)
|
2021-08-27 12:52:33 +00:00
|
|
|
|
2021-09-12 23:32:32 +00:00
|
|
|
if args.spoiler:
|
2021-07-21 16:08:15 +00:00
|
|
|
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
2021-08-27 12:52:33 +00:00
|
|
|
|
2021-07-25 14:15:51 +00:00
|
|
|
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
|
|
|
logger.info(f'Creating final archive at {zipfilename}.')
|
|
|
|
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
2021-07-21 16:08:15 +00:00
|
|
|
compresslevel=9) as zf:
|
|
|
|
for file in os.scandir(temp_dir):
|
2021-08-27 12:52:33 +00:00
|
|
|
zf.write(file.path, arcname=file.name)
|
2019-04-18 09:23:24 +00:00
|
|
|
|
2020-08-21 16:35:48 +00:00
|
|
|
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
|
2017-05-15 18:28:04 +00:00
|
|
|
return world
|
|
|
|
|
2018-03-23 03:18:40 +00:00
|
|
|
|
2017-05-16 19:23:47 +00:00
|
|
|
def create_playthrough(world):
|
2021-04-08 17:53:24 +00:00
|
|
|
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
2017-05-16 19:23:47 +00:00
|
|
|
# get locations containing progress items
|
2021-02-27 16:11:54 +00:00
|
|
|
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
|
2018-01-01 20:55:13 +00:00
|
|
|
state_cache = [None]
|
2017-05-16 19:23:47 +00:00
|
|
|
collection_spheres = []
|
|
|
|
state = CollectionState(world)
|
2021-02-27 16:11:54 +00:00
|
|
|
sphere_candidates = set(prog_locations)
|
2020-08-13 22:34:41 +00:00
|
|
|
logging.debug('Building up collection spheres.')
|
2017-05-16 19:23:47 +00:00
|
|
|
while sphere_candidates:
|
2017-06-24 09:11:56 +00:00
|
|
|
|
2021-07-21 16:08:15 +00:00
|
|
|
# build up spheres of collection radius.
|
|
|
|
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
2021-02-27 16:11:54 +00:00
|
|
|
|
|
|
|
sphere = {location for location in sphere_candidates if state.can_reach(location)}
|
2017-05-16 19:23:47 +00:00
|
|
|
|
|
|
|
for location in sphere:
|
2018-01-01 20:55:13 +00:00
|
|
|
state.collect(location.item, True, location)
|
2017-06-17 12:40:37 +00:00
|
|
|
|
2021-02-27 16:11:54 +00:00
|
|
|
sphere_candidates -= sphere
|
2017-05-16 19:23:47 +00:00
|
|
|
collection_spheres.append(sphere)
|
2018-01-01 20:55:13 +00:00
|
|
|
state_cache.append(state.copy())
|
2017-05-26 07:55:24 +00:00
|
|
|
|
2020-08-13 22:34:41 +00:00
|
|
|
logging.debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere),
|
|
|
|
len(prog_locations))
|
2017-05-26 07:55:24 +00:00
|
|
|
if not sphere:
|
2020-08-13 22:34:41 +00:00
|
|
|
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
|
|
|
|
location.item.name, location.item.player, location.name, location.player) for location in
|
|
|
|
sphere_candidates])
|
2021-09-16 22:17:54 +00:00
|
|
|
if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
|
2020-08-13 22:34:41 +00:00
|
|
|
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
|
|
|
f'Something went terribly wrong here.')
|
2017-06-23 20:15:29 +00:00
|
|
|
else:
|
2021-03-17 09:53:40 +00:00
|
|
|
world.spoiler.unreachables = sphere_candidates
|
2017-06-23 20:15:29 +00:00
|
|
|
break
|
2021-03-17 10:46:44 +00:00
|
|
|
|
|
|
|
# in the second phase, we cull each sphere such that the game is still beatable,
|
|
|
|
# reducing each range of influence to the bare minimum required inside it
|
2021-03-17 09:53:40 +00:00
|
|
|
restore_later = {}
|
2021-02-03 06:14:53 +00:00
|
|
|
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
2021-02-03 05:55:08 +00:00
|
|
|
to_delete = set()
|
2017-05-16 19:23:47 +00:00
|
|
|
for location in sphere:
|
|
|
|
# we remove the item at location and check if game is still beatable
|
2021-06-11 12:22:44 +00:00
|
|
|
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
|
|
|
location.item.player)
|
2017-05-16 19:23:47 +00:00
|
|
|
old_item = location.item
|
|
|
|
location.item = None
|
2019-07-11 04:12:09 +00:00
|
|
|
if world.can_beat_game(state_cache[num]):
|
2021-02-03 05:55:08 +00:00
|
|
|
to_delete.add(location)
|
2021-03-17 09:53:40 +00:00
|
|
|
restore_later[location] = old_item
|
2017-05-16 19:23:47 +00:00
|
|
|
else:
|
|
|
|
# still required, got to keep it around
|
|
|
|
location.item = old_item
|
|
|
|
|
|
|
|
# cull entries in spheres for spoiler walkthrough at end
|
2021-02-03 05:55:08 +00:00
|
|
|
sphere -= to_delete
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2020-01-09 07:31:49 +00:00
|
|
|
# second phase, sphere 0
|
2021-03-17 10:46:44 +00:00
|
|
|
removed_precollected = []
|
2021-10-10 22:12:00 +00:00
|
|
|
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
|
2021-02-27 16:11:54 +00:00
|
|
|
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
2021-10-10 23:39:25 +00:00
|
|
|
world.precollected_items[item.player].remove(item)
|
2020-01-09 07:31:49 +00:00
|
|
|
world.state.remove(item)
|
|
|
|
if not world.can_beat_game():
|
|
|
|
world.push_precollected(item)
|
2021-03-17 10:46:44 +00:00
|
|
|
else:
|
|
|
|
removed_precollected.append(item)
|
2020-01-09 07:31:49 +00:00
|
|
|
|
2018-01-06 19:25:49 +00:00
|
|
|
# we are now down to just the required progress items in collection_spheres. Unfortunately
|
|
|
|
# the previous pruning stage could potentially have made certain items dependant on others
|
|
|
|
# in the same or later sphere (because the location had 2 ways to access but the item originally
|
|
|
|
# used to access it was deemed not required.) So we need to do one final sphere collection pass
|
|
|
|
# to build up the correct spheres
|
|
|
|
|
2021-02-03 05:55:08 +00:00
|
|
|
required_locations = {item for sphere in collection_spheres for item in sphere}
|
2018-01-06 19:25:49 +00:00
|
|
|
state = CollectionState(world)
|
|
|
|
collection_spheres = []
|
|
|
|
while required_locations:
|
2019-12-13 21:37:52 +00:00
|
|
|
state.sweep_for_events(key_only=True)
|
2018-01-06 19:25:49 +00:00
|
|
|
|
2021-02-03 13:26:00 +00:00
|
|
|
sphere = set(filter(state.can_reach, required_locations))
|
2018-01-06 19:25:49 +00:00
|
|
|
|
|
|
|
for location in sphere:
|
|
|
|
state.collect(location.item, True, location)
|
|
|
|
|
2021-02-27 16:11:54 +00:00
|
|
|
required_locations -= sphere
|
|
|
|
|
2018-01-06 19:25:49 +00:00
|
|
|
collection_spheres.append(sphere)
|
|
|
|
|
2021-06-11 12:22:44 +00:00
|
|
|
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
|
|
|
|
len(sphere), len(required_locations))
|
2018-01-06 19:25:49 +00:00
|
|
|
if not sphere:
|
2021-02-25 01:07:28 +00:00
|
|
|
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2018-01-01 20:55:13 +00:00
|
|
|
def flist_to_iter(node):
|
|
|
|
while node:
|
|
|
|
value, node = node
|
|
|
|
yield value
|
|
|
|
|
2018-01-06 21:25:14 +00:00
|
|
|
def get_path(state, region):
|
|
|
|
reversed_path_as_flist = state.path.get(region, (region, None))
|
|
|
|
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
|
|
|
|
# Now we combine the flat string list into (region, exit) pairs
|
|
|
|
pathsiter = iter(string_path_flat)
|
|
|
|
pathpairs = zip_longest(pathsiter, pathsiter)
|
|
|
|
return list(pathpairs)
|
|
|
|
|
2021-07-09 15:44:24 +00:00
|
|
|
world.spoiler.paths = {}
|
|
|
|
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
|
|
|
|
for player in topology_worlds:
|
2021-06-11 12:22:44 +00:00
|
|
|
world.spoiler.paths.update(
|
|
|
|
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
|
2021-07-09 15:44:24 +00:00
|
|
|
sphere if location.player == player})
|
2021-07-21 16:08:15 +00:00
|
|
|
if player in world.get_game_players("A Link to the Past"):
|
2021-08-28 10:56:52 +00:00
|
|
|
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
|
|
|
|
# Maybe move the big bomb over to the Event system instead?
|
|
|
|
if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
|
|
|
|
if world.mode[player] != 'inverted':
|
|
|
|
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
|
|
|
|
get_path(state, world.get_region('Big Bomb Shop', player))
|
|
|
|
else:
|
|
|
|
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
|
|
|
|
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
2018-01-01 20:55:13 +00:00
|
|
|
|
2017-05-16 19:23:47 +00:00
|
|
|
# we can finally output our playthrough
|
2021-10-10 22:12:00 +00:00
|
|
|
world.spoiler.playthrough = {"0": sorted([str(item) for item in
|
|
|
|
chain.from_iterable(world.precollected_items.values())
|
2021-10-10 14:50:01 +00:00
|
|
|
if item.advancement])}
|
2021-02-03 13:26:00 +00:00
|
|
|
|
2020-01-09 07:31:49 +00:00
|
|
|
for i, sphere in enumerate(collection_spheres):
|
2021-03-17 09:53:40 +00:00
|
|
|
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
|
|
|
|
|
|
|
|
# repair the world again
|
|
|
|
for location, item in restore_later.items():
|
|
|
|
location.item = item
|
2021-03-17 10:46:44 +00:00
|
|
|
|
|
|
|
for item in removed_precollected:
|
|
|
|
world.push_precollected(item)
|