2022-07-03 12:11:52 +00:00
from __future__ import annotations
2019-12-16 01:05:33 +00:00
import argparse
import logging
import random
import urllib . request
import urllib . parse
2022-04-12 08:57:29 +00:00
from typing import Set , Dict , Tuple , Callable , Any , Union
2020-01-26 00:37:29 +00:00
import os
2022-08-09 00:29:00 +00:00
from collections import Counter , ChainMap
2021-03-28 23:52:32 +00:00
import string
2022-07-03 12:11:52 +00:00
import enum
2020-01-19 22:30:22 +00:00
import ModuleUpdate
ModuleUpdate . update ( )
2021-11-12 22:43:22 +00:00
import Utils
2021-07-22 23:57:45 +00:00
from worlds . alttp import Options as LttPOptions
2022-02-14 03:58:21 +00:00
from worlds . generic import PlandoConnection
2022-04-12 08:57:29 +00:00
from Utils import parse_yamls , version_tuple , __version__ , tuplize_version , get_options , local_path , user_path
2020-10-24 03:38:56 +00:00
from worlds . alttp . EntranceRandomizer import parse_arguments
2021-02-21 19:17:24 +00:00
from Main import main as ERmain
2021-10-06 09:32:49 +00:00
from BaseClasses import seeddigits , get_seed
2021-03-14 07:38:02 +00:00
import Options
2021-01-17 05:54:38 +00:00
from worlds . alttp . Text import TextTable
2021-06-15 12:11:46 +00:00
from worlds . AutoWorld import AutoWorldRegister
2022-01-06 22:04:55 +00:00
import copy
2019-12-16 01:05:33 +00:00
2022-07-03 12:11:52 +00:00
class PlandoSettings ( enum . IntFlag ) :
items = 0b0001
connections = 0b0010
texts = 0b0100
bosses = 0b1000
@classmethod
def from_option_string ( cls , option_string : str ) - > PlandoSettings :
result = cls ( 0 )
for part in option_string . split ( " , " ) :
part = part . strip ( ) . lower ( )
if part :
result = cls . _handle_part ( part , result )
return result
@classmethod
def from_set ( cls , option_set : Set [ str ] ) - > PlandoSettings :
result = cls ( 0 )
for part in option_set :
result = cls . _handle_part ( part , result )
return result
@classmethod
def _handle_part ( cls , part : str , base : PlandoSettings ) - > PlandoSettings :
try :
part = cls [ part ]
except Exception as e :
raise KeyError ( f " { part } is not a recognized name for a plando module. "
f " Known options: { ' , ' . join ( flag . name for flag in cls ) } " ) from e
else :
return base | part
2020-01-19 22:30:22 +00:00
2022-07-31 10:02:36 +00:00
def __str__ ( self ) - > str :
if self . value :
2022-08-30 15:12:33 +00:00
return " , " . join ( flag . name for flag in PlandoSettings if self . value & flag . value )
2022-07-31 10:02:36 +00:00
return " Off "
2022-03-31 03:08:15 +00:00
2020-06-29 01:59:16 +00:00
def mystery_argparse ( ) :
2021-07-21 16:08:15 +00:00
options = get_options ( )
defaults = options [ " generator " ]
2019-12-17 21:41:19 +00:00
2022-04-12 08:57:29 +00:00
def resolve_path ( path : str , resolver : Callable [ [ str ] , str ] ) - > str :
2022-03-31 03:08:15 +00:00
return path if os . path . isabs ( path ) else resolver ( path )
2021-07-21 16:08:15 +00:00
parser = argparse . ArgumentParser ( description = " CMD Generation Interface, defaults come from host.yaml. " )
2022-03-31 03:08:15 +00:00
parser . add_argument ( ' --weights_file_path ' , default = defaults [ " weights_file_path " ] ,
2020-01-19 22:30:22 +00:00
help = ' Path to the weights file to use for rolling game settings, urls are also valid ' )
parser . add_argument ( ' --samesettings ' , help = ' Rolls settings per weights file rather than per player ' ,
action = ' store_true ' )
2022-03-31 03:08:15 +00:00
parser . add_argument ( ' --player_files_path ' , default = resolve_path ( defaults [ " player_files_path " ] , user_path ) ,
2021-07-21 16:08:15 +00:00
help = " Input directory for player files. " )
2019-12-16 01:05:33 +00:00
parser . add_argument ( ' --seed ' , help = ' Define seed number to generate. ' , type = int )
2021-10-21 06:15:47 +00:00
parser . add_argument ( ' --multi ' , default = defaults [ " players " ] , type = lambda value : max ( int ( value ) , 1 ) )
2021-07-21 16:08:15 +00:00
parser . add_argument ( ' --spoiler ' , type = int , default = defaults [ " spoiler " ] )
2022-03-31 03:08:15 +00:00
parser . add_argument ( ' --outputpath ' , default = resolve_path ( options [ " general_options " ] [ " output_path " ] , user_path ) ,
help = " Path to output folder. Absolute or relative to cwd. " ) # absolute or relative to cwd
2021-07-21 16:08:15 +00:00
parser . add_argument ( ' --race ' , action = ' store_true ' , default = defaults [ " race " ] )
parser . add_argument ( ' --meta_file_path ' , default = defaults [ " meta_file_path " ] )
parser . add_argument ( ' --log_level ' , default = ' info ' , help = ' Sets log level ' )
2021-10-21 06:15:47 +00:00
parser . add_argument ( ' --yaml_output ' , default = 0 , type = lambda value : max ( int ( value ) , 0 ) ,
2020-04-25 00:25:46 +00:00
help = ' Output rolled mystery results to yaml up to specified number (made for async multiworld) ' )
2021-07-21 16:08:15 +00:00
parser . add_argument ( ' --plando ' , default = defaults [ " plando_options " ] ,
2021-01-02 11:49:43 +00:00
help = ' List of options that can be set manually. Can be combined, for example " bosses, items " ' )
2019-12-16 01:05:33 +00:00
args = parser . parse_args ( )
2021-07-21 16:08:15 +00:00
if not os . path . isabs ( args . weights_file_path ) :
args . weights_file_path = os . path . join ( args . player_files_path , args . weights_file_path )
if not os . path . isabs ( args . meta_file_path ) :
args . meta_file_path = os . path . join ( args . player_files_path , args . meta_file_path )
2022-07-03 12:11:52 +00:00
args . plando : PlandoSettings = PlandoSettings . from_option_string ( args . plando )
2021-07-21 16:08:15 +00:00
return args , options
2020-06-29 01:59:16 +00:00
2021-06-15 12:11:46 +00:00
2022-01-18 05:16:16 +00:00
def get_seed_name ( random_source ) - > str :
return f " { random_source . randint ( 0 , pow ( 10 , seeddigits ) - 1 ) } " . zfill ( seeddigits )
2020-06-29 01:59:16 +00:00
2021-06-15 12:11:46 +00:00
2020-09-13 15:15:49 +00:00
def main ( args = None , callback = ERmain ) :
2020-06-29 01:59:16 +00:00
if not args :
2021-07-21 16:08:15 +00:00
args , options = mystery_argparse ( )
2019-12-16 01:05:33 +00:00
2020-06-27 22:24:45 +00:00
seed = get_seed ( args . seed )
2019-12-17 21:41:19 +00:00
random . seed ( seed )
2021-07-21 16:08:15 +00:00
seed_name = get_seed_name ( random )
2019-12-16 01:05:33 +00:00
2020-10-21 09:02:13 +00:00
if args . race :
random . seed ( ) # reset to time-based random source
2022-04-12 08:57:29 +00:00
weights_cache : Dict [ str , Tuple [ Any , . . . ] ] = { }
2021-07-21 16:08:15 +00:00
if args . weights_file_path and os . path . exists ( args . weights_file_path ) :
2020-02-18 08:14:31 +00:00
try :
2022-04-12 08:57:29 +00:00
weights_cache [ args . weights_file_path ] = read_weights_yamls ( args . weights_file_path )
2020-02-18 08:14:31 +00:00
except Exception as e :
2021-07-21 16:08:15 +00:00
raise ValueError ( f " File { args . weights_file_path } is destroyed. Please fix your yaml. " ) from e
print ( f " Weights: { args . weights_file_path } >> "
2022-04-12 08:57:29 +00:00
f " { get_choice ( ' description ' , weights_cache [ args . weights_file_path ] [ - 1 ] , ' No description specified ' ) } " )
2021-07-21 16:08:15 +00:00
if args . meta_file_path and os . path . exists ( args . meta_file_path ) :
2020-02-18 08:14:31 +00:00
try :
2022-08-09 00:29:00 +00:00
meta_weights = read_weights_yamls ( args . meta_file_path ) [ - 1 ]
2020-02-18 08:14:31 +00:00
except Exception as e :
2021-07-21 16:08:15 +00:00
raise ValueError ( f " File { args . meta_file_path } is destroyed. Please fix your yaml. " ) from e
2021-11-21 17:09:06 +00:00
print ( f " Meta: { args . meta_file_path } >> { get_choice ( ' meta_description ' , meta_weights ) } " )
2022-08-09 00:29:00 +00:00
try : # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
del ( meta_weights [ " meta_description " ] )
except Exception as e :
raise ValueError ( " No meta description found for meta.yaml. Unable to verify. " ) from e
2020-02-18 08:14:31 +00:00
if args . samesettings :
raise Exception ( " Cannot mix --samesettings with --meta " )
2021-07-21 16:08:15 +00:00
else :
meta_weights = None
player_id = 1
player_files = { }
for file in os . scandir ( args . player_files_path ) :
fname = file . name
2022-06-15 01:10:41 +00:00
if file . is_file ( ) and not file . name . startswith ( " . " ) and \
os . path . join ( args . player_files_path , fname ) not in { args . meta_file_path , args . weights_file_path } :
2021-07-21 16:08:15 +00:00
path = os . path . join ( args . player_files_path , fname )
2020-03-05 01:31:26 +00:00
try :
2022-04-12 08:57:29 +00:00
weights_cache [ fname ] = read_weights_yamls ( path )
2020-03-05 01:31:26 +00:00
except Exception as e :
2021-07-21 16:08:15 +00:00
raise ValueError ( f " File { fname } is destroyed. Please fix your yaml. " ) from e
2022-06-05 20:49:08 +00:00
# sort dict for consistent results across platforms:
weights_cache = { key : value for key , value in sorted ( weights_cache . items ( ) ) }
for filename , yaml_data in weights_cache . items ( ) :
for yaml in yaml_data :
print ( f " P { player_id } Weights: { filename } >> "
f " { get_choice ( ' description ' , yaml , ' No description specified ' ) } " )
player_files [ player_id ] = filename
player_id + = 1
2021-07-21 16:08:15 +00:00
2022-08-09 00:29:00 +00:00
args . multi = max ( player_id - 1 , args . multi )
2021-09-10 02:28:06 +00:00
print ( f " Generating for { args . multi } player { ' s ' if args . multi > 1 else ' ' } , { seed_name } Seed { seed } with plando: "
2022-07-03 12:11:52 +00:00
f " { args . plando } " )
2021-07-21 16:08:15 +00:00
if not weights_cache :
raise Exception ( f " No weights found. Provide a general weights file ( { args . weights_file_path } ) or individual player files. "
f " A mix is also permitted. " )
2019-12-17 21:41:19 +00:00
erargs = parse_arguments ( [ ' --multi ' , str ( args . multi ) ] )
2019-12-16 01:05:33 +00:00
erargs . seed = seed
2021-07-21 16:08:15 +00:00
erargs . glitch_triforce = options [ " generator " ] [ " glitch_triforce_room " ]
2021-09-12 23:32:32 +00:00
erargs . spoiler = args . spoiler
2020-01-13 18:47:30 +00:00
erargs . race = args . race
2021-04-12 07:45:07 +00:00
erargs . outputname = seed_name
2019-12-16 01:05:33 +00:00
erargs . outputpath = args . outputpath
2021-11-18 17:54:17 +00:00
Utils . init_logging ( f " Generate_ { seed } " , loglevel = args . log_level )
2020-02-23 16:06:44 +00:00
2022-04-12 08:57:29 +00:00
settings_cache : Dict [ str , Tuple [ argparse . Namespace , . . . ] ] = \
2022-08-09 00:29:00 +00:00
{ fname : ( tuple ( roll_settings ( yaml , args . plando ) for yaml in yamls ) if args . samesettings else None )
for fname , yamls in weights_cache . items ( ) }
2020-02-18 08:14:31 +00:00
2021-07-21 16:08:15 +00:00
if meta_weights :
2021-11-21 17:09:06 +00:00
for category_name , category_dict in meta_weights . items ( ) :
for key in category_dict :
2022-08-09 00:29:00 +00:00
option = roll_meta_option ( key , category_name , category_dict )
2021-11-21 17:09:06 +00:00
if option is not None :
2022-08-09 00:29:00 +00:00
for path in weights_cache :
2022-04-12 08:57:29 +00:00
for yaml in weights_cache [ path ] :
if category_name is None :
2022-08-09 00:29:00 +00:00
for category in yaml :
if category in AutoWorldRegister . world_types and key in Options . common_options :
yaml [ category ] [ key ] = option
2022-04-12 08:57:29 +00:00
elif category_name not in yaml :
logging . warning ( f " Meta: Category { category_name } is not present in { path } . " )
else :
2022-08-09 00:29:00 +00:00
yaml [ category_name ] [ key ] = option
2019-12-17 21:41:19 +00:00
2022-08-09 00:29:00 +00:00
player_path_cache = { }
for player in range ( 1 , args . multi + 1 ) :
player_path_cache [ player ] = player_files . get ( player , args . weights_file_path )
2021-03-13 01:43:50 +00:00
name_counter = Counter ( )
2021-03-14 07:38:02 +00:00
erargs . player_settings = { }
2022-04-12 08:57:29 +00:00
player = 1
while player < = args . multi :
2020-02-18 08:14:31 +00:00
path = player_path_cache [ player ]
2019-12-17 21:41:19 +00:00
if path :
2020-01-12 16:03:30 +00:00
try :
2022-04-12 08:57:29 +00:00
settings : Tuple [ argparse . Namespace , . . . ] = settings_cache [ path ] if settings_cache [ path ] else \
tuple ( roll_settings ( yaml , args . plando ) for yaml in weights_cache [ path ] )
for settingsObject in settings :
for k , v in vars ( settingsObject ) . items ( ) :
if v is not None :
try :
getattr ( erargs , k ) [ player ] = v
except AttributeError :
setattr ( erargs , k , { player : v } )
except Exception as e :
raise Exception ( f " Error setting { k } to { v } for player { player } " ) from e
if path == args . weights_file_path : # if name came from the weights file, just use base player name
erargs . name [ player ] = f " Player { player } "
elif not erargs . name [ player ] : # if name was not specified, generate it from filename
erargs . name [ player ] = os . path . splitext ( os . path . split ( path ) [ - 1 ] ) [ 0 ]
erargs . name [ player ] = handle_name ( erargs . name [ player ] , player , name_counter )
player + = 1
2020-02-18 08:14:31 +00:00
except Exception as e :
raise ValueError ( f " File { path } is destroyed. Please fix your yaml. " ) from e
2019-12-17 21:41:19 +00:00
else :
raise RuntimeError ( f ' No weights specified for player { player } ' )
2021-03-17 05:26:06 +00:00
2021-08-26 15:22:55 +00:00
if len ( set ( erargs . name . values ( ) ) ) != len ( erargs . name ) :
2022-02-14 03:58:21 +00:00
raise Exception ( f " Names have to be unique. Names: { Counter ( erargs . name . values ( ) ) } " )
2021-08-26 15:22:55 +00:00
2020-04-25 00:24:37 +00:00
if args . yaml_output :
import yaml
important = { }
for option , player_settings in vars ( erargs ) . items ( ) :
if type ( player_settings ) == dict :
2020-10-07 18:19:31 +00:00
if all ( type ( value ) != list for value in player_settings . values ( ) ) :
2021-02-05 07:07:12 +00:00
if len ( player_settings . values ( ) ) > 1 :
2020-10-07 18:19:31 +00:00
important [ option ] = { player : value for player , value in player_settings . items ( ) if
player < = args . yaml_output }
else :
logging . debug ( f " No player settings defined for option ' { option } ' " )
2020-04-25 00:24:37 +00:00
else :
if player_settings != " " : # is not empty name
important [ option ] = player_settings
2020-04-26 00:57:20 +00:00
else :
logging . debug ( f " No player settings defined for option ' { option } ' " )
if args . outputpath :
os . makedirs ( args . outputpath , exist_ok = True )
2021-07-23 23:42:00 +00:00
with open ( os . path . join ( args . outputpath if args . outputpath else " . " , f " generate_ { seed_name } .yaml " ) , " wt " ) as f :
2020-04-25 00:24:37 +00:00
yaml . dump ( important , f )
2020-02-23 16:06:44 +00:00
2020-07-25 20:40:24 +00:00
callback ( erargs , seed )
2019-12-17 21:41:19 +00:00
2020-02-23 16:06:44 +00:00
2022-04-12 08:57:29 +00:00
def read_weights_yamls ( path ) - > Tuple [ Any , . . . ] :
2019-12-17 21:41:19 +00:00
try :
2022-03-31 06:22:01 +00:00
if urllib . parse . urlparse ( path ) . scheme in ( ' https ' , ' file ' ) :
2022-04-03 19:55:46 +00:00
yaml = str ( urllib . request . urlopen ( path ) . read ( ) , " utf-8-sig " )
2020-04-10 04:41:32 +00:00
else :
2019-12-17 21:41:19 +00:00
with open ( path , ' rb ' ) as f :
2022-04-03 19:55:46 +00:00
yaml = str ( f . read ( ) , " utf-8-sig " )
2019-12-17 21:41:19 +00:00
except Exception as e :
2020-09-18 04:03:04 +00:00
raise Exception ( f " Failed to read weights ( { path } ) " ) from e
2019-12-17 21:41:19 +00:00
2022-04-12 08:57:29 +00:00
return tuple ( parse_yamls ( yaml ) )
2019-12-17 21:41:19 +00:00
2020-01-19 22:30:22 +00:00
2022-01-18 05:16:16 +00:00
def interpret_on_off ( value ) - > bool :
2020-01-19 22:30:22 +00:00
return { " on " : True , " off " : False } . get ( value , value )
2020-02-23 16:06:44 +00:00
2022-01-18 05:16:16 +00:00
def convert_to_on_off ( value ) - > str :
2020-01-20 02:29:13 +00:00
return { True : " on " , False : " off " } . get ( value , value )
2020-01-19 22:30:22 +00:00
2020-02-23 16:06:44 +00:00
2022-04-12 08:57:29 +00:00
def get_choice_legacy ( option , root , value = None ) - > Any :
2020-02-18 08:14:31 +00:00
if option not in root :
2020-06-18 15:51:38 +00:00
return value
2020-11-06 01:03:10 +00:00
if type ( root [ option ] ) is list :
return interpret_on_off ( random . choices ( root [ option ] ) [ 0 ] )
2020-02-18 08:14:31 +00:00
if type ( root [ option ] ) is not dict :
return interpret_on_off ( root [ option ] )
if not root [ option ] :
2020-06-18 15:51:38 +00:00
return value
2020-06-19 02:21:52 +00:00
if any ( root [ option ] . values ( ) ) :
2020-06-18 15:48:33 +00:00
return interpret_on_off (
random . choices ( list ( root [ option ] . keys ( ) ) , weights = list ( map ( int , root [ option ] . values ( ) ) ) ) [ 0 ] )
2020-12-31 12:23:32 +00:00
raise RuntimeError ( f " All options specified in \" { option } \" are weighted as zero. " )
2019-12-17 21:41:19 +00:00
2020-02-23 16:06:44 +00:00
2022-04-12 08:57:29 +00:00
def get_choice ( option , root , value = None ) - > Any :
2021-08-29 18:21:49 +00:00
if option not in root :
return value
if type ( root [ option ] ) is list :
return random . choices ( root [ option ] ) [ 0 ]
if type ( root [ option ] ) is not dict :
return root [ option ]
if not root [ option ] :
return value
if any ( root [ option ] . values ( ) ) :
return random . choices ( list ( root [ option ] . keys ( ) ) , weights = list ( map ( int , root [ option ] . values ( ) ) ) ) [ 0 ]
raise RuntimeError ( f " All options specified in \" { option } \" are weighted as zero. " )
2021-03-28 23:52:32 +00:00
class SafeDict ( dict ) :
def __missing__ ( self , key ) :
return ' { ' + key + ' } '
def handle_name ( name : str , player : int , name_counter : Counter ) :
name_counter [ name ] + = 1
new_name = " % " . join ( [ x . replace ( " % number % " , " {number} " ) . replace ( " % player % " , " {player} " ) for x in name . split ( " %% " ) ] )
new_name = string . Formatter ( ) . vformat ( new_name , ( ) , SafeDict ( number = name_counter [ name ] ,
2021-04-03 12:47:49 +00:00
NUMBER = ( name_counter [ name ] if name_counter [
name ] > 1 else ' ' ) ,
2021-03-28 23:52:32 +00:00
player = player ,
PLAYER = ( player if player > 1 else ' ' ) ) )
2021-08-09 07:15:41 +00:00
new_name = new_name . strip ( ) [ : 16 ]
2021-05-11 21:08:50 +00:00
if new_name == " Archipelago " :
raise Exception ( f " You cannot name yourself \" { new_name } \" " )
return new_name
2020-02-23 16:06:44 +00:00
2022-04-12 08:57:29 +00:00
def prefer_int ( input_data : str ) - > Union [ str , int ] :
2020-10-18 05:12:09 +00:00
try :
return int ( input_data )
except :
return input_data
2020-12-31 12:23:32 +00:00
2021-04-03 12:47:49 +00:00
goals = {
' ganon ' : ' ganon ' ,
' crystals ' : ' crystals ' ,
' bosses ' : ' bosses ' ,
' pedestal ' : ' pedestal ' ,
' ganon_pedestal ' : ' ganonpedestal ' ,
' triforce_hunt ' : ' triforcehunt ' ,
' local_triforce_hunt ' : ' localtriforcehunt ' ,
' ganon_triforce_hunt ' : ' ganontriforcehunt ' ,
' local_ganon_triforce_hunt ' : ' localganontriforcehunt ' ,
' ice_rod_hunt ' : ' icerodhunt ' ,
}
2021-04-03 13:06:32 +00:00
2022-04-12 08:57:29 +00:00
def roll_percentage ( percentage : Union [ int , float ] ) - > bool :
2021-01-02 11:49:43 +00:00
""" Roll a percentage chance.
percentage is expected to be in range [ 0 , 100 ] """
return random . random ( ) < ( float ( percentage ) / 100 )
2021-04-03 12:47:49 +00:00
2021-02-13 18:26:02 +00:00
def update_weights ( weights : dict , new_weights : dict , type : str , name : str ) - > dict :
logging . debug ( f ' Applying { new_weights } ' )
new_options = set ( new_weights ) - set ( weights )
weights . update ( new_weights )
if new_options :
for new_option in new_options :
logging . warning ( f ' { type } Suboption " { new_option } " of " { name } " did not '
f ' overwrite a root option. '
f ' This is probably in error. ' )
return weights
2021-04-03 12:47:49 +00:00
2022-08-09 00:29:00 +00:00
def roll_meta_option ( option_key , game : str , category_dict : Dict ) - > Any :
if not game :
return get_choice ( option_key , category_dict )
if game in AutoWorldRegister . world_types :
game_world = AutoWorldRegister . world_types [ game ]
2022-08-15 21:46:59 +00:00
options = ChainMap ( game_world . option_definitions , Options . per_game_common_options )
2022-08-09 00:29:00 +00:00
if option_key in options :
if options [ option_key ] . supports_weighting :
return get_choice ( option_key , category_dict )
return options [ option_key ]
if game == " A Link to the Past " : # TODO wow i hate this
if option_key in { " glitches_required " , " dark_room_logic " , " entrance_shuffle " , " goals " , " triforce_pieces_mode " ,
" triforce_pieces_percentage " , " triforce_pieces_available " , " triforce_pieces_extra " ,
" triforce_pieces_required " , " shop_shuffle " , " mode " , " item_pool " , " item_functionality " ,
" boss_shuffle " , " enemy_damage " , " enemy_health " , " timer " , " countdown_start_time " ,
" red_clock_time " , " blue_clock_time " , " green_clock_time " , " dungeon_counters " , " shuffle_prizes " ,
" misery_mire_medallion " , " turtle_rock_medallion " , " sprite_pool " , " sprite " ,
" random_sprite_on_event " } :
return get_choice ( option_key , category_dict )
raise Exception ( f " Error generating meta option { option_key } for { game } . " )
2021-02-13 13:03:23 +00:00
def roll_linked_options ( weights : dict ) - > dict :
2022-01-06 22:04:55 +00:00
weights = copy . deepcopy ( weights ) # make sure we don't write back to other weights sets in same_settings
2021-02-13 13:03:23 +00:00
for option_set in weights [ " linked_options " ] :
if " name " not in option_set :
raise ValueError ( " One of your linked options does not have a name. " )
try :
if roll_percentage ( option_set [ " percentage " ] ) :
logging . debug ( f " Linked option { option_set [ ' name ' ] } triggered. " )
2021-06-15 12:11:46 +00:00
new_options = option_set [ " options " ]
for category_name , category_options in new_options . items ( ) :
currently_targeted_weights = weights
if category_name :
currently_targeted_weights = currently_targeted_weights [ category_name ]
update_weights ( currently_targeted_weights , category_options , " Linked " , option_set [ " name " ] )
2021-02-13 13:03:23 +00:00
else :
logging . debug ( f " linked option { option_set [ ' name ' ] } skipped. " )
except Exception as e :
raise ValueError ( f " Linked option { option_set [ ' name ' ] } is destroyed. "
f " Please fix your linked option. " ) from e
return weights
2021-01-02 11:49:43 +00:00
2021-04-03 12:47:49 +00:00
2021-10-17 18:53:06 +00:00
def roll_triggers ( weights : dict , triggers : list ) - > dict :
2022-01-06 22:04:55 +00:00
weights = copy . deepcopy ( weights ) # make sure we don't write back to other weights sets in same_settings
2021-11-28 01:57:15 +00:00
weights [ " _Generator_Version " ] = Utils . __version__
2021-10-17 18:53:06 +00:00
for i , option_set in enumerate ( triggers ) :
2021-02-13 13:03:23 +00:00
try :
2021-06-15 12:11:46 +00:00
currently_targeted_weights = weights
category = option_set . get ( " option_category " , None )
if category :
currently_targeted_weights = currently_targeted_weights [ category ]
2021-02-13 13:03:23 +00:00
key = get_choice ( " option_name " , option_set )
2021-06-15 12:11:46 +00:00
if key not in currently_targeted_weights :
2021-02-13 21:57:52 +00:00
logging . warning ( f ' Specified option name { option_set [ " option_name " ] } did not '
f ' match with a root option. '
f ' This is probably in error. ' )
2021-02-13 13:03:23 +00:00
trigger_result = get_choice ( " option_result " , option_set )
2021-06-15 12:11:46 +00:00
result = get_choice ( key , currently_targeted_weights )
currently_targeted_weights [ key ] = result
2021-02-13 13:03:23 +00:00
if result == trigger_result and roll_percentage ( get_choice ( " percentage " , option_set , 100 ) ) :
2021-06-15 12:11:46 +00:00
for category_name , category_options in option_set [ " options " ] . items ( ) :
currently_targeted_weights = weights
if category_name :
currently_targeted_weights = currently_targeted_weights [ category_name ]
update_weights ( currently_targeted_weights , category_options , " Triggered " , option_set [ " option_name " ] )
2021-06-14 00:14:02 +00:00
2021-02-13 13:03:23 +00:00
except Exception as e :
2021-06-15 12:11:46 +00:00
raise ValueError ( f " Your trigger number { i + 1 } is destroyed. "
2021-02-13 13:03:23 +00:00
f " Please fix your triggers. " ) from e
return weights
2021-03-07 11:38:49 +00:00
2022-09-17 00:55:33 +00:00
def handle_option ( ret : argparse . Namespace , game_weights : dict , option_key : str , option : type ( Options . Option ) , plando_options : PlandoSettings ) :
2021-09-16 22:17:54 +00:00
if option_key in game_weights :
try :
if not option . supports_weighting :
player_option = option . from_any ( game_weights [ option_key ] )
else :
player_option = option . from_any ( get_choice ( option_key , game_weights ) )
setattr ( ret , option_key , player_option )
except Exception as e :
raise Exception ( f " Error generating option { option_key } in { ret . game } " ) from e
else :
2022-09-17 00:55:33 +00:00
player_option . verify ( AutoWorldRegister . world_types [ ret . game ] , ret . name , plando_options )
2021-09-16 22:17:54 +00:00
else :
2022-09-19 20:40:15 +00:00
setattr ( ret , option_key , option . from_any ( option . default ) ) # call the from_any here to support default "random"
2021-09-16 22:17:54 +00:00
2022-07-03 12:11:52 +00:00
def roll_settings ( weights : dict , plando_options : PlandoSettings = PlandoSettings . bosses ) :
2020-04-25 00:24:37 +00:00
if " linked_options " in weights :
2021-02-13 13:03:23 +00:00
weights = roll_linked_options ( weights )
if " triggers " in weights :
2021-10-17 18:53:06 +00:00
weights = roll_triggers ( weights , weights [ " triggers " ] )
2020-04-25 00:24:37 +00:00
2021-06-18 21:17:12 +00:00
requirements = weights . get ( " requires " , { } )
if requirements :
version = requirements . get ( " version " , __version__ )
if tuplize_version ( version ) > version_tuple :
raise Exception ( f " Settings reports required version of generator is at least { version } , "
f " however generator is of version { __version__ } " )
2022-07-03 12:11:52 +00:00
required_plando_options = PlandoSettings . from_option_string ( requirements . get ( " plando " , " " ) )
if required_plando_options not in plando_options :
2021-06-18 23:00:41 +00:00
if required_plando_options :
2022-07-03 12:11:52 +00:00
raise Exception ( f " Settings reports required plando module { str ( required_plando_options ) } , "
f " which is not enabled. " )
2021-06-18 21:17:12 +00:00
2021-02-13 13:03:23 +00:00
ret = argparse . Namespace ( )
2021-09-16 22:17:54 +00:00
for option_key in Options . per_game_common_options :
2021-12-28 17:43:52 +00:00
if option_key in weights and option_key not in Options . common_options :
2021-09-16 22:17:54 +00:00
raise Exception ( f " Option { option_key } has to be in a game ' s section, not on its own. " )
2021-06-15 13:10:31 +00:00
ret . game = get_choice ( " game " , weights )
if ret . game not in weights :
raise Exception ( f " No game options for selected game \" { ret . game } \" found. " )
2021-10-17 18:53:06 +00:00
2021-07-12 11:54:47 +00:00
world_type = AutoWorldRegister . world_types [ ret . game ]
2021-06-15 13:10:31 +00:00
game_weights = weights [ ret . game ]
2021-07-14 13:24:34 +00:00
2021-10-17 18:53:06 +00:00
if " triggers " in game_weights :
weights = roll_triggers ( weights , game_weights [ " triggers " ] )
game_weights = weights [ ret . game ]
ret . name = get_choice ( ' name ' , weights )
for option_key , option in Options . common_options . items ( ) :
setattr ( ret , option_key , option . from_any ( get_choice ( option_key , weights , option . default ) ) )
2021-07-04 14:18:21 +00:00
if ret . game in AutoWorldRegister . world_types :
2022-08-15 21:46:59 +00:00
for option_key , option in world_type . option_definitions . items ( ) :
2022-09-17 00:55:33 +00:00
handle_option ( ret , game_weights , option_key , option , plando_options )
2021-09-16 22:17:54 +00:00
for option_key , option in Options . per_game_common_options . items ( ) :
2022-01-04 19:04:02 +00:00
# skip setting this option if already set from common_options, defaulting to root option
if not ( option_key in Options . common_options and option_key not in game_weights ) :
2022-09-17 00:55:33 +00:00
handle_option ( ret , game_weights , option_key , option , plando_options )
2022-07-03 12:11:52 +00:00
if PlandoSettings . items in plando_options :
2022-01-20 18:34:17 +00:00
ret . plando_items = game_weights . get ( " plando_items " , [ ] )
2021-11-20 21:36:57 +00:00
if ret . game == " Minecraft " or ret . game == " Ocarina of Time " :
2021-06-25 21:32:13 +00:00
# bad hardcoded behavior to make this work for now
ret . plando_connections = [ ]
2022-07-03 12:11:52 +00:00
if PlandoSettings . connections in plando_options :
2021-06-25 21:32:13 +00:00
options = game_weights . get ( " plando_connections " , [ ] )
for placement in options :
if roll_percentage ( get_choice ( " percentage " , placement , 100 ) ) :
ret . plando_connections . append ( PlandoConnection (
get_choice ( " entrance " , placement ) ,
get_choice ( " exit " , placement ) ,
2021-11-20 21:36:57 +00:00
get_choice ( " direction " , placement )
2021-06-25 21:32:13 +00:00
) )
2021-07-04 14:18:21 +00:00
elif ret . game == " A Link to the Past " :
roll_alttp_settings ( ret , game_weights , plando_options )
2021-03-14 07:38:02 +00:00
else :
raise Exception ( f " Unsupported game { ret . game } " )
2022-02-22 09:14:26 +00:00
2021-03-14 07:38:02 +00:00
return ret
2022-02-06 15:37:21 +00:00
2021-03-14 07:38:02 +00:00
def roll_alttp_settings ( ret : argparse . Namespace , weights , plando_options ) :
2021-09-01 16:18:43 +00:00
if " dungeon_items " in weights and get_choice_legacy ( ' dungeon_items ' , weights , " none " ) != " none " :
2021-09-01 15:56:35 +00:00
raise Exception ( f " dungeon_items key in A Link to the Past was removed, but is present in these weights as { get_choice_legacy ( ' dungeon_items ' , weights , False ) } . " )
2021-08-29 18:21:49 +00:00
glitches_required = get_choice_legacy ( ' glitches_required ' , weights )
2021-06-08 00:34:00 +00:00
if glitches_required not in [ None , ' none ' , ' no_logic ' , ' overworld_glitches ' , ' hybrid_major_glitches ' , ' minor_glitches ' ] :
logging . warning ( " Only NMG, OWG, HMG and No Logic supported " )
2019-12-17 21:41:19 +00:00
glitches_required = ' none '
2020-06-10 17:02:11 +00:00
ret . logic = { None : ' noglitches ' , ' none ' : ' noglitches ' , ' no_logic ' : ' nologic ' , ' overworld_glitches ' : ' owglitches ' ,
2021-06-07 06:19:27 +00:00
' minor_glitches ' : ' minorglitches ' , ' hybrid_major_glitches ' : ' hybridglitches ' } [
2020-04-25 00:24:37 +00:00
glitches_required ]
2020-10-07 17:51:46 +00:00
2021-08-29 18:21:49 +00:00
ret . dark_room_logic = get_choice_legacy ( " dark_room_logic " , weights , " lamp " )
2020-10-07 21:19:16 +00:00
if not ret . dark_room_logic : # None/False
2020-10-07 17:51:46 +00:00
ret . dark_room_logic = " none "
if ret . dark_room_logic == " sconces " :
ret . dark_room_logic = " torches "
if ret . dark_room_logic not in { " lamp " , " torches " , " none " } :
raise ValueError ( f " Unknown Dark Room Logic: \" { ret . dark_room_logic } \" " )
2021-08-29 18:21:49 +00:00
entrance_shuffle = get_choice_legacy ( ' entrance_shuffle ' , weights , ' vanilla ' )
2021-02-24 21:23:42 +00:00
if entrance_shuffle . startswith ( ' none- ' ) :
ret . shuffle = ' vanilla '
else :
ret . shuffle = entrance_shuffle if entrance_shuffle != ' none ' else ' vanilla '
2019-12-16 01:05:33 +00:00
2021-08-29 18:21:49 +00:00
goal = get_choice_legacy ( ' goals ' , weights , ' ganon ' )
2021-04-03 12:47:49 +00:00
ret . goal = goals [ goal ]
2020-09-11 01:23:00 +00:00
2019-12-16 01:05:33 +00:00
2021-08-29 18:21:49 +00:00
extra_pieces = get_choice_legacy ( ' triforce_pieces_mode ' , weights , ' available ' )
2020-06-17 08:02:54 +00:00
2021-08-29 18:21:49 +00:00
ret . triforce_pieces_required = LttPOptions . TriforcePieces . from_any ( get_choice_legacy ( ' triforce_pieces_required ' , weights , 20 ) )
2020-06-07 13:22:24 +00:00
2020-09-23 17:43:18 +00:00
# sum a percentage to required
if extra_pieces == ' percentage ' :
2021-08-29 18:21:49 +00:00
percentage = max ( 100 , float ( get_choice_legacy ( ' triforce_pieces_percentage ' , weights , 150 ) ) ) / 100
2020-10-18 05:12:09 +00:00
ret . triforce_pieces_available = int ( round ( ret . triforce_pieces_required * percentage , 0 ) )
2020-09-23 17:43:18 +00:00
# vanilla mode (specify how many pieces are)
elif extra_pieces == ' available ' :
2021-07-04 14:18:21 +00:00
ret . triforce_pieces_available = LttPOptions . TriforcePieces . from_any (
2021-08-29 18:21:49 +00:00
get_choice_legacy ( ' triforce_pieces_available ' , weights , 30 ) )
2020-09-23 17:43:18 +00:00
# required pieces + fixed extra
elif extra_pieces == ' extra ' :
2021-08-29 18:21:49 +00:00
extra_pieces = max ( 0 , int ( get_choice_legacy ( ' triforce_pieces_extra ' , weights , 10 ) ) )
2020-09-23 17:43:18 +00:00
ret . triforce_pieces_available = ret . triforce_pieces_required + extra_pieces
# change minimum to required pieces to avoid problems
2020-10-07 21:19:16 +00:00
ret . triforce_pieces_available = min ( max ( ret . triforce_pieces_required , int ( ret . triforce_pieces_available ) ) , 90 )
2020-11-24 02:05:04 +00:00
2021-08-29 18:21:49 +00:00
ret . shop_shuffle = get_choice_legacy ( ' shop_shuffle ' , weights , ' ' )
2020-08-23 13:03:06 +00:00
if not ret . shop_shuffle :
2020-08-23 19:38:21 +00:00
ret . shop_shuffle = ' '
2020-08-23 13:03:06 +00:00
2021-08-29 18:21:49 +00:00
ret . mode = get_choice_legacy ( " mode " , weights )
2019-12-16 01:05:33 +00:00
2021-08-29 18:21:49 +00:00
ret . difficulty = get_choice_legacy ( ' item_pool ' , weights )
2019-12-16 01:05:33 +00:00
2021-08-29 18:21:49 +00:00
ret . item_functionality = get_choice_legacy ( ' item_functionality ' , weights )
2019-12-16 01:05:33 +00:00
2020-09-20 05:25:58 +00:00
ret . enemy_damage = { None : ' default ' ,
' default ' : ' default ' ,
2019-12-17 21:41:19 +00:00
' shuffled ' : ' shuffled ' ,
2021-08-09 07:15:41 +00:00
' random ' : ' chaos ' , # to be removed
' chaos ' : ' chaos ' ,
2021-08-29 18:21:49 +00:00
} [ get_choice_legacy ( ' enemy_damage ' , weights ) ]
2019-12-16 01:05:33 +00:00
2021-08-29 18:21:49 +00:00
ret . enemy_health = get_choice_legacy ( ' enemy_health ' , weights )
2019-12-16 01:05:33 +00:00
2020-03-04 12:55:03 +00:00
ret . timer = { ' none ' : False ,
None : False ,
False : False ,
2020-02-03 01:10:56 +00:00
' timed ' : ' timed ' ,
' timed_ohko ' : ' timed-ohko ' ,
' ohko ' : ' ohko ' ,
' timed_countdown ' : ' timed-countdown ' ,
2021-08-29 18:21:49 +00:00
' display ' : ' display ' } [ get_choice_legacy ( ' timer ' , weights , False ) ]
2020-02-03 01:10:56 +00:00
2021-08-29 18:21:49 +00:00
ret . countdown_start_time = int ( get_choice_legacy ( ' countdown_start_time ' , weights , 10 ) )
ret . red_clock_time = int ( get_choice_legacy ( ' red_clock_time ' , weights , - 2 ) )
ret . blue_clock_time = int ( get_choice_legacy ( ' blue_clock_time ' , weights , 2 ) )
ret . green_clock_time = int ( get_choice_legacy ( ' green_clock_time ' , weights , 4 ) )
2020-10-28 23:20:59 +00:00
2021-08-29 18:21:49 +00:00
ret . dungeon_counters = get_choice_legacy ( ' dungeon_counters ' , weights , ' default ' )
2020-04-12 22:46:32 +00:00
2021-08-29 18:21:49 +00:00
ret . shuffle_prizes = get_choice_legacy ( ' shuffle_prizes ' , weights , " g " )
2020-09-20 02:35:45 +00:00
2021-08-29 18:21:49 +00:00
ret . required_medallions = [ get_choice_legacy ( " misery_mire_medallion " , weights , " random " ) ,
get_choice_legacy ( " turtle_rock_medallion " , weights , " random " ) ]
2021-01-25 02:14:58 +00:00
for index , medallion in enumerate ( ret . required_medallions ) :
2021-03-14 07:38:02 +00:00
ret . required_medallions [ index ] = { " ether " : " Ether " , " quake " : " Quake " , " bombos " : " Bombos " , " random " : " random " } \
2021-01-25 02:14:58 +00:00
. get ( medallion . lower ( ) , None )
if not ret . required_medallions [ index ] :
2021-03-07 11:31:36 +00:00
raise Exception ( f " unknown Medallion { medallion } for { ' misery mire ' if index == 0 else ' turtle rock ' } " )
2021-01-25 02:14:58 +00:00
2021-01-02 15:44:58 +00:00
ret . plando_texts = { }
2022-07-03 12:11:52 +00:00
if PlandoSettings . texts in plando_options :
2021-01-03 15:14:31 +00:00
tt = TextTable ( )
tt . removeUnwantedText ( )
2021-01-02 15:44:58 +00:00
options = weights . get ( " plando_texts " , [ ] )
for placement in options :
2021-08-29 18:21:49 +00:00
if roll_percentage ( get_choice_legacy ( " percentage " , placement , 100 ) ) :
at = str ( get_choice_legacy ( " at " , placement ) )
2021-01-03 15:14:31 +00:00
if at not in tt :
raise Exception ( f " No text target \" { at } \" found. " )
2021-08-29 18:21:49 +00:00
ret . plando_texts [ at ] = str ( get_choice_legacy ( " text " , placement ) )
2021-01-02 15:44:58 +00:00
2021-01-02 21:41:03 +00:00
ret . plando_connections = [ ]
2022-07-03 12:11:52 +00:00
if PlandoSettings . connections in plando_options :
2021-01-02 21:41:03 +00:00
options = weights . get ( " plando_connections " , [ ] )
for placement in options :
2021-08-29 18:21:49 +00:00
if roll_percentage ( get_choice_legacy ( " percentage " , placement , 100 ) ) :
2021-01-02 21:41:03 +00:00
ret . plando_connections . append ( PlandoConnection (
2021-08-29 18:21:49 +00:00
get_choice_legacy ( " entrance " , placement ) ,
get_choice_legacy ( " exit " , placement ) ,
get_choice_legacy ( " direction " , placement , " both " )
2021-01-02 21:41:03 +00:00
) )
2021-06-15 19:15:57 +00:00
ret . sprite_pool = weights . get ( ' sprite_pool ' , [ ] )
2021-08-29 18:21:49 +00:00
ret . sprite = get_choice_legacy ( ' sprite ' , weights , " Link " )
2021-06-15 19:15:57 +00:00
if ' random_sprite_on_event ' in weights :
randomoneventweights = weights [ ' random_sprite_on_event ' ]
2021-08-29 18:21:49 +00:00
if get_choice_legacy ( ' enabled ' , randomoneventweights , False ) :
2021-06-15 19:15:57 +00:00
ret . sprite = ' randomon '
2021-08-29 18:21:49 +00:00
ret . sprite + = ' -hit ' if get_choice_legacy ( ' on_hit ' , randomoneventweights , True ) else ' '
ret . sprite + = ' -enter ' if get_choice_legacy ( ' on_enter ' , randomoneventweights , False ) else ' '
ret . sprite + = ' -exit ' if get_choice_legacy ( ' on_exit ' , randomoneventweights , False ) else ' '
ret . sprite + = ' -slash ' if get_choice_legacy ( ' on_slash ' , randomoneventweights , False ) else ' '
ret . sprite + = ' -item ' if get_choice_legacy ( ' on_item ' , randomoneventweights , False ) else ' '
ret . sprite + = ' -bonk ' if get_choice_legacy ( ' on_bonk ' , randomoneventweights , False ) else ' '
ret . sprite = ' randomonall ' if get_choice_legacy ( ' on_everything ' , randomoneventweights , False ) else ret . sprite
2021-06-15 19:15:57 +00:00
ret . sprite = ' randomonnone ' if ret . sprite == ' randomon ' else ret . sprite
2021-08-29 18:21:49 +00:00
if ( not ret . sprite_pool or get_choice_legacy ( ' use_weighted_sprite_pool ' , randomoneventweights , False ) ) \
2021-06-15 19:15:57 +00:00
and ' sprite ' in weights : # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
for key , value in weights [ ' sprite ' ] . items ( ) :
if key . startswith ( ' random ' ) :
ret . sprite_pool + = [ ' random ' ] * int ( value )
else :
ret . sprite_pool + = [ key ] * int ( value )
2021-04-03 12:47:49 +00:00
2019-12-16 01:05:33 +00:00
if __name__ == ' __main__ ' :
2021-08-01 23:36:04 +00:00
import atexit
2021-08-02 02:57:57 +00:00
confirmation = atexit . register ( input , " Press enter to close. " )
2019-12-16 01:05:33 +00:00
main ( )
2021-08-02 02:57:57 +00:00
# in case of error-free exit should not need confirmation
2021-09-03 10:50:26 +00:00
atexit . unregister ( confirmation )