From ba3bb201cdaec9d77c46e7afa592ca53f7b97679 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 9 Aug 2021 09:15:41 +0200 Subject: [PATCH] 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 --- BaseClasses.py | 37 ++++------ Fill.py | 2 +- Generate.py | 27 ++----- LttPAdjuster.py | 42 +++++------ Main.py | 107 +++++++-------------------- Options.py | 16 ++++- Utils.py | 20 +----- WebHostLib/generate.py | 2 - host.yaml | 3 - playerSettings.yaml | 16 ++--- worlds/AutoWorld.py | 14 +++- worlds/alttp/EntranceRandomizer.py | 54 ++------------ worlds/alttp/EntranceShuffle.py | 2 +- worlds/alttp/ItemPool.py | 18 +++-- worlds/alttp/Options.py | 112 ++++++++++++++++++++++++++++- worlds/alttp/Rom.py | 42 +++++------ worlds/alttp/Rules.py | 2 +- worlds/alttp/__init__.py | 67 +++++++++++++++++ worlds/factorio/Mod.py | 6 +- worlds/factorio/Options.py | 4 +- worlds/minecraft/Regions.py | 8 +-- worlds/minecraft/Rules.py | 2 +- worlds/minecraft/__init__.py | 4 +- 23 files changed, 328 insertions(+), 279 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7f456d89..e3bca8fe 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -13,7 +13,7 @@ import random class MultiWorld(): debug_types = False - player_names: Dict[int, List[str]] + player_name: Dict[int, str] _region_cache: Dict[int, Dict[str, Region]] difficulty_requirements: dict required_medallions: dict @@ -36,7 +36,6 @@ class MultiWorld(): def __init__(self, players: int): self.random = random.Random() # world-local random state is saved for multiple generations running concurrently self.players = players - self.teams = 1 self.glitch_triforce = False self.algorithm = 'balanced' self.dungeons = [] @@ -83,11 +82,9 @@ class MultiWorld(): set_player_attr('item_functionality', 'normal') set_player_attr('timer', False) set_player_attr('goal', 'ganon') - set_player_attr('progressive', 'on') set_player_attr('accessibility', 'items') set_player_attr('retro', False) set_player_attr('hints', True) - set_player_attr('player_names', []) set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('swamp_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) 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: - return ", ".join(self.player_names[player]) + def get_player_name(self, player: int) -> str: + return self.player_name[player] def initialize_regions(self, regions=None): for region in regions if regions else self.regions: @@ -174,7 +171,7 @@ class MultiWorld(): @functools.cached_property 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): """Rebuild world cache""" @@ -1132,8 +1129,8 @@ class Spoiler(): def parse_data(self): self.medallions = OrderedDict() 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'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1] + 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_name(player)})'] = self.world.required_medallions[player][1] self.startinventory = list(map(str, self.world.precollected_items)) @@ -1241,7 +1238,6 @@ class Spoiler(): 'progressive': self.world.progressive, 'shufflepots': self.world.shufflepots, 'players': self.world.players, - 'teams': self.world.teams, 'progression_balancing': self.world.progression_balancing, 'triforce_pieces_available': self.world.triforce_pieces_available, 'triforce_pieces_required': self.world.triforce_pieces_required, @@ -1261,7 +1257,7 @@ class Spoiler(): out['Starting Inventory'] = self.startinventory out['Special'] = self.medallions 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: out['Shops'] = self.shops out['playthrough'] = self.playthrough @@ -1286,10 +1282,10 @@ class Spoiler(): self.metadata['version'], self.world.seed)) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) 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): 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]) if self.world.players > 1: outfile.write('Progression Balanced: %s\n' % ( @@ -1303,11 +1299,7 @@ class Spoiler(): outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n') 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' % ( - 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('%s%s\n' % ('Hash: ', self.hashes[player])) outfile.write('Logic: %s\n' % self.metadata['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]) outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][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]) if self.metadata['shuffle'][player] != "vanilla": outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player]) @@ -1369,7 +1360,7 @@ class Spoiler(): self.metadata['shuffle_prizes'][player]) if self.entrances: 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 entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', @@ -1383,7 +1374,7 @@ class Spoiler(): if factorio_players: outfile.write('\n\nRecipes:\n') 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(): 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"): if self.world.boss_shuffle[player] != 'none': 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\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()])) diff --git a/Fill.py b/Fill.py index 18d43a4f..b77f0d05 100644 --- a/Fill.py +++ b/Fill.py @@ -444,4 +444,4 @@ def distribute_planned(world: MultiWorld): except ValueError: placement.warn(f"Could not remove {item} from pool as it's already missing from it.") 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 diff --git a/Generate.py b/Generate.py index 797cb3ef..97295ae4 100644 --- a/Generate.py +++ b/Generate.py @@ -40,7 +40,6 @@ def mystery_argparse(): help="Input directory for player files.") 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('--teams', default=1, type=lambda value: max(int(value), 1)) 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('--enemizercli', default=defaults["enemizer_path"]) @@ -128,7 +127,6 @@ def main(args=None, callback=ERmain): erargs.skip_playthrough = args.spoiler < 2 erargs.outputname = seed_name erargs.outputpath = args.outputpath - erargs.teams = args.teams # set up logger if args.log_level: @@ -179,6 +177,8 @@ def main(args=None, callback=ERmain): 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 except Exception as e: raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e 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] = 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: import yaml important = {} @@ -267,7 +265,7 @@ def handle_name(name: str, player: int, name_counter: Counter): name] > 1 else ''), player=player, PLAYER=(player if player > 1 else ''))) - new_name = new_name.strip().replace(' ', '_')[:16] + new_name = new_name.strip()[:16] if new_name == "Archipelago": raise Exception(f"You cannot name yourself \"{new_name}\"") return new_name @@ -610,7 +608,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.enemy_damage = {None: 'default', 'default': 'default', 'shuffled': 'shuffled', - 'random': 'chaos' + 'random': 'chaos', # to be removed + 'chaos': 'chaos', }[get_choice('enemy_damage', 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.progressive = convert_to_on_off(get_choice('progressive', weights, 'on')) - ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g") 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: 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__': import atexit diff --git a/LttPAdjuster.py b/LttPAdjuster.py index f8b800e5..cb37e9f5 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -44,7 +44,7 @@ def main(): help='Path to an ALttP JAP(1.0) rom to use as a base.') parser.add_argument('--loglevel', default='info', const='info', nargs='?', 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'], help='''\ 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('--update_sprites', action='store_true', help='Update Sprite Database, then exit.') args = parser.parse_args() + args.music = not args.disablemusic if args.update_sprites: run_sprite_update() sys.exit() @@ -150,7 +151,7 @@ def adjust(args): if hasattr(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) path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc') rom.write_to_file(path) @@ -195,14 +196,14 @@ def adjustGUI(): guiargs = Namespace() guiargs.heartbeep = rom_vars.heartbeepVar.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.uw_palettes = rom_vars.uwPalettesVar.get() guiargs.hud_palettes = rom_vars.hudPalettesVar.get() guiargs.sword_palettes = rom_vars.swordPalettesVar.get() guiargs.shield_palettes = rom_vars.shieldPalettesVar.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.rom = romVar2.get() guiargs.baserom = romVar.get() @@ -439,9 +440,10 @@ def get_rom_options_frame(parent=None): romOptionsFrame.rowconfigure(i, weight=1) vars = Namespace() - vars.disableMusicVar = IntVar() - disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=vars.disableMusicVar) - disableMusicCheckbutton.grid(row=0, column=0, sticky=E) + vars.MusicVar = IntVar() + vars.MusicVar.set(1) + MusicCheckbutton = Checkbutton(romOptionsFrame, text="Music", variable=vars.MusicVar) + MusicCheckbutton.grid(row=0, column=0, sticky=E) vars.disableFlashingVar = IntVar(value=1) 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.grid(row=1, column=0, sticky=E) - fastMenuFrame = Frame(romOptionsFrame) - fastMenuFrame.grid(row=1, column=1, sticky=E) - fastMenuLabel = Label(fastMenuFrame, text='Menu speed') - fastMenuLabel.pack(side=LEFT) - vars.fastMenuVar = StringVar() - vars.fastMenuVar.set('normal') - fastMenuOptionMenu = OptionMenu(fastMenuFrame, vars.fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half') - fastMenuOptionMenu.pack(side=LEFT) + menuspeedFrame = Frame(romOptionsFrame) + menuspeedFrame.grid(row=1, column=1, sticky=E) + menuspeedLabel = Label(menuspeedFrame, text='Menu speed') + menuspeedLabel.pack(side=LEFT) + vars.menuspeedVar = StringVar() + vars.menuspeedVar.set('normal') + menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half') + menuspeedOptionMenu.pack(side=LEFT) heartcolorFrame = Frame(romOptionsFrame) heartcolorFrame.grid(row=2, column=0, sticky=E) @@ -518,7 +520,7 @@ def get_rom_options_frame(parent=None): owPalettesLabel.pack(side=LEFT) vars.owPalettesVar = StringVar() 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) uwPalettesFrame = Frame(romOptionsFrame) @@ -527,7 +529,7 @@ def get_rom_options_frame(parent=None): uwPalettesLabel.pack(side=LEFT) vars.uwPalettesVar = StringVar() 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) hudPalettesFrame = Frame(romOptionsFrame) @@ -536,7 +538,7 @@ def get_rom_options_frame(parent=None): hudPalettesLabel.pack(side=LEFT) vars.hudPalettesVar = StringVar() 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) swordPalettesFrame = Frame(romOptionsFrame) @@ -545,7 +547,7 @@ def get_rom_options_frame(parent=None): swordPalettesLabel.pack(side=LEFT) vars.swordPalettesVar = StringVar() 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) shieldPalettesFrame = Frame(romOptionsFrame) @@ -554,7 +556,7 @@ def get_rom_options_frame(parent=None): shieldPalettesLabel.pack(side=LEFT) vars.shieldPalettesVar = StringVar() 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) spritePoolFrame = Frame(romOptionsFrame) diff --git a/Main.py b/Main.py index 2de97265..77ac4c44 100644 --- a/Main.py +++ b/Main.py @@ -10,17 +10,15 @@ import tempfile import zipfile 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.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 worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots -from worlds.alttp.ItemPool import difficulties, fill_prizes -from Utils import output_path, parse_player_names, get_options, __version__, version_tuple +from worlds.alttp.ItemPool import difficulties +from Utils import output_path, get_options, __version__, version_tuple from worlds.generic.Rules import locality_rules, exclusion_rules from worlds import AutoWorld -import Patch seeddigits = 20 @@ -66,7 +64,6 @@ def main(args, seed=None): world.difficulty = args.difficulty.copy() world.item_functionality = args.item_functionality.copy() world.timer = args.timer.copy() - world.progressive = args.progressive.copy() world.goal = args.goal.copy() world.local_items = args.local_items.copy() 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.green_clock_time = args.green_clock_time.copy() world.shufflepots = args.shufflepots.copy() - world.progressive = args.progressive.copy() world.dungeon_counters = args.dungeon_counters.copy() world.glitch_boots = args.glitch_boots.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.game = args.game.copy() 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.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(): 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('') 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.') 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() output = tempfile.TemporaryDirectory() with output as temp_dir: check_accessibility_task = pool.submit(world.fulfills_accessibility) - rom_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: 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): for entrance in region.entrances: @@ -365,12 +313,8 @@ def main(args, seed=None): FillDisabledShopSlots(world) - def write_multidata(roms, outputs): - import base64 + def write_multidata(): import NetUtils - for future in roms: - rom_name = future.result() - rom_names.append(rom_name) slot_data = {} client_versions = {} minimum_versions = {"server": (0, 1, 1), "clients": client_versions} @@ -378,8 +322,6 @@ def main(args, seed=None): for slot in world.player_ids: client_versions[slot] = world.worlds[slot].get_required_client_version() 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)} for item in world.precollected_items: precollected_items[item.player].append(item.code) @@ -390,11 +332,6 @@ def main(args, seed=None): if world.tech_tree_information[player].value == 2: 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: 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.item.player].add(hint) - multidata = zlib.compress(pickle.dumps({ + multidata = { "slot_data": slot_data, "games": games, - "names": parsed_names, - "connect_names": connect_names, + "names": [{player: name for player, name in world.player_name.items()}], + "connect_names": {name: (0, player) for player, name in world.player_name.items()}, "remote_items": {player for player in world.player_ids if world.worlds[player].remote_items}, "locations": locations_data, @@ -431,15 +368,17 @@ def main(args, seed=None): "tags": ["AP"], "minimum_versions": minimum_versions, "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(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 world.can_beat_game(): raise Exception("Game appears as unbeatable. Aborting.") @@ -451,8 +390,10 @@ def main(args, seed=None): if not args.skip_playthrough: logger.info('Calculating playthrough.') 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)) + for future in output_file_futures: + future.result() zipfilename = output_path(f"AP_{world.seed_name}.zip") logger.info(f'Creating final archive at {zipfilename}.') with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, diff --git a/Options.py b/Options.py index 21a7b911..a8f662ac 100644 --- a/Options.py +++ b/Options.py @@ -11,9 +11,11 @@ class AssembleOptions(type): for base in bases: if hasattr(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 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()}) options.update(new_options) @@ -47,7 +49,12 @@ class Option(metaclass=AssembleOptions): def __hash__(self): return hash(self.value) + @property + def current_key(self) -> str: + return self.name_lookup[self.value] + def get_current_option_name(self) -> str: + """For display purposes.""" return self.get_option_name(self.value) def get_option_name(self, value: typing.Any) -> str: @@ -122,8 +129,13 @@ class Choice(Option): @classmethod 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(): - if optionname == text.lower(): + if optionname == text: return cls(value) raise KeyError( f'Could not find option "{text}" for "{cls.__name__}", ' diff --git a/Utils.py b/Utils.py index 75b64876..fcb44431 100644 --- a/Utils.py +++ b/Utils.py @@ -51,24 +51,6 @@ def snes_to_pc(value): 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): if function.__code__.co_argcount: 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.baserom = Patch.get_base_rom_path() adjuster_settings.world = None - whitelist = {"disablemusic", "fastmenu", "heartbeep", "heartcolor", "ow_palettes", "quickswap", + whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", "uw_palettes", "sprite"} printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist} if hasattr(adjuster_settings, "sprite_pool"): diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 0c270cc7..99af0d39 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -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] = 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) return upload_to_db(target.name, owner, sid, race) diff --git a/host.yaml b/host.yaml index d0f54a09..8687110f 100644 --- a/host.yaml +++ b/host.yaml @@ -45,9 +45,6 @@ server_options: log_network: 0 # Options for Generation 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 enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe" # Folder from which the player yaml files are pulled from diff --git a/playerSettings.yaml b/playerSettings.yaml index 24bf07f8..bacf12a0 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -106,7 +106,7 @@ Factorio: progressive: on: 1 off: 0 - random: 0 + grouped_random: 0 tech_tree_information: none: 0 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) on: 50 # All 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: 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 @@ -596,7 +596,7 @@ A Link to the Past: off: 0 ow_palettes: # Change the colors of the overworld 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 grayscale: 0 negative: 0 @@ -606,7 +606,7 @@ A Link to the Past: puke: 0 uw_palettes: # Change the colors of caves and dungeons 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 grayscale: 0 negative: 0 @@ -616,7 +616,7 @@ A Link to the Past: puke: 0 hud_palettes: # Change the colors of the hud 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 grayscale: 0 negative: 0 @@ -626,7 +626,7 @@ A Link to the Past: puke: 0 sword_palettes: # Change the colors of swords 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 grayscale: 0 negative: 0 @@ -636,7 +636,7 @@ A Link to the Past: puke: 0 shield_palettes: # Change the colors of shields 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 grayscale: 0 negative: 0 @@ -693,7 +693,7 @@ linked_options: singularity: 1 enemy_damage: shuffled: 1 - random: 1 + chaos: 1 enemy_health: easy: 1 hard: 1 diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index ffbe9c1f..71e64c35 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -51,7 +51,15 @@ def call_all(world: MultiWorld, method_name: str, *args): for world_type in world_types: stage_callable = getattr(world_type, f"stage_{method_name}", None) 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): @@ -127,6 +135,10 @@ class World(metaclass=AutoWorldRegister): """Fill in the slot_data field in the Connected network package.""" return {} + def modify_multidata(self, multidata: dict): + """For deeper modification of server multidata.""" + pass + def get_required_client_version(self) -> Tuple[int, int, int]: return 0, 0, 3 diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index 6dcc0a1b..79d7ce00 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -143,20 +143,7 @@ def parse_arguments(argv, no_defaults=False): off. 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='?', choices=['freshness', 'flood', 'vt25', 'vt26', 'balanced'], help='''\ @@ -218,22 +205,7 @@ def parse_arguments(argv, no_defaults=False): --seed given will produce the same 10 (different) roms each time). ''', 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), help='Maps are no longer restricted to their dungeons, but can be anywhere', 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 included entrance shuffle pool. ''', 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='''\ 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', 'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer', 'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots', - 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', - 'heartbeep', "progression_balancing", "triforce_pieces_available", + 'sprite', + "progression_balancing", "triforce_pieces_available", "triforce_pieces_required", "shop_shuffle", "required_medallions", "start_hints", "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', - 'restrict_dungeon_item_on_boss', 'reduceflashing', 'game', - 'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes', 'triforcehud']: + 'restrict_dungeon_item_on_boss', 'game']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index 62e7dd45..2a4514bc 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1064,7 +1064,7 @@ def link_entrances(world, player): connect_doors(world, single_doors, door_targets, player) else: 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 if world.logic[player] in ['hybridglitches', 'nologic']: diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 4dfa75eb..acecb67b 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -399,7 +399,7 @@ def generate_itempool(world): if additional_triforce_pieces: if additional_triforce_pieces > len(nonprogressionitems): 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 nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool nonprogressionitems = nonprogressionitems[additional_triforce_pieces:] @@ -563,16 +563,14 @@ def get_pool_core(world, player: int): assert loc not in placed_items 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 if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt': precollected_items.append('Pegasus Boots') pool.remove('Pegasus Boots') pool.append('Rupees (20)') + want_progressives = world.progressive[player].want_progressives - if want_progressives(): + if want_progressives(world.random): pool.extend(diff.progressiveglove) else: pool.extend(diff.basicglove) @@ -599,22 +597,22 @@ def get_pool_core(world, player: int): thisbottle = world.random.choice(diff.bottles) pool.append(thisbottle) - if want_progressives(): + if want_progressives(world.random): pool.extend(diff.progressiveshield) else: pool.extend(diff.basicshield) - if want_progressives(): + if want_progressives(world.random): pool.extend(diff.progressivearmor) else: pool.extend(diff.basicarmor) - if want_progressives(): + if want_progressives(world.random): pool.extend(diff.progressivemagic) else: pool.extend(diff.basicmagic) - if want_progressives(): + if want_progressives(world.random): pool.extend(diff.progressivebow) elif (swordless or logic == 'noglitches') and goal != 'icerodhunt': swordless_bows = ['Bow', 'Silver Bow'] @@ -627,7 +625,7 @@ def get_pool_core(world, player: int): if swordless: pool.extend(diff.swordless) else: - progressive_swords = want_progressives() + progressive_swords = want_progressives(world.random) pool.extend(diff.progressivesword if progressive_swords else diff.basicsword) extraitems = total_items_to_place - len(pool) - len(placed_items) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index cdfe36cc..1ead4010 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,6 +1,6 @@ import typing -from Options import Choice, Range, Option +from Options import Choice, Range, Option, Toggle, DefaultOnToggle class Logic(Choice): @@ -70,8 +70,116 @@ class Enemies(Choice): option_shuffled = 1 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)] = { "crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_ganon": CrystalsGanon, + "progressive": Progressive, "shop_item_slots": ShopItemSlots, -} \ No newline at end of file + "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 + +} diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index f7bdca5e..f7af250a 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -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) -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) - randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{team}_{player}.sfc')) - options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{team}_{player}.json')) - enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{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_{player}.json')) + enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc')) # write options file for enemizer options = { @@ -756,7 +756,7 @@ def get_nonnative_item_sprite(item: str) -> int: # 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] # 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_bytes(0x4BA2A, tile_set.get_bytes()) - write_strings(rom, world, player, team) + write_strings(rom, world, player) # remote items flag, does not currently work rom.write_byte(0x18637C, int(world.worlds[player].remote_items)) @@ -1654,13 +1654,13 @@ def patch_rom(world, rom, player, team, enemized): # 21 bytes from Main import __version__ # 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.write_bytes(0x7FC0, rom.name) # set player names 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 hashint = int(rom.get_hash(), 16) @@ -1756,13 +1756,13 @@ def hud_format_text(text): 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, triforcehud: str = None): local_random = random if not world else world.slot_seeds[player] - + disable_music: bool = music # enable instant item menu - if fastmenu == 'instant': + if menuspeed == 'instant': rom.write_byte(0x6DD9A, 0x20) rom.write_byte(0x6DF2A, 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(0x6DF2A, 0x12) rom.write_byte(0x6E0E9, 0x12) - if fastmenu == 'instant': + if menuspeed == 'instant': rom.write_byte(0x180048, 0xE8) - elif fastmenu == 'double': + elif menuspeed == 'double': rom.write_byte(0x180048, 0x10) - elif fastmenu == 'triple': + elif menuspeed == 'triple': rom.write_byte(0x180048, 0x18) - elif fastmenu == 'quadruple': + elif menuspeed == 'quadruple': rom.write_byte(0x180048, 0x20) - elif fastmenu == 'half': + elif menuspeed == 'half': rom.write_byte(0x180048, 0x04) else: rom.write_byte(0x180048, 0x08) @@ -1854,7 +1854,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr while True: yield ColorF(local_random.random(), local_random.random(), local_random.random()) - if mode == 'random': + if mode == 'good': mode = 'maseya' 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)) -def write_strings(rom, world, player, team): +def write_strings(rom, world, player): local_random = world.slot_seeds[player] tt = TextTable() @@ -2098,11 +2098,11 @@ def write_strings(rom, world, player, team): hint = dest.hint_text if dest.hint_text else "something" if dest.player != player: 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]: - hint += f" in {world.player_names[dest.player][team]}'s world" + hint += f" in {world.player_name[dest.player]}'s world" else: - hint += f" for {world.player_names[dest.player][team]}" + hint += f" for {world.player_name[dest.player]}" return hint # For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances. diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 49d06400..08b25bb3 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -118,7 +118,7 @@ def mirrorless_path_to_castle_courtyard(world, player): else: 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): diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index d1de3c36..f93ba2a1 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -1,5 +1,7 @@ import random import logging +import os +import threading from BaseClasses import Item, CollectionState from .SubClasses import ALttPItem @@ -11,6 +13,8 @@ from .Rules import set_rules from .ItemPool import generate_itempool from .Shops import create_shops 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 .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect @@ -37,6 +41,8 @@ class ALTTPWorld(World): create_items = generate_itempool def create_regions(self): + self.rom_name_available_event = threading.Event() + player = self.player world = self.world if world.open_pyramid[player] == 'goal': @@ -175,6 +181,67 @@ class ALTTPWorld(World): from .Dungeons import fill_dungeons_restrictive 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: return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version()) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index b4e4e500..79f36cf4 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -57,12 +57,12 @@ def generate_mod(world, output_directory: str): locale_template = template_env.get_template(r"locale/en/locale.cfg") control_template = template_env.get_template("control.lua") # 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 = [] for location in multiworld.get_filled_locations(player): if location.address: 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, 1: 0.25, 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(), "tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies, "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, "random": random, "flop_random": flop_random, "static_nodes": multiworld.worlds[player].static_nodes, diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index dfd600f0..6f42deb2 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -103,14 +103,14 @@ class RecipeTime(Choice): class Progressive(Choice): displayname = "Progressive Technologies" option_off = 0 - option_random = 1 + 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_random else int(self.value) + return random.choice([True, False]) if self.value == self.option_grouped_random else int(self.value) class RecipeIngredients(Choice): diff --git a/worlds/minecraft/Regions.py b/worlds/minecraft/Regions.py index 682ad032..8257ac81 100644 --- a/worlds/minecraft/Regions.py +++ b/worlds/minecraft/Regions.py @@ -13,7 +13,7 @@ def link_minecraft_structures(world, player): try: assert len(exits) == len(structs) 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 = {} @@ -23,7 +23,7 @@ def link_minecraft_structures(world, player): exits.remove(exit) structs.remove(struct) 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 if world.plando_connections[player]: @@ -38,7 +38,7 @@ def link_minecraft_structures(world, player): try: exit = world.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) 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) else: # write remaining default connections for (exit, struct) in default_connections: @@ -49,7 +49,7 @@ def link_minecraft_structures(world, player): try: assert len(exits) == len(structs) == 0 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: world.get_entrance(exit, player).connect(world.get_region(pairs[exit], player)) diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index 17584b31..ae220394 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -43,7 +43,7 @@ class MinecraftLogic(LogicMixin): # Difficulty-dependent functions 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): if self._mc_combat_difficulty(player) == 'easy': diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 5c293a66..c576c4f8 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -30,7 +30,7 @@ class MinecraftWorld(World): return { 'world_seed': self.world.slot_seeds[self.player].getrandbits(32), '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, 'client_version': client_version, '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): 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: f.write(b64encode(bytes(json.dumps(data), 'utf-8')))