From 109eb5b9dca7ae8ea32047bb489dae5066251093 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 13 May 2021 01:34:59 +0200 Subject: [PATCH 01/34] start of split --- Main.py | 22 +++++++++++----------- test_loader.py | 2 ++ worlds/BaseWorld.py | 13 +++++++++++++ worlds/factorio/__init__.py | 18 ++++++++++++++++++ worlds/loader.py | 12 ++++++++++++ 5 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 test_loader.py create mode 100644 worlds/BaseWorld.py create mode 100644 worlds/loader.py diff --git a/Main.py b/Main.py index cfa9116f34..03100949e3 100644 --- a/Main.py +++ b/Main.py @@ -24,8 +24,8 @@ from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple from worlds.hk import gen_hollow from worlds.hk import create_regions as hk_create_regions -from worlds.factorio import gen_factorio, factorio_create_regions -from worlds.factorio.Mod import generate_mod +# from worlds.factorio import gen_factorio, factorio_create_regions +# from worlds.factorio.Mod import generate_mod from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data from worlds.minecraft.Regions import minecraft_create_regions from worlds.generic.Rules import locality_rules @@ -135,8 +135,8 @@ def main(args, seed=None): import Options for hk_option in Options.hollow_knight_options: setattr(world, hk_option, getattr(args, hk_option, {})) - for factorio_option in Options.factorio_options: - setattr(world, factorio_option, getattr(args, factorio_option, {})) + # for factorio_option in Options.factorio_options: + # setattr(world, factorio_option, getattr(args, factorio_option, {})) for minecraft_option in Options.minecraft_options: setattr(world, minecraft_option, getattr(args, minecraft_option, {})) world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. @@ -207,8 +207,8 @@ def main(args, seed=None): for player in world.hk_player_ids: hk_create_regions(world, player) - for player in world.factorio_player_ids: - factorio_create_regions(world, player) + # for player in world.factorio_player_ids: + # factorio_create_regions(world, player) for player in world.minecraft_player_ids: minecraft_create_regions(world, player) @@ -269,8 +269,8 @@ def main(args, seed=None): for player in world.hk_player_ids: gen_hollow(world, player) - for player in world.factorio_player_ids: - gen_factorio(world, player) + # for player in world.factorio_player_ids: + # gen_factorio(world, player) for player in world.minecraft_player_ids: gen_minecraft(world, player) @@ -423,9 +423,9 @@ def main(args, seed=None): for team in range(world.teams): for player in world.alttp_player_ids: rom_futures.append(pool.submit(_gen_rom, team, player)) - for player in world.factorio_player_ids: - mod_futures.append(pool.submit(generate_mod, world, player, - str(args.outputname if args.outputname else world.seed))) + # for player in world.factorio_player_ids: + # mod_futures.append(pool.submit(generate_mod, world, player, + # str(args.outputname if args.outputname else world.seed))) def get_entrance_to_region(region: Region): for entrance in region.entrances: diff --git a/test_loader.py b/test_loader.py new file mode 100644 index 0000000000..1ee0621e92 --- /dev/null +++ b/test_loader.py @@ -0,0 +1,2 @@ +import worlds.loader +print(worlds.loader.world_types) \ No newline at end of file diff --git a/worlds/BaseWorld.py b/worlds/BaseWorld.py new file mode 100644 index 0000000000..34c3ef8901 --- /dev/null +++ b/worlds/BaseWorld.py @@ -0,0 +1,13 @@ +class AutoWorldRegister(type): + _world_types = {} + + def __new__(cls, name, bases, dct): + new_class = super().__new__(cls, name, bases, dct) + AutoWorldRegister._world_types[name] = new_class + return new_class + +class World(metaclass=AutoWorldRegister): + """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. + A Game should have its own subclass of World in which it defines the required data structures.""" + def __init__(self): + pass \ No newline at end of file diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index f0ded67dc4..4cd8464c63 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,7 +1,25 @@ +from ..BaseWorld import World + + from BaseClasses import Region, Entrance, Location, MultiWorld, Item from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, required_technologies from .Shapes import get_shapes +class Factorio(World): + def generate_basic(self, world: MultiWorld, player: int): + static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option? + for tech_name, tech_id in tech_table.items(): + tech_item = Item(tech_name, tech_name in advancement_technologies, tech_id, player) + tech_item.game = "Factorio" + if tech_name in static_nodes: + loc = world.get_location(tech_name, player) + loc.item = tech_item + loc.locked = True + loc.event = tech_item.advancement + else: + world.itempool.append(tech_item) + world.custom_data[player]["custom_technologies"] = custom_technologies = set_custom_technologies(world, player) + set_rules(world, player, custom_technologies) def gen_factorio(world: MultiWorld, player: int): static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option? diff --git a/worlds/loader.py b/worlds/loader.py new file mode 100644 index 0000000000..52c1349df2 --- /dev/null +++ b/worlds/loader.py @@ -0,0 +1,12 @@ +import importlib +import os +world_types = [] +world_folder = os.path.dirname(__file__) +for entry in os.scandir(world_folder): + if entry.is_dir(): + entryname = entry.name + if not entryname.startswith("_"): + world_module = importlib.import_module("."+entry.name, package="worlds") + world_types.append(world_module) +print(world_folder) +print(world_types) \ No newline at end of file From 5ea03c71c01b34eb2d37b981b1cbea6d802d2618 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 8 Jun 2021 21:58:11 +0200 Subject: [PATCH 02/34] start moving some alttp options over to the new system --- BaseClasses.py | 12 +++------- Gui.py | 2 +- Main.py | 13 ++++------- Mystery.py | 14 ++++++----- Options.py | 37 +++++++++++++++++++++++++----- playerSettings.yaml | 6 ++--- worlds/alttp/EntranceRandomizer.py | 25 ++------------------ worlds/alttp/Rom.py | 4 ++-- worlds/alttp/Shops.py | 2 +- 9 files changed, 55 insertions(+), 60 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0f5e5e6b48..37a420683a 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -113,8 +113,6 @@ class MultiWorld(): set_player_attr('bush_shuffle', False) set_player_attr('beemizer', 0) set_player_attr('escape_assist', []) - set_player_attr('crystals_needed_for_ganon', 7) - set_player_attr('crystals_needed_for_gt', 7) set_player_attr('open_pyramid', False) set_player_attr('treasure_hunt_icon', 'Triforce Piece') set_player_attr('treasure_hunt_count', 0) @@ -131,7 +129,6 @@ class MultiWorld(): set_player_attr('triforce_pieces_available', 30) set_player_attr('triforce_pieces_required', 20) set_player_attr('shop_shuffle', 'off') - set_player_attr('shop_shuffle_slots', 0) set_player_attr('shuffle_prizes', "g") set_player_attr('sprite_pool', []) set_player_attr('dark_room_logic', "lamp") @@ -141,9 +138,6 @@ class MultiWorld(): set_player_attr('plando_connections', []) set_player_attr('game', "A Link to the Past") set_player_attr('completion_condition', lambda state: True) - import Options - for hk_option in Options.hollow_knight_options: - set_player_attr(hk_option, False) self.custom_data = {} for player in range(1, players+1): self.custom_data[player] = {} @@ -1448,7 +1442,7 @@ class Spoiler(object): 'triforce_pieces_available': self.world.triforce_pieces_available, 'triforce_pieces_required': self.world.triforce_pieces_required, 'shop_shuffle': self.world.shop_shuffle, - 'shop_shuffle_slots': self.world.shop_shuffle_slots, + 'shop_item_slots': self.world.shop_item_slots, 'shuffle_prizes': self.world.shuffle_prizes, 'sprite_pool': self.world.sprite_pool, 'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss, @@ -1565,8 +1559,8 @@ class Spoiler(object): "f" in self.metadata["shop_shuffle"][player])) outfile.write('Custom Potion Shop: %s\n' % bool_to_text("w" in self.metadata["shop_shuffle"][player])) - outfile.write('Shop Slots: %s\n' % - self.metadata["shop_shuffle_slots"][player]) + outfile.write('Shop Item Slots: %s\n' % + self.metadata["shop_item_slots"][player]) outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) outfile.write( 'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player])) diff --git a/Gui.py b/Gui.py index 73f5788250..0dcbd479dc 100755 --- a/Gui.py +++ b/Gui.py @@ -468,7 +468,7 @@ def guiMain(args=None): if shopWitchShuffleVar.get(): guiargs.shop_shuffle += "w" if shopPoolShuffleVar.get(): - guiargs.shop_shuffle_slots = 30 + guiargs.shop_item_slots = 30 guiargs.shuffle_prizes = {"none": "", "bonk": "b", "general": "g", diff --git a/Main.py b/Main.py index 0eeee31268..ca76c7fe84 100644 --- a/Main.py +++ b/Main.py @@ -94,8 +94,6 @@ def main(args, seed=None): world.compassshuffle = args.compassshuffle.copy() world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() - world.crystals_needed_for_ganon = args.crystals_ganon.copy() - world.crystals_needed_for_gt = args.crystals_gt.copy() world.open_pyramid = args.open_pyramid.copy() world.boss_shuffle = args.shufflebosses.copy() world.enemy_shuffle = args.enemy_shuffle.copy() @@ -117,7 +115,6 @@ def main(args, seed=None): world.triforce_pieces_available = args.triforce_pieces_available.copy() world.triforce_pieces_required = args.triforce_pieces_required.copy() world.shop_shuffle = args.shop_shuffle.copy() - world.shop_shuffle_slots = args.shop_shuffle_slots.copy() world.progression_balancing = args.progression_balancing.copy() world.shuffle_prizes = args.shuffle_prizes.copy() world.sprite_pool = args.sprite_pool.copy() @@ -130,12 +127,10 @@ def main(args, seed=None): world.required_medallions = args.required_medallions.copy() world.game = args.game.copy() import Options - for hk_option in Options.hollow_knight_options: - setattr(world, hk_option, getattr(args, hk_option, {})) - for factorio_option in Options.factorio_options: - setattr(world, factorio_option, getattr(args, factorio_option, {})) - for minecraft_option in Options.minecraft_options: - setattr(world, minecraft_option, getattr(args, minecraft_option, {})) + for option_set in Options.option_sets: + for option in option_set: + setattr(world, option, getattr(args, option, {})) + world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)} diff --git a/Mystery.py b/Mystery.py index a4fd82f21d..b1818f0056 100644 --- a/Mystery.py +++ b/Mystery.py @@ -581,6 +581,12 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): + for option_name, option in Options.alttp_options.items(): + if option_name in weights: + setattr(ret, option_name, option.from_any(get_choice(option_name, weights))) + else: + setattr(ret, option_name, option(option.default)) + glitches_required = get_choice('glitches_required', weights) if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']: logging.warning("Only NMG, OWG and No Logic supported") @@ -632,12 +638,9 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): # fast ganon + ganon at hole ret.open_pyramid = get_choice('open_pyramid', weights, 'goal') - ret.crystals_gt = Options.Crystals.from_any(get_choice('tower_open', weights)).value - ret.crystals_ganon = Options.Crystals.from_any(get_choice('ganon_open', weights)).value - extra_pieces = get_choice('triforce_pieces_mode', weights, 'available') - ret.triforce_pieces_required = Options.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights)).value + ret.triforce_pieces_required = Options.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights)) # sum a percentage to required if extra_pieces == 'percentage': @@ -645,7 +648,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0)) # vanilla mode (specify how many pieces are) elif extra_pieces == 'available': - ret.triforce_pieces_available = Options.TriforcePieces.from_any(get_choice('triforce_pieces_available', weights)).value + ret.triforce_pieces_available = Options.TriforcePieces.from_any(get_choice('triforce_pieces_available', weights)) # required pieces + fixed extra elif extra_pieces == 'extra': extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10))) @@ -653,7 +656,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): # change minimum to required pieces to avoid problems ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90) - ret.shop_shuffle_slots = Options.TriforcePieces.from_any(get_choice('shop_shuffle_slots', weights)).value ret.shop_shuffle = get_choice('shop_shuffle', weights, '') if not ret.shop_shuffle: diff --git a/Options.py b/Options.py index 5200bed07b..92213f9641 100644 --- a/Options.py +++ b/Options.py @@ -8,8 +8,9 @@ class AssembleOptions(type): options = attrs["options"] = {} name_lookup = attrs["name_lookup"] = {} for base in bases: - options.update(base.options) - name_lookup.update(name_lookup) + if hasattr(base, "options"): + options.update(base.options) + name_lookup.update(name_lookup) new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if name.startswith("option_")} attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) @@ -110,7 +111,7 @@ class Choice(Option): return cls.from_text(str(data)) -class Range(Option): +class Range(Option, int): range_start = 0 range_end = 1 @@ -119,7 +120,7 @@ class Range(Option): raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}") elif value > self.range_end: raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__}") - self.value: int = value + self.value = value @classmethod def from_text(cls, text: str) -> Range: @@ -139,6 +140,9 @@ class Range(Option): return cls(data) return cls.from_text(str(data)) + def __str__(self): + return str(self.value) + class OptionNameSet(Option): default = frozenset() @@ -210,12 +214,20 @@ class Crystals(Range): range_end = 7 +class CrystalsTower(Crystals): + pass + + +class CrystalsGanon(Crystals): + default = 7 + + class TriforcePieces(Range): range_start = 1 range_end = 90 -class ShopShuffleSlots(Range): +class ShopItemSlots(Range): range_start = 0 range_end = 30 @@ -240,6 +252,12 @@ class Enemies(Choice): option_chaos = 2 +alttp_options: typing.Dict[str, type(Option)] = { + "crystals_needed_for_gt": CrystalsTower, + "crystals_needed_for_ganon": CrystalsGanon, + "shop_item_slots": ShopItemSlots, +} + mapshuffle = Toggle compassshuffle = Toggle keyshuffle = Toggle @@ -268,7 +286,7 @@ RandomizeLoreTablets = Toggle RandomizeLifebloodCocoons = Toggle RandomizeFlames = Toggle -hollow_knight_randomize_options: typing.Dict[str, Option] = { +hollow_knight_randomize_options: typing.Dict[str, type(Option)] = { "RandomizeDreamers": RandomizeDreamers, "RandomizeSkills": RandomizeSkills, "RandomizeCharms": RandomizeCharms, @@ -409,6 +427,13 @@ minecraft_options: typing.Dict[str, type(Option)] = { "shuffle_structures": Toggle } +option_sets = ( + minecraft_options, + factorio_options, + alttp_options, + hollow_knight_options +) + if __name__ == "__main__": import argparse diff --git a/playerSettings.yaml b/playerSettings.yaml index 2bc168c4f8..3f09f9d8c5 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -220,7 +220,7 @@ triforce_pieces_required: # Set to how many out of X triforce pieces you need to 30: 0 40: 0 50: 0 -tower_open: # Crystals required to open GT +crystals_needed_for_gt: # Crystals required to open GT 0: 0 1: 0 2: 0 @@ -232,7 +232,7 @@ tower_open: # Crystals required to open GT random: 0 random-low: 50 # any valid number, weighted towards the lower end random-high: 0 -ganon_open: # Crystals required to hurt Ganon +crystals_needed_for_ganon: # Crystals required to hurt Ganon 0: 0 1: 0 2: 0 @@ -317,7 +317,7 @@ beemizer: # Remove items from the global item pool and replace them with single 4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees 5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees ### Shop Settings ### -shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl) +shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl) 0: 50 5: 0 15: 0 diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index 2673a41af7..e0e0dcf945 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -196,22 +196,6 @@ def parse_arguments(argv, no_defaults=False): The dungeon variants only mix up dungeons and keep the rest of the overworld vanilla. ''') - parser.add_argument('--crystals_ganon', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'], - help='''\ - How many crystals are needed to defeat ganon. Any other - requirements for ganon for the selected goal still apply. - This setting does not apply when the all dungeons goal is - selected. (default: %(default)s) - Random: Picks a random value between 0 and 7 (inclusive). - 0-7: Number of crystals needed - ''') - parser.add_argument('--crystals_gt', default=defval('7'), const='7', nargs='?', - choices=['0', '1', '2', '3', '4', '5', '6', '7'], - help='''\ - How many crystals are needed to open GT. For inverted mode - this applies to the castle tower door instead. (default: %(default)s) - 0-7: Number of crystals needed - ''') parser.add_argument('--open_pyramid', default=defval('auto'), help='''\ Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it. Depending on goal, you might still need to beat Agahnim 2 in order to beat ganon. @@ -337,11 +321,6 @@ def parse_arguments(argv, no_defaults=False): u: shuffle capacity upgrades into the item pool w: consider witch's hut like any other shop and shuffle/randomize it too ''') - parser.add_argument('--shop_shuffle_slots', default=defval(0), - type=lambda value: min(max(int(value), 1), 96), - help=''' - Maximum amount of shop slots able to be filled by items from the item pool. - ''') parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb']) parser.add_argument('--sprite_pool', help='''\ Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''') @@ -397,14 +376,14 @@ def parse_arguments(argv, no_defaults=False): playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True) for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality', - 'shuffle', 'crystals_ganon', 'crystals_gt', 'open_pyramid', 'timer', + 'shuffle', 'open_pyramid', 'timer', 'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time', '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", - "triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots", + "triforce_pieces_required", "shop_shuffle", "required_medallions", "start_hints", "plando_items", "plando_texts", "plando_connections", "er_seeds", 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 92ed48f42f..dcc906cd99 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -885,7 +885,7 @@ def patch_rom(world, rom, player, team, enemized): credits_total = 216 if world.retro[player]: # Old man cave and Take any caves will count towards collection rate. credits_total += 5 - if world.shop_shuffle_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle. + if world.shop_item_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle. credits_total += 30 if 'w' in world.shop_shuffle[player] else 27 rom.write_byte(0x187010, credits_total) # dynamic credits @@ -1705,7 +1705,7 @@ def write_custom_shops(rom, world, player): slot = 0 if shop.type == ShopType.TakeAny else index if item is None: break - if world.shop_shuffle_slots[player] or shop.type == ShopType.TakeAny: + if world.shop_item_slots[player] or shop.type == ShopType.TakeAny: count_shop = (shop.region.name != 'Potion Shop' or 'w' in world.shop_shuffle[player]) and \ shop.region.name != 'Capacity Upgrade' rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0) diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 3514d48acf..64cdafc275 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -243,7 +243,7 @@ def create_shops(world, player: int): else: dynamic_shop_slots = total_dynamic_shop_slots - num_slots = min(dynamic_shop_slots, max(0, int(world.shop_shuffle_slots[player]))) # 0 to 30 + num_slots = min(dynamic_shop_slots, world.shop_item_slots[player]) single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots) world.random.shuffle(single_purchase_slots) From 1e414dd370d223465e3f77c768060678c6a3b1b9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 8 Jun 2021 22:14:56 +0200 Subject: [PATCH 03/34] fix tests --- Options.py | 2 +- test/dungeons/TestDungeon.py | 3 +++ test/hollow_knight/__init__.py | 4 +++- test/inverted/TestInverted.py | 4 +++- test/inverted_minor_glitches/TestInvertedMinor.py | 4 +++- test/inverted_owg/TestInvertedOWG.py | 3 +++ test/minor_glitches/TestMinor.py | 4 +++- test/owg/TestVanillaOWG.py | 3 +++ test/vanilla/TestVanilla.py | 4 +++- 9 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Options.py b/Options.py index 92213f9641..b0bdb76711 100644 --- a/Options.py +++ b/Options.py @@ -215,7 +215,7 @@ class Crystals(Range): class CrystalsTower(Crystals): - pass + default = 7 class CrystalsGanon(Crystals): diff --git a/test/dungeons/TestDungeon.py b/test/dungeons/TestDungeon.py index ea8a87aec8..7483009dab 100644 --- a/test/dungeons/TestDungeon.py +++ b/test/dungeons/TestDungeon.py @@ -8,11 +8,14 @@ from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import create_regions from worlds.alttp.Shops import create_shops from worlds.alttp.Rules import set_rules +from Options import alttp_options class TestDungeon(unittest.TestCase): def setUp(self): self.world = MultiWorld(1) + for option_name, option in alttp_options.items(): + setattr(self.world, option_name, {1: option.default}) self.starting_regions = [] # Where to start exploring self.remove_exits = [] # Block dungeon exits self.world.difficulty_requirements[1] = difficulties['normal'] diff --git a/test/hollow_knight/__init__.py b/test/hollow_knight/__init__.py index 63212fdf90..39a155d032 100644 --- a/test/hollow_knight/__init__.py +++ b/test/hollow_knight/__init__.py @@ -11,6 +11,8 @@ class TestVanilla(TestBase): self.world.game[1] = "Hollow Knight" import Options for hk_option in Options.hollow_knight_randomize_options: - getattr(self.world, hk_option)[1] = True + setattr(self.world, hk_option, {1: True}) + for hk_option, option in Options.hollow_knight_skip_options.items(): + setattr(self.world, hk_option, {1: option.default}) create_regions(self.world, 1) gen_hollow(self.world, 1) \ No newline at end of file diff --git a/test/inverted/TestInverted.py b/test/inverted/TestInverted.py index 08f3c8cd8a..f3a716cc9f 100644 --- a/test/inverted/TestInverted.py +++ b/test/inverted/TestInverted.py @@ -8,11 +8,13 @@ from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops from worlds.alttp.Rules import set_rules from test.TestBase import TestBase - +from Options import alttp_options class TestInverted(TestBase): def setUp(self): self.world = MultiWorld(1) + for option_name, option in alttp_options.items(): + setattr(self.world, option_name, {1: option.default}) self.world.difficulty_requirements[1] = difficulties['normal'] self.world.mode[1] = "inverted" create_inverted_regions(self.world, 1) diff --git a/test/inverted_minor_glitches/TestInvertedMinor.py b/test/inverted_minor_glitches/TestInvertedMinor.py index e28d18c2ee..41e93c6e5b 100644 --- a/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/test/inverted_minor_glitches/TestInvertedMinor.py @@ -8,11 +8,13 @@ from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops from worlds.alttp.Rules import set_rules from test.TestBase import TestBase - +from Options import alttp_options class TestInvertedMinor(TestBase): def setUp(self): self.world = MultiWorld(1) + for option_name, option in alttp_options.items(): + setattr(self.world, option_name, {1: option.default}) self.world.mode[1] = "inverted" self.world.logic[1] = "minorglitches" self.world.difficulty_requirements[1] = difficulties['normal'] diff --git a/test/inverted_owg/TestInvertedOWG.py b/test/inverted_owg/TestInvertedOWG.py index 5dd3fa6e2c..1a2c377a65 100644 --- a/test/inverted_owg/TestInvertedOWG.py +++ b/test/inverted_owg/TestInvertedOWG.py @@ -8,11 +8,14 @@ from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops from worlds.alttp.Rules import set_rules from test.TestBase import TestBase +from Options import alttp_options class TestInvertedOWG(TestBase): def setUp(self): self.world = MultiWorld(1) + for option_name, option in alttp_options.items(): + setattr(self.world, option_name, {1: option.default}) self.world.logic[1] = "owglitches" self.world.mode[1] = "inverted" self.world.difficulty_requirements[1] = difficulties['normal'] diff --git a/test/minor_glitches/TestMinor.py b/test/minor_glitches/TestMinor.py index c7efa5ffd7..b573bac198 100644 --- a/test/minor_glitches/TestMinor.py +++ b/test/minor_glitches/TestMinor.py @@ -8,11 +8,13 @@ from worlds.alttp.Regions import create_regions from worlds.alttp.Shops import create_shops from worlds.alttp.Rules import set_rules from test.TestBase import TestBase - +from Options import alttp_options class TestMinor(TestBase): def setUp(self): self.world = MultiWorld(1) + for option_name, option in alttp_options.items(): + setattr(self.world, option_name, {1: option.default}) self.world.logic[1] = "minorglitches" self.world.difficulty_requirements[1] = difficulties['normal'] create_regions(self.world, 1) diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index 824d72f865..a4082b31dd 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -8,11 +8,14 @@ from worlds.alttp.Regions import create_regions from worlds.alttp.Shops import create_shops from worlds.alttp.Rules import set_rules from test.TestBase import TestBase +from Options import alttp_options class TestVanillaOWG(TestBase): def setUp(self): self.world = MultiWorld(1) + for option_name, option in alttp_options.items(): + setattr(self.world, option_name, {1: option.default}) self.world.difficulty_requirements[1] = difficulties['normal'] self.world.logic[1] = "owglitches" create_regions(self.world, 1) diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index a5d53e1b47..5e1dde2bba 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -8,11 +8,13 @@ from worlds.alttp.Regions import create_regions from worlds.alttp.Shops import create_shops from worlds.alttp.Rules import set_rules from test.TestBase import TestBase - +from Options import alttp_options class TestVanilla(TestBase): def setUp(self): self.world = MultiWorld(1) + for option_name, option in alttp_options.items(): + setattr(self.world, option_name, {1: option.default}) self.world.logic[1] = "noglitches" self.world.difficulty_requirements[1] = difficulties['normal'] create_regions(self.world, 1) From 534dd331b93c62274acfc4d4d9df7fcb73c496aa Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 9 Jun 2021 10:13:18 +0200 Subject: [PATCH 04/34] document item locality options properly --- playerSettings.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/playerSettings.yaml b/playerSettings.yaml index 3f09f9d8c5..7c3e4fc45d 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -43,6 +43,13 @@ progression_balancing: # Arrow Upgrade (+10): 4 # start_hints: # Begin the game with these items' locations revealed to you at the start of the game. Get the info via !hint in your client. # - Moon Pearl +# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords" +# - "Moon Pearl" +# - "Small Keys" +# - "Big Keys" +# non_local_items: # Force certain items to appear outside your world only, unless in single-player. Recognizes some group names, like "Swords" +# - "Progressive Weapons" + # Factorio options: tech_tree_layout: single: 1 @@ -364,11 +371,6 @@ green_clock_time: # For all timer modes, the amount of time in minutes to gain o 4: 50 10: 0 15: 0 -# Can be uncommented to use it -# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords" -# - "Moon Pearl" -# - "Small Keys" -# - "Big Keys" glitch_boots: on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them off: 0 From 568a71cdbe648d685a32698b127985ab3307531f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 11 Jun 2021 14:22:44 +0200 Subject: [PATCH 05/34] Start implementing object oriented scaffold for world types (There's still a lot of work ahead, such as: registering locations and items to the World, as well as methods to create_item_from_name() many more method names for various stages embedding Options into the world type and many more...) --- BaseClasses.py | 7 +- Main.py | 181 +++++++++++++++++++---------------- Options.py | 31 ++++++ worlds/AutoWorld.py | 37 +++++++ worlds/BaseWorld.py | 13 --- worlds/alttp/__init__.py | 92 +----------------- worlds/factorio/Shapes.py | 2 +- worlds/factorio/__init__.py | 76 +++++++-------- worlds/hk/__init__.py | 3 + worlds/minecraft/__init__.py | 35 ++++--- 10 files changed, 233 insertions(+), 244 deletions(-) create mode 100644 worlds/AutoWorld.py delete mode 100644 worlds/BaseWorld.py diff --git a/BaseClasses.py b/BaseClasses.py index 37a420683a..1b4cfcc7c0 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -32,7 +32,7 @@ class MultiWorld(): return self.rule(player) def __init__(self, players: int): - # TODO: move per-player settings into new classes per game-type instead of clumping it all together here + from worlds import AutoWorld self.random = random.Random() # world-local random state is saved for multiple generations running concurrently self.players = players @@ -139,11 +139,10 @@ class MultiWorld(): set_player_attr('game', "A Link to the Past") set_player_attr('completion_condition', lambda state: True) self.custom_data = {} + self.worlds = {} for player in range(1, players+1): self.custom_data[player] = {} - # self.worlds = [] - # for i in range(players): - # self.worlds.append(worlds.alttp.ALTTPWorld({}, i)) + self.worlds[player] = AutoWorld.AutoWorldRegister.world_types[self.game[player]](player) def secure(self): self.random = secrets.SystemRandom() diff --git a/Main.py b/Main.py index 78d6fd1a52..c3f2b56763 100644 --- a/Main.py +++ b/Main.py @@ -24,12 +24,10 @@ from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple from worlds.hk import gen_hollow from worlds.hk import create_regions as hk_create_regions -# from worlds.factorio import gen_factorio, factorio_create_regions -# from worlds.factorio.Mod import generate_mod from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data from worlds.minecraft.Regions import minecraft_create_regions from worlds.generic.Rules import locality_rules -from worlds import Games, lookup_any_item_name_to_id +from worlds import Games, lookup_any_item_name_to_id, AutoWorld import Patch seeddigits = 20 @@ -79,7 +77,7 @@ def main(args, seed=None): world.progressive = args.progressive.copy() world.goal = args.goal.copy() world.local_items = args.local_items.copy() - if hasattr(args, "algorithm"): # current GUI options + if hasattr(args, "algorithm"): # current GUI options world.algorithm = args.algorithm world.shuffleganon = args.shuffleganon world.custom = args.custom @@ -128,13 +126,14 @@ def main(args, seed=None): world.game = args.game.copy() import Options for option_set in Options.option_sets: - # for option in option_set: - # setattr(world, option, getattr(args, option, {})) + for option in option_set: + setattr(world, option, getattr(args, option, {})) world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. - world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)} + world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in + range(1, world.players + 1)} - for player in range(1, world.players+1): + for player in range(1, world.players + 1): world.er_seeds[player] = str(world.random.randint(0, 2 ** 64)) if "-" in world.shuffle[player]: @@ -144,7 +143,8 @@ def main(args, seed=None): world.er_seeds[player] = "vanilla" elif seed.startswith("group-") or args.race: # renamed from team to group to not confuse with existing team name use - world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player])) + world.er_seeds[player] = get_same_seed(world, ( + shuffle, seed, world.retro[player], world.mode[player], world.logic[player])) else: # not a race or group seed, use set seed as is. world.er_seeds[player] = seed elif world.shuffle[player] == "vanilla": @@ -152,6 +152,10 @@ def main(args, seed=None): logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) + logger.info("Found World Types:") + for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): + logger.info(f" {name:30} {cls}") + parsed_names = parse_player_names(args.names, world.players, args.teams) world.teams = len(parsed_names) for i, team in enumerate(parsed_names, 1): @@ -199,23 +203,26 @@ def main(args, seed=None): for player in world.hk_player_ids: hk_create_regions(world, player) - # for player in world.factorio_player_ids: - # factorio_create_regions(world, player) + AutoWorld.call_all(world, "create_regions") for player in world.minecraft_player_ids: minecraft_create_regions(world, player) for player in world.alttp_player_ids: if world.open_pyramid[player] == 'goal': - world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} + world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', + 'localganontriforcehunt', 'ganonpedestal'} elif world.open_pyramid[player] == 'auto': - world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \ - (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not world.shuffle_ganon) + world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', + 'localganontriforcehunt', 'ganonpedestal'} and \ + (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', + 'dungeonscrossed'} or not world.shuffle_ganon) else: - world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player]) + world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get( + world.open_pyramid[player], world.open_pyramid[player]) - - world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player]) + world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], + world.triforce_pieces_required[player]) if world.mode[player] != 'inverted': create_regions(world, player) @@ -261,8 +268,7 @@ def main(args, seed=None): for player in world.hk_player_ids: gen_hollow(world, player) - # for player in world.factorio_player_ids: - # gen_factorio(world, player) + AutoWorld.call_all(world, "generate_basic") for player in world.minecraft_player_ids: gen_minecraft(world, player) @@ -327,13 +333,13 @@ def main(args, seed=None): world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash) - palettes_options={} - palettes_options['dungeon']=args.uw_palettes[player] - palettes_options['overworld']=args.ow_palettes[player] - palettes_options['hud']=args.hud_palettes[player] - palettes_options['sword']=args.sword_palettes[player] - palettes_options['shield']=args.shield_palettes[player] - palettes_options['link']=args.link_palettes[player] + palettes_options = {} + palettes_options['dungeon'] = args.uw_palettes[player] + palettes_options['overworld'] = args.ow_palettes[player] + palettes_options['hud'] = args.hud_palettes[player] + palettes_options['sword'] = args.sword_palettes[player] + palettes_options['shield'] = args.shield_palettes[player] + palettes_options['link'] = args.link_palettes[player] apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], @@ -349,8 +355,8 @@ def main(args, seed=None): world.bigkeyshuffle[player]].count(True) == 1: mcsb_name = '-mapshuffle' if world.mapshuffle[player] else \ '-compassshuffle' if world.compassshuffle[player] else \ - '-universal_keys' if world.keyshuffle[player] == "universal" else \ - '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle' + '-universal_keys' if world.keyshuffle[player] == "universal" else \ + '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle' elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]): mcsb_name = '-%s%s%s%sshuffle' % ( @@ -362,46 +368,46 @@ def main(args, seed=None): outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \ if world.player_names[player][team] != 'Player%d' % player else '' outfilestuffs = { - "logic": world.logic[player], # 0 - "difficulty": world.difficulty[player], # 1 - "item_functionality": world.item_functionality[player], # 2 - "mode": world.mode[player], # 3 - "goal": world.goal[player], # 4 - "timer": str(world.timer[player]), # 5 - "shuffle": world.shuffle[player], # 6 - "algorithm": world.algorithm, # 7 - "mscb": mcsb_name, # 8 - "retro": world.retro[player], # 9 - "progressive": world.progressive, # A - "hints": 'True' if world.hints[player] else 'False' # B + "logic": world.logic[player], # 0 + "difficulty": world.difficulty[player], # 1 + "item_functionality": world.item_functionality[player], # 2 + "mode": world.mode[player], # 3 + "goal": world.goal[player], # 4 + "timer": str(world.timer[player]), # 5 + "shuffle": world.shuffle[player], # 6 + "algorithm": world.algorithm, # 7 + "mscb": mcsb_name, # 8 + "retro": world.retro[player], # 9 + "progressive": world.progressive, # A + "hints": 'True' if world.hints[player] else 'False' # B } # 0 1 2 3 4 5 6 7 8 9 A B outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % ( - # 0 1 2 3 4 5 6 7 8 9 A B C - # _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints - # _noglitches_normal-normal-open-ganon _simple-balanced-keysanity-retro - # _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random - # _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints - outfilestuffs["logic"], # 0 + # 0 1 2 3 4 5 6 7 8 9 A B C + # _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints + # _noglitches_normal-normal-open-ganon _simple-balanced-keysanity-retro + # _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random + # _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints + outfilestuffs["logic"], # 0 - outfilestuffs["difficulty"], # 1 - outfilestuffs["item_functionality"], # 2 - outfilestuffs["mode"], # 3 - outfilestuffs["goal"], # 4 - "" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5 + outfilestuffs["difficulty"], # 1 + outfilestuffs["item_functionality"], # 2 + outfilestuffs["mode"], # 3 + outfilestuffs["goal"], # 4 + "" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5 - outfilestuffs["shuffle"], # 6 - outfilestuffs["algorithm"], # 7 - outfilestuffs["mscb"], # 8 + outfilestuffs["shuffle"], # 6 + outfilestuffs["algorithm"], # 7 + outfilestuffs["mscb"], # 8 - "-retro" if outfilestuffs["retro"] == "True" else "", # 9 - "-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A - "-nohints" if not outfilestuffs["hints"] == "True" else "") # B - ) if not args.outputname else '' + "-retro" if outfilestuffs["retro"] == "True" else "", # 9 + "-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A + "-nohints" if not outfilestuffs["hints"] == "True" else "") # B + ) if not args.outputname else '' rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc') rom.write_to_file(rompath, hide_enemizer=True) if args.create_diff: - Patch.create_patch_file(rompath, player=player, player_name = world.player_names[player][team]) + Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team]) return player, team, bytes(rom.name) pool = concurrent.futures.ThreadPoolExecutor() @@ -409,12 +415,12 @@ def main(args, seed=None): check_accessibility_task = pool.submit(world.fulfills_accessibility) rom_futures = [] - mod_futures = [] + output_file_futures = [] for team in range(world.teams): for player in world.alttp_player_ids: rom_futures.append(pool.submit(_gen_rom, team, player)) - for player in world.factorio_player_ids: - mod_futures.append(pool.submit(generate_mod, world, player)) + for player in world.player_ids: + output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player)) def get_entrance_to_region(region: Region): for entrance in region.entrances: @@ -424,7 +430,8 @@ def main(args, seed=None): return get_entrance_to_region(entrance.parent_region) # collect ER hint info - er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla" or world.retro[player]} + er_hint_data = {player: {} for player in range(1, world.players + 1) if + world.shuffle[player] != "vanilla" or world.retro[player]} from worlds.alttp.Regions import RegionType for region in world.regions: if region.player in er_hint_data and region.locations: @@ -450,7 +457,7 @@ def main(args, seed=None): checks_in_area[location.player]["Light World"].append(location.address) elif location.parent_region.dungeon: dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', - 'Inverted Ganons Tower': 'Ganons Tower'}\ + 'Inverted Ganons Tower': 'Ganons Tower'} \ .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) checks_in_area[location.player][dungeonname].append(location.address) elif main_entrance.parent_region.type == RegionType.LightWorld: @@ -462,8 +469,10 @@ def main(args, seed=None): oldmancaves = [] takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] for index, take_any in enumerate(takeanyregions): - for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if world.retro[player]]: - item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player) + for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if + world.retro[player]]: + item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], + region.player) player = region.player location_id = SHOP_ID_START + total_shop_slots + index @@ -477,11 +486,9 @@ def main(args, seed=None): er_hint_data[player][location_id] = main_entrance.name oldmancaves.append(((location_id, player), (item.code, player))) - - FillDisabledShopSlots(world) - def write_multidata(roms, mods): + def write_multidata(roms, outputs): import base64 import NetUtils for future in roms: @@ -498,11 +505,11 @@ def main(args, seed=None): client_versions[slot] = (0, 0, 3) games[slot] = world.game[slot] connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for - slot, team, rom_name in rom_names} - precollected_items = {player: [] for player in range(1, world.players+1)} + 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) - precollected_hints = {player: set() for player in range(1, world.players+1)} + precollected_hints = {player: set() for player in range(1, world.players + 1)} # for now special case Factorio visibility sending_visible_players = set() for player in world.factorio_player_ids: @@ -538,7 +545,7 @@ def main(args, seed=None): precollected_hints[location.item.player].add(hint) multidata = zlib.compress(pickle.dumps({ - "slot_data" : slot_data, + "slot_data": slot_data, "games": games, "names": parsed_names, "connect_names": connect_names, @@ -559,10 +566,10 @@ def main(args, seed=None): with open(output_path('%s.archipelago' % outfilebase), 'wb') as f: f.write(bytes([1])) # version of format f.write(multidata) - for future in mods: - future.result() # collect errors if they occured + for future in outputs: + future.result() # collect errors if they occured - multidata_task = pool.submit(write_multidata, rom_futures, mod_futures) + multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures) if not check_accessibility_task.result(): if not world.can_beat_game(): raise Exception("Game appears as unbeatable. Aborting.") @@ -571,7 +578,7 @@ def main(args, seed=None): if multidata_task: multidata_task.result() # retrieve exception if one exists pool.shutdown() # wait for all queued tasks to complete - for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error + for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error generate_mc_data(world, player) if not args.skip_playthrough: logger.info('Calculating playthrough.') @@ -626,7 +633,8 @@ def create_playthrough(world): to_delete = set() for location in sphere: # we remove the item at location and check if game is still beatable - logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) + logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, + location.item.player) old_item = location.item location.item = None if world.can_beat_game(state_cache[num]): @@ -671,7 +679,8 @@ def create_playthrough(world): collection_spheres.append(sphere) - logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) + logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), + len(sphere), len(required_locations)) if not sphere: raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}') @@ -690,14 +699,22 @@ def create_playthrough(world): world.spoiler.paths = dict() for player in range(1, world.players + 1): - world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) + world.spoiler.paths.update( + {str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in + sphere if location.player == player}) if player in world.alttp_player_ids: for path in dict(world.spoiler.paths).values(): if any(exit == 'Pyramid Fairy' for (_, exit) in path): if world.mode[player] != 'inverted': - world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) + world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, + world.get_region( + 'Big Bomb Shop', + player)) else: - world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player)) + world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, + world.get_region( + 'Inverted Big Bomb Shop', + player)) # we can finally output our playthrough world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])} diff --git a/Options.py b/Options.py index b0bdb76711..5150ec800b 100644 --- a/Options.py +++ b/Options.py @@ -22,6 +22,37 @@ class AssembleOptions(type): return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs) +class AssembleCategoryPath(type): + def __new__(mcs, name, bases, attrs): + path = [] + for base in bases: + if hasattr(base, "segment"): + path += base.segment + path += attrs["segment"] + attrs["path"] = path + return super(AssembleCategoryPath, mcs).__new__(mcs, name, bases, attrs) + + +class RootCategory(metaclass=AssembleCategoryPath): + segment = [] + + +class LttPCategory(RootCategory): + segment = ["A Link to the Past"] + + +class LttPRomCategory(LttPCategory): + segment = ["rom"] + + +class FactorioCategory(RootCategory): + segment = ["Factorio"] + + +class MinecraftCategory(RootCategory): + segment = ["Minecraft"] + + class Option(metaclass=AssembleOptions): value: int name_lookup: typing.Dict[int, str] diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py new file mode 100644 index 0000000000..c5644bcd81 --- /dev/null +++ b/worlds/AutoWorld.py @@ -0,0 +1,37 @@ +from BaseClasses import MultiWorld + +class AutoWorldRegister(type): + world_types = {} + + def __new__(cls, name, bases, dct): + new_class = super().__new__(cls, name, bases, dct) + if "game" in dct: + AutoWorldRegister.world_types[dct["game"]] = new_class + return new_class + + +def call_single(world: MultiWorld, method_name: str, player: int): + method = getattr(world.worlds[player], method_name) + return method(world, player) + + +def call_all(world: MultiWorld, method_name: str): + for player in world.player_ids: + call_single(world, method_name, player) + + +class World(metaclass=AutoWorldRegister): + """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. + A Game should have its own subclass of World in which it defines the required data structures.""" + + def __init__(self, player: int): + self.player = int + + def generate_basic(self, world: MultiWorld, player: int): + pass + + def generate_output(self, world: MultiWorld, player: int): + pass + + def create_regions(self, world: MultiWorld, player: int): + pass diff --git a/worlds/BaseWorld.py b/worlds/BaseWorld.py deleted file mode 100644 index 34c3ef8901..0000000000 --- a/worlds/BaseWorld.py +++ /dev/null @@ -1,13 +0,0 @@ -class AutoWorldRegister(type): - _world_types = {} - - def __new__(cls, name, bases, dct): - new_class = super().__new__(cls, name, bases, dct) - AutoWorldRegister._world_types[name] = new_class - return new_class - -class World(metaclass=AutoWorldRegister): - """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. - A Game should have its own subclass of World in which it defines the required data structures.""" - def __init__(self): - pass \ No newline at end of file diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index c3f7edbbf5..11ed34041a 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -1,96 +1,10 @@ from typing import Optional from BaseClasses import Location, Item +from ..AutoWorld import World - -#class ALTTPWorld(World): -# """WIP""" -# def __init__(self, options, slot: int): -# self._region_cache = {} -# self.slot = slot -# self.shuffle = shuffle -# self.logic = logic -# self.mode = mode -# self.swords = swords -# self.difficulty = difficulty -# self.difficulty_adjustments = difficulty_adjustments -# self.timer = timer -# self.progressive = progressive -# self.goal = goal -# self.dungeons = [] -# self.regions = [] -# self.shops = [] -# self.itempool = [] -# self.seed = None -# self.precollected_items = [] -# self.state = CollectionState(self) -# self._cached_entrances = None -# self._cached_locations = None -# self._entrance_cache = {} -# self._location_cache = {} -# self.required_locations = [] -# self.light_world_light_cone = False -# self.dark_world_light_cone = False -# self.rupoor_cost = 10 -# self.aga_randomness = True -# self.lock_aga_door_in_escape = False -# self.save_and_quit_from_boss = True -# self.accessibility = accessibility -# self.shuffle_ganon = shuffle_ganon -# self.fix_gtower_exit = self.shuffle_ganon -# self.retro = retro -# self.custom = custom -# self.customitemarray: List[int] = customitemarray -# self.hints = hints -# self.dynamic_regions = [] -# self.dynamic_locations = [] -# -# -# self.remote_items = False -# self.required_medallions = ['Ether', 'Quake'] -# self.swamp_patch_required = False -# self.powder_patch_required = False -# self.ganon_at_pyramid = True -# self.ganonstower_vanilla = True -# -# -# self.can_access_trock_eyebridge = None -# self.can_access_trock_front = None -# self.can_access_trock_big_chest = None -# self.can_access_trock_middle = None -# self.fix_fake_world = True -# self.mapshuffle = False -# self.compassshuffle = False -# self.keyshuffle = False -# self.bigkeyshuffle = False -# self.difficulty_requirements = None -# self.boss_shuffle = 'none' -# self.enemy_shuffle = False -# self.enemy_health = 'default' -# self.enemy_damage = 'default' -# self.killable_thieves = False -# self.tile_shuffle = False -# self.bush_shuffle = False -# self.beemizer = 0 -# self.escape_assist = [] -# self.crystals_needed_for_ganon = 7 -# self.crystals_needed_for_gt = 7 -# self.open_pyramid = False -# self.treasure_hunt_icon = 'Triforce Piece' -# self.treasure_hunt_count = 0 -# self.clock_mode = False -# self.can_take_damage = True -# self.glitch_boots = True -# self.progression_balancing = True -# self.local_items = set() -# self.triforce_pieces_available = 30 -# self.triforce_pieces_required = 20 -# self.shop_shuffle = 'off' -# self.shuffle_prizes = "g" -# self.sprite_pool = [] -# self.dark_room_logic = "lamp" -# self.restrict_dungeon_item_on_boss = False -# +class ALTTPWorld(World): + game: str = "A Link to the Past" class ALttPLocation(Location): diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 2bde14eb85..fea4e848b2 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -15,7 +15,7 @@ def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]: prerequisites: Dict[str, Set[str]] = {} layout = world.tech_tree_layout[player].value custom_technologies = world.custom_data[player]["custom_technologies"] - tech_names: List[str] = list(set(custom_technologies) - world._static_nodes) + tech_names: List[str] = list(set(custom_technologies) - world.worlds[player].static_nodes) tech_names.sort() world.random.shuffle(tech_names) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 7d26862f39..a1e806bb06 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,66 +1,56 @@ -from ..BaseWorld import World - +from ..AutoWorld import World from BaseClasses import Region, Entrance, Location, MultiWorld, Item from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, \ all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes from .Shapes import get_shapes +from .Mod import generate_mod + class Factorio(World): + game: str = "Factorio" + static_nodes = {"automation", "logistics"} + def generate_basic(self, world: MultiWorld, player: int): - static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option? + victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value])) for tech_name, tech_id in tech_table.items(): - tech_item = Item(tech_name, tech_name in advancement_technologies, tech_id, player) + tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names, + tech_id, player) tech_item.game = "Factorio" - if tech_name in static_nodes: - loc = world.get_location(tech_name, player) - loc.item = tech_item - loc.locked = True - loc.event = tech_item.advancement + if tech_name in self.static_nodes: + world.get_location(tech_name, player).place_locked_item(tech_item) else: world.itempool.append(tech_item) world.custom_data[player]["custom_technologies"] = custom_technologies = set_custom_technologies(world, player) set_rules(world, player, custom_technologies) -def gen_factorio(world: MultiWorld, player: int): - static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option? - victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value])) - for tech_name, tech_id in tech_table.items(): - tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names, - tech_id, player) - tech_item.game = "Factorio" - if tech_name in static_nodes: - world.get_location(tech_name, player).place_locked_item(tech_item) - else: - world.itempool.append(tech_item) - world.custom_data[player]["custom_technologies"] = custom_technologies = set_custom_technologies(world, player) - set_rules(world, player, custom_technologies) + def generate_output(self, world: MultiWorld, player: int): + generate_mod(world, player) + def create_regions(self, world: MultiWorld, player: int): + menu = Region("Menu", None, "Menu", player) + crash = Entrance(player, "Crash Land", menu) + menu.exits.append(crash) + nauvis = Region("Nauvis", None, "Nauvis", player) + nauvis.world = menu.world = world -def factorio_create_regions(world: MultiWorld, player: int): - menu = Region("Menu", None, "Menu", player) - crash = Entrance(player, "Crash Land", menu) - menu.exits.append(crash) - nauvis = Region("Nauvis", None, "Nauvis", player) - nauvis.world = menu.world = world - - for tech_name, tech_id in tech_table.items(): - tech = Location(player, tech_name, tech_id, nauvis) - nauvis.locations.append(tech) - tech.game = "Factorio" - location = Location(player, "Rocket Launch", None, nauvis) - nauvis.locations.append(location) - event = Item("Victory", True, None, player) - world.push_item(location, event, False) - location.event = location.locked = True - for ingredient in all_ingredient_names: - location = Location(player, f"Automate {ingredient}", None, nauvis) + for tech_name, tech_id in tech_table.items(): + tech = Location(player, tech_name, tech_id, nauvis) + nauvis.locations.append(tech) + tech.game = "Factorio" + location = Location(player, "Rocket Launch", None, nauvis) nauvis.locations.append(location) - event = Item(f"Automated {ingredient}", True, None, player) + event = Item("Victory", True, None, player) world.push_item(location, event, False) location.event = location.locked = True - crash.connect(nauvis) - world.regions += [menu, nauvis] + for ingredient in all_ingredient_names: + location = Location(player, f"Automate {ingredient}", None, nauvis) + nauvis.locations.append(location) + event = Item(f"Automated {ingredient}", True, None, player) + world.push_item(location, event, False) + location.event = location.locked = True + crash.connect(nauvis) + world.regions += [menu, nauvis] def set_custom_technologies(world: MultiWorld, player: int): diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 65ea8421a3..12bc644888 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -8,7 +8,10 @@ from .Regions import create_regions from .Rules import set_rules from BaseClasses import Region, Entrance, Location, MultiWorld, Item +from ..AutoWorld import World +class HKWorld(World): + game: str = "Hollow Knight" def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): ret = Region(name, None, name, player) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 676a72d0bf..a170daae54 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -4,15 +4,24 @@ from .Locations import exclusion_table, events_table from .Regions import link_minecraft_structures from .Rules import set_rules -from BaseClasses import Region, Entrance, Location, MultiWorld, Item +from BaseClasses import MultiWorld from Options import minecraft_options +from ..AutoWorld import World + + +class MinecraftWorld(World): + game: str = "Minecraft" + client_version = (0, 3) + def get_mc_data(world: MultiWorld, player: int): - exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", "The End Structure"] + exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", + "The End Structure"] return { - 'world_seed': Random(world.rom_seeds[player]).getrandbits(32), # consistent and doesn't interfere with other generation + 'world_seed': Random(world.rom_seeds[player]).getrandbits(32), + # consistent and doesn't interfere with other generation 'seed_name': world.seed_name, 'player_name': world.get_player_names(player), 'player_id': player, @@ -20,25 +29,27 @@ def get_mc_data(world: MultiWorld, player: int): 'structures': {exit: world.get_entrance(exit, player).connected_region.name for exit in exits} } -def generate_mc_data(world: MultiWorld, player: int): + +def generate_mc_data(world: MultiWorld, player: int): import base64, json from Utils import output_path data = get_mc_data(world, player) filename = f"AP_{world.seed_name}_P{player}_{world.get_player_names(player)}.apmc" - with open(output_path(filename), 'wb') as f: + with open(output_path(filename), 'wb') as f: f.write(base64.b64encode(bytes(json.dumps(data), 'utf-8'))) -def fill_minecraft_slot_data(world: MultiWorld, player: int): + +def fill_minecraft_slot_data(world: MultiWorld, player: int): slot_data = get_mc_data(world, player) for option_name in minecraft_options: option = getattr(world, option_name)[player] slot_data[option_name] = int(option.value) return slot_data -# Generates the item pool given the table and frequencies in Items.py. -def minecraft_gen_item_pool(world: MultiWorld, player: int): +# Generates the item pool given the table and frequencies in Items.py. +def minecraft_gen_item_pool(world: MultiWorld, player: int): pool = [] for item_name, item_data in item_table.items(): for count in range(item_frequencies.get(item_name, 1)): @@ -47,8 +58,8 @@ def minecraft_gen_item_pool(world: MultiWorld, player: int): prefill_pool = {} prefill_pool.update(events_table) exclusion_pools = ['hard', 'insane', 'postgame'] - for key in exclusion_pools: - if not getattr(world, f"include_{key}_advancements")[player]: + for key in exclusion_pools: + if not getattr(world, f"include_{key}_advancements")[player]: prefill_pool.update(exclusion_table[key]) for loc_name, item_name in prefill_pool.items(): @@ -62,9 +73,9 @@ def minecraft_gen_item_pool(world: MultiWorld, player: int): world.itempool += pool -# Generate Minecraft world. + +# Generate Minecraft world. def gen_minecraft(world: MultiWorld, player: int): link_minecraft_structures(world, player) minecraft_gen_item_pool(world, player) set_rules(world, player) - From 20ca09c730c7407905719dc82da0fbfda70a892e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 11 Jun 2021 14:23:59 +0200 Subject: [PATCH 06/34] remove test modules --- test_loader.py | 2 -- worlds/loader.py | 12 ------------ 2 files changed, 14 deletions(-) delete mode 100644 test_loader.py delete mode 100644 worlds/loader.py diff --git a/test_loader.py b/test_loader.py deleted file mode 100644 index 1ee0621e92..0000000000 --- a/test_loader.py +++ /dev/null @@ -1,2 +0,0 @@ -import worlds.loader -print(worlds.loader.world_types) \ No newline at end of file diff --git a/worlds/loader.py b/worlds/loader.py deleted file mode 100644 index 52c1349df2..0000000000 --- a/worlds/loader.py +++ /dev/null @@ -1,12 +0,0 @@ -import importlib -import os -world_types = [] -world_folder = os.path.dirname(__file__) -for entry in os.scandir(world_folder): - if entry.is_dir(): - entryname = entry.name - if not entryname.startswith("_"): - world_module = importlib.import_module("."+entry.name, package="worlds") - world_types.append(world_module) -print(world_folder) -print(world_types) \ No newline at end of file From 278f40471b968c7a0190e6d42e6187c7521d7a18 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 11 Jun 2021 14:26:12 +0200 Subject: [PATCH 07/34] fix open_pyramid default --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index c3f2b56763..15f98cd79d 100644 --- a/Main.py +++ b/Main.py @@ -219,7 +219,7 @@ def main(args, seed=None): 'dungeonscrossed'} or not world.shuffle_ganon) else: world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get( - world.open_pyramid[player], world.open_pyramid[player]) + world.open_pyramid[player], 'auto') world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player]) From 760fb320169e35078ec1ae63f16f04694cae602f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 11 Jun 2021 14:47:13 +0200 Subject: [PATCH 08/34] fix Factorio Recipe Time randomization not being deterministic --- LttPAdjuster.py | 2 +- Main.py | 4 ++-- worlds/alttp/Rom.py | 14 +++++++------- worlds/factorio/Mod.py | 2 +- worlds/minecraft/__init__.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 01f5fec94b..c8a998b063 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -18,7 +18,7 @@ class AdjusterWorld(object): def __init__(self, sprite_pool): import random self.sprite_pool = {1: sprite_pool} - self.rom_seeds = {1: random} + self.slot_seeds = {1: random} class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): diff --git a/Main.py b/Main.py index 15f98cd79d..9b957f2fc4 100644 --- a/Main.py +++ b/Main.py @@ -130,8 +130,8 @@ def main(args, seed=None): setattr(world, option, getattr(args, option, {})) world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. - world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in - range(1, world.players + 1)} + world.slot_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in + range(1, world.players + 1)} for player in range(1, world.players + 1): world.er_seeds[player] = str(world.random.randint(0, 2 ** 64)) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index dcc906cd99..3f02c2ea26 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -80,7 +80,7 @@ class LocalRom(object): self.write_bytes(startaddress + i, bytearray(data)) def encrypt(self, world, player): - local_random = world.rom_seeds[player] + local_random = world.slot_seeds[player] key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big')) self.write_bytes(0x1800B0, bytearray(key)) self.write_int16(0x180087, 1) @@ -384,7 +384,7 @@ def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli): max_enemizer_tries = 5 for i in range(max_enemizer_tries): - enemizer_seed = str(world.rom_seeds[player].randint(0, 999999999)) + enemizer_seed = str(world.slot_seeds[player].randint(0, 999999999)) enemizer_command = [os.path.abspath(enemizercli), '--rom', randopatch_path, '--seed', enemizer_seed, @@ -414,7 +414,7 @@ def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli): continue for j in range(i + 1, max_enemizer_tries): - world.rom_seeds[player].randint(0, 999999999) + world.slot_seeds[player].randint(0, 999999999) # Sacrifice all remaining random numbers that would have been used for unused enemizer tries. # This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness break @@ -760,7 +760,7 @@ def get_nonnative_item_sprite(game): return game_to_id.get(game, 0x6B) # default to Power Star def patch_rom(world, rom, player, team, enemized): - local_random = world.rom_seeds[player] + local_random = world.slot_seeds[player] # progressive bow silver arrow hint hack prog_bow_locs = world.find_items('Progressive Bow', player) @@ -1643,7 +1643,7 @@ def patch_rom(world, rom, player, team, enemized): rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit if world.tile_shuffle[player]: - tile_set = TileSet.get_random_tile_set(world.rom_seeds[player]) + tile_set = TileSet.get_random_tile_set(world.slot_seeds[player]) rom.write_byte(0x4BA21, tile_set.get_speed()) rom.write_byte(0x4BA1D, tile_set.get_len()) rom.write_bytes(0x4BA2A, tile_set.get_bytes()) @@ -1774,7 +1774,7 @@ def hud_format_text(text): def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, 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.rom_seeds[player] + local_random = random if not world else world.slot_seeds[player] # enable instant item menu if fastmenu == 'instant': @@ -2091,7 +2091,7 @@ def write_string_to_rom(rom, target, string): def write_strings(rom, world, player, team): - local_random = world.rom_seeds[player] + local_random = world.slot_seeds[player] tt = TextTable() tt.removeUnwantedText() diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 25b75510f1..b438cb9cb2 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -74,7 +74,7 @@ def generate_mod(world: MultiWorld, player: int): "rocket_recipe": rocket_recipes[world.max_science_pack[player].value], "slot_name": world.player_names[player][0], "seed_name": world.seed_name, "starting_items": world.starting_items[player], "recipes": recipes, - "random": world.random, + "random": world.slot_seeds[player], "recipe_time_scale": recipe_time_scales[world.recipe_time[player].value]} for factorio_option in Options.factorio_options: diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index a170daae54..f81c2bcfdd 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -20,7 +20,7 @@ def get_mc_data(world: MultiWorld, player: int): exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", "The End Structure"] return { - 'world_seed': Random(world.rom_seeds[player]).getrandbits(32), + 'world_seed': world.slot_seeds[player].getrandbits(32), # consistent and doesn't interfere with other generation 'seed_name': world.seed_name, 'player_name': world.get_player_names(player), From 2c4c8991794ed9ffa40b276a02ce21d4fa75777d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 11 Jun 2021 18:02:48 +0200 Subject: [PATCH 09/34] move more Factorio stuff around --- BaseClasses.py | 15 +++- Main.py | 7 +- .../mod_template/data-final-fixes.lua | 2 +- worlds/AutoWorld.py | 21 +++-- worlds/factorio/Mod.py | 2 +- worlds/factorio/Shapes.py | 6 +- worlds/factorio/__init__.py | 82 +++++++++---------- 7 files changed, 75 insertions(+), 60 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1b4cfcc7c0..ebb43e8720 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -23,6 +23,7 @@ class MultiWorld(): plando_items: List[PlandoItem] plando_connections: List[PlandoConnection] er_seeds: Dict[int, str] + worlds: Dict[int, "AutoWorld.World"] class AttributeProxy(): def __init__(self, rule): @@ -32,8 +33,6 @@ class MultiWorld(): return self.rule(player) def __init__(self, players: int): - from worlds import AutoWorld - self.random = random.Random() # world-local random state is saved for multiple generations running concurrently self.players = players self.teams = 1 @@ -140,9 +139,17 @@ class MultiWorld(): set_player_attr('completion_condition', lambda state: True) self.custom_data = {} self.worlds = {} - for player in range(1, players+1): + + + def set_options(self, args): + import Options + from worlds import AutoWorld + for option_set in Options.option_sets: + for option in option_set: + setattr(self, option, getattr(args, option, {})) + for player in self.player_ids: self.custom_data[player] = {} - self.worlds[player] = AutoWorld.AutoWorldRegister.world_types[self.game[player]](player) + self.worlds[player] = AutoWorld.AutoWorldRegister.world_types[self.game[player]](self, player) def secure(self): self.random = secrets.SystemRandom() diff --git a/Main.py b/Main.py index 9b957f2fc4..4a7a66d0e9 100644 --- a/Main.py +++ b/Main.py @@ -124,10 +124,7 @@ def main(args, seed=None): world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy() world.required_medallions = args.required_medallions.copy() world.game = args.game.copy() - import Options - for option_set in Options.option_sets: - for option in option_set: - setattr(world, option, getattr(args, option, {})) + world.set_options(args) world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. world.slot_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in @@ -262,6 +259,8 @@ def main(args, seed=None): for player in world.player_ids: locality_rules(world, player) + AutoWorld.call_all(world, "set_rules") + for player in world.alttp_player_ids: set_rules(world, player) diff --git a/data/factorio/mod_template/data-final-fixes.lua b/data/factorio/mod_template/data-final-fixes.lua index d6a96ee1f4..335eca344a 100644 --- a/data/factorio/mod_template/data-final-fixes.lua +++ b/data/factorio/mod_template/data-final-fixes.lua @@ -8,7 +8,7 @@ local technologies = data.raw["technology"] local original_tech local new_tree_copy allowed_ingredients = {} -{%- for tech_name, technology in custom_data["custom_technologies"].items() %} +{%- for tech_name, technology in custom_technologies.items() %} allowed_ingredients["{{ tech_name }}"] = { {%- for ingredient in technology.ingredients %} ["{{ingredient}}"] = 1, diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index c5644bcd81..d27b324be3 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -1,5 +1,6 @@ from BaseClasses import MultiWorld + class AutoWorldRegister(type): world_types = {} @@ -12,7 +13,7 @@ class AutoWorldRegister(type): def call_single(world: MultiWorld, method_name: str, player: int): method = getattr(world.worlds[player], method_name) - return method(world, player) + return method() def call_all(world: MultiWorld, method_name: str): @@ -24,14 +25,22 @@ class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. A Game should have its own subclass of World in which it defines the required data structures.""" - def __init__(self, player: int): - self.player = int + world: MultiWorld + player: int - def generate_basic(self, world: MultiWorld, player: int): + def __init__(self, world: MultiWorld, player: int): + self.world = world + self.player = player + + # overwritable methods that get called by Main.py + def generate_basic(self): pass - def generate_output(self, world: MultiWorld, player: int): + def set_rules(self): pass - def create_regions(self, world: MultiWorld, player: int): + def create_regions(self): + pass + + def generate_output(self): pass diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index b438cb9cb2..7fb6e17b57 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -69,7 +69,7 @@ def generate_mod(world: MultiWorld, player: int): 6: 10}[world.tech_cost[player].value] template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table, "mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(), - "tech_cost_scale": tech_cost, "custom_data": world.custom_data[player], + "tech_cost_scale": tech_cost, "custom_technologies": world.worlds[player].custom_technologies, "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player], "rocket_recipe": rocket_recipes[world.max_science_pack[player].value], "slot_name": world.player_names[player][0], "seed_name": world.seed_name, diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index fea4e848b2..617ba1638b 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -11,10 +11,12 @@ funnel_slice_sizes = {TechTreeLayout.option_small_funnels: 6, TechTreeLayout.option_medium_funnels: 10, TechTreeLayout.option_large_funnels: 15} -def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]: +def get_shapes(factorio_world) -> Dict[str, List[str]]: + world = factorio_world.world + player = factorio_world.player prerequisites: Dict[str, Set[str]] = {} layout = world.tech_tree_layout[player].value - custom_technologies = world.custom_data[player]["custom_technologies"] + custom_technologies = factorio_world.custom_technologies tech_names: List[str] = list(set(custom_technologies) - world.worlds[player].static_nodes) tech_names.sort() world.random.shuffle(tech_names) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index a1e806bb06..e6858a21eb 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -11,28 +11,28 @@ class Factorio(World): game: str = "Factorio" static_nodes = {"automation", "logistics"} - def generate_basic(self, world: MultiWorld, player: int): - victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value])) + def generate_basic(self): + victory_tech_names = get_rocket_requirements( + frozenset(rocket_recipes[self.world.max_science_pack[self.player].value])) for tech_name, tech_id in tech_table.items(): tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names, - tech_id, player) + tech_id, self.player) tech_item.game = "Factorio" if tech_name in self.static_nodes: - world.get_location(tech_name, player).place_locked_item(tech_item) + self.world.get_location(tech_name, self.player).place_locked_item(tech_item) else: - world.itempool.append(tech_item) - world.custom_data[player]["custom_technologies"] = custom_technologies = set_custom_technologies(world, player) - set_rules(world, player, custom_technologies) + self.world.itempool.append(tech_item) - def generate_output(self, world: MultiWorld, player: int): - generate_mod(world, player) + def generate_output(self): + generate_mod(self.world, self.player) - def create_regions(self, world: MultiWorld, player: int): + def create_regions(self): + player = self.player menu = Region("Menu", None, "Menu", player) crash = Entrance(player, "Crash Land", menu) menu.exits.append(crash) nauvis = Region("Nauvis", None, "Nauvis", player) - nauvis.world = menu.world = world + nauvis.world = menu.world = self.world for tech_name, tech_id in tech_table.items(): tech = Location(player, tech_name, tech_id, nauvis) @@ -41,49 +41,47 @@ class Factorio(World): location = Location(player, "Rocket Launch", None, nauvis) nauvis.locations.append(location) event = Item("Victory", True, None, player) - world.push_item(location, event, False) + self.world.push_item(location, event, False) location.event = location.locked = True for ingredient in all_ingredient_names: location = Location(player, f"Automate {ingredient}", None, nauvis) nauvis.locations.append(location) event = Item(f"Automated {ingredient}", True, None, player) - world.push_item(location, event, False) + self.world.push_item(location, event, False) location.event = location.locked = True crash.connect(nauvis) - world.regions += [menu, nauvis] + self.world.regions += [menu, nauvis] + def set_rules(self): + world = self.world + player = self.player + self.custom_technologies = set_custom_technologies(self.world, self.player) + shapes = get_shapes(self) + if world.logic[player] != 'nologic': + from worlds.generic import Rules + for ingredient in all_ingredient_names: + location = world.get_location(f"Automate {ingredient}", player) + location.access_rule = lambda state, ingredient=ingredient: \ + all(state.has(technology.name, player) for technology in required_technologies[ingredient]) + for tech_name, technology in self.custom_technologies.items(): + location = world.get_location(tech_name, player) + Rules.set_rule(location, technology.build_rule(player)) + prequisites = shapes.get(tech_name) + if prequisites: + locations = {world.get_location(requisite, player) for requisite in prequisites} + Rules.add_rule(location, lambda state, + locations=locations: all(state.can_reach(loc) for loc in locations)) + # get all science pack technologies (but not the ability to craft them) + victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value])) + world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) + for technology in + victory_tech_names) + + world.completion_condition[player] = lambda state: state.has('Victory', player) def set_custom_technologies(world: MultiWorld, player: int): custom_technologies = {} - world_custom = getattr(world, "_custom_technologies", {}) - world_custom[player] = custom_technologies - world._custom_technologies = world_custom allowed_packs = world.max_science_pack[player].get_allowed_packs() for technology_name, technology in technology_table.items(): custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player) return custom_technologies - - -def set_rules(world: MultiWorld, player: int, custom_technologies): - shapes = get_shapes(world, player) - if world.logic[player] != 'nologic': - from worlds.generic import Rules - for ingredient in all_ingredient_names: - location = world.get_location(f"Automate {ingredient}", player) - location.access_rule = lambda state, ingredient=ingredient: \ - all(state.has(technology.name, player) for technology in required_technologies[ingredient]) - for tech_name, technology in custom_technologies.items(): - location = world.get_location(tech_name, player) - Rules.set_rule(location, technology.build_rule(player)) - prequisites = shapes.get(tech_name) - if prequisites: - locations = {world.get_location(requisite, player) for requisite in prequisites} - Rules.add_rule(location, lambda state, - locations=locations: all(state.can_reach(loc) for loc in locations)) - # get all science pack technologies (but not the ability to craft them) - victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value])) - world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) - for technology in - victory_tech_names) - - world.completion_condition[player] = lambda state: state.has('Victory', player) From 2a13fe05c62603129a24d6c4b47808ddf106d240 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 11 Jun 2021 18:05:49 +0200 Subject: [PATCH 10/34] fix import error for Hollow Knight --- Main.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Main.py b/Main.py index 4a7a66d0e9..874cac83d2 100644 --- a/Main.py +++ b/Main.py @@ -519,11 +519,13 @@ def main(args, seed=None): for player, name in enumerate(team, 1): if player not in world.alttp_player_ids: connect_names[name] = (i, player) - for slot in world.hk_player_ids: - slots_data = slot_data[slot] = {} - for option_name in Options.hollow_knight_options: - option = getattr(world, option_name)[slot] - slots_data[option_name] = int(option.value) + if world.hk_player_ids: + import Options + for slot in world.hk_player_ids: + slots_data = slot_data[slot] = {} + for option_name in Options.hollow_knight_options: + option = getattr(world, option_name)[slot] + slots_data[option_name] = int(option.value) for slot in world.minecraft_player_ids: slot_data[slot] = fill_minecraft_slot_data(world, slot) From 3ea7f1cb0359e6a89193255eba9dea25cfe8f8d4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 11 Jun 2021 20:18:28 +0200 Subject: [PATCH 11/34] Factorio Funnels: only sort current funnel, not all funnels --- Main.py | 1 - worlds/factorio/Shapes.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Main.py b/Main.py index 874cac83d2..c35f5b9d3c 100644 --- a/Main.py +++ b/Main.py @@ -1,4 +1,3 @@ -import copy from itertools import zip_longest import logging import os diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 617ba1638b..481b64cf44 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -173,15 +173,14 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout in funnel_layers: slice_size = funnel_slice_sizes[layout] - world.random.shuffle(tech_names) - tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) while len(tech_names) > slice_size: tech_names = tech_names[slice_size:] current_tech_names = tech_names[:slice_size] layer_size = funnel_layers[layout] previous_slice = [] + current_tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) for layer in range(funnel_layers[layout]): slice = current_tech_names[:layer_size] current_tech_names = current_tech_names[layer_size:] From 7b495f3d817840a45be3c51887e52406f5733616 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 11 Jun 2021 20:22:47 -0400 Subject: [PATCH 12/34] Website landing page preliminary update --- WebHostLib/static/styles/landing.css | 47 +++++++++++---------- WebHostLib/templates/header/baseHeader.html | 4 +- WebHostLib/templates/landing.html | 30 ++++++------- 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/WebHostLib/static/styles/landing.css b/WebHostLib/static/styles/landing.css index 23da669077..846cfb05b1 100644 --- a/WebHostLib/static/styles/landing.css +++ b/WebHostLib/static/styles/landing.css @@ -7,7 +7,6 @@ html{ flex-direction: column; justify-content: center; flex-wrap: wrap; - margin-top: 60px; } #landing-header{ @@ -53,18 +52,19 @@ html{ font-size: 1.4rem; } -#uploads-button{ - top: 65px; +#far-left-button{ + top: 115px; left: calc(50% - 416px - 200px - 75px); background-image: url("/static/static/button-images/button-a.png"); background-size: 200px auto; width: 200px; height: calc(156px - 40px); padding-top: 40px; + cursor: default; } -#setup-guide-button{ - top: 270px; +#mid-left-button{ + top: 320px; left: calc(50% - 416px - 200px + 140px); background-image: url("/static/static/button-images/button-b.png"); background-size: 260px auto; @@ -73,8 +73,8 @@ html{ padding-top: 35px; } -#player-settings-button{ - top: 350px; +#mid-button{ + top: 400px; left: calc(50% - 100px); background-image: url("/static/static/button-images/button-a.png"); background-size: 200px auto; @@ -83,8 +83,8 @@ html{ padding-top: 38px; } -#discord-button{ - top: 250px; +#mid-right-button{ + top: 300px; left: calc(50% + 416px - 166px); background-image: url("/static/static/button-images/button-c.png"); background-size: 250px auto; @@ -94,14 +94,15 @@ html{ padding-left: 20px; } -#generate-button{ - top: 75px; +#far-right-button{ + top: 125px; left: calc(50% + 416px + 75px); background-image: url("/static/static/button-images/button-b.png"); background-size: 260px auto; width: 260px; height: calc(130px - 35px); padding-top: 35px; + cursor: default; } #landing-clouds{ @@ -111,7 +112,7 @@ html{ #landing-clouds #cloud1{ position: absolute; left: 10px; - top: 265px; + top: 365px; width: 400px; height: 350px; @@ -147,23 +148,23 @@ html{ @keyframes c1-float{ from{ left: 10px; - top: 265px; + top: 365px; } 25%{ left: 14px; - top: 267px; + top: 367px; } 50%{ left: 17px; - top: 265px; + top: 365px; } 75%{ left: 14px; - top: 262px; + top: 362px; } to{ left: 10px; - top: 265px; + top: 365px; } } @@ -241,32 +242,32 @@ html{ } #landing-deco-1{ - top: 430px; + top: 480px; left: calc(50% - 276px); } #landing-deco-2{ - top: 200px; + top: 250px; left: calc(50% + 150px); } #landing-deco-3{ - top: 300px; + top: 350px; left: calc(50% - 150px); } #landing-deco-4{ - top: 240px; + top: 290px; left: calc(50% - 580px); } #landing-deco-5{ - top: 40px; + top: 90px; left: calc(50% + 450px); } #landing-deco-6{ - top: 412px; + top: 462px; left: calc(50% + 196px); } diff --git a/WebHostLib/templates/header/baseHeader.html b/WebHostLib/templates/header/baseHeader.html index b47537bae6..e7d82dec6d 100644 --- a/WebHostLib/templates/header/baseHeader.html +++ b/WebHostLib/templates/header/baseHeader.html @@ -11,10 +11,8 @@ archipelago diff --git a/WebHostLib/templates/landing.html b/WebHostLib/templates/landing.html index 79fbdb0f72..2de5668562 100644 --- a/WebHostLib/templates/landing.html +++ b/WebHostLib/templates/landing.html @@ -6,17 +6,18 @@ {% endblock %} {% block body %} + {% include 'header/oceanHeader.html' %}
-

