From 88c5ebdd2fae2229e7c39da9f5fd9564055a0294 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 31 Aug 2021 18:58:54 +0200 Subject: [PATCH 01/13] WebHost: add per-game yaml file downloads --- WebHostLib/downloads.py | 16 +++++++++++++--- WebHostLib/templates/templates.html | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 WebHostLib/templates/templates.html diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 2d944117..4ba358cc 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -1,8 +1,8 @@ -from flask import send_file, Response +from flask import send_file, Response, render_template from pony.orm import select from Patch import update_patch_data -from WebHostLib import app, Slot, Room, Seed +from WebHostLib import app, Slot, Room, Seed, cache import zipfile @app.route("/dl_patch//") @@ -68,4 +68,14 @@ def download_slot_file(room_id, player_id: int): fname = name.rsplit("/", 1)[0]+".zip" else: return "Game download not supported." - return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname) \ No newline at end of file + return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname) + +@app.route("/templates") +@cache.cached() +def list_yaml_templates(): + import os + files = [] + for file in os.scandir(os.path.join(app.static_folder, "generated")): + if file.is_file() and file.name.endswith(".yaml"): + files.append(file.name) + return render_template("templates.html", files=files) \ No newline at end of file diff --git a/WebHostLib/templates/templates.html b/WebHostLib/templates/templates.html new file mode 100644 index 00000000..0f5920df --- /dev/null +++ b/WebHostLib/templates/templates.html @@ -0,0 +1,21 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + {% include 'header/grassHeader.html' %} + Option Templates (YAML) + + +{% endblock %} + +{% block body %} +
+

Option Templates (YAML)

+
    + {% for file in files %} +
  • {{ file }}
  • + {% endfor %} +
+
+{% endblock %} From a6a859b27210cbc198b8703dbc6a45392350f078 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 31 Aug 2021 19:06:24 +0200 Subject: [PATCH 02/13] WebHost: fix sample yamls that have no options. WebHost: hide hidden games from templates listing --- WebHostLib/downloads.py | 8 ++++---- WebHostLib/templates/options.yaml | 3 ++- WebHostLib/templates/templates.html | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 4ba358cc..77dc179e 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -73,9 +73,9 @@ def download_slot_file(room_id, player_id: int): @app.route("/templates") @cache.cached() def list_yaml_templates(): - import os files = [] - for file in os.scandir(os.path.join(app.static_folder, "generated")): - if file.is_file() and file.name.endswith(".yaml"): - files.append(file.name) + from worlds.AutoWorld import AutoWorldRegister + for world_name, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + files.append(world_name) return render_template("templates.html", files=files) \ No newline at end of file diff --git a/WebHostLib/templates/options.yaml b/WebHostLib/templates/options.yaml index 105cd895..746ff8be 100644 --- a/WebHostLib/templates/options.yaml +++ b/WebHostLib/templates/options.yaml @@ -68,4 +68,5 @@ progression_balancing: {%- else %} {{ yaml_dump(option.default) | indent(4, first=False) }} {%- endif -%} - {%- endfor -%} \ No newline at end of file + {%- endfor -%} + {% if not options %}{}{% endif %} \ No newline at end of file diff --git a/WebHostLib/templates/templates.html b/WebHostLib/templates/templates.html index 0f5920df..5c5fe5f1 100644 --- a/WebHostLib/templates/templates.html +++ b/WebHostLib/templates/templates.html @@ -14,7 +14,7 @@

Option Templates (YAML)

From ff2e57705e9743ac565a844be7a61c4b45818d38 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 31 Aug 2021 19:54:55 +0200 Subject: [PATCH 03/13] WebHost: sample yamls now render Range defaults correctly --- WebHostLib/options.py | 12 +++++++++++- WebHostLib/templates/options.yaml | 19 +++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 56d01737..9edbbe6c 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -11,8 +11,18 @@ target_folder = os.path.join("WebHostLib", "static", "generated") def create(): for game_name, world in AutoWorldRegister.world_types.items(): + def dictify_range(option): + data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0, + option.default: 50} + notes = { + option.range_start: "minimum value", + option.range_end: "maximum value" + } + return data, notes + res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( - options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump + options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, + dictify_range=dictify_range ) with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f: diff --git a/WebHostLib/templates/options.yaml b/WebHostLib/templates/options.yaml index 746ff8be..c84ee3f8 100644 --- a/WebHostLib/templates/options.yaml +++ b/WebHostLib/templates/options.yaml @@ -18,7 +18,8 @@ # http://www.yamllint.com/ description: Default {{ game }} Template # Used to describe your yaml. Useful if you have multiple files -name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit +# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit +name: YourName{number} #{player} will be replaced with the player's slot number. #{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1. #{number} will be replaced with the counter value of the name. @@ -51,16 +52,18 @@ progression_balancing: # - "Progressive Weapons" # exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk. # - "Master Sword Pedestal" +{%- macro range_option(option) %} + # you can add additional values between minimum and maximum + {%- set data, notes = dictify_range(option) %} + {%- for entry, default in data.items() %} + {{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %} + {%- endfor -%} +{%- endmacro -%} {{ game }}: {%- for option_key, option in options.items() %} {{ option_key }}:{% if option.__doc__ %} # {{ option.__doc__ | replace('\n', '\n#') | indent(4, first=False) }}{% endif %} {%- if option.range_start is defined %} - # you can add additional values between minimum and maximum - {{ option.range_start }}: 0 # minimum value - {{ option.range_end }}: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 + {{- range_option(option) -}} {%- elif option.options -%} {%- for suboption_option_id, sub_option_name in option.name_lookup.items() %} {{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %} @@ -69,4 +72,4 @@ progression_balancing: {{ yaml_dump(option.default) | indent(4, first=False) }} {%- endif -%} {%- endfor -%} - {% if not options %}{}{% endif %} \ No newline at end of file + {% if not options %}{}{% endif %} From ddc619f2e7ac4d751a20ee412b1be75a7201d439 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 31 Aug 2021 19:56:45 +0200 Subject: [PATCH 04/13] WebHost: sample yamls: some formatting issues --- WebHostLib/templates/options.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/templates/options.yaml b/WebHostLib/templates/options.yaml index c84ee3f8..6fc06c46 100644 --- a/WebHostLib/templates/options.yaml +++ b/WebHostLib/templates/options.yaml @@ -58,7 +58,7 @@ progression_balancing: {%- for entry, default in data.items() %} {{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %} {%- endfor -%} -{%- endmacro -%} +{% endmacro %} {{ game }}: {%- for option_key, option in options.items() %} {{ option_key }}:{% if option.__doc__ %} # {{ option.__doc__ | replace('\n', '\n#') | indent(4, first=False) }}{% endif %} @@ -71,5 +71,5 @@ progression_balancing: {%- else %} {{ yaml_dump(option.default) | indent(4, first=False) }} {%- endif -%} - {%- endfor -%} + {%- endfor %} {% if not options %}{}{% endif %} From adfd68f83c1c1b49e161c3e9f665850daa9dd2fe Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 31 Aug 2021 22:14:18 +0200 Subject: [PATCH 05/13] Options: fix get_option_name --- Options.py | 9 +++++---- WebHostLib/options.py | 17 ++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Options.py b/Options.py index f31c7e6f..af7d048f 100644 --- a/Options.py +++ b/Options.py @@ -57,11 +57,12 @@ class Option(metaclass=AssembleOptions): """For display purposes.""" return self.get_option_name(self.value) - def get_option_name(self, value: typing.Any) -> str: - if self.autodisplayname: - return self.name_lookup[self.value].replace("_", " ").title() + @classmethod + def get_option_name(cls, value: typing.Any) -> str: + if cls.autodisplayname: + return cls.name_lookup[value].replace("_", " ").title() else: - return self.name_lookup[self.value] + return cls.name_lookup[value] def __int__(self) -> int: return self.value diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 9edbbe6c..fa0b1db6 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -10,16 +10,15 @@ target_folder = os.path.join("WebHostLib", "static", "generated") def create(): + def dictify_range(option): + data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0, + option.default: 50} + notes = { + option.range_start: "minimum value", + option.range_end: "maximum value" + } + return data, notes for game_name, world in AutoWorldRegister.world_types.items(): - def dictify_range(option): - data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0, - option.default: 50} - notes = { - option.range_start: "minimum value", - option.range_end: "maximum value" - } - return data, notes - res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, dictify_range=dictify_range From 66627d8a6609b530a0e85b309fa3264783da36a0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 31 Aug 2021 22:52:14 +0200 Subject: [PATCH 06/13] Options: match Toggle's get_option_name signature to Choice's --- Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index af7d048f..fbb19d88 100644 --- a/Options.py +++ b/Options.py @@ -115,7 +115,8 @@ class Toggle(Option): def __int__(self): return int(self.value) - def get_option_name(self, value): + @classmethod + def get_option_name(cls, value): return ["No", "Yes"][int(value)] class DefaultOnToggle(Toggle): From 4fcce665053e6f0a3c2c1d4a753dee6ee9688394 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 31 Aug 2021 17:28:46 -0400 Subject: [PATCH 07/13] Move game names and descriptions into AutoWorld, fix option value names on player-settings pages --- WebHostLib/__init__.py | 50 ++++----------------------- WebHostLib/options.py | 2 +- WebHostLib/templates/games/games.html | 6 ++-- worlds/alttp/__init__.py | 12 +++++-- worlds/factorio/__init__.py | 5 +++ worlds/minecraft/__init__.py | 16 ++++++--- worlds/subnautica/__init__.py | 7 +++- 7 files changed, 41 insertions(+), 57 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 59091f6a..eb8b4724 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -8,6 +8,7 @@ from pony.flask import Pony from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory from flask_caching import Cache from flask_compress import Compress +from worlds.AutoWorld import AutoWorldRegister from .models import * @@ -81,49 +82,6 @@ def page_not_found(err): return render_template('404.html'), 404 -games_list = { - "A Link to the Past": ("The Legend of Zelda: A Link to the Past", - """ - The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of - Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine - dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil - Ganon!"""), - "Factorio": ("Factorio", - """ - Factorio is a game about automation. You play as an engineer who has crash landed on the planet - Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory, - research new technologies, and become more efficient in your quest to build a rocket and return home. - """), - "Minecraft": ("Minecraft", - """ - Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine, - craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient - structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim - victory!"""), - "Subnautica": ("Subnautica", - """ - Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by - an unknown bacteria. The planet's automatic quarantine will shoot you down if you try to leave. - You must find a cure for yourself, build an escape rocket, and leave the planet. - """), - "Ocarina of Time": ("The Legend of Zelda: Ocarina of Time", - """ - The Legend of Zelda: Ocarina of Time was the first three dimensional Zelda game. Journey as - Link as he quests to fulfil his destiny. Journey across Hyrule and defeat the evil masters of - corrupted temples or seek out the pieces of the Triforce. Defeat the evil Ganondorf to become - the Hero of Time and save Hyrule! - """), - "Super Metroid": ("Super Metroid", - """ - Samus is back in her first 16 bit adventure! Space pirates have attacked Ceres station and stolen - the last living Metroid. Go to planet Zebes and search out the abilities you will need to power - up your suit and defeat the villainous leader of the space pirates, Mother Brain. - """), - # "Ori and the Blind Forest": ("Ori and the Blind Forest", "Coming Soon™"), - # "Hollow Knight": ("Hollow Knight", "Coming Soon™"), -} - - # Player settings pages @app.route('/games//player-settings') def player_settings(game): @@ -145,7 +103,11 @@ def game_page(game): # List of supported games @app.route('/games') def games(): - return render_template("games/games.html", games_list=games_list) + worlds = {} + for game, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + worlds[game] = world.__doc__ if world.__doc__ else "No description provided." + return render_template("games/games.html", worlds=worlds) @app.route('/tutorial///') diff --git a/WebHostLib/options.py b/WebHostLib/options.py index fa0b1db6..6a052fa2 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -48,7 +48,7 @@ def create(): for sub_option_name, sub_option_id in option.options.items(): this_option["options"].append({ - "name": sub_option_name, + "name": option.get_option_name(sub_option_id), "value": sub_option_name, }) diff --git a/WebHostLib/templates/games/games.html b/WebHostLib/templates/games/games.html index 9b5bec8d..3aa1eb49 100644 --- a/WebHostLib/templates/games/games.html +++ b/WebHostLib/templates/games/games.html @@ -9,9 +9,9 @@ {% include 'header/grassHeader.html' %}

Currently Supported Games

- {% for game, (display_name, description) in games_list.items() %} -

{{ display_name}}

-

{{ description}}

+ {% for game, description in worlds.items() %} +

{{ game }}

+

{{ description }}

{% endfor %}
{% endblock %} diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 10006daf..5ef5d332 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -24,6 +24,12 @@ lttp_logger = logging.getLogger("A Link to the Past") class ALTTPWorld(World): + """ + The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of + Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine + dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil + Ganon! + """ game: str = "A Link to the Past" options = alttp_options topology_present = True @@ -192,8 +198,8 @@ class ALTTPWorld(World): elif 'Bow' in item_name: if state.has('Silver Bow', item.player): return - elif state.has('Bow', item.player) and (self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2 - or self.world.logic[item.player] == 'noglitches' + elif state.has('Bow', item.player) and (self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2 + or self.world.logic[item.player] == 'noglitches' or self.world.swordless[item.player]): # modes where silver bow is always required for ganon return 'Silver Bow' elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1: @@ -401,4 +407,4 @@ class ALttPLogic(LogicMixin): return True if self.world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: return self.can_buy_unlimited('Small Key (Universal)', player) - return self.prog_items[item, player] >= count \ No newline at end of file + return self.prog_items[item, player] >= count diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 6c4bce45..6028ae61 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -24,6 +24,11 @@ all_items["Evolution Trap"] = factorio_base_id - 2 class Factorio(World): + """ + Factorio is a game about automation. You play as an engineer who has crash landed on the planet + Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory, + research new technologies, and become more efficient in your quest to build a rocket and return home. + """ game: str = "Factorio" static_nodes = {"automation", "logistics", "rocket-silo"} custom_recipes = {} diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 78ab60b9..fd972740 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -16,6 +16,12 @@ from ..AutoWorld import World client_version = 6 class MinecraftWorld(World): + """ + Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine, + craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient + structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim + victory! + """ game: str = "Minecraft" options = minecraft_options topology_present = True @@ -47,7 +53,7 @@ class MinecraftWorld(World): itempool = [] junk_pool = junk_weights.copy() # Add all required progression items - for (name, num) in required_items.items(): + for (name, num) in required_items.items(): itempool += [name] * num # Add structure compasses if desired if self.world.structure_compasses[self.player]: @@ -85,9 +91,9 @@ class MinecraftWorld(World): def MCRegion(region_name: str, exits=[]): ret = Region(region_name, None, region_name, self.player, self.world) ret.locations = [MinecraftAdvancement(self.player, loc_name, loc_data.id, ret) - for loc_name, loc_data in advancement_table.items() + for loc_name, loc_data in advancement_table.items() if loc_data.region == region_name] - for exit in exits: + for exit in exits: ret.exits.append(Entrance(self.player, exit, ret)) return ret @@ -100,7 +106,7 @@ class MinecraftWorld(World): with open(os.path.join(output_directory, filename), 'wb') as f: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) - def fill_slot_data(self): + def fill_slot_data(self): slot_data = self._get_mc_data() for option_name in minecraft_options: option = getattr(self.world, option_name)[self.player] @@ -115,7 +121,7 @@ class MinecraftWorld(World): item.never_exclude = True return item -def mc_update_output(raw_data, server, port): +def mc_update_output(raw_data, server, port): data = json.loads(b64decode(raw_data)) data['server'] = server data['port'] = port diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 527cf8ee..50376a37 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -15,6 +15,11 @@ from ..AutoWorld import World class SubnauticaWorld(World): + """ + Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by + an unknown bacteria. The planet's automatic quarantine will shoot you down if you try to leave. + You must find a cure for yourself, build an escape rocket, and leave the planet. + """ game: str = "Subnautica" item_name_to_id = items_lookup_name_to_id @@ -53,7 +58,7 @@ class SubnauticaWorld(World): pass - def fill_slot_data(self): + def fill_slot_data(self): slot_data = {} return slot_data From 138c884684337e565245cba6ce402940b20e93d2 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Mon, 30 Aug 2021 19:43:48 -0500 Subject: [PATCH 08/13] wipe reachable regions during TR key logic checks to ensure properly finding logic regions --- worlds/alttp/Rules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index c9837e78..28b4acb8 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -854,6 +854,7 @@ def set_trock_key_rules(world, player): set_rule(world.get_entrance(entrance, player), lambda state: False) all_state = world.get_all_state(True) + all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work # Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon. can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge[player] is None else world.can_access_trock_eyebridge[player] From 7972aa6320e45c122160245d433e7b2c1844f23c Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 31 Aug 2021 14:47:57 -0500 Subject: [PATCH 09/13] split building owg connections and setting the rules for those connections --- worlds/alttp/EntranceShuffle.py | 9 ++++--- worlds/alttp/OverworldGlitchRules.py | 35 ++++++++++++++++++++++------ worlds/alttp/Rules.py | 4 ++-- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index 2a4514bc..6871f99b 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1,5 +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.OverworldGlitchRules import overworld_glitch_connections from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections def link_entrances(world, player): @@ -1066,9 +1067,11 @@ def link_entrances(world, player): raise NotImplementedError( f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}') - # mandatory hybrid major glitches connections - if world.logic[player] in ['hybridglitches', 'nologic']: - underworld_glitch_connections(world, player) + if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: + overworld_glitch_connections(world, 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)': diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py index 593a5407..705db7e7 100644 --- a/worlds/alttp/OverworldGlitchRules.py +++ b/worlds/alttp/OverworldGlitchRules.py @@ -235,24 +235,41 @@ def no_logic_rules(world, player): create_no_logic_connections(player, world, get_mirror_offset_spots_lw(player)) +def overworld_glitch_connections(world, player): + + # Boots-accessible locations. + create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted')) + create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player)) + + # Glitched speed drops. + create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted')) + + # Mirror clip spots. + if world.mode[player] != 'inverted': + create_owg_connections(player, world, get_mirror_clip_spots_dw()) + create_owg_connections(player, world, get_mirror_offset_spots_dw()) + else: + create_owg_connections(player, world, get_mirror_offset_spots_lw(player)) + + def overworld_glitches_rules(world, player): # Boots-accessible locations. - create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player)) - create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player)) + set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player)) + set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player)) # Glitched speed drops. - create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player)) + set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player)) # Dark Death Mountain Ledge Clip Spot also accessible with mirror. if world.mode[player] != 'inverted': add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player)) # Mirror clip spots. if world.mode[player] != 'inverted': - create_owg_connections(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player)) - create_owg_connections(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player)) + set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player)) + set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player)) else: - create_owg_connections(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player)) + set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player)) # Regions that require the boots and some other stuff. if world.mode[player] != 'inverted': @@ -282,12 +299,16 @@ def create_no_logic_connections(player, world, connections): parent.exits.append(connection) connection.connect(target) -def create_owg_connections(player, world, connections, default_rule): +def create_owg_connections(player, world, connections): for entrance, parent_region, target_region, *rule_override in connections: parent = world.get_region(parent_region, player) target = world.get_region(target_region, player) connection = Entrance(player, entrance, parent) parent.exits.append(connection) connection.connect(target) + +def set_owg_connection_rules(player, world, connections, default_rule): + for entrance, _, _, *rule_override in connections: + connection = world.get_entrance(entrance, player) rule = rule_override[0] if len(rule_override) > 0 else default_rule connection.access_rule = rule diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 28b4acb8..97fe40ff 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -56,11 +56,11 @@ def set_rules(world): # entrances. The overworld_glitches_rules set is primarily additive. no_glitches_rules(world, player) fake_flipper_rules(world, player) - overworld_glitches_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) + # overworld_glitches_rules(world, player) underworld_glitches_rules(world, player) elif world.logic[player] == 'minorglitches': no_glitches_rules(world, player) From 631b6788c69a64a05a3d383617280b27fe5d7d09 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 31 Aug 2021 19:19:26 -0500 Subject: [PATCH 10/13] remove keys option for get_all_state, collect dungeon-local keys, and fix all uses of the state --- BaseClasses.py | 30 +++++++----------------------- worlds/alttp/Dungeons.py | 2 ++ worlds/alttp/Rules.py | 2 +- worlds/alttp/__init__.py | 2 +- 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index e101642a..e799af5e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -213,9 +213,8 @@ class MultiWorld(): except KeyError as e: raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e - def get_all_state(self, keys=False) -> CollectionState: - key = f"_all_state_{keys}" - cached = getattr(self, key, None) + def get_all_state(self) -> CollectionState: + cached = getattr(self, "_all_state", None) if cached: return cached.copy() @@ -223,27 +222,12 @@ class MultiWorld(): for item in self.itempool: self.worlds[item.player].collect(ret, item) - - if keys: - for p in self.get_game_players("A Link to the Past"): - world = self.worlds[p] - from worlds.alttp.Items import ItemFactory - for item in ItemFactory( - ['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)', - 'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)', - 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', - 'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + [ - 'Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + [ - 'Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)', - 'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + [ - 'Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)', - 'Big Key (Ganons Tower)'] + [ - 'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [ - 'Small Key (Ganons Tower)'] * 4, - p): - world.collect(ret, item) + from worlds.alttp.Dungeons import get_dungeon_item_pool + for item in get_dungeon_item_pool(self): + subworld = self.worlds[item.player] + if item.name in subworld.dungeon_local_item_names: + subworld.collect(ret, item) ret.sweep_for_events() - setattr(self, key, ret) return ret def get_items(self) -> list: diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 76d4953f..000af504 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -157,6 +157,8 @@ def fill_dungeons_restrictive(autoworld, world): in_dungeon_items.sort( key=lambda item: sort_order.get(item.type, 1) + (5 if (item.player, item.name) in dungeon_specific else 0)) + for item in in_dungeon_items: + all_state_base.remove(item) fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 97fe40ff..083f0aba 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -853,7 +853,7 @@ def set_trock_key_rules(world, player): for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room', 'Turtle Rock Big Key Door']: set_rule(world.get_entrance(entrance, player), lambda state: False) - all_state = world.get_all_state(True) + all_state = world.get_all_state() all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work # Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon. diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 5ef5d332..b7bdb348 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -212,7 +212,7 @@ class ALTTPWorld(World): attempts = 5 world = self.world player = self.player - all_state = world.get_all_state(keys=True) + all_state = world.get_all_state() crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']] crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), From 17929415eecee301cb0bb5b1b94c1bf15c2e1d60 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 31 Aug 2021 19:44:58 -0500 Subject: [PATCH 11/13] actually set owg rules --- worlds/alttp/Rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 083f0aba..492dcf2e 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -56,11 +56,11 @@ def set_rules(world): # entrances. The overworld_glitches_rules set is primarily additive. no_glitches_rules(world, player) fake_flipper_rules(world, player) - # overworld_glitches_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) + overworld_glitches_rules(world, player) underworld_glitches_rules(world, player) elif world.logic[player] == 'minorglitches': no_glitches_rules(world, player) From fb0f70b3e3c998733137003e9fcfaa38869e05f7 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 31 Aug 2021 19:49:12 -0500 Subject: [PATCH 12/13] make owg entrances in inverted --- worlds/alttp/EntranceShuffle.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index 6871f99b..2d555930 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1774,9 +1774,11 @@ 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) + if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: + overworld_glitch_connections(world, player) + # 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)': From fbb8d6b1324f1f6ddd171662a0d7309e080e2eef Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 31 Aug 2021 20:07:23 -0500 Subject: [PATCH 13/13] invalidate state cache so that reachable_regions are recalculated during TR key logic --- worlds/alttp/Rules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 492dcf2e..4e986c0d 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -855,6 +855,7 @@ def set_trock_key_rules(world, player): all_state = world.get_all_state() all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work + all_state.stale[player] = True # Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon. can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge[player] is None else world.can_access_trock_eyebridge[player]