From ac31671914702b4cc980dfa7c4c8a36cd2668aa5 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Mon, 7 Jun 2021 00:38:30 -0500 Subject: [PATCH 01/42] initial hybridmg logic file commit --- worlds/alttp/UnderworldGlitchRules.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 worlds/alttp/UnderworldGlitchRules.py diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py new file mode 100644 index 00000000..e69de29b From 16c6e17a49873e8d432567ddd7bcdd3636e6b83f Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Mon, 7 Jun 2021 01:19:27 -0500 Subject: [PATCH 02/42] Initial handling of hybrid glitch logic outside of UnderworldGlitchRules --- BaseClasses.py | 3 +++ Mystery.py | 2 +- Options.py | 2 ++ worlds/alttp/ItemPool.py | 2 +- worlds/alttp/Rules.py | 10 ++++++++-- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0f5e5e6b..4c103b4d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -813,6 +813,9 @@ class CollectionState(object): rules.append(self.has('Moon Pearl', player)) return all(rules) + def can_bomb_clip(self, region: Region, player: int) -> bool: + return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player) + # Minecraft logic functions def has_iron_ingots(self, player: int): return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player) diff --git a/Mystery.py b/Mystery.py index a80872b8..8f26afdc 100644 --- a/Mystery.py +++ b/Mystery.py @@ -586,7 +586,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): logging.warning("Only NMG, OWG and No Logic supported") glitches_required = 'none' ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches', - 'minor_glitches': 'minorglitches'}[ + 'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[ glitches_required] ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp") diff --git a/Options.py b/Options.py index 3795b817..9582a3ea 100644 --- a/Options.py +++ b/Options.py @@ -146,8 +146,10 @@ class Logic(Choice): option_no_glitches = 0 option_minor_glitches = 1 option_overworld_glitches = 2 + option_hybrid_major_glitches = 3 option_no_logic = 4 alias_owg = 2 + alias_hmg = 3 class Objective(Choice): diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index d03b686b..a42598d1 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -571,7 +571,7 @@ def get_pool_core(world, player: int): return world.random.choice([True, False]) if progressive == 'random' else progressive == 'on' # provide boots to major glitch dependent seeds - if logic in {'owglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt': + if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt': precollected_items.append('Pegasus Boots') pool.remove('Pegasus Boots') pool.append('Rupees (20)') diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index faf37308..d2c9ac49 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -47,12 +47,17 @@ def set_rules(world, player): if world.logic[player] == 'noglitches': no_glitches_rules(world, player) - elif world.logic[player] in ['owglitches', 'nologic']: + elif world.logic[player] == 'owglitches': # Initially setting no_glitches_rules to set the baseline rules for some # entrances. The overworld_glitches_rules set is primarily additive. no_glitches_rules(world, player) fake_flipper_rules(world, player) overworld_glitches_rules(world, player) + elif world.logic[player] in ['hybridglitches', 'nologic']: + no_glitches_rules(world, player) + fake_flipper_rules(world, player) + overworld_glitches_rules(world, player) + underworld_glitches_rules(world, player) elif world.logic[player] == 'minorglitches': no_glitches_rules(world, player) fake_flipper_rules(world, player) @@ -75,7 +80,8 @@ def set_rules(world, player): set_inverted_big_bomb_rules(world, player) # if swamp and dam have not been moved we require mirror for swamp palace - if not world.swamp_patch_required[player]: + # however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself. + if not world.swamp_patch_required[player] and world.logic[player] not in ['hybridglitches', 'nologic']: add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) # GT Entrance may be required for Turtle Rock for OWG and < 7 required From fae14ad2836789ec7e467c67169f5fff62de3d2e Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Mon, 7 Jun 2021 19:34:00 -0500 Subject: [PATCH 03/42] Mystery.py correctly recognizes HMG as an option --- Mystery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mystery.py b/Mystery.py index 8f26afdc..f250740c 100644 --- a/Mystery.py +++ b/Mystery.py @@ -582,8 +582,8 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): 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") + if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']: + logging.warning("Only NMG, OWG, HMG and No Logic supported") glitches_required = 'none' ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches', 'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[ From eb9ee9f41ea105e28a1a28f8776355e0fde1e3e5 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Mon, 7 Jun 2021 20:19:03 -0500 Subject: [PATCH 04/42] Hybrid Major Glitches connections and logic --- worlds/alttp/EntranceShuffle.py | 10 ++- worlds/alttp/Regions.py | 4 +- worlds/alttp/Rules.py | 1 + worlds/alttp/UnderworldGlitchRules.py | 113 ++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 3 deletions(-) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index f26c59ab..87d9911d 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1,6 +1,6 @@ # ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. from collections import defaultdict - +from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections def link_entrances(world, player): connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now @@ -17,6 +17,10 @@ def link_entrances(world, player): for exitname, regionname in mandatory_connections: connect_simple(world, exitname, regionname, player) + # mandatory hybrid major glitches connections + if world.logic[player] in ['hybridglitches', 'nologic']: + underworld_glitch_connections(world, player) + # if we do not shuffle, set default connections if world.shuffle[player] == 'vanilla': for exitname, regionname in default_connections: @@ -1096,6 +1100,10 @@ def link_inverted_entrances(world, player): for exitname, regionname in inverted_mandatory_connections: connect_simple(world, exitname, regionname, player) + # mandatory hybrid major glitches connections + if world.logic[player] in ['hybridglitches', 'nologic']: + underworld_glitch_connections(world, player) + # if we do not shuffle, set default connections if world.shuffle[player] == 'vanilla': for exitname, regionname in inverted_default_connections: diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index 89a462ac..7373a05a 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -126,7 +126,7 @@ def create_regions(world, player): create_cave_region(player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), create_lw_region(player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']), create_cave_region(player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), - create_cave_region(player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), + create_cave_region(player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit', 'Kiki Skip']), create_cave_region(player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), create_lw_region(player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']), create_cave_region(player, 'Hookshot Fairy', 'fairies deep in a cave'), @@ -250,7 +250,7 @@ def create_regions(world, player): create_dungeon_region(player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), create_dungeon_region(player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], ['Misery Mire (West)', 'Misery Mire Big Key Door']), - create_dungeon_region(player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), + create_dungeon_region(player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest'], ['Mire to Hera Clip', 'Hera to Swamp Clip']), create_dungeon_region(player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), create_dungeon_region(player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), create_dungeon_region(player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index d2c9ac49..6adb0f58 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -4,6 +4,7 @@ from worlds.alttp import OverworldGlitchRules from BaseClasses import RegionType, MultiWorld, Entrance from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules +from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules from worlds.alttp.Bosses import GanonDefeatRule from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \ item_name diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index e69de29b..ed07a34b 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -0,0 +1,113 @@ + +from BaseClasses import Entrance +from worlds.generic.Rules import set_rule, add_rule + +# We actually need the logic to properly "mark" these regions as Light or Dark world. +# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. +def underworld_glitch_connections(world, player): + kikiskip = world.get_entrance('Kiki Skip', player) + mire_to_hera = world.get_entrance('Mire to Hera Clip', player) + mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player) + + if world.fix_fake_world[player]: + kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region) + mire_to_hera.connect(world.get_entrance('Tower of Hera Exit').connected_region) + mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit').connected_region) + else: + kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player)) + mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player)) + mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player)) + + +def underworld_glitches_rules(world, player): + fix_dungeon_exits = world.fix_palaceofdarkness_exit[player] + fix_fake_worlds = world.fix_fake_world[player] + + # Ice Palace Entrance Clip + # This is the easiest one since it's a simple internal clip. Just need to also add melting to freezor chest since it's otherwise assumed. + add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_bomb_clip(world.get_region('Ice Palace (Entrance)', player), player), combine='or') + add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player)) + + + + # Kiki Skip + kikiskip = world.get_entrance('Kiki Skip', player) + set_rule(kikiskip, lambda state: state.can_bomb_clip(kikiskip.parent_region, player)) + # Behavior differs based on what type of ER shuffle we're playing. + if not fix_dungeon_exits: # vanilla, simple, restricted, dungeonssimple (this should always have no FWF) + # Dungeons are only shuffled among themselves. We need to check SW and MM because they can't be reentered easily. + pod = world.get_region('Palace of Darkness (Entrance)', player) + if pod.entrances[0].name == 'Skull Woods Final Section': + set_rule(kikiskip, lambda state: False) + elif pod.entrances[0].name == 'Misery Mire': + add_rule(kikiskip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) + + # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. + add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod.entrances[0].can_reach(state)) + elif not fix_fake_worlds: # full, dungeonsfull; has fixed exits but no FWF + # Entry requires the entrance's requirements, but you don't have logical access to the surrounding region. + pod = world.get_region('Palace of Darkness (Entrance)', player) + add_rule(kikiskip, pod.entrances[0].access_rule) + # exiting restriction + add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod.entrances[0].can_reach(state)) + + + + # Mire -> Hera -> Swamp + # Using mire keys on other dungeon doors + mire = world.get_region('Misery Mire (West)', player) + mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and state.can_bomb_clip(mire, player) and state.has_fire_source(player) + hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and state.can_bomb_clip(world.get_region('Tower of Hera (Top)', player), player) + add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or') + add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or') + add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or') + + # Build the rule for SP moat. + # We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT. + # First we require a certain type of entrance shuffle, then build the rule from its pieces. + if not world.swamp_patch_required[player]: + mirrorless_moat_rules = [] + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + mirrorless_moat_rules.append(lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state)) + rule_map = { + 'Misery Mire (Entrance)': (lambda state: True), + 'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player)) + } + inverted = world.mode[player] == 'inverted' + hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \ + rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state) + gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \ + rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state) + mirrorless_moat_rules.append(lambda state: hera_rule(state) or gt_rule(state)) + else: + mirrorless_moat_rules.append(lambda state: False) # all function returns True on empty list + + add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or all([rule(state) for rule in mirrorless_moat_rules])) + + # Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys + mire_to_hera = world.get_entrance('Mire to Hera Clip', player) + mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player) + set_rule(mire_to_hera, mire_clip) + set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player)) + if not fix_dungeon_exits: + hera = world.get_region('Tower of Hera (Bottom)', player) + if hera.entrances[0].name == 'Skull Woods Final Section': + set_rule(mire_to_hera, lambda state: False) + elif hera.entrances[0].name == 'Misery Mire': + add_rule(mire_to_hera, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) + add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera.entrances[0].can_reach(state)) + + swamp = world.get_region('Swamp Palace (Entrance)', player) + if swamp.entrances[0].name == 'Skull Woods Final Section': + set_rule(mire_to_swamp, lambda state: False) + elif swamp.entrances[0].name == 'Misery Mire': + add_rule(mire_to_swamp, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) + add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp.entrances[0].can_reach(state)) + elif not fix_fake_worlds: + hera = world.get_region('Tower of Hera (Bottom)', player) + add_rule(mire_to_hera, hera.entrances[0].access_rule) + add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera.entrances[0].can_reach(state)) + + swamp = world.get_region('Swamp Palace (Entrance)', player) + add_rule(mire_to_swamp, swamp.entrances[0].access_rule) + add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp.entrances[0].can_reach(state)) From b9a783d7d70c0eaa3d85eb462aafc78d158ff226 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 8 Jun 2021 00:50:28 -0500 Subject: [PATCH 05/42] Fixed open connections breaking non-HMG seed generation --- worlds/alttp/Regions.py | 4 ++-- worlds/alttp/UnderworldGlitchRules.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index 7373a05a..89a462ac 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -126,7 +126,7 @@ def create_regions(world, player): create_cave_region(player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), create_lw_region(player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']), create_cave_region(player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), - create_cave_region(player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit', 'Kiki Skip']), + create_cave_region(player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), create_cave_region(player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), create_lw_region(player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']), create_cave_region(player, 'Hookshot Fairy', 'fairies deep in a cave'), @@ -250,7 +250,7 @@ def create_regions(world, player): create_dungeon_region(player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), create_dungeon_region(player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], ['Misery Mire (West)', 'Misery Mire Big Key Door']), - create_dungeon_region(player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest'], ['Mire to Hera Clip', 'Hera to Swamp Clip']), + create_dungeon_region(player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), create_dungeon_region(player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), create_dungeon_region(player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), create_dungeon_region(player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index ed07a34b..b2ecb092 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -5,14 +5,19 @@ from worlds.generic.Rules import set_rule, add_rule # We actually need the logic to properly "mark" these regions as Light or Dark world. # Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. def underworld_glitch_connections(world, player): - kikiskip = world.get_entrance('Kiki Skip', player) - mire_to_hera = world.get_entrance('Mire to Hera Clip', player) - mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player) + specrock = world.get_region('Spectacle Rock Cave (Bottom)', player) + mire = world.get_region('Misery Mire (West)', player) + + kikiskip = Entrance(player, 'Kiki Skip', specrock) + mire_to_hera = Entrance(player, 'Mire to Hera Clip', mire) + mire_to_swamp = Entrance(player, 'Hera to Swamp Clip', mire) + specrock.exits.append(kikiskip) + mire.exits.extend([mire_to_hera, mire_to_swamp]) if world.fix_fake_world[player]: kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region) - mire_to_hera.connect(world.get_entrance('Tower of Hera Exit').connected_region) - mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit').connected_region) + mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region) + mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region) else: kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player)) mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player)) From 2001ca6566d7ab0938f193199e824d0348754076 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 8 Jun 2021 01:22:16 -0500 Subject: [PATCH 06/42] Fixed the check on dungeon reentry not working properly --- worlds/alttp/UnderworldGlitchRules.py | 38 +++++++++++++-------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index b2ecb092..37678ce8 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -38,23 +38,23 @@ def underworld_glitches_rules(world, player): # Kiki Skip kikiskip = world.get_entrance('Kiki Skip', player) set_rule(kikiskip, lambda state: state.can_bomb_clip(kikiskip.parent_region, player)) + pod_entrance = [r for r in world.get_region('Palace of Darkness (Entrance)', player).entrances if r.name != 'Kiki Skip'][0] # Behavior differs based on what type of ER shuffle we're playing. if not fix_dungeon_exits: # vanilla, simple, restricted, dungeonssimple (this should always have no FWF) # Dungeons are only shuffled among themselves. We need to check SW and MM because they can't be reentered easily. - pod = world.get_region('Palace of Darkness (Entrance)', player) - if pod.entrances[0].name == 'Skull Woods Final Section': + if pod_entrance.name == 'Skull Woods Final Section': set_rule(kikiskip, lambda state: False) - elif pod.entrances[0].name == 'Misery Mire': + elif pod_entrance.name == 'Misery Mire': add_rule(kikiskip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. - add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod.entrances[0].can_reach(state)) + add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod_entrance.can_reach(state)) elif not fix_fake_worlds: # full, dungeonsfull; has fixed exits but no FWF # Entry requires the entrance's requirements, but you don't have logical access to the surrounding region. pod = world.get_region('Palace of Darkness (Entrance)', player) - add_rule(kikiskip, pod.entrances[0].access_rule) + add_rule(kikiskip, pod_entrance.access_rule) # exiting restriction - add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod.entrances[0].can_reach(state)) + add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod_entrance.can_reach(state)) @@ -94,25 +94,23 @@ def underworld_glitches_rules(world, player): mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player) set_rule(mire_to_hera, mire_clip) set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player)) + hera_entrance = [r for r in world.get_region('Tower of Hera (Bottom)', player).entrances if r.name != 'Mire to Hera Clip'][0] + swamp_entrance = [r for r in world.get_region('Swamp Palace (Entrance)', player).entrances if r.name != 'Hera to Swamp Clip'][0] if not fix_dungeon_exits: - hera = world.get_region('Tower of Hera (Bottom)', player) - if hera.entrances[0].name == 'Skull Woods Final Section': + if hera_entrance.name == 'Skull Woods Final Section': set_rule(mire_to_hera, lambda state: False) - elif hera.entrances[0].name == 'Misery Mire': + elif hera_entrance.name == 'Misery Mire': add_rule(mire_to_hera, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) - add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera.entrances[0].can_reach(state)) + add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera_entrance.can_reach(state)) - swamp = world.get_region('Swamp Palace (Entrance)', player) - if swamp.entrances[0].name == 'Skull Woods Final Section': + if swamp_entrance.name == 'Skull Woods Final Section': set_rule(mire_to_swamp, lambda state: False) - elif swamp.entrances[0].name == 'Misery Mire': + elif swamp_entrance.name == 'Misery Mire': add_rule(mire_to_swamp, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) - add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp.entrances[0].can_reach(state)) + add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp_entrance.can_reach(state)) elif not fix_fake_worlds: - hera = world.get_region('Tower of Hera (Bottom)', player) - add_rule(mire_to_hera, hera.entrances[0].access_rule) - add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera.entrances[0].can_reach(state)) + add_rule(mire_to_hera, hera_entrance.access_rule) + add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera_entrance.can_reach(state)) - swamp = world.get_region('Swamp Palace (Entrance)', player) - add_rule(mire_to_swamp, swamp.entrances[0].access_rule) - add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp.entrances[0].can_reach(state)) + add_rule(mire_to_swamp, swamp_entrance.access_rule) + add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp_entrance.can_reach(state)) From a73189338c1a45eca713041e01927d9b79bf5688 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 8 Jun 2021 18:15:47 -0500 Subject: [PATCH 07/42] Fixed full ER HMG not ignoring pearl requirements on entrances --- worlds/alttp/UnderworldGlitchRules.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index 37678ce8..21a322d3 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -1,5 +1,6 @@ from BaseClasses import Entrance +from worlds.alttp.Items import ItemFactory from worlds.generic.Rules import set_rule, add_rule # We actually need the logic to properly "mark" these regions as Light or Dark world. @@ -24,6 +25,16 @@ def underworld_glitch_connections(world, player): mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player)) +# For some entrances, we need to fake having pearl, because we're in fake DW/LW. +# This creates a copy of the input state that has Moon Pearl. +def fake_pearl_state(state, player): + if state.has('Moon Pearl', player): + return state + fake_state = state.copy() + fake_state.prog_items['Moon Pearl', player] += 1 + return fake_state + + def underworld_glitches_rules(world, player): fix_dungeon_exits = world.fix_palaceofdarkness_exit[player] fix_fake_worlds = world.fix_fake_world[player] @@ -41,7 +52,7 @@ def underworld_glitches_rules(world, player): pod_entrance = [r for r in world.get_region('Palace of Darkness (Entrance)', player).entrances if r.name != 'Kiki Skip'][0] # Behavior differs based on what type of ER shuffle we're playing. if not fix_dungeon_exits: # vanilla, simple, restricted, dungeonssimple (this should always have no FWF) - # Dungeons are only shuffled among themselves. We need to check SW and MM because they can't be reentered easily. + # Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially. if pod_entrance.name == 'Skull Woods Final Section': set_rule(kikiskip, lambda state: False) elif pod_entrance.name == 'Misery Mire': @@ -50,9 +61,8 @@ def underworld_glitches_rules(world, player): # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod_entrance.can_reach(state)) elif not fix_fake_worlds: # full, dungeonsfull; has fixed exits but no FWF - # Entry requires the entrance's requirements, but you don't have logical access to the surrounding region. - pod = world.get_region('Palace of Darkness (Entrance)', player) - add_rule(kikiskip, pod_entrance.access_rule) + # Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region. + add_rule(kikiskip, lambda state: pod_entrance.access_rule(fake_pearl_state(state, player))) # exiting restriction add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod_entrance.can_reach(state)) @@ -109,8 +119,8 @@ def underworld_glitches_rules(world, player): add_rule(mire_to_swamp, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp_entrance.can_reach(state)) elif not fix_fake_worlds: - add_rule(mire_to_hera, hera_entrance.access_rule) + add_rule(mire_to_hera, lambda state: hera_entrance.access_rule(fake_pearl_state(state, player))) add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera_entrance.can_reach(state)) - add_rule(mire_to_swamp, swamp_entrance.access_rule) + add_rule(mire_to_swamp, lambda state: swamp_entrance.access_rule(fake_pearl_state(state, player))) add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp_entrance.can_reach(state)) From e0d90e0b2135c3a9f6365bd874cf5e9fd3e65233 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 8 Jun 2021 18:17:21 -0500 Subject: [PATCH 08/42] Properly accounting for agatower not freely opening for dungeon reentry --- worlds/alttp/UnderworldGlitchRules.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index 21a322d3..d4fc2f35 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -57,6 +57,8 @@ def underworld_glitches_rules(world, player): set_rule(kikiskip, lambda state: False) elif pod_entrance.name == 'Misery Mire': add_rule(kikiskip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) + elif pod_entrance.name == 'Agahnims Tower': + add_rule(kikiskip, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod_entrance.can_reach(state)) @@ -111,12 +113,16 @@ def underworld_glitches_rules(world, player): set_rule(mire_to_hera, lambda state: False) elif hera_entrance.name == 'Misery Mire': add_rule(mire_to_hera, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) + elif hera_entrance.name == 'Agahnims Tower': + add_rule(mire_to_hera, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera_entrance.can_reach(state)) if swamp_entrance.name == 'Skull Woods Final Section': set_rule(mire_to_swamp, lambda state: False) elif swamp_entrance.name == 'Misery Mire': add_rule(mire_to_swamp, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) + elif swamp_entrance.name == 'Agahnims Tower': + add_rule(mire_to_swamp, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp_entrance.can_reach(state)) elif not fix_fake_worlds: add_rule(mire_to_hera, lambda state: hera_entrance.access_rule(fake_pearl_state(state, player))) From a582a3781bf70d1c5e6779665131dbf3c376e82a Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 8 Jun 2021 18:32:22 -0500 Subject: [PATCH 09/42] Moved the addition of HMG-specific connections to fix crossed ER --- worlds/alttp/EntranceShuffle.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index 87d9911d..64d45d17 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -17,10 +17,6 @@ def link_entrances(world, player): for exitname, regionname in mandatory_connections: connect_simple(world, exitname, regionname, player) - # mandatory hybrid major glitches connections - if world.logic[player] in ['hybridglitches', 'nologic']: - underworld_glitch_connections(world, player) - # if we do not shuffle, set default connections if world.shuffle[player] == 'vanilla': for exitname, regionname in default_connections: @@ -1070,6 +1066,10 @@ def link_entrances(world, player): raise NotImplementedError( f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_names(player)}') + # mandatory hybrid major glitches connections + if world.logic[player] in ['hybridglitches', 'nologic']: + underworld_glitch_connections(world, player) + # check for swamp palace fix if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': world.swamp_patch_required[player] = True @@ -1100,10 +1100,6 @@ def link_inverted_entrances(world, player): for exitname, regionname in inverted_mandatory_connections: connect_simple(world, exitname, regionname, player) - # mandatory hybrid major glitches connections - if world.logic[player] in ['hybridglitches', 'nologic']: - underworld_glitch_connections(world, player) - # if we do not shuffle, set default connections if world.shuffle[player] == 'vanilla': for exitname, regionname in inverted_default_connections: @@ -1775,6 +1771,10 @@ def link_inverted_entrances(world, player): else: raise NotImplementedError('Shuffling not supported yet') + # mandatory hybrid major glitches connections + if world.logic[player] in ['hybridglitches', 'nologic']: + underworld_glitch_connections(world, player) + # patch swamp drain if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': world.swamp_patch_required[player] = True From eaf19643a94c710c6b642ca4877eb210edc7e3d3 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 8 Jun 2021 19:18:28 -0500 Subject: [PATCH 10/42] Cleaned up code for assigning dungeon reentry rules --- worlds/alttp/UnderworldGlitchRules.py | 73 +++++++++++---------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index d4fc2f35..e8d3eb00 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -35,6 +35,32 @@ def fake_pearl_state(state, player): return fake_state +# Sets the rules on where we can actually go using this clip. +# Behavior differs based on what type of ER shuffle we're playing. +def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, dungeon_exit: str): + fix_dungeon_exits = world.fix_palaceofdarkness_exit[player] + fix_fake_worlds = world.fix_fake_world[player] + + dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0] + if not fix_dungeon_exits: # vanilla, simple, restricted, dungeonssimple; should never have fake worlds fix + # Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially. + if dungeon_entrance.name == 'Skull Woods Final Section': + set_rule(clip, lambda state: False) # entrance doesn't exist until you fire rod it from the other side + elif dungeon_entrance.name == 'Misery Mire': + add_rule(clip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # open the dungeon + elif dungeon_entrance.name == 'Agahnims Tower': + add_rule(clip, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier + # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. + add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) + elif not fix_fake_worlds: # full, dungeonsfull; fixed dungeon exits, but no fake worlds fix + # Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region. + add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player))) + # exiting restriction + add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) + # Otherwise, the shuffle type is crossed, dungeonscrossed, or insanity; all of these do not need additional rules on where we can go, + # since the clip links directly to the exterior region. + + def underworld_glitches_rules(world, player): fix_dungeon_exits = world.fix_palaceofdarkness_exit[player] fix_fake_worlds = world.fix_fake_world[player] @@ -45,29 +71,10 @@ def underworld_glitches_rules(world, player): add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player)) - # Kiki Skip kikiskip = world.get_entrance('Kiki Skip', player) set_rule(kikiskip, lambda state: state.can_bomb_clip(kikiskip.parent_region, player)) - pod_entrance = [r for r in world.get_region('Palace of Darkness (Entrance)', player).entrances if r.name != 'Kiki Skip'][0] - # Behavior differs based on what type of ER shuffle we're playing. - if not fix_dungeon_exits: # vanilla, simple, restricted, dungeonssimple (this should always have no FWF) - # Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially. - if pod_entrance.name == 'Skull Woods Final Section': - set_rule(kikiskip, lambda state: False) - elif pod_entrance.name == 'Misery Mire': - add_rule(kikiskip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) - elif pod_entrance.name == 'Agahnims Tower': - add_rule(kikiskip, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) - - # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. - add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod_entrance.can_reach(state)) - elif not fix_fake_worlds: # full, dungeonsfull; has fixed exits but no FWF - # Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region. - add_rule(kikiskip, lambda state: pod_entrance.access_rule(fake_pearl_state(state, player))) - # exiting restriction - add_rule(world.get_entrance('Palace of Darkness Exit', player), lambda state: pod_entrance.can_reach(state)) - + dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit') # Mire -> Hera -> Swamp @@ -106,27 +113,5 @@ def underworld_glitches_rules(world, player): mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player) set_rule(mire_to_hera, mire_clip) set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player)) - hera_entrance = [r for r in world.get_region('Tower of Hera (Bottom)', player).entrances if r.name != 'Mire to Hera Clip'][0] - swamp_entrance = [r for r in world.get_region('Swamp Palace (Entrance)', player).entrances if r.name != 'Hera to Swamp Clip'][0] - if not fix_dungeon_exits: - if hera_entrance.name == 'Skull Woods Final Section': - set_rule(mire_to_hera, lambda state: False) - elif hera_entrance.name == 'Misery Mire': - add_rule(mire_to_hera, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) - elif hera_entrance.name == 'Agahnims Tower': - add_rule(mire_to_hera, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) - add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera_entrance.can_reach(state)) - - if swamp_entrance.name == 'Skull Woods Final Section': - set_rule(mire_to_swamp, lambda state: False) - elif swamp_entrance.name == 'Misery Mire': - add_rule(mire_to_swamp, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) - elif swamp_entrance.name == 'Agahnims Tower': - add_rule(mire_to_swamp, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) - add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp_entrance.can_reach(state)) - elif not fix_fake_worlds: - add_rule(mire_to_hera, lambda state: hera_entrance.access_rule(fake_pearl_state(state, player))) - add_rule(world.get_entrance('Tower of Hera Exit', player), lambda state: hera_entrance.can_reach(state)) - - add_rule(mire_to_swamp, lambda state: swamp_entrance.access_rule(fake_pearl_state(state, player))) - add_rule(world.get_entrance('Swamp Palace Exit', player), lambda state: swamp_entrance.can_reach(state)) + dungeon_reentry_rules(world, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit') + dungeon_reentry_rules(world, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit') From 671fd50cfbaaeb48180f4ac0e48b25cd43a2f5b7 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 8 Jun 2021 19:19:11 -0500 Subject: [PATCH 11/42] Moved the add_rule for mirrorless swamp to speed it up on invalid entrance shuffle type --- worlds/alttp/UnderworldGlitchRules.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index e8d3eb00..27b40845 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -102,11 +102,9 @@ def underworld_glitches_rules(world, player): rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state) gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \ rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state) - mirrorless_moat_rules.append(lambda state: hera_rule(state) or gt_rule(state)) + add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or all([rule(state) for rule in mirrorless_moat_rules])) else: - mirrorless_moat_rules.append(lambda state: False) # all function returns True on empty list - - add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or all([rule(state) for rule in mirrorless_moat_rules])) + add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) # Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys mire_to_hera = world.get_entrance('Mire to Hera Clip', player) From b3b56fcafd3d24205737fd34eb384c17409ebe40 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 8 Jun 2021 19:32:27 -0500 Subject: [PATCH 12/42] removed unnecessary import --- worlds/alttp/UnderworldGlitchRules.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index 27b40845..88c7474c 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -1,6 +1,5 @@ from BaseClasses import Entrance -from worlds.alttp.Items import ItemFactory from worlds.generic.Rules import set_rule, add_rule # We actually need the logic to properly "mark" these regions as Light or Dark world. From 96e13786cd086f74160dc8ac3be1389a947311aa Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Thu, 10 Jun 2021 18:10:25 -0500 Subject: [PATCH 13/42] Fixed broken mirrorless swamp rules --- worlds/alttp/UnderworldGlitchRules.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index 88c7474c..f7e77367 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -89,9 +89,7 @@ def underworld_glitches_rules(world, player): # We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT. # First we require a certain type of entrance shuffle, then build the rule from its pieces. if not world.swamp_patch_required[player]: - mirrorless_moat_rules = [] if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: - mirrorless_moat_rules.append(lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state)) rule_map = { 'Misery Mire (Entrance)': (lambda state: True), 'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player)) @@ -101,7 +99,8 @@ def underworld_glitches_rules(world, player): rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state) gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \ rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state) - add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or all([rule(state) for rule in mirrorless_moat_rules])) + mirrorless_moat_rule = lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state) and (hera_rule(state) or gt_rule(state)) + add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state)) else: add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) From d425e5eb6a4f9859e430524022cc7cfcc1767b4e Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sat, 12 Jun 2021 13:11:14 -0500 Subject: [PATCH 14/42] disable GT junk fill in hybrid --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index 7be4a105..8ecf4428 100644 --- a/Fill.py +++ b/Fill.py @@ -93,7 +93,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # fill in gtower locations with trash first for player in world.alttp_player_ids: if not gftower_trash or not world.ganonstower_vanilla[player] or \ - world.logic[player] in {'owglitches', "nologic"}: + world.logic[player] in {'owglitches', 'hybridmg', "nologic"}: gtower_trash_count = 0 elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1): gtower_trash_count = world.random.randint(world.crystals_needed_for_gt[player] * 2, From deff3569104e9365e082f745829fce1ca0e55b84 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Mon, 14 Jun 2021 22:10:26 -0500 Subject: [PATCH 15/42] Added HMG check to all checks for OWG and NL --- Fill.py | 2 +- worlds/alttp/Bosses.py | 4 ++-- worlds/alttp/EntranceRandomizer.py | 3 ++- worlds/alttp/EntranceShuffle.py | 2 +- worlds/alttp/Rules.py | 14 +++++++------- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Fill.py b/Fill.py index 8ecf4428..d51e2c90 100644 --- a/Fill.py +++ b/Fill.py @@ -93,7 +93,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # fill in gtower locations with trash first for player in world.alttp_player_ids: if not gftower_trash or not world.ganonstower_vanilla[player] or \ - world.logic[player] in {'owglitches', 'hybridmg', "nologic"}: + world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}: gtower_trash_count = 0 elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1): gtower_trash_count = world.random.randint(world.crystals_needed_for_gt[player] * 2, diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 3e462a54..5ea7aba5 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -121,8 +121,8 @@ def GanonDefeatRule(state, player: int): can_hurt = state.has_beam_sword(player) common = can_hurt and state.has_fire_source(player) - # silverless ganon may be needed in minor glitches - if state.world.logic[player] in {"owglitches", "minorglitches", "none"}: + # silverless ganon may be needed in anything higher than no glitches + if state.world.logic[player] != 'noglitches': # need to light torch a sufficient amount of times return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or ( state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index 0a62a2c3..d4c7886b 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -21,13 +21,14 @@ def parse_arguments(argv, no_defaults=False): parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true') - parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'owglitches', 'nologic'], + parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'owglitches', 'hybridglitches', 'nologic'], help='''\ Select Enforcement of Item Requirements. (default: %(default)s) No Glitches: Minor Glitches: May require Fake Flippers, Bunny Revival and Dark Room Navigation. Overworld Glitches: May require overworld glitches. + Hybrid Major Glitches: May require both overworld and underworld clipping. No Logic: Distribute items without regard for item requirements. ''') diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index 64d45d17..62e7dd45 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1949,7 +1949,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): invalid_connections = Must_Exit_Invalid_Connections.copy() invalid_cave_connections = defaultdict(set) - if world.logic[player] in ['owglitches', 'nologic']: + if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: from worlds.alttp import OverworldGlitchRules for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): invalid_connections[entrance] = set() diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 6adb0f58..301df4aa 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -74,7 +74,7 @@ def set_rules(world, player): if world.mode[player] != 'inverted': set_big_bomb_rules(world, player) - if world.logic[player] in {'owglitches', 'nologic'} and world.shuffle[player] not in {'insanity', 'insanity_legacy', 'madness'}: + if world.logic[player] in {'owglitches', 'hybridglitches', 'nologic'} and world.shuffle[player] not in {'insanity', 'insanity_legacy', 'madness'}: path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.world.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or') else: @@ -87,13 +87,13 @@ def set_rules(world, player): # GT Entrance may be required for Turtle Rock for OWG and < 7 required ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.mode[player] == 'inverted' else 'Ganons Tower', player) - if world.crystals_needed_for_gt[player] == 7 and not (world.logic[player] in ['owglitches', 'nologic'] and world.mode[player] != 'inverted'): + if world.crystals_needed_for_gt[player] == 7 and not (world.logic[player] in ['owglitches', 'hybridglitches', 'nologic'] and world.mode[player] != 'inverted'): set_rule(ganons_tower, lambda state: False) set_trock_key_rules(world, player) set_rule(ganons_tower, lambda state: state.has_crystals(state.world.crystals_needed_for_gt[player], player)) - if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'nologic']: + if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') set_bunny_rules(world, player, world.mode[player] == 'inverted') @@ -1394,7 +1394,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): def get_rule_to_add(region, location = None, connecting_entrance = None): # In OWG, a location can potentially be superbunny-mirror accessible or # bunny revival accessible. - if world.logic[player] in ['minorglitches', 'owglitches', 'nologic']: + if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic']: if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic return lambda state: state.has('Moon Pearl', player) if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch @@ -1434,7 +1434,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): seen.add(new_region) if not is_link(new_region): # For glitch rulesets, establish superbunny and revival rules. - if world.logic[player] in ['minorglitches', 'owglitches', 'nologic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): + if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions(): possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has_sword(player)) elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions() @@ -1472,7 +1472,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): # Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival for entrance in world.get_entrances(): if entrance.player == player and is_bunny(entrance.connected_region): - if world.logic[player] in ['minorglitches', 'owglitches', 'nologic'] : + if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] : if entrance.connected_region.type == RegionType.Dungeon: if entrance.parent_region.type != RegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance)) @@ -1480,7 +1480,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): if entrance.connected_region.name == 'Turtle Rock (Entrance)': add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance)) for location in entrance.connected_region.locations: - if world.logic[player] in ['minorglitches', 'owglitches', 'nologic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): + if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): continue if location.name in bunny_accessible_locations: continue From bc9c93b180da45fdb273a9e4dba8055914f880d0 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 15 Jun 2021 21:18:14 -0400 Subject: [PATCH 16/42] Improvements to the WebHost - Improved routing structure - Improved style imports across site - Added placeholder player-settings pages for Factorio and Minecraft --- WebHostLib/__init__.py | 26 +--- .../{playerSettings.js => player-settings.js} | 0 .../static/styles/factorio/factorio.css | 3 + .../player-settings.css} | 0 WebHostLib/static/styles/games.css | 6 - WebHostLib/static/styles/globalStyles.css | 3 - .../static/styles/header/dirtHeader.css | 6 + .../static/styles/header/grassHeader.css | 8 +- .../static/styles/header/oceanHeader.css | 6 + WebHostLib/static/styles/hostRoom.css | 6 - .../static/styles/minecraft/minecraft.css | 3 + .../styles/minecraft/player-settings.css | 129 ++++++++++++++++++ WebHostLib/static/styles/tracker.css | 6 - WebHostLib/static/styles/tutorial.css | 6 - WebHostLib/static/styles/tutorialLanding.css | 6 - WebHostLib/static/styles/weightedSettings.css | 6 - .../static/styles/zelda3/player-settings.css | 129 ++++++++++++++++++ WebHostLib/static/styles/zelda3/zelda3.css | 3 + .../templates/games/factorio/factorio.html | 22 +-- .../games/factorio/player-settings.html | 24 ++++ .../templates/games/minecraft/minecraft.html | 22 +-- .../games/minecraft/player-settings.html | 24 ++++ ...ayerSettings.html => player-settings.html} | 8 +- WebHostLib/templates/games/zelda3/zelda3.html | 25 ++-- 24 files changed, 385 insertions(+), 92 deletions(-) rename WebHostLib/static/assets/zelda3/{playerSettings.js => player-settings.js} (100%) create mode 100644 WebHostLib/static/styles/factorio/factorio.css rename WebHostLib/static/styles/{zelda3/playerSettings.css => factorio/player-settings.css} (100%) create mode 100644 WebHostLib/static/styles/minecraft/minecraft.css create mode 100644 WebHostLib/static/styles/minecraft/player-settings.css create mode 100644 WebHostLib/static/styles/zelda3/player-settings.css create mode 100644 WebHostLib/templates/games/factorio/player-settings.html create mode 100644 WebHostLib/templates/games/minecraft/player-settings.html rename WebHostLib/templates/games/zelda3/{playerSettings.html => player-settings.html} (90%) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 53b87be5..427b1172 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -103,28 +103,10 @@ games_list = { } -# Player settings pages -@app.route('/games//player-settings') -def player_settings(game): - return render_template(f"/games/{game}/playerSettings.html") - - -# Zelda3 pages -@app.route('/games/zelda3/') -def zelda3_pages(page): - return render_template(f"/games/zelda3/{page}.html") - - -# Factorio pages -@app.route('/games/factorio/') -def factorio_pages(page): - return render_template(f"/games/factorio/{page}.html") - - -# Minecraft pages -@app.route('/games/minecraft/') -def minecraft_pages(page): - return render_template(f"/games/factorio/{page}.html") +# Game sub-pages +@app.route('/games//') +def game_pages(game, page): + return render_template(f"/games/{game}/{page}.html") # Game landing pages diff --git a/WebHostLib/static/assets/zelda3/playerSettings.js b/WebHostLib/static/assets/zelda3/player-settings.js similarity index 100% rename from WebHostLib/static/assets/zelda3/playerSettings.js rename to WebHostLib/static/assets/zelda3/player-settings.js diff --git a/WebHostLib/static/styles/factorio/factorio.css b/WebHostLib/static/styles/factorio/factorio.css new file mode 100644 index 00000000..8039500b --- /dev/null +++ b/WebHostLib/static/styles/factorio/factorio.css @@ -0,0 +1,3 @@ +#factorio{ + margin: 1rem; +} diff --git a/WebHostLib/static/styles/zelda3/playerSettings.css b/WebHostLib/static/styles/factorio/player-settings.css similarity index 100% rename from WebHostLib/static/styles/zelda3/playerSettings.css rename to WebHostLib/static/styles/factorio/player-settings.css diff --git a/WebHostLib/static/styles/games.css b/WebHostLib/static/styles/games.css index 0abd17d1..7ba4e2bb 100644 --- a/WebHostLib/static/styles/games.css +++ b/WebHostLib/static/styles/games.css @@ -1,9 +1,3 @@ -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; diff --git a/WebHostLib/static/styles/globalStyles.css b/WebHostLib/static/styles/globalStyles.css index 26b6c48a..b4470a3a 100644 --- a/WebHostLib/static/styles/globalStyles.css +++ b/WebHostLib/static/styles/globalStyles.css @@ -4,9 +4,6 @@ } html{ - background-image: url('../static/backgrounds/oceans/oceans-0002.png'); - background-repeat: repeat; - background-size: 250px 250px; font-family: 'Jost', sans-serif; font-size: 1.1rem; color: #000000; diff --git a/WebHostLib/static/styles/header/dirtHeader.css b/WebHostLib/static/styles/header/dirtHeader.css index b2df59a0..a94fd2fd 100644 --- a/WebHostLib/static/styles/header/dirtHeader.css +++ b/WebHostLib/static/styles/header/dirtHeader.css @@ -1,3 +1,9 @@ +html{ + background-image: url('../../static/backgrounds/dirt/dirt-0005-large.png'); + background-repeat: repeat; + background-size: 900px 900px; +} + #base-header{ background: url('../../static/backgrounds/header/dirt-header.png') repeat-x; } diff --git a/WebHostLib/static/styles/header/grassHeader.css b/WebHostLib/static/styles/header/grassHeader.css index e5943404..7229272d 100644 --- a/WebHostLib/static/styles/header/grassHeader.css +++ b/WebHostLib/static/styles/header/grassHeader.css @@ -1,3 +1,9 @@ -#base-header{ +html{ + background-image: url('../../static/backgrounds/grass/grass-0007-large.png'); + background-repeat: repeat; + background-size: 650px 650px; +} + +#base-header { background: url('../../static/backgrounds/header/grass-header.png') repeat-x; } diff --git a/WebHostLib/static/styles/header/oceanHeader.css b/WebHostLib/static/styles/header/oceanHeader.css index 05d80293..9cea5b28 100644 --- a/WebHostLib/static/styles/header/oceanHeader.css +++ b/WebHostLib/static/styles/header/oceanHeader.css @@ -1,3 +1,9 @@ +html{ + background-image: url('../../static/backgrounds/oceans/oceans-0002.png'); + background-repeat: repeat; + background-size: 250px 250px; +} + #base-header{ background: url('../../static/backgrounds/header/ocean-header.png') repeat-x; } diff --git a/WebHostLib/static/styles/hostRoom.css b/WebHostLib/static/styles/hostRoom.css index ac839d1e..bef8d147 100644 --- a/WebHostLib/static/styles/hostRoom.css +++ b/WebHostLib/static/styles/hostRoom.css @@ -1,9 +1,3 @@ -html{ - background-image: url('../static/backgrounds/grass/grass-0007-large.png'); - background-repeat: repeat; - background-size: 650px 650px; -} - #host-room{ width: calc(100% - 5rem); margin-left: auto; diff --git a/WebHostLib/static/styles/minecraft/minecraft.css b/WebHostLib/static/styles/minecraft/minecraft.css new file mode 100644 index 00000000..063cc624 --- /dev/null +++ b/WebHostLib/static/styles/minecraft/minecraft.css @@ -0,0 +1,3 @@ +#minecraft{ + margin: 1rem; +} diff --git a/WebHostLib/static/styles/minecraft/player-settings.css b/WebHostLib/static/styles/minecraft/player-settings.css new file mode 100644 index 00000000..e92c4868 --- /dev/null +++ b/WebHostLib/static/styles/minecraft/player-settings.css @@ -0,0 +1,129 @@ +html{ + background-image: url('../../static/backgrounds/grass/grass-0007-large.png'); + background-repeat: repeat; + background-size: 650px 650px; +} + +#player-settings{ + max-width: 1000px; + margin-left: auto; + margin-right: auto; + background-color: rgba(0, 0, 0, 0.15); + border-radius: 8px; + padding: 1rem; + color: #eeffeb; +} + +#player-settings #player-settings-button-row{ + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 15px; +} + +#player-settings code{ + background-color: #d9cd8e; + border-radius: 4px; + padding-left: 0.25rem; + padding-right: 0.25rem; + color: #000000; +} + +#player-settings #user-message{ + display: none; + width: calc(100% - 8px); + background-color: #ffe86b; + border-radius: 4px; + color: #000000; + padding: 4px; + text-align: center; +} + +#player-settings #user-message.visible{ + display: block; +} + +#player-settings 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; +} + +#player-settings 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; +} + +#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{ + color: #ffffff; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} + +#player-settings a{ + color: #ffef00; +} + +#player-settings input:not([type]){ + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; +} + +#player-settings input:not([type]):focus{ + border: 1px solid #ffffff; +} + +#player-settings select{ + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; + background-color: #ffffff; +} + +#player-settings #game-options, #player-settings #rom-options{ + display: flex; + flex-direction: row; +} + +#player-settings .left, #player-settings .right{ + flex-grow: 1; +} + +#player-settings table select{ + width: 250px; +} + +#player-settings table label{ + display: block; + min-width: 200px; + margin-right: 4px; + cursor: default; +} + +@media all and (max-width: 1000px), all and (orientation: portrait){ + #player-settings #game-options, #player-settings #rom-options{ + justify-content: flex-start; + flex-wrap: wrap; + } + + #player-settings .left, #player-settings .right{ + flex-grow: unset; + } + + #game-options table label, #rom-options table label{ + display: block; + min-width: 200px; + } +} diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css index dc12f205..c6e84aea 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/tracker.css @@ -1,9 +1,3 @@ -html{ - background-image: url('../static/backgrounds/dirt/dirt-0005-large.png'); - background-repeat: repeat; - background-size: 900px 900px; -} - #tracker-wrapper { display: flex; flex-direction: column; diff --git a/WebHostLib/static/styles/tutorial.css b/WebHostLib/static/styles/tutorial.css index b7ebeae4..55717fdc 100644 --- a/WebHostLib/static/styles/tutorial.css +++ b/WebHostLib/static/styles/tutorial.css @@ -1,9 +1,3 @@ -html{ - background-image: url('../static/backgrounds/grass/grass-0007-large.png'); - background-repeat: repeat; - background-size: 650px 650px; -} - #tutorial-wrapper{ display: flex; flex-direction: column; diff --git a/WebHostLib/static/styles/tutorialLanding.css b/WebHostLib/static/styles/tutorialLanding.css index f20489a8..d1d71012 100644 --- a/WebHostLib/static/styles/tutorialLanding.css +++ b/WebHostLib/static/styles/tutorialLanding.css @@ -1,9 +1,3 @@ -html{ - background-image: url('../static/backgrounds/grass/grass-0007-large.png'); - background-repeat: repeat; - background-size: 650px 650px; -} - #tutorial-landing{ display: flex; flex-direction: column; diff --git a/WebHostLib/static/styles/weightedSettings.css b/WebHostLib/static/styles/weightedSettings.css index d887f599..95c004b2 100644 --- a/WebHostLib/static/styles/weightedSettings.css +++ b/WebHostLib/static/styles/weightedSettings.css @@ -1,9 +1,3 @@ -html{ - background-image: url('../static/backgrounds/grass/grass-0007-large.png'); - background-repeat: repeat; - background-size: 650px 650px; -} - #weighted-settings{ width: 60rem; margin-left: auto; diff --git a/WebHostLib/static/styles/zelda3/player-settings.css b/WebHostLib/static/styles/zelda3/player-settings.css new file mode 100644 index 00000000..e92c4868 --- /dev/null +++ b/WebHostLib/static/styles/zelda3/player-settings.css @@ -0,0 +1,129 @@ +html{ + background-image: url('../../static/backgrounds/grass/grass-0007-large.png'); + background-repeat: repeat; + background-size: 650px 650px; +} + +#player-settings{ + max-width: 1000px; + margin-left: auto; + margin-right: auto; + background-color: rgba(0, 0, 0, 0.15); + border-radius: 8px; + padding: 1rem; + color: #eeffeb; +} + +#player-settings #player-settings-button-row{ + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 15px; +} + +#player-settings code{ + background-color: #d9cd8e; + border-radius: 4px; + padding-left: 0.25rem; + padding-right: 0.25rem; + color: #000000; +} + +#player-settings #user-message{ + display: none; + width: calc(100% - 8px); + background-color: #ffe86b; + border-radius: 4px; + color: #000000; + padding: 4px; + text-align: center; +} + +#player-settings #user-message.visible{ + display: block; +} + +#player-settings 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; +} + +#player-settings 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; +} + +#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{ + color: #ffffff; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} + +#player-settings a{ + color: #ffef00; +} + +#player-settings input:not([type]){ + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; +} + +#player-settings input:not([type]):focus{ + border: 1px solid #ffffff; +} + +#player-settings select{ + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; + background-color: #ffffff; +} + +#player-settings #game-options, #player-settings #rom-options{ + display: flex; + flex-direction: row; +} + +#player-settings .left, #player-settings .right{ + flex-grow: 1; +} + +#player-settings table select{ + width: 250px; +} + +#player-settings table label{ + display: block; + min-width: 200px; + margin-right: 4px; + cursor: default; +} + +@media all and (max-width: 1000px), all and (orientation: portrait){ + #player-settings #game-options, #player-settings #rom-options{ + justify-content: flex-start; + flex-wrap: wrap; + } + + #player-settings .left, #player-settings .right{ + flex-grow: unset; + } + + #game-options table label, #rom-options table label{ + display: block; + min-width: 200px; + } +} diff --git a/WebHostLib/static/styles/zelda3/zelda3.css b/WebHostLib/static/styles/zelda3/zelda3.css index e69de29b..bf00d4a2 100644 --- a/WebHostLib/static/styles/zelda3/zelda3.css +++ b/WebHostLib/static/styles/zelda3/zelda3.css @@ -0,0 +1,3 @@ +#zelda3{ + margin: 1rem; +} diff --git a/WebHostLib/templates/games/factorio/factorio.html b/WebHostLib/templates/games/factorio/factorio.html index e808d9ab..6dc6a530 100644 --- a/WebHostLib/templates/games/factorio/factorio.html +++ b/WebHostLib/templates/games/factorio/factorio.html @@ -1,9 +1,15 @@ - - - +{% extends 'pageWrapper.html' %} + +{% block head %} Factorio - - - Factorio - - + + + +{% endblock %} + +{% block body %} + {% include 'header/grassHeader.html' %} +
+ Coming Soonâ„¢ +
+{% endblock %} diff --git a/WebHostLib/templates/games/factorio/player-settings.html b/WebHostLib/templates/games/factorio/player-settings.html new file mode 100644 index 00000000..57435936 --- /dev/null +++ b/WebHostLib/templates/games/factorio/player-settings.html @@ -0,0 +1,24 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + Factorio Settings + +{% endblock %} + +{% block body %} + {% include 'header/grassHeader.html' %} +
+
+

Factorio Settings

+

Choose the options you would like to play with! You may generate a single-player game from this page, + or download a settings file you can use to participate in a MultiWorld. If you would like to make + your settings extra random, check out the advanced weighted settings + page. There, you will find examples of all available sprites as well.

+ +

A list of all games you have generated can be found here.

+ +
+ More content coming soonâ„¢. +
+
+{% endblock %} diff --git a/WebHostLib/templates/games/minecraft/minecraft.html b/WebHostLib/templates/games/minecraft/minecraft.html index f37dc14f..1e758411 100644 --- a/WebHostLib/templates/games/minecraft/minecraft.html +++ b/WebHostLib/templates/games/minecraft/minecraft.html @@ -1,9 +1,15 @@ - - - +{% extends 'pageWrapper.html' %} + +{% block head %} Minecraft - - - Minecraft - - + + + +{% endblock %} + +{% block body %} + {% include 'header/grassHeader.html' %} +
+ Coming Soonâ„¢ +
+{% endblock %} diff --git a/WebHostLib/templates/games/minecraft/player-settings.html b/WebHostLib/templates/games/minecraft/player-settings.html new file mode 100644 index 00000000..71865756 --- /dev/null +++ b/WebHostLib/templates/games/minecraft/player-settings.html @@ -0,0 +1,24 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + Minecraft Settings + +{% endblock %} + +{% block body %} + {% include 'header/grassHeader.html' %} +
+
+

Minecraft Settings

+

Choose the options you would like to play with! You may generate a single-player game from this page, + or download a settings file you can use to participate in a MultiWorld. If you would like to make + your settings extra random, check out the advanced weighted settings + page. There, you will find examples of all available sprites as well.

+ +

A list of all games you have generated can be found here.

+ +
+ More content coming soonâ„¢. +
+
+{% endblock %} diff --git a/WebHostLib/templates/games/zelda3/playerSettings.html b/WebHostLib/templates/games/zelda3/player-settings.html similarity index 90% rename from WebHostLib/templates/games/zelda3/playerSettings.html rename to WebHostLib/templates/games/zelda3/player-settings.html index 681b7140..107b9bf3 100644 --- a/WebHostLib/templates/games/zelda3/playerSettings.html +++ b/WebHostLib/templates/games/zelda3/player-settings.html @@ -1,18 +1,18 @@ {% extends 'pageWrapper.html' %} {% block head %} - Player Settings - + A Link to the Past Settings + - + {% endblock %} {% block body %} {% include 'header/grassHeader.html' %}
-

Start Game

+

A Link to the Past Settings

Choose the options you would like to play with! You may generate a single-player game from this page, or download a settings file you can use to participate in a MultiWorld. If you would like to make your settings extra random, check out the advanced weighted settings diff --git a/WebHostLib/templates/games/zelda3/zelda3.html b/WebHostLib/templates/games/zelda3/zelda3.html index 4d8b6192..49d4c702 100644 --- a/WebHostLib/templates/games/zelda3/zelda3.html +++ b/WebHostLib/templates/games/zelda3/zelda3.html @@ -1,10 +1,15 @@ - - - - Link to the Past - - - Link to the Past
- Player Settings - - +{% extends 'pageWrapper.html' %} + +{% block head %} + A Link to the Past + + + +{% endblock %} + +{% block body %} + {% include 'header/grassHeader.html' %} +

+ Coming Soonâ„¢ +
+{% endblock %} From a5bf3a8407fd228f8f43c61b0f5a27c193e896e9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 16 Jun 2021 23:41:38 +0200 Subject: [PATCH 17/42] Factorio: remove option to turn off random_tech_ingredients --- Options.py | 1 - worlds/factorio/Technologies.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Options.py b/Options.py index 4ba04a04..fea57b88 100644 --- a/Options.py +++ b/Options.py @@ -433,7 +433,6 @@ factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxScien "tech_cost": TechCost, "free_samples": FreeSamples, "visibility": Visibility, - "random_tech_ingredients": Toggle, "starting_items": FactorioStartItems, "recipe_time": RecipeTime} diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 4eb01e6e..6754f5aa 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -66,7 +66,7 @@ class CustomTechnology(Technology): def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int): ingredients = origin.ingredients & allowed_packs self.player = player - if world.random_tech_ingredients[player] and origin.name not in world.worlds[player].static_nodes: + if origin.name not in world.worlds[player].static_nodes: ingredients = list(ingredients) ingredients.sort() # deterministic sample ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients))) From a08d7bb1b2e6c79629becab1f1fd565dafbf3b33 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 18 Jun 2021 22:15:54 +0200 Subject: [PATCH 18/42] Settings: add requires --- FactorioClient.py | 2 +- LttPClient.py | 2 +- Main.py | 4 ++-- MultiServer.py | 10 +++++----- Mystery.py | 18 +++++++++++++++++- Utils.py | 4 ++-- playerSettings.yaml | 2 ++ setup.py | 4 ++-- 8 files changed, 32 insertions(+), 14 deletions(-) diff --git a/FactorioClient.py b/FactorioClient.py index 8567f299..0e9017fd 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -76,7 +76,7 @@ class FactorioContext(CommonContext): await super(FactorioContext, self).server_auth(password_requested) await self.send_msgs([{"cmd": 'Connect', - 'password': self.password, 'name': self.auth, 'version': Utils._version_tuple, + 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, 'tags': ['AP'], 'uuid': Utils.get_unique_identifier(), 'game': "Factorio" }]) diff --git a/LttPClient.py b/LttPClient.py index 828fe17e..4c0f1f65 100644 --- a/LttPClient.py +++ b/LttPClient.py @@ -97,7 +97,7 @@ class Context(CommonContext): self.auth = self.rom auth = base64.b64encode(self.rom).decode() await self.send_msgs([{"cmd": 'Connect', - 'password': self.password, 'name': auth, 'version': Utils._version_tuple, + 'password': self.password, 'name': auth, 'version': Utils.version_tuple, 'tags': get_tags(self), 'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past" }]) diff --git a/Main.py b/Main.py index c35f5b9d..2ef8ccc4 100644 --- a/Main.py +++ b/Main.py @@ -20,7 +20,7 @@ from worlds.alttp.Dungeons import create_dungeons, fill_dungeons, fill_dungeons_ from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes -from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple +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.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data @@ -557,7 +557,7 @@ def main(args, seed=None): "er_hint_data": er_hint_data, "precollected_items": precollected_items, "precollected_hints": precollected_hints, - "version": tuple(_version_tuple), + "version": tuple(version_tuple), "tags": ["AP"], "minimum_versions": minimum_versions, "seed_name": world.seed_name diff --git a/MultiServer.py b/MultiServer.py index 592dc13b..feba63bf 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -30,7 +30,7 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_ lookup_any_location_id_to_name, lookup_any_location_name_to_id import Utils from Utils import get_item_name_from_id, get_location_name_from_id, \ - _version_tuple, restricted_loads, Version + version_tuple, restricted_loads, Version from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer colorama.init() @@ -136,9 +136,9 @@ class Context(Node): def _load(self, decoded_obj: dict, use_embedded_server_options: bool): mdata_ver = decoded_obj["minimum_versions"]["server"] - if mdata_ver > Utils._version_tuple: + if mdata_ver > Utils.version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," - f"however this server is of version {Utils._version_tuple}") + f"however this server is of version {Utils.version_tuple}") clients_ver = decoded_obj["minimum_versions"].get("clients", {}) self.minimum_client_versions = {} for player, version in clients_ver.items(): @@ -379,7 +379,7 @@ async def on_client_connected(ctx: Context, client: Client): # tags are for additional features in the communication. # Name them by feature or fork, as you feel is appropriate. 'tags': ctx.tags, - 'version': Utils._version_tuple, + 'version': Utils.version_tuple, 'forfeit_mode': ctx.forfeit_mode, 'remaining_mode': ctx.remaining_mode, 'hint_cost': ctx.hint_cost, @@ -1011,7 +1011,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): errors.add('IncompatibleVersion') # only exact version match allowed - if ctx.compatibility == 0 and args['version'] != _version_tuple: + if ctx.compatibility == 0 and args['version'] != version_tuple: errors.add('IncompatibleVersion') if errors: logging.info(f"A client connection was refused due to: {errors}") diff --git a/Mystery.py b/Mystery.py index 074b3e87..4f56ee14 100644 --- a/Mystery.py +++ b/Mystery.py @@ -13,7 +13,7 @@ from worlds.generic import PlandoItem, PlandoConnection ModuleUpdate.update() -from Utils import parse_yaml +from Utils import parse_yaml, version_tuple, __version__, tuplize_version from worlds.alttp.EntranceRandomizer import parse_arguments from Main import main as ERmain from Main import get_seed, seeddigits @@ -453,6 +453,22 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))): + requirements = weights.get("requires", {}) + if requirements: + version = requirements.get("version", __version__) + if tuplize_version(version) > version_tuple: + raise Exception(f"Settings reports required version of generator is at least {version}, " + f"however generator is of version {__version__}") + required_plando_options = requirements.get("plando", "") + required_plando_options = set(option.strip() for option in required_plando_options.split(",")) + required_plando_options -= plando_options + if required_plando_options: + if len(required_plando_options) == 1: + raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, " + f"which is not enabled.") + else: + raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, " + f"which are not enabled.") if "pre_rolled" in weights: pre_rolled = weights["pre_rolled"] diff --git a/Utils.py b/Utils.py index 1e1e3341..f82c9622 100644 --- a/Utils.py +++ b/Utils.py @@ -12,8 +12,8 @@ class Version(typing.NamedTuple): minor: int build: int -__version__ = "0.1.2" -_version_tuple = tuplize_version(__version__) +__version__ = "0.1.3" +version_tuple = tuplize_version(__version__) import builtins import os diff --git a/playerSettings.yaml b/playerSettings.yaml index b6f6f6c3..769f3208 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -27,6 +27,8 @@ game: A Link to the Past: 1 Factorio: 1 Minecraft: 1 +requires: + version: 0.1.3 # Version of Archipelago required for this yaml to work as expected. # Shared Options supported by all games: accessibility: items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations diff --git a/setup.py b/setup.py index 8bc3f45d..34bba7f4 100644 --- a/setup.py +++ b/setup.py @@ -48,10 +48,10 @@ def manifest_creation(folder): path = os.path.join(dirpath, filename) hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path) import json - from Utils import _version_tuple + from Utils import version_tuple manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"), "hashes": {path: hash.result() for path, hash in hashes.items()}, - "version": _version_tuple} + "version": version_tuple} json.dump(manifest, open(manifestpath, "wt"), indent=4) print("Created Manifest") From 741ec36ee11033caf7060a10e0bafcc99a6c4b1f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 18 Jun 2021 23:17:12 +0200 Subject: [PATCH 19/42] all requires to be modified by trigggers and linked options --- Mystery.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Mystery.py b/Mystery.py index 4f56ee14..947a67fb 100644 --- a/Mystery.py +++ b/Mystery.py @@ -453,22 +453,6 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))): - requirements = weights.get("requires", {}) - if requirements: - version = requirements.get("version", __version__) - if tuplize_version(version) > version_tuple: - raise Exception(f"Settings reports required version of generator is at least {version}, " - f"however generator is of version {__version__}") - required_plando_options = requirements.get("plando", "") - required_plando_options = set(option.strip() for option in required_plando_options.split(",")) - required_plando_options -= plando_options - if required_plando_options: - if len(required_plando_options) == 1: - raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, " - f"which is not enabled.") - else: - raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, " - f"which are not enabled.") if "pre_rolled" in weights: pre_rolled = weights["pre_rolled"] @@ -506,6 +490,23 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b if "triggers" in weights: weights = roll_triggers(weights) + requirements = weights.get("requires", {}) + if requirements: + version = requirements.get("version", __version__) + if tuplize_version(version) > version_tuple: + raise Exception(f"Settings reports required version of generator is at least {version}, " + f"however generator is of version {__version__}") + required_plando_options = requirements.get("plando", "") + required_plando_options = set(option.strip() for option in required_plando_options.split(",")) + required_plando_options -= plando_options + if required_plando_options: + if len(required_plando_options) == 1: + raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, " + f"which is not enabled.") + else: + raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, " + f"which are not enabled.") + ret = argparse.Namespace() ret.name = get_choice('name', weights) ret.accessibility = get_choice('accessibility', weights) From 644d62c9151ad5746f3ca108c2eab9f4477388a6 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 18 Jun 2021 14:23:55 -0700 Subject: [PATCH 20/42] Ignore Factorio AP savegame file. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4dc8cdd5..8681ab69 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,5 @@ dmypy.json # Cython debug symbols cython_debug/ + +Archipelago.zip From f8fd8b3585fc14d2f654f0df003eda7648c24f05 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 19 Jun 2021 01:00:21 +0200 Subject: [PATCH 21/42] Factorio: add toggle to disable imported blueprints --- Options.py | 93 +++++++++++--------------- data/factorio/mod_template/control.lua | 18 +++-- playerSettings.yaml | 2 +- 3 files changed, 52 insertions(+), 61 deletions(-) diff --git a/Options.py b/Options.py index fea57b88..6a24b96d 100644 --- a/Options.py +++ b/Options.py @@ -121,6 +121,8 @@ class Toggle(Option): def get_option_name(self): return bool(self.value) +class DefaultOnToggle(Toggle): + default = 1 class Choice(Option): def __init__(self, value: int): @@ -292,56 +294,30 @@ alttp_options: typing.Dict[str, type(Option)] = { "shop_item_slots": ShopItemSlots, } -mapshuffle = Toggle -compassshuffle = Toggle -keyshuffle = Toggle -bigkeyshuffle = Toggle -hints = Toggle -RandomizeDreamers = Toggle -RandomizeSkills = Toggle -RandomizeCharms = Toggle -RandomizeKeys = Toggle -RandomizeGeoChests = Toggle -RandomizeMaskShards = Toggle -RandomizeVesselFragments = Toggle -RandomizeCharmNotches = Toggle -RandomizePaleOre = Toggle -RandomizeRancidEggs = Toggle -RandomizeRelics = Toggle -RandomizeMaps = Toggle -RandomizeStags = Toggle -RandomizeGrubs = Toggle -RandomizeWhisperingRoots = Toggle -RandomizeRocks = Toggle -RandomizeSoulTotems = Toggle -RandomizePalaceTotems = Toggle -RandomizeLoreTablets = Toggle -RandomizeLifebloodCocoons = Toggle -RandomizeFlames = Toggle hollow_knight_randomize_options: typing.Dict[str, type(Option)] = { - "RandomizeDreamers": RandomizeDreamers, - "RandomizeSkills": RandomizeSkills, - "RandomizeCharms": RandomizeCharms, - "RandomizeKeys": RandomizeKeys, - "RandomizeGeoChests": RandomizeGeoChests, - "RandomizeMaskShards": RandomizeMaskShards, - "RandomizeVesselFragments": RandomizeVesselFragments, - "RandomizeCharmNotches": RandomizeCharmNotches, - "RandomizePaleOre": RandomizePaleOre, - "RandomizeRancidEggs": RandomizeRancidEggs, - "RandomizeRelics": RandomizeRelics, - "RandomizeMaps": RandomizeMaps, - "RandomizeStags": RandomizeStags, - "RandomizeGrubs": RandomizeGrubs, - "RandomizeWhisperingRoots": RandomizeWhisperingRoots, - "RandomizeRocks": RandomizeRocks, - "RandomizeSoulTotems": RandomizeSoulTotems, - "RandomizePalaceTotems": RandomizePalaceTotems, - "RandomizeLoreTablets": RandomizeLoreTablets, - "RandomizeLifebloodCocoons": RandomizeLifebloodCocoons, - "RandomizeFlames": RandomizeFlames + "RandomizeDreamers": DefaultOnToggle, + "RandomizeSkills": DefaultOnToggle, + "RandomizeCharms": DefaultOnToggle, + "RandomizeKeys": DefaultOnToggle, + "RandomizeGeoChests": Toggle, + "RandomizeMaskShards": DefaultOnToggle, + "RandomizeVesselFragments": DefaultOnToggle, + "RandomizeCharmNotches": Toggle, + "RandomizePaleOre": DefaultOnToggle, + "RandomizeRancidEggs": Toggle, + "RandomizeRelics": DefaultOnToggle, + "RandomizeMaps": Toggle, + "RandomizeStags": Toggle, + "RandomizeGrubs": Toggle, + "RandomizeWhisperingRoots": Toggle, + "RandomizeRocks": Toggle, + "RandomizeSoulTotems": Toggle, + "RandomizePalaceTotems": Toggle, + "RandomizeLoreTablets": Toggle, + "RandomizeLifebloodCocoons": Toggle, + "RandomizeFlames": Toggle } hollow_knight_skip_options: typing.Dict[str, type(Option)] = { @@ -428,13 +404,16 @@ class FactorioStartItems(OptionDict): default = {"burner-mining-drill": 19, "stone-furnace": 19} -factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack, - "tech_tree_layout": TechTreeLayout, - "tech_cost": TechCost, - "free_samples": FreeSamples, - "visibility": Visibility, - "starting_items": FactorioStartItems, - "recipe_time": RecipeTime} +factorio_options: typing.Dict[str, type(Option)] = { + "max_science_pack": MaxSciencePack, + "tech_tree_layout": TechTreeLayout, + "tech_cost": TechCost, + "free_samples": FreeSamples, + "visibility": Visibility, + "starting_items": FactorioStartItems, + "recipe_time": RecipeTime, + "imported_blueprints": DefaultOnToggle, +} class AdvancementGoal(Choice): @@ -469,7 +448,11 @@ option_sets = ( if __name__ == "__main__": import argparse - + mapshuffle = Toggle + compassshuffle = Toggle + keyshuffle = Toggle + bigkeyshuffle = Toggle + hints = Toggle test = argparse.Namespace() test.logic = Logic.from_text("no_logic") test.mapshuffle = mapshuffle.from_text("ON") diff --git a/data/factorio/mod_template/control.lua b/data/factorio/mod_template/control.lua index c7dfe867..47e97fe0 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -6,7 +6,16 @@ require "util" FREE_SAMPLES = {{ free_samples }} SLOT_NAME = "{{ slot_name }}" SEED_NAME = "{{ seed_name }}" ---SUPPRESS_INVENTORY_EVENTS = false + +{% if not imported_blueprints -%} +function set_permissions() + local group = game.permissions.get_group("Default") + group.set_allows_action(defines.input_action.open_blueprint_library_gui, false) + group.set_allows_action(defines.input_action.import_blueprint, false) + group.set_allows_action(defines.input_action.import_blueprint_string, false) + group.set_allows_action(defines.input_action.import_blueprints_filtered, false) +end +{%- endif %} -- Initialize force data, either from it being created or already being part of the game when the mod was added. function on_force_created(event) @@ -63,7 +72,7 @@ function update_player(index) local sent --player.print(serpent.block(data['pending_samples'])) local stack = {} - --SUPPRESS_INVENTORY_EVENTS = true + for name, count in pairs(samples) do stack.name = name stack.count = count @@ -87,16 +96,14 @@ function update_player(index) samples[name] = nil -- Remove from the list end end - --SUPPRESS_INVENTORY_EVENTS = false + end -- Update players upon them connecting, since updates while they're offline are suppressed. script.on_event(defines.events.on_player_joined_game, function(event) update_player(event.player_index) end) function update_player_event(event) - --if not SUPPRESS_INVENTORY_EVENTS then update_player(event.player_index) - --end end script.on_event(defines.events.on_player_main_inventory_changed, update_player_event) @@ -115,6 +122,7 @@ function add_samples(force, name, count) end script.on_init(function() + {% if not imported_blueprints %}set_permissions(){% endif %} global.forcedata = {} global.playerdata = {} -- Fire dummy events for all currently existing forces. diff --git a/playerSettings.yaml b/playerSettings.yaml index 769f3208..4ca845c1 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -94,7 +94,7 @@ Factorio: visibility: none: 0 sending: 1 - random_tech_ingredients: + imported_blueprints: # can be turned off to prevent access to blueprints created outside the current world on: 1 off: 0 starting_items: From 1e7214a86b7ebc05b339c3323020b1492dedcf7a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 19 Jun 2021 01:00:41 +0200 Subject: [PATCH 22/42] fix required plando options triggering on empty string --- Mystery.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Mystery.py b/Mystery.py index 947a67fb..8f36e7d7 100644 --- a/Mystery.py +++ b/Mystery.py @@ -497,15 +497,16 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b raise Exception(f"Settings reports required version of generator is at least {version}, " f"however generator is of version {__version__}") required_plando_options = requirements.get("plando", "") - required_plando_options = set(option.strip() for option in required_plando_options.split(",")) - required_plando_options -= plando_options if required_plando_options: - if len(required_plando_options) == 1: - raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, " - f"which is not enabled.") - else: - raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, " - f"which are not enabled.") + required_plando_options = set(option.strip() for option in required_plando_options.split(",")) + required_plando_options -= plando_options + if required_plando_options: + if len(required_plando_options) == 1: + raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, " + f"which is not enabled.") + else: + raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, " + f"which are not enabled.") ret = argparse.Namespace() ret.name = get_choice('name', weights) From f4a2f344a7cac15edb800c0b621eccc93527cc8b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 19 Jun 2021 03:03:06 +0200 Subject: [PATCH 23/42] format MultiServer.py --- MultiServer.py | 68 +++++++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index feba63bf..9d51352f 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -39,6 +39,7 @@ all_items = frozenset(lookup_any_item_name_to_id) all_locations = frozenset(lookup_any_location_name_to_id) all_console_names = frozenset(all_items | all_locations) + class Client(Endpoint): version = Version(0, 0, 0) tags: typing.List[str] = [] @@ -75,10 +76,10 @@ class Context(Node): self.save_filename = None self.saving = False self.player_names = {} - self.connect_names = {} # names of slots clients can connect to + self.connect_names = {} # names of slots clients can connect to self.allow_forfeits = {} self.remote_items = set() - self.locations:typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {} + self.locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {} self.host = host self.port = port self.server_password = server_password @@ -166,7 +167,6 @@ class Context(Node): server_options = decoded_obj.get("server_options", {}) self._set_options(server_options) - def get_players_package(self): return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()] @@ -174,7 +174,7 @@ class Context(Node): for key, value in server_options.items(): data_type = self.simple_options.get(key, None) if data_type is not None: - if value not in {False, True, None}: # some can be boolean OR text, such as password + if value not in {False, True, None}: # some can be boolean OR text, such as password try: value = data_type(value) except Exception as e: @@ -200,7 +200,7 @@ class Context(Node): return False - def _save(self, exit_save:bool=False) -> bool: + def _save(self, exit_save: bool = False) -> bool: try: encoded_save = pickle.dumps(self.get_save()) with open(self.save_filename, "wb") as f: @@ -366,7 +366,8 @@ async def server(websocket, path, ctx: Context): if not isinstance(e, websockets.WebSocketException): logging.exception(e) finally: - logging.info("Disconnected") + if ctx.log_network: + logging.info("Disconnected") await ctx.disconnect(client) @@ -374,8 +375,10 @@ async def on_client_connected(ctx: Context, client: Client): await ctx.send_msgs(client, [{ 'cmd': 'RoomInfo', 'password': ctx.password is not None, - 'players': [NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name), client.name) for client - in ctx.endpoints if client.auth], + 'players': [ + NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name), + client.name) for client + in ctx.endpoints if client.auth], # tags are for additional features in the communication. # Name them by feature or fork, as you feel is appropriate. 'tags': ctx.tags, @@ -403,9 +406,11 @@ async def on_client_joined(ctx: Context, client: Client): ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) + async def on_client_left(ctx: Context, client: Client): update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN) - ctx.notify_all("%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1)) + ctx.notify_all( + "%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1)) ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) @@ -447,7 +452,7 @@ def get_received_items(ctx: Context, team: int, player: int) -> typing.List[Netw def send_new_items(ctx: Context): for client in ctx.endpoints: - if client.auth: # can't send to disconnected client + if client.auth: # can't send to disconnected client items = get_received_items(ctx, client.team, client.slot) if len(items) > client.send_index: asyncio.create_task(ctx.send_msgs(client, [{ @@ -504,7 +509,6 @@ def notify_team(ctx: Context, team: int, text: str): ctx.broadcast_team(team, [['Print', {"text": text}]]) - def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]: hints = [] seeked_item_id = lookup_any_item_name_to_id[item] @@ -520,7 +524,6 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[ def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: - seeked_location: int = Regions.lookup_name_to_id[location] item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None)) if item_id: @@ -540,6 +543,7 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: text += f" at {hint.entrance}" return text + (". (found)" if hint.found else ".") + def json_format_send_event(net_item: NetworkItem, receiving_player: int): parts = [] NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id) @@ -559,7 +563,9 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int): return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend", "receiving": receiving_player, "sending": net_item.player} -def get_intended_text(input_text: str, possible_answers: typing.Iterable[str]= all_console_names) -> typing.Tuple[str, bool, str]: + +def get_intended_text(input_text: str, possible_answers: typing.Iterable[str] = all_console_names) -> typing.Tuple[ + str, bool, str]: picks = fuzzy_process.extract(input_text, possible_answers, limit=2) if len(picks) > 1: dif = picks[0][1] - picks[1][1] @@ -684,11 +690,12 @@ class CommonCommandProcessor(CommandProcessor): """List all current options. Warning: lists password.""" self.output("Current options:") for option in self.ctx.simple_options: - if option == "server_password" and self.marker == "!": #Do not display the server password to the client. - self.output(f"Option server_password is set to {('*' * random.randint(4,16))}") + if option == "server_password" and self.marker == "!": # Do not display the server password to the client. + self.output(f"Option server_password is set to {('*' * random.randint(4, 16))}") else: self.output(f"Option {option} is set to {getattr(self.ctx, option)}") + class ClientMessageProcessor(CommonCommandProcessor): marker = "!" @@ -715,11 +722,14 @@ class ClientMessageProcessor(CommonCommandProcessor): """Allow remote administration of the multiworld server""" output = f"!admin {command}" - if output.lower().startswith("!admin login"): # disallow others from seeing the supplied password, whether or not it is correct. + if output.lower().startswith( + "!admin login"): # disallow others from seeing the supplied password, whether or not it is correct. output = f"!admin login {('*' * random.randint(4, 16))}" - elif output.lower().startswith("!admin /option server_password"): # disallow others from knowing what the new remote administration password is. + elif output.lower().startswith( + "!admin /option server_password"): # disallow others from knowing what the new remote administration password is. output = f"!admin /option server_password {('*' * random.randint(4, 16))}" - self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + output) # Otherwise notify the others what is happening. + self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team, + self.client.slot) + ': ' + output) # Otherwise notify the others what is happening. if not self.ctx.server_password: self.output("Sorry, Remote administration is disabled") @@ -727,7 +737,8 @@ class ClientMessageProcessor(CommonCommandProcessor): if not command: if self.is_authenticated(): - self.output("Usage: !admin [Server command].\nUse !admin /help for help.\nUse !admin logout to log out of the current session.") + self.output( + "Usage: !admin [Server command].\nUse !admin /help for help.\nUse !admin logout to log out of the current session.") else: self.output("Usage: !admin login [password]") return True @@ -810,7 +821,6 @@ class ClientMessageProcessor(CommonCommandProcessor): "Sorry, !remaining requires you to have beaten the game on this server") return False - def _cmd_missing(self) -> bool: """List all missing location checks from the server's perspective""" @@ -850,7 +860,9 @@ class ClientMessageProcessor(CommonCommandProcessor): if usable: new_item = NetworkItem(Items.item_table[item_name][2], -1, self.client.slot) get_received_items(self.ctx, self.client.team, self.client.slot).append(new_item) - self.ctx.notify_all('Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team, self.client.slot)) + self.ctx.notify_all( + 'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team, + self.client.slot)) send_new_items(self.ctx) return True else: @@ -959,7 +971,7 @@ def get_client_points(ctx: Context, client: Client) -> int: async def process_client_cmd(ctx: Context, client: Client, args: dict): try: - cmd:str = args["cmd"] + cmd: str = args["cmd"] except: logging.exception(f"Could not get command from {args}") raise @@ -1045,7 +1057,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): items = get_received_items(ctx, client.team, client.slot) if items: client.send_index = len(items) - await ctx.send_msgs(client, [{"cmd": "ReceivedItems","index": 0, + await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0, "items": items}]) elif cmd == 'LocationChecks': @@ -1067,11 +1079,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): if cmd == 'Say': if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable(): - await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text" : 'Say'}]) + await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'Say'}]) return client.messageprocessor(args["text"]) + def update_client_status(ctx: Context, client: Client, new_status: ClientStatus): current = ctx.client_game_state[client.team, client.slot] if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion @@ -1083,6 +1096,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus) ctx.client_game_state[client.team, client.slot] = new_status + class ServerCommandProcessor(CommonCommandProcessor): def __init__(self, ctx: Context): self.ctx = ctx @@ -1190,7 +1204,8 @@ class ServerCommandProcessor(CommonCommandProcessor): for (team, slot), name in self.ctx.player_names.items(): if name.lower() == seeked_player: self.ctx.allow_forfeits[(team, slot)] = False - self.output(f"Player {player_name} has to follow the server restrictions on use of the !forfeit command.") + self.output( + f"Player {player_name} has to follow the server restrictions on use of the !forfeit command.") return True self.output(f"Could not find player {player_name} to forbid the !forfeit command for.") @@ -1270,6 +1285,7 @@ class ServerCommandProcessor(CommonCommandProcessor): f"{', '.join(known)}") return False + async def console(ctx: Context): session = prompt_toolkit.PromptSession() while ctx.running: @@ -1356,7 +1372,7 @@ async def auto_shutdown(ctx, to_cancel=None): async def main(args: argparse.Namespace): - logging.basicConfig(force = True, + logging.basicConfig(force=True, format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO)) ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, From b51b094cc189264760c0907b211256fd027569de Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Fri, 18 Jun 2021 23:45:03 -0500 Subject: [PATCH 24/42] Added HMG to playerSettings --- playerSettings.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/playerSettings.yaml b/playerSettings.yaml index 4912d84e..30789abc 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -119,6 +119,7 @@ glitches_required: # Determine the logic required to complete the seed none: 50 # No glitches required minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches + hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons. no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx. # Other players items are placed into your world under OWG logic dark_room_logic: # Logic for unlit dark rooms From 30190f373a1469acfc4dd0b8b350567d1839ca5f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 21 Jun 2021 02:14:25 +0200 Subject: [PATCH 25/42] send /received output to self.output --- CommonClient.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index de17302c..a2058688 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -47,12 +47,9 @@ class ClientCommandProcessor(CommandProcessor): def _cmd_received(self) -> bool: """List all received items""" - logger.info('Received items:') + logger.info(f'{len(self.ctx.items_received)} received items:') for index, item in enumerate(self.ctx.items_received, 1): - logging.info('%s from %s (%s) (%d/%d in list)' % ( - color(self.ctx.item_name_getter(item.item), 'red', 'bold'), - color(self.ctx.player_names[item.player], 'yellow'), - self.ctx.location_name_getter(item.location), index, len(self.ctx.items_received))) + self.output(f"{self.ctx.item_name_getter(item.item)} from {self.ctx.player_names[item.player]}") return True def _cmd_missing(self) -> bool: From 07d61f6d4759f41ca7052e5745b7737f3efcbd7e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 21 Jun 2021 02:51:54 +0200 Subject: [PATCH 26/42] fix playerSettings.yaml post-merge --- playerSettings.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/playerSettings.yaml b/playerSettings.yaml index de693727..ea00b2c4 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -127,8 +127,9 @@ A Link to the Past: none: 50 # No glitches required minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches - hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons.no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx. - # Other players items are placed into your world under OWG logic + hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons. + no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx. + # Other players items are placed into your world under HMG logic dark_room_logic: # Logic for unlit dark rooms lamp: 50 # require the Lamp for these rooms to be considered accessible. torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access From 023a798ac1b972db33f1476778f9178c021d5745 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 21 Jun 2021 22:25:49 +0200 Subject: [PATCH 27/42] Factorio: refactor visibility option into tech_tree_information set vanilla technologies to be hidden instead of disabled fix advancement icon still showing when no information in tech was supposed to be given --- Main.py | 4 ++-- Options.py | 13 +++++++------ data/factorio/mod_template/data-final-fixes.lua | 6 +++--- data/factorio/mod_template/locale/en/locale.cfg | 8 +++++--- playerSettings.yaml | 5 +++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Main.py b/Main.py index 2ef8ccc4..f21c50f8 100644 --- a/Main.py +++ b/Main.py @@ -508,10 +508,10 @@ def main(args, seed=None): 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)} - # for now special case Factorio visibility + # for now special case Factorio tech_tree_information sending_visible_players = set() for player in world.factorio_player_ids: - if world.visibility[player]: + if world.tech_tree_information[player].value == 2: sending_visible_players.add(player) for i, team in enumerate(parsed_names): diff --git a/Options.py b/Options.py index c7821e04..5052a4fa 100644 --- a/Options.py +++ b/Options.py @@ -348,8 +348,8 @@ class MaxSciencePack(Choice): default = 6 def get_allowed_packs(self): - return {option.replace("_", "-") for option, value in self.options.items() - if value <= self.value} + return {option.replace("_", "-") for option, value in self.options.items() if value <= self.value} - \ + {"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense class TechCost(Choice): @@ -388,10 +388,11 @@ class TechTreeLayout(Choice): default = 0 -class Visibility(Choice): +class TechTreeInformation(Choice): option_none = 0 - option_sending = 1 - default = 1 + option_advancement = 1 + option_full = 2 + default = 2 class RecipeTime(Choice): @@ -411,7 +412,7 @@ factorio_options: typing.Dict[str, type(Option)] = { "tech_tree_layout": TechTreeLayout, "tech_cost": TechCost, "free_samples": FreeSamples, - "visibility": Visibility, + "tech_tree_information": TechTreeInformation, "starting_items": FactorioStartItems, "recipe_time": RecipeTime, "imported_blueprints": DefaultOnToggle, diff --git a/data/factorio/mod_template/data-final-fixes.lua b/data/factorio/mod_template/data-final-fixes.lua index 58eb4425..9efd1539 100644 --- a/data/factorio/mod_template/data-final-fixes.lua +++ b/data/factorio/mod_template/data-final-fixes.lua @@ -23,7 +23,7 @@ template_tech.effects = {} template_tech.prerequisites = {} function prep_copy(new_copy, old_tech) - old_tech.enabled = false + old_tech.hidden = true new_copy.unit = table.deepcopy(old_tech.unit) local ingredient_filter = allowed_ingredients[old_tech.name] if ingredient_filter ~= nil then @@ -69,12 +69,12 @@ prep_copy(new_tree_copy, original_tech) {% if tech_cost_scale != 1 %} new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }})) {% endif %} -{%- if item_name in tech_table and visibility -%} +{%- if item_name in tech_table and tech_tree_information == 2 -%} {#- copy Factorio Technology Icon -#} copy_factorio_icon(new_tree_copy, "{{ item_name }}") {%- else -%} {#- use default AP icon if no Factorio graphics exist -#} -{% if advancement %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %} +{% if advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %} {%- endif -%} {#- connect Technology #} {%- if original_tech_name in tech_tree_layout_prerequisites %} diff --git a/data/factorio/mod_template/locale/en/locale.cfg b/data/factorio/mod_template/locale/en/locale.cfg index bce4d5f9..9a6202e7 100644 --- a/data/factorio/mod_template/locale/en/locale.cfg +++ b/data/factorio/mod_template/locale/en/locale.cfg @@ -1,17 +1,19 @@ [technology-name] {% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if visibility -%} +{%- if tech_tree_information == 2 -%} ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }} {% else %} -ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable +ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable {%- endif -%} {% endfor %} [technology-description] {% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if visibility -%} +{%- if tech_tree_information == 2 -%} ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. +{%- elif tech_tree_information == 1 and advancement -%} +ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. {% else %} ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. {%- endif -%} diff --git a/playerSettings.yaml b/playerSettings.yaml index ea00b2c4..e46428d4 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -91,9 +91,10 @@ Factorio: single_craft: 0 half_stack: 0 stack: 0 - visibility: + tech_tree_information: none: 0 - sending: 1 + advancement: 0 # show which items are a logical advancement + full: 1 # show full info on each tech node imported_blueprints: # can be turned off to prevent access to blueprints created outside the current world on: 1 off: 0 From 937fee90190427f09627926403f62a843ffc1600 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 22 Jun 2021 02:00:35 +0200 Subject: [PATCH 28/42] Factorio: fix locale file formatting --- data/factorio/mod_template/locale/en/locale.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/factorio/mod_template/locale/en/locale.cfg b/data/factorio/mod_template/locale/en/locale.cfg index 9a6202e7..b9924ad6 100644 --- a/data/factorio/mod_template/locale/en/locale.cfg +++ b/data/factorio/mod_template/locale/en/locale.cfg @@ -10,11 +10,11 @@ ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable [technology-description] {% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if tech_tree_information == 2 -%} +{%- if tech_tree_information == 2 %} ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. -{%- elif tech_tree_information == 1 and advancement -%} +{%- elif tech_tree_information == 1 and advancement %} ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. -{% else %} +{%- else %} ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. {%- endif -%} {% endfor %} \ No newline at end of file From 6c1d1643308d84653c06ac33b45a18b668991be8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 22 Jun 2021 06:25:19 +0200 Subject: [PATCH 29/42] LttP: set non-native items to Power Star --- worlds/alttp/Rom.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 3f02c2ea..591c4e5b 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -751,13 +751,18 @@ bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A] -def get_nonnative_item_sprite(game): - game_to_id = { - "Factorio": 0x09, # Hammer - "Hollow Knight": 0x21, # Bug Catching Net - "Minecraft": 0x13, # Shovel - } - return game_to_id.get(game, 0x6B) # default to Power Star +def get_nonnative_item_sprite(game: str) -> int: + return 0x6B # set all non-native sprites to Power Star as per 13 to 2 vote at + # https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886 + +# def get_nonnative_item_sprite(game): +# game_to_id = { +# "Factorio": 0x09, # Hammer +# "Hollow Knight": 0x21, # Bug Catching Net +# "Minecraft": 0x13, # Shovel +# } +# return game_to_id.get(game, 0x6B) # default to Power Star + def patch_rom(world, rom, player, team, enemized): local_random = world.slot_seeds[player] @@ -1730,7 +1735,7 @@ def write_custom_shops(rom, world, player): item_game = 'Factorio' elif item_name in mc_lookup.values(): item_game = 'Minecraft' - else: + else: item_game = 'Generic' item_code = get_nonnative_item_sprite(item_game) else: From 9f2f343f76dee329ea4b848d14cb48b658ad2d31 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 24 Jun 2021 23:51:42 +0200 Subject: [PATCH 30/42] Factorio: always display static nodes with full info --- LttPClient.py | 5 +++-- data/factorio/mod_template/data-final-fixes.lua | 2 +- data/factorio/mod_template/locale/en/locale.cfg | 8 ++++---- worlds/factorio/Mod.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/LttPClient.py b/LttPClient.py index 4c0f1f65..13674cbd 100644 --- a/LttPClient.py +++ b/LttPClient.py @@ -852,10 +852,11 @@ async def game_watcher(ctx: Context): if recv_index < len(ctx.items_received) and recv_item == 0: item = ctx.items_received[recv_index] + recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_name_getter(item.location), recv_index + 1, len(ctx.items_received))) - recv_index += 1 + ctx.location_name_getter(item.location), recv_index, len(ctx.items_received))) + snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item])) snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0])) diff --git a/data/factorio/mod_template/data-final-fixes.lua b/data/factorio/mod_template/data-final-fixes.lua index 9efd1539..137a03bb 100644 --- a/data/factorio/mod_template/data-final-fixes.lua +++ b/data/factorio/mod_template/data-final-fixes.lua @@ -69,7 +69,7 @@ prep_copy(new_tree_copy, original_tech) {% if tech_cost_scale != 1 %} new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }})) {% endif %} -{%- if item_name in tech_table and tech_tree_information == 2 -%} +{%- if item_name in tech_table and tech_tree_information == 2 or original_tech_name in static_nodes -%} {#- copy Factorio Technology Icon -#} copy_factorio_icon(new_tree_copy, "{{ item_name }}") {%- else -%} diff --git a/data/factorio/mod_template/locale/en/locale.cfg b/data/factorio/mod_template/locale/en/locale.cfg index b9924ad6..f3a0d044 100644 --- a/data/factorio/mod_template/locale/en/locale.cfg +++ b/data/factorio/mod_template/locale/en/locale.cfg @@ -1,7 +1,7 @@ [technology-name] {% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if tech_tree_information == 2 -%} +{%- if tech_tree_information == 2 or original_tech_name in static_nodes -%} ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }} {% else %} ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable @@ -10,10 +10,10 @@ ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable [technology-description] {% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if tech_tree_information == 2 %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. +{%- if tech_tree_information == 2 or original_tech_name in static_nodes %} +ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. For purposes of hints, this location is called "{{ original_tech_name }}". {%- elif tech_tree_information == 1 and advancement %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. +ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. For purposes of hints, this location is called "{{ original_tech_name }}". {%- else %} ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. {%- endif -%} diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index ef31663d..c68f7102 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -75,7 +75,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.slot_seeds[player], + "random": world.slot_seeds[player], "static_nodes": world.worlds[player].static_nodes, "recipe_time_scale": recipe_time_scales[world.recipe_time[player].value]} for factorio_option in Options.factorio_options: From 91655a855decbe6961246e55cd2da10182c4f54d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 01:31:48 +0200 Subject: [PATCH 31/42] Factorio: exclude science packs and rocket-part from free samples --- FactorioClientGUI.py | 2 +- data/factorio/mod_template/control.lua | 46 ++++++++++++++------------ worlds/factorio/Mod.py | 5 +-- worlds/factorio/Technologies.py | 20 +++++++---- 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/FactorioClientGUI.py b/FactorioClientGUI.py index b33f6a49..d985c5d3 100644 --- a/FactorioClientGUI.py +++ b/FactorioClientGUI.py @@ -59,7 +59,7 @@ class FactorioManager(App): super(FactorioManager, self).__init__() self.ctx = ctx self.commandprocessor = ctx.command_processor(ctx) - self.icon = "data/icon.png" + self.icon = r"data/icon.png" def build(self): self.grid = GridLayout() diff --git a/data/factorio/mod_template/control.lua b/data/factorio/mod_template/control.lua index 47e97fe0..d38e44c3 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -6,6 +6,7 @@ require "util" FREE_SAMPLES = {{ free_samples }} SLOT_NAME = "{{ slot_name }}" SEED_NAME = "{{ seed_name }}" +FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }} {% if not imported_blueprints -%} function set_permissions() @@ -152,29 +153,32 @@ script.on_event(defines.events.on_research_finished, function(event) local technology = event.research if technology.researched and string.find(technology.name, "ap%-") == 1 then dumpInfo(technology.force) --is sendable - end - if FREE_SAMPLES == 0 then - return -- Nothing else to do - end - if not technology.effects then - return -- No technology effects, so nothing to do. - end - for _, effect in pairs(technology.effects) do - if effect.type == "unlock-recipe" then - local recipe = game.recipe_prototypes[effect.recipe] - for _, result in pairs(recipe.products) do - if result.type == "item" and result.amount then - local name = result.name - local count - if FREE_SAMPLES == 1 then - count = result.amount - else - count = get_any_stack_size(result.name) - if FREE_SAMPLES == 2 then - count = math.ceil(count / 2) + else + if FREE_SAMPLES == 0 then + return -- Nothing else to do + end + if not technology.effects then + return -- No technology effects, so nothing to do. + end + for _, effect in pairs(technology.effects) do + if effect.type == "unlock-recipe" then + local recipe = game.recipe_prototypes[effect.recipe] + for _, result in pairs(recipe.products) do + if result.type == "item" and result.amount then + local name = result.name + if FREE_SAMPLE_BLACKLIST[name] ~= 1 then + local count + if FREE_SAMPLES == 1 then + count = result.amount + else + count = get_any_stack_size(result.name) + if FREE_SAMPLES == 2 then + count = math.ceil(count / 2) + end + end + add_samples(technology.force, name, count) end end - add_samples(technology.force, name, count) end end end diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index c68f7102..6d5a3d9e 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -11,7 +11,7 @@ import Utils import shutil import Options from BaseClasses import MultiWorld -from .Technologies import tech_table, rocket_recipes, recipes +from .Technologies import tech_table, rocket_recipes, recipes, free_sample_blacklist template_env: Optional[jinja2.Environment] = None @@ -76,7 +76,8 @@ def generate_mod(world: MultiWorld, player: int): "slot_name": world.player_names[player][0], "seed_name": world.seed_name, "starting_items": world.starting_items[player], "recipes": recipes, "random": world.slot_seeds[player], "static_nodes": world.worlds[player].static_nodes, - "recipe_time_scale": recipe_time_scales[world.recipe_time[player].value]} + "recipe_time_scale": recipe_time_scales[world.recipe_time[player].value], + "free_sample_blacklist": {item : 1 for item in free_sample_blacklist}} for factorio_option in Options.factorio_options: template_data[factorio_option] = getattr(world, factorio_option)[player].value diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 6754f5aa..06e265b6 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -99,6 +99,7 @@ class Machine(FactorioElement): self.name: str = name self.categories: set = categories + # recipes and technologies can share names in Factorio for technology_name in sorted(raw): data = raw[technology_name] @@ -125,7 +126,8 @@ for recipe_name, recipe_data in raw_recipes.items(): recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"])) recipes[recipe_name] = Recipe - if recipe.products.isdisjoint(recipe.ingredients) and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging + if recipe.products.isdisjoint( + recipe.ingredients) and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging for product_name in recipe.products: all_product_sources.setdefault(product_name, set()).add(recipe) @@ -153,6 +155,7 @@ def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]: current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done) return current_technologies + def unlock(recipe: Recipe, _done) -> Set[Technology]: current_technologies = set() current_technologies |= recipe.unlocking_technologies @@ -162,7 +165,9 @@ def unlock(recipe: Recipe, _done) -> Set[Technology]: return current_technologies -def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_func=unlock_just_tech) -> Set[Technology]: + +def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_func=unlock_just_tech) -> Set[ + Technology]: if _done: if ingredient_name in _done: return set() @@ -180,7 +185,6 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_f return current_technologies - required_machine_technologies: Dict[str, FrozenSet[Technology]] = {} for ingredient_name in machines: required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name)) @@ -192,14 +196,14 @@ for machine in machines.values(): if machine != pot_source_machine \ and machine.categories.issuperset(pot_source_machine.categories) \ and required_machine_technologies[machine.name].issuperset( - required_machine_technologies[pot_source_machine.name]): + required_machine_technologies[pot_source_machine.name]): logically_useful = False break if logically_useful: logical_machines[machine.name] = machine -del(required_machine_technologies) +del (required_machine_technologies) machines_per_category: Dict[str: Set[Machine]] = {} for machine in logical_machines.values(): @@ -219,11 +223,11 @@ for ingredient_name in all_ingredient_names: required_technologies[ingredient_name] = frozenset( recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)) - advancement_technologies: Set[str] = set() for technologies in required_technologies.values(): advancement_technologies |= {technology.name for technology in technologies} + @functools.lru_cache(10) def get_rocket_requirements(ingredients: Set[str]) -> Set[str]: techs = recursively_get_unlocking_technologies("rocket-silo") @@ -232,6 +236,8 @@ def get_rocket_requirements(ingredients: Set[str]) -> Set[str]: return {tech.name for tech in techs} +free_sample_blacklist = all_ingredient_names | {"rocket-part"} + rocket_recipes = { Options.MaxSciencePack.option_space_science_pack: {"rocket-control-unit": 10, "low-density-structure": 10, "rocket-fuel": 10}, @@ -247,4 +253,4 @@ rocket_recipes = { {"electronic-circuit": 10, "stone-brick": 10, "coal": 10}, Options.MaxSciencePack.option_automation_science_pack: {"copper-cable": 10, "iron-plate": 10, "wood": 10} -} \ No newline at end of file +} From 20729242f9abab9be58fe1687be599cab21cbeb5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 01:55:58 +0200 Subject: [PATCH 32/42] allow nested dictionaries in dict_to_lua --- data/factorio/mod_template/macros.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/factorio/mod_template/macros.lua b/data/factorio/mod_template/macros.lua index 47b179ed..fbd309fa 100644 --- a/data/factorio/mod_template/macros.lua +++ b/data/factorio/mod_template/macros.lua @@ -1,7 +1,7 @@ {% macro dict_to_lua(dict) -%} { {%- for key, value in dict.items() -%} - ["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %} + ["{{ key }}"] = {% if value is mapping %}{{ dict_to_lua(value) }}{% else %}{{ value | safe }}{% endif %}{% if not loop.last %},{% endif %} {% endfor -%} } {%- endmacro %} From 0e32393acbba4217173716eb009933d68d12d8ae Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 07:11:06 +0200 Subject: [PATCH 33/42] FactorioClient: only await awaitable tasks --- FactorioClientGUI.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/FactorioClientGUI.py b/FactorioClientGUI.py index d985c5d3..bdb6207b 100644 --- a/FactorioClientGUI.py +++ b/FactorioClientGUI.py @@ -30,13 +30,16 @@ async def main(): ctx.server_address = None ctx.snes_reconnect_address = None # allow tasks to quit - await ui_task - await factorio_server_task - await ctx.server_task + if ui_task: + await ui_task + if factorio_server_task: + await factorio_server_task + if ctx.server_task: + await ctx.server_task - if ctx.server is not None and not ctx.server.socket.closed: + if ctx.server and not ctx.server.socket.closed: await ctx.server.socket.close() - if ctx.server_task is not None: + if ctx.server_task: await ctx.server_task while ctx.input_requests > 0: # clear queue for shutdown From 007f66d86e2e989c16c0e80e0896b500fe5b74e5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 07:25:03 +0200 Subject: [PATCH 34/42] CommonClient.py: fix generic error --- CommonClient.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index a2058688..9610fd33 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -331,9 +331,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict): logger.error('Invalid password') ctx.password = None await ctx.server_auth(True) - else: + elif errors: raise Exception("Unknown connection errors: " + str(errors)) - raise Exception('Connection refused by the multiworld host, no reason provided') + else: + raise Exception('Connection refused by the multiworld host, no reason provided') elif cmd == 'Connected': ctx.team = args["team"] From f870bb3fad77ddb3d53d9b50392d4d2e4cbb20ec Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 21:04:37 +0200 Subject: [PATCH 35/42] MultiServer: implement a hint recheck that triggers on get_save() Still torn if I want a single hint list per team and filter on demand, or have filtered lists and re_check on demand. --- MultiServer.py | 8 ++++++++ Mystery.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index 9d51352f..45363b24 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -244,7 +244,15 @@ class Context(Node): import atexit atexit.register(self._save, True) # make sure we save on exit too + def recheck_hints(self): + for team, slot in self.hints: + self.hints[team, slot] = { + hint.re_check(self, team) for hint in + self.hints[team, slot] + } + def get_save(self) -> dict: + self.recheck_hints() d = { "connect_names": self.connect_names, "received_items": self.received_items, diff --git a/Mystery.py b/Mystery.py index b0dc1f35..0109e515 100644 --- a/Mystery.py +++ b/Mystery.py @@ -550,7 +550,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b roll_alttp_settings(ret, game_weights, plando_options) elif ret.game == "Hollow Knight": for option_name, option in Options.hollow_knight_options.items(): - setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights, True))) + setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights))) elif ret.game == "Factorio": for option_name, option in Options.factorio_options.items(): if option_name in game_weights: From d1fd1cd7884a0f40dd9d86e7d4cac9c9ab0895d1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 21:05:44 +0200 Subject: [PATCH 36/42] Tracker: sort Last Activity numerically, instead of text. --- WebHostLib/static/assets/tracker.js | 21 +++++++++++++++++++++ WebHostLib/templates/tracker.html | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/WebHostLib/static/assets/tracker.js b/WebHostLib/static/assets/tracker.js index e88b2ee6..9fa57310 100644 --- a/WebHostLib/static/assets/tracker.js +++ b/WebHostLib/static/assets/tracker.js @@ -17,6 +17,27 @@ window.addEventListener('load', () => { paging: false, info: false, dom: "t", + columnDefs: [ + { + targets: 'hours', + render: function (data, type, row) { + if (type === "sort" || type === 'type') { + if (data === "None") + return -1 + + return parseInt(data); + } + if (data === "None") + return data + + var hours = Math.floor(data / 3600); + var minutes = Math.floor((data - (hours * 3600)) / 60); + + if (minutes < 10) {minutes = "0"+minutes;} + return hours+':'+minutes; + } + }, + ], // DO NOT use the scrollX or scrollY options. They cause DataTables to split the thead from // the tbody and render two separate tables. diff --git a/WebHostLib/templates/tracker.html b/WebHostLib/templates/tracker.html index 52419acd..1b710ae8 100644 --- a/WebHostLib/templates/tracker.html +++ b/WebHostLib/templates/tracker.html @@ -98,7 +98,7 @@ {{ area }} {%- endif -%} {%- endfor -%} - Last
Activity + Last
Activity {% for area in ordered_areas %} @@ -141,7 +141,7 @@ {%- endif -%} {%- endfor -%} {%- if activity_timers[(team, player)] -%} - {{ activity_timers[(team, player)] | render_timedelta }} + {{ activity_timers[(team, player)].total_seconds() }} {%- else -%} None {%- endif -%} From 4b495557cdd6666d7c8ab29a06cc2b46794767ed Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 21:15:54 +0200 Subject: [PATCH 37/42] Tracker: sort numbers and fractions numerically --- WebHostLib/static/assets/tracker.js | 28 ++++++++++++++++++++++++---- WebHostLib/templates/tracker.html | 6 +++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/WebHostLib/static/assets/tracker.js b/WebHostLib/static/assets/tracker.js index 9fa57310..c4b59958 100644 --- a/WebHostLib/static/assets/tracker.js +++ b/WebHostLib/static/assets/tracker.js @@ -23,20 +23,40 @@ window.addEventListener('load', () => { render: function (data, type, row) { if (type === "sort" || type === 'type') { if (data === "None") - return -1 + return -1; return parseInt(data); } if (data === "None") - return data + return data; - var hours = Math.floor(data / 3600); - var minutes = Math.floor((data - (hours * 3600)) / 60); + let hours = Math.floor(data / 3600); + let minutes = Math.floor((data - (hours * 3600)) / 60); if (minutes < 10) {minutes = "0"+minutes;} return hours+':'+minutes; } }, + { + targets: 'number', + render: function (data, type, row) { + if (type === "sort" || type === 'type') { + return parseFloat(data); + } + return data; + } + }, + { + targets: 'fraction', + render: function (data, type, row) { + let splitted = data.split("/", 1); + let current = splitted[0] + if (type === "sort" || type === 'type') { + return parseInt(current); + } + return data; + } + }, ], // DO NOT use the scrollX or scrollY options. They cause DataTables to split the thead from diff --git a/WebHostLib/templates/tracker.html b/WebHostLib/templates/tracker.html index 1b710ae8..40faddfc 100644 --- a/WebHostLib/templates/tracker.html +++ b/WebHostLib/templates/tracker.html @@ -102,16 +102,16 @@ {% for area in ordered_areas %} - + Checks {% if area in key_locations %} - + Small Key {% endif %} {% if area in big_key_locations %} - + Big Key {%- endif -%} From 878ab3303976fcdde030bb30f224ba588c2fadb4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 22:09:09 +0200 Subject: [PATCH 38/42] Factorio: fix incomplete crafting category copy --- data/factorio/mod_template/data-final-fixes.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data/factorio/mod_template/data-final-fixes.lua b/data/factorio/mod_template/data-final-fixes.lua index 137a03bb..cdcc1230 100644 --- a/data/factorio/mod_template/data-final-fixes.lua +++ b/data/factorio/mod_template/data-final-fixes.lua @@ -57,7 +57,10 @@ function adjust_energy(recipe_name, factor) data.raw.recipe[recipe_name].energy_required = energy * factor end -table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid") +data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) +data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) +data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes) + {# each randomized tech gets set to be invisible, with new nodes added that trigger those #} {%- for original_tech_name, item_name, receiving_player, advancement in locations %} From cc85edafc496ea271a7b4a6beaa6038f1d719a85 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 25 Jun 2021 16:59:59 -0400 Subject: [PATCH 39/42] Add "Host Game" button back to the website landing page --- WebHostLib/static/styles/landing.css | 1 - WebHostLib/templates/landing.html | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/WebHostLib/static/styles/landing.css b/WebHostLib/static/styles/landing.css index 846cfb05..1192584f 100644 --- a/WebHostLib/static/styles/landing.css +++ b/WebHostLib/static/styles/landing.css @@ -102,7 +102,6 @@ html{ width: 260px; height: calc(130px - 35px); padding-top: 35px; - cursor: default; } #landing-clouds{ diff --git a/WebHostLib/templates/landing.html b/WebHostLib/templates/landing.html index 14207b5e..21f78de8 100644 --- a/WebHostLib/templates/landing.html +++ b/WebHostLib/templates/landing.html @@ -16,7 +16,7 @@ start
playing
setup guide - + Host Game discord
From 7f8bb10fc596e0ffb90ae98c5213980e77682ec0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 23:32:13 +0200 Subject: [PATCH 40/42] Move Factorio, Hollow Knight and Minecraft Options into AutoWorld --- BaseClasses.py | 19 ++- Main.py | 3 +- Mystery.py | 38 +++--- Options.py | 153 +---------------------- test/hollow_knight/__init__.py | 6 +- test/minecraft/TestMinecraft.py | 5 +- worlds/AutoWorld.py | 1 + worlds/alttp/Rom.py | 26 +--- worlds/factorio/Mod.py | 2 +- worlds/factorio/Options.py | 85 +++++++++++++ worlds/factorio/Shapes.py | 3 +- worlds/factorio/Technologies.py | 3 +- worlds/factorio/__init__.py | 4 +- worlds/hk/Options.py | 39 ++++++ worlds/minecraft/Options.py | 27 +++++ worlds/minecraft/Rules.py | 209 +++++++++++++++++++++----------- worlds/minecraft/__init__.py | 4 +- 17 files changed, 338 insertions(+), 289 deletions(-) create mode 100644 worlds/factorio/Options.py create mode 100644 worlds/hk/Options.py create mode 100644 worlds/minecraft/Options.py diff --git a/BaseClasses.py b/BaseClasses.py index 2724bf29..13e7c508 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -147,6 +147,9 @@ class MultiWorld(): for option_set in Options.option_sets: for option in option_set: setattr(self, option, getattr(args, option, {})) + for world in AutoWorld.AutoWorldRegister.world_types.values(): + for option in world.options: + 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]](self, player) @@ -1501,22 +1504,14 @@ class Spoiler(object): outfile.write('Progression Balanced: %s\n' % ( 'Yes' if self.metadata['progression_balancing'][player] else 'No')) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) - if player in self.world.hk_player_ids: - for hk_option in Options.hollow_knight_options: - res = getattr(self.world, hk_option)[player] - outfile.write(f'{hk_option+":":33}{res}\n') - - elif player in self.world.factorio_player_ids: - for f_option in Options.factorio_options: + options = self.world.worlds[player].options + if options: + for f_option in options: res = getattr(self.world, f_option)[player] outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n') - elif player in self.world.minecraft_player_ids: - for mc_option in Options.minecraft_options: - res = getattr(self.world, mc_option)[player] - outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n') - elif player in self.world.alttp_player_ids: + if player in self.world.alttp_player_ids: for team in range(self.world.teams): outfile.write('%s%s\n' % ( f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if diff --git a/Main.py b/Main.py index f21c50f8..c237cf6a 100644 --- a/Main.py +++ b/Main.py @@ -519,10 +519,9 @@ def main(args, seed=None): if player not in world.alttp_player_ids: connect_names[name] = (i, player) 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: + for option_name in world.worlds[slot].options: option = getattr(world, option_name)[slot] slots_data[option_name] = int(option.value) for slot in world.minecraft_player_ids: diff --git a/Mystery.py b/Mystery.py index 0109e515..60cedea7 100644 --- a/Mystery.py +++ b/Mystery.py @@ -548,35 +548,27 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b if ret.game == "A Link to the Past": roll_alttp_settings(ret, game_weights, plando_options) - elif ret.game == "Hollow Knight": - for option_name, option in Options.hollow_knight_options.items(): - setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights))) - elif ret.game == "Factorio": - for option_name, option in Options.factorio_options.items(): + elif ret.game in AutoWorldRegister.world_types: + for option_name, option in AutoWorldRegister.world_types[ret.game].options.items(): if option_name in game_weights: - if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class + if issubclass(option, Options.OptionDict): setattr(ret, option_name, option.from_any(game_weights[option_name])) else: setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights))) else: setattr(ret, option_name, option(option.default)) - elif ret.game == "Minecraft": - for option_name, option in Options.minecraft_options.items(): - if option_name in game_weights: - setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights))) - else: - setattr(ret, option_name, option(option.default)) - # bad hardcoded behavior to make this work for now - ret.plando_connections = [] - if "connections" in plando_options: - options = game_weights.get("plando_connections", []) - for placement in options: - if roll_percentage(get_choice("percentage", placement, 100)): - ret.plando_connections.append(PlandoConnection( - get_choice("entrance", placement), - get_choice("exit", placement), - get_choice("direction", placement, "both") - )) + if ret.game == "Minecraft": + # bad hardcoded behavior to make this work for now + ret.plando_connections = [] + if "connections" in plando_options: + options = game_weights.get("plando_connections", []) + for placement in options: + if roll_percentage(get_choice("percentage", placement, 100)): + ret.plando_connections.append(PlandoConnection( + get_choice("entrance", placement), + get_choice("exit", placement), + get_choice("direction", placement, "both") + )) else: raise Exception(f"Unsupported game {ret.game}") return ret diff --git a/Options.py b/Options.py index 5052a4fa..9a8d59e7 100644 --- a/Options.py +++ b/Options.py @@ -296,157 +296,12 @@ alttp_options: typing.Dict[str, type(Option)] = { "shop_item_slots": ShopItemSlots, } - - -hollow_knight_randomize_options: typing.Dict[str, type(Option)] = { - "RandomizeDreamers": DefaultOnToggle, - "RandomizeSkills": DefaultOnToggle, - "RandomizeCharms": DefaultOnToggle, - "RandomizeKeys": DefaultOnToggle, - "RandomizeGeoChests": Toggle, - "RandomizeMaskShards": DefaultOnToggle, - "RandomizeVesselFragments": DefaultOnToggle, - "RandomizeCharmNotches": Toggle, - "RandomizePaleOre": DefaultOnToggle, - "RandomizeRancidEggs": Toggle, - "RandomizeRelics": DefaultOnToggle, - "RandomizeMaps": Toggle, - "RandomizeStags": Toggle, - "RandomizeGrubs": Toggle, - "RandomizeWhisperingRoots": Toggle, - "RandomizeRocks": Toggle, - "RandomizeSoulTotems": Toggle, - "RandomizePalaceTotems": Toggle, - "RandomizeLoreTablets": Toggle, - "RandomizeLifebloodCocoons": Toggle, - "RandomizeFlames": Toggle -} - -hollow_knight_skip_options: typing.Dict[str, type(Option)] = { - "MILDSKIPS": Toggle, - "SPICYSKIPS": Toggle, - "FIREBALLSKIPS": Toggle, - "ACIDSKIPS": Toggle, - "SPIKETUNNELS": Toggle, - "DARKROOMS": Toggle, - "CURSED": Toggle, - "SHADESKIPS": Toggle, -} - -hollow_knight_options: typing.Dict[str, type(Option)] = {**hollow_knight_randomize_options, - **hollow_knight_skip_options} - - -class MaxSciencePack(Choice): - option_automation_science_pack = 0 - option_logistic_science_pack = 1 - option_military_science_pack = 2 - option_chemical_science_pack = 3 - option_production_science_pack = 4 - option_utility_science_pack = 5 - option_space_science_pack = 6 - default = 6 - - def get_allowed_packs(self): - return {option.replace("_", "-") for option, value in self.options.items() if value <= self.value} - \ - {"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense - - -class TechCost(Choice): - option_very_easy = 0 - option_easy = 1 - option_kind = 2 - option_normal = 3 - option_hard = 4 - option_very_hard = 5 - option_insane = 6 - default = 3 - - -class FreeSamples(Choice): - option_none = 0 - option_single_craft = 1 - option_half_stack = 2 - option_stack = 3 - default = 3 - - -class TechTreeLayout(Choice): - option_single = 0 - option_small_diamonds = 1 - option_medium_diamonds = 2 - option_large_diamonds = 3 - option_small_pyramids = 4 - option_medium_pyramids = 5 - option_large_pyramids = 6 - option_small_funnels = 7 - option_medium_funnels = 8 - option_large_funnels = 9 - option_funnels = 4 - alias_pyramid = 6 - alias_funnel = 9 - default = 0 - - -class TechTreeInformation(Choice): - option_none = 0 - option_advancement = 1 - option_full = 2 - default = 2 - - -class RecipeTime(Choice): - option_vanilla = 0 - option_fast = 1 - option_normal = 2 - option_slow = 4 - option_chaos = 5 - - -class FactorioStartItems(OptionDict): - default = {"burner-mining-drill": 19, "stone-furnace": 19} - - -factorio_options: typing.Dict[str, type(Option)] = { - "max_science_pack": MaxSciencePack, - "tech_tree_layout": TechTreeLayout, - "tech_cost": TechCost, - "free_samples": FreeSamples, - "tech_tree_information": TechTreeInformation, - "starting_items": FactorioStartItems, - "recipe_time": RecipeTime, - "imported_blueprints": DefaultOnToggle, -} - - -class AdvancementGoal(Choice): - option_few = 0 - option_normal = 1 - option_many = 2 - default = 1 - - -class CombatDifficulty(Choice): - option_easy = 0 - option_normal = 1 - option_hard = 2 - default = 1 - - -minecraft_options: typing.Dict[str, type(Option)] = { - "advancement_goal": AdvancementGoal, - "combat_difficulty": CombatDifficulty, - "include_hard_advancements": Toggle, - "include_insane_advancements": Toggle, - "include_postgame_advancements": Toggle, - "shuffle_structures": Toggle -} - +# replace with World.options option_sets = ( - minecraft_options, - factorio_options, + # minecraft_options, + # factorio_options, alttp_options, - hollow_knight_options + # hollow_knight_options ) if __name__ == "__main__": diff --git a/test/hollow_knight/__init__.py b/test/hollow_knight/__init__.py index 39a155d0..d12f521c 100644 --- a/test/hollow_knight/__init__.py +++ b/test/hollow_knight/__init__.py @@ -1,3 +1,4 @@ +import worlds.hk.Options from BaseClasses import MultiWorld from worlds.hk.Regions import create_regions from worlds.hk import gen_hollow @@ -9,10 +10,9 @@ class TestVanilla(TestBase): def setUp(self): self.world = MultiWorld(1) self.world.game[1] = "Hollow Knight" - import Options - for hk_option in Options.hollow_knight_randomize_options: + for hk_option in worlds.hk.Options.hollow_knight_randomize_options: setattr(self.world, hk_option, {1: True}) - for hk_option, option in Options.hollow_knight_skip_options.items(): + for hk_option, option in worlds.hk.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/minecraft/TestMinecraft.py b/test/minecraft/TestMinecraft.py index 375de5f5..56ec7306 100644 --- a/test/minecraft/TestMinecraft.py +++ b/test/minecraft/TestMinecraft.py @@ -1,3 +1,4 @@ +import worlds.minecraft.Options from test.TestBase import TestBase from BaseClasses import MultiWorld from worlds.minecraft import minecraft_gen_item_pool @@ -31,9 +32,9 @@ class TestMinecraft(TestBase): exclusion_pools = ['hard', 'insane', 'postgame'] for pool in exclusion_pools: setattr(self.world, f"include_{pool}_advancements", [False, False]) - setattr(self.world, "advancement_goal", [0, Options.AdvancementGoal(value=0)]) + setattr(self.world, "advancement_goal", [0, worlds.minecraft.Options.AdvancementGoal(value=0)]) setattr(self.world, "shuffle_structures", [False, False]) - setattr(self.world, "combat_difficulty", [0, Options.CombatDifficulty(value=1)]) + setattr(self.world, "combat_difficulty", [0, worlds.minecraft.Options.CombatDifficulty(value=1)]) minecraft_create_regions(self.world, 1) link_minecraft_structures(self.world, 1) minecraft_gen_item_pool(self.world, 1) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index d27b324b..8f94d257 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -27,6 +27,7 @@ class World(metaclass=AutoWorldRegister): world: MultiWorld player: int + options: dict = {} def __init__(self, world: MultiWorld, player: int): self.world = world diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 591c4e5b..443b1f99 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -751,18 +751,11 @@ bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A] -def get_nonnative_item_sprite(game: str) -> int: + +def get_nonnative_item_sprite(item: str) -> int: return 0x6B # set all non-native sprites to Power Star as per 13 to 2 vote at # https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886 -# def get_nonnative_item_sprite(game): -# game_to_id = { -# "Factorio": 0x09, # Hammer -# "Hollow Knight": 0x21, # Bug Catching Net -# "Minecraft": 0x13, # Shovel -# } -# return game_to_id.get(game, 0x6B) # default to Power Star - def patch_rom(world, rom, player, team, enemized): local_random = world.slot_seeds[player] @@ -1724,20 +1717,7 @@ def write_custom_shops(rom, world, player): if item is None: break if not item['item'] in item_table: # item not native to ALTTP - # This is a terrible way to do this, please fix later - from worlds.hk.Items import lookup_id_to_name as hk_lookup - from worlds.factorio.Technologies import lookup_id_to_name as factorio_lookup - from worlds.minecraft.Items import lookup_id_to_name as mc_lookup - item_name = item['item'] - if item_name in hk_lookup.values(): - item_game = 'Hollow Knight' - elif item_name in factorio_lookup.values(): - item_game = 'Factorio' - elif item_name in mc_lookup.values(): - item_game = 'Minecraft' - else: - item_game = 'Generic' - item_code = get_nonnative_item_sprite(item_game) + item_code = get_nonnative_item_sprite(item['item']) else: item_code = ItemFactory(item['item'], player).code if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]: diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 6d5a3d9e..cd50702e 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -9,7 +9,7 @@ import json import jinja2 import Utils import shutil -import Options +from . import Options from BaseClasses import MultiWorld from .Technologies import tech_table, rocket_recipes, recipes, free_sample_blacklist diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py new file mode 100644 index 00000000..3647841c --- /dev/null +++ b/worlds/factorio/Options.py @@ -0,0 +1,85 @@ +import typing + +from Options import Choice, OptionDict, Option, DefaultOnToggle + + +class MaxSciencePack(Choice): + option_automation_science_pack = 0 + option_logistic_science_pack = 1 + option_military_science_pack = 2 + option_chemical_science_pack = 3 + option_production_science_pack = 4 + option_utility_science_pack = 5 + option_space_science_pack = 6 + default = 6 + + def get_allowed_packs(self): + return {option.replace("_", "-") for option, value in self.options.items() if value <= self.value} - \ + {"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense + + +class TechCost(Choice): + option_very_easy = 0 + option_easy = 1 + option_kind = 2 + option_normal = 3 + option_hard = 4 + option_very_hard = 5 + option_insane = 6 + default = 3 + + +class FreeSamples(Choice): + option_none = 0 + option_single_craft = 1 + option_half_stack = 2 + option_stack = 3 + default = 3 + + +class TechTreeLayout(Choice): + option_single = 0 + option_small_diamonds = 1 + option_medium_diamonds = 2 + option_large_diamonds = 3 + option_small_pyramids = 4 + option_medium_pyramids = 5 + option_large_pyramids = 6 + option_small_funnels = 7 + option_medium_funnels = 8 + option_large_funnels = 9 + option_funnels = 4 + alias_pyramid = 6 + alias_funnel = 9 + default = 0 + + +class TechTreeInformation(Choice): + option_none = 0 + option_advancement = 1 + option_full = 2 + default = 2 + + +class RecipeTime(Choice): + option_vanilla = 0 + option_fast = 1 + option_normal = 2 + option_slow = 4 + option_chaos = 5 + + +class FactorioStartItems(OptionDict): + default = {"burner-mining-drill": 19, "stone-furnace": 19} + + +factorio_options: typing.Dict[str, type(Option)] = { + "max_science_pack": MaxSciencePack, + "tech_tree_layout": TechTreeLayout, + "tech_cost": TechCost, + "free_samples": FreeSamples, + "tech_tree_information": TechTreeInformation, + "starting_items": FactorioStartItems, + "recipe_time": RecipeTime, + "imported_blueprints": DefaultOnToggle, +} \ No newline at end of file diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 481b64cf..4b8a3c3d 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -1,7 +1,6 @@ from typing import Dict, List, Set -from BaseClasses import MultiWorld -from Options import TechTreeLayout +from worlds.factorio.Options import TechTreeLayout funnel_layers = {TechTreeLayout.option_small_funnels: 3, TechTreeLayout.option_medium_funnels: 4, diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 06e265b6..a00a189b 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -4,11 +4,12 @@ from typing import Dict, Set, FrozenSet import os import json -import Options import Utils import logging import functools +from . import Options + factorio_id = 2 ** 17 source_folder = Utils.local_path("data", "factorio") diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index f925f68a..ec6bf1da 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -5,7 +5,7 @@ from .Technologies import tech_table, recipe_sources, technology_table, advancem all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes from .Shapes import get_shapes from .Mod import generate_mod - +from .Options import factorio_options class Factorio(World): game: str = "Factorio" @@ -80,6 +80,8 @@ class Factorio(World): world.completion_condition[player] = lambda state: state.has('Victory', player) + options = factorio_options + def set_custom_technologies(world: MultiWorld, player: int): custom_technologies = {} allowed_packs = world.max_science_pack[player].get_allowed_packs() diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py new file mode 100644 index 00000000..e2be94fa --- /dev/null +++ b/worlds/hk/Options.py @@ -0,0 +1,39 @@ +import typing + +from Options import Option, DefaultOnToggle, Toggle + +hollow_knight_randomize_options: typing.Dict[str, type(Option)] = { + "RandomizeDreamers": DefaultOnToggle, + "RandomizeSkills": DefaultOnToggle, + "RandomizeCharms": DefaultOnToggle, + "RandomizeKeys": DefaultOnToggle, + "RandomizeGeoChests": Toggle, + "RandomizeMaskShards": DefaultOnToggle, + "RandomizeVesselFragments": DefaultOnToggle, + "RandomizeCharmNotches": Toggle, + "RandomizePaleOre": DefaultOnToggle, + "RandomizeRancidEggs": Toggle, + "RandomizeRelics": DefaultOnToggle, + "RandomizeMaps": Toggle, + "RandomizeStags": Toggle, + "RandomizeGrubs": Toggle, + "RandomizeWhisperingRoots": Toggle, + "RandomizeRocks": Toggle, + "RandomizeSoulTotems": Toggle, + "RandomizePalaceTotems": Toggle, + "RandomizeLoreTablets": Toggle, + "RandomizeLifebloodCocoons": Toggle, + "RandomizeFlames": Toggle +} +hollow_knight_skip_options: typing.Dict[str, type(Option)] = { + "MILDSKIPS": Toggle, + "SPICYSKIPS": Toggle, + "FIREBALLSKIPS": Toggle, + "ACIDSKIPS": Toggle, + "SPIKETUNNELS": Toggle, + "DARKROOMS": Toggle, + "CURSED": Toggle, + "SHADESKIPS": Toggle, +} +hollow_knight_options: typing.Dict[str, type(Option)] = {**hollow_knight_randomize_options, + **hollow_knight_skip_options} \ No newline at end of file diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py new file mode 100644 index 00000000..8dd6ba2d --- /dev/null +++ b/worlds/minecraft/Options.py @@ -0,0 +1,27 @@ +import typing + +from Options import Choice, Option, Toggle + + +class AdvancementGoal(Choice): + option_few = 0 + option_normal = 1 + option_many = 2 + default = 1 + + +class CombatDifficulty(Choice): + option_easy = 0 + option_normal = 1 + option_hard = 2 + default = 1 + + +minecraft_options: typing.Dict[str, type(Option)] = { + "advancement_goal": AdvancementGoal, + "combat_difficulty": CombatDifficulty, + "include_hard_advancements": Toggle, + "include_insane_advancements": Toggle, + "include_postgame_advancements": Toggle, + "shuffle_structures": Toggle +} \ No newline at end of file diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index b4a58476..d986844f 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -1,17 +1,16 @@ from ..generic.Rules import set_rule from .Locations import exclusion_table, events_table -from BaseClasses import Region, Entrance, Location, MultiWorld, Item -from Options import AdvancementGoal +from BaseClasses import MultiWorld + def set_rules(world: MultiWorld, player: int): - def reachable_locations(state): postgame_advancements = set(exclusion_table['postgame'].keys()) postgame_advancements.add('Free the End') for event in events_table.keys(): postgame_advancements.add(event) - return [location for location in world.get_locations() if - (player is None or location.player == player) and + return [location for location in world.get_locations() if + (player is None or location.player == player) and (location.name not in postgame_advancements) and location.can_reach(state)] @@ -19,18 +18,22 @@ def set_rules(world: MultiWorld, player: int): goal_map = { 'few': 30, 'normal': 50, - 'many': 70 + 'many': 70 } goal = goal_map[getattr(world, 'advancement_goal')[player].get_option_name()] - can_complete = lambda state: len(reachable_locations(state)) >= goal and state.can_reach('The End', 'Region', player) and state.can_kill_ender_dragon(player) + can_complete = lambda state: len(reachable_locations(state)) >= goal and state.can_reach('The End', 'Region', + player) and state.can_kill_ender_dragon( + player) - if world.logic[player] != 'nologic': + if world.logic[player] != 'nologic': world.completion_condition[player] = lambda state: state.has('Victory', player) - set_rule(world.get_entrance("Nether Portal", player), lambda state: state.has('Flint and Steel', player) and - (state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and + set_rule(world.get_entrance("Nether Portal", player), lambda state: state.has('Flint and Steel', player) and + (state.has('Bucket', player) or state.has( + 'Progressive Tools', player, 3)) and state.has_iron_ingots(player)) - set_rule(world.get_entrance("End Portal", player), lambda state: state.enter_stronghold(player) and state.has('3 Ender Pearls', player, 4)) + set_rule(world.get_entrance("End Portal", player), + lambda state: state.enter_stronghold(player) and state.has('3 Ender Pearls', player, 4)) set_rule(world.get_entrance("Overworld Structure 1", player), lambda state: state.can_adventure(player)) set_rule(world.get_entrance("Overworld Structure 2", player), lambda state: state.can_adventure(player)) set_rule(world.get_entrance("Nether Structure 1", player), lambda state: state.can_adventure(player)) @@ -41,108 +44,178 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state.can_piglin_trade(player)) set_rule(world.get_location("Oh Shiny", player), lambda state: state.can_piglin_trade(player)) - set_rule(world.get_location("Suit Up", player), lambda state: state.has("Progressive Armor", player) and state.has_iron_ingots(player)) - set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and state.can_use_anvil(player) and state.can_enchant(player) and \ - ((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player))) # need villager into the overworld for lightning strike - set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player)) - set_rule(world.get_location("Free the End", player), lambda state: can_complete(state) and state.has('Ingot Crafting', player) and state.can_reach('The Nether', 'Region', player)) - set_rule(world.get_location("A Furious Cocktail", player), lambda state: state.can_brew_potions(player) and - state.has("Fishing Rod", player) and # Water Breathing - state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets - state.can_reach('Village', 'Region', player) and # Night Vision, Invisibility - state.can_reach('Bring Home the Beacon', 'Location', player)) # Resistance + set_rule(world.get_location("Suit Up", player), + lambda state: state.has("Progressive Armor", player) and state.has_iron_ingots(player)) + set_rule(world.get_location("Very Very Frightening", player), + lambda state: state.has("Channeling Book", player) and state.can_use_anvil(player) and state.can_enchant( + player) and \ + ((world.get_region('Village', player).entrances[ + 0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', + player)) or state.can_reach( + 'Zombie Doctor', 'Location', + player))) # need villager into the overworld for lightning strike + set_rule(world.get_location("Hot Stuff", player), + lambda state: state.has("Bucket", player) and state.has_iron_ingots(player)) + set_rule(world.get_location("Free the End", player), + lambda state: can_complete(state) and state.has('Ingot Crafting', player) and state.can_reach('The Nether', + 'Region', + player)) + set_rule(world.get_location("A Furious Cocktail", player), lambda state: state.can_brew_potions(player) and + state.has("Fishing Rod", + player) and # Water Breathing + state.can_reach('The Nether', 'Region', + player) and # Regeneration, Fire Resistance, gold nuggets + state.can_reach('Village', 'Region', + player) and # Night Vision, Invisibility + state.can_reach('Bring Home the Beacon', + 'Location', + player)) # Resistance set_rule(world.get_location("Best Friends Forever", player), lambda state: True) - set_rule(world.get_location("Bring Home the Beacon", player), lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and - state.has("Ingot Crafting", player) and state.has("Resource Blocks", player)) - set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state.has_iron_ingots(player)) - set_rule(world.get_location("Isn't It Iron Pick", player), lambda state: state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player)) + set_rule(world.get_location("Bring Home the Beacon", player), + lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and + state.has("Ingot Crafting", player) and state.has("Resource Blocks", player)) + set_rule(world.get_location("Not Today, Thank You", player), + lambda state: state.has("Shield", player) and state.has_iron_ingots(player)) + set_rule(world.get_location("Isn't It Iron Pick", player), + lambda state: state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player)) set_rule(world.get_location("Local Brewery", player), lambda state: state.can_brew_potions(player)) set_rule(world.get_location("The Next Generation", player), lambda state: can_complete(state)) set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player)) set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: True) - set_rule(world.get_location("This Boat Has Legs", player), lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and state.has("Fishing Rod", player)) + set_rule(world.get_location("This Boat Has Legs", player), + lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and state.has("Fishing Rod", + player)) set_rule(world.get_location("Sniper Duel", player), lambda state: state.has("Archery", player)) set_rule(world.get_location("Nether", player), lambda state: True) set_rule(world.get_location("Great View From Up Here", player), lambda state: state.basic_combat(player)) - set_rule(world.get_location("How Did We Get Here?", player), lambda state: state.can_brew_potions(player) and state.has_gold_ingots(player) and # most effects; Absorption - state.can_reach('End City', 'Region', player) and state.can_reach('The Nether', 'Region', player) and # Levitation; potion ingredients - state.has("Fishing Rod", player) and state.has("Archery", player) and # Pufferfish, Nautilus Shells; spectral arrows - state.can_reach("Bring Home the Beacon", "Location", player) and # Haste - state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village - set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player)) + set_rule(world.get_location("How Did We Get Here?", player), + lambda state: state.can_brew_potions(player) and state.has_gold_ingots( + player) and # most effects; Absorption + state.can_reach('End City', 'Region', player) and state.can_reach('The Nether', 'Region', + player) and # Levitation; potion ingredients + state.has("Fishing Rod", player) and state.has("Archery", + player) and # Pufferfish, Nautilus Shells; spectral arrows + state.can_reach("Bring Home the Beacon", "Location", player) and # Haste + state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village + set_rule(world.get_location("Bullseye", player), + lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, + 2) and state.has_iron_ingots(player)) set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state.basic_combat(player)) - set_rule(world.get_location("Two by Two", player), lambda state: state.has_iron_ingots(player) and state.can_adventure(player)) # shears > seagrass > turtles; nether > striders; gold carrots > horses skips ingots + set_rule(world.get_location("Two by Two", player), + lambda state: state.has_iron_ingots(player) and state.can_adventure( + player)) # shears > seagrass > turtles; nether > striders; gold carrots > horses skips ingots set_rule(world.get_location("Stone Age", player), lambda state: True) - set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state.craft_crossbow(player) and state.can_enchant(player)) + set_rule(world.get_location("Two Birds, One Arrow", player), + lambda state: state.craft_crossbow(player) and state.can_enchant(player)) set_rule(world.get_location("We Need to Go Deeper", player), lambda state: True) set_rule(world.get_location("Who's the Pillager Now?", player), lambda state: state.craft_crossbow(player)) set_rule(world.get_location("Getting an Upgrade", player), lambda state: state.has("Progressive Tools", player)) - set_rule(world.get_location("Tactical Fishing", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player)) - set_rule(world.get_location("Zombie Doctor", player), lambda state: state.can_brew_potions(player) and state.has_gold_ingots(player)) + set_rule(world.get_location("Tactical Fishing", player), + lambda state: state.has("Bucket", player) and state.has_iron_ingots(player)) + set_rule(world.get_location("Zombie Doctor", player), + lambda state: state.can_brew_potions(player) and state.has_gold_ingots(player)) set_rule(world.get_location("The City at the End of the Game", player), lambda state: True) set_rule(world.get_location("Ice Bucket Challenge", player), lambda state: state.has_diamond_pickaxe(player)) set_rule(world.get_location("Remote Getaway", player), lambda state: True) set_rule(world.get_location("Into Fire", player), lambda state: state.basic_combat(player)) set_rule(world.get_location("War Pigs", player), lambda state: state.basic_combat(player)) set_rule(world.get_location("Take Aim", player), lambda state: state.has("Archery", player)) - set_rule(world.get_location("Total Beelocation", player), lambda state: state.has("Silk Touch Book", player) and state.can_use_anvil(player) and state.can_enchant(player)) - set_rule(world.get_location("Arbalistic", player), lambda state: state.craft_crossbow(player) and state.has("Piercing IV Book", player) and - state.can_use_anvil(player) and state.can_enchant(player)) - set_rule(world.get_location("The End... Again...", player), lambda state: can_complete(state) and state.has("Ingot Crafting", player) and state.can_reach('The Nether', 'Region', player)) # furnace for glass, nether for ghast tears + set_rule(world.get_location("Total Beelocation", player), + lambda state: state.has("Silk Touch Book", player) and state.can_use_anvil(player) and state.can_enchant( + player)) + set_rule(world.get_location("Arbalistic", player), + lambda state: state.craft_crossbow(player) and state.has("Piercing IV Book", player) and + state.can_use_anvil(player) and state.can_enchant(player)) + set_rule(world.get_location("The End... Again...", player), + lambda state: can_complete(state) and state.has("Ingot Crafting", player) and state.can_reach('The Nether', + 'Region', + player)) # furnace for glass, nether for ghast tears set_rule(world.get_location("Acquire Hardware", player), lambda state: state.has_iron_ingots(player)) - set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state.can_piglin_trade(player) and state.has("Resource Blocks", player)) - set_rule(world.get_location("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player)) + set_rule(world.get_location("Not Quite \"Nine\" Lives", player), + lambda state: state.can_piglin_trade(player) and state.has("Resource Blocks", player)) + set_rule(world.get_location("Cover Me With Diamonds", player), + lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", + player)) set_rule(world.get_location("Sky's the Limit", player), lambda state: state.basic_combat(player)) - set_rule(world.get_location("Hired Help", player), lambda state: state.has("Resource Blocks", player) and state.has_iron_ingots(player)) + set_rule(world.get_location("Hired Help", player), + lambda state: state.has("Resource Blocks", player) and state.has_iron_ingots(player)) set_rule(world.get_location("Return to Sender", player), lambda state: True) - set_rule(world.get_location("Sweet Dreams", player), lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player)) - set_rule(world.get_location("You Need a Mint", player), lambda state: can_complete(state) and state.has_bottle_mc(player)) + set_rule(world.get_location("Sweet Dreams", player), + lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player)) + set_rule(world.get_location("You Need a Mint", player), + lambda state: can_complete(state) and state.has_bottle_mc(player)) set_rule(world.get_location("Adventure", player), lambda state: True) - set_rule(world.get_location("Monsters Hunted", player), lambda state: can_complete(state) and state.can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing + set_rule(world.get_location("Monsters Hunted", player), + lambda state: can_complete(state) and state.can_kill_wither(player) and state.has("Fishing Rod", + player)) # pufferfish for Water Breathing set_rule(world.get_location("Enchanter", player), lambda state: state.can_enchant(player)) set_rule(world.get_location("Voluntary Exile", player), lambda state: state.basic_combat(player)) set_rule(world.get_location("Eye Spy", player), lambda state: state.enter_stronghold(player)) set_rule(world.get_location("The End", player), lambda state: True) - set_rule(world.get_location("Serious Dedication", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots(player)) + set_rule(world.get_location("Serious Dedication", player), + lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots( + player)) set_rule(world.get_location("Postmortal", player), lambda state: state.complete_raid(player)) set_rule(world.get_location("Monster Hunter", player), lambda state: True) set_rule(world.get_location("Adventuring Time", player), lambda state: state.can_adventure(player)) set_rule(world.get_location("A Seedy Place", player), lambda state: True) set_rule(world.get_location("Those Were the Days", player), lambda state: True) set_rule(world.get_location("Hero of the Village", player), lambda state: state.complete_raid(player)) - set_rule(world.get_location("Hidden in the Depths", player), lambda state: state.can_brew_potions(player) and state.has("Bed", player) and state.has_diamond_pickaxe(player)) # bed mining :) - set_rule(world.get_location("Beaconator", player), lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and - state.has("Ingot Crafting", player) and state.has("Resource Blocks", player)) + set_rule(world.get_location("Hidden in the Depths", player), + lambda state: state.can_brew_potions(player) and state.has("Bed", player) and state.has_diamond_pickaxe( + player)) # bed mining :) + set_rule(world.get_location("Beaconator", player), + lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and + state.has("Ingot Crafting", player) and state.has("Resource Blocks", player)) set_rule(world.get_location("Withering Heights", player), lambda state: state.can_kill_wither(player)) - set_rule(world.get_location("A Balanced Diet", player), lambda state: state.has_bottle_mc(player) and state.has_gold_ingots(player) and # honey bottle; gapple - state.has("Resource Blocks", player) and state.can_reach('The End', 'Region', player)) # notch apple, chorus fruit + set_rule(world.get_location("A Balanced Diet", player), + lambda state: state.has_bottle_mc(player) and state.has_gold_ingots(player) and # honey bottle; gapple + state.has("Resource Blocks", player) and state.can_reach('The End', 'Region', + player)) # notch apple, chorus fruit set_rule(world.get_location("Subspace Bubble", player), lambda state: state.has_diamond_pickaxe(player)) set_rule(world.get_location("Husbandry", player), lambda state: True) - set_rule(world.get_location("Country Lode, Take Me Home", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots(player)) - set_rule(world.get_location("Bee Our Guest", player), lambda state: state.has("Campfire", player) and state.has_bottle_mc(player)) + set_rule(world.get_location("Country Lode, Take Me Home", player), + lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots( + player)) + set_rule(world.get_location("Bee Our Guest", player), + lambda state: state.has("Campfire", player) and state.has_bottle_mc(player)) set_rule(world.get_location("What a Deal!", player), lambda state: True) - set_rule(world.get_location("Uneasy Alliance", player), lambda state: state.has_diamond_pickaxe(player) and state.has('Fishing Rod', player)) - set_rule(world.get_location("Diamonds!", player), lambda state: state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player)) - set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything - set_rule(world.get_location("A Throwaway Joke", player), lambda state: True) # kill drowned + set_rule(world.get_location("Uneasy Alliance", player), + lambda state: state.has_diamond_pickaxe(player) and state.has('Fishing Rod', player)) + set_rule(world.get_location("Diamonds!", player), + lambda state: state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player)) + set_rule(world.get_location("A Terrible Fortress", player), + lambda state: True) # since you don't have to fight anything + set_rule(world.get_location("A Throwaway Joke", player), lambda state: True) # kill drowned set_rule(world.get_location("Minecraft", player), lambda state: True) - set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state.has_bottle_mc(player)) + set_rule(world.get_location("Sticky Situation", player), + lambda state: state.has("Campfire", player) and state.has_bottle_mc(player)) set_rule(world.get_location("Ol' Betsy", player), lambda state: state.craft_crossbow(player)) - set_rule(world.get_location("Cover Me in Debris", player), lambda state: state.has("Progressive Armor", player, 2) and - state.has("8 Netherite Scrap", player, 2) and state.has("Ingot Crafting", player) and - state.can_reach("Diamonds!", "Location", player) and state.can_reach("Hidden in the Depths", "Location", player)) + set_rule(world.get_location("Cover Me in Debris", player), + lambda state: state.has("Progressive Armor", player, 2) and + state.has("8 Netherite Scrap", player, 2) and state.has("Ingot Crafting", player) and + state.can_reach("Diamonds!", "Location", player) and state.can_reach("Hidden in the Depths", + "Location", player)) set_rule(world.get_location("The End?", player), lambda state: True) set_rule(world.get_location("The Parrots and the Bats", player), lambda state: True) - set_rule(world.get_location("A Complete Catalogue", player), lambda state: True) # kill fish for raw + set_rule(world.get_location("A Complete Catalogue", player), lambda state: True) # kill fish for raw set_rule(world.get_location("Getting Wood", player), lambda state: True) set_rule(world.get_location("Time to Mine!", player), lambda state: True) set_rule(world.get_location("Hot Topic", player), lambda state: state.has("Ingot Crafting", player)) set_rule(world.get_location("Bake Bread", player), lambda state: True) - set_rule(world.get_location("The Lie", player), lambda state: state.has_iron_ingots(player) and state.has("Bucket", player)) - set_rule(world.get_location("On a Rail", player), lambda state: state.has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails + set_rule(world.get_location("The Lie", player), + lambda state: state.has_iron_ingots(player) and state.has("Bucket", player)) + set_rule(world.get_location("On a Rail", player), + lambda state: state.has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails set_rule(world.get_location("Time to Strike!", player), lambda state: True) set_rule(world.get_location("Cow Tipper", player), lambda state: True) - set_rule(world.get_location("When Pigs Fly", player), lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and state.has("Fishing Rod", player) and state.can_adventure(player)) - set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit + set_rule(world.get_location("When Pigs Fly", player), + lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and state.has("Fishing Rod", + player) and state.can_adventure( + player)) + set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and ( + state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', + player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player)) - set_rule(world.get_location("Overpowered", player), lambda state: state.has("Resource Blocks", player) and state.has_gold_ingots(player)) + set_rule(world.get_location("Overpowered", player), + lambda state: state.has("Resource Blocks", player) and state.has_gold_ingots(player)) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index f81c2bcf..91d0f0db 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -1,16 +1,16 @@ -from random import Random from .Items import MinecraftItem, item_table, item_frequencies from .Locations import exclusion_table, events_table from .Regions import link_minecraft_structures from .Rules import set_rules from BaseClasses import MultiWorld -from Options import minecraft_options +from .Options import minecraft_options from ..AutoWorld import World class MinecraftWorld(World): game: str = "Minecraft" + options = minecraft_options client_version = (0, 3) From 43e3c846350e399eec716e0e90fd63bbae499d9d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 25 Jun 2021 23:39:22 +0200 Subject: [PATCH 41/42] fix the Hollow Knight Unittest. Yes, the one test. --- test/hollow_knight/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/hollow_knight/__init__.py b/test/hollow_knight/__init__.py index d12f521c..ec29f896 100644 --- a/test/hollow_knight/__init__.py +++ b/test/hollow_knight/__init__.py @@ -1,4 +1,4 @@ -import worlds.hk.Options +from worlds.hk import Options from BaseClasses import MultiWorld from worlds.hk.Regions import create_regions from worlds.hk import gen_hollow @@ -10,9 +10,9 @@ class TestVanilla(TestBase): def setUp(self): self.world = MultiWorld(1) self.world.game[1] = "Hollow Knight" - for hk_option in worlds.hk.Options.hollow_knight_randomize_options: + for hk_option in Options.hollow_knight_randomize_options: setattr(self.world, hk_option, {1: True}) - for hk_option, option in worlds.hk.Options.hollow_knight_skip_options.items(): + 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 From 5fdcd2d7c7c15a73a945e2f5c83d1cb0a3edbda1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 26 Jun 2021 00:54:27 +0200 Subject: [PATCH 42/42] Factorio: locale formatting fixes --- data/factorio/mod_template/locale/en/locale.cfg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/factorio/mod_template/locale/en/locale.cfg b/data/factorio/mod_template/locale/en/locale.cfg index f3a0d044..9c35cb8b 100644 --- a/data/factorio/mod_template/locale/en/locale.cfg +++ b/data/factorio/mod_template/locale/en/locale.cfg @@ -1,9 +1,9 @@ [technology-name] {% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if tech_tree_information == 2 or original_tech_name in static_nodes -%} +{%- if tech_tree_information == 2 or original_tech_name in static_nodes %} ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }} -{% else %} +{%- else %} ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable {%- endif -%} {% endfor %} @@ -11,10 +11,10 @@ ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable [technology-description] {% for original_tech_name, item_name, receiving_player, advancement in locations %} {%- if tech_tree_information == 2 or original_tech_name in static_nodes %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. For purposes of hints, this location is called "{{ original_tech_name }}". +ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. {%- elif tech_tree_information == 1 and advancement %} ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. For purposes of hints, this location is called "{{ original_tech_name }}". {%- else %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. +ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. For purposes of hints, this location is called "{{ original_tech_name }}". {%- endif -%} {% endfor %} \ No newline at end of file