the legend of zelda: a link to the past

-

MULTIWORLD RANDOMIZER

+

ARCHIPELAGO

+

multiworld randomizer ecosystem

@@ -33,14 +34,13 @@
-

Welcome to the Archipelago Multiworld Randomizer!

-

This is a randomizer for The Legend of Zelda: A - Link to the Past.

-

It is also a multi-world, meaning Link's items may have been placed into other players' games. - When a player picks up an item which does not belong to them, it is sent back to the player - it belongs to.

-

On this website you are able to generate and host multiworld games, and item and location - trackers are provided for games hosted here.

+

Welcome to Archipelago!

+

+ This is a cross-game modification system which randomizes different games, then uses the result to + build a single unified multi-player game. Items from one game may be present in another, and + you will need your fellow players to find items you need in their games to help you complete + your own. +

This project is the cumulative effort of many talented people. From 8c82d3e747c3dc81c89625c67ddc17bcef842b3f Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sat, 12 Jun 2021 02:49:36 -0400 Subject: [PATCH 13/34] Added a page to describe the games currently supported by AP --- WebHostLib/__init__.py | 5 +++ WebHostLib/static/styles/games.css | 67 ++++++++++++++++++++++++++++++ WebHostLib/templates/games.html | 35 ++++++++++++++++ WebHostLib/templates/landing.html | 2 +- 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 WebHostLib/static/styles/games.css create mode 100644 WebHostLib/templates/games.html diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index a6c030759c..dedbe2a19a 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -94,6 +94,11 @@ def player_settings(): return render_template("weightedSettings.html") +@app.route('/games') +def games(): + return render_template("games.html") + + @app.route('/seed/') def viewSeed(seed: UUID): seed = Seed.get(id=seed) diff --git a/WebHostLib/static/styles/games.css b/WebHostLib/static/styles/games.css new file mode 100644 index 0000000000..0abd17d1bc --- /dev/null +++ b/WebHostLib/static/styles/games.css @@ -0,0 +1,67 @@ +html{ + background-image: url('../static/backgrounds/grass/grass-0007-large.png'); + background-repeat: repeat; + background-size: 650px 650px; +} + +#games{ + max-width: 1000px; + margin-left: auto; + margin-right: auto; + background-color: rgba(0, 0, 0, 0.15); + border-radius: 8px; + padding: 1rem; + color: #eeffeb; +} + +#games p{ + margin-top: 0.25rem; +} + +#games code{ + background-color: #d9cd8e; + border-radius: 4px; + padding-left: 0.25rem; + padding-right: 0.25rem; + color: #000000; +} + +#games #user-message{ + display: none; + width: calc(100% - 8px); + background-color: #ffe86b; + border-radius: 4px; + color: #000000; + padding: 4px; + text-align: center; +} + +#games h1{ + font-size: 2.5rem; + font-weight: normal; + border-bottom: 1px solid #ffffff; + width: 100%; + margin-bottom: 0.5rem; + color: #ffffff; + text-shadow: 1px 1px 4px #000000; +} + +#games h2{ + font-size: 2rem; + font-weight: normal; + border-bottom: 1px solid #ffffff; + width: 100%; + margin-bottom: 0.5rem; + color: #ffe993; + text-transform: lowercase; + text-shadow: 1px 1px 2px #000000; +} + +#games h3, #games h4, #games h5, #games h6{ + color: #ffffff; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} + +#games a{ + color: #ffef00; +} diff --git a/WebHostLib/templates/games.html b/WebHostLib/templates/games.html new file mode 100644 index 0000000000..e96aa1c378 --- /dev/null +++ b/WebHostLib/templates/games.html @@ -0,0 +1,35 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + Player Settings + + +{% endblock %} + +{% block body %} + {% include 'header/grassHeader.html' %} +

+

Currently Supported Games

+

The Legend of Zelda: A Link to the Past

+

+ The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of Link, + a boy who is destined to save the land of Hyrule. Delve through three palaces and nine dungeons on + your quest to rescue the descendents of the seven wise men and defeat the evil Ganon! +

+ +

Factorio

+

+ Factorio is a game about automation. You play as an engineer who has crash landed on the planet + Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory, + research new technologies, and become more efficient in your quest to build a rocket and return home. +

+ +

Minecraft

+

+ Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine, + craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient + structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim + victory! +

+
+{% endblock %} diff --git a/WebHostLib/templates/landing.html b/WebHostLib/templates/landing.html index 2de5668562..14207b5ea9 100644 --- a/WebHostLib/templates/landing.html +++ b/WebHostLib/templates/landing.html @@ -13,7 +13,7 @@

multiworld randomizer ecosystem