2017-12-17 05:25:46 +00:00
from collections import OrderedDict
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 json
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
2017-12-17 05:25:46 +00:00
2018-09-23 02:51:54 +00:00
from BaseClasses import World , CollectionState , Item , Region , Location , Shop
2020-11-22 21:53:31 +00:00
from Items import ItemFactory , item_table
2020-06-20 10:22:50 +00:00
from Regions import create_regions , create_shops , mark_light_world_regions , lookup_vanilla_location_to_entrance
2019-07-27 13:13:13 +00:00
from InvertedRegions import create_inverted_regions , mark_dark_world_regions
from EntranceShuffle import link_entrances , link_inverted_entrances
2020-06-09 20:11:14 +00:00
from Rom import patch_rom , patch_race_rom , patch_enemizer , apply_rom_settings , LocalRom , get_hash_string
2017-05-15 18:28:04 +00:00
from Rules import set_rules
2017-10-15 17:52:42 +00:00
from Dungeons import create_dungeons , fill_dungeons , fill_dungeons_restrictive
2020-08-25 15:44:03 +00:00
from Fill import distribute_items_restrictive , flood_items , balance_multiworld_progression
2020-08-20 13:43:22 +00:00
from 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
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
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-10-24 00:44:27 +00:00
2020-01-02 01:38:26 +00:00
start = time . perf_counter ( )
2017-05-15 18:28:04 +00:00
# initialize the world
2020-01-22 05:28:58 +00:00
world = World ( args . multi , args . shuffle , args . logic , args . mode , args . swords , args . difficulty ,
args . item_functionality , args . timer , args . progressive . copy ( ) , args . goal , args . algorithm ,
args . accessibility , args . shuffleganon , args . retro , args . custom , args . customitemarray , args . hints )
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 )
2017-05-20 12:07:40 +00:00
2020-01-18 08:50:12 +00:00
world . remote_items = args . remote_items . 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 ( )
2020-05-18 01:54:29 +00:00
world . progression_balancing = { player : not balance for player , balance in args . skip_progression_balancing . items ( ) }
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 ( )
world . restrict_dungeon_item_on_boss = args . restrict_dungeon_item_on_boss . copy ( )
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
2020-04-20 12:50:49 +00:00
logger . info ( ' ALttP Berserker \' s Multiworld 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 ( ' ' )
2017-05-20 12:07:40 +00:00
2019-12-16 15:54:46 +00:00
for player in range ( 1 , world . players + 1 ) :
2019-12-16 16:46:21 +00:00
world . difficulty_requirements [ player ] = difficulties [ world . difficulty [ player ] ]
2018-01-04 06:06:22 +00:00
2020-01-06 18:13:42 +00:00
for tok in filter ( None , args . startinventory [ player ] . split ( ' , ' ) ) :
item = ItemFactory ( tok . strip ( ) , player )
if item :
world . push_precollected ( item )
2020-11-22 21:53:31 +00:00
# item in item_table gets checked in mystery, but not CLI - so we double-check here
world . local_items [ player ] = { item . strip ( ) for item in args . local_items [ player ] . split ( ' , ' ) if
item . strip ( ) in item_table }
2020-11-22 22:29:11 +00:00
world . non_local_items [ player ] = { item . strip ( ) for item in args . non_local_items [ player ] . split ( ' , ' ) if
2020-11-22 21:53:31 +00:00
item . strip ( ) in item_table }
# 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-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. ' )
2019-12-16 15:54:46 +00:00
for player in range ( 1 , world . players + 1 ) :
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
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 )
2017-05-20 12:07:40 +00:00
2019-04-18 21:11:11 +00:00
logger . info ( ' Generating Item Pool. ' )
2019-04-18 09:23:24 +00:00
for player in range ( 1 , world . players + 1 ) :
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. ' )
2019-04-18 09:23:24 +00:00
for player in range ( 1 , world . players + 1 ) :
set_rules ( world , player )
2017-05-20 12:07:40 +00:00
2019-04-18 21:11:11 +00:00
logger . info ( ' Placing Dungeon Prizes. ' )
2017-08-05 15:52:18 +00:00
2019-04-18 21:11:11 +00:00
fill_prizes ( world )
2017-08-05 15:52:18 +00:00
2017-06-23 19:32:31 +00:00
logger . info ( ' Placing Dungeon Items. ' )
2017-05-20 12:07:40 +00:00
2017-10-15 20:34:46 +00:00
shuffled_locations = None
2019-12-16 20:46:47 +00:00
if args . algorithm in [ ' balanced ' , ' vt26 ' ] or any ( 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. ' )
2017-05-25 13:58:35 +00:00
if args . algorithm == ' flood ' :
2017-05-20 12:07:40 +00:00
flood_items ( world ) # different algo, biased towards early game progress items
2017-08-01 17:07:44 +00:00
elif args . algorithm == ' vt25 ' :
2019-12-16 14:27:20 +00:00
distribute_items_restrictive ( world , False )
2017-10-15 17:52:42 +00:00
elif args . algorithm == ' vt26 ' :
2019-12-16 14:27:20 +00:00
distribute_items_restrictive ( world , True , shuffled_locations )
2017-11-12 00:05:06 +00:00
elif args . algorithm == ' balanced ' :
2019-12-16 14:27:20 +00:00
distribute_items_restrictive ( world , True )
2017-06-03 19:28:02 +00:00
2020-12-23 17:28:42 +00:00
for x in range ( 1 , world . players + 1 ) :
shop_slot_bad = [ ]
candidates = [ ]
for location in world . get_locations ( ) :
if ' Shop Slot ' in location . name and location . parent_region . shop is not None :
slot_num = int ( location . name [ - 1 ] ) - 1
shop_item = location . parent_region . shop . inventory [ slot_num ]
item = location . item
print ( item . name , shop_item [ ' item ' ] )
if ( shop_item is not None and shop_item [ ' item ' ] == item . name ) or ' Rupee ' in item . name or ( item . name in [ ' Bee ' ] ) :
shop_slot_bad . append ( location )
elif location . item . name in [ ' Progressive Shield ' , ' Bombs (3) ' , ' Bombs (10) ' , ' Bee Trap ' ,
' Arrows (10) ' , ' Single Bomb ' , ' Single Arrow ' , ' Arrow Upgrade (+5) ' , ' Bomb Upgrade (+5) ' ,
' Shovel ' , ' Progressive Mail ' , ' Bug Catching Net ' , ' Cane of Byrna ' , ' Triforce Piece '
' Piece of Heart ' , ' Boss Heart Container ' , ' Sanctuary Heart Container ' ] :
candidates . append ( location )
elif any ( [ x in location . item . name for x in [ ' Key ' , ' Map ' , ' Compass ' , ' Clock ' ] ] ) :
candidates . append ( location )
world . random . shuffle ( candidates )
for slot in shop_slot_bad : # shop slot locations with redundant items
for c in candidates : # chosen item locations
if ' Rupee ' in c . item . name or ' Shop Slot ' in c . name : continue
if c . item_rule ( slot . item ) :
print ( ' Swapping {} with {} :: {} ||| {} ' . format ( c , slot , c . item , slot . item ) )
c . item , slot . item = slot . item , c . item
if not world . can_beat_game ( ) :
print ( ' NOT GOOD ' )
c . item , slot . item = slot . item , c . item
else : break
2020-05-18 01:54:29 +00:00
if world . players > 1 :
2019-04-18 09:23:24 +00:00
balance_multiworld_progression ( world )
2017-05-20 12:07:40 +00:00
logger . info ( ' Patching ROM. ' )
2020-06-10 19:25:14 +00:00
outfilebase = ' BM_ %s ' % ( args . outputname if args . outputname else world . seed )
2017-05-26 16:39:32 +00:00
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-08-19 21:24:17 +00:00
or world . killable_thieves [ player ] or world . tile_shuffle [ 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 :
2020-10-04 17:57:30 +00:00
patch_enemizer ( world , 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 ]
2020-03-06 22:08:46 +00:00
apply_rom_settings ( rom , args . heartbeep [ player ] , args . heartcolor [ player ] , args . quickswap [ player ] ,
args . fastmenu [ player ] , args . disablemusic [ player ] , args . sprite [ player ] ,
2020-10-24 00:44:27 +00:00
palettes_options , world , player , True )
2017-06-04 11:09:47 +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 %s shuffle ' % (
' 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
outfilepname = f ' _T { team + 1 } ' if world . teams > 1 else ' '
2020-08-02 20:11:52 +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
" difficulty_adjustments " : world . difficulty_adjustments [ player ] , # 2
" 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
}
# 0 1 2 3 4 5 6 7 8 9 A B
outfilesuffix = ( ' _ %s _ %s - %s - %s - %s %s _ %s - %s %s %s %s %s ' % (
# 0 1 2 3 4 5 6 7 8 9 A B C
# _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints
# _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
outfilestuffs [ " difficulty_adjustments " ] , # 2
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 :
Patch . create_patch_file ( rompath )
2020-07-14 02:48:56 +00:00
return player , team , bytes ( rom . name ) . decode ( )
2020-03-06 22:08:46 +00:00
2020-08-21 16:35:48 +00:00
pool = concurrent . futures . ThreadPoolExecutor ( )
2020-08-23 10:06:00 +00:00
multidata_task = None
2020-03-06 22:08:46 +00:00
if not args . suppress_rom :
2020-08-21 16:35:48 +00:00
rom_futures = [ ]
for team in range ( world . teams ) :
for player in range ( 1 , world . players + 1 ) :
rom_futures . append ( pool . submit ( _gen_rom , team , player ) )
2020-05-18 03: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 " }
from Regions import RegionType
for region in world . regions :
if region . player in er_hint_data and region . locations :
main_entrance = get_entrance_to_region ( region )
for location in region . locations :
if type ( location . address ) == int : # skips events and crystals
2020-12-15 08:34:22 +00:00
if location . address > = 0x400000 : continue
2020-06-20 10:22:50 +00:00
if lookup_vanilla_location_to_entrance [ location . address ] != main_entrance . name :
er_hint_data [ region . player ] [ location . address ] = main_entrance . name
2020-05-18 03:40:36 +00:00
2020-10-29 22:18:21 +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 " )
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 ] :
2020-10-29 22:32:05 +00:00
main_entrance = get_entrance_to_region ( location . parent_region )
2020-10-29 22:18:21 +00:00
if location . parent_region . dungeon :
2020-10-31 06:41:56 +00:00
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 )
2020-10-29 22:18:21 +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
2020-06-23 21:50:37 +00:00
precollected_items = [ [ ] for player in range ( world . players ) ]
for item in world . precollected_items :
precollected_items [ item . player - 1 ] . append ( item . code )
2020-08-21 16:35:48 +00:00
def write_multidata ( roms ) :
for future in roms :
rom_name = future . result ( )
rom_names . append ( rom_name )
2020-10-21 09:02:13 +00:00
multidatatags = [ " ER " ]
if args . race :
multidatatags . append ( " Race " )
if args . create_spoiler :
multidatatags . append ( " Spoiler " )
if not args . skip_playthrough :
multidatatags . append ( " Play through " )
2020-08-21 16:35:48 +00:00
multidata = zlib . compress ( json . dumps ( { " names " : parsed_names ,
# backwards compat for < 2.4.1
" roms " : [ ( slot , team , list ( name . encode ( ) ) )
for ( slot , team , name ) in rom_names ] ,
" rom_strings " : rom_names ,
" remote_items " : [ player for player in range ( 1 , world . players + 1 ) if
world . remote_items [ player ] ] ,
" locations " : [ ( ( location . address , location . player ) ,
( location . item . code , location . item . player ) )
for location in world . get_filled_locations ( ) if
type ( location . address ) is int ] ,
2020-10-29 22:18:21 +00:00
" checks_in_area " : checks_in_area ,
2020-08-21 16:35:48 +00:00
" server_options " : get_options ( ) [ " server_options " ] ,
" er_hint_data " : er_hint_data ,
" precollected_items " : precollected_items ,
" version " : _version_tuple ,
2020-10-21 09:02:13 +00:00
" tags " : multidatatags
2020-08-21 16:35:48 +00:00
} ) . encode ( " utf-8 " ) , 9 )
with open ( output_path ( ' %s .multidata ' % outfilebase ) , ' wb ' ) as f :
f . write ( multidata )
2020-08-23 10:06:00 +00:00
multidata_task = pool . submit ( write_multidata , rom_futures )
2017-05-20 12:07:40 +00:00
2019-04-18 09:23:24 +00:00
if not args . skip_playthrough :
logger . info ( ' Calculating playthrough. ' )
create_playthrough ( world )
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
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
2020-05-09 00:00:41 +00:00
def copy_world ( world ) :
# ToDo: Not good yet
ret = World ( world . players , world . shuffle , world . logic , world . mode , world . swords , world . difficulty , world . difficulty_adjustments , world . timer , world . progressive , world . goal , world . algorithm , world . accessibility , world . shuffle_ganon , world . retro , world . custom , world . customitemarray , world . hints )
ret . teams = world . teams
ret . player_names = copy . deepcopy ( world . player_names )
ret . remote_items = world . remote_items . copy ( )
ret . required_medallions = world . required_medallions . copy ( )
ret . swamp_patch_required = world . swamp_patch_required . copy ( )
ret . ganon_at_pyramid = world . ganon_at_pyramid . copy ( )
ret . powder_patch_required = world . powder_patch_required . copy ( )
ret . ganonstower_vanilla = world . ganonstower_vanilla . copy ( )
ret . treasure_hunt_count = world . treasure_hunt_count . copy ( )
ret . treasure_hunt_icon = world . treasure_hunt_icon . copy ( )
ret . sewer_light_cone = world . sewer_light_cone . copy ( )
ret . light_world_light_cone = world . light_world_light_cone
ret . dark_world_light_cone = world . dark_world_light_cone
ret . seed = world . seed
ret . can_access_trock_eyebridge = world . can_access_trock_eyebridge . copy ( )
ret . can_access_trock_front = world . can_access_trock_front . copy ( )
ret . can_access_trock_big_chest = world . can_access_trock_big_chest . copy ( )
ret . can_access_trock_middle = world . can_access_trock_middle . copy ( )
ret . can_take_damage = world . can_take_damage
ret . difficulty_requirements = world . difficulty_requirements . copy ( )
ret . fix_fake_world = world . fix_fake_world . copy ( )
ret . mapshuffle = world . mapshuffle . copy ( )
ret . compassshuffle = world . compassshuffle . copy ( )
ret . keyshuffle = world . keyshuffle . copy ( )
ret . bigkeyshuffle = world . bigkeyshuffle . copy ( )
ret . crystals_needed_for_ganon = world . crystals_needed_for_ganon . copy ( )
ret . crystals_needed_for_gt = world . crystals_needed_for_gt . copy ( )
ret . open_pyramid = world . open_pyramid . copy ( )
ret . boss_shuffle = world . boss_shuffle . copy ( )
ret . enemy_shuffle = world . enemy_shuffle . copy ( )
ret . enemy_health = world . enemy_health . copy ( )
ret . enemy_damage = world . enemy_damage . copy ( )
ret . beemizer = world . beemizer . copy ( )
ret . timer = world . timer . copy ( )
ret . shufflepots = world . shufflepots . copy ( )
2020-10-07 17:51:46 +00:00
ret . shuffle_prizes = world . shuffle_prizes . copy ( )
ret . dark_room_logic = world . dark_room_logic . copy ( )
ret . restrict_dungeon_item_on_boss = world . restrict_dungeon_item_on_boss . copy ( )
2020-05-10 04:56:52 +00:00
for player in range ( 1 , world . players + 1 ) :
if world . mode [ player ] != ' inverted ' :
create_regions ( ret , player )
else :
create_inverted_regions ( ret , player )
create_shops ( ret , player )
create_dungeons ( ret , player )
copy_dynamic_regions_and_locations ( world , ret )
# copy bosses
for dungeon in world . dungeons :
for level , boss in dungeon . bosses . items ( ) :
ret . get_dungeon ( dungeon . name , dungeon . player ) . bosses [ level ] = boss
for shop in world . shops :
copied_shop = ret . get_region ( shop . region . name , shop . region . player ) . shop
copied_shop . inventory = copy . copy ( shop . inventory )
# connect copied world
for region in world . regions :
copied_region = ret . get_region ( region . name , region . player )
copied_region . is_light_world = region . is_light_world
copied_region . is_dark_world = region . is_dark_world
for exit in copied_region . exits :
old_connection = world . get_entrance ( exit . name , exit . player ) . connected_region
exit . connect ( ret . get_region ( old_connection . name , old_connection . player ) )
# fill locations
for location in world . get_locations ( ) :
if location . item is not None :
item = Item ( location . item . name , location . item . advancement , location . item . priority , location . item . type , player = location . item . player )
ret . get_location ( location . name , location . player ) . item = item
item . location = ret . get_location ( location . name , location . player )
item . world = ret
if location . event :
ret . get_location ( location . name , location . player ) . event = True
if location . locked :
ret . get_location ( location . name , location . player ) . locked = True
# copy remaining itempool. No item in itempool should have an assigned location
for item in world . itempool :
ret . itempool . append ( Item ( item . name , item . advancement , item . priority , item . type , player = item . player ) )
for item in world . precollected_items :
ret . push_precollected ( ItemFactory ( item . name , item . player ) )
# copy progress items in state
2020-05-09 00:00:41 +00:00
ret . state . prog_items = world . state . prog_items . copy ( )
ret . state . stale = { player : True for player in range ( 1 , world . players + 1 ) }
2020-05-10 04:56:52 +00:00
for player in range ( 1 , world . players + 1 ) :
set_rules ( ret , player )
2020-05-09 00:00:41 +00:00
return ret
2020-05-10 04:56:52 +00:00
def copy_dynamic_regions_and_locations ( world , ret ) :
for region in world . dynamic_regions :
new_reg = Region ( region . name , region . type , region . hint_text , region . player )
ret . regions . append ( new_reg )
ret . initialize_regions ( [ new_reg ] )
ret . dynamic_regions . append ( new_reg )
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
if region . shop :
2020-08-25 12:31:20 +00:00
new_reg . shop = region . shop . __class__ ( new_reg , region . shop . room_id , region . shop . shopkeeper_config ,
region . shop . custom , region . shop . locked )
2020-05-10 04:56:52 +00:00
ret . shops . append ( new_reg . shop )
for location in world . dynamic_locations :
new_reg = ret . get_region ( location . parent_region . name , location . parent_region . player )
new_loc = Location ( location . player , location . name , location . address , location . crystal , location . hint_text , new_reg )
# todo: this is potentially dangerous. later refactor so we
# can apply dynamic region rules on top of copied world like other rules
new_loc . access_rule = location . access_rule
new_loc . always_allow = location . always_allow
new_loc . item_rule = location . item_rule
new_reg . locations . append ( new_loc )
2020-08-27 02:05:11 +00:00
ret . clear_location_cache ( )
2020-05-10 04:56:52 +00:00
2017-05-16 19:23:47 +00:00
def create_playthrough ( world ) :
# create a copy as we will modify it
2017-06-04 14:15:59 +00:00
old_world = world
2020-05-09 00:00:41 +00:00
world = copy_world ( world )
2017-05-26 07:53:34 +00:00
2017-06-23 20:15:29 +00:00
# if we only check for beatable, we can do this sanity check first before writing down spheres
2019-12-17 11:14:29 +00:00
if not world . can_beat_game ( ) :
2017-06-23 20:15:29 +00:00
raise RuntimeError ( ' Cannot beat game. Something went terribly wrong here! ' )
2017-05-16 19:23:47 +00:00
# get locations containing progress items
2018-01-03 01:01:16 +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 )
sphere_candidates = list ( 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
sphere = [ ]
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in sphere_candidates :
if state . can_reach ( location ) :
sphere . append ( location )
for location in sphere :
sphere_candidates . remove ( location )
2018-01-01 20:55:13 +00:00
state . collect ( location . item , True , location )
2017-06-17 12:40:37 +00:00
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 :
2019-12-21 12:33:07 +00:00
old_world . spoiler . unreachables = sphere_candidates . copy ( )
2017-06-23 20:15:29 +00:00
break
2017-05-26 07:55:24 +00:00
2017-05-16 19:23:47 +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
2018-01-01 20:55:13 +00:00
for num , sphere in reversed ( list ( enumerate ( collection_spheres ) ) ) :
2017-05-16 19:23:47 +00:00
to_delete = [ ]
for location in sphere :
# we remove the item at location and check if game is still beatable
2019-04-18 09:23:24 +00:00
logging . getLogger ( ' ' ) . 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 ] ) :
2017-05-16 19:23:47 +00:00
to_delete . append ( location )
else :
# still required, got to keep it around
location . item = old_item
# cull entries in spheres for spoiler walkthrough at end
for location in to_delete :
sphere . remove ( location )
2020-01-09 07:31:49 +00:00
# second phase, sphere 0
for item in [ i for i in world . precollected_items if i . advancement ] :
logging . getLogger ( ' ' ) . debug ( ' Checking if %s (Player %d ) is required to beat the game. ' , item . name , item . player )
world . precollected_items . remove ( item )
world . state . remove ( item )
if not world . can_beat_game ( ) :
world . push_precollected ( item )
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
required_locations = [ item for sphere in collection_spheres for item in sphere ]
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
sphere = list ( filter ( state . can_reach , required_locations ) )
for location in sphere :
required_locations . remove ( location )
state . collect ( location . item , True , location )
collection_spheres . append ( sphere )
logging . getLogger ( ' ' ) . debug ( ' Calculated final sphere %i , containing %i of %i progress items. ' , len ( collection_spheres ) , len ( sphere ) , len ( required_locations ) )
if not sphere :
raise RuntimeError ( ' Not all required items reachable. Something went terribly wrong here. ' )
2017-05-16 19:23:47 +00:00
2017-06-04 14:15:59 +00:00
# store the required locations for statistical analysis
2019-04-18 09:23:24 +00:00
old_world . required_locations = [ ( location . name , location . player ) for sphere in collection_spheres for location in sphere ]
2017-06-04 14:15:59 +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 )
2019-04-18 09:23:24 +00:00
old_world . spoiler . paths = dict ( )
for player in range ( 1 , world . players + 1 ) :
old_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 } )
for _ , path in dict ( old_world . spoiler . paths ) . items ( ) :
if any ( exit == ' Pyramid Fairy ' for ( _ , exit ) in path ) :
2019-12-16 15:54:46 +00:00
if world . mode [ player ] != ' inverted ' :
2019-07-27 13:13:13 +00:00
old_world . spoiler . paths [ str ( world . get_region ( ' Big Bomb Shop ' , player ) ) ] = get_path ( state , world . get_region ( ' Big Bomb Shop ' , player ) )
else :
old_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
2020-01-10 06:02:44 +00:00
old_world . spoiler . playthrough = OrderedDict ( [ ( " 0 " , [ str ( item ) for item in world . precollected_items if item . advancement ] ) ] )
2020-01-09 07:31:49 +00:00
for i , sphere in enumerate ( collection_spheres ) :
old_world . spoiler . playthrough [ str ( i + 1 ) ] = { str ( location ) : str ( location . item ) for location in sphere }