2018-02-17 23:38:54 +00:00
|
|
|
import copy
|
2018-01-06 21:25:14 +00:00
|
|
|
from itertools import zip_longest
|
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 random
|
|
|
|
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-05-11 21:08:50 +00:00
|
|
|
from typing import Dict, Tuple
|
2017-12-17 05:25:46 +00:00
|
|
|
|
2021-02-24 05:02:51 +00:00
|
|
|
from BaseClasses import MultiWorld, CollectionState, Region, Item
|
2021-04-08 17:53:24 +00:00
|
|
|
from worlds.alttp.Items import ItemFactory, item_name_groups
|
2021-01-30 22:29:32 +00:00
|
|
|
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
|
2021-01-03 13:32:32 +00:00
|
|
|
lookup_vanilla_location_to_entrance
|
2020-10-24 03:38:56 +00:00
|
|
|
from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
2021-01-03 13:32:32 +00:00
|
|
|
from worlds.alttp.EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
|
2020-10-24 03:38:56 +00:00
|
|
|
from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
|
|
|
|
from worlds.alttp.Rules import set_rules
|
|
|
|
from worlds.alttp.Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
|
2021-01-04 14:14:20 +00:00
|
|
|
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
2021-01-30 22:43:15 +00:00
|
|
|
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
2020-10-24 03:38:56 +00:00
|
|
|
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
|
2020-07-14 02:48:56 +00:00
|
|
|
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
|
2021-04-08 17:53:24 +00:00
|
|
|
from worlds.hk import gen_hollow
|
2021-02-24 05:02:51 +00:00
|
|
|
from worlds.hk import create_regions as hk_create_regions
|
2021-04-01 09:40:58 +00:00
|
|
|
from worlds.factorio import gen_factorio, factorio_create_regions
|
|
|
|
from worlds.factorio.Mod import generate_mod
|
Minecraft Randomizer
Squash merge, original Commits:
* Minecraft locations, items, and generation without logic
* added id lookup for minecraft
* typing import fix in minecraft/Items.py
* fix 2
* implementing Minecraft options and hard/postgame advancement exclusion
* first logic pass (75/80)
* logic pass 2 and proper completion conditions
* added insane difficulty pool, modified method of excluding item pools for easier extension
* bump network_data_package version
* minecraft testing framework
* switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item
* Testing now functions, split tests up by advancement pane, added some story tests
* Newer testing framework: every advancement gets its own function, for ease of testing
* fixed logic for The End... Again...
* changed option names to "include_hard_advancements" etc.
* village/pillager-related advancements now require can_adventure: weapon + food
* a few minecraft tests
* rename "Flint & Steel" to "Flint and Steel" for parity with in-game name
* additional MC tests
* more tests, mostly nether-related tests
* more tests, removed anvil path for Two Birds One Arrow
* include Minecraft slot data, and a world seed for each Minecraft player slot
* Added new items: ender pearls, lapis, porkchops
* All remaining Minecraft tests
* formatting of Minecraft tests and logic for better readability
* require Wither kill for Monsters Hunted
* properly removed 8 Emeralds item from item pool
* enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill
* Added 12 new advancements (ported from old achievement system)
* renamed "On a Rail" for consistency with modern advancements
* tests for the new advancements
* moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data
* output minecraft options in the spoiler log
* modified advancement goal values for new advancements
* make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars
* fixed glowstone block logic for Not Quite Nine Lives
* setup for shuffling MC structures: building ER world and shuffling regions/entrances
* ensured Nether Fortresses can't be placed in the End
* finished logic for structure randomization
* fixed nonnative items always showing up as Hammers in ALttP shops
* output minecraft structure info in the spoiler
* generate .apmc file for communication with MC client
* fixed structure rando always using the same seed
* move stuff to worlds/minecraft/Regions.py
* make output apmc file have consistent name with other files
* added minecraft bottle macro; fixed tests imports
* generalizing MC region generation
* restructured structure shuffling in preparation for structure plando
* only output structure rando info in spoiler if they are shuffled
* Force structure rando to always be off, for the stable release
* added Minecraft options to player settings
* formally added combat_difficulty as an option
* Added Ender Dragon into playthrough, cleaned up goal map
* Added new difficulties: Easy, Normal, Hard combat
* moved .apmc generation time to prevent outputs on failed generation
* updated tests for new combat logic
* Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix
* moved all MC-specific functions into gen_minecraft
* renamed "logic_version" to "client_version"
* bug fixes
properly flagged event locations/items with id None
moved generation back to Main.py to fix mysterious generation failures
* moved link_minecraft_regions into minecraft init, left create_regions in Main for caching
* added seed_name, player_name, client_version to apmc file
* reenabled structure shuffle
* added entrance tests for minecraft
Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-08 11:38:57 +00:00
|
|
|
from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data
|
|
|
|
from worlds.minecraft.Regions import minecraft_create_regions
|
2021-02-23 23:36:37 +00:00
|
|
|
from worlds.generic.Rules import locality_rules
|
2021-05-09 19:22:21 +00:00
|
|
|
from worlds import Games, lookup_any_item_name_to_id
|
2020-07-10 20:43:54 +00:00
|
|
|
import Patch
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-06-27 22:24:45 +00:00
|
|
|
seeddigits = 20
|
|
|
|
|
|
|
|
|
|
|
|
def get_seed(seed=None):
|
|
|
|
if seed is None:
|
|
|
|
random.seed(None)
|
|
|
|
return random.randint(0, pow(10, seeddigits) - 1)
|
|
|
|
return seed
|
|
|
|
|
2019-04-29 21:11:23 +00:00
|
|
|
|
2021-03-03 00:54:52 +00:00
|
|
|
def get_same_seed(world: MultiWorld, seed_def: tuple) -> str:
|
2021-03-07 19:11:36 +00:00
|
|
|
seeds: Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
2021-02-22 20:42:14 +00:00
|
|
|
if seed_def in seeds:
|
|
|
|
return seeds[seed_def]
|
|
|
|
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
|
2021-03-07 19:11:36 +00:00
|
|
|
world.__named_seeds = seeds
|
2021-02-22 20:42:14 +00:00
|
|
|
return seeds[seed_def]
|
|
|
|
|
|
|
|
|
2017-05-25 13:58:35 +00:00
|
|
|
def main(args, seed=None):
|
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
|
|
|
|
2017-05-20 12:07:40 +00:00
|
|
|
logger = logging.getLogger('')
|
2020-06-27 22:24:45 +00:00
|
|
|
world.seed = get_seed(seed)
|
2020-07-14 05:01:51 +00:00
|
|
|
if args.race:
|
|
|
|
world.secure()
|
|
|
|
else:
|
|
|
|
world.random.seed(world.seed)
|
2021-05-15 22:21:00 +00:00
|
|
|
world.seed_name = 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()
|
2021-04-09 18:40:45 +00:00
|
|
|
world.swordless = args.swordless.copy()
|
2021-03-14 07:38:02 +00:00
|
|
|
world.difficulty = args.difficulty.copy()
|
|
|
|
world.item_functionality = args.item_functionality.copy()
|
|
|
|
world.timer = args.timer.copy()
|
|
|
|
world.progressive = args.progressive.copy()
|
|
|
|
world.goal = args.goal.copy()
|
2021-04-01 09:40:58 +00:00
|
|
|
world.local_items = args.local_items.copy()
|
2021-03-14 07:38:02 +00:00
|
|
|
if hasattr(args, "algorithm"): # current GUI options
|
|
|
|
world.algorithm = args.algorithm
|
|
|
|
world.shuffleganon = args.shuffleganon
|
|
|
|
world.custom = args.custom
|
|
|
|
world.customitemarray = args.customitemarray
|
|
|
|
|
|
|
|
world.accessibility = args.accessibility.copy()
|
|
|
|
world.retro = args.retro.copy()
|
|
|
|
|
|
|
|
world.hints = args.hints.copy()
|
|
|
|
|
2019-12-16 20:46:47 +00:00
|
|
|
world.mapshuffle = args.mapshuffle.copy()
|
|
|
|
world.compassshuffle = args.compassshuffle.copy()
|
|
|
|
world.keyshuffle = args.keyshuffle.copy()
|
|
|
|
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
2020-07-14 05:01:51 +00:00
|
|
|
world.crystals_needed_for_ganon = {
|
|
|
|
player: world.random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(
|
|
|
|
args.crystals_ganon[player]) for player in range(1, world.players + 1)}
|
|
|
|
world.crystals_needed_for_gt = {
|
|
|
|
player: world.random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player])
|
|
|
|
for player in range(1, world.players + 1)}
|
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()
|
2020-08-19 21:24:17 +00:00
|
|
|
world.enemy_shuffle = args.enemy_shuffle.copy()
|
2019-12-17 14:55:53 +00:00
|
|
|
world.enemy_health = args.enemy_health.copy()
|
|
|
|
world.enemy_damage = args.enemy_damage.copy()
|
2020-08-19 21:24:17 +00:00
|
|
|
world.killable_thieves = args.killable_thieves.copy()
|
|
|
|
world.bush_shuffle = args.bush_shuffle.copy()
|
|
|
|
world.tile_shuffle = args.tile_shuffle.copy()
|
2019-12-30 02:03:53 +00:00
|
|
|
world.beemizer = args.beemizer.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-01-18 17:51:10 +00:00
|
|
|
world.shufflepots = args.shufflepots.copy()
|
2020-01-22 05:28:58 +00:00
|
|
|
world.progressive = args.progressive.copy()
|
2020-04-12 22:46:32 +00:00
|
|
|
world.dungeon_counters = args.dungeon_counters.copy()
|
2020-04-16 09:02:16 +00:00
|
|
|
world.glitch_boots = args.glitch_boots.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-11-24 02:05:04 +00:00
|
|
|
world.shop_shuffle_slots = args.shop_shuffle_slots.copy()
|
2021-03-17 05:29:48 +00:00
|
|
|
world.progression_balancing = args.progression_balancing.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-03-14 07:38:02 +00:00
|
|
|
world.er_seeds = getattr(args, "er_seeds", {})
|
2020-10-07 17:51:46 +00:00
|
|
|
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.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-03-20 23:47:17 +00:00
|
|
|
import Options
|
|
|
|
for hk_option in Options.hollow_knight_options:
|
2021-03-23 20:56:11 +00:00
|
|
|
setattr(world, hk_option, getattr(args, hk_option, {}))
|
2021-04-03 12:47:49 +00:00
|
|
|
for factorio_option in Options.factorio_options:
|
|
|
|
setattr(world, factorio_option, getattr(args, factorio_option, {}))
|
Minecraft Randomizer
Squash merge, original Commits:
* Minecraft locations, items, and generation without logic
* added id lookup for minecraft
* typing import fix in minecraft/Items.py
* fix 2
* implementing Minecraft options and hard/postgame advancement exclusion
* first logic pass (75/80)
* logic pass 2 and proper completion conditions
* added insane difficulty pool, modified method of excluding item pools for easier extension
* bump network_data_package version
* minecraft testing framework
* switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item
* Testing now functions, split tests up by advancement pane, added some story tests
* Newer testing framework: every advancement gets its own function, for ease of testing
* fixed logic for The End... Again...
* changed option names to "include_hard_advancements" etc.
* village/pillager-related advancements now require can_adventure: weapon + food
* a few minecraft tests
* rename "Flint & Steel" to "Flint and Steel" for parity with in-game name
* additional MC tests
* more tests, mostly nether-related tests
* more tests, removed anvil path for Two Birds One Arrow
* include Minecraft slot data, and a world seed for each Minecraft player slot
* Added new items: ender pearls, lapis, porkchops
* All remaining Minecraft tests
* formatting of Minecraft tests and logic for better readability
* require Wither kill for Monsters Hunted
* properly removed 8 Emeralds item from item pool
* enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill
* Added 12 new advancements (ported from old achievement system)
* renamed "On a Rail" for consistency with modern advancements
* tests for the new advancements
* moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data
* output minecraft options in the spoiler log
* modified advancement goal values for new advancements
* make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars
* fixed glowstone block logic for Not Quite Nine Lives
* setup for shuffling MC structures: building ER world and shuffling regions/entrances
* ensured Nether Fortresses can't be placed in the End
* finished logic for structure randomization
* fixed nonnative items always showing up as Hammers in ALttP shops
* output minecraft structure info in the spoiler
* generate .apmc file for communication with MC client
* fixed structure rando always using the same seed
* move stuff to worlds/minecraft/Regions.py
* make output apmc file have consistent name with other files
* added minecraft bottle macro; fixed tests imports
* generalizing MC region generation
* restructured structure shuffling in preparation for structure plando
* only output structure rando info in spoiler if they are shuffled
* Force structure rando to always be off, for the stable release
* added Minecraft options to player settings
* formally added combat_difficulty as an option
* Added Ender Dragon into playthrough, cleaned up goal map
* Added new difficulties: Easy, Normal, Hard combat
* moved .apmc generation time to prevent outputs on failed generation
* updated tests for new combat logic
* Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix
* moved all MC-specific functions into gen_minecraft
* renamed "logic_version" to "client_version"
* bug fixes
properly flagged event locations/items with id None
moved generation back to Main.py to fix mysterious generation failures
* moved link_minecraft_regions into minecraft init, left create_regions in Main for caching
* added seed_name, player_name, client_version to apmc file
* reenabled structure shuffle
* added entrance tests for minecraft
Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-08 11:38:57 +00:00
|
|
|
for minecraft_option in Options.minecraft_options:
|
|
|
|
setattr(world, minecraft_option, getattr(args, minecraft_option, {}))
|
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
|
|
|
|
2020-07-16 06:01:29 +00:00
|
|
|
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
|
2019-04-18 09:23:24 +00:00
|
|
|
|
2021-02-20 20:01:38 +00:00
|
|
|
for player in range(1, world.players+1):
|
|
|
|
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
|
|
|
|
|
|
|
|
if "-" in world.shuffle[player]:
|
2021-02-22 20:42:14 +00:00
|
|
|
shuffle, seed = world.shuffle[player].split("-", 1)
|
2021-02-20 20:01:38 +00:00
|
|
|
world.shuffle[player] = shuffle
|
2021-02-24 21:23:42 +00:00
|
|
|
if shuffle == "vanilla":
|
|
|
|
world.er_seeds[player] = "vanilla"
|
2021-04-01 09:44:37 +00:00
|
|
|
elif seed.startswith("group-") or args.race:
|
2021-03-15 14:30:17 +00:00
|
|
|
# renamed from team to group to not confuse with existing team name use
|
2021-02-22 20:52:39 +00:00
|
|
|
world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
2021-03-15 14:30:17 +00:00
|
|
|
else: # not a race or group seed, use set seed as is.
|
2021-02-22 20:42:14 +00:00
|
|
|
world.er_seeds[player] = seed
|
2021-02-24 21:23:42 +00:00
|
|
|
elif world.shuffle[player] == "vanilla":
|
|
|
|
world.er_seeds[player] = "vanilla"
|
2021-02-20 20:01: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
|
|
|
|
|
|
|
parsed_names = parse_player_names(args.names, world.players, args.teams)
|
|
|
|
world.teams = len(parsed_names)
|
|
|
|
for i, team in enumerate(parsed_names, 1):
|
|
|
|
if world.players > 1:
|
|
|
|
logger.info('%s%s', 'Team%d: ' % i if world.teams > 1 else 'Players: ', ', '.join(team))
|
|
|
|
for player, name in enumerate(team, 1):
|
|
|
|
world.player_names[player].append(name)
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2020-01-14 09:42:27 +00:00
|
|
|
logger.info('')
|
2021-05-09 19:22:21 +00:00
|
|
|
for player in world.player_ids:
|
|
|
|
for item_name in args.startinventory[player]:
|
|
|
|
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
|
|
|
|
world.push_precollected(item)
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2021-03-07 21:05:07 +00:00
|
|
|
for player in world.alttp_player_ids:
|
|
|
|
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
for player in world.player_ids:
|
2021-01-05 17:56:20 +00:00
|
|
|
|
|
|
|
# enforce pre-defined local items.
|
|
|
|
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
|
|
|
world.local_items[player].add('Triforce Piece')
|
|
|
|
|
2020-11-22 21:53:31 +00:00
|
|
|
# items can't be both local and non-local, prefer local
|
|
|
|
world.non_local_items[player] -= world.local_items[player]
|
2020-01-06 18:13:42 +00:00
|
|
|
|
2020-12-19 23:36:29 +00:00
|
|
|
# dungeon items can't be in non-local if the appropriate dungeon item shuffle setting is not set.
|
|
|
|
if not world.mapshuffle[player]:
|
|
|
|
world.non_local_items[player] -= item_name_groups['Maps']
|
|
|
|
|
|
|
|
if not world.compassshuffle[player]:
|
|
|
|
world.non_local_items[player] -= item_name_groups['Compasses']
|
|
|
|
|
|
|
|
if not world.keyshuffle[player]:
|
|
|
|
world.non_local_items[player] -= item_name_groups['Small Keys']
|
|
|
|
|
|
|
|
if not world.bigkeyshuffle[player]:
|
|
|
|
world.non_local_items[player] -= item_name_groups['Big Keys']
|
|
|
|
|
2020-12-20 00:23:21 +00:00
|
|
|
# Not possible to place pendants/crystals out side of boss prizes yet.
|
|
|
|
world.non_local_items[player] -= item_name_groups['Pendants']
|
|
|
|
world.non_local_items[player] -= item_name_groups['Crystals']
|
2020-01-06 18:13:42 +00:00
|
|
|
|
2021-02-24 05:02:51 +00:00
|
|
|
for player in world.hk_player_ids:
|
|
|
|
hk_create_regions(world, player)
|
2021-02-21 19:17:24 +00:00
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
for player in world.factorio_player_ids:
|
|
|
|
factorio_create_regions(world, player)
|
|
|
|
|
Minecraft Randomizer
Squash merge, original Commits:
* Minecraft locations, items, and generation without logic
* added id lookup for minecraft
* typing import fix in minecraft/Items.py
* fix 2
* implementing Minecraft options and hard/postgame advancement exclusion
* first logic pass (75/80)
* logic pass 2 and proper completion conditions
* added insane difficulty pool, modified method of excluding item pools for easier extension
* bump network_data_package version
* minecraft testing framework
* switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item
* Testing now functions, split tests up by advancement pane, added some story tests
* Newer testing framework: every advancement gets its own function, for ease of testing
* fixed logic for The End... Again...
* changed option names to "include_hard_advancements" etc.
* village/pillager-related advancements now require can_adventure: weapon + food
* a few minecraft tests
* rename "Flint & Steel" to "Flint and Steel" for parity with in-game name
* additional MC tests
* more tests, mostly nether-related tests
* more tests, removed anvil path for Two Birds One Arrow
* include Minecraft slot data, and a world seed for each Minecraft player slot
* Added new items: ender pearls, lapis, porkchops
* All remaining Minecraft tests
* formatting of Minecraft tests and logic for better readability
* require Wither kill for Monsters Hunted
* properly removed 8 Emeralds item from item pool
* enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill
* Added 12 new advancements (ported from old achievement system)
* renamed "On a Rail" for consistency with modern advancements
* tests for the new advancements
* moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data
* output minecraft options in the spoiler log
* modified advancement goal values for new advancements
* make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars
* fixed glowstone block logic for Not Quite Nine Lives
* setup for shuffling MC structures: building ER world and shuffling regions/entrances
* ensured Nether Fortresses can't be placed in the End
* finished logic for structure randomization
* fixed nonnative items always showing up as Hammers in ALttP shops
* output minecraft structure info in the spoiler
* generate .apmc file for communication with MC client
* fixed structure rando always using the same seed
* move stuff to worlds/minecraft/Regions.py
* make output apmc file have consistent name with other files
* added minecraft bottle macro; fixed tests imports
* generalizing MC region generation
* restructured structure shuffling in preparation for structure plando
* only output structure rando info in spoiler if they are shuffled
* Force structure rando to always be off, for the stable release
* added Minecraft options to player settings
* formally added combat_difficulty as an option
* Added Ender Dragon into playthrough, cleaned up goal map
* Added new difficulties: Easy, Normal, Hard combat
* moved .apmc generation time to prevent outputs on failed generation
* updated tests for new combat logic
* Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix
* moved all MC-specific functions into gen_minecraft
* renamed "logic_version" to "client_version"
* bug fixes
properly flagged event locations/items with id None
moved generation back to Main.py to fix mysterious generation failures
* moved link_minecraft_regions into minecraft init, left create_regions in Main for caching
* added seed_name, player_name, client_version to apmc file
* reenabled structure shuffle
* added entrance tests for minecraft
Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-08 11:38:57 +00:00
|
|
|
for player in world.minecraft_player_ids:
|
|
|
|
minecraft_create_regions(world, player)
|
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
for player in world.alttp_player_ids:
|
|
|
|
if world.open_pyramid[player] == 'goal':
|
|
|
|
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
|
|
|
|
elif world.open_pyramid[player] == 'auto':
|
|
|
|
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
|
2021-05-08 10:04:03 +00:00
|
|
|
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not world.shuffle_ganon)
|
2021-02-21 19:17:24 +00:00
|
|
|
else:
|
|
|
|
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player])
|
|
|
|
|
|
|
|
|
2020-06-17 08:33:34 +00:00
|
|
|
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
|
|
|
|
|
2019-12-16 15:54:46 +00:00
|
|
|
if world.mode[player] != 'inverted':
|
2019-07-27 13:13:13 +00:00
|
|
|
create_regions(world, player)
|
2019-12-16 15:54:46 +00:00
|
|
|
else:
|
2019-07-27 13:13:13 +00:00
|
|
|
create_inverted_regions(world, player)
|
2020-01-10 10:41:22 +00:00
|
|
|
create_shops(world, player)
|
2019-12-16 15:54:46 +00:00
|
|
|
create_dungeons(world, player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2017-05-20 12:07:40 +00:00
|
|
|
logger.info('Shuffling the World about.')
|
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
for player in world.alttp_player_ids:
|
2020-08-22 13:28:24 +00:00
|
|
|
if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
|
2020-07-16 02:14:44 +00:00
|
|
|
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
|
|
|
|
world.fix_fake_world[player] = False
|
|
|
|
|
2021-02-20 01:30:55 +00:00
|
|
|
# seeded entrance shuffle
|
2021-02-20 20:01:38 +00:00
|
|
|
old_random = world.random
|
|
|
|
world.random = random.Random(world.er_seeds[player])
|
2021-02-20 01:30:55 +00:00
|
|
|
|
2019-12-16 15:54:46 +00:00
|
|
|
if world.mode[player] != 'inverted':
|
2019-07-27 13:13:13 +00:00
|
|
|
link_entrances(world, player)
|
2019-12-16 15:54:46 +00:00
|
|
|
mark_light_world_regions(world, player)
|
|
|
|
else:
|
2019-07-27 13:13:13 +00:00
|
|
|
link_inverted_entrances(world, player)
|
2019-12-16 15:54:46 +00:00
|
|
|
mark_dark_world_regions(world, player)
|
2021-02-20 01:30:55 +00:00
|
|
|
|
|
|
|
world.random = old_random
|
2021-01-02 21:41:03 +00:00
|
|
|
plando_connect(world, player)
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2019-04-18 21:11:11 +00:00
|
|
|
logger.info('Generating Item Pool.')
|
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
for player in world.alttp_player_ids:
|
2019-04-18 09:23:24 +00:00
|
|
|
generate_itempool(world, player)
|
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-04-01 09:40:58 +00:00
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
for player in world.alttp_player_ids:
|
2019-04-18 09:23:24 +00:00
|
|
|
set_rules(world, player)
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
for player in world.hk_player_ids:
|
|
|
|
gen_hollow(world, player)
|
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
for player in world.factorio_player_ids:
|
|
|
|
gen_factorio(world, player)
|
|
|
|
|
Minecraft Randomizer
Squash merge, original Commits:
* Minecraft locations, items, and generation without logic
* added id lookup for minecraft
* typing import fix in minecraft/Items.py
* fix 2
* implementing Minecraft options and hard/postgame advancement exclusion
* first logic pass (75/80)
* logic pass 2 and proper completion conditions
* added insane difficulty pool, modified method of excluding item pools for easier extension
* bump network_data_package version
* minecraft testing framework
* switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item
* Testing now functions, split tests up by advancement pane, added some story tests
* Newer testing framework: every advancement gets its own function, for ease of testing
* fixed logic for The End... Again...
* changed option names to "include_hard_advancements" etc.
* village/pillager-related advancements now require can_adventure: weapon + food
* a few minecraft tests
* rename "Flint & Steel" to "Flint and Steel" for parity with in-game name
* additional MC tests
* more tests, mostly nether-related tests
* more tests, removed anvil path for Two Birds One Arrow
* include Minecraft slot data, and a world seed for each Minecraft player slot
* Added new items: ender pearls, lapis, porkchops
* All remaining Minecraft tests
* formatting of Minecraft tests and logic for better readability
* require Wither kill for Monsters Hunted
* properly removed 8 Emeralds item from item pool
* enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill
* Added 12 new advancements (ported from old achievement system)
* renamed "On a Rail" for consistency with modern advancements
* tests for the new advancements
* moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data
* output minecraft options in the spoiler log
* modified advancement goal values for new advancements
* make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars
* fixed glowstone block logic for Not Quite Nine Lives
* setup for shuffling MC structures: building ER world and shuffling regions/entrances
* ensured Nether Fortresses can't be placed in the End
* finished logic for structure randomization
* fixed nonnative items always showing up as Hammers in ALttP shops
* output minecraft structure info in the spoiler
* generate .apmc file for communication with MC client
* fixed structure rando always using the same seed
* move stuff to worlds/minecraft/Regions.py
* make output apmc file have consistent name with other files
* added minecraft bottle macro; fixed tests imports
* generalizing MC region generation
* restructured structure shuffling in preparation for structure plando
* only output structure rando info in spoiler if they are shuffled
* Force structure rando to always be off, for the stable release
* added Minecraft options to player settings
* formally added combat_difficulty as an option
* Added Ender Dragon into playthrough, cleaned up goal map
* Added new difficulties: Easy, Normal, Hard combat
* moved .apmc generation time to prevent outputs on failed generation
* updated tests for new combat logic
* Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix
* moved all MC-specific functions into gen_minecraft
* renamed "logic_version" to "client_version"
* bug fixes
properly flagged event locations/items with id None
moved generation back to Main.py to fix mysterious generation failures
* moved link_minecraft_regions into minecraft init, left create_regions in Main for caching
* added seed_name, player_name, client_version to apmc file
* reenabled structure shuffle
* added entrance tests for minecraft
Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-08 11:38:57 +00:00
|
|
|
for player in world.minecraft_player_ids:
|
|
|
|
gen_minecraft(world, player)
|
|
|
|
|
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-01-24 07:26:39 +00:00
|
|
|
logger.info('Placing Dungeon Prizes.')
|
|
|
|
|
|
|
|
fill_prizes(world)
|
|
|
|
|
2017-06-23 19:32:31 +00:00
|
|
|
logger.info('Placing Dungeon Items.')
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2021-03-14 07:38:02 +00:00
|
|
|
if world.algorithm in ['balanced', 'vt26'] or any(
|
2021-01-02 11:49:43 +00:00
|
|
|
list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
|
|
|
|
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
|
2020-10-07 17:51:46 +00:00
|
|
|
fill_dungeons_restrictive(world)
|
2017-10-15 17:52:42 +00:00
|
|
|
else:
|
|
|
|
fill_dungeons(world)
|
2017-05-20 12:07:40 +00:00
|
|
|
|
|
|
|
logger.info('Fill the world.')
|
|
|
|
|
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 == 'vt25':
|
2019-12-16 14:27:20 +00:00
|
|
|
distribute_items_restrictive(world, False)
|
2021-03-14 07:38:02 +00:00
|
|
|
elif world.algorithm == 'vt26':
|
2021-01-02 11:49:43 +00:00
|
|
|
distribute_items_restrictive(world, True)
|
2021-03-14 07:38:02 +00:00
|
|
|
elif world.algorithm == 'balanced':
|
2019-12-16 14:27:20 +00:00
|
|
|
distribute_items_restrictive(world, True)
|
2017-06-03 19:28:02 +00:00
|
|
|
|
2021-01-16 01:23:23 +00:00
|
|
|
logger.info("Filling Shop Slots")
|
|
|
|
|
|
|
|
ShopSlotFill(world)
|
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-02-21 19:17:24 +00:00
|
|
|
logger.info('Generating output files.')
|
2021-05-15 22:21:00 +00:00
|
|
|
outfilebase = 'AP_' + world.seed_name
|
2020-01-04 21:08:13 +00:00
|
|
|
rom_names = []
|
2017-05-20 12:07:40 +00:00
|
|
|
|
2020-03-06 22:08:46 +00:00
|
|
|
def _gen_rom(team: int, player: int):
|
2020-08-19 21:24:17 +00:00
|
|
|
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
2020-03-06 22:08:46 +00:00
|
|
|
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
2020-10-04 17:57:30 +00:00
|
|
|
or world.shufflepots[player] or world.bush_shuffle[player]
|
2020-12-22 09:05:48 +00:00
|
|
|
or world.killable_thieves[player])
|
2019-05-29 23:10:16 +00:00
|
|
|
|
2020-06-09 19:52:46 +00:00
|
|
|
rom = LocalRom(args.rom)
|
2019-04-18 09:23:24 +00:00
|
|
|
|
2020-03-06 22:08:46 +00:00
|
|
|
patch_rom(world, rom, player, team, use_enemizer)
|
2019-05-29 23:10:16 +00:00
|
|
|
|
2020-06-09 19:52:46 +00:00
|
|
|
if use_enemizer:
|
2021-01-27 01:39:12 +00:00
|
|
|
patch_enemizer(world, team, player, rom, args.enemizercli)
|
2019-05-29 23:10:16 +00:00
|
|
|
|
2020-03-06 22:08:46 +00:00
|
|
|
if args.race:
|
2020-10-20 08:16:20 +00:00
|
|
|
patch_race_rom(rom, world, player)
|
2019-04-18 09:23:24 +00:00
|
|
|
|
2020-03-06 22:08:46 +00:00
|
|
|
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
|
2019-05-29 23:10:16 +00:00
|
|
|
|
2020-10-24 00:44:27 +00:00
|
|
|
palettes_options={}
|
|
|
|
palettes_options['dungeon']=args.uw_palettes[player]
|
|
|
|
palettes_options['overworld']=args.ow_palettes[player]
|
|
|
|
palettes_options['hud']=args.hud_palettes[player]
|
|
|
|
palettes_options['sword']=args.sword_palettes[player]
|
|
|
|
palettes_options['shield']=args.shield_palettes[player]
|
|
|
|
palettes_options['link']=args.link_palettes[player]
|
2021-01-03 12:13:59 +00:00
|
|
|
|
2020-03-06 22:08:46 +00:00
|
|
|
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
|
2021-03-03 00:59:33 +00:00
|
|
|
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
|
|
|
palettes_options, world, player, True,
|
2021-03-07 19:11:36 +00:00
|
|
|
reduceflashing=args.reduceflashing[player] or args.race,
|
2021-03-03 00:59:33 +00:00
|
|
|
triforcehud=args.triforcehud[player])
|
2020-10-24 00:44:27 +00:00
|
|
|
|
2020-06-09 19:52:46 +00:00
|
|
|
mcsb_name = ''
|
|
|
|
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
|
|
|
world.bigkeyshuffle[player]]):
|
|
|
|
mcsb_name = '-keysanity'
|
|
|
|
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
|
|
|
world.bigkeyshuffle[player]].count(True) == 1:
|
2020-09-02 22:06:36 +00:00
|
|
|
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else \
|
|
|
|
'-compassshuffle' if world.compassshuffle[player] else \
|
|
|
|
'-universal_keys' if world.keyshuffle[player] == "universal" else \
|
|
|
|
'-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
2020-06-09 19:52:46 +00:00
|
|
|
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
|
|
|
world.bigkeyshuffle[player]]):
|
|
|
|
mcsb_name = '-%s%s%s%sshuffle' % (
|
|
|
|
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
2020-09-02 22:06:36 +00:00
|
|
|
'U' if world.keyshuffle[player] == "universal" else 'S' if world.keyshuffle[player] else '',
|
|
|
|
'B' if world.bigkeyshuffle[player] else '')
|
2020-06-09 19:52:46 +00:00
|
|
|
|
2021-05-14 13:25:57 +00:00
|
|
|
outfilepname = f'_P{player}'
|
2020-09-02 22:06:36 +00:00
|
|
|
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \
|
|
|
|
if world.player_names[player][team] != 'Player%d' % player else ''
|
|
|
|
outfilestuffs = {
|
|
|
|
"logic": world.logic[player], # 0
|
|
|
|
"difficulty": world.difficulty[player], # 1
|
2021-02-10 06:01:03 +00:00
|
|
|
"item_functionality": world.item_functionality[player], # 2
|
2020-09-02 22:06:36 +00:00
|
|
|
"mode": world.mode[player], # 3
|
|
|
|
"goal": world.goal[player], # 4
|
|
|
|
"timer": str(world.timer[player]), # 5
|
|
|
|
"shuffle": world.shuffle[player], # 6
|
|
|
|
"algorithm": world.algorithm, # 7
|
|
|
|
"mscb": mcsb_name, # 8
|
|
|
|
"retro": world.retro[player], # 9
|
|
|
|
"progressive": world.progressive, # A
|
|
|
|
"hints": 'True' if world.hints[player] else 'False' # B
|
|
|
|
}
|
2021-01-11 03:06:25 +00:00
|
|
|
# 0 1 2 3 4 5 6 7 8 9 A B
|
2020-09-02 22:06:36 +00:00
|
|
|
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
|
|
|
|
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity-retro
|
|
|
|
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random
|
|
|
|
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints
|
|
|
|
outfilestuffs["logic"], # 0
|
|
|
|
|
|
|
|
outfilestuffs["difficulty"], # 1
|
2021-02-10 06:01:03 +00:00
|
|
|
outfilestuffs["item_functionality"], # 2
|
2020-09-02 22:06:36 +00:00
|
|
|
outfilestuffs["mode"], # 3
|
|
|
|
outfilestuffs["goal"], # 4
|
|
|
|
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
|
|
|
|
|
|
|
|
outfilestuffs["shuffle"], # 6
|
|
|
|
outfilestuffs["algorithm"], # 7
|
|
|
|
outfilestuffs["mscb"], # 8
|
|
|
|
|
|
|
|
"-retro" if outfilestuffs["retro"] == "True" else "", # 9
|
|
|
|
"-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A
|
|
|
|
"-nohints" if not outfilestuffs["hints"] == "True" else "") # B
|
|
|
|
) if not args.outputname else ''
|
2020-06-09 19:52:46 +00:00
|
|
|
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
|
2020-08-16 09:13:50 +00:00
|
|
|
rom.write_to_file(rompath, hide_enemizer=True)
|
2020-06-09 19:52:46 +00:00
|
|
|
if args.create_diff:
|
2021-05-14 13:25:57 +00:00
|
|
|
Patch.create_patch_file(rompath, player=player, player_name = world.player_names[player][team])
|
2020-10-19 06:26:31 +00:00
|
|
|
return player, team, bytes(rom.name)
|
2020-03-06 22:08:46 +00:00
|
|
|
|
2020-08-21 16:35:48 +00:00
|
|
|
pool = concurrent.futures.ThreadPoolExecutor()
|
2021-05-19 19:55:18 +00:00
|
|
|
|
2021-01-11 18:56:18 +00:00
|
|
|
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
2021-05-12 23:40:36 +00:00
|
|
|
|
|
|
|
rom_futures = []
|
|
|
|
mod_futures = []
|
|
|
|
for team in range(world.teams):
|
|
|
|
for player in world.alttp_player_ids:
|
|
|
|
rom_futures.append(pool.submit(_gen_rom, team, player))
|
|
|
|
for player in world.factorio_player_ids:
|
2021-05-15 22:21:00 +00:00
|
|
|
mod_futures.append(pool.submit(generate_mod, world, player))
|
2021-05-12 23:40:36 +00:00
|
|
|
|
|
|
|
def get_entrance_to_region(region: Region):
|
|
|
|
for entrance in region.entrances:
|
|
|
|
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld):
|
|
|
|
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 range(1, world.players + 1) if world.shuffle[player] != "vanilla" or world.retro[player]}
|
|
|
|
from worlds.alttp.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 lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
|
|
|
er_hint_data[region.player][location.address] = main_entrance.name
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
|
|
|
|
main_entrance = get_entrance_to_region(location.parent_region)
|
|
|
|
if location.game != Games.LTTP:
|
|
|
|
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)
|
|
|
|
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):
|
|
|
|
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if world.retro[player]]:
|
|
|
|
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player)
|
|
|
|
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)))
|
|
|
|
|
2021-05-13 00:10:37 +00:00
|
|
|
|
2021-05-12 23:40:36 +00:00
|
|
|
|
|
|
|
FillDisabledShopSlots(world)
|
|
|
|
|
|
|
|
def write_multidata(roms, mods):
|
|
|
|
import base64
|
2021-05-13 00:10:37 +00:00
|
|
|
import NetUtils
|
2021-05-12 23:40:36 +00:00
|
|
|
for future in roms:
|
|
|
|
rom_name = future.result()
|
|
|
|
rom_names.append(rom_name)
|
|
|
|
slot_data = {}
|
|
|
|
client_versions = {}
|
|
|
|
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
|
|
|
|
games = {}
|
|
|
|
for slot in world.player_ids:
|
|
|
|
client_versions[slot] = (0, 0, 3)
|
|
|
|
games[slot] = world.game[slot]
|
|
|
|
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
|
|
|
slot, team, rom_name in rom_names}
|
2021-05-13 00:10:37 +00:00
|
|
|
precollected_items = {player: [] for player in range(1, world.players+1)}
|
|
|
|
for item in world.precollected_items:
|
|
|
|
precollected_items[item.player].append(item.code)
|
2021-05-13 23:25:41 +00:00
|
|
|
precollected_hints = {player: set() for player in range(1, world.players+1)}
|
2021-05-13 00:10:37 +00:00
|
|
|
# for now special case Factorio visibility
|
|
|
|
sending_visible_players = set()
|
|
|
|
for player in world.factorio_player_ids:
|
|
|
|
if world.visibility[player]:
|
|
|
|
sending_visible_players.add(player)
|
2021-05-12 23:40:36 +00:00
|
|
|
|
|
|
|
for i, team in enumerate(parsed_names):
|
|
|
|
for player, name in enumerate(team, 1):
|
|
|
|
if player not in world.alttp_player_ids:
|
|
|
|
connect_names[name] = (i, player)
|
|
|
|
for slot in world.hk_player_ids:
|
|
|
|
slots_data = slot_data[slot] = {}
|
|
|
|
for option_name in Options.hollow_knight_options:
|
|
|
|
option = getattr(world, option_name)[slot]
|
|
|
|
slots_data[option_name] = int(option.value)
|
|
|
|
for slot in world.minecraft_player_ids:
|
|
|
|
slot_data[slot] = fill_minecraft_slot_data(world, slot)
|
|
|
|
|
|
|
|
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
|
|
|
for location in world.get_filled_locations():
|
|
|
|
if type(location.address) == int:
|
|
|
|
locations_data[location.player][location.address] = (location.item.code, location.item.player)
|
2021-05-13 00:10:37 +00:00
|
|
|
if location.player in sending_visible_players and location.item.player != location.player:
|
2021-05-13 23:25:41 +00:00
|
|
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
|
|
|
location.item.code, False)
|
|
|
|
precollected_hints[location.player].add(hint)
|
|
|
|
precollected_hints[location.item.player].add(hint)
|
2021-05-13 00:35:50 +00:00
|
|
|
elif location.item.name in args.start_hints[location.item.player]:
|
2021-05-13 23:25:41 +00:00
|
|
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
|
|
|
location.item.code, False,
|
|
|
|
er_hint_data.get(location.player, {}).get(location.address, ""))
|
|
|
|
precollected_hints[location.player].add(hint)
|
|
|
|
precollected_hints[location.item.player].add(hint)
|
|
|
|
|
2021-05-12 23:40:36 +00:00
|
|
|
multidata = zlib.compress(pickle.dumps({
|
|
|
|
"slot_data" : slot_data,
|
|
|
|
"games": games,
|
|
|
|
"names": parsed_names,
|
|
|
|
"connect_names": connect_names,
|
|
|
|
"remote_items": {player for player in range(1, world.players + 1) if
|
|
|
|
world.remote_items[player]},
|
|
|
|
"locations": locations_data,
|
|
|
|
"checks_in_area": checks_in_area,
|
|
|
|
"server_options": get_options()["server_options"],
|
|
|
|
"er_hint_data": er_hint_data,
|
|
|
|
"precollected_items": precollected_items,
|
2021-05-13 00:10:37 +00:00
|
|
|
"precollected_hints": precollected_hints,
|
2021-05-12 23:40:36 +00:00
|
|
|
"version": tuple(_version_tuple),
|
|
|
|
"tags": ["AP"],
|
|
|
|
"minimum_versions": minimum_versions,
|
2021-05-15 22:21:00 +00:00
|
|
|
"seed_name": world.seed_name
|
2021-05-12 23:40:36 +00:00
|
|
|
}), 9)
|
|
|
|
|
|
|
|
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
|
|
|
|
f.write(bytes([1])) # version of format
|
|
|
|
f.write(multidata)
|
|
|
|
for future in mods:
|
|
|
|
future.result() # collect errors if they occured
|
|
|
|
|
|
|
|
multidata_task = pool.submit(write_multidata, rom_futures, mod_futures)
|
2021-01-11 18:56:18 +00:00
|
|
|
if not check_accessibility_task.result():
|
|
|
|
if not world.can_beat_game():
|
2021-02-22 10:18:53 +00:00
|
|
|
raise Exception("Game appears as unbeatable. Aborting.")
|
2021-01-11 18:56:18 +00:00
|
|
|
else:
|
|
|
|
logger.warning("Location Accessibility requirements not fulfilled.")
|
2020-08-23 10:06:00 +00:00
|
|
|
if multidata_task:
|
|
|
|
multidata_task.result() # retrieve exception if one exists
|
2020-08-21 16:35:48 +00:00
|
|
|
pool.shutdown() # wait for all queued tasks to complete
|
Minecraft Randomizer
Squash merge, original Commits:
* Minecraft locations, items, and generation without logic
* added id lookup for minecraft
* typing import fix in minecraft/Items.py
* fix 2
* implementing Minecraft options and hard/postgame advancement exclusion
* first logic pass (75/80)
* logic pass 2 and proper completion conditions
* added insane difficulty pool, modified method of excluding item pools for easier extension
* bump network_data_package version
* minecraft testing framework
* switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item
* Testing now functions, split tests up by advancement pane, added some story tests
* Newer testing framework: every advancement gets its own function, for ease of testing
* fixed logic for The End... Again...
* changed option names to "include_hard_advancements" etc.
* village/pillager-related advancements now require can_adventure: weapon + food
* a few minecraft tests
* rename "Flint & Steel" to "Flint and Steel" for parity with in-game name
* additional MC tests
* more tests, mostly nether-related tests
* more tests, removed anvil path for Two Birds One Arrow
* include Minecraft slot data, and a world seed for each Minecraft player slot
* Added new items: ender pearls, lapis, porkchops
* All remaining Minecraft tests
* formatting of Minecraft tests and logic for better readability
* require Wither kill for Monsters Hunted
* properly removed 8 Emeralds item from item pool
* enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill
* Added 12 new advancements (ported from old achievement system)
* renamed "On a Rail" for consistency with modern advancements
* tests for the new advancements
* moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data
* output minecraft options in the spoiler log
* modified advancement goal values for new advancements
* make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars
* fixed glowstone block logic for Not Quite Nine Lives
* setup for shuffling MC structures: building ER world and shuffling regions/entrances
* ensured Nether Fortresses can't be placed in the End
* finished logic for structure randomization
* fixed nonnative items always showing up as Hammers in ALttP shops
* output minecraft structure info in the spoiler
* generate .apmc file for communication with MC client
* fixed structure rando always using the same seed
* move stuff to worlds/minecraft/Regions.py
* make output apmc file have consistent name with other files
* added minecraft bottle macro; fixed tests imports
* generalizing MC region generation
* restructured structure shuffling in preparation for structure plando
* only output structure rando info in spoiler if they are shuffled
* Force structure rando to always be off, for the stable release
* added Minecraft options to player settings
* formally added combat_difficulty as an option
* Added Ender Dragon into playthrough, cleaned up goal map
* Added new difficulties: Easy, Normal, Hard combat
* moved .apmc generation time to prevent outputs on failed generation
* updated tests for new combat logic
* Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix
* moved all MC-specific functions into gen_minecraft
* renamed "logic_version" to "client_version"
* bug fixes
properly flagged event locations/items with id None
moved generation back to Main.py to fix mysterious generation failures
* moved link_minecraft_regions into minecraft init, left create_regions in Main for caching
* added seed_name, player_name, client_version to apmc file
* reenabled structure shuffle
* added entrance tests for minecraft
Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-08 11:38:57 +00:00
|
|
|
for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error
|
2021-05-15 22:21:00 +00:00
|
|
|
generate_mc_data(world, player)
|
2021-03-17 09:53:40 +00:00
|
|
|
if not args.skip_playthrough:
|
|
|
|
logger.info('Calculating playthrough.')
|
Minecraft Randomizer
Squash merge, original Commits:
* Minecraft locations, items, and generation without logic
* added id lookup for minecraft
* typing import fix in minecraft/Items.py
* fix 2
* implementing Minecraft options and hard/postgame advancement exclusion
* first logic pass (75/80)
* logic pass 2 and proper completion conditions
* added insane difficulty pool, modified method of excluding item pools for easier extension
* bump network_data_package version
* minecraft testing framework
* switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item
* Testing now functions, split tests up by advancement pane, added some story tests
* Newer testing framework: every advancement gets its own function, for ease of testing
* fixed logic for The End... Again...
* changed option names to "include_hard_advancements" etc.
* village/pillager-related advancements now require can_adventure: weapon + food
* a few minecraft tests
* rename "Flint & Steel" to "Flint and Steel" for parity with in-game name
* additional MC tests
* more tests, mostly nether-related tests
* more tests, removed anvil path for Two Birds One Arrow
* include Minecraft slot data, and a world seed for each Minecraft player slot
* Added new items: ender pearls, lapis, porkchops
* All remaining Minecraft tests
* formatting of Minecraft tests and logic for better readability
* require Wither kill for Monsters Hunted
* properly removed 8 Emeralds item from item pool
* enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill
* Added 12 new advancements (ported from old achievement system)
* renamed "On a Rail" for consistency with modern advancements
* tests for the new advancements
* moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data
* output minecraft options in the spoiler log
* modified advancement goal values for new advancements
* make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars
* fixed glowstone block logic for Not Quite Nine Lives
* setup for shuffling MC structures: building ER world and shuffling regions/entrances
* ensured Nether Fortresses can't be placed in the End
* finished logic for structure randomization
* fixed nonnative items always showing up as Hammers in ALttP shops
* output minecraft structure info in the spoiler
* generate .apmc file for communication with MC client
* fixed structure rando always using the same seed
* move stuff to worlds/minecraft/Regions.py
* make output apmc file have consistent name with other files
* added minecraft bottle macro; fixed tests imports
* generalizing MC region generation
* restructured structure shuffling in preparation for structure plando
* only output structure rando info in spoiler if they are shuffled
* Force structure rando to always be off, for the stable release
* added Minecraft options to player settings
* formally added combat_difficulty as an option
* Added Ender Dragon into playthrough, cleaned up goal map
* Added new difficulties: Easy, Normal, Hard combat
* moved .apmc generation time to prevent outputs on failed generation
* updated tests for new combat logic
* Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix
* moved all MC-specific functions into gen_minecraft
* renamed "logic_version" to "client_version"
* bug fixes
properly flagged event locations/items with id None
moved generation back to Main.py to fix mysterious generation failures
* moved link_minecraft_regions into minecraft init, left create_regions in Main for caching
* added seed_name, player_name, client_version to apmc file
* reenabled structure shuffle
* added entrance tests for minecraft
Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-08 11:38:57 +00:00
|
|
|
create_playthrough(world)
|
2020-08-21 16:35:48 +00:00
|
|
|
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done
|
2019-04-18 09:23:24 +00:00
|
|
|
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
|
|
|
|
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:
|
2019-12-13 21:37:52 +00:00
|
|
|
state.sweep_for_events(key_only=True)
|
2017-06-24 09:11:56 +00:00
|
|
|
|
2017-05-16 19:23:47 +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])
|
2019-12-17 11:14:29 +00:00
|
|
|
if any([world.accessibility[location.item.player] != 'none' 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-02-27 16:11:54 +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-02-03 06:14:53 +00:00
|
|
|
for item in (i for i in world.precollected_items 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)
|
2020-01-09 07:31:49 +00:00
|
|
|
world.precollected_items.remove(item)
|
|
|
|
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-02-25 01:07:28 +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-03-17 09:53:40 +00:00
|
|
|
world.spoiler.paths = dict()
|
2019-04-18 09:23:24 +00:00
|
|
|
for player in range(1, world.players + 1):
|
2021-03-17 09:53:40 +00:00
|
|
|
world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
|
2021-02-21 19:17:24 +00:00
|
|
|
if player in world.alttp_player_ids:
|
2021-03-17 09:53:40 +00:00
|
|
|
for path in dict(world.spoiler.paths).values():
|
2021-02-21 19:17:24 +00:00
|
|
|
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
|
|
|
|
if world.mode[player] != 'inverted':
|
2021-03-17 09:53:40 +00:00
|
|
|
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
|
2021-02-21 19:17:24 +00:00
|
|
|
else:
|
2021-03-17 09:53:40 +00:00
|
|
|
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-03-17 09:53:40 +00:00
|
|
|
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items 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)
|