Multiple: Followed a rabbit hole of moving LttP Rom generation to AutoWorld

Generator: Re-allow names with spaces (and see what breaks)
Generator: Removed teams (Note that teams are intended to move from a generation step feature to a server runtime feature, allowing dynamic creation of an already generated MW)
LttP: All Rom Options are now on the new system
LttP: palette option "random" is now called "good"
LttP: Roms are now created as part of the general output file creation step
LttP: disable Music is now Music, removing potential double negatives
LttP & Factorio: Progressive option random is now grouped_random
LttP: Enemy damage option random is now Enemy damage: chaos
This commit is contained in:
Fabian Dill 2021-08-09 09:15:41 +02:00
parent 01d88c362a
commit ba3bb201cd
23 changed files with 328 additions and 279 deletions

View File

@ -13,7 +13,7 @@ import random
class MultiWorld(): class MultiWorld():
debug_types = False debug_types = False
player_names: Dict[int, List[str]] player_name: Dict[int, str]
_region_cache: Dict[int, Dict[str, Region]] _region_cache: Dict[int, Dict[str, Region]]
difficulty_requirements: dict difficulty_requirements: dict
required_medallions: dict required_medallions: dict
@ -36,7 +36,6 @@ class MultiWorld():
def __init__(self, players: int): def __init__(self, players: int):
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
self.players = players self.players = players
self.teams = 1
self.glitch_triforce = False self.glitch_triforce = False
self.algorithm = 'balanced' self.algorithm = 'balanced'
self.dungeons = [] self.dungeons = []
@ -83,11 +82,9 @@ class MultiWorld():
set_player_attr('item_functionality', 'normal') set_player_attr('item_functionality', 'normal')
set_player_attr('timer', False) set_player_attr('timer', False)
set_player_attr('goal', 'ganon') set_player_attr('goal', 'ganon')
set_player_attr('progressive', 'on')
set_player_attr('accessibility', 'items') set_player_attr('accessibility', 'items')
set_player_attr('retro', False) set_player_attr('retro', False)
set_player_attr('hints', True) set_player_attr('hints', True)
set_player_attr('player_names', [])
set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False) set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False) set_player_attr('powder_patch_required', False)
@ -162,10 +159,10 @@ class MultiWorld():
return tuple(player for player in self.player_ids if self.game[player] == game_name) return tuple(player for player in self.player_ids if self.game[player] == game_name)
def get_name_string_for_object(self, obj) -> str: def get_name_string_for_object(self, obj) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
def get_player_names(self, player: int) -> str: def get_player_name(self, player: int) -> str:
return ", ".join(self.player_names[player]) return self.player_name[player]
def initialize_regions(self, regions=None): def initialize_regions(self, regions=None):
for region in regions if regions else self.regions: for region in regions if regions else self.regions:
@ -174,7 +171,7 @@ class MultiWorld():
@functools.cached_property @functools.cached_property
def world_name_lookup(self): def world_name_lookup(self):
return {self.player_names[player_id][0]: player_id for player_id in self.player_ids} return {self.player_name[player_id]: player_id for player_id in self.player_ids}
def _recache(self): def _recache(self):
"""Rebuild world cache""" """Rebuild world cache"""
@ -1132,8 +1129,8 @@ class Spoiler():
def parse_data(self): def parse_data(self):
self.medallions = OrderedDict() self.medallions = OrderedDict()
for player in self.world.get_game_players("A Link to the Past"): for player in self.world.get_game_players("A Link to the Past"):
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0] self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][0]
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1] self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][1]
self.startinventory = list(map(str, self.world.precollected_items)) self.startinventory = list(map(str, self.world.precollected_items))
@ -1241,7 +1238,6 @@ class Spoiler():
'progressive': self.world.progressive, 'progressive': self.world.progressive,
'shufflepots': self.world.shufflepots, 'shufflepots': self.world.shufflepots,
'players': self.world.players, 'players': self.world.players,
'teams': self.world.teams,
'progression_balancing': self.world.progression_balancing, 'progression_balancing': self.world.progression_balancing,
'triforce_pieces_available': self.world.triforce_pieces_available, 'triforce_pieces_available': self.world.triforce_pieces_available,
'triforce_pieces_required': self.world.triforce_pieces_required, 'triforce_pieces_required': self.world.triforce_pieces_required,
@ -1261,7 +1257,7 @@ class Spoiler():
out['Starting Inventory'] = self.startinventory out['Starting Inventory'] = self.startinventory
out['Special'] = self.medallions out['Special'] = self.medallions
if self.hashes: if self.hashes:
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()} out['Hashes'] = self.hashes
if self.shops: if self.shops:
out['Shops'] = self.shops out['Shops'] = self.shops
out['playthrough'] = self.playthrough out['playthrough'] = self.playthrough
@ -1286,10 +1282,10 @@ class Spoiler():
self.metadata['version'], self.world.seed)) self.metadata['version'], self.world.seed))
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Players: %d\n' % self.world.players) outfile.write('Players: %d\n' % self.world.players)
outfile.write('Teams: %d\n' % self.world.teams)
for player in range(1, self.world.players + 1): for player in range(1, self.world.players + 1):
if self.world.players > 1: if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
outfile.write('Game: %s\n' % self.metadata['game'][player]) outfile.write('Game: %s\n' % self.metadata['game'][player])
if self.world.players > 1: if self.world.players > 1:
outfile.write('Progression Balanced: %s\n' % ( outfile.write('Progression Balanced: %s\n' % (
@ -1303,11 +1299,7 @@ class Spoiler():
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n') outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
if player in self.world.get_game_players("A Link to the Past"): if player in self.world.get_game_players("A Link to the Past"):
for team in range(self.world.teams): outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
(player in self.world.get_game_players("A Link to the Past") and self.world.teams > 1) else 'Hash: ',
self.hashes[player, team]))
outfile.write('Logic: %s\n' % self.metadata['logic'][player]) outfile.write('Logic: %s\n' % self.metadata['logic'][player])
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player]) outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
@ -1326,7 +1318,6 @@ class Spoiler():
self.metadata["triforce_pieces_required"][player]) self.metadata["triforce_pieces_required"][player])
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Item Progression: %s\n' % self.metadata['progressive'][player])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != "vanilla": if self.metadata['shuffle'][player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player]) outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
@ -1369,7 +1360,7 @@ class Spoiler():
self.metadata['shuffle_prizes'][player]) self.metadata['shuffle_prizes'][player])
if self.entrances: if self.entrances:
outfile.write('\n\nEntrances:\n\n') outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_name(entry["player"])}: '
if self.world.players > 1 else '', entry['entrance'], if self.world.players > 1 else '', entry['entrance'],
'<=>' if entry['direction'] == 'both' else '<=>' if entry['direction'] == 'both' else
'<=' if entry['direction'] == 'exit' else '=>', '<=' if entry['direction'] == 'exit' else '=>',
@ -1383,7 +1374,7 @@ class Spoiler():
if factorio_players: if factorio_players:
outfile.write('\n\nRecipes:\n') outfile.write('\n\nRecipes:\n')
for player in factorio_players: for player in factorio_players:
name = self.world.get_player_names(player) name = self.world.get_player_name(player)
for recipe in self.world.worlds[player].custom_recipes.values(): for recipe in self.world.worlds[player].custom_recipes.values():
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}") outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
@ -1401,7 +1392,7 @@ class Spoiler():
for player in self.world.get_game_players("A Link to the Past"): for player in self.world.get_game_players("A Link to the Past"):
if self.world.boss_shuffle[player] != 'none': if self.world.boss_shuffle[player] != 'none':
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n') outfile.write(f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
outfile.write(' '+'\n '.join([f'{x}: {y}' for x, y in bossmap.items()])) outfile.write(' '+'\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
outfile.write('\n\nPlaythrough:\n\n') outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()])) outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))

View File

@ -444,4 +444,4 @@ def distribute_planned(world: MultiWorld):
except ValueError: except ValueError:
placement.warn(f"Could not remove {item} from pool as it's already missing from it.") placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
except Exception as e: except Exception as e:
raise Exception(f"Error running plando for player {player} ({world.player_names[player]})") from e raise Exception(f"Error running plando for player {player} ({world.player_name[player]})") from e

View File

@ -40,7 +40,6 @@ def mystery_argparse():
help="Input directory for player files.") help="Input directory for player files.")
parser.add_argument('--seed', help='Define seed number to generate.', type=int) parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--multi', default=defaults["players"], type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"]) parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.") parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
parser.add_argument('--enemizercli', default=defaults["enemizer_path"]) parser.add_argument('--enemizercli', default=defaults["enemizer_path"])
@ -128,7 +127,6 @@ def main(args=None, callback=ERmain):
erargs.skip_playthrough = args.spoiler < 2 erargs.skip_playthrough = args.spoiler < 2
erargs.outputname = seed_name erargs.outputname = seed_name
erargs.outputpath = args.outputpath erargs.outputpath = args.outputpath
erargs.teams = args.teams
# set up logger # set up logger
if args.log_level: if args.log_level:
@ -179,6 +177,8 @@ def main(args=None, callback=ERmain):
getattr(erargs, k)[player] = v getattr(erargs, k)[player] = v
except AttributeError: except AttributeError:
setattr(erargs, k, {player: v}) setattr(erargs, k, {player: v})
except Exception as e:
raise Exception(f"Error setting {k} to {v} for player {player}") from e
except Exception as e: except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
else: else:
@ -189,8 +189,6 @@ def main(args=None, callback=ERmain):
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter) erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
erargs.names = ",".join(erargs.name[i] for i in range(1, args.multi + 1))
del (erargs.name)
if args.yaml_output: if args.yaml_output:
import yaml import yaml
important = {} important = {}
@ -267,7 +265,7 @@ def handle_name(name: str, player: int, name_counter: Counter):
name] > 1 else ''), name] > 1 else ''),
player=player, player=player,
PLAYER=(player if player > 1 else ''))) PLAYER=(player if player > 1 else '')))
new_name = new_name.strip().replace(' ', '_')[:16] new_name = new_name.strip()[:16]
if new_name == "Archipelago": if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"") raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name return new_name
@ -610,7 +608,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.enemy_damage = {None: 'default', ret.enemy_damage = {None: 'default',
'default': 'default', 'default': 'default',
'shuffled': 'shuffled', 'shuffled': 'shuffled',
'random': 'chaos' 'random': 'chaos', # to be removed
'chaos': 'chaos',
}[get_choice('enemy_damage', weights)] }[get_choice('enemy_damage', weights)]
ret.enemy_health = get_choice('enemy_health', weights) ret.enemy_health = get_choice('enemy_health', weights)
@ -635,8 +634,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default') ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on'))
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g") ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
ret.required_medallions = [get_choice("misery_mire_medallion", weights, "random"), ret.required_medallions = [get_choice("misery_mire_medallion", weights, "random"),
@ -737,20 +734,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
else: else:
ret.sprite_pool += [key] * int(value) ret.sprite_pool += [key] * int(value)
ret.disablemusic = get_choice('disablemusic', weights, False)
ret.triforcehud = get_choice('triforcehud', weights, 'hide_goal')
ret.quickswap = get_choice('quickswap', weights, True)
ret.fastmenu = get_choice('menuspeed', weights, "normal")
ret.reduceflashing = get_choice('reduceflashing', weights, False)
ret.heartcolor = get_choice('heartcolor', weights, "red")
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', weights, "normal"))
ret.ow_palettes = get_choice('ow_palettes', weights, "default")
ret.uw_palettes = get_choice('uw_palettes', weights, "default")
ret.hud_palettes = get_choice('hud_palettes', weights, "default")
ret.sword_palettes = get_choice('sword_palettes', weights, "default")
ret.shield_palettes = get_choice('shield_palettes', weights, "default")
ret.link_palettes = get_choice('link_palettes', weights, "default")
if __name__ == '__main__': if __name__ == '__main__':
import atexit import atexit

View File

@ -44,7 +44,7 @@ def main():
help='Path to an ALttP JAP(1.0) rom to use as a base.') help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?', parser.add_argument('--loglevel', default='info', const='info', nargs='?',
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\ help='''\
Select the rate at which the menu opens and closes. Select the rate at which the menu opens and closes.
@ -100,6 +100,7 @@ def main():
parser.add_argument('--names', default='', type=str) parser.add_argument('--names', default='', type=str)
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.') parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
args = parser.parse_args() args = parser.parse_args()
args.music = not args.disablemusic
if args.update_sprites: if args.update_sprites:
run_sprite_update() run_sprite_update()
sys.exit() sys.exit()
@ -150,7 +151,7 @@ def adjust(args):
if hasattr(args, "world"): if hasattr(args, "world"):
world = getattr(args, "world") world = getattr(args, "world")
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world) args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world)
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc') path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
rom.write_to_file(path) rom.write_to_file(path)
@ -195,14 +196,14 @@ def adjustGUI():
guiargs = Namespace() guiargs = Namespace()
guiargs.heartbeep = rom_vars.heartbeepVar.get() guiargs.heartbeep = rom_vars.heartbeepVar.get()
guiargs.heartcolor = rom_vars.heartcolorVar.get() guiargs.heartcolor = rom_vars.heartcolorVar.get()
guiargs.fastmenu = rom_vars.fastMenuVar.get() guiargs.menuspeed = rom_vars.menuspeedVar.get()
guiargs.ow_palettes = rom_vars.owPalettesVar.get() guiargs.ow_palettes = rom_vars.owPalettesVar.get()
guiargs.uw_palettes = rom_vars.uwPalettesVar.get() guiargs.uw_palettes = rom_vars.uwPalettesVar.get()
guiargs.hud_palettes = rom_vars.hudPalettesVar.get() guiargs.hud_palettes = rom_vars.hudPalettesVar.get()
guiargs.sword_palettes = rom_vars.swordPalettesVar.get() guiargs.sword_palettes = rom_vars.swordPalettesVar.get()
guiargs.shield_palettes = rom_vars.shieldPalettesVar.get() guiargs.shield_palettes = rom_vars.shieldPalettesVar.get()
guiargs.quickswap = bool(rom_vars.quickSwapVar.get()) guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
guiargs.disablemusic = bool(rom_vars.disableMusicVar.get()) guiargs.music = bool(rom_vars.MusicVar.get())
guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get()) guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
guiargs.rom = romVar2.get() guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get() guiargs.baserom = romVar.get()
@ -439,9 +440,10 @@ def get_rom_options_frame(parent=None):
romOptionsFrame.rowconfigure(i, weight=1) romOptionsFrame.rowconfigure(i, weight=1)
vars = Namespace() vars = Namespace()
vars.disableMusicVar = IntVar() vars.MusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=vars.disableMusicVar) vars.MusicVar.set(1)
disableMusicCheckbutton.grid(row=0, column=0, sticky=E) MusicCheckbutton = Checkbutton(romOptionsFrame, text="Music", variable=vars.MusicVar)
MusicCheckbutton.grid(row=0, column=0, sticky=E)
vars.disableFlashingVar = IntVar(value=1) vars.disableFlashingVar = IntVar(value=1)
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)", variable=vars.disableFlashingVar) disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)", variable=vars.disableFlashingVar)
@ -485,14 +487,14 @@ def get_rom_options_frame(parent=None):
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar) quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E) quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
fastMenuFrame = Frame(romOptionsFrame) menuspeedFrame = Frame(romOptionsFrame)
fastMenuFrame.grid(row=1, column=1, sticky=E) menuspeedFrame.grid(row=1, column=1, sticky=E)
fastMenuLabel = Label(fastMenuFrame, text='Menu speed') menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
fastMenuLabel.pack(side=LEFT) menuspeedLabel.pack(side=LEFT)
vars.fastMenuVar = StringVar() vars.menuspeedVar = StringVar()
vars.fastMenuVar.set('normal') vars.menuspeedVar.set('normal')
fastMenuOptionMenu = OptionMenu(fastMenuFrame, vars.fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half') menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu.pack(side=LEFT) menuspeedOptionMenu.pack(side=LEFT)
heartcolorFrame = Frame(romOptionsFrame) heartcolorFrame = Frame(romOptionsFrame)
heartcolorFrame.grid(row=2, column=0, sticky=E) heartcolorFrame.grid(row=2, column=0, sticky=E)
@ -518,7 +520,7 @@ def get_rom_options_frame(parent=None):
owPalettesLabel.pack(side=LEFT) owPalettesLabel.pack(side=LEFT)
vars.owPalettesVar = StringVar() vars.owPalettesVar = StringVar()
vars.owPalettesVar.set('default') vars.owPalettesVar.set('default')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
owPalettesOptionMenu.pack(side=LEFT) owPalettesOptionMenu.pack(side=LEFT)
uwPalettesFrame = Frame(romOptionsFrame) uwPalettesFrame = Frame(romOptionsFrame)
@ -527,7 +529,7 @@ def get_rom_options_frame(parent=None):
uwPalettesLabel.pack(side=LEFT) uwPalettesLabel.pack(side=LEFT)
vars.uwPalettesVar = StringVar() vars.uwPalettesVar = StringVar()
vars.uwPalettesVar.set('default') vars.uwPalettesVar.set('default')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
uwPalettesOptionMenu.pack(side=LEFT) uwPalettesOptionMenu.pack(side=LEFT)
hudPalettesFrame = Frame(romOptionsFrame) hudPalettesFrame = Frame(romOptionsFrame)
@ -536,7 +538,7 @@ def get_rom_options_frame(parent=None):
hudPalettesLabel.pack(side=LEFT) hudPalettesLabel.pack(side=LEFT)
vars.hudPalettesVar = StringVar() vars.hudPalettesVar = StringVar()
vars.hudPalettesVar.set('default') vars.hudPalettesVar.set('default')
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
hudPalettesOptionMenu.pack(side=LEFT) hudPalettesOptionMenu.pack(side=LEFT)
swordPalettesFrame = Frame(romOptionsFrame) swordPalettesFrame = Frame(romOptionsFrame)
@ -545,7 +547,7 @@ def get_rom_options_frame(parent=None):
swordPalettesLabel.pack(side=LEFT) swordPalettesLabel.pack(side=LEFT)
vars.swordPalettesVar = StringVar() vars.swordPalettesVar = StringVar()
vars.swordPalettesVar.set('default') vars.swordPalettesVar.set('default')
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
swordPalettesOptionMenu.pack(side=LEFT) swordPalettesOptionMenu.pack(side=LEFT)
shieldPalettesFrame = Frame(romOptionsFrame) shieldPalettesFrame = Frame(romOptionsFrame)
@ -554,7 +556,7 @@ def get_rom_options_frame(parent=None):
shieldPalettesLabel.pack(side=LEFT) shieldPalettesLabel.pack(side=LEFT)
vars.shieldPalettesVar = StringVar() vars.shieldPalettesVar = StringVar()
vars.shieldPalettesVar.set('default') vars.shieldPalettesVar.set('default')
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
shieldPalettesOptionMenu.pack(side=LEFT) shieldPalettesOptionMenu.pack(side=LEFT)
spritePoolFrame = Frame(romOptionsFrame) spritePoolFrame = Frame(romOptionsFrame)

107
Main.py
View File

@ -10,17 +10,15 @@ import tempfile
import zipfile import zipfile
from typing import Dict, Tuple from typing import Dict, Tuple
from BaseClasses import MultiWorld, CollectionState, Region, Item from BaseClasses import MultiWorld, CollectionState, Region
from worlds.alttp.Items import item_name_groups from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import difficulties, fill_prizes from worlds.alttp.ItemPool import difficulties
from Utils import output_path, parse_player_names, get_options, __version__, version_tuple from Utils import output_path, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules from worlds.generic.Rules import locality_rules, exclusion_rules
from worlds import AutoWorld from worlds import AutoWorld
import Patch
seeddigits = 20 seeddigits = 20
@ -66,7 +64,6 @@ def main(args, seed=None):
world.difficulty = args.difficulty.copy() world.difficulty = args.difficulty.copy()
world.item_functionality = args.item_functionality.copy() world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy() world.timer = args.timer.copy()
world.progressive = args.progressive.copy()
world.goal = args.goal.copy() world.goal = args.goal.copy()
world.local_items = args.local_items.copy() world.local_items = args.local_items.copy()
if hasattr(args, "algorithm"): # current GUI options if hasattr(args, "algorithm"): # current GUI options
@ -99,7 +96,6 @@ def main(args, seed=None):
world.blue_clock_time = args.blue_clock_time.copy() world.blue_clock_time = args.blue_clock_time.copy()
world.green_clock_time = args.green_clock_time.copy() world.green_clock_time = args.green_clock_time.copy()
world.shufflepots = args.shufflepots.copy() world.shufflepots = args.shufflepots.copy()
world.progressive = args.progressive.copy()
world.dungeon_counters = args.dungeon_counters.copy() world.dungeon_counters = args.dungeon_counters.copy()
world.glitch_boots = args.glitch_boots.copy() world.glitch_boots = args.glitch_boots.copy()
world.triforce_pieces_available = args.triforce_pieces_available.copy() world.triforce_pieces_available = args.triforce_pieces_available.copy()
@ -117,6 +113,10 @@ def main(args, seed=None):
world.required_medallions = args.required_medallions.copy() world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy() world.game = args.game.copy()
world.set_options(args) world.set_options(args)
world.player_name = args.name.copy()
world.alttp_rom = args.rom
world.enemizer = args.enemizercli
world.sprite = args.sprite.copy()
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
@ -148,13 +148,6 @@ def main(args, seed=None):
for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations") logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations")
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)
logger.info('') logger.info('')
for player in world.get_game_players("A Link to the Past"): for player in world.get_game_players("A Link to the Past"):
@ -241,63 +234,18 @@ def main(args, seed=None):
logger.info('Generating output files.') logger.info('Generating output files.')
outfilebase = 'AP_' + world.seed_name outfilebase = 'AP_' + world.seed_name
rom_names = []
def _gen_rom(team: int, player: int, output_directory:str):
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.shufflepots[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
rom = LocalRom(args.rom)
patch_rom(world, rom, player, team, use_enemizer)
if use_enemizer:
patch_enemizer(world, team, player, rom, args.enemizercli, output_directory)
if args.race:
patch_race_rom(rom, world, player)
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
palettes_options = {
'dungeon': args.uw_palettes[player],
'overworld': args.ow_palettes[player],
'hud': args.hud_palettes[player],
'sword': args.sword_palettes[player],
'shield': args.shield_palettes[player],
'link': args.link_palettes[player]
}
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
palettes_options, world, player, True,
reduceflashing=args.reduceflashing[player] or args.race,
triforcehud=args.triforcehud[player])
outfilepname = f'_P{player}'
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \
if world.player_names[player][team] != 'Player%d' % player else ''
rompath = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
rom.write_to_file(rompath, hide_enemizer=True)
Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team])
os.unlink(rompath)
return player, team, bytes(rom.name)
pool = concurrent.futures.ThreadPoolExecutor() pool = concurrent.futures.ThreadPoolExecutor()
output = tempfile.TemporaryDirectory() output = tempfile.TemporaryDirectory()
with output as temp_dir: with output as temp_dir:
check_accessibility_task = pool.submit(world.fulfills_accessibility) check_accessibility_task = pool.submit(world.fulfills_accessibility)
rom_futures = []
output_file_futures = [] output_file_futures = []
for team in range(world.teams):
for player in world.get_game_players("A Link to the Past"):
rom_futures.append(pool.submit(_gen_rom, team, player, temp_dir))
for player in world.player_ids: for player in world.player_ids:
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
def get_entrance_to_region(region: Region): def get_entrance_to_region(region: Region):
for entrance in region.entrances: for entrance in region.entrances:
@ -365,12 +313,8 @@ def main(args, seed=None):
FillDisabledShopSlots(world) FillDisabledShopSlots(world)
def write_multidata(roms, outputs): def write_multidata():
import base64
import NetUtils import NetUtils
for future in roms:
rom_name = future.result()
rom_names.append(rom_name)
slot_data = {} slot_data = {}
client_versions = {} client_versions = {}
minimum_versions = {"server": (0, 1, 1), "clients": client_versions} minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
@ -378,8 +322,6 @@ def main(args, seed=None):
for slot in world.player_ids: for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version() client_versions[slot] = world.worlds[slot].get_required_client_version()
games[slot] = world.game[slot] games[slot] = world.game[slot]
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names}
precollected_items = {player: [] for player in range(1, world.players + 1)} precollected_items = {player: [] for player in range(1, world.players + 1)}
for item in world.precollected_items: for item in world.precollected_items:
precollected_items[item.player].append(item.code) precollected_items[item.player].append(item.code)
@ -390,11 +332,6 @@ def main(args, seed=None):
if world.tech_tree_information[player].value == 2: if world.tech_tree_information[player].value == 2:
sending_visible_players.add(player) sending_visible_players.add(player)
for i, team in enumerate(parsed_names):
for player, name in enumerate(team, 1):
if player not in world.get_game_players("A Link to the Past"):
connect_names[name] = (i, player)
for slot in world.player_ids: for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data() slot_data[slot] = world.worlds[slot].fill_slot_data()
@ -414,11 +351,11 @@ def main(args, seed=None):
precollected_hints[location.player].add(hint) precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint) precollected_hints[location.item.player].add(hint)
multidata = zlib.compress(pickle.dumps({ multidata = {
"slot_data": slot_data, "slot_data": slot_data,
"games": games, "games": games,
"names": parsed_names, "names": [{player: name for player, name in world.player_name.items()}],
"connect_names": connect_names, "connect_names": {name: (0, player) for player, name in world.player_name.items()},
"remote_items": {player for player in world.player_ids if "remote_items": {player for player in world.player_ids if
world.worlds[player].remote_items}, world.worlds[player].remote_items},
"locations": locations_data, "locations": locations_data,
@ -431,15 +368,17 @@ def main(args, seed=None):
"tags": ["AP"], "tags": ["AP"],
"minimum_versions": minimum_versions, "minimum_versions": minimum_versions,
"seed_name": world.seed_name "seed_name": world.seed_name
}), 9) }
AutoWorld.call_all(world, "modify_multidata", multidata)
with open(os.path.join(temp_dir, '%s.archipelago' % outfilebase), 'wb') as f: multidata = zlib.compress(pickle.dumps(multidata), 9)
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
f.write(bytes([1])) # version of format f.write(bytes([1])) # version of format
f.write(multidata) f.write(multidata)
for future in outputs:
future.result() # collect errors if they occured
multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures)
multidata_task = pool.submit(write_multidata)
if not check_accessibility_task.result(): if not check_accessibility_task.result():
if not world.can_beat_game(): if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.") raise Exception("Game appears as unbeatable. Aborting.")
@ -451,8 +390,10 @@ def main(args, seed=None):
if not args.skip_playthrough: if not args.skip_playthrough:
logger.info('Calculating playthrough.') logger.info('Calculating playthrough.')
create_playthrough(world) create_playthrough(world)
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done if args.create_spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
for future in output_file_futures:
future.result()
zipfilename = output_path(f"AP_{world.seed_name}.zip") zipfilename = output_path(f"AP_{world.seed_name}.zip")
logger.info(f'Creating final archive at {zipfilename}.') logger.info(f'Creating final archive at {zipfilename}.')
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,

View File

@ -11,9 +11,11 @@ class AssembleOptions(type):
for base in bases: for base in bases:
if hasattr(base, "options"): if hasattr(base, "options"):
options.update(base.options) options.update(base.options)
name_lookup.update(name_lookup) name_lookup.update(base.name_lookup)
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("option_")} name.startswith("option_")}
if "random" in new_options:
raise Exception("Choice option 'random' cannot be manually assigned.")
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
options.update(new_options) options.update(new_options)
@ -47,7 +49,12 @@ class Option(metaclass=AssembleOptions):
def __hash__(self): def __hash__(self):
return hash(self.value) return hash(self.value)
@property
def current_key(self) -> str:
return self.name_lookup[self.value]
def get_current_option_name(self) -> str: def get_current_option_name(self) -> str:
"""For display purposes."""
return self.get_option_name(self.value) return self.get_option_name(self.value)
def get_option_name(self, value: typing.Any) -> str: def get_option_name(self, value: typing.Any) -> str:
@ -122,8 +129,13 @@ class Choice(Option):
@classmethod @classmethod
def from_text(cls, text: str) -> Choice: def from_text(cls, text: str) -> Choice:
text = text.lower()
# TODO: turn on after most people have adjusted their yamls to no longer have suboptions with "random" in them
# maybe in 0.2?
# if text == "random":
# return cls(random.choice(list(cls.options.values())))
for optionname, value in cls.options.items(): for optionname, value in cls.options.items():
if optionname == text.lower(): if optionname == text:
return cls(value) return cls(value)
raise KeyError( raise KeyError(
f'Could not find option "{text}" for "{cls.__name__}", ' f'Could not find option "{text}" for "{cls.__name__}", '

View File

@ -51,24 +51,6 @@ def snes_to_pc(value):
return ((value & 0x7F0000) >> 1) | (value & 0x7FFF) return ((value & 0x7F0000) >> 1) | (value & 0x7FFF)
def parse_player_names(names, players, teams):
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
if len(names) != len(set(names)):
name_counter = collections.Counter(names)
raise ValueError(f"Duplicate Player names is not supported, "
f'found multiple "{name_counter.most_common(1)[0][0]}".')
ret = []
while names or len(ret) < teams:
team = [n[:16] for n in names[:players]]
# 16 bytes in rom per player, which will map to more in unicode, but those characters later get filtered
while len(team) != players:
team.append(f"Player{len(team) + 1}")
ret.append(team)
names = names[players:]
return ret
def cache_argsless(function): def cache_argsless(function):
if function.__code__.co_argcount: if function.__code__.co_argcount:
raise Exception("Can only cache 0 argument functions with this cache.") raise Exception("Can only cache 0 argument functions with this cache.")
@ -308,7 +290,7 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
adjuster_settings.rom = romfile adjuster_settings.rom = romfile
adjuster_settings.baserom = Patch.get_base_rom_path() adjuster_settings.baserom = Patch.get_base_rom_path()
adjuster_settings.world = None adjuster_settings.world = None
whitelist = {"disablemusic", "fastmenu", "heartbeep", "heartcolor", "ow_palettes", "quickswap", whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite"} "uw_palettes", "sprite"}
printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist} printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist}
if hasattr(adjuster_settings, "sprite_pool"): if hasattr(adjuster_settings, "sprite_pool"):

View File

@ -91,8 +91,6 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0] erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter) erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1))
del (erargs.name)
ERmain(erargs, seed) ERmain(erargs, seed)
return upload_to_db(target.name, owner, sid, race) return upload_to_db(target.name, owner, sid, race)

View File

@ -45,9 +45,6 @@ server_options:
log_network: 0 log_network: 0
# Options for Generation # Options for Generation
generator: generator:
# Teams
# Note that this feature is TODO: to move it to dynamic creation on server, not during generation
teams: 1
# Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases # Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases
enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe" enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe"
# Folder from which the player yaml files are pulled from # Folder from which the player yaml files are pulled from

View File

@ -106,7 +106,7 @@ Factorio:
progressive: progressive:
on: 1 on: 1
off: 0 off: 0
random: 0 grouped_random: 0
tech_tree_information: tech_tree_information:
none: 0 none: 0
advancement: 0 # show which items are a logical advancement advancement: 0 # show which items are a logical advancement
@ -307,7 +307,7 @@ A Link to the Past:
progressive: # Enable or disable progressive items (swords, shields, bow) progressive: # Enable or disable progressive items (swords, shields, bow)
on: 50 # All items are progressive on: 50 # All items are progressive
off: 0 # No items are progressive off: 0 # No items are progressive
random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be grouped_random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
entrance_shuffle: entrance_shuffle:
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
@ -596,7 +596,7 @@ A Link to the Past:
off: 0 off: 0
ow_palettes: # Change the colors of the overworld ow_palettes: # Change the colors of the overworld
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@ -606,7 +606,7 @@ A Link to the Past:
puke: 0 puke: 0
uw_palettes: # Change the colors of caves and dungeons uw_palettes: # Change the colors of caves and dungeons
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@ -616,7 +616,7 @@ A Link to the Past:
puke: 0 puke: 0
hud_palettes: # Change the colors of the hud hud_palettes: # Change the colors of the hud
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@ -626,7 +626,7 @@ A Link to the Past:
puke: 0 puke: 0
sword_palettes: # Change the colors of swords sword_palettes: # Change the colors of swords
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@ -636,7 +636,7 @@ A Link to the Past:
puke: 0 puke: 0
shield_palettes: # Change the colors of shields shield_palettes: # Change the colors of shields
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@ -693,7 +693,7 @@ linked_options:
singularity: 1 singularity: 1
enemy_damage: enemy_damage:
shuffled: 1 shuffled: 1
random: 1 chaos: 1
enemy_health: enemy_health:
easy: 1 easy: 1
hard: 1 hard: 1

View File

@ -51,7 +51,15 @@ def call_all(world: MultiWorld, method_name: str, *args):
for world_type in world_types: for world_type in world_types:
stage_callable = getattr(world_type, f"stage_{method_name}", None) stage_callable = getattr(world_type, f"stage_{method_name}", None)
if stage_callable: if stage_callable:
stage_callable(world) stage_callable(world, *args)
def call_stage(world: MultiWorld, method_name: str, *args):
world_types = {world.worlds[player].__class__ for player in world.player_ids}
for world_type in world_types:
stage_callable = getattr(world_type, f"stage_{method_name}", None)
if stage_callable:
stage_callable(world, *args)
class World(metaclass=AutoWorldRegister): class World(metaclass=AutoWorldRegister):
@ -127,6 +135,10 @@ class World(metaclass=AutoWorldRegister):
"""Fill in the slot_data field in the Connected network package.""" """Fill in the slot_data field in the Connected network package."""
return {} return {}
def modify_multidata(self, multidata: dict):
"""For deeper modification of server multidata."""
pass
def get_required_client_version(self) -> Tuple[int, int, int]: def get_required_client_version(self) -> Tuple[int, int, int]:
return 0, 0, 3 return 0, 0, 3

View File

@ -143,20 +143,7 @@ def parse_arguments(argv, no_defaults=False):
off. off.
Off: Dungeon counters are never shown. Off: Dungeon counters are never shown.
''') ''')
parser.add_argument('--progressive', default=defval('on'), const='normal', nargs='?', choices=['on', 'off', 'random'],
help='''\
Select progressive equipment setting. Affects available itempool. (default: %(default)s)
On: Swords, Shields, Armor, and Gloves will
all be progressive equipment. Each subsequent
item of the same type the player finds will
upgrade that piece of equipment by one stage.
Off: Swords, Shields, Armor, and Gloves will not
be progressive equipment. Higher level items may
be found at any time. Downgrades are not possible.
Random: Swords, Shields, Armor, and Gloves will, per
category, be randomly progressive or not.
Link will die in one hit.
''')
parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?', parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?',
choices=['freshness', 'flood', 'vt25', 'vt26', 'balanced'], choices=['freshness', 'flood', 'vt25', 'vt26', 'balanced'],
help='''\ help='''\
@ -218,22 +205,7 @@ def parse_arguments(argv, no_defaults=False):
--seed given will produce the same 10 (different) roms each --seed given will produce the same 10 (different) roms each
time). time).
''', type=int) ''', type=int)
parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?',
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
Select the rate at which the menu opens and closes.
(default: %(default)s)
''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?', choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
help='''\
Hide the triforce hud in certain circumstances.
hide_goal will hide the hud until finding a triforce piece, hide_required will hide the total amount needed to win
(Both can be revealed when speaking to Murahalda)
(default: %(default)s)
''')
parser.add_argument('--enableflashing', help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)', action='store_false', dest="reduceflashing")
parser.add_argument('--mapshuffle', default=defval(False), parser.add_argument('--mapshuffle', default=defval(False),
help='Maps are no longer restricted to their dungeons, but can be anywhere', help='Maps are no longer restricted to their dungeons, but can be anywhere',
action='store_true') action='store_true')
@ -276,19 +248,6 @@ def parse_arguments(argv, no_defaults=False):
If set, the Pyramid Hole and Ganon's Tower are not If set, the Pyramid Hole and Ganon's Tower are not
included entrance shuffle pool. included entrance shuffle pool.
''', action='store_false', dest='shuffleganon') ''', action='store_false', dest='shuffleganon')
parser.add_argument('--heartbeep', default=defval('normal'), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
help='''\
Select the rate at which the heart beep sound is played at
low health. (default: %(default)s)
''')
parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--hud_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--shield_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--sword_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--link_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--sprite', help='''\ parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in Path to a sprite sheet to use for Link. Needs to be in
@ -380,15 +339,14 @@ def parse_arguments(argv, no_defaults=False):
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer', 'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'sprite',
'heartbeep', "progression_balancing", "triforce_pieces_available", "progression_balancing", "triforce_pieces_available",
"triforce_pieces_required", "shop_shuffle", "triforce_pieces_required", "shop_shuffle",
"required_medallions", "start_hints", "required_medallions", "start_hints",
"plando_items", "plando_texts", "plando_connections", "er_seeds", "plando_items", "plando_texts", "plando_connections", "er_seeds",
'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', 'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
'restrict_dungeon_item_on_boss', 'reduceflashing', 'game', 'restrict_dungeon_item_on_boss', 'game']:
'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes', 'triforcehud']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:
setattr(ret, name, {1: value}) setattr(ret, name, {1: value})

View File

@ -1064,7 +1064,7 @@ def link_entrances(world, player):
connect_doors(world, single_doors, door_targets, player) connect_doors(world, single_doors, door_targets, player)
else: else:
raise NotImplementedError( raise NotImplementedError(
f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_names(player)}') f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}')
# mandatory hybrid major glitches connections # mandatory hybrid major glitches connections
if world.logic[player] in ['hybridglitches', 'nologic']: if world.logic[player] in ['hybridglitches', 'nologic']:

View File

@ -399,7 +399,7 @@ def generate_itempool(world):
if additional_triforce_pieces: if additional_triforce_pieces:
if additional_triforce_pieces > len(nonprogressionitems): if additional_triforce_pieces > len(nonprogressionitems):
raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player " raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player "
f"{world.get_player_names(player)}.") f"{world.get_player_name(player)}.")
progressionitems += [ItemFactory("Triforce Piece", player)] * additional_triforce_pieces progressionitems += [ItemFactory("Triforce Piece", player)] * additional_triforce_pieces
nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool
nonprogressionitems = nonprogressionitems[additional_triforce_pieces:] nonprogressionitems = nonprogressionitems[additional_triforce_pieces:]
@ -563,16 +563,14 @@ def get_pool_core(world, player: int):
assert loc not in placed_items assert loc not in placed_items
placed_items[loc] = item placed_items[loc] = item
def want_progressives():
return world.random.choice([True, False]) if progressive == 'random' else progressive == 'on'
# provide boots to major glitch dependent seeds # provide boots to major glitch dependent seeds
if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt': if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt':
precollected_items.append('Pegasus Boots') precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots') pool.remove('Pegasus Boots')
pool.append('Rupees (20)') pool.append('Rupees (20)')
want_progressives = world.progressive[player].want_progressives
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressiveglove) pool.extend(diff.progressiveglove)
else: else:
pool.extend(diff.basicglove) pool.extend(diff.basicglove)
@ -599,22 +597,22 @@ def get_pool_core(world, player: int):
thisbottle = world.random.choice(diff.bottles) thisbottle = world.random.choice(diff.bottles)
pool.append(thisbottle) pool.append(thisbottle)
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressiveshield) pool.extend(diff.progressiveshield)
else: else:
pool.extend(diff.basicshield) pool.extend(diff.basicshield)
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressivearmor) pool.extend(diff.progressivearmor)
else: else:
pool.extend(diff.basicarmor) pool.extend(diff.basicarmor)
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressivemagic) pool.extend(diff.progressivemagic)
else: else:
pool.extend(diff.basicmagic) pool.extend(diff.basicmagic)
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressivebow) pool.extend(diff.progressivebow)
elif (swordless or logic == 'noglitches') and goal != 'icerodhunt': elif (swordless or logic == 'noglitches') and goal != 'icerodhunt':
swordless_bows = ['Bow', 'Silver Bow'] swordless_bows = ['Bow', 'Silver Bow']
@ -627,7 +625,7 @@ def get_pool_core(world, player: int):
if swordless: if swordless:
pool.extend(diff.swordless) pool.extend(diff.swordless)
else: else:
progressive_swords = want_progressives() progressive_swords = want_progressives(world.random)
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword) pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
extraitems = total_items_to_place - len(pool) - len(placed_items) extraitems = total_items_to_place - len(pool) - len(placed_items)

View File

@ -1,6 +1,6 @@
import typing import typing
from Options import Choice, Range, Option from Options import Choice, Range, Option, Toggle, DefaultOnToggle
class Logic(Choice): class Logic(Choice):
@ -70,8 +70,116 @@ class Enemies(Choice):
option_shuffled = 1 option_shuffled = 1
option_chaos = 2 option_chaos = 2
class Progressive(Choice):
displayname = "Progressive Items"
option_off = 0
option_grouped_random = 1
option_on = 2
alias_false = 0
alias_true = 2
default = 2
def want_progressives(self, random):
return random.choice([True, False]) if self.value == self.option_grouped_random else int(self.value)
class Palette(Choice):
option_default = 0
option_good = 1
option_blackout = 2
option_puke = 3
option_classic = 4
option_grayscale = 5
option_negative = 6
option_dizzy = 7
option_sick = 8
class OWPalette(Palette):
displayname = "Overworld Palette"
class UWPalette(Palette):
displayname = "Underworld Palette"
class HUDPalette(Palette):
displayname = "Menu Palette"
class SwordPalette(Palette):
displayname = "Sword Palette"
class ShieldPalette(Palette):
displayname = "Shield Palette"
class LinkPalette(Palette):
displayname = "Link Palette"
class HeartBeep(Choice):
displayname = "Heart Beep Rate"
option_normal = 0
option_double = 1
option_half = 2,
option_quarter = 3
option_off = 4
class HeartColor(Choice):
displayname = "Heart Color"
option_red = 0
option_blue = 1
option_green = 2
option_yellow = 3
class QuickSwap(DefaultOnToggle):
displayname = "L/R Quickswapping"
class MenuSpeed(Choice):
displayname = "Menu Speed"
option_normal = 0
option_instant = 1,
option_double = 2
option_triple = 3
option_quadruple = 4
option_half = 5
class Music(DefaultOnToggle):
displayname = "Play music"
class ReduceFlashing(DefaultOnToggle):
displayname = "Reduce Screen Flashes"
class TriforceHud(Choice):
displayname = "Display Method for Triforce Hunt"
option_normal = 0
option_hide_goal = 1
option_hide_required = 2
option_hide_both = 3
alttp_options: typing.Dict[str, type(Option)] = { alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon, "crystals_needed_for_ganon": CrystalsGanon,
"progressive": Progressive,
"shop_item_slots": ShopItemSlots, "shop_item_slots": ShopItemSlots,
"ow_palettes": OWPalette,
"uw_palettes": UWPalette,
"hud_palettes": HUDPalette,
"sword_palettes": SwordPalette,
"shield_palettes": ShieldPalette,
"link_palettes": LinkPalette,
"heartbeep": HeartBeep,
"heartcolor": HeartColor,
"quickswap": QuickSwap,
"menuspeed": MenuSpeed,
"music": Music,
"reduceflashing": ReduceFlashing,
"triforcehud": TriforceHud
} }

View File

@ -279,11 +279,11 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette) rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli, output_directory): def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_directory):
check_enemizer(enemizercli) check_enemizer(enemizercli)
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{team}_{player}.sfc')) randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{team}_{player}.json')) options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{team}_{player}.sfc')) enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc'))
# write options file for enemizer # write options file for enemizer
options = { options = {
@ -756,7 +756,7 @@ def get_nonnative_item_sprite(item: str) -> int:
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886 # https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
def patch_rom(world, rom, player, team, enemized): def patch_rom(world, rom, player, enemized):
local_random = world.slot_seeds[player] local_random = world.slot_seeds[player]
# progressive bow silver arrow hint hack # progressive bow silver arrow hint hack
@ -1645,7 +1645,7 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x4BA1D, tile_set.get_len()) rom.write_byte(0x4BA1D, tile_set.get_len())
rom.write_bytes(0x4BA2A, tile_set.get_bytes()) rom.write_bytes(0x4BA2A, tile_set.get_bytes())
write_strings(rom, world, player, team) write_strings(rom, world, player)
# remote items flag, does not currently work # remote items flag, does not currently work
rom.write_byte(0x18637C, int(world.worlds[player].remote_items)) rom.write_byte(0x18637C, int(world.worlds[player].remote_items))
@ -1654,13 +1654,13 @@ def patch_rom(world, rom, player, team, enemized):
# 21 bytes # 21 bytes
from Main import __version__ from Main import __version__
# TODO: Adjust Enemizer to accept AP and AD # TODO: Adjust Enemizer to accept AP and AD
rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{team + 1}_{player}_{world.seed:09}\0', 'utf8')[:21] rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name))) rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name) rom.write_bytes(0x7FC0, rom.name)
# set player names # set player names
for p in range(1, min(world.players, 255) + 1): for p in range(1, min(world.players, 255) + 1):
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_names[p][team])) rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p]))
# Write title screen Code # Write title screen Code
hashint = int(rom.get_hash(), 16) hashint = int(rom.get_hash(), 16)
@ -1756,13 +1756,13 @@ def hud_format_text(text):
return output[:32] return output[:32]
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite: str, palettes_options, def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False, world=None, player=1, allow_random_on_event=False, reduceflashing=False,
triforcehud: str = None): triforcehud: str = None):
local_random = random if not world else world.slot_seeds[player] local_random = random if not world else world.slot_seeds[player]
disable_music: bool = music
# enable instant item menu # enable instant item menu
if fastmenu == 'instant': if menuspeed == 'instant':
rom.write_byte(0x6DD9A, 0x20) rom.write_byte(0x6DD9A, 0x20)
rom.write_byte(0x6DF2A, 0x20) rom.write_byte(0x6DF2A, 0x20)
rom.write_byte(0x6E0E9, 0x20) rom.write_byte(0x6E0E9, 0x20)
@ -1770,15 +1770,15 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
rom.write_byte(0x6DD9A, 0x11) rom.write_byte(0x6DD9A, 0x11)
rom.write_byte(0x6DF2A, 0x12) rom.write_byte(0x6DF2A, 0x12)
rom.write_byte(0x6E0E9, 0x12) rom.write_byte(0x6E0E9, 0x12)
if fastmenu == 'instant': if menuspeed == 'instant':
rom.write_byte(0x180048, 0xE8) rom.write_byte(0x180048, 0xE8)
elif fastmenu == 'double': elif menuspeed == 'double':
rom.write_byte(0x180048, 0x10) rom.write_byte(0x180048, 0x10)
elif fastmenu == 'triple': elif menuspeed == 'triple':
rom.write_byte(0x180048, 0x18) rom.write_byte(0x180048, 0x18)
elif fastmenu == 'quadruple': elif menuspeed == 'quadruple':
rom.write_byte(0x180048, 0x20) rom.write_byte(0x180048, 0x20)
elif fastmenu == 'half': elif menuspeed == 'half':
rom.write_byte(0x180048, 0x04) rom.write_byte(0x180048, 0x04)
else: else:
rom.write_byte(0x180048, 0x08) rom.write_byte(0x180048, 0x08)
@ -1854,7 +1854,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
while True: while True:
yield ColorF(local_random.random(), local_random.random(), local_random.random()) yield ColorF(local_random.random(), local_random.random(), local_random.random())
if mode == 'random': if mode == 'good':
mode = 'maseya' mode = 'maseya'
z3pr.randomize(rom.buffer, mode, offset_collections=offsets_array, random_colors=next_color_generator()) z3pr.randomize(rom.buffer, mode, offset_collections=offsets_array, random_colors=next_color_generator())
@ -2075,7 +2075,7 @@ def write_string_to_rom(rom, target, string):
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes)) rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
def write_strings(rom, world, player, team): def write_strings(rom, world, player):
local_random = world.slot_seeds[player] local_random = world.slot_seeds[player]
tt = TextTable() tt = TextTable()
@ -2098,11 +2098,11 @@ def write_strings(rom, world, player, team):
hint = dest.hint_text if dest.hint_text else "something" hint = dest.hint_text if dest.hint_text else "something"
if dest.player != player: if dest.player != player:
if ped_hint: if ped_hint:
hint += f" for {world.player_names[dest.player][team]}!" hint += f" for {world.player_name[dest.player]}!"
elif type(dest) in [Region, ALttPLocation]: elif type(dest) in [Region, ALttPLocation]:
hint += f" in {world.player_names[dest.player][team]}'s world" hint += f" in {world.player_name[dest.player]}'s world"
else: else:
hint += f" for {world.player_names[dest.player][team]}" hint += f" for {world.player_name[dest.player]}"
return hint return hint
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances. # For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.

View File

@ -118,7 +118,7 @@ def mirrorless_path_to_castle_courtyard(world, player):
else: else:
queue.append((entrance.connected_region, new_path)) queue.append((entrance.connected_region, new_path))
raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_names(player)})") raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_name(player)})")
def set_defeat_dungeon_boss_rule(location): def set_defeat_dungeon_boss_rule(location):

View File

@ -1,5 +1,7 @@
import random import random
import logging import logging
import os
import threading
from BaseClasses import Item, CollectionState from BaseClasses import Item, CollectionState
from .SubClasses import ALttPItem from .SubClasses import ALttPItem
@ -11,6 +13,8 @@ from .Rules import set_rules
from .ItemPool import generate_itempool from .ItemPool import generate_itempool
from .Shops import create_shops from .Shops import create_shops
from .Dungeons import create_dungeons from .Dungeons import create_dungeons
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string
import Patch
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
@ -37,6 +41,8 @@ class ALTTPWorld(World):
create_items = generate_itempool create_items = generate_itempool
def create_regions(self): def create_regions(self):
self.rom_name_available_event = threading.Event()
player = self.player player = self.player
world = self.world world = self.world
if world.open_pyramid[player] == 'goal': if world.open_pyramid[player] == 'goal':
@ -175,6 +181,67 @@ class ALTTPWorld(World):
from .Dungeons import fill_dungeons_restrictive from .Dungeons import fill_dungeons_restrictive
fill_dungeons_restrictive(world) fill_dungeons_restrictive(world)
def generate_output(self, output_directory: str):
world = self.world
player = self.player
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.shufflepots[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
rom = LocalRom(world.alttp_rom)
patch_rom(world, rom, player, use_enemizer)
if use_enemizer:
patch_enemizer(world, player, rom, world.enemizer, output_directory)
if world.is_race:
patch_race_rom(rom, world, player)
world.spoiler.hashes[player] = get_hash_string(rom.hash)
palettes_options = {
'dungeon': world.uw_palettes[player],
'overworld': world.ow_palettes[player],
'hud': world.hud_palettes[player],
'sword': world.sword_palettes[player],
'shield': world.shield_palettes[player],
'link': world.link_palettes[player]
}
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
apply_rom_settings(rom, world.heartbeep[player].current_key,
world.heartcolor[player].current_key,
world.quickswap[player],
world.menuspeed[player].current_key,
world.music[player],
world.sprite[player],
palettes_options, world, player, True,
reduceflashing=world.reduceflashing[player] or world.is_race,
triforcehud=world.triforcehud[player].current_key)
outfilepname = f'_P{player}'
outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \
if world.player_name[player] != 'Player%d' % player else ''
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
rom.write_to_file(rompath, hide_enemizer=True)
Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player])
os.unlink(rompath)
self.rom_name = rom.name
self.rom_name_available_event.set()
def modify_multidata(self, multidata: dict):
import base64
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
new_name = base64.b64encode(bytes(self.rom_name)).decode()
payload = multidata["connect_names"][self.world.player_name[self.player]]
multidata["connect_names"][new_name] = payload
del (multidata["connect_names"][self.world.player_name[self.player]])
def get_required_client_version(self) -> tuple: def get_required_client_version(self) -> tuple:
return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version()) return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version())

View File

@ -57,12 +57,12 @@ def generate_mod(world, output_directory: str):
locale_template = template_env.get_template(r"locale/en/locale.cfg") locale_template = template_env.get_template(r"locale/en/locale.cfg")
control_template = template_env.get_template("control.lua") control_template = template_env.get_template("control.lua")
# get data for templates # get data for templates
player_names = {x: multiworld.player_names[x][0] for x in multiworld.player_ids} player_names = {x: multiworld.player_name[x] for x in multiworld.player_ids}
locations = [] locations = []
for location in multiworld.get_filled_locations(player): for location in multiworld.get_filled_locations(player):
if location.address: if location.address:
locations.append((location.name, location.item.name, location.item.player, location.item.advancement)) locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_names[player][0]}" mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_name[player]}"
tech_cost_scale = {0: 0.1, tech_cost_scale = {0: 0.1,
1: 0.25, 1: 0.25,
2: 0.5, 2: 0.5,
@ -87,7 +87,7 @@ def generate_mod(world, output_directory: str):
"mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), "mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies, "tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player], "tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
"slot_name": multiworld.player_names[player][0], "seed_name": multiworld.seed_name, "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
"starting_items": multiworld.starting_items[player], "recipes": recipes, "starting_items": multiworld.starting_items[player], "recipes": recipes,
"random": random, "flop_random": flop_random, "random": random, "flop_random": flop_random,
"static_nodes": multiworld.worlds[player].static_nodes, "static_nodes": multiworld.worlds[player].static_nodes,

View File

@ -103,14 +103,14 @@ class RecipeTime(Choice):
class Progressive(Choice): class Progressive(Choice):
displayname = "Progressive Technologies" displayname = "Progressive Technologies"
option_off = 0 option_off = 0
option_random = 1 option_grouped_random = 1
option_on = 2 option_on = 2
alias_false = 0 alias_false = 0
alias_true = 2 alias_true = 2
default = 2 default = 2
def want_progressives(self, random): def want_progressives(self, random):
return random.choice([True, False]) if self.value == self.option_random else int(self.value) return random.choice([True, False]) if self.value == self.option_grouped_random else int(self.value)
class RecipeIngredients(Choice): class RecipeIngredients(Choice):

View File

@ -13,7 +13,7 @@ def link_minecraft_structures(world, player):
try: try:
assert len(exits) == len(structs) assert len(exits) == len(structs)
except AssertionError as e: # this should never happen except AssertionError as e: # this should never happen
raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_names[player]})") raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_name[player]})")
pairs = {} pairs = {}
@ -23,7 +23,7 @@ def link_minecraft_structures(world, player):
exits.remove(exit) exits.remove(exit)
structs.remove(struct) structs.remove(struct)
else: else:
raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({world.player_names[player]})") raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({world.player_name[player]})")
# Connect plando structures first # Connect plando structures first
if world.plando_connections[player]: if world.plando_connections[player]:
@ -38,7 +38,7 @@ def link_minecraft_structures(world, player):
try: try:
exit = world.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) exit = world.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])])
except IndexError: except IndexError:
raise Exception(f"No valid structure placements remaining for player {player} ({world.player_names[player]})") raise Exception(f"No valid structure placements remaining for player {player} ({world.player_name[player]})")
set_pair(exit, struct) set_pair(exit, struct)
else: # write remaining default connections else: # write remaining default connections
for (exit, struct) in default_connections: for (exit, struct) in default_connections:
@ -49,7 +49,7 @@ def link_minecraft_structures(world, player):
try: try:
assert len(exits) == len(structs) == 0 assert len(exits) == len(structs) == 0
except AssertionError: except AssertionError:
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_names[player]})") raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_name[player]})")
for exit in exits_spoiler: for exit in exits_spoiler:
world.get_entrance(exit, player).connect(world.get_region(pairs[exit], player)) world.get_entrance(exit, player).connect(world.get_region(pairs[exit], player))

View File

@ -43,7 +43,7 @@ class MinecraftLogic(LogicMixin):
# Difficulty-dependent functions # Difficulty-dependent functions
def _mc_combat_difficulty(self, player: int): def _mc_combat_difficulty(self, player: int):
return self.world.combat_difficulty[player].get_current_option_name().lower() return self.world.combat_difficulty[player].current_key
def _mc_can_adventure(self, player: int): def _mc_can_adventure(self, player: int):
if self._mc_combat_difficulty(player) == 'easy': if self._mc_combat_difficulty(player) == 'easy':

View File

@ -30,7 +30,7 @@ class MinecraftWorld(World):
return { return {
'world_seed': self.world.slot_seeds[self.player].getrandbits(32), 'world_seed': self.world.slot_seeds[self.player].getrandbits(32),
'seed_name': self.world.seed_name, 'seed_name': self.world.seed_name,
'player_name': self.world.get_player_names(self.player), 'player_name': self.world.get_player_name(self.player),
'player_id': self.player, 'player_id': self.player,
'client_version': client_version, 'client_version': client_version,
'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits}, 'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits},
@ -95,7 +95,7 @@ class MinecraftWorld(World):
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
data = self._get_mc_data() data = self._get_mc_data()
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_names(self.player)}.apmc" filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}.apmc"
with open(os.path.join(output_directory, filename), 'wb') as f: with open(os.path.join(output_directory, filename), 'wb') as f:
f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) f.write(b64encode(bytes(json.dumps(data), 'utf-8')))