From 60b80083e0fedcc3311e345518a4690c152ea8f6 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 4 Jul 2022 19:09:03 +0200 Subject: [PATCH 001/138] LttP: fix shop inventory corruption in upgrade fairy --- worlds/alttp/Shops.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 77eec9dd0f..5abbdd07bc 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -460,10 +460,11 @@ def shuffle_shops(world, items, player: int): f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.") bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item) arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item) + slots = iter(range(2)) if bombupgrades: - capacityshop.add_inventory(1, 'Bomb Upgrade (+5)', 100, bombupgrades) + capacityshop.add_inventory(next(slots), 'Bomb Upgrade (+5)', 100, bombupgrades) if arrowupgrades: - capacityshop.add_inventory(1, 'Arrow Upgrade (+5)', 100, arrowupgrades) + capacityshop.add_inventory(next(slots), 'Arrow Upgrade (+5)', 100, arrowupgrades) else: for item in new_items: world.push_precollected(ItemFactory(item, player)) From 9ac780102e282e1f92e774f00fad03247ec13fce Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 7 Jul 2022 00:01:28 +0200 Subject: [PATCH 002/138] Subnautica: display item_pool as Item Pool on the settings page --- worlds/subnautica/Options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index 0695a08950..4189aecb19 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -4,6 +4,7 @@ from Options import Choice class ItemPool(Choice): """Valuable item pool moves all not progression relevant items to starting inventory and creates random duplicates of important items in their place.""" + display_name = "Item Pool" option_standard = 0 option_valuable = 1 From 2f53972c853543190eb9d088621f43db36217d5f Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 8 Jul 2022 06:35:33 -0700 Subject: [PATCH 003/138] Factorio: fix accidental removal of fluids from make_balanced_recipe (#754) --- worlds/factorio/Technologies.py | 1 + worlds/factorio/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 0d32f51cbb..f89dc53ee3 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -501,6 +501,7 @@ def get_science_pack_pools() -> Dict[str, Set[str]]: item_stack_sizes: Dict[str, int] = items_future.result() non_stacking_items: Set[str] = {item for item, stack in item_stack_sizes.items() if stack == 1} stacking_items: Set[str] = set(item_stack_sizes) - non_stacking_items +valid_ingredients: Set[str] = stacking_items | fluids # cleanup async helpers pool.shutdown() diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 53c9897c17..14304c4b8b 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -8,7 +8,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ - fluids, stacking_items + fluids, stacking_items, valid_ingredients from .Shapes import get_shapes from .Mod import generate_mod from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal @@ -231,7 +231,7 @@ class Factorio(World): """Generate a recipe from pool with time and cost similar to original * factor""" new_ingredients = {} # have to first sort for determinism, while filtering out non-stacking items - pool: typing.List[str] = sorted(pool & stacking_items) + pool: typing.List[str] = sorted(pool & valid_ingredients) # then sort with random data to shuffle self.world.random.shuffle(pool) target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor) From 17db0805a7fa6757d0862a5a36a7c4862fc9d5ca Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 9 Jul 2022 03:35:38 -0700 Subject: [PATCH 004/138] Allow potentially all rocket-part ingredients to be fluids. (#753) --- worlds/factorio/__init__.py | 4 +- .../data/mod_template/data-final-fixes.lua | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 14304c4b8b..33f1809cf7 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -329,10 +329,8 @@ class Factorio(World): def set_custom_recipes(self): original_rocket_part = recipes["rocket-part"] science_pack_pools = get_science_pack_pools() - valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()] & stacking_items) + valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()] & valid_ingredients) self.world.random.shuffle(valid_pool) - while any([valid_pool[x] in fluids for x in range(3)]): - self.world.random.shuffle(valid_pool) self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, {valid_pool[x]: 10 for x in range(3)}, original_rocket_part.products, diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 7da4f3a62d..29bfa7276a 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -1,6 +1,48 @@ {% from "macros.lua" import dict_to_recipe %} -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template require('lib') +data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = { + { + production_type = "input", + pipe_picture = assembler2pipepictures(), + pipe_covers = pipecoverspictures(), + base_area = 10, + base_level = -1, + pipe_connections = { + { type = "input", position = { 0, 5 } }, + { type = "input", position = { 0, -5 } }, + { type = "input", position = { 5, 0 } }, + { type = "input", position = { -5, 0 } } + } + }, + { + production_type = "input", + pipe_picture = assembler2pipepictures(), + pipe_covers = pipecoverspictures(), + base_area = 10, + base_level = -1, + pipe_connections = { + { type = "input", position = { -3, 5 } }, + { type = "input", position = { -3, -5 } }, + { type = "input", position = { 5, -3 } }, + { type = "input", position = { -5, -3 } } + } + }, + { + production_type = "input", + pipe_picture = assembler2pipepictures(), + pipe_covers = pipecoverspictures(), + base_area = 10, + base_level = -1, + pipe_connections = { + { type = "input", position = { 3, 5 } }, + { type = "input", position = { 3, -5 } }, + { type = "input", position = { 5, 3 } }, + { type = "input", position = { -5, 3 } } + } + }, + off_when_no_fluid_recipe = true +} {%- for recipe_name, recipe in custom_recipes.items() %} data.raw["recipe"]["{{recipe_name}}"].category = "{{recipe.category}}" From 1cc9c7a469ff750cf1ef22b6f8d3fd7af2567347 Mon Sep 17 00:00:00 2001 From: Bicoloursnake <60069210+Bicoloursnake@users.noreply.github.com> Date: Sat, 9 Jul 2022 21:16:41 -0400 Subject: [PATCH 005/138] Doc: Add english mac guide (running from source) (#744) * Create RunFromSourceGuideForMac.md * Update and rename RunFromSourceGuideForMac.md to docs/RunFromSourceGuideForMac.md * Clarified the source code download. * Rename docs/RunFromSourceGuideForMac.md to worlds/generic/docs/RunFromSourceGuideForMac.md * Update __init__.py * Noted the case where a user might want EnemizerCLI * Updated document to reflect requested changes Updated to reflect the requested changes as well as including some information on virtual environments. * Added Capital Letters to SNIClient.py * Reworked Document Structure Numeric order of lists now makes sense and changed the virtual environment section to match Archipelago tradition. * Update __init__.py * Minor Changes for clarity's sake * Renamed file to make webhost happy * Changed mac guide filename --- worlds/generic/__init__.py | 4 +++- worlds/generic/docs/mac_en.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 worlds/generic/docs/mac_en.md diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 3baba9a709..0d8a220d98 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -15,6 +15,8 @@ class GenericWeb(WebWorld): commands = Tutorial('Archipelago Server and Client Commands', 'A guide detailing the commands available to the user when participating in an Archipelago session.', 'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu']) + mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.', + 'English', 'mac_en.md','mac/en', ['Bicoloursnake']) plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.', 'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav']) setup = Tutorial('Multiworld Setup Tutorial', @@ -25,7 +27,7 @@ class GenericWeb(WebWorld): using_website = Tutorial('Archipelago Website User Guide', 'A guide to using the Archipelago website to generate multiworlds or host pre-generated multiworlds.', 'English', 'using_website_en.md', 'using_website/en', ['alwaysintreble']) - tutorials = [setup, using_website, commands, advanced_settings, triggers, plando] + tutorials = [setup, using_website, mac, commands, advanced_settings, triggers, plando] class GenericWorld(World): diff --git a/worlds/generic/docs/mac_en.md b/worlds/generic/docs/mac_en.md new file mode 100644 index 0000000000..1e2d235c8c --- /dev/null +++ b/worlds/generic/docs/mac_en.md @@ -0,0 +1,32 @@ +# Guide to Run Archipelago from Source Code on macOS +Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal. +## Prerequisite Software +Here is a list of software to install and source code to download. +1. Python 3.8 or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). +2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835). +3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases). +4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases). +5. If you would like to generate Enemized seeds for ALTTP locally (not on the website), you may need the EnemizerCLI from its [Github releases page](https://github.com/Ijwu/Enemizer/releases). +6. An Emulator of your choosing for games that need an emulator. For SNES games, I recommend RetroArch, entirely because it was the easiest for me to setup on macOS. It can be downloaded at the [RetroArch downloads page](https://www.retroarch.com/?page=platforms) +## Extracting the Archipelago Directory +1. Double click on the Archipelago source code zip file to extract the files to an Archipelago directory. +2. Move this Archipelago directory out of your downloads directory. +3. Open terminal and navigate to your Archipelago directory. +## Setting up a Virtual Environment +It is generally recommended that you use a virtual environment to run python based software to avoid contamination that can break some software. If Archipelago is the only piece of software you use that runs from python source code however, it is not necessary to use a virtual environment. +1. Open terminal and navigate to the Archipelago directory. +2. Run the command `python3 -m venv venv` to create a virtual environment. Running this command will create a new directory at the specified path, so make sure that path is clear for a new directory to be created. +3. Run the command `source venv/bin/activate` to activate the virtual environment. +4. If you want to exit the virtual environment, run the command `deactivate`. +## Steps to Run the Clients +1. If your game doesn't have a patch file, run the command `python3 SNIClient.py`, changing the filename with the file of the client you want to run. +2. If your game does have a patch file, move the base rom to the Archipelago directory and run the command `python3 SNIClient.py 'patchfile'` with the filename extension for the patch file (apsm, aplttp, apsmz3, etc.) included and changing the filename with the file of the client you want to run. +3. Your client should now be running and rom created (where applicable). +## Additional Steps for SNES Games +1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch. +2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier. +3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier. +4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually. +5. If using EnemizerCLI, extract that downloaded directory and rename it to EnemizerCLI. +6. Move the EnemizerCLI directory into the Archipelago directory so that Generate.py can take advantage of it. +7. Now that SNI, the client, and the emulator are all running, you should be good to go. From beac0b1acdba270c939e72885af0362731a786ff Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 9 Jul 2022 14:57:35 +0200 Subject: [PATCH 006/138] Requirements: update some modules --- WebHostLib/requirements.txt | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 83132b5625..6280ecdfc6 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,7 +1,7 @@ flask>=2.1.2 pony>=0.7.16 waitress>=2.1.1 -flask-caching>=1.11.1 +flask-caching>=2.0.0 Flask-Compress>=1.12 -Flask-Limiter>=2.4.6 +Flask-Limiter>=2.5.0 bokeh>=2.4.3 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0067d461d7..661209e072 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -colorama>=0.4.4 +colorama>=0.4.5 websockets>=10.3 PyYAML>=6.0 jellyfish>=0.9.0 From 0d3bd6e2e87842df76fa92f1efd69d07b7a1e151 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sun, 10 Jul 2022 19:24:07 +0000 Subject: [PATCH 007/138] gitignore general Windows/macOS files (#763) --- .gitignore | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b052625508..3733723427 100644 --- a/.gitignore +++ b/.gitignore @@ -152,10 +152,17 @@ dmypy.json # Cython debug symbols cython_debug/ -#minecraft server stuff +# minecraft server stuff jdk*/ minecraft*/ minecraft_versions.json -#pyenv +# pyenv .python-version + +# OS General Files +.DS_Store +.AppleDouble +.LSOverride +Thumbs.db +[Dd]esktop.ini From c80636646964fad23385f6d01b9105a04464a48d Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 14 Jul 2022 03:37:45 -0400 Subject: [PATCH 008/138] Sm comeback too strict (#755) --- worlds/sm/__init__.py | 11 ++++++----- worlds/sm/variaRandomizer/graph/graph.py | 16 ++++++++++++++++ worlds/sm/variaRandomizer/randomizer.py | 2 ++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 29d428abc2..fe1323caec 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -7,6 +7,8 @@ import threading import base64 from typing import Set, List, TextIO +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils + logger = logging.getLogger("Super Metroid") from .Locations import lookup_name_to_id as locations_lookup_name_to_id @@ -654,11 +656,10 @@ class SMLocation(Location): def can_comeback(self, state: CollectionState, item: Item): randoExec = state.world.worlds[self.player].variaRando.randoExec for key in locationsDict[self.name].AccessFrom.keys(): - if (randoExec.areaGraph.canAccess( state.smbm[self.player], - key, - randoExec.graphSettings.startAP, - state.smbm[self.player].maxDiff, - None)): + if (randoExec.areaGraph.canAccessList( state.smbm[self.player], + key, + [randoExec.graphSettings.startAP, 'Landing Site'] if not GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else ['Landing Site'], + state.smbm[self.player].maxDiff)): return True return False diff --git a/worlds/sm/variaRandomizer/graph/graph.py b/worlds/sm/variaRandomizer/graph/graph.py index bcbf138123..6ca7465a7e 100644 --- a/worlds/sm/variaRandomizer/graph/graph.py +++ b/worlds/sm/variaRandomizer/graph/graph.py @@ -367,6 +367,22 @@ class AccessGraph(object): #print("canAccess: {}".format(can)) return can + # test access from an access point to a list of others, given an optional item + def canAccessList(self, smbm, srcAccessPointName, destAccessPointNameList, maxDiff, item=None): + if item is not None: + smbm.addItem(item) + #print("canAccess: item: {}, src: {}, dest: {}".format(item, srcAccessPointName, destAccessPointName)) + destAccessPointList = [self.accessPoints[destAccessPointName] for destAccessPointName in destAccessPointNameList] + srcAccessPoint = self.accessPoints[srcAccessPointName] + availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff, item) + can = any(ap in availAccessPoints for ap in destAccessPointList) + # if not can: + # self.log.debug("canAccess KO: avail = {}".format([ap.Name for ap in availAccessPoints.keys()])) + if item is not None: + smbm.removeItem(item) + #print("canAccess: {}".format(can)) + return can + # returns a list of AccessPoint instances from srcAccessPointName to destAccessPointName # (not including source ap) # or None if no possible path diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 0da2b2d042..ebb87c520b 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -341,6 +341,8 @@ class VariaRandomizer: if preset == 'custom': PresetLoader.factory(world.custom_preset[player].value).load(self.player) elif preset == 'varia_custom': + if len(world.varia_custom_preset[player].value) == 0: + raise Exception("varia_custom was chosen but varia_custom_preset is missing.") url = 'https://randommetroidsolver.pythonanywhere.com/presetWebService' preset_name = next(iter(world.varia_custom_preset[player].value)) payload = '{{"preset": "{}"}}'.format(preset_name) From 122590fc6873340ce9a6e0530dbc0014ee6d96e3 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 14 Jul 2022 02:39:53 -0500 Subject: [PATCH 009/138] lttp: move open pyramid to new options system (#762) --- BaseClasses.py | 3 --- Generate.py | 3 --- Main.py | 1 - worlds/alttp/Options.py | 31 +++++++++++++++++++++++++++++++ worlds/alttp/Rom.py | 2 +- worlds/alttp/__init__.py | 11 ----------- 6 files changed, 32 insertions(+), 19 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a186404727..796db59219 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -126,7 +126,6 @@ class MultiWorld(): set_player_attr('beemizer_total_chance', 0) set_player_attr('beemizer_trap_chance', 0) set_player_attr('escape_assist', []) - set_player_attr('open_pyramid', False) set_player_attr('treasure_hunt_icon', 'Triforce Piece') set_player_attr('treasure_hunt_count', 0) set_player_attr('clock_mode', False) @@ -1431,8 +1430,6 @@ class Spoiler(): outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player]) if self.world.shuffle[player] != "vanilla": outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed) - outfile.write('Pyramid hole pre-opened: %s\n' % ( - 'Yes' if self.world.open_pyramid[player] else 'No')) outfile.write('Shop inventory shuffle: %s\n' % bool_to_text("i" in self.world.shop_shuffle[player])) outfile.write('Shop price shuffle: %s\n' % diff --git a/Generate.py b/Generate.py index b46c730c9a..125fab4163 100644 --- a/Generate.py +++ b/Generate.py @@ -583,9 +583,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.goal = goals[goal] - # TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when - # fast ganon + ganon at hole - ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal') extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available') diff --git a/Main.py b/Main.py index acbb4ad5cf..6daa16d908 100644 --- a/Main.py +++ b/Main.py @@ -47,7 +47,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.item_functionality = args.item_functionality.copy() world.timer = args.timer.copy() world.goal = args.goal.copy() - world.open_pyramid = args.open_pyramid.copy() world.boss_shuffle = args.shufflebosses.copy() world.enemy_health = args.enemy_health.copy() world.enemy_damage = args.enemy_damage.copy() diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index d7f9becbfd..b42a5eb377 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,5 +1,6 @@ import typing +from BaseClasses import MultiWorld from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink @@ -27,6 +28,35 @@ class Goal(Choice): option_hand_in = 2 +class OpenPyramid(Choice): + """Determines whether the hole at the top of pyramid is open. + Goal will open the pyramid if the goal requires you to kill Ganon, without needing to kill Agahnim 2. + Auto is the same as goal except if Ganon's dropdown is in another location, the hole will be closed.""" + display_name = "Open Pyramid Hole" + option_closed = 0 + option_open = 1 + option_goal = 2 + option_auto = 3 + default = option_goal + + alias_true = option_open + alias_false = option_closed + alias_yes = option_open + alias_no = option_closed + + def to_bool(self, world: MultiWorld, player: int) -> bool: + if self.value == self.option_goal: + return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} + elif self.value == self.option_auto: + return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \ + and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not + world.shuffle_ganon) + elif self.value == self.option_open: + return True + else: + return False + + class DungeonItem(Choice): value: int option_original_dungeon = 0 @@ -331,6 +361,7 @@ class AllowCollect(Toggle): alttp_options: typing.Dict[str, type(Option)] = { "crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_ganon": CrystalsGanon, + "open_pyramid": OpenPyramid, "bigkey_shuffle": bigkey_shuffle, "smallkey_shuffle": smallkey_shuffle, "compass_shuffle": compass_shuffle, diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 72cd1ceac5..c16bbf5322 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1247,7 +1247,7 @@ def patch_rom(world, rom, player, enemized): rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest - rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player] else 0x00) # pre-open Pyramid Hole + rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, player) else 0x00) # pre-open Pyramid Hole rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[ player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0 rom.write_byte(0xF5D73, 0xF0) # bees are catchable diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 8e4ec1c143..871c44684d 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -176,17 +176,6 @@ class ALTTPWorld(World): def create_regions(self): player = self.player world = self.world - if world.open_pyramid[player] == 'goal': - world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', - 'localganontriforcehunt', 'ganonpedestal'} - elif world.open_pyramid[player] == 'auto': - world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', - 'localganontriforcehunt', 'ganonpedestal'} and \ - (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', - 'dungeonscrossed'} or not world.shuffle_ganon) - else: - world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get( - world.open_pyramid[player], 'auto') world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player]) From 6e0a0c5c4ae33b1a860d9a8a2b9d696ff9ebb2af Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 14 Jul 2022 09:46:03 +0200 Subject: [PATCH 010/138] Core: skip second sanity check when pushing an item into a location (-O) (#745) --- BaseClasses.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 796db59219..6816617279 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -389,20 +389,14 @@ class MultiWorld(): self.state.collect(item, True) def push_item(self, location: Location, item: Item, collect: bool = True): - if not isinstance(location, Location): - raise RuntimeError( - 'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player)) + assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}." + location.item = item + item.location = location + item.world = self # try to not have this here anymore and create it with item? + if collect: + self.state.collect(item, location.event, location) - if location.can_fill(self.state, item, False): - location.item = item - item.location = location - item.world = self # try to not have this here anymore - if collect: - self.state.collect(item, location.event, location) - - logging.debug('Placed %s at %s', item, location) - else: - raise RuntimeError('Cannot assign item %s to location %s.' % (item, location)) + logging.debug('Placed %s at %s', item, location) def get_entrances(self) -> List[Entrance]: if self._cached_entrances is None: From e804f592debb7e0942bdee34a2e591eacd7ecbe5 Mon Sep 17 00:00:00 2001 From: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Date: Thu, 14 Jul 2022 03:51:00 -0400 Subject: [PATCH 011/138] SC2: Windows ".dll missing" fix and fix for finding SC2 install automatically (#721) --- Starcraft2Client.py | 124 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index f9b6b43fe3..e9e06335ac 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -19,7 +19,13 @@ from worlds.sc2wol.Items import lookup_id_to_name, item_table from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET from worlds.sc2wol import SC2WoLWorld -from Utils import init_logging +from pathlib import Path +import re +from MultiServer import mark_raw +import ctypes +import sys + +from Utils import init_logging, is_windows if __name__ == "__main__": init_logging("SC2Client", exception_logger="Client") @@ -73,6 +79,17 @@ class StarcraftClientProcessor(ClientCommandProcessor): request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx) return True + @mark_raw + def _cmd_set_path(self, path: str = '') -> bool: + """Manually set the SC2 install directory (if the automatic detection fails).""" + if path: + os.environ["SC2PATH"] = path + check_mod_install() + return True + else: + sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.") + return False + class SC2Context(CommonContext): command_processor = StarcraftClientProcessor @@ -111,6 +128,11 @@ class SC2Context(CommonContext): for mission in slot_req_table: self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission]) + # Look for and set SC2PATH. + # check_game_install_path() returns True if and only if it finds + sets SC2PATH. + if "SC2PATH" not in os.environ and check_game_install_path(): + check_mod_install() + if cmd in {"PrintJSON"}: if "receiving" in args: if self.slot_concerns_self(args["receiving"]): @@ -415,8 +437,9 @@ async def starcraft_launch(ctx: SC2Context, mission_id): sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.") - run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), - name="Archipelago", fullscreen=True)], realtime=True) + with DllDirectory(None): + run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), + name="Archipelago", fullscreen=True)], realtime=True) class ArchipelagoBot(sc2.bot_ai.BotAI): @@ -796,6 +819,101 @@ def initialize_blank_mission_dict(location_table): return unlocks +def check_game_install_path() -> bool: + # First thing: go to the default location for ExecuteInfo. + # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it. + if is_windows: + # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow. + # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555# + import ctypes.wintypes + CSIDL_PERSONAL = 5 # My Documents + SHGFP_TYPE_CURRENT = 0 # Get current, not default value + + buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) + ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf) + documentspath = buf.value + einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt")) + else: + einfo = str(sc2.paths.get_home() / Path(sc2.paths.USERPATH[sc2.paths.PF])) + + # Check if the file exists. + if os.path.isfile(einfo): + + # Open the file and read it, picking out the latest executable's path. + with open(einfo) as f: + content = f.read() + if content: + base = re.search(r" = (.*)Versions", content).group(1) + if os.path.exists(base): + executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions") + + # Finally, check the path for an actual executable. + # If we find one, great. Set up the SC2PATH. + if os.path.isfile(executable): + sc2_logger.info(f"Found an SC2 install at {base}!") + sc2_logger.debug(f"Latest executable at {executable}.") + os.environ["SC2PATH"] = base + sc2_logger.debug(f"SC2PATH set to {base}.") + return True + else: + sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.") + else: + sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.") + else: + sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.") + return False + + +def check_mod_install() -> bool: + # Pull up the SC2PATH if set. If not, encourage the user to manually run /set_path. + try: + # Check inside the Mods folder for Archipelago.SC2Mod. If found, tell user. If not, tell user. + if os.path.isfile(modfile := (os.environ["SC2PATH"] / Path("Mods") / Path("Archipelago.SC2Mod"))): + sc2_logger.info(f"Archipelago mod found at {modfile}.") + return True + else: + sc2_logger.warning(f"Archipelago mod could not be found at {modfile}. Please install the mod file there.") + except KeyError: + sc2_logger.warning(f"SC2PATH isn't set. Please run /set_path with the path to your SC2 install.") + return False + + +class DllDirectory: + # Credit to Black Sliver for this code. + # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw + _old: typing.Optional[str] = None + _new: typing.Optional[str] = None + + def __init__(self, new: typing.Optional[str]): + self._new = new + + def __enter__(self): + old = self.get() + if self.set(self._new): + self._old = old + + def __exit__(self, *args): + if self._old is not None: + self.set(self._old) + + @staticmethod + def get() -> str: + if sys.platform == "win32": + n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) + buf = ctypes.create_unicode_buffer(n) + ctypes.windll.kernel32.GetDllDirectoryW(n, buf) + return buf.value + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return None + + @staticmethod + def set(s: typing.Optional[str]) -> bool: + if sys.platform == "win32": + return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0 + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return False + + if __name__ == '__main__': colorama.init() asyncio.run(main()) From e38308bac326652ba17a4ae008810d724454fed8 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 14 Jul 2022 18:37:14 +0200 Subject: [PATCH 012/138] sm64ex: Allow setting Big Star Door requirements (#773) * sm64ex: Allow setting Big Star Door requirements * sm64ex: Lower requirements for StarsToFinish --- worlds/sm64ex/Options.py | 24 +++++++++++++++++++++++- worlds/sm64ex/Rules.py | 8 ++++---- worlds/sm64ex/__init__.py | 8 +++++--- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 99b3b3ee0b..bddfc3fb31 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -13,9 +13,28 @@ class StrictCannonRequirements(DefaultOnToggle): """If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy Checks are enabled""" display_name = "Strict Cannon Requirements" +class FirstBowserStarDoorCost(Range): + """How many stars are required at the Star Door to Bowser in the Dark World""" + range_start = 0 + range_end = 20 + default = 8 + +class BasementStarDoorCost(Range): + """How many stars are required at the Star Door in the Basement""" + range_start = 0 + range_end = 50 + default = 30 + +class SecondFloorStarDoorCost(Range): + """How many stars are required to access the third floor""" + range_start = 0 + range_end = 50 + default = 50 + class StarsToFinish(Range): """How many stars are required at the infinite stairs""" - range_start = 50 + display_name = "Endless Stairs Stars" + range_start = 0 range_end = 100 default = 70 @@ -43,6 +62,9 @@ sm64_options: typing.Dict[str,type(Option)] = { "EnableCoinStars": EnableCoinStars, "StrictCapRequirements": StrictCapRequirements, "StrictCannonRequirements": StrictCannonRequirements, + "FirstBowserStarDoorCost": FirstBowserStarDoorCost, + "BasementStarDoorCost": BasementStarDoorCost, + "SecondFloorStarDoorCost": SecondFloorStarDoorCost, "StarsToFinish": StarsToFinish, "ExtraStars": ExtraStars, "death_link": DeathLink, diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index fcd5619323..cad9dd239a 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -12,7 +12,7 @@ def set_rules(world, player: int, area_connections): connect_regions(world, player, "Menu", sm64courses[area_connections[1]], lambda state: state.has("Power Star", player, 1)) connect_regions(world, player, "Menu", sm64courses[area_connections[2]], lambda state: state.has("Power Star", player, 3)) connect_regions(world, player, "Menu", sm64courses[area_connections[3]], lambda state: state.has("Power Star", player, 3)) - connect_regions(world, player, "Menu", "Bowser in the Dark World", lambda state: state.has("Power Star", player, 8)) + connect_regions(world, player, "Menu", "Bowser in the Dark World", lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) connect_regions(world, player, "Menu", sm64courses[area_connections[4]], lambda state: state.has("Power Star", player, 12)) connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1)) @@ -20,8 +20,8 @@ def set_rules(world, player: int, area_connections): connect_regions(world, player, "Basement", sm64courses[area_connections[5]]) connect_regions(world, player, "Basement", sm64courses[area_connections[6]]) connect_regions(world, player, "Basement", sm64courses[area_connections[7]]) - connect_regions(world, player, "Basement", sm64courses[area_connections[8]], lambda state: state.has("Power Star", player, 30)) - connect_regions(world, player, "Basement", "Bowser in the Fire Sea", lambda state: state.has("Power Star", player, 30) and + connect_regions(world, player, "Basement", sm64courses[area_connections[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) + connect_regions(world, player, "Basement", "Bowser in the Fire Sea", lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) @@ -31,7 +31,7 @@ def set_rules(world, player: int, area_connections): connect_regions(world, player, "Second Floor", sm64courses[area_connections[11]]) connect_regions(world, player, "Second Floor", sm64courses[area_connections[12]]) - connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, 50)) + connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value)) connect_regions(world, player, "Third Floor", sm64courses[area_connections[13]]) connect_regions(world, player, "Third Floor", sm64courses[area_connections[14]]) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index f19f8ee79e..64b3874651 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -9,9 +9,6 @@ from .Regions import create_regions, sm64courses from BaseClasses import Item, Tutorial, ItemClassification from ..AutoWorld import World, WebWorld -client_version = 1 - - class SM64Web(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", @@ -38,6 +35,8 @@ class SM64World(World): location_name_to_id = location_table data_version = 6 + client_version = 2 + forced_auto_forfeit = False area_connections: typing.Dict[int, int] @@ -115,6 +114,9 @@ class SM64World(World): def fill_slot_data(self): return { "AreaRando": self.area_connections, + "FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[self.player].value, + "BasementDoorCost": self.world.BasementStarDoorCost[self.player].value, + "SecondFloorCost": self.world.SecondFloorStarDoorCost[self.player].value, "StarsToFinish": self.world.StarsToFinish[self.player].value, "DeathLink": self.world.death_link[self.player].value, } From 76f6eb1434d349116142e6cb693724a3d3f3fc4c Mon Sep 17 00:00:00 2001 From: jsd1982 Date: Fri, 8 Jul 2022 09:36:14 -0500 Subject: [PATCH 013/138] SNIClient: update default SNI port from 8080 to 23074 --- SNIClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index e313feff00..151a68da11 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -62,7 +62,7 @@ class SNIClientCommandProcessor(ClientCommandProcessor): def _cmd_snes(self, snes_options: str = "") -> bool: """Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices; and a SNES device number if more than one SNES is detected. - Examples: "/snes", "/snes 1", "/snes localhost:8080 1" """ + Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """ snes_address = self.ctx.snes_address snes_device_number = -1 @@ -1296,7 +1296,7 @@ async def main(): parser = get_base_parser() parser.add_argument('diff_file', default="", type=str, nargs="?", help='Path to a Archipelago Binary Patch file') - parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.') + parser.add_argument('--snes', default='localhost:23074', help='Address of the SNI server.') parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) args = parser.parse_args() From aa954b776d8fabffde060ba240e7c7114db8c38e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 3 Jul 2022 14:03:49 +0200 Subject: [PATCH 014/138] MultiServer: add /status and allow status command to dynamically filter for Tags --- MultiServer.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 06f9a9f9cd..dd695fc1bb 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -720,16 +720,16 @@ def get_players_string(ctx: Context): return f'{len(auth_clients)} players of {total} connected ' + text[:-1] -def get_status_string(ctx: Context, team: int): - text = "Player Status on your team:" +def get_status_string(ctx: Context, team: int, tag: str): + text = f"Player Status on team {team}:" for slot in ctx.locations: connected = len(ctx.clients[team][slot]) - death_link = len([client for client in ctx.clients[team][slot] if "DeathLink" in client.tags]) + tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags]) completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})" - death_text = f" {death_link} of which are death link" if connected else "" + tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else "" goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "." text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \ - f"{death_text}{goal_text} {completion_text}" + f"{tag_text}{goal_text} {completion_text}" return text @@ -1113,9 +1113,11 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output(get_players_string(self.ctx)) return True - def _cmd_status(self) -> bool: - """Get status information about your team.""" - self.output(get_status_string(self.ctx, self.client.team)) + def _cmd_status(self, tag:str="") -> bool: + """Get status information about your team. + Optionally mention a Tag name and get information on who has that Tag. + For example: DeathLink or EnergyLink.""" + self.output(get_status_string(self.ctx, self.client.team, tag)) return True def _cmd_release(self) -> bool: @@ -1657,6 +1659,14 @@ class ServerCommandProcessor(CommonCommandProcessor): self.output(get_players_string(self.ctx)) return True + def _cmd_status(self, tag: str = "") -> bool: + """Get status information about teams. + Optionally mention a Tag name and get information on who has that Tag. + For example: DeathLink or EnergyLink.""" + for team in self.ctx.clients: + self.output(get_status_string(self.ctx, team, tag)) + return True + def _cmd_exit(self) -> bool: """Shutdown the server""" asyncio.create_task(self.ctx.server.ws_server._close()) From 8e15fe51b6b2c0360791a0c1a3fc5c64bda2fd17 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 15 Jul 2022 06:54:29 +0200 Subject: [PATCH 015/138] Put common options first (#774) * this applies to yaml and webhost * this allows overwriting common options from the world --- WebHostLib/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 203f223561..2cab7728da 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -60,7 +60,7 @@ def create(): for game_name, world in AutoWorldRegister.world_types.items(): - all_options = {**world.options, **Options.per_game_common_options} + all_options = {**Options.per_game_common_options, **world.options} res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( options=all_options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, From 73fb1b80740f1e2a4691949ec5c6040d753d8b6c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 15 Jul 2022 17:41:53 +0200 Subject: [PATCH 016/138] Subnautica: updates (#759) * Subnautica: add more goals * Subnautica: fix wrongly positioned Databox * Subnautica: allow techs to remain vanilla * Subnautica: make zipimport compatible * Subnautica: force two Seaglide fragments into local sphere 1 --- Main.py | 3 +- worlds/subnautica/Items.py | 360 ++++++++++++++++++- worlds/subnautica/Locations.py | 576 ++++++++++++++++++++++++++++++- worlds/subnautica/Options.py | 29 +- worlds/subnautica/Regions.py | 8 - worlds/subnautica/Rules.py | 71 ++-- worlds/subnautica/__init__.py | 123 ++++--- worlds/subnautica/items.json | 83 ----- worlds/subnautica/locations.json | 521 ---------------------------- 9 files changed, 1059 insertions(+), 715 deletions(-) delete mode 100644 worlds/subnautica/Regions.py delete mode 100644 worlds/subnautica/items.json delete mode 100644 worlds/subnautica/locations.json diff --git a/Main.py b/Main.py index 6daa16d908..3912e65c99 100644 --- a/Main.py +++ b/Main.py @@ -363,7 +363,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for location in world.get_filled_locations(): if type(location.address) == int: assert location.item.code is not None, "item code None should be event, " \ - "location.address should then also be None" + "location.address should then also be None. Location: " \ + f" {location}" locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags if location.name in world.start_location_hints[location.player]: diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index b9377b7ae6..b55efe2453 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -1,23 +1,353 @@ -import json -import os +from BaseClasses import ItemClassification +from typing import TypedDict, Dict, Set -with open(os.path.join(os.path.dirname(__file__), 'items.json'), 'r') as file: - item_table = json.loads(file.read()) -lookup_id_to_name = {} -lookup_name_to_item = {} -advancement_item_names = set() -non_advancement_item_names = set() +class ItemDict(TypedDict): + classification: ItemClassification + count: int + name: str + tech_type: str -for item in item_table: - item_name = item["name"] - lookup_id_to_name[item["id"]] = item_name - lookup_name_to_item[item_name] = item - if item["progression"]: + +item_table: Dict[int, ItemDict] = { + 35000: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Compass', + 'tech_type': 'Compass'}, + 35001: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Lightweight High Capacity Tank', + 'tech_type': 'PlasteelTank'}, + 35002: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Vehicle Upgrade Console', + 'tech_type': 'BaseUpgradeConsole'}, + 35003: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ultra Glide Fins', + 'tech_type': 'UltraGlideFins'}, + 35004: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Sonar Upgrade', + 'tech_type': 'CyclopsSonarModule'}, + 35005: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Reinforced Dive Suit', + 'tech_type': 'ReinforcedDiveSuit'}, + 35006: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Thermal Reactor Module', + 'tech_type': 'CyclopsThermalReactorModule'}, + 35007: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Stillsuit', + 'tech_type': 'Stillsuit'}, + 35008: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Alien Containment Fragment', + 'tech_type': 'BaseWaterParkFragment'}, + 35009: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Creature Decoy', + 'tech_type': 'CyclopsDecoy'}, + 35010: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Fire Suppression System', + 'tech_type': 'CyclopsFireSuppressionModule'}, + 35011: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Swim Charge Fins', + 'tech_type': 'SwimChargeFins'}, + 35012: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Repulsion Cannon', + 'tech_type': 'RepulsionCannon'}, + 35013: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Decoy Tube Upgrade', + 'tech_type': 'CyclopsDecoyModule'}, + 35014: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Cyclops Shield Generator', + 'tech_type': 'CyclopsShieldModule'}, + 35015: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Cyclops Depth Module MK1', + 'tech_type': 'CyclopsHullModule1'}, + 35016: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Docking Bay Repair Module', + 'tech_type': 'CyclopsSeamothRepairModule'}, + 35017: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Battery Charger fragment', + 'tech_type': 'BatteryChargerFragment'}, + 35018: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Beacon Fragment', + 'tech_type': 'BeaconFragment'}, + 35019: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Bioreactor Fragment', + 'tech_type': 'BaseBioReactorFragment'}, + 35020: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Cyclops Bridge Fragment', + 'tech_type': 'CyclopsBridgeFragment'}, + 35021: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Cyclops Engine Fragment', + 'tech_type': 'CyclopsEngineFragment'}, + 35022: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Cyclops Hull Fragment', + 'tech_type': 'CyclopsHullFragment'}, + 35023: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Grav Trap Fragment', + 'tech_type': 'GravSphereFragment'}, + 35024: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Laser Cutter Fragment', + 'tech_type': 'LaserCutterFragment'}, + 35025: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Light Stick Fragment', + 'tech_type': 'TechlightFragment'}, + 35026: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Mobile Vehicle Bay Fragment', + 'tech_type': 'ConstructorFragment'}, + 35027: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Modification Station Fragment', + 'tech_type': 'WorkbenchFragment'}, + 35028: {'classification': ItemClassification.progression, + 'count': 2, + 'name': 'Moonpool Fragment', + 'tech_type': 'MoonpoolFragment'}, + 35029: {'classification': ItemClassification.useful, + 'count': 3, + 'name': 'Nuclear Reactor Fragment', + 'tech_type': 'BaseNuclearReactorFragment'}, + 35030: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Power Cell Charger Fragment', + 'tech_type': 'PowerCellChargerFragment'}, + 35031: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Power Transmitter Fragment', + 'tech_type': 'PowerTransmitterFragment'}, + 35032: {'classification': ItemClassification.progression, + 'count': 4, + 'name': 'Prawn Suit Fragment', + 'tech_type': 'ExosuitFragment'}, + 35033: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Prawn Suit Drill Arm Fragment', + 'tech_type': 'ExosuitDrillArmFragment'}, + 35034: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Prawn Suit Grappling Arm Fragment', + 'tech_type': 'ExosuitGrapplingArmFragment'}, + 35035: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Prawn Suit Propulsion Cannon Fragment', + 'tech_type': 'ExosuitPropulsionArmFragment'}, + 35036: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Prawn Suit Torpedo Arm Fragment', + 'tech_type': 'ExosuitTorpedoArmFragment'}, + 35037: {'classification': ItemClassification.useful, + 'count': 3, + 'name': 'Scanner Room Fragment', + 'tech_type': 'BaseMapRoomFragment'}, + 35038: {'classification': ItemClassification.progression, + 'count': 5, + 'name': 'Seamoth Fragment', + 'tech_type': 'SeamothFragment'}, + 35039: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Stasis Rifle Fragment', + 'tech_type': 'StasisRifleFragment'}, + 35040: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Thermal Plant Fragment', + 'tech_type': 'ThermalPlantFragment'}, + 35041: {'classification': ItemClassification.progression, + 'count': 2, + 'name': 'Seaglide Fragment', + 'tech_type': 'SeaglideFragment'}, + 35042: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Radiation Suit', + 'tech_type': 'RadiationSuit'}, + 35043: {'classification': ItemClassification.progression, + 'count': 2, + 'name': 'Propulsion Cannon Fragment', + 'tech_type': 'PropulsionCannonFragment'}, + 35044: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Launch Platform', + 'tech_type': 'RocketBase'}, + 35045: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ion Power Cell', + 'tech_type': 'PrecursorIonPowerCell'}, + 35046: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Exterior Growbed Fragment', + 'tech_type': 'FarmingTrayFragment'}, + 35047: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Picture Frame', + 'tech_type': 'PictureFrameFragment'}, + 35048: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Bench Fragment', + 'tech_type': 'BenchFragment'}, + 35049: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Basic Plant Pot', + 'tech_type': 'PlanterPotFragment'}, + 35050: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Interior Growbed', + 'tech_type': 'PlanterBoxFragment'}, + 35051: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Plant Shelf', + 'tech_type': 'PlanterShelfFragment'}, + 35052: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Observatory Fragment', + 'tech_type': 'BaseObservatoryFragment'}, + 35053: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Multipurpose Room Fragment', + 'tech_type': 'BaseRoomFragment'}, + 35054: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Bulkhead Fragment', + 'tech_type': 'BaseBulkheadFragment'}, + 35055: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Spotlight', + 'tech_type': 'Spotlight'}, + 35056: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Desk', + 'tech_type': 'StarshipDesk'}, + 35057: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Swivel Chair', + 'tech_type': 'StarshipChair'}, + 35058: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Office Chair', + 'tech_type': 'StarshipChair2'}, + 35059: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Command Chair', + 'tech_type': 'StarshipChair3'}, + 35060: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Counter', + 'tech_type': 'LabCounter'}, + 35061: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Single Bed', + 'tech_type': 'NarrowBed'}, + 35062: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Basic Double Bed', + 'tech_type': 'Bed1'}, + 35063: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Quilted Double Bed', + 'tech_type': 'Bed2'}, + 35064: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Coffee Vending Machine', + 'tech_type': 'CoffeeVendingMachine'}, + 35065: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Trash Can', + 'tech_type': 'Trashcans'}, + 35066: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Floodlight', + 'tech_type': 'Techlight'}, + 35067: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Bar Table', + 'tech_type': 'BarTable'}, + 35068: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Vending Machine', + 'tech_type': 'VendingMachine'}, + 35069: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Single Wall Shelf', + 'tech_type': 'SingleWallShelf'}, + 35070: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Wall Shelves', + 'tech_type': 'WallShelves'}, + 35071: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Round Plant Pot', + 'tech_type': 'PlanterPot2'}, + 35072: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Chic Plant Pot', + 'tech_type': 'PlanterPot3'}, + 35073: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Nuclear Waste Disposal', + 'tech_type': 'LabTrashcan'}, + 35074: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Wall Planter', + 'tech_type': 'BasePlanter'}, + 35075: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ion Battery', + 'tech_type': 'PrecursorIonBattery'}, + 35076: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Gantry', + 'tech_type': 'RocketBaseLadder'}, + 35077: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Boosters', + 'tech_type': 'RocketStage1'}, + 35078: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Fuel Reserve', + 'tech_type': 'RocketStage2'}, + 35079: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Cockpit', + 'tech_type': 'RocketStage3'}, + 35080: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Water Filtration Machine', + 'tech_type': 'BaseFiltrationMachine'}} + +advancement_item_names: Set[str] = set() +non_advancement_item_names: Set[str] = set() + +for item_id, item_data in item_table.items(): + item_name = item_data["name"] + if ItemClassification.progression in item_data["classification"]: advancement_item_names.add(item_name) else: non_advancement_item_names.add(item_name) -lookup_id_to_name[None] = "Victory" +if False: # turn to True to export for Subnautica mod + payload = {item_id: item_data["tech_type"] for item_id, item_data in item_table.items()} + import json -lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()} \ No newline at end of file + with open("items.json", "w") as f: + json.dump(payload, f) diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index 361a712ba8..c437fbc9bf 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -1,12 +1,570 @@ -import json -import os +from typing import Dict, TypedDict, List -with open(os.path.join(os.path.dirname(__file__), 'locations.json'), 'r') as file: - location_table = json.loads(file.read()) -lookup_id_to_name = {} -for item in location_table: - lookup_id_to_name[item["id"]] = item["name"] +class Vector(TypedDict): + x: float + y: float + z: float -lookup_id_to_name[None] = "Neptune Launch" -lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()} + +class LocationDict(TypedDict, total=False): + name: str + can_slip_through: bool + need_laser_cutter: bool + position: Vector + need_propulsion_cannon: bool + + +events: List[str] = ["Neptune Launch", "Disable Quarantine", "Full Infection", "Repair Aurora Drive"] + +location_table: Dict[int, LocationDict] = { + 33000: {'can_slip_through': False, + 'name': 'Blood Kelp Trench Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1234.3, 'y': -349.7, 'z': -396.0}}, + 33001: {'can_slip_through': False, + 'name': 'Blood Kelp Trench Wreck - Inside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1208.0, 'y': -349.6, 'z': -383.0}}, + 33002: {'can_slip_through': False, + 'name': 'Blood Kelp Trench Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -1210.6, 'y': -340.7, 'z': -393.4}}, + 33003: {'can_slip_through': False, + 'name': 'Bulb Zone West Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': 903.8, 'y': -220.3, 'z': 590.9}}, + 33004: {'can_slip_through': False, + 'name': 'Bulb Zone West Wreck - Under Databox', + 'need_laser_cutter': False, + 'position': {'x': 910.9, 'y': -201.8, 'z': 623.5}}, + 33005: {'can_slip_through': False, + 'name': 'Bulb Zone West Wreck - Inside Databox', + 'need_laser_cutter': True, + 'position': {'x': 914.9, 'y': -202.1, 'z': 611.8}}, + 33006: {'can_slip_through': False, + 'name': 'Bulb Zone West Wreck - PDA', + 'need_laser_cutter': True, + 'position': {'x': 912.6, 'y': -202.0, 'z': 609.5}}, + 33007: {'can_slip_through': False, + 'name': 'Bulb Zone East Wreck - Databox', + 'need_laser_cutter': False, + 'position': {'x': 1327.1, 'y': -234.9, 'z': 575.8}}, + 33008: {'can_slip_through': False, + 'name': 'Dunes North Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1407.7, 'y': -344.2, 'z': 721.5}}, + 33009: {'can_slip_through': False, + 'name': 'Dunes North Wreck - Office Databox', + 'need_laser_cutter': False, + 'position': {'x': -1393.9, 'y': -329.7, 'z': 733.5}}, + 33010: {'can_slip_through': False, + 'name': 'Dunes North Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -1396.3, 'y': -330.8, 'z': 730.0}}, + 33011: {'can_slip_through': False, + 'name': 'Dunes North Wreck - Cargo Databox', + 'need_laser_cutter': True, + 'position': {'x': -1409.8, 'y': -332.4, 'z': 706.9}}, + 33012: {'can_slip_through': False, + 'name': 'Dunes West Wreck - Databox', + 'need_laser_cutter': False, + 'position': {'x': -1626.2, 'y': -357.5, 'z': 99.5}}, + 33013: {'can_slip_through': False, + 'name': 'Dunes East Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1196.3, 'y': -223.0, 'z': 12.5}}, + 33014: {'can_slip_through': False, + 'name': 'Dunes East Wreck - Inside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1206.4, 'y': -225.6, 'z': 4.0}}, + 33015: {'can_slip_through': False, + 'name': 'Grand Reef North Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -269.7, 'y': -262.8, 'z': -764.3}}, + 33016: {'can_slip_through': False, + 'name': 'Grand Reef North Wreck - Elevator Databox', + 'need_laser_cutter': True, + 'position': {'x': -285.8, 'y': -240.2, 'z': -786.5}}, + 33017: {'can_slip_through': False, + 'name': 'Grand Reef North Wreck - Bottom Databox', + 'need_laser_cutter': False, + 'position': {'x': -285.2, 'y': -262.4, 'z': -788.4}}, + 33018: {'can_slip_through': False, + 'name': 'Grand Reef North Wreck - Hangar PDA', + 'need_laser_cutter': False, + 'position': {'x': -272.5, 'y': -254.7, 'z': -788.5}}, + 33019: {'can_slip_through': False, + 'name': 'Grand Reef South Wreck - Trench Databox', + 'need_laser_cutter': False, + 'position': {'x': -850.9, 'y': -473.2, 'z': -1414.6}}, + 33020: {'can_slip_through': False, + 'name': 'Grand Reef South Wreck - Comms Databox', + 'need_laser_cutter': True, + 'position': {'x': -889.4, 'y': -433.8, 'z': -1424.8}}, + 33021: {'can_slip_through': False, + 'name': 'Grand Reef South Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -862.4, 'y': -437.5, 'z': -1444.1}}, + 33022: {'can_slip_through': False, + 'name': 'Grand Reef South Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -887.9, 'y': -446.0, 'z': -1422.7}}, + 33023: {'can_slip_through': False, + 'name': 'Grassy Plateaus South Wreck - Databox', + 'need_laser_cutter': False, + 'position': {'x': -23.3, 'y': -105.8, 'z': -604.2}}, + 33024: {'can_slip_through': False, + 'name': 'Grassy Plateaus South Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -27.3, 'y': -106.8, 'z': -607.2}}, + 33025: {'can_slip_through': True, + 'name': 'Grassy Plateaus East Wreck - Breach Databox', + 'need_laser_cutter': True, + 'position': {'x': 313.9, 'y': -91.8, 'z': 432.6}}, + 33026: {'can_slip_through': True, + 'name': 'Grassy Plateaus East Wreck - Hangar Databox', + 'need_laser_cutter': True, + 'position': {'x': 319.4, 'y': -104.3, 'z': 441.5}}, + 33027: {'can_slip_through': False, + 'name': 'Grassy Plateaus West Wreck - Locker PDA', + 'need_laser_cutter': False, + 'position': {'x': -632.3, 'y': -75.0, 'z': -8.9}}, + 33028: {'can_slip_through': False, + 'name': 'Grassy Plateaus West Wreck - Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -664.4, 'y': -97.8, 'z': -8.0}}, + 33029: {'can_slip_through': False, + 'name': 'Grassy Plateaus West Wreck - Databox', + 'need_laser_cutter': True, + 'position': {'x': -421.4, 'y': -107.8, 'z': -266.5}}, + 33030: {'can_slip_through': False, + 'name': 'Safe Shallows Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -44.0, 'y': -29.1, 'z': -403.6}}, + 33031: {'can_slip_through': False, + 'name': 'Kelp Forest Wreck - Databox', + 'need_laser_cutter': False, + 'position': {'x': -317.6, 'y': -78.8, 'z': 247.4}}, + 33032: {'can_slip_through': False, + 'name': 'Kelp Forest Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': 63.2, 'y': -38.5, 'z': 382.9}}, + 33033: {'can_slip_through': False, + 'name': 'Mountains West Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': 740.3, 'y': -389.2, 'z': 1179.8}}, + 33034: {'can_slip_through': False, + 'name': 'Mountains West Wreck - Data Terminal', + 'need_laser_cutter': True, + 'position': {'x': 703.7, 'y': -365.9, 'z': 1199.3}}, + 33035: {'can_slip_through': False, + 'name': 'Mountains West Wreck - Hangar Databox', + 'need_laser_cutter': True, + 'position': {'x': 698.2, 'y': -350.8, 'z': 1186.9}}, + 33036: {'can_slip_through': False, + 'name': 'Mountains West Wreck - Office Databox', + 'need_laser_cutter': False, + 'position': {'x': 676.3, 'y': -343.6, 'z': 1204.6}}, + 33037: {'can_slip_through': False, + 'name': 'Mountains East Wreck - Comms Databox', + 'need_laser_cutter': False, + 'position': {'x': 1068.5, 'y': -283.4, 'z': 1345.3}}, + 33038: {'can_slip_through': False, + 'name': 'Mountains East Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': 1075.7, 'y': -288.9, 'z': 1321.8}}, + 33039: {'can_slip_through': False, + 'name': 'Northwestern Mushroom Forest Wreck - Cargo Databox', + 'need_laser_cutter': True, + 'position': {'x': -655.1, 'y': -109.6, 'z': 791.0}}, + 33040: {'can_slip_through': False, + 'name': 'Northwestern Mushroom Forest Wreck - Office Databox', + 'need_laser_cutter': False, + 'position': {'x': -663.4, 'y': -111.9, 'z': 777.9}}, + 33041: {'can_slip_through': False, + 'name': 'Northwestern Mushroom Forest Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -662.2, 'y': -113.4, 'z': 777.7}}, + 33042: {'can_slip_through': False, + 'name': "Sea Treader's Path Wreck - Outside Databox", + 'need_laser_cutter': False, + 'position': {'x': -1161.1, 'y': -191.7, 'z': -758.3}}, + 33043: {'can_slip_through': False, + 'name': "Sea Treader's Path Wreck - Hangar Databox", + 'need_laser_cutter': True, + 'position': {'x': -1129.5, 'y': -155.2, 'z': -729.3}}, + 33044: {'can_slip_through': False, + 'name': "Sea Treader's Path Wreck - Lobby Databox", + 'need_laser_cutter': False, + 'position': {'x': -1115.9, 'y': -175.3, 'z': -724.5}}, + 33045: {'can_slip_through': False, + 'name': "Sea Treader's Path Wreck - PDA", + 'need_laser_cutter': False, + 'position': {'x': -1136.8, 'y': -157.0, 'z': -734.6}}, + 33046: {'can_slip_through': False, + 'name': 'Sparse Reef Wreck - Locker Databox', + 'need_laser_cutter': True, + 'position': {'x': -789.8, 'y': -216.1, 'z': -711.0}}, + 33047: {'can_slip_through': False, + 'name': 'Sparse Reef Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -810.7, 'y': -209.3, 'z': -685.5}}, + 33048: {'can_slip_through': False, + 'name': 'Sparse Reef Wreck - Lab Databox', + 'need_laser_cutter': True, + 'position': {'x': -795.5, 'y': -204.1, 'z': -774.7}}, + 33049: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -170.8, 'y': -187.6, 'z': 880.7}}, + 33050: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Hangar Databox', + 'need_laser_cutter': True, + 'position': {'x': -138.4, 'y': -193.6, 'z': 888.7}}, + 33051: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Data Terminal', + 'need_laser_cutter': True, + 'position': {'x': -130.7, 'y': -193.2, 'z': 883.3}}, + 33052: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Cable Databox', + 'need_laser_cutter': False, + 'position': {'x': -137.8, 'y': -193.4, 'z': 879.4}}, + 33053: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Pipes Databox 1', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': -124.4, 'y': -200.7, 'z': 853.0}}, + 33054: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Pipes Databox 2', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': -126.8, 'y': -201.1, 'z': 852.1}}, + 33055: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Bedroom Databox', + 'need_laser_cutter': False, + 'position': {'x': -643.8, 'y': -509.9, 'z': -941.9}}, + 33056: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Observatory Databox', + 'need_laser_cutter': False, + 'position': {'x': -635.1, 'y': -502.7, 'z': -951.4}}, + 33057: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Bedroom PDA', + 'need_laser_cutter': False, + 'position': {'x': -645.8, 'y': -508.7, 'z': -943.0}}, + 33058: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Outside PDA', + 'need_laser_cutter': False, + 'position': {'x': -630.5, 'y': -511.1, 'z': -936.1}}, + 33059: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Observatory PDA', + 'need_laser_cutter': False, + 'position': {'x': -647.7, 'y': -502.6, 'z': -935.8}}, + 33060: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Lab PDA', + 'need_laser_cutter': False, + 'position': {'x': -639.6, 'y': -505.9, 'z': -946.6}}, + 33061: {'can_slip_through': False, + 'name': 'Floating Island - Lake PDA', + 'need_laser_cutter': False, + 'position': {'x': -707.2, 'y': 0.5, 'z': -1096.7}}, + 33062: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - Databox', + 'need_laser_cutter': False, + 'position': {'x': -765.7, 'y': 17.6, 'z': -1116.4}}, + 33063: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - Room PDA', + 'need_laser_cutter': False, + 'position': {'x': -754.9, 'y': 14.6, 'z': -1108.9}}, + 33064: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - Green Wall PDA', + 'need_laser_cutter': False, + 'position': {'x': -765.3, 'y': 14.1, 'z': -1115.0}}, + 33065: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - Corridor PDA', + 'need_laser_cutter': False, + 'position': {'x': -758.6, 'y': 14.1, 'z': -1111.3}}, + 33066: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - North Observatory PDA', + 'need_laser_cutter': False, + 'position': {'x': -805.4, 'y': 76.9, 'z': -1055.7}}, + 33067: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - South Observatory PDA', + 'need_laser_cutter': False, + 'position': {'x': -715.9, 'y': 75.4, 'z': -1168.8}}, + 33068: {'can_slip_through': False, + 'name': 'Jellyshroom Cave - PDA', + 'need_laser_cutter': False, + 'position': {'x': -540.5, 'y': -250.8, 'z': -83.4}}, + 33069: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Bedroom Databox', + 'need_laser_cutter': False, + 'position': {'x': 110.6, 'y': -264.9, 'z': -369.0}}, + 33070: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Detached PDA', + 'need_laser_cutter': False, + 'position': {'x': 80.6, 'y': -268.6, 'z': -358.3}}, + 33071: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Office PDA', + 'need_laser_cutter': False, + 'position': {'x': 78.2, 'y': -265.0, 'z': -373.4}}, + 33072: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Locker PDA', + 'need_laser_cutter': False, + 'position': {'x': 85.1, 'y': -264.1, 'z': -372.8}}, + 33073: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Bedroom PDA', + 'need_laser_cutter': False, + 'position': {'x': 112.3, 'y': -264.9, 'z': -369.3}}, + 33074: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Observatory PDA', + 'need_laser_cutter': False, + 'position': {'x': 95.5, 'y': -258.9, 'z': -366.5}}, + 33075: {'can_slip_through': False, + 'name': 'Lifepod 2 - Databox', + 'need_laser_cutter': False, + 'position': {'x': -483.6, 'y': -504.7, 'z': 1326.6}}, + 33076: {'can_slip_through': False, + 'name': 'Lifepod 2 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -481.4, 'y': -503.6, 'z': 1324.1}}, + 33077: {'can_slip_through': False, + 'name': 'Lifepod 3 - Databox', + 'need_laser_cutter': False, + 'position': {'x': -34.2, 'y': -22.4, 'z': 410.5}}, + 33078: {'can_slip_through': False, + 'name': 'Lifepod 3 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -33.8, 'y': -22.5, 'z': 408.8}}, + 33079: {'can_slip_through': False, + 'name': 'Lifepod 4 - Databox', + 'need_laser_cutter': False, + 'position': {'x': 712.4, 'y': -3.4, 'z': 160.8}}, + 33080: {'can_slip_through': False, + 'name': 'Lifepod 4 - PDA', + 'need_laser_cutter': False, + 'position': {'x': 712.0, 'y': -3.5, 'z': 161.5}}, + 33081: {'can_slip_through': False, + 'name': 'Lifepod 6 - Databox', + 'need_laser_cutter': False, + 'position': {'x': 358.7, 'y': -117.1, 'z': 306.8}}, + 33082: {'can_slip_through': False, + 'name': 'Lifepod 6 - Inside PDA', + 'need_laser_cutter': False, + 'position': {'x': 361.8, 'y': -116.2, 'z': 309.5}}, + 33083: {'can_slip_through': False, + 'name': 'Lifepod 6 - Outside PDA', + 'need_laser_cutter': False, + 'position': {'x': 359.9, 'y': -117.0, 'z': 312.1}}, + 33084: {'can_slip_through': False, + 'name': 'Lifepod 7 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -56.0, 'y': -182.0, 'z': -1039.0}}, + 33085: {'can_slip_through': False, + 'name': 'Lifepod 12 - Databox', + 'need_laser_cutter': False, + 'position': {'x': 1119.5, 'y': -271.7, 'z': 561.7}}, + 33086: {'can_slip_through': False, + 'name': 'Lifepod 12 - PDA', + 'need_laser_cutter': False, + 'position': {'x': 1116.1, 'y': -271.3, 'z': 566.9}}, + 33087: {'can_slip_through': False, + 'name': 'Lifepod 13 - Databox', + 'need_laser_cutter': False, + 'position': {'x': -926.4, 'y': -185.2, 'z': 501.8}}, + 33088: {'can_slip_through': False, + 'name': 'Lifepod 13 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -926.8, 'y': -184.4, 'z': 506.6}}, + 33089: {'can_slip_through': False, + 'name': 'Lifepod 17 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -514.5, 'y': -98.1, 'z': -56.5}}, + 33090: {'can_slip_through': False, + 'name': 'Lifepod 19 - Databox', + 'need_laser_cutter': False, + 'position': {'x': -809.8, 'y': -302.2, 'z': -876.9}}, + 33091: {'can_slip_through': False, + 'name': 'Lifepod 19 - Outside PDA', + 'need_laser_cutter': False, + 'position': {'x': -806.1, 'y': -294.1, 'z': -866.0}}, + 33092: {'can_slip_through': False, + 'name': 'Lifepod 19 - Inside PDA', + 'need_laser_cutter': False, + 'position': {'x': -810.5, 'y': -299.4, 'z': -873.1}}, + 33093: {'can_slip_through': False, + 'name': 'Aurora Seamoth Bay - Upgrade Console', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 903.5, 'y': -0.2, 'z': 16.1}}, + 33094: {'can_slip_through': False, + 'name': 'Aurora Drive Room - Upgrade Console', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 872.5, 'y': 2.7, 'z': -0.7}}, + 33095: {'can_slip_through': False, + 'name': 'Aurora Prawn Suit Bay - Upgrade Console', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 991.6, 'y': 3.2, 'z': -31.0}}, + 33096: {'can_slip_through': False, + 'name': 'Aurora - Office PDA', + 'need_laser_cutter': False, + 'position': {'x': 952.1, 'y': 41.2, 'z': 113.9}}, + 33097: {'can_slip_through': False, + 'name': 'Aurora - Corridor PDA', + 'need_laser_cutter': False, + 'position': {'x': 977.2, 'y': 39.1, 'z': 83.0}}, + 33098: {'can_slip_through': False, + 'name': 'Aurora - Cargo Bay PDA', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 954.9, 'y': 11.2, 'z': 3.4}}, + 33099: {'can_slip_through': False, + 'name': 'Aurora - Seamoth Bay PDA', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 907.1, 'y': -1.5, 'z': 15.3}}, + 33100: {'can_slip_through': False, + 'name': 'Aurora - Medkit Locker PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 951.8, 'y': -2.3, 'z': -34.7}}, + 33101: {'can_slip_through': False, + 'name': 'Aurora - Locker PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 952.0, 'y': -3.7, 'z': -23.4}}, + 33102: {'can_slip_through': False, + 'name': 'Aurora - Canteen PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 986.5, 'y': 9.6, 'z': -48.6}}, + 33103: {'can_slip_through': False, + 'name': 'Aurora - Cabin 4 PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 951.3, 'y': 11.2, 'z': -51.0}}, + 33104: {'can_slip_through': False, + 'name': 'Aurora - Cabin 7 PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 967.1, 'y': 10.4, 'z': -47.4}}, + 33105: {'can_slip_through': False, + 'name': 'Aurora - Cabin 1 PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 964.1, 'y': 11.1, 'z': -61.9}}, + 33106: {'can_slip_through': False, + 'name': 'Aurora - Captain PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 971.2, 'y': 10.8, 'z': -70.4}}, + 33107: {'can_slip_through': False, + 'name': 'Aurora - Ring PDA', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 1033.6, 'y': -8.5, 'z': 16.2}}, + 33108: {'can_slip_through': False, + 'name': 'Aurora - Lab PDA', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 1032.5, 'y': -7.8, 'z': 32.4}}, + 33109: {'can_slip_through': False, + 'name': 'Aurora - Office Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': 945.8, 'y': 40.8, 'z': 115.1}}, + 33110: {'can_slip_through': False, + 'name': 'Aurora - Captain Data Terminal', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 974.8, 'y': 10.0, 'z': -77.0}}, + 33111: {'can_slip_through': False, + 'name': 'Aurora - Battery Room Data Terminal', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 1040.8, 'y': -11.4, 'z': -3.4}}, + 33112: {'can_slip_through': False, + 'name': 'Aurora - Lab Data Terminal', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 1029.5, 'y': -8.7, 'z': 35.9}}, + 33113: {'can_slip_through': False, + 'name': "Quarantine Enforcement Platform's - Upper Alien Data " + 'Terminal', + 'need_laser_cutter': False, + 'position': {'x': 432.2, 'y': 3.0, 'z': 1193.2}}, + 33114: {'can_slip_through': False, + 'name': "Quarantine Enforcement Platform's - Mid Alien Data Terminal", + 'need_laser_cutter': False, + 'position': {'x': 474.4, 'y': -4.5, 'z': 1224.4}}, + 33115: {'can_slip_through': False, + 'name': 'Dunes Sanctuary - Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -1224.2, 'y': -400.4, 'z': 1057.9}}, + 33116: {'can_slip_through': False, + 'name': 'Deep Sparse Reef Sanctuary - Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -895.5, 'y': -311.6, 'z': -838.1}}, + 33117: {'can_slip_through': False, + 'name': 'Northern Blood Kelp Zone Sanctuary - Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -642.9, 'y': -563.5, 'z': 1485.5}}, + 33118: {'can_slip_through': False, + 'name': 'Lost River Laboratory Cache - Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -1112.3, 'y': -687.3, 'z': -695.5}}, + 33119: {'can_slip_through': False, + 'name': 'Disease Research Facility - Upper Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -280.2, 'y': -804.3, 'z': 305.1}}, + 33120: {'can_slip_through': False, + 'name': 'Disease Research Facility - Mid Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -267.9, 'y': -806.6, 'z': 250.0}}, + 33121: {'can_slip_through': False, + 'name': 'Disease Research Facility - Lower Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -286.2, 'y': -815.6, 'z': 297.8}}, + 33122: {'can_slip_through': False, + 'name': 'Alien Thermal Plant - Entrance Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -71.3, 'y': -1227.2, 'z': 104.8}}, + 33123: {'can_slip_through': False, + 'name': 'Alien Thermal Plant - Green Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -38.7, 'y': -1226.6, 'z': 111.8}}, + 33124: {'can_slip_through': False, + 'name': 'Alien Thermal Plant - Yellow Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -30.4, 'y': -1220.3, 'z': 111.8}}, + 33125: {'can_slip_through': False, + 'name': "Primary Containment Facility's Antechamber - Alien Data " + 'Terminal', + 'need_laser_cutter': False, + 'position': {'x': 245.8, 'y': -1430.6, 'z': -311.5}}, + 33126: {'can_slip_through': False, + 'name': "Primary Containment Facility's Egg Laboratory - Alien Data " + 'Terminal', + 'need_laser_cutter': False, + 'position': {'x': 165.5, 'y': -1442.4, 'z': -385.8}}, + 33127: {'can_slip_through': False, + 'name': "Primary Containment Facility's Pipe Room - Alien Data " + 'Terminal', + 'need_laser_cutter': False, + 'position': {'x': 348.7, 'y': -1443.5, 'z': -291.9}}, + 33128: {'can_slip_through': False, + 'name': 'Grassy Plateaus West Wreck - Beam PDA', + 'need_laser_cutter': True, + 'position': {'x': -641.8, 'y': -111.3, 'z': -19.7}}, + 33129: {'can_slip_through': False, + 'name': 'Floating Island - Cave Entrance PDA', + 'need_laser_cutter': False, + 'position': {'x': -748.9, 'y': 14.4, 'z': -1179.5}}} + +if False: # turn to True to export for Subnautica mod + payload = {location_id: location_data["position"] for location_id, location_data in location_table.items()} + import json + + with open("locations.json", "w") as f: + json.dump(payload, f) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index 4189aecb19..cae7ba6c0e 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -2,13 +2,36 @@ from Options import Choice class ItemPool(Choice): - """Valuable item pool moves all not progression relevant items to starting inventory and - creates random duplicates of important items in their place.""" + """Valuable item pool leaves all filler items in their vanilla locations and + creates random duplicates of important items into freed spots.""" display_name = "Item Pool" option_standard = 0 option_valuable = 1 +class Goal(Choice): + """Goal to complete. + Launch: Leave the planet. + Free: Disable quarantine. + Infected: Reach maximum infection level. + Drive: Repair the Aurora's Drive Core""" + auto_display_name = True + display_name = "Goal" + option_launch = 0 + option_free = 1 + option_infected = 2 + option_drive = 3 + + def get_event_name(self) -> str: + return { + self.option_launch: "Neptune Launch", + self.option_infected: "Full Infection", + self.option_free: "Disable Quarantine", + self.option_drive: "Repair Aurora Drive" + }[self.value] + + options = { - "item_pool": ItemPool + "item_pool": ItemPool, + "goal": Goal, } diff --git a/worlds/subnautica/Regions.py b/worlds/subnautica/Regions.py deleted file mode 100644 index 1eb0e12f61..0000000000 --- a/worlds/subnautica/Regions.py +++ /dev/null @@ -1,8 +0,0 @@ -def create_regions(world, player: int): - from . import create_region - from .Locations import lookup_name_to_id as location_lookup_name_to_id - - world.regions += [ - create_region(world, player, 'Menu', None, ['Lifepod 5']), - create_region(world, player, 'Planet 4546B', [location for location in location_lookup_name_to_id]) - ] diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 8d5bcae457..131a537f04 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,6 +1,5 @@ -from ..generic.Rules import set_rule -from .Locations import location_table -import logging +from worlds.generic.Rules import set_rule +from .Locations import location_table, LocationDict import math @@ -197,32 +196,32 @@ def get_max_depth(state, player): get_prawn_max_depth(state, player)) -def can_access_location(state, player, loc): - pos_x = loc.get("position").get("x") - pos_y = loc.get("position").get("y") - pos_z = loc.get("position").get("z") - depth = -pos_y # y-up - map_center_dist = math.sqrt(pos_x ** 2 + pos_z ** 2) - aurora_dist = math.sqrt((pos_x - 1038.0) ** 2 + (pos_y - -3.4) ** 2 + (pos_z - -163.1) ** 2) - - need_radiation_suit = aurora_dist < 950 +def can_access_location(state, player: int, loc: LocationDict): need_laser_cutter = loc.get("need_laser_cutter", False) - need_propulsion_cannon = loc.get("need_propulsion_cannon", False) - if need_laser_cutter and not has_laser_cutter(state, player): return False - if need_radiation_suit and not state.has("Radiation Suit", player): + need_propulsion_cannon = loc.get("need_propulsion_cannon", False) + if need_propulsion_cannon and not has_propulsion_cannon(state, player): return False - if need_propulsion_cannon and not has_propulsion_cannon(state, player): + pos = loc["position"] + pos_x = pos["x"] + pos_y = pos["y"] + pos_z = pos["z"] + + aurora_dist = math.sqrt((pos_x - 1038.0) ** 2 + (pos_y - -3.4) ** 2 + (pos_z - -163.1) ** 2) + need_radiation_suit = aurora_dist < 950 + if need_radiation_suit and not state.has("Radiation Suit", player): return False # Seaglide doesn't unlock anything specific, but just allows for faster movement. # Otherwise the game is painfully slow. + map_center_dist = math.sqrt(pos_x ** 2 + pos_z ** 2) if (map_center_dist > 800 or pos_y < -200) and not has_seaglide(state, player): return False + depth = -pos_y # y-up return get_max_depth(state, player) >= depth @@ -230,21 +229,33 @@ def set_location_rule(world, player, loc): set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc)) -def set_rules(world, player): - for loc in location_table: +def set_rules(subnautica_world): + player = subnautica_world.player + world = subnautica_world.world + + for loc in location_table.values(): set_location_rule(world, player, loc) - # Victory location - set_rule(world.get_location("Neptune Launch", player), lambda state: \ - get_max_depth(state, player) >= 1444 and \ - has_mobile_vehicle_bay(state, player) and \ - state.has('Neptune Launch Platform', player) and \ - state.has('Neptune Gantry', player) and \ - state.has('Neptune Boosters', player) and \ - state.has('Neptune Fuel Reserve', player) and \ - state.has('Neptune Cockpit', player) and \ - state.has('Ion Power Cell', player) and \ - state.has('Ion Battery', player) and \ + # Victory locations + set_rule(world.get_location("Neptune Launch", player), lambda state: + get_max_depth(state, player) >= 1444 and + has_mobile_vehicle_bay(state, player) and + state.has("Neptune Launch Platform", player) and + state.has("Neptune Gantry", player) and + state.has("Neptune Boosters", player) and + state.has("Neptune Fuel Reserve", player) and + state.has("Neptune Cockpit", player) and + state.has("Ion Power Cell", player) and + state.has("Ion Battery", player) and has_cyclops_shield(state, player)) - world.completion_condition[player] = lambda state: state.has('Victory', player) + set_rule(world.get_location("Disable Quarantine", player), lambda state: + get_max_depth(state, player) >= 1444) + + set_rule(world.get_location("Full Infection", player), lambda state: + get_max_depth(state, player) >= 900) + + room = world.get_location("Aurora Drive Room - Upgrade Console", player) + set_rule(world.get_location("Repair Aurora Drive", player), lambda state: room.can_reach(state)) + + world.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index ae92331809..f2fa5497cf 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -1,18 +1,16 @@ import logging +from typing import List, Dict, Any + +from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, RegionType +from worlds.AutoWorld import World, WebWorld +from . import Items +from . import Locations +from . import Options +from .Items import item_table +from .Rules import set_rules logger = logging.getLogger("Subnautica") -from .Locations import lookup_name_to_id as locations_lookup_name_to_id -from .Items import item_table, lookup_name_to_item, advancement_item_names -from .Items import lookup_name_to_id as items_lookup_name_to_id - -from .Regions import create_regions -from .Rules import set_rules -from .Options import options - -from BaseClasses import Region, Entrance, Location, MultiWorld, Item, Tutorial, ItemClassification, RegionType -from ..AutoWorld import World, WebWorld - class SubnaticaWeb(WebWorld): tutorials = [Tutorial( @@ -34,34 +32,51 @@ class SubnauticaWorld(World): game: str = "Subnautica" web = SubnaticaWeb() - item_name_to_id = items_lookup_name_to_id - location_name_to_id = locations_lookup_name_to_id - options = options + item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} + location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} + options = Options.options data_version = 2 - required_client_version = (0, 1, 9) + required_client_version = (0, 3, 3) + + prefill_items: List[Item] + + def generate_early(self) -> None: + self.prefill_items = [ + self.create_item("Seaglide Fragment"), + self.create_item("Seaglide Fragment") + ] + + def create_regions(self): + self.world.regions += [ + self.create_region("Menu", None, ["Lifepod 5"]), + self.create_region("Planet 4546B", + Locations.events + [location["name"] for location in Locations.location_table.values()]) + ] + + # refer to Rules.py + set_rules = set_rules def generate_basic(self): # Link regions - self.world.get_entrance('Lifepod 5', self.player).connect(self.world.get_region('Planet 4546B', self.player)) + self.world.get_entrance("Lifepod 5", self.player).connect(self.world.get_region("Planet 4546B", self.player)) # Generate item pool pool = [] neptune_launch_platform = None extras = 0 - valuable = self.world.item_pool[self.player] == "valuable" - for item in item_table: + valuable = self.world.item_pool[self.player] == Options.ItemPool.option_valuable + for item in item_table.values(): for i in range(item["count"]): subnautica_item = self.create_item(item["name"]) if item["name"] == "Neptune Launch Platform": neptune_launch_platform = subnautica_item - elif valuable and not item["progression"]: - self.world.push_precollected(subnautica_item) + elif valuable and ItemClassification.filler == item["classification"]: extras += 1 else: pool.append(subnautica_item) - for item_name in self.world.random.choices(sorted(advancement_item_names - {"Neptune Launch Platform"}), + for item_name in self.world.random.choices(sorted(Items.advancement_item_names - {"Neptune Launch Platform"}), k=extras): item = self.create_item(item_name) item.classification = ItemClassification.filler # as it's an extra, just fast-fill it somewhere @@ -72,39 +87,57 @@ class SubnauticaWorld(World): # Victory item self.world.get_location("Aurora - Captain Data Terminal", self.player).place_locked_item( neptune_launch_platform) - self.world.get_location("Neptune Launch", self.player).place_locked_item( - SubnauticaItem("Victory", ItemClassification.progression, None, player=self.player)) + for event in Locations.events: + self.world.get_location(event, self.player).place_locked_item( + SubnauticaItem(event, ItemClassification.progression, None, player=self.player)) + # make the goal event the victory "item" + self.world.get_location(self.world.goal[self.player].get_event_name(), self.player).item.name = "Victory" - def set_rules(self): - set_rules(self.world, self.player) + def fill_slot_data(self) -> Dict[str, Any]: + goal: Options.Goal = self.world.goal[self.player] + item_pool: Options.ItemPool = self.world.item_pool[self.player] + vanilla_tech: List[str] = [] + if item_pool == Options.ItemPool.option_valuable: + for item in Items.item_table.values(): + if item["classification"] == ItemClassification.filler: + vanilla_tech.append(item["tech_type"]) - def create_regions(self): - create_regions(self.world, self.player) + slot_data: Dict[str, Any] = { + "goal": goal.current_key, + "vanilla_tech": vanilla_tech, + } - def fill_slot_data(self): - slot_data = {} return slot_data def create_item(self, name: str) -> Item: - item = lookup_name_to_item[name] + item_id: int = self.item_name_to_id[name] + return SubnauticaItem(name, - ItemClassification.progression if item["progression"] else ItemClassification.filler, - item["id"], player=self.player) + item_table[item_id]["classification"], + item_id, player=self.player) + def create_region(self, name: str, locations=None, exits=None): + ret = Region(name, RegionType.Generic, name, self.player) + ret.world = self.world + if locations: + for location in locations: + loc_id = self.location_name_to_id.get(location, None) + location = SubnauticaLocation(self.player, location, loc_id, ret) + ret.locations.append(location) + if exits: + for region_exit in exits: + ret.exits.append(Entrance(self.player, region_exit, ret)) + return ret -def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): - ret = Region(name, RegionType.Generic, name, player) - ret.world = world - if locations: - for location in locations: - loc_id = locations_lookup_name_to_id.get(location, 0) - location = SubnauticaLocation(player, location, loc_id, ret) - ret.locations.append(location) - if exits: - for exit in exits: - ret.exits.append(Entrance(player, exit, ret)) + def get_pre_fill_items(self) -> List[Item]: + return self.prefill_items - return ret + def pre_fill(self) -> None: + reachable = self.world.get_reachable_locations(player=self.player) + self.world.random.shuffle(reachable) + items = self.prefill_items.copy() + for item in items: + reachable.pop().place_locked_item(item) class SubnauticaLocation(Location): @@ -112,4 +145,4 @@ class SubnauticaLocation(Location): class SubnauticaItem(Item): - game = "Subnautica" + game: str = "Subnautica" diff --git a/worlds/subnautica/items.json b/worlds/subnautica/items.json deleted file mode 100644 index 2dc4b22575..0000000000 --- a/worlds/subnautica/items.json +++ /dev/null @@ -1,83 +0,0 @@ -[ - { "id": 35000, "count": 1, "progression": false, "tech_type": "Compass", "name": "Compass" }, - { "id": 35001, "count": 1, "progression": true, "tech_type": "PlasteelTank", "name": "Lightweight High Capacity Tank" }, - { "id": 35002, "count": 1, "progression": true, "tech_type": "BaseUpgradeConsole", "name": "Vehicle Upgrade Console" }, - { "id": 35003, "count": 1, "progression": true, "tech_type": "UltraGlideFins", "name": "Ultra Glide Fins" }, - { "id": 35004, "count": 1, "progression": false, "tech_type": "CyclopsSonarModule", "name": "Cyclops Sonar Upgrade" }, - { "id": 35005, "count": 1, "progression": false, "tech_type": "ReinforcedDiveSuit", "name": "Reinforced Dive Suit" }, - { "id": 35006, "count": 1, "progression": false, "tech_type": "CyclopsThermalReactorModule", "name": "Cyclops Thermal Reactor Module" }, - { "id": 35007, "count": 1, "progression": false, "tech_type": "Stillsuit", "name": "Stillsuit" }, - { "id": 35008, "count": 2, "progression": false, "tech_type": "BaseWaterParkFragment", "name": "Alien Containment Fragment" }, - { "id": 35009, "count": 1, "progression": false, "tech_type": "CyclopsDecoy", "name": "Creature Decoy" }, - { "id": 35010, "count": 1, "progression": false, "tech_type": "CyclopsFireSuppressionModule", "name": "Cyclops Fire Suppression System" }, - { "id": 35011, "count": 1, "progression": false, "tech_type": "SwimChargeFins", "name": "Swim Charge Fins" }, - { "id": 35012, "count": 1, "progression": false, "tech_type": "RepulsionCannon", "name": "Repulsion Cannon" }, - { "id": 35013, "count": 1, "progression": false, "tech_type": "CyclopsDecoyModule", "name": "Cyclops Decoy Tube Upgrade" }, - { "id": 35014, "count": 1, "progression": true, "tech_type": "CyclopsShieldModule", "name": "Cyclops Shield Generator" }, - { "id": 35015, "count": 1, "progression": true, "tech_type": "CyclopsHullModule1", "name": "Cyclops Depth Module MK1" }, - { "id": 35016, "count": 1, "progression": false, "tech_type": "CyclopsSeamothRepairModule", "name": "Cyclops Docking Bay Repair Module" }, - { "id": 35017, "count": 2, "progression": false, "tech_type": "BatteryChargerFragment", "name": "Battery Charger fragment" }, - { "id": 35018, "count": 2, "progression": false, "tech_type": "BeaconFragment", "name": "Beacon Fragment" }, - { "id": 35019, "count": 2, "progression": false, "tech_type": "BaseBioReactorFragment", "name": "Bioreactor Fragment" }, - { "id": 35020, "count": 3, "progression": true, "tech_type": "CyclopsBridgeFragment", "name": "Cyclops Bridge Fragment" }, - { "id": 35021, "count": 3, "progression": true, "tech_type": "CyclopsEngineFragment", "name": "Cyclops Engine Fragment" }, - { "id": 35022, "count": 3, "progression": true, "tech_type": "CyclopsHullFragment", "name": "Cyclops Hull Fragment" }, - { "id": 35023, "count": 2, "progression": false, "tech_type": "GravSphereFragment", "name": "Grav Trap Fragment" }, - { "id": 35024, "count": 3, "progression": true, "tech_type": "LaserCutterFragment", "name": "Laser Cutter Fragment" }, - { "id": 35025, "count": 1, "progression": false, "tech_type": "TechlightFragment", "name": "Light Stick Fragment" }, - { "id": 35026, "count": 3, "progression": true, "tech_type": "ConstructorFragment", "name": "Mobile Vehicle Bay Fragment" }, - { "id": 35027, "count": 3, "progression": true, "tech_type": "WorkbenchFragment", "name": "Modification Station Fragment" }, - { "id": 35028, "count": 2, "progression": true, "tech_type": "MoonpoolFragment", "name": "Moonpool Fragment" }, - { "id": 35029, "count": 3, "progression": false, "tech_type": "BaseNuclearReactorFragment", "name": "Nuclear Reactor Fragment" }, - { "id": 35030, "count": 2, "progression": false, "tech_type": "PowerCellChargerFragment", "name": "Power Cell Charger Fragment" }, - { "id": 35031, "count": 1, "progression": false, "tech_type": "PowerTransmitterFragment", "name": "Power Transmitter Fragment" }, - { "id": 35032, "count": 4, "progression": true, "tech_type": "ExosuitFragment", "name": "Prawn Suit Fragment" }, - { "id": 35033, "count": 2, "progression": false, "tech_type": "ExosuitDrillArmFragment", "name": "Prawn Suit Drill Arm Fragment" }, - { "id": 35034, "count": 2, "progression": false, "tech_type": "ExosuitGrapplingArmFragment", "name": "Prawn Suit Grappling Arm Fragment" }, - { "id": 35035, "count": 2, "progression": false, "tech_type": "ExosuitPropulsionArmFragment", "name": "Prawn Suit Propulsion Cannon Fragment" }, - { "id": 35036, "count": 2, "progression": false, "tech_type": "ExosuitTorpedoArmFragment", "name": "Prawn Suit Torpedo Arm Fragment" }, - { "id": 35037, "count": 3, "progression": false, "tech_type": "BaseMapRoomFragment", "name": "Scanner Room Fragment" }, - { "id": 35038, "count": 5, "progression": true, "tech_type": "SeamothFragment", "name": "Seamoth Fragment" }, - { "id": 35039, "count": 2, "progression": false, "tech_type": "StasisRifleFragment", "name": "Stasis Rifle Fragment" }, - { "id": 35040, "count": 2, "progression": false, "tech_type": "ThermalPlantFragment", "name": "Thermal Plant Fragment" }, - { "id": 35041, "count": 4, "progression": true, "tech_type": "SeaglideFragment", "name": "Seaglide Fragment" }, - { "id": 35042, "count": 1, "progression": true, "tech_type": "RadiationSuit", "name": "Radiation Suit" }, - { "id": 35043, "count": 2, "progression": true, "tech_type": "PropulsionCannonFragment", "name": "Propulsion Cannon Fragment" }, - { "id": 35044, "count": 1, "progression": true, "tech_type": "RocketBase", "name": "Neptune Launch Platform" }, - { "id": 35045, "count": 1, "progression": true, "tech_type": "PrecursorIonPowerCell", "name": "Ion Power Cell" }, - { "id": 35046, "count": 2, "progression": false, "tech_type": "FarmingTrayFragment", "name": "Exterior Growbed Fragment" }, - { "id": 35047, "count": 1, "progression": false, "tech_type": "PictureFrameFragment", "name": "Picture Frame" }, - { "id": 35048, "count": 2, "progression": false, "tech_type": "BenchFragment", "name": "Bench Fragment" }, - { "id": 35049, "count": 1, "progression": false, "tech_type": "PlanterPotFragment", "name": "Basic Plant Pot" }, - { "id": 35050, "count": 1, "progression": false, "tech_type": "PlanterBoxFragment", "name": "Interior Growbed" }, - { "id": 35051, "count": 1, "progression": false, "tech_type": "PlanterShelfFragment", "name": "Plant Shelf" }, - { "id": 35052, "count": 2, "progression": false, "tech_type": "BaseObservatoryFragment", "name": "Observatory Fragment" }, - { "id": 35053, "count": 2, "progression": false, "tech_type": "BaseRoomFragment", "name": "Multipurpose Room Fragment" }, - { "id": 35054, "count": 2, "progression": false, "tech_type": "BaseBulkheadFragment", "name": "Bulkhead Fragment" }, - { "id": 35055, "count": 1, "progression": false, "tech_type": "Spotlight", "name": "Spotlight" }, - { "id": 35056, "count": 2, "progression": false, "tech_type": "StarshipDesk", "name": "Desk" }, - { "id": 35057, "count": 1, "progression": false, "tech_type": "StarshipChair", "name": "Swivel Chair" }, - { "id": 35058, "count": 1, "progression": false, "tech_type": "StarshipChair2", "name": "Office Chair" }, - { "id": 35059, "count": 1, "progression": false, "tech_type": "StarshipChair3", "name": "Command Chair" }, - { "id": 35060, "count": 2, "progression": false, "tech_type": "LabCounter", "name": "Counter" }, - { "id": 35061, "count": 1, "progression": false, "tech_type": "NarrowBed", "name": "Single Bed" }, - { "id": 35062, "count": 1, "progression": false, "tech_type": "Bed1", "name": "Basic Double Bed" }, - { "id": 35063, "count": 1, "progression": false, "tech_type": "Bed2", "name": "Quilted Double Bed" }, - { "id": 35064, "count": 2, "progression": false, "tech_type": "CoffeeVendingMachine", "name": "Coffee Vending Machine" }, - { "id": 35065, "count": 2, "progression": false, "tech_type": "Trashcans", "name": "Trash Can" }, - { "id": 35066, "count": 1, "progression": false, "tech_type": "Techlight", "name": "Floodlight" }, - { "id": 35067, "count": 1, "progression": false, "tech_type": "BarTable", "name": "Bar Table" }, - { "id": 35068, "count": 1, "progression": false, "tech_type": "VendingMachine", "name": "Vending Machine" }, - { "id": 35069, "count": 1, "progression": false, "tech_type": "SingleWallShelf", "name": "Single Wall Shelf" }, - { "id": 35070, "count": 1, "progression": false, "tech_type": "WallShelves", "name": "Wall Shelves" }, - { "id": 35071, "count": 1, "progression": false, "tech_type": "PlanterPot2", "name": "Round Plant Pot" }, - { "id": 35072, "count": 1, "progression": false, "tech_type": "PlanterPot3", "name": "Chic Plant Pot" }, - { "id": 35073, "count": 1, "progression": false, "tech_type": "LabTrashcan", "name": "Nuclear Waste Disposal" }, - { "id": 35074, "count": 1, "progression": false, "tech_type": "BasePlanter", "name": "Wall Planter" }, - { "id": 35075, "count": 1, "progression": true, "tech_type": "PrecursorIonBattery", "name": "Ion Battery" }, - { "id": 35076, "count": 1, "progression": true, "tech_type": "RocketBaseLadder", "name": "Neptune Gantry" }, - { "id": 35077, "count": 1, "progression": true, "tech_type": "RocketStage1", "name": "Neptune Boosters" }, - { "id": 35078, "count": 1, "progression": true, "tech_type": "RocketStage2", "name": "Neptune Fuel Reserve" }, - { "id": 35079, "count": 1, "progression": true, "tech_type": "RocketStage3", "name": "Neptune Cockpit" }, - { "id": 35080, "count": 1, "progression": false, "tech_type": "BaseFiltrationMachine", "name": "Water Filtration Machine" } -] diff --git a/worlds/subnautica/locations.json b/worlds/subnautica/locations.json deleted file mode 100644 index f39e453e67..0000000000 --- a/worlds/subnautica/locations.json +++ /dev/null @@ -1,521 +0,0 @@ -[ - { "id": 33000, "position": { "x": -1234.3, "y": -349.7, "z": -396.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Blood Kelp Trench Wreck - Outside Databox" }, - - { "id": 33001, "position": { "x": -1208.0, "y": -349.6, "z": -383.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Blood Kelp Trench Wreck - Inside Databox" }, - - { "id": 33002, "position": { "x": -1210.6, "y": -340.7, "z": -393.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Blood Kelp Trench Wreck - PDA" }, - - { "id": 33003, "position": { "x": 903.8, "y": -220.3, "z": 590.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Bulb Zone West Wreck - Outside Databox" }, - - { "id": 33004, "position": { "x": 910.9, "y": -201.8, "z": 623.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Bulb Zone West Wreck - Under Databox" }, - - { "id": 33005, "position": { "x": 914.9, "y": -202.1, "z": 611.8}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Bulb Zone West Wreck - Inside Databox" }, - - { "id": 33006, "position": { "x": 912.6, "y": -202.0, "z": 609.5}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Bulb Zone West Wreck - PDA" }, - - { "id": 33007, "position": { "x": 1327.1, "y": -234.9, "z": 575.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Bulb Zone East Wreck - Databox" }, - - { "id": 33008, "position": { "x": -1407.7, "y": -344.2, "z": 721.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes North Wreck - Outside Databox" }, - - { "id": 33009, "position": { "x": -1393.9, "y": -329.7, "z": 733.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes North Wreck - Office Databox" }, - - { "id": 33010, "position": { "x": -1396.3, "y": -330.8, "z": 730.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes North Wreck - PDA" }, - - { "id": 33011, "position": { "x": -1409.8, "y": -332.4, "z": 706.9}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Dunes North Wreck - Cargo Databox" }, - - { "id": 33012, "position": { "x": -1626.2, "y": -357.5, "z": 99.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes West Wreck - Databox" }, - - { "id": 33013, "position": { "x": -1196.3, "y": -223.0, "z": 12.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes East Wreck - Outside Databox" }, - - { "id": 33014, "position": { "x": -1206.4, "y": -225.6, "z": 4.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes East Wreck - Inside Databox" }, - - { "id": 33015, "position": { "x": -269.7, "y": -262.8, "z": -764.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef North Wreck - Outside Databox" }, - - { "id": 33016, "position": { "x": -285.8, "y": -240.2, "z": -786.5}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Grand Reef North Wreck - Elevator Databox" }, - - { "id": 33017, "position": { "x": -285.2, "y": -262.4, "z": -788.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef North Wreck - Bottom Databox" }, - - { "id": 33018, "position": { "x": -272.5, "y": -254.7, "z": -788.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef North Wreck - Hangar PDA" }, - - { "id": 33019, "position": { "x": -850.9, "y": -473.2, "z": -1414.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef South Wreck - Trench Databox" }, - - { "id": 33020, "position": { "x": -889.4, "y": -433.8, "z": -1424.8}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Grand Reef South Wreck - Comms Databox" }, - - { "id": 33021, "position": { "x": -862.4, "y": -437.5, "z": -1444.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef South Wreck - Outside Databox" }, - - { "id": 33022, "position": { "x": -887.9, "y": -446.0, "z": -1422.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef South Wreck - PDA" }, - - { "id": 33023, "position": { "x": -23.3, "y": -105.8, "z": -604.2}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grassy Plateaus South Wreck - Databox" }, - - { "id": 33024, "position": { "x": -27.3, "y": -106.8, "z": -607.2}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grassy Plateaus South Wreck - PDA" }, - - { "id": 33025, "position": { "x": 313.9, "y": -91.8, "z": 432.6}, - "need_laser_cutter": true, "can_slip_through": true, - "name": "Grassy Plateaus East Wreck - Breach Databox" }, - - { "id": 33026, "position": { "x": 319.4, "y": -104.3, "z": 441.5}, - "need_laser_cutter": true, "can_slip_through": true, - "name": "Grassy Plateaus East Wreck - Hangar Databox" }, - - { "id": 33027, "position": { "x": -632.3, "y": -75.0, "z": -8.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grassy Plateaus West Wreck - Locker PDA" }, - - { "id": 33028, "position": { "x": -664.4, "y": -97.8, "z": -8.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grassy Plateaus West Wreck - Data Terminal" }, - - { "id": 33029, "position": { "x": -421.4, "y": -107.8, "z": -266.5}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Grassy Plateaus West Wreck - Databox" }, - - { "id": 33030, "position": { "x": -44.0, "y": -29.1, "z": -403.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Safe Shallows Wreck - PDA" }, - - { "id": 33031, "position": { "x": -317.1, "y": -79.0, "z": 248.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Kelp Forest Wreck - Databox" }, - - { "id": 33032, "position": { "x": 63.2, "y": -38.5, "z": 382.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Kelp Forest Wreck - PDA" }, - - { "id": 33033, "position": { "x": 740.3, "y": -389.2, "z": 1179.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Mountains West Wreck - Outside Databox" }, - - { "id": 33034, "position": { "x": 703.7, "y": -365.9, "z": 1199.3}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Mountains West Wreck - Data Terminal" }, - - { "id": 33035, "position": { "x": 698.2, "y": -350.8, "z": 1186.9}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Mountains West Wreck - Hangar Databox" }, - - { "id": 33036, "position": { "x": 676.3, "y": -343.6, "z": 1204.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Mountains West Wreck - Office Databox" }, - - { "id": 33037, "position": { "x": 1068.5, "y": -283.4, "z": 1345.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Mountains East Wreck - Comms Databox" }, - - { "id": 33038, "position": { "x": 1075.7, "y": -288.9, "z": 1321.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Mountains East Wreck - Outside Databox" }, - - { "id": 33039, "position": { "x": -655.1, "y": -109.6, "z": 791.0}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Northwestern Mushroom Forest Wreck - Cargo Databox" }, - - { "id": 33040, "position": { "x": -663.4, "y": -111.9, "z": 777.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Northwestern Mushroom Forest Wreck - Office Databox" }, - - { "id": 33041, "position": { "x": -662.2, "y": -113.4, "z": 777.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Northwestern Mushroom Forest Wreck - PDA" }, - - { "id": 33042, "position": { "x": -1161.1, "y": -191.7, "z": -758.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Sea Treader's Path Wreck - Outside Databox" }, - - { "id": 33043, "position": { "x": -1129.5, "y": -155.2, "z": -729.3}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Sea Treader's Path Wreck - Hangar Databox" }, - - { "id": 33044, "position": { "x": -1115.9, "y": -175.3, "z": -724.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Sea Treader's Path Wreck - Lobby Databox" }, - - { "id": 33045, "position": { "x": -1136.8, "y": -157.0, "z": -734.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Sea Treader's Path Wreck - PDA" }, - - { "id": 33046, "position": { "x": -789.8, "y": -216.1, "z": -711.0}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Sparse Reef Wreck - Locker Databox" }, - - { "id": 33047, "position": { "x": -810.7, "y": -209.3, "z": -685.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Sparse Reef Wreck - Outside Databox" }, - - { "id": 33048, "position": { "x": -795.5, "y": -204.1, "z": -774.7}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Sparse Reef Wreck - Lab Databox" }, - - { "id": 33049, "position": { "x": -170.8, "y": -187.6, "z": 880.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Underwater Islands Wreck - Outside Databox" }, - - { "id": 33050, "position": { "x": -138.4, "y": -193.6, "z": 888.7}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Underwater Islands Wreck - Hangar Databox" }, - - { "id": 33051, "position": { "x": -130.7, "y": -193.2, "z": 883.3}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Underwater Islands Wreck - Data Terminal" }, - - { "id": 33052, "position": { "x": -137.8, "y": -193.4, "z": 879.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Underwater Islands Wreck - Cable Databox" }, - - { "id": 33053, "position": { "x": -124.4, "y": -200.7, "z": 853.0}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Underwater Islands Wreck - Pipes Databox 1" }, - - { "id": 33054, "position": { "x": -126.8, "y": -201.1, "z": 852.1}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Underwater Islands Wreck - Pipes Databox 2" }, - - { "id": 33055, "position": { "x": -643.8, "y": -509.9, "z": -941.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Bedroom Databox" }, - - { "id": 33056, "position": { "x": -635.1, "y": -502.7, "z": -951.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Observatory Databox" }, - - { "id": 33057, "position": { "x": -645.8, "y": -508.7, "z": -943.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Bedroom PDA" }, - - { "id": 33058, "position": { "x": -630.5, "y": -511.1, "z": -936.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Outside PDA" }, - - { "id": 33059, "position": { "x": -647.7, "y": -502.6, "z": -935.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Observatory PDA" }, - - { "id": 33060, "position": { "x": -639.6, "y": -505.9, "z": -946.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Lab PDA" }, - - { "id": 33061, "position": { "x": -707.2, "y": 0.5, "z": -1096.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Floating Island - Lake PDA" }, - - { "id": 33062, "position": { "x": -765.7, "y": 17.6, "z": -1116.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - Databox" }, - - { "id": 33063, "position": { "x": -754.9, "y": 14.6, "z": -1108.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - Room PDA" }, - - { "id": 33064, "position": { "x": -765.3, "y": 14.1, "z": -1115.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - Green Wall PDA" }, - - { "id": 33065, "position": { "x": -758.6, "y": 14.1, "z": -1111.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - Corridor PDA" }, - - { "id": 33066, "position": { "x": -805.4, "y": 76.9, "z": -1055.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - North Observatory PDA" }, - - { "id": 33067, "position": { "x": -715.9, "y": 75.4, "z": -1168.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - South Observatory PDA" }, - - { "id": 33068, "position": { "x": -540.5, "y": -250.8, "z": -83.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Jellyshroom Cave - PDA" }, - - { "id": 33069, "position": { "x": 110.6, "y": -264.9, "z": -369.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Bedroom Databox" }, - - { "id": 33070, "position": { "x": 80.6, "y": -268.6, "z": -358.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Detached PDA" }, - - { "id": 33071, "position": { "x": 78.2, "y": -265.0, "z": -373.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Office PDA" }, - - { "id": 33072, "position": { "x": 85.1, "y": -264.1, "z": -372.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Locker PDA" }, - - { "id": 33073, "position": { "x": 112.3, "y": -264.9, "z": -369.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Bedroom PDA" }, - - { "id": 33074, "position": { "x": 95.5, "y": -258.9, "z": -366.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Observatory PDA" }, - - { "id": 33075, "position": { "x": -483.6, "y": -504.7, "z": 1326.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 2 - Databox" }, - - { "id": 33076, "position": { "x": -481.4, "y": -503.6, "z": 1324.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 2 - PDA" }, - - { "id": 33077, "position": { "x": -34.2, "y": -22.4, "z": 410.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 3 - Databox" }, - - { "id": 33078, "position": { "x": -33.8, "y": -22.5, "z": 408.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 3 - PDA" }, - - { "id": 33079, "position": { "x": 712.4, "y": -3.4, "z": 160.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 4 - Databox" }, - - { "id": 33080, "position": { "x": 712.0, "y": -3.5, "z": 161.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 4 - PDA" }, - - { "id": 33081, "position": { "x": 358.7, "y": -117.1, "z": 306.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 6 - Databox" }, - - { "id": 33082, "position": { "x": 361.8, "y": -116.2, "z": 309.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 6 - Inside PDA" }, - - { "id": 33083, "position": { "x": 359.9, "y": -117.0, "z": 312.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 6 - Outside PDA" }, - - { "id": 33084, "position": { "x": -56.0, "y": -182.0, "z": -1039.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 7 - PDA" }, - - { "id": 33085, "position": { "x": 1119.5, "y": -271.7, "z": 561.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 12 - Databox" }, - - { "id": 33086, "position": { "x": 1116.1, "y": -271.3, "z": 566.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 12 - PDA" }, - - { "id": 33087, "position": { "x": -926.4, "y": -185.2, "z": 501.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 13 - Databox" }, - - { "id": 33088, "position": { "x": -926.8, "y": -184.4, "z": 506.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 13 - PDA" }, - - { "id": 33089, "position": { "x": -514.5, "y": -98.1, "z": -56.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 17 - PDA" }, - - { "id": 33090, "position": { "x": -809.8, "y": -302.2, "z": -876.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 19 - Databox" }, - - { "id": 33091, "position": { "x": -806.1, "y": -294.1, "z": -866.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 19 - Outside PDA" }, - - { "id": 33092, "position": { "x": -810.5, "y": -299.4, "z": -873.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 19 - Inside PDA" }, - - { "id": 33093, "position": { "x": 903.5, "y": -0.2, "z": 16.1}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora Seamoth Bay - Upgrade Console" }, - - { "id": 33094, "position": { "x": 872.5, "y": 2.7, "z": -0.7}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora Drive Room - Upgrade Console" }, - - { "id": 33095, "position": { "x": 991.6, "y": 3.2, "z": -31.0}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora Prawn Suit Bay - Upgrade Console" }, - - { "id": 33096, "position": { "x": 952.1, "y": 41.2, "z": 113.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Aurora - Office PDA" }, - - { "id": 33097, "position": { "x": 977.2, "y": 39.1, "z": 83.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Aurora - Corridor PDA" }, - - { "id": 33098, "position": { "x": 954.9, "y": 11.2, "z": 3.4}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Cargo Bay PDA" }, - - { "id": 33099, "position": { "x": 907.1, "y": -1.5, "z": 15.3}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Seamoth Bay PDA" }, - - { "id": 33100, "position": { "x": 951.8, "y": -2.3, "z": -34.7}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Medkit Locker PDA" }, - - { "id": 33101, "position": { "x": 952.0, "y": -3.7, "z": -23.4}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Locker PDA" }, - - { "id": 33102, "position": { "x": 986.5, "y": 9.6, "z": -48.6}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Canteen PDA" }, - - { "id": 33103, "position": { "x": 951.3, "y": 11.2, "z": -51.0}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Cabin 4 PDA" }, - - { "id": 33104, "position": { "x": 967.1, "y": 10.4, "z": -47.4}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Cabin 7 PDA" }, - - { "id": 33105, "position": { "x": 964.1, "y": 11.1, "z": -61.9}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Cabin 1 PDA" }, - - { "id": 33106, "position": { "x": 971.2, "y": 10.8, "z": -70.4}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Captain PDA" }, - - { "id": 33107, "position": { "x": 1033.6, "y": -8.5, "z": 16.2}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Ring PDA" }, - - { "id": 33108, "position": { "x": 1032.5, "y": -7.8, "z": 32.4}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Lab PDA" }, - - { "id": 33109, "position": { "x": 945.8, "y": 40.8, "z": 115.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Aurora - Office Data Terminal" }, - - { "id": 33110, "position": { "x": 974.8, "y": 10.0, "z": -77.0}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Captain Data Terminal" }, - - { "id": 33111, "position": { "x": 1040.8, "y": -11.4, "z": -3.4}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Battery Room Data Terminal" }, - - { "id": 33112, "position": { "x": 1029.5, "y": -8.7, "z": 35.9}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Lab Data Terminal" }, - - { "id": 33113, "position": { "x": 432.2, "y": 3.0, "z": 1193.2}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Quarantine Enforcement Platform's - Upper Alien Data Terminal" }, - - { "id": 33114, "position": { "x": 474.4, "y": -4.5, "z": 1224.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Quarantine Enforcement Platform's - Mid Alien Data Terminal" }, - - { "id": 33115, "position": { "x": -1224.2, "y": -400.4, "z": 1057.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes Sanctuary - Alien Data Terminal" }, - - { "id": 33116, "position": { "x": -895.5, "y": -311.6, "z": -838.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Deep Sparse Reef Sanctuary - Alien Data Terminal" }, - - { "id": 33117, "position": { "x": -642.9, "y": -563.5, "z": 1485.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Northern Blood Kelp Zone Sanctuary - Alien Data Terminal" }, - - { "id": 33118, "position": { "x": -1112.3, "y": -687.3, "z": -695.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lost River Laboratory Cache - Alien Data Terminal" }, - - { "id": 33119, "position": { "x": -280.2, "y": -804.3, "z": 305.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Disease Research Facility - Upper Alien Data Terminal" }, - - { "id": 33120, "position": { "x": -267.9, "y": -806.6, "z": 250.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Disease Research Facility - Mid Alien Data Terminal" }, - - { "id": 33121, "position": { "x": -286.2, "y": -815.6, "z": 297.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Disease Research Facility - Lower Alien Data Terminal" }, - - { "id": 33122, "position": { "x": -71.3, "y": -1227.2, "z": 104.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Alien Thermal Plant - Entrance Alien Data Terminal" }, - - { "id": 33123, "position": { "x": -38.7, "y": -1226.6, "z": 111.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Alien Thermal Plant - Green Alien Data Terminal" }, - - { "id": 33124, "position": { "x": -30.4, "y": -1220.3, "z": 111.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Alien Thermal Plant - Yellow Alien Data Terminal" }, - - { "id": 33125, "position": { "x": 245.8, "y": -1430.6, "z": -311.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Primary Containment Facility's Antechamber - Alien Data Terminal" }, - - { "id": 33126, "position": { "x": 165.5, "y": -1442.4, "z": -385.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Primary Containment Facility's Egg Laboratory - Alien Data Terminal" }, - - { "id": 33127, "position": { "x": 348.7, "y": -1443.5, "z": -291.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Primary Containment Facility's Pipe Room - Alien Data Terminal" }, - - { "id": 33128, "position": { "x": -641.8, "y": -111.3, "z": -19.7}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Grassy Plateaus West Wreck - Beam PDA" }, - - { "id": 33129, "position": { "x": -748.9, "y": 14.4, "z": -1179.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Floating Island - Cave Entrance PDA" } -] From ce789d1e3ed3cb468631afcfc972319076b38a38 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 15 Jul 2022 18:01:07 +0200 Subject: [PATCH 017/138] SoE: texts, energy core, fragments, useful (#777) * fix missing fields in custom prog balancing option * fix typos and pep8 * update and implement pyevermizer 0.41.3 * allow randomizing energy core * add energy core fragments (turn in at Prof. Ruffleberg) * rename some items to avoid confusion * differentiate between progression and useful * remove obsolete 'Bazooka' group * don't add items to the pool that get removed --- worlds/soe/Logic.py | 10 ++++- worlds/soe/Options.py | 31 ++++++++++++++ worlds/soe/__init__.py | 81 ++++++++++++++++++++++++++++--------- worlds/soe/requirements.txt | 28 ++++++------- 4 files changed, 115 insertions(+), 35 deletions(-) diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index f25f2ada1b..d08e6a3e96 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -1,5 +1,6 @@ from BaseClasses import MultiWorld from ..AutoWorld import LogicMixin +from .Options import EnergyCore from typing import Set # TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? @@ -8,9 +9,9 @@ from . import pyevermizer # TODO: resolve/flatten/expand rules to get rid of recursion below where possible # Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] -# Logic.items are all items excluding non-progression items and duplicates +# Logic.items are all items and extra items excluding non-progression items and duplicates item_names: Set[str] = set() -items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items()) +items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items()) if item.name not in item_names and not item_names.add(item.name)] @@ -47,4 +48,9 @@ class SecretOfEvermoreLogic(LogicMixin): """ Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE """ + if progress == pyevermizer.P_ENERGY_CORE: # logic is shared between worlds, so we override in the call + w = world.worlds[player] + if w.energy_core == EnergyCore.option_fragments: + progress = pyevermizer.P_CORE_FRAGMENT + count = w.required_fragments return self._soe_count(progress, world, player, count) >= count diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index 0c24399ddd..4ec0ce2bcc 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -37,6 +37,32 @@ class Difficulty(EvermizerFlags, Choice): flags = ['e', 'n', 'h', 'x'] +class EnergyCore(EvermizerFlags, Choice): + """How to obtain the Energy Core""" + display_name = "Energy Core" + option_vanilla = 0 + option_shuffle = 1 + option_fragments = 2 + default = 1 + flags = ['z', '', 'Z'] + + +class RequiredFragments(Range): + """Required fragment count for Energy Core = Fragments""" + display_name = "Required Fragments" + range_start = 1 + range_end = 99 + default = 10 + + +class AvailableFragments(Range): + """Placed fragment count for Energy Core = Fragments""" + display_name = "Available Fragments" + range_start = 1 + range_end = 99 + default = 11 + + class MoneyModifier(Range): """Money multiplier in %""" display_name = "Money Modifier" @@ -186,10 +212,15 @@ class TrapChanceOHKO(TrapChance): class SoEProgressionBalancing(ProgressionBalancing): default = 30 + __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") + special_range_names = {**ProgressionBalancing.special_range_names, "normal": default} soe_options: typing.Dict[str, type(Option)] = { "difficulty": Difficulty, + "energy_core": EnergyCore, + "required_fragments": RequiredFragments, + "available_fragments": AvailableFragments, "money_modifier": MoneyModifier, "exp_modifier": ExpModifier, "fix_sequence": FixSequence, diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index f00ebbe143..d708d6d7d3 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -16,7 +16,7 @@ except ImportError: from . import pyevermizer # as part of the source tree from . import Logic # load logic mixin -from .Options import soe_options +from .Options import soe_options, EnergyCore, RequiredFragments, AvailableFragments from .Patch import SoEDeltaPatch, get_base_rom_path """ @@ -52,7 +52,6 @@ Item grouping currently supports * Ingredients - Matches all ingredient drops * Alchemy - Matches all alchemy formulas * Weapons - Matches all weapons but Bazooka, Bone Crusher, Neutron Blade -* Bazooka - Matches all bazookas (currently only one) * Traps - Matches all traps """ @@ -63,12 +62,14 @@ _id_offset: typing.Dict[int, int] = { pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399 pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499 # TODO: sniff 64500..64799 - pyevermizer.CHECK_TRAP: _id_base + 900, # npc 64900..64999 + pyevermizer.CHECK_EXTRA: _id_base + 800, # extra items 64800..64899 + pyevermizer.CHECK_TRAP: _id_base + 900, # trap 64900..64999 } # cache native evermizer items and locations _items = pyevermizer.get_items() _traps = pyevermizer.get_traps() +_extras = pyevermizer.get_extra_items() # items that are not placed by default _locations = pyevermizer.get_locations() # fix up texts for AP for _loc in _locations: @@ -104,7 +105,7 @@ def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[i def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]: name_to_id = {} id_to_raw = {} - for item in itertools.chain(_items, _traps): + for item in itertools.chain(_items, _extras, _traps): if item.name in name_to_id: continue ap_id = _id_offset[item.type] + item.index @@ -127,7 +128,6 @@ def _get_item_grouping() -> typing.Dict[str, typing.Set[str]]: groups['Alchemy'] = set(item.name for item in _items if item.type == pyevermizer.CHECK_ALCHEMY) groups['Weapons'] = {'Spider Claw', 'Horn Spear', 'Gladiator Sword', 'Bronze Axe', 'Bronze Spear', 'Crusader Sword', 'Lance (Weapon)', 'Knight Basher', 'Atom Smasher', 'Laser Lance'} - groups['Bazooka'] = {'Bazooka+Shells / Shining Armor / 5k Gold'} groups['Traps'] = {trap.name for trap in _traps} return groups @@ -136,7 +136,8 @@ class SoEWebWorld(WebWorld): theme = 'jungle' tutorials = [Tutorial( "Multiworld Setup Guide", - "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related software.", + "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related" + " software.", "English", "multiworld_en.md", "multiworld/en", @@ -153,9 +154,9 @@ class SoEWorld(World): options = soe_options topology_present = False remote_items = False - data_version = 2 + data_version = 3 web = SoEWebWorld() - required_client_version = (0, 2, 6) + required_client_version = (0, 3, 3) item_name_to_id, item_id_to_raw = _get_item_mapping() location_name_to_id, location_id_to_raw = _get_location_mapping() @@ -165,6 +166,9 @@ class SoEWorld(World): evermizer_seed: int connect_name: str + energy_core: int + available_fragments: int + required_fragments: int _halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name] @@ -172,6 +176,14 @@ class SoEWorld(World): self.connect_name_available_event = threading.Event() super(SoEWorld, self).__init__(*args, **kwargs) + def generate_early(self) -> None: + # store option values that change logic + self.energy_core = self.world.energy_core[self.player].value + self.required_fragments = self.world.required_fragments[self.player].value + if self.required_fragments > self.world.available_fragments[self.player].value: + self.world.available_fragments[self.player].value = self.required_fragments + self.available_fragments = self.world.available_fragments[self.player].value + def create_event(self, event: str) -> Item: return SoEItem(event, ItemClassification.progression, None, self.player) @@ -182,6 +194,8 @@ class SoEWorld(World): classification = ItemClassification.trap elif item.progression: classification = ItemClassification.progression + elif item.useful: + classification = ItemClassification.useful else: classification = ItemClassification.filler @@ -208,9 +222,33 @@ class SoEWorld(World): self.world.get_entrance('New Game', self.player).connect(self.world.get_region('Ingame', self.player)) def create_items(self): - # add items to the pool - items = list(map(lambda item: self.create_item(item), _items)) + # add regular items to the pool + exclusions: typing.List[str] = [] + if self.energy_core != EnergyCore.option_shuffle: + exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below + items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions))) + # remove one pair of wings that will be placed in generate_basic + items.remove(self.create_item("Wings")) + + def is_ingredient(item): + for ingredient in _ingredients: + if _match_item_name(item, ingredient): + return True + return False + + # add energy core fragments to the pool + ingredients = [n for n, item in enumerate(items) if is_ingredient(item)] + if self.energy_core == EnergyCore.option_fragments: + items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core + for _ in range(self.available_fragments - 1): + if len(ingredients) < 1: + break # out of ingredients to replace + r = self.world.random.choice(ingredients) + ingredients.remove(r) + items[r] = self.create_item("Energy Core Fragment") + + # add traps to the pool trap_count = self.world.trap_count[self.player].value trap_chances = {} trap_names = {} @@ -232,13 +270,12 @@ class SoEWorld(World): return self.create_item(trap_names[t]) v -= c - while trap_count > 0: - r = self.world.random.randrange(len(items)) - for ingredient in _ingredients: - if _match_item_name(items[r], ingredient): - items[r] = create_trap() - trap_count -= 1 - break + for _ in range(trap_count): + if len(ingredients) < 1: + break # out of ingredients to replace + r = self.world.random.choice(ingredients) + ingredients.remove(r) + items[r] = create_trap() self.world.itempool += items @@ -271,7 +308,10 @@ class SoEWorld(World): wings_location = self.world.random.choice(self._halls_ne_chest_names) wings_item = self.create_item('Wings') self.world.get_location(wings_location, self.player).place_locked_item(wings_item) - self.world.itempool.remove(wings_item) + # place energy core at vanilla location for vanilla mode + if self.energy_core == EnergyCore.option_vanilla: + energy_core = self.create_item('Energy Core') + self.world.get_location('Energy Core #285', self.player).place_locked_item(energy_core) # generate stuff for later self.evermizer_seed = self.world.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando? @@ -286,9 +326,12 @@ class SoEWorld(World): try: money = self.world.money_modifier[self.player].value exp = self.world.exp_modifier[self.player].value - switches = [] + switches: typing.List[str] = [] if self.world.death_link[self.player].value: switches.append("--death-link") + if self.energy_core == EnergyCore.option_fragments: + switches.extend(('--available-fragments', str(self.available_fragments), + '--required-fragments', str(self.required_fragments))) rom_file = get_base_rom_path() out_base = output_path(output_directory, f'AP_{self.world.seed_name}_P{self.player}_' f'{self.world.get_file_safe_player_name(self.player)}') diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 8dfdc0de9f..7f6a11e490 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,14 +1,14 @@ -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2.tar.gz#egg=pyevermizer; python_version == '3.11' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3.tar.gz#egg=pyevermizer; python_version == '3.11' From 86112351a67772409fc34fb24282344d7040cfcc Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Fri, 15 Jul 2022 20:04:26 +0200 Subject: [PATCH 018/138] sm64ex: Adapt area_connections slotdata Format (#767) --- worlds/sm64ex/Regions.py | 2 ++ worlds/sm64ex/Rules.py | 48 +++++++++++++++++++++++---------------- worlds/sm64ex/__init__.py | 16 ++++++------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index 85c08933f5..d9a314dfff 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -11,6 +11,8 @@ sm64courses = ["Bob-omb Battlefield", "Whomp's Fortress", "Jolly Roger Bay", "Co "Wet-Dry World", "Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride"] +# sm64paintings is list of strings for quick reference for Painting IDs (NOT warp node IDs!) +sm64paintings = ["BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", "WDW", "TTM", "THI Tiny", "THI Huge", "TTC", "RR"] def create_regions(world: MultiWorld, player: int): regSS = Region("Menu", RegionType.Generic, "Castle Area", player, world) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index cad9dd239a..6dc6e84964 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -1,40 +1,50 @@ from ..generic.Rules import add_rule -from .Regions import connect_regions, sm64courses +from .Regions import connect_regions, sm64courses, sm64paintings def set_rules(world, player: int, area_connections): - courseshuffle = list(range(len(sm64courses))) + entrance_ids = list(range(len(sm64paintings))) + destination_courses = list(range(13)) + [12,13,14] # Two instances of Destination Course THI if world.AreaRandomizer[player]: - world.random.shuffle(courseshuffle) - area_connections.update({index: value for index, value in enumerate(courseshuffle)}) + world.random.shuffle(entrance_ids) + temp_assign = dict(zip(entrance_ids,destination_courses)) # Used for Rules only - connect_regions(world, player, "Menu", sm64courses[area_connections[0]]) - connect_regions(world, player, "Menu", sm64courses[area_connections[1]], lambda state: state.has("Power Star", player, 1)) - connect_regions(world, player, "Menu", sm64courses[area_connections[2]], lambda state: state.has("Power Star", player, 3)) - connect_regions(world, player, "Menu", sm64courses[area_connections[3]], lambda state: state.has("Power Star", player, 3)) + # Destination Format: LVL | AREA with LVL = Course ID, 0-indexed, AREA = Area as used in sm64 code + area_connections.update({entrance: (destination_course*10 + 1) for entrance, destination_course in temp_assign.items()}) + for i in range(len(area_connections)): + if (int(area_connections[i]/10) == 12): + # Change first occurence of course 12 (THI) to Area 2 (THI Tiny) + area_connections[i] = 12*10 + 2 + break + + connect_regions(world, player, "Menu", sm64courses[temp_assign[0]]) + connect_regions(world, player, "Menu", sm64courses[temp_assign[1]], lambda state: state.has("Power Star", player, 1)) + connect_regions(world, player, "Menu", sm64courses[temp_assign[2]], lambda state: state.has("Power Star", player, 3)) + connect_regions(world, player, "Menu", sm64courses[temp_assign[3]], lambda state: state.has("Power Star", player, 3)) connect_regions(world, player, "Menu", "Bowser in the Dark World", lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) - connect_regions(world, player, "Menu", sm64courses[area_connections[4]], lambda state: state.has("Power Star", player, 12)) + connect_regions(world, player, "Menu", sm64courses[temp_assign[4]], lambda state: state.has("Power Star", player, 12)) connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1)) - connect_regions(world, player, "Basement", sm64courses[area_connections[5]]) - connect_regions(world, player, "Basement", sm64courses[area_connections[6]]) - connect_regions(world, player, "Basement", sm64courses[area_connections[7]]) - connect_regions(world, player, "Basement", sm64courses[area_connections[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) + connect_regions(world, player, "Basement", sm64courses[temp_assign[5]]) + connect_regions(world, player, "Basement", sm64courses[temp_assign[6]]) + connect_regions(world, player, "Basement", sm64courses[temp_assign[7]]) + connect_regions(world, player, "Basement", sm64courses[temp_assign[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) connect_regions(world, player, "Basement", "Bowser in the Fire Sea", lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) - connect_regions(world, player, "Second Floor", sm64courses[area_connections[9]]) - connect_regions(world, player, "Second Floor", sm64courses[area_connections[10]]) - connect_regions(world, player, "Second Floor", sm64courses[area_connections[11]]) - connect_regions(world, player, "Second Floor", sm64courses[area_connections[12]]) + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[9]]) + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[10]]) + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[11]]) + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[12]]) # THI Tiny + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[13]]) # THI Huge connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value)) - connect_regions(world, player, "Third Floor", sm64courses[area_connections[13]]) - connect_regions(world, player, "Third Floor", sm64courses[area_connections[14]]) + connect_regions(world, player, "Third Floor", sm64courses[temp_assign[14]]) + connect_regions(world, player, "Third Floor", sm64courses[temp_assign[15]]) #Special Rules for some Locations add_rule(world.get_location("Tower of the Wing Cap Switch", player), lambda state: state.has("Power Star", player, 10)) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 64b3874651..bcf1bf2a8a 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -5,7 +5,7 @@ from .Items import item_table, cannon_item_table, SM64Item from .Locations import location_table, SM64Location from .Options import sm64_options from .Rules import set_rules -from .Regions import create_regions, sm64courses +from .Regions import create_regions, sm64courses, sm64paintings from BaseClasses import Item, Tutorial, ItemClassification from ..AutoWorld import World, WebWorld @@ -35,7 +35,7 @@ class SM64World(World): location_name_to_id = location_table data_version = 6 - client_version = 2 + required_client_version = (0,3,0) forced_auto_forfeit = False @@ -54,10 +54,10 @@ class SM64World(World): set_rules(self.world, self.player, self.area_connections) if self.topology_present: # Write area_connections to spoiler log - for painting_id, course_id in self.area_connections.items(): + for painting_id, destination in self.area_connections.items(): self.world.spoiler.set_entrance( - sm64courses[painting_id] + " Painting", - sm64courses[course_id], + sm64paintings[painting_id] + " Painting", + sm64courses[destination // 10], 'entrance', self.player) def create_item(self, name: str) -> Item: @@ -145,8 +145,8 @@ class SM64World(World): def modify_multidata(self, multidata): if self.topology_present: er_hint_data = {} - for painting_id, course_id in self.area_connections.items(): - region = self.world.get_region(sm64courses[course_id], self.player) + for painting_id, destination in self.area_connections.items(): + region = self.world.get_region(sm64courses[destination // 10], self.player) for location in region.locations: - er_hint_data[location.address] = sm64courses[painting_id] + er_hint_data[location.address] = sm64paintings[painting_id] multidata['er_hint_data'][self.player] = er_hint_data From 82850d7f66a1f7a6032d94135ed7d471c775ceb3 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 15 Jul 2022 16:19:36 -0500 Subject: [PATCH 019/138] Ror2: reduce locations to 250 and mark legendary items as useful (#776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * reduce total locations to 250 * minor styling cleanup. mark legendary items as useful * 😡 Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/ror2/Options.py | 6 ++++-- worlds/ror2/__init__.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index d747f3801c..4abca9a33a 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -6,7 +6,7 @@ class TotalLocations(Range): """Number of location checks which are added to the Risk of Rain playthrough.""" display_name = "Total Locations" range_start = 10 - range_end = 500 + range_end = 250 default = 20 @@ -122,6 +122,7 @@ class ItemPoolPresetToggle(DefaultOnToggle): """Will use the item weight presets when set to true, otherwise will use the custom set item pool weights.""" display_name = "Item Weight Presets" + class ItemWeights(Choice): """Preset choices for determining the weights of the item pool.
New is a test for a potential adjustment to the default weights.
@@ -143,7 +144,8 @@ class ItemWeights(Choice): option_even = 7 option_scraps_only = 8 -#define a dictionary for the weights of the generated item pool. + +# define a dictionary for the weights of the generated item pool. ror2_weights: typing.Dict[str, type(Option)] = { "green_scrap": GreenScrap, "red_scrap": RedScrap, diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 06df244b97..6e240ee1c1 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -116,9 +116,9 @@ class RiskOfRainWorld(World): def create_item(self, name: str) -> Item: item_id = item_table[name] item = RiskOfRainItem(name, ItemClassification.filler, item_id, self.player) - if name == 'Dio\'s Best Friend': + if name == "Dio's Best Friend": item.classification = ItemClassification.progression - elif name == 'Equipment': + elif name in {"Equipment", "Legendary Item"}: item.classification = ItemClassification.useful return item From 090c5bcf00a41e632857081118abf60ef0728d5b Mon Sep 17 00:00:00 2001 From: Vale <58179315+Vale-X@users.noreply.github.com> Date: Fri, 15 Jul 2022 22:21:36 +0100 Subject: [PATCH 020/138] RoR2: FinalStageDeath (#766) Added a YAML option for 'FinalStageDeath', a toggle for 'death on the final boss stage counts as a win'. Defaults to on. Co-authored-by: Vale <58179315+DelosIX@users.noreply.github.com> --- worlds/ror2/Options.py | 5 +++++ worlds/ror2/__init__.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index 4abca9a33a..0ab43443e0 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -36,6 +36,10 @@ class AllowLunarItems(DefaultOnToggle): class StartWithRevive(DefaultOnToggle): """Start the game with a `Dio's Best Friend` item.""" display_name = "Start with a Revive" + +class FinalStageDeath(DefaultOnToggle): + """Death on the final boss stage counts as a win.""" + display_name = "Final Stage Death is Win" class GreenScrap(Range): @@ -163,6 +167,7 @@ ror2_options: typing.Dict[str, type(Option)] = { "total_locations": TotalLocations, "total_revivals": TotalRevivals, "start_with_revive": StartWithRevive, + "final_stage_death": FinalStageDeath, "item_pickup_step": ItemPickupStep, "enable_lunar": AllowLunarItems, "item_weights": ItemWeights, diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 6e240ee1c1..1a7060786f 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -110,7 +110,8 @@ class RiskOfRainWorld(World): "seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for i in range(16)), "totalLocations": self.world.total_locations[self.player].value, "totalRevivals": self.world.total_revivals[self.player].value, - "startWithDio": self.world.start_with_revive[self.player].value + "startWithDio": self.world.start_with_revive[self.player].value, + "FinalStageDeath": self.world.final_stage_death[self.player].value } def create_item(self, name: str) -> Item: From a4211d5f11d122ef235924f29fd8a5f494e47b14 Mon Sep 17 00:00:00 2001 From: Rome Reginelli Date: Fri, 15 Jul 2022 14:24:40 -0700 Subject: [PATCH 021/138] Improve Risk of Rain 2 docs (#770) * Improve Risk of Rain 2 docs * RoR2: clarify custom item weight settings * Update worlds/ror2/docs/en_Risk of Rain 2.md Co-authored-by: Hussein Farran --- worlds/ror2/Options.py | 36 +++++++++---------- worlds/ror2/docs/en_Risk of Rain 2.md | 51 +++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index 0ab43443e0..727d01ffaa 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -43,7 +43,7 @@ class FinalStageDeath(DefaultOnToggle): class GreenScrap(Range): - """Weight of Green Scraps in the item pool.""" + """Weight of Green Scraps in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Green Scraps" range_start = 0 range_end = 100 @@ -51,7 +51,7 @@ class GreenScrap(Range): class RedScrap(Range): - """Weight of Red Scraps in the item pool.""" + """Weight of Red Scraps in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Red Scraps" range_start = 0 range_end = 100 @@ -59,7 +59,7 @@ class RedScrap(Range): class YellowScrap(Range): - """Weight of yellow scraps in the item pool.""" + """Weight of yellow scraps in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Yellow Scraps" range_start = 0 range_end = 100 @@ -67,7 +67,7 @@ class YellowScrap(Range): class WhiteScrap(Range): - """Weight of white scraps in the item pool.""" + """Weight of white scraps in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "White Scraps" range_start = 0 range_end = 100 @@ -75,7 +75,7 @@ class WhiteScrap(Range): class CommonItem(Range): - """Weight of common items in the item pool.""" + """Weight of common items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Common Items" range_start = 0 range_end = 100 @@ -83,7 +83,7 @@ class CommonItem(Range): class UncommonItem(Range): - """Weight of uncommon items in the item pool.""" + """Weight of uncommon items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Uncommon Items" range_start = 0 range_end = 100 @@ -91,7 +91,7 @@ class UncommonItem(Range): class LegendaryItem(Range): - """Weight of legendary items in the item pool.""" + """Weight of legendary items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Legendary Items" range_start = 0 range_end = 100 @@ -99,7 +99,7 @@ class LegendaryItem(Range): class BossItem(Range): - """Weight of boss items in the item pool.""" + """Weight of boss items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Boss Items" range_start = 0 range_end = 100 @@ -107,7 +107,7 @@ class BossItem(Range): class LunarItem(Range): - """Weight of lunar items in the item pool.""" + """Weight of lunar items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Lunar Items" range_start = 0 range_end = 100 @@ -115,7 +115,7 @@ class LunarItem(Range): class Equipment(Range): - """Weight of equipment items in the item pool.""" + """Weight of equipment items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Equipment" range_start = 0 range_end = 100 @@ -128,14 +128,14 @@ class ItemPoolPresetToggle(DefaultOnToggle): class ItemWeights(Choice): - """Preset choices for determining the weights of the item pool.
- New is a test for a potential adjustment to the default weights.
- Uncommon puts a large number of uncommon items in the pool.
- Legendary puts a large number of legendary items in the pool.
- Lunartic makes everything a lunar item.
- Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.
- No Scraps removes all scrap items from the item pool.
- Even generates the item pool with every item having an even weight.
+ """Preset choices for determining the weights of the item pool. + New is a test for a potential adjustment to the default weights. + Uncommon puts a large number of uncommon items in the pool. + Legendary puts a large number of legendary items in the pool. + Lunartic makes everything a lunar item. + Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy. + No Scraps removes all scrap items from the item pool. + Even generates the item pool with every item having an even weight. Scraps Only will be only scrap items in the item pool.""" display_name = "Item Weights" option_default = 0 diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md index 92232116cc..a58269a35b 100644 --- a/worlds/ror2/docs/en_Risk of Rain 2.md +++ b/worlds/ror2/docs/en_Risk of Rain 2.md @@ -12,6 +12,32 @@ functionality in which certain chests (made clear via a location check progress multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by other players in other worlds. +## What is the goal of Risk of Rain 2 in Archipelago? + +Just like in the original game, any way to "beat the game or obliterate" counts as a win. By default, if you die while +on a final boss stage, that also counts as a win. (You can turn this off in your player settings.) **You do not need to +complete all the location checks** to win; any item you don't collect is automatically sent out to the multiworld when +you meet your goal. + +If you die before you accomplish your goal, you can start a new run. You will start the run with any items that you +received from other players. Any items that you picked up the "normal" way will be lost. + +Note, you can play Simulacrum mode as part of an Archipelago, but you can't achieve any of the victory conditions in +Simulacrum. So you could, for example, collect most of your items through a Simulacrum run, then finish a normal mode +run while keeping the items you received via the multiworld. + +## Can you play multiplayer? + +Yes! You can have a single multiplayer instance as one world in the multiworld. All the players involved need to have +the Archipelago mod, but only the host needs to configure the Archipelago settings. When someone finds an item for your +world, all the connected players will receive a copy of the item, and the location check bar will increase whenever any +player finds an item in Risk of Rain. + +You cannot have players with different player slots in the same co-op game instance. Only the host's Archipelago +settings apply, so each Risk of Rain 2 player slot in the multiworld needs to be a separate game instance. You could, +for example, have two players trade off hosting and making progress on each other's player slot, but a single co-op +instance can't make progress towards multiple player slots in the multiworld. + ## What Risk of Rain items can appear in other players' worlds? The Risk of Rain items are: @@ -31,13 +57,34 @@ in-game item of that tier will appear in the Risk of Rain player's inventory. If the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and _ the new equipment_ will take it's place. (If you want the old one back, pick it up.) +### How many items are there? + +Since a Risk of Rain 2 run can go on indefinitely, you have to configure how many collectible items (also known as +"checks") the game has for purposes of Archipelago when you set up a multiworld. You can configure anywhere from **10 +to 250** items. The number of items will be randomized between all players, so you may want to adjust the number and +item pickup step based on how many items the other players in the multiworld have. (Around 100 seems to be a good +ballpark if you want to have a similar number of items to most other games.) + +After you have completed the specified number of checks, you won't send anything else to the multiworld. You can +receive up to the specified number of randomized items from the multiworld as the players find them. In either case, +you can continue to collect items as normal in Risk of Rain 2 if you've already found all your location checks. + ## What does another world's item look like in Risk of Rain? When the Risk of Rain player fills up their location check bar then the next spawned item will become an item grant for -another player's world. The item in Risk of Rain will disappear in a poof of smoke and the grant will automatically go -out to the multiworld. +another player's world (or possibly get sent back to yourself). The item in Risk of Rain will disappear in a poof of +smoke and the grant will automatically go out to the multiworld. ## What is the item pickup step? The item pickup step is a YAML setting which allows you to set how many items you need to spawn before the _next_ item that is spawned disappears (in a poof of smoke) and goes out to the multiworld. + +## Is Archipelago compatible with other Risk of Rain 2 mods? + +Mostly, yes. Not every mod will work; in particular, anything that causes items to go directly into your inventory +rather than spawning onto the map will interfere with the way the Archipelago mod works. However, many common mods work +just fine with Archipelago. + +For competitive play, of course, you should only use mods that are agreed-upon by the competitors so that you don't +have an unfair advantage. From 3c6bd555b4d0e535e29ca02043fb7c5d4e581fdc Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 15 Jul 2022 23:52:35 +0200 Subject: [PATCH 022/138] doc: add style guide (#746) * doc: add style guide * doc: style guide for python and markdown * doc: consistent use of periods and explicit double quotes in style guide Co-authored-by: Hussein Farran * doc: better define string style in style guide * doc: add format string literals to style guide * doc: add HTML, CSS and JS to style guide Co-authored-by: Hussein Farran --- docs/style.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++ docs/world api.md | 8 ++++---- 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 docs/style.md diff --git a/docs/style.md b/docs/style.md new file mode 100644 index 0000000000..a9f55caa7c --- /dev/null +++ b/docs/style.md @@ -0,0 +1,49 @@ +# Style Guide + +## Generic + +* This guide can be ignored for data files that are not to be viewed in an editor. +* 120 character per line for all source files. +* Avoid white space errors like trailing spaces. + + +## Python Code + +* We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences. +* 120 characters per line. PyCharm does this automatically, other editors can be configured for it. +* Strings in core code will be `"strings"`. In other words: double quote your strings. +* Strings in worlds should use double quotes as well, but imported code may differ. +* Prefer [format string literals](https://peps.python.org/pep-0498/) over string concatenation, + use single quotes inside them: `f"Like {dct['key']}"` +* Use type annotation where possible. + + +## Markdown + +* We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html). + Read below for differences. +* For existing documents, try to follow its style or ask to completely reformat it. +* 120 characters per line. +* One space between bullet/number and text. +* No lazy numbering. + + +## HTML + +* Indent with 2 spaces for new code. +* kebab-case for ids and classes. + + +## CSS + +* Indent with 2 spaces for new code. +* `{` on the same line as the selector. +* No space between selector and `{`. + + +## JS + +* Indent with 2 spaces. +* Indent `case` inside `switch ` with 2 spaces. +* Use single quotes. +* Semicolons are required after every statement. diff --git a/docs/world api.md b/docs/world api.md index a1138c9e16..4fa81f4aab 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -236,7 +236,7 @@ class MyGameLocation(Location): game: str = "My Game" # override constructor to automatically mark event locations as such - def __init__(self, player: int, name = '', code = None, parent = None): + def __init__(self, player: int, name = "", code = None, parent = None): super(MyGameLocation, self).__init__(player, name, code, parent) self.event = code is None ``` @@ -487,14 +487,14 @@ def create_items(self) -> None: for item in map(self.create_item, mygame_items): if item in exclude: exclude.remove(item) # this is destructive. create unique list above - self.world.itempool.append(self.create_item('nothing')) + self.world.itempool.append(self.create_item("nothing")) else: self.world.itempool.append(item) # itempool and number of locations should match up. # If this is not the case we want to fill the itempool with junk. junk = 0 # calculate this based on player settings - self.world.itempool += [self.create_item('nothing') for _ in range(junk)] + self.world.itempool += [self.create_item("nothing") for _ in range(junk)] ``` #### create_regions @@ -628,7 +628,7 @@ class MyGameLogic(LogicMixin): def _mygame_has_key(self, world: MultiWorld, player: int): # Arguments above are free to choose # it may make sense to use World as argument instead of MultiWorld - return self.has('key', player) # or whatever + return self.has("key", player) # or whatever ``` ```python # __init__.py From a42f7f99fead3676a79169f462ed53f01cf878c9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 16 Jul 2022 01:22:14 +0200 Subject: [PATCH 023/138] Factorio: specify rcon version --- worlds/factorio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt index ce5a83049a..00d9d20af1 100644 --- a/worlds/factorio/requirements.txt +++ b/worlds/factorio/requirements.txt @@ -1 +1 @@ -factorio-rcon-py>=1.2.1 +factorio-rcon-py==1.2.1 From 622af177059265346819f6abfbabb9a3a7342a5d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 16 Jul 2022 11:44:56 +0200 Subject: [PATCH 024/138] MultiServer: make !hint prefer non-local --- MultiServer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MultiServer.py b/MultiServer.py index dd695fc1bb..e8e1cc8d4c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1308,6 +1308,8 @@ class ClientMessageProcessor(CommonCommandProcessor): can_pay = 1000 self.ctx.random.shuffle(not_found_hints) + # By popular vote, make hints prefer non-local placements + not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player)) hints = found_hints while can_pay > 0: From 449bc9330702244ed739a5b3e5142e2e8bdfc65a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 16 Jul 2022 19:36:14 +0200 Subject: [PATCH 025/138] Rogue Legacy: obliterate any outdated remnants before installer adds new files --- inno_setup.iss | 1 + 1 file changed, 1 insertion(+) diff --git a/inno_setup.iss b/inno_setup.iss index 1dee01af18..1005cadad0 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -129,6 +129,7 @@ Type: dirifempty; Name: "{app}" [InstallDelete] Type: files; Name: "{app}\ArchipelagoLttPClient.exe" +Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*" [Registry] From 74b19dc1f55cef020e9a79be26a5ec404384e0cc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 7 Jul 2022 01:38:50 +0200 Subject: [PATCH 026/138] WebHost: cleanup generate and hopefully fix SQL concurrency problems --- WebHostLib/autolauncher.py | 4 +++- WebHostLib/generate.py | 38 ++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 9d7b7f4959..6f978211fb 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -154,8 +154,10 @@ def autogen(config: dict): while 1: time.sleep(0.1) with db_session: + # for update locks the database row(s) during transaction, preventing writes from elsewhere to_start = select( - generation for generation in Generation if generation.state == STATE_QUEUED) + generation for generation in Generation + if generation.state == STATE_QUEUED).for_update() for generation in to_start: launch_generator(generator_pool, generation) except AlreadyRunningException: diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index c33d2648a7..15067e131b 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -4,7 +4,7 @@ import random import json import zipfile from collections import Counter -from typing import Dict, Optional as TypeOptional +from typing import Dict, Optional, Any from Utils import __version__ from flask import request, flash, redirect, url_for, session, render_template @@ -15,7 +15,7 @@ from BaseClasses import seeddigits, get_seed from Generate import handle_name, PlandoSettings import pickle -from .models import * +from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID from WebHostLib import app from .check import get_yaml_data, roll_options from .upload import upload_zip_to_db @@ -30,16 +30,15 @@ def get_meta(options_source: dict) -> dict: } plando_options -= {""} - meta = { + server_options = { "hint_cost": int(options_source.get("hint_cost", 10)), "forfeit_mode": options_source.get("forfeit_mode", "goal"), "remaining_mode": options_source.get("remaining_mode", "disabled"), "collect_mode": options_source.get("collect_mode", "disabled"), "item_cheat": bool(int(options_source.get("item_cheat", 1))), "server_password": options_source.get("server_password", None), - "plando_options": list(plando_options) } - return meta + return {"server_options": server_options, "plando_options": list(plando_options)} @app.route('/generate', methods=['GET', 'POST']) @@ -60,13 +59,13 @@ def generate(race=False): results, gen_options = roll_options(options, meta["plando_options"]) if race: - meta["item_cheat"] = False - meta["remaining_mode"] = "disabled" + meta["server_options"]["item_cheat"] = False + meta["server_options"]["remaining_mode"] = "disabled" if any(type(result) == str for result in results.values()): return render_template("checkResult.html", results=results) elif len(gen_options) > app.config["MAX_ROLL"]: - flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. " + flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " f"If you have a larger group, please generate it yourself and upload it.") elif len(gen_options) >= app.config["JOB_THRESHOLD"]: gen = Generation( @@ -92,23 +91,22 @@ def generate(race=False): return render_template("generate.html", race=race, version=__version__) -def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None): +def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None): if not meta: - meta: Dict[str, object] = {} + meta: Dict[str, Any] = {} + + meta.setdefault("server_options", {}).setdefault("hint_cost", 10) + race = meta.setdefault("race", False) - meta.setdefault("hint_cost", 10) - race = meta.get("race", False) - del (meta["race"]) - plando_options = meta.get("plando", {"bosses", "items", "connections", "texts"}) - del (meta["plando_options"]) try: target = tempfile.TemporaryDirectory() playercount = len(gen_options) seed = get_seed() - random.seed(seed) if race: - random.seed() # reset to time-based random source + random.seed() # use time-based random source + else: + random.seed(seed) seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)) @@ -120,7 +118,8 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No erargs.outputname = seedname erargs.outputpath = target.name erargs.teams = 1 - erargs.plando_options = PlandoSettings.from_set(plando_options) + erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options", + {"bosses", "items", "connections", "texts"})) name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): @@ -136,7 +135,7 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No erargs.name[player] = handle_name(erargs.name[player], player, name_counter) if len(set(erargs.name.values())) != len(erargs.name): raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}") - ERmain(erargs, seed, baked_server_options=meta) + ERmain(erargs, seed, baked_server_options=meta["server_options"]) return upload_to_db(target.name, sid, owner, race) except BaseException as e: @@ -148,7 +147,6 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No meta = json.loads(gen.meta) meta["error"] = (e.__class__.__name__ + ": " + str(e)) gen.meta = json.dumps(meta) - commit() raise From b3ad76668069bf004b5854391406ae84c0faaa47 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 16 Jul 2022 13:47:26 -0400 Subject: [PATCH 027/138] SMZ3: Item link support (#756) * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * - enabled local item dialog boxes for dungeon and keycard items when keysanity is used * - fixed ItemLink support * fixed shops sending checks * Added get_filler_item_name() returning a random junk item Co-authored-by: Fabian Dill --- worlds/smz3/TotalSMZ3/Patch.py | 1 + worlds/smz3/__init__.py | 24 +++++++++++++++++------- worlds/smz3/data/zsm.ips | Bin 1460417 -> 1460427 bytes 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index 54395714ba..d029e58473 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -619,6 +619,7 @@ class Patch: if (self.myWorld.Config.Keysanity): self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass + self.patches.append((Snes(0x40016A), [ 0x01 ])) #// enable local item dialog boxes for dungeon and keycard items def WriteSMKeyCardDoors(self): if (not self.myWorld.Config.Keysanity): diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 7f05e0dfd5..e440eab2c9 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -27,14 +27,18 @@ class SMZ3CollectionState(metaclass=AutoLogicRegister): # for unit tests where MultiWorld is instantiated before worlds if hasattr(parent, "state"): self.smz3state = {player: TotalSMZ3Item.Progression([]) for player in parent.get_game_players("SMZ3")} + for player, group in parent.groups.items(): + if (group["game"] == "SMZ3"): + self.smz3state[player] = TotalSMZ3Item.Progression([]) + if player not in parent.state.smz3state: + parent.state.smz3state[player] = TotalSMZ3Item.Progression([]) else: self.smz3state = {} def copy_mixin(self, ret) -> CollectionState: - ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.world.get_game_players("SMZ3")} + ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.smz3state} return ret - class SMZ3Web(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", @@ -106,6 +110,7 @@ class SMZ3World(World): niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World) junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World) allJunkItems = niceItems + junkItems + self.junkItemsNames = [item.Type.name for item in junkItems] if (self.smz3World.Config.Keysanity): progressionItems = self.progression + self.dungeon + self.keyCardsItems @@ -256,11 +261,11 @@ class SMZ3World(World): base_combined_rom = basepatch.apply(base_combined_rom) patcher = TotalSMZ3Patch(self.smz3World, - [world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World)], + [world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World) and hasattr(world, "smz3World")], self.world.seed_name, self.world.seed, self.local_random, - self.world.world_name_lookup, + {v: k for k, v in self.world.player_name.items()}, next(iter(loc.player for loc in self.world.get_locations() if (loc.item.name == "SilverArrows" and loc.item.player == self.player)))) patches = patcher.Create(self.smz3World.Config) patches.update(self.apply_sm_custom_sprite()) @@ -312,7 +317,7 @@ class SMZ3World(World): return slot_data def collect(self, state: CollectionState, item: Item) -> bool: - state.smz3state[item.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)]) + state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) if item.advancement: state.prog_items[item.name, item.player] += 1 return True # indicate that a logical state change has occured @@ -321,7 +326,7 @@ class SMZ3World(World): def remove(self, state: CollectionState, item: Item) -> bool: name = self.collect_item(state, item, True) if name: - state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)]) + state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) state.prog_items[name, item.player] -= 1 if state.prog_items[name, item.player] < 1: del (state.prog_items[name, item.player]) @@ -330,7 +335,9 @@ class SMZ3World(World): def create_item(self, name: str) -> Item: return SMZ3Item(name, ItemClassification.progression, - TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], player = self.player) + TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], + self.player, + TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[name], self)) def pre_fill(self): from Fill import fill_restrictive @@ -364,6 +371,9 @@ class SMZ3World(World): else: return [] + def get_filler_item_name(self) -> str: + return self.world.random.choice(self.junkItemsNames) + def write_spoiler(self, spoiler_handle: TextIO): self.world.spoiler.unreachables.update(self.unreachable) diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index faf7443a573a49fd7eab0f80588382f043753394..6faeeaa2fb4bb4f2b29203806e3d43e91be85d55 100644 GIT binary patch delta 695 zcmZvXQD{WL|T;kYFxez7IGMG?dU?8>&yH-$vt)7G^Xb?RW6-rF6SCFaT zKI|!ai0whPDEG~WA@&fvi%5c?hY0d1R-l>GoJAeCMG<@WopZkb_ve4UubK_N$cAH2 zAv6M$^wbErp#O5QHSo+M1Jl&V4AXSS4%a;-;RTnr?%`gV*Xkbn#0w=w*xfC@RXFwC zG}aI6vbAP$=DV+^8GlAIM;JfK_lh7N*hZcPd3YN+3bMN=lj;$$+WyZv1M=~W+Xmi3 zo~n<65p3;F3KxBHM%QP5O=V6;V-`75Gjj9PeGC*jkccOMZi$=>z(W|Ps{zOYz)pS+vK*9%bBH}*##fA8 zNy^I0lVXvL+ zqd^eJEg$OuK`i)KTJRaJkD{cD8GO@;GC>4Hu|8YrGbNo8-RP zbjTunO^rT(&!{Rw3x`f`pzq8kKIRhIe8S2omqOSbjIu0z;}iSv+|!4*bT6YR3nY{? z+0P5#dGRXRE`8yX0`|KNJ~@tkqRA(tjE0L$F0Rh+=aaM-)#8(lO^>AA^hgJLk!!p( v#;CaP!XhtSMB$uR&6#L5XZEXgkxokJx)>WLiTZ*W*@ak;eyT6*UYPq0`-&C# delta 782 zcmZvXUr1AN6vuyOE;rYmyK5@Sv|VoxwbXn_k;G_mW$cocBZx+O^Tp^7LG)tb9nsu& z&D{3j#n&uBMAk$3Q$j{Qn7ryhyp}s_#a+Al!YrTSCSf>TgG=74R4E&Ds|lfX^b>!pkui}(90UK^P%-MAn;H3rGLAoAPB$(C4j zDJg}fN@a@^`FHTiOCciu^=MI}@@W Date: Sat, 16 Jul 2022 16:45:40 +0200 Subject: [PATCH 028/138] Subnautica: add creature scans --- worlds/subnautica/Creatures.py | 82 ++++++++++++++++++++++++++++++++ worlds/subnautica/Items.py | 2 +- worlds/subnautica/Options.py | 11 ++++- worlds/subnautica/Rules.py | 86 ++++++++++++++++++++++------------ worlds/subnautica/__init__.py | 19 ++++++-- 5 files changed, 164 insertions(+), 36 deletions(-) create mode 100644 worlds/subnautica/Creatures.py diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py new file mode 100644 index 0000000000..56e2a7efa1 --- /dev/null +++ b/worlds/subnautica/Creatures.py @@ -0,0 +1,82 @@ +from typing import Dict, Set, List + +# EN Locale Creature Name to rough depth in meters found at +all_creatures: Dict[str, int] = { + "Gasopod": 0, + "Bladderfish": 0, + "Ancient Floater": 0, + "Skyray": 0, + "Garryfish": 0, + "Peeper": 0, + "Shuttlebug": 0, + "Rabbit Ray": 0, + "Stalker": 0, + "Floater": 0, + "Holefish": 0, + "Cave Crawler": 0, + "Hoopfish": 0, + "Crashfish": 0, + "Hoverfish": 0, + "Spadefish": 0, + "Reefback Leviathan": 0, + "Reaper Leviathan": 0, + "Warper": 0, + "Boomerang": 0, + "Biter": 200, + "Sand Shark": 200, + "Bleeder": 200, + "Crabsnake": 300, + "Jellyray": 300, + "Oculus": 300, + "Mesmer": 300, + "Eyeye": 300, + "Reginald": 400, + "Sea Treader Leviathan": 400, + "Crabsquid": 400, + "Ampeel": 400, + "Boneshark": 400, + "Rockgrub": 400, + "Ghost Leviathan": 500, + "Ghost Leviathan Juvenile": 500, + "Spinefish": 600, + "Blighter": 600, + "Blood Crawler": 600, + "Ghostray": 1000, + "Amoeboid": 1000, + "River Prowler": 1000, + "Red Eyeye": 1300, + "Magmarang": 1300, + "Crimson Ray": 1300, + "Lava Larva": 1300, + "Lava Lizard": 1300, + "Sea Dragon Leviathan": 1300, + "Sea Emperor Leviathan": 1700, + "Sea Emperor Juvenile": 1700, + + # "Cuddlefish": 300, # maybe at some point, needs hatching in containment chamber (20 real-life minutes) +} + +# be nice and make these require Stasis Rifle +aggressive: Set[str] = { + "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency + "Crashfish", + "Bleeder", + "Mesmer", + "Reaper Leviathan", + "Crabsquid", + "Warper", + "Crabsnake", + "Ampeel", + "Boneshark", + "Lava Lizard", + "Sea Dragon Leviathan", + "River Prowler", +} + +suffix: str = " Scan" + +creature_locations: Dict[str, int] = { + creature+suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) +} + +all_creatures_presorted: List[str] = sorted(all_creatures) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index b55efe2453..f3a6ded5aa 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -166,7 +166,7 @@ item_table: Dict[int, ItemDict] = { 'count': 5, 'name': 'Seamoth Fragment', 'tech_type': 'SeamothFragment'}, - 35039: {'classification': ItemClassification.useful, + 35039: {'classification': ItemClassification.progression, 'count': 2, 'name': 'Stasis Rifle Fragment', 'tech_type': 'StasisRifleFragment'}, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index cae7ba6c0e..b5dc2241fb 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,4 +1,5 @@ -from Options import Choice +from Options import Choice, Range +from .Creatures import all_creatures class ItemPool(Choice): @@ -31,7 +32,15 @@ class Goal(Choice): }[self.value] +class CreatureScans(Range): + """Place items on specific creature scans. + Warning: Includes aggressive Leviathans.""" + display_name = "Creature Scans" + range_end = len(all_creatures) + + options = { "item_pool": ItemPool, "goal": Goal, + "creature_scans": CreatureScans } diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 131a537f04..b8f8f1a7b4 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,112 +1,122 @@ +from typing import TYPE_CHECKING + from worlds.generic.Rules import set_rule from .Locations import location_table, LocationDict +from .Creatures import all_creatures, aggressive, suffix import math +if TYPE_CHECKING: + from . import SubnauticaWorld -def has_seaglide(state, player): + +def has_seaglide(state, player: int): return state.has("Seaglide Fragment", player, 2) -def has_modification_station(state, player): +def has_modification_station(state, player: int): return state.has("Modification Station Fragment", player, 3) -def has_mobile_vehicle_bay(state, player): +def has_mobile_vehicle_bay(state, player: int): return state.has("Mobile Vehicle Bay Fragment", player, 3) -def has_moonpool(state, player): +def has_moonpool(state, player: int): return state.has("Moonpool Fragment", player, 2) -def has_vehicle_upgrade_console(state, player): +def has_vehicle_upgrade_console(state, player: int): return state.has("Vehicle Upgrade Console", player) and \ has_moonpool(state, player) -def has_seamoth(state, player): +def has_seamoth(state, player: int): return state.has("Seamoth Fragment", player, 3) and \ has_mobile_vehicle_bay(state, player) -def has_seamoth_depth_module_mk1(state, player): +def has_seamoth_depth_module_mk1(state, player: int): return has_vehicle_upgrade_console(state, player) -def has_seamoth_depth_module_mk2(state, player): +def has_seamoth_depth_module_mk2(state, player: int): return has_seamoth_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_seamoth_depth_module_mk3(state, player): +def has_seamoth_depth_module_mk3(state, player: int): return has_seamoth_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_cyclops_bridge(state, player): +def has_cyclops_bridge(state, player: int): return state.has("Cyclops Bridge Fragment", player, 3) -def has_cyclops_engine(state, player): +def has_cyclops_engine(state, player: int): return state.has("Cyclops Engine Fragment", player, 3) -def has_cyclops_hull(state, player): +def has_cyclops_hull(state, player: int): return state.has("Cyclops Hull Fragment", player, 3) -def has_cyclops(state, player): +def has_cyclops(state, player: int): return has_cyclops_bridge(state, player) and \ has_cyclops_engine(state, player) and \ has_cyclops_hull(state, player) and \ has_mobile_vehicle_bay(state, player) -def has_cyclops_depth_module_mk1(state, player): +def has_cyclops_depth_module_mk1(state, player: int): return state.has("Cyclops Depth Module MK1", player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk2(state, player): +def has_cyclops_depth_module_mk2(state, player: int): return has_cyclops_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk3(state, player): +def has_cyclops_depth_module_mk3(state, player: int): return has_cyclops_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_prawn(state, player): +def has_prawn(state, player: int): return state.has("Prawn Suit Fragment", player, 4) and \ has_mobile_vehicle_bay(state, player) -def has_praw_propulsion_arm(state, player): +def has_praw_propulsion_arm(state, player: int): return state.has("Prawn Suit Propulsion Cannon Fragment", player, 2) and \ has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk1(state, player): +def has_prawn_depth_module_mk1(state, player: int): return has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk2(state, player): +def has_prawn_depth_module_mk2(state, player: int): return has_prawn_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_laser_cutter(state, player): +def has_laser_cutter(state, player: int): return state.has("Laser Cutter Fragment", player, 3) +def has_stasis_rile(state, player: int): + return state.has("Stasis Rifle Fragment", player, 2) + + # Either we have propulsion cannon, or prawn + propulsion cannon arm -def has_propulsion_cannon(state, player): +def has_propulsion_cannon(state, player: int): return state.has("Propulsion Cannon Fragment", player, 2) or \ (has_prawn(state, player) and has_praw_propulsion_arm(state, player)) -def has_cyclops_shield(state, player): +def has_cyclops_shield(state, player: int): return has_cyclops(state, player) and \ state.has("Cyclops Shield Generator", player) @@ -119,7 +129,7 @@ def has_cyclops_shield(state, player): # negligeable with from high capacity tank. 430m -> 460m # Fins are not used when using seaglide # -def get_max_swim_depth(state, player): +def get_max_swim_depth(state, player: int): # TODO, Make this a difficulty setting. # Only go up to 200m without any submarines for now. return 200 @@ -130,7 +140,7 @@ def get_max_swim_depth(state, player): # has_ultra_glide_fins = state.has("Ultra Glide Fins", player) # max_depth = 400 # More like 430m. Give some room - # if has_seaglide(state, player): + # if has_seaglide(state, player: int): # if has_ultra_high_capacity_tank: # max_depth = 750 # It's about 50m more. Give some room # else: @@ -146,7 +156,7 @@ def get_max_swim_depth(state, player): # return max_depth -def get_seamoth_max_depth(state, player): +def get_seamoth_max_depth(state, player: int): if has_seamoth(state, player): if has_seamoth_depth_module_mk3(state, player): return 900 @@ -186,7 +196,7 @@ def get_prawn_max_depth(state, player): return 0 -def get_max_depth(state, player): +def get_max_depth(state, player: int): # TODO, Difficulty option, we can add vehicle depth + swim depth # But at this point, we have to consider traver distance in caves, not # just depth @@ -196,7 +206,7 @@ def get_max_depth(state, player): get_prawn_max_depth(state, player)) -def can_access_location(state, player: int, loc: LocationDict): +def can_access_location(state, player: int, loc: LocationDict) -> bool: need_laser_cutter = loc.get("need_laser_cutter", False) if need_laser_cutter and not has_laser_cutter(state, player): return False @@ -225,17 +235,33 @@ def can_access_location(state, player: int, loc: LocationDict): return get_max_depth(state, player) >= depth -def set_location_rule(world, player, loc): +def set_location_rule(world, player: int, loc: LocationDict): set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc)) -def set_rules(subnautica_world): +def can_scan_creature(state, player: int, creature: str) -> bool: + if not has_seaglide(state, player): + return False + if creature in aggressive and not has_stasis_rile(state, player): + return False + return get_max_depth(state, player) >= all_creatures[creature] + + +def set_creature_rule(world, player, creature_name: str): + set_rule(world.get_location(creature_name + suffix, player), + lambda state: can_scan_creature(state, player, creature_name)) + + +def set_rules(subnautica_world: "SubnauticaWorld"): player = subnautica_world.player world = subnautica_world.world for loc in location_table.values(): set_location_rule(world, player, loc) + for creature_name in subnautica_world.creatures_to_scan: + set_creature_rule(world, player, creature_name) + # Victory locations set_rule(world.get_location("Neptune Launch", player), lambda state: get_max_depth(state, player) >= 1444 and diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index f2fa5497cf..9ad4feb1a4 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -5,6 +5,7 @@ from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassifi from worlds.AutoWorld import World, WebWorld from . import Items from . import Locations +from . import Creatures from . import Options from .Items import item_table from .Rules import set_rules @@ -23,6 +24,10 @@ class SubnaticaWeb(WebWorld): )] +all_locations = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} +all_locations.update(Creatures.creature_locations) + + class SubnauticaWorld(World): """ Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by @@ -33,25 +38,30 @@ class SubnauticaWorld(World): web = SubnaticaWeb() item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} - location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} + location_name_to_id = all_locations options = Options.options - data_version = 2 + data_version = 3 required_client_version = (0, 3, 3) prefill_items: List[Item] + creatures_to_scan: List[str] def generate_early(self) -> None: self.prefill_items = [ self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] + self.creatures_to_scan = self.world.random.sample(Creatures.all_creatures_presorted, + self.world.creature_scans[self.player].value) def create_regions(self): self.world.regions += [ self.create_region("Menu", None, ["Lifepod 5"]), self.create_region("Planet 4546B", - Locations.events + [location["name"] for location in Locations.location_table.values()]) + Locations.events + + [location["name"] for location in Locations.location_table.values()] + + [creature+Creatures.suffix for creature in self.creatures_to_scan]) ] # refer to Rules.py @@ -64,7 +74,7 @@ class SubnauticaWorld(World): # Generate item pool pool = [] neptune_launch_platform = None - extras = 0 + extras = self.world.creature_scans[self.player].value valuable = self.world.item_pool[self.player] == Options.ItemPool.option_valuable for item in item_table.values(): for i in range(item["count"]): @@ -105,6 +115,7 @@ class SubnauticaWorld(World): slot_data: Dict[str, Any] = { "goal": goal.current_key, "vanilla_tech": vanilla_tech, + "creatures_to_scan": self.creatures_to_scan } return slot_data From 9897f4eb4bea540996c0d75cb25b6b117dfd2093 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Sat, 16 Jul 2022 13:56:23 -0400 Subject: [PATCH 029/138] LTTP: Yaml Update (#765) removes vendor option from hints, adds scam setting, and adds P option to shop shuffle. --- playerSettings.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/playerSettings.yaml b/playerSettings.yaml index ff3596a77a..4ebae9e6d7 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -175,12 +175,15 @@ A Link to the Past: retro_caves: on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion. off: 50 - hints: # Vendors: King Zora and Bottle Merchant say what they're selling. - # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints. + hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints. 'on': 50 - vendors: 0 'off': 0 full: 0 + scams: # If on, these Merchants will no longer tell you what they're selling. + 'off': 50 + 'king_zora': 0 + 'bottle_merchant': 0 + 'all': 0 swordless: on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change off: 1 @@ -273,6 +276,7 @@ A Link to the Past: p: 0 # Randomize the prices of the items in shop inventories u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld) w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too + P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees ip: 0 # Shuffle inventories and randomize prices fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool From 828bcb12661fffd3e65be59fa20706d0c36347d6 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Sat, 16 Jul 2022 14:00:00 -0400 Subject: [PATCH 030/138] OoT: Fix gerudo_fortress on normal (#784) --- worlds/oot/ItemPool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 24dda8e24f..301c502a7e 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -1088,10 +1088,10 @@ def get_pool_core(world): placed_items['Hideout Jail Guard (4 Torches)'] = 'Recovery Heart' skip_in_spoiler_locations.extend(['Hideout Jail Guard (2 Torches)', 'Hideout Jail Guard (3 Torches)', 'Hideout Jail Guard (4 Torches)']) else: - placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Gerudo Fortress)' + placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Thieves Hideout)' if world.shuffle_gerudo_card and world.gerudo_fortress != 'open': pool.append('Gerudo Membership Card') From 472e114fb955dc4bcddfb03a7e9813edb88014e3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 16 Jul 2022 23:52:47 +0200 Subject: [PATCH 031/138] Final Fantasy: fix outdated advancement flag --- worlds/ff1/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 761d9fbbe4..9818bed974 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -1,5 +1,5 @@ from typing import Dict -from BaseClasses import Item, Location, MultiWorld, Tutorial +from BaseClasses import Item, Location, MultiWorld, Tutorial, ItemClassification from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST, FF1_BRIDGE from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT from .Options import ff1_options @@ -55,7 +55,7 @@ class FF1World(World): rules = get_options(self.world, 'rules', self.player) menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules) terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region) - terminated_item = Item(CHAOS_TERMINATED_EVENT, True, EventId, self.player) + terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player) terminated_event.place_locked_item(terminated_item) items = get_options(self.world, 'items', self.player) @@ -114,5 +114,6 @@ class FF1World(World): def get_filler_item_name(self) -> str: return self.world.random.choice(["Heal", "Pure", "Soft", "Tent", "Cabin", "House"]) + def get_options(world: MultiWorld, name: str, player: int): return getattr(world, name, None)[player].value From bd4850b2b5ba0d5b36a899a353ce30b6d1a4632d Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 17 Jul 2022 12:56:22 +0200 Subject: [PATCH 032/138] The Witness 0.3.4 features (#780) New options: Shuffle Doors: Many doors in the game will open on their own upon receiving an item ("key"). Variant - Shuffle Door/Control Panels: Many panels in the game that open doors or control devices in the world will be off until receiving their respective item ("key"). Shuffle Lasers: Lasers no longer activate by solving the laser panel, instead you will get an item that activates the laser. Shuffle Symbols: Now that there is something else to shuffle (doors / door panels), you can turn off Symbol Rando. Shuffle Postgame (replaces "Shuffle Hard"): The randomizer will now determine by your settings which panels are in the "postgame" - Meaning they can only be accessed after you can complete your win condition anyway. --- worlds/witness/Door_Shuffle.txt | 30 - worlds/witness/Early_UTM.txt | 5 - worlds/witness/Options.py | 51 +- worlds/witness/WitnessItems.txt | 173 ++ worlds/witness/WitnessLogic.txt | 1508 ++++++++++------- worlds/witness/__init__.py | 21 +- worlds/witness/items.py | 33 +- worlds/witness/locations.py | 148 +- worlds/witness/player_logic.py | 230 +-- worlds/witness/regions.py | 19 +- worlds/witness/rules.py | 53 +- .../{ => settings}/Disable_Unrandomized.txt | 16 +- .../witness/settings/Door_Panel_Shuffle.txt | 31 + worlds/witness/settings/Doors_Complex.txt | 201 +++ worlds/witness/settings/Doors_Max.txt | 212 +++ worlds/witness/settings/Doors_Simple.txt | 146 ++ worlds/witness/settings/Early_UTM.txt | 9 + worlds/witness/settings/Laser_Shuffle.txt | 12 + worlds/witness/settings/Symbol_Shuffle.txt | 14 + worlds/witness/static_logic.py | 106 +- worlds/witness/utils.py | 34 +- 21 files changed, 2058 insertions(+), 994 deletions(-) delete mode 100644 worlds/witness/Door_Shuffle.txt delete mode 100644 worlds/witness/Early_UTM.txt rename worlds/witness/{ => settings}/Disable_Unrandomized.txt (94%) create mode 100644 worlds/witness/settings/Door_Panel_Shuffle.txt create mode 100644 worlds/witness/settings/Doors_Complex.txt create mode 100644 worlds/witness/settings/Doors_Max.txt create mode 100644 worlds/witness/settings/Doors_Simple.txt create mode 100644 worlds/witness/settings/Early_UTM.txt create mode 100644 worlds/witness/settings/Laser_Shuffle.txt create mode 100644 worlds/witness/settings/Symbol_Shuffle.txt diff --git a/worlds/witness/Door_Shuffle.txt b/worlds/witness/Door_Shuffle.txt deleted file mode 100644 index 7d48064cc8..0000000000 --- a/worlds/witness/Door_Shuffle.txt +++ /dev/null @@ -1,30 +0,0 @@ -100 - 0x01A54 - None - Glass Factory Entry Door -105 - 0x000B0 - 0x0343A - Door to Symmetry Island Lower -107 - 0x1C349 - 0x00076 - Door to Symmetry Island Upper -110 - 0x0C339 - 0x09F94 - Door to Desert Flood Light Room -111 - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B - None - Desert Flood Room Flood Controls -120 - 0x03678 - None - Quarry Mill Ramp Control -122 - 0x03679 - 0x014E8 - Quarry Mill Elevator Control -125 - 0x03852 - 0x034D4,0x021D5 - Quarry Boathouse Ramp Height Control -127 - 0x03858 - 0x021AE - Quarry Boathouse Ramp Horizontal Control -131 - 0x334DB,0x334DC - None - Shadows Door Timer -150 - 0x00B10 - None - Monastery Entry Door Left -151 - 0x00C92 - None - Monastery Entry Door Right -162 - 0x28998 - None - Town Door to RGB House -163 - 0x28A0D - 0x28998 - Town Door to Church -166 - 0x28A79 - None - Town Maze Panel (Drop-Down Staircase) -169 - 0x17F5F - None - Windmill Door -200 - 0x0288C - None - Treehouse First & Second Door -202 - 0x0A182 - None - Treehouse Third Door -205 - 0x2700B - None - Treehouse Laser House Door Timer -208 - 0x17CBC - None - Treehouse Shortcut Drop-Down Bridge -175 - 0x17CAB - 0x002C7 - Jungle Popup Wall -180 - 0x17C2E - None - Bunker Entry Door -183 - 0x0A099 - 0x09DAF - Inside Bunker Door to Bunker Proper -186 - 0x0A079 - None - Bunker Elevator Control -190 - 0x0056E - None - Swamp Entry Door -192 - 0x00609,0x18488 - 0x181A9 - Swamp Sliding Bridge -195 - 0x181F5 - None - Swamp Rotating Bridge -197 - 0x17C0A - None - Swamp Maze Control -300 - 0x0042D - None - Mountaintop River Shape Panel (Shortcut to Secret Area) -310 - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 - None - Boat diff --git a/worlds/witness/Early_UTM.txt b/worlds/witness/Early_UTM.txt deleted file mode 100644 index 57da491e35..0000000000 --- a/worlds/witness/Early_UTM.txt +++ /dev/null @@ -1,5 +0,0 @@ -Event Items: -Shortcut to Secret Area Opens - 0x0042D - -Region Changes: -Inside Mountain Secret Area (Inside Mountain Secret Area) - Inside Mountain Path to Secret Area - 0x00FF8 - Main Island - 0x021D7 | 0x0042D - Main Island - 0x17CF2 \ No newline at end of file diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index e8b7e576da..2bfe3f41e1 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -7,37 +7,48 @@ from Options import Toggle, DefaultOnToggle, Option, Range, Choice # "Play the randomizer in hardmode" # display_name = "Hard Mode" -# class UnlockSymbols(DefaultOnToggle): -# "All Puzzle symbols of a specific panel need to be unlocked before the panel can be used" -# display_name = "Unlock Symbols" - class DisableNonRandomizedPuzzles(DefaultOnToggle): - """Disable puzzles that cannot be randomized. - Non randomized puzzles are Shadows, Monastery, and Greenhouse. + """Disables puzzles that cannot be randomized. + This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard. The lasers for those areas will be activated as you solve optional puzzles throughout the island.""" display_name = "Disable non randomized puzzles" class EarlySecretArea(Toggle): - """The Mountainside shortcut to the Mountain Secret Area is open from the start. + """Opens the Mountainside shortcut to the Mountain Secret Area from the start. (Otherwise known as "UTM", "Caves" or the "Challenge Area")""" display_name = "Early Secret Area" class ShuffleSymbols(DefaultOnToggle): - """You will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols.""" + """You will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols. + If you turn this off, there will be no progression items in the game unless you turn on door shuffle.""" display_name = "Shuffle Symbols" -class ShuffleDoors(Toggle): - """Many doors around the island will have their panels turned off initially. - You will need to find the items that power the panels to open those doors.""" +class ShuffleLasers(Toggle): + """If on, the 11 lasers are turned into items and will activate on their own upon receiving them. + Note: There is a visual bug that can occur with the Desert Laser. It does not affect gameplay - The Laser can still + be redirected as normal, for both applications of redirection.""" + display_name = "Shuffle Lasers" + + +class ShuffleDoors(Choice): + """If on, opening doors will require their respective "keys". + If set to "panels", those keys will unlock the panels on doors. + In "doors_simple" and "doors_complex", the doors will magically open by themselves upon receiving the key.""" display_name = "Shuffle Doors" + option_none = 0 + option_panels = 1 + option_doors_simple = 2 + option_doors_complex = 3 + option_max = 4 class ShuffleDiscardedPanels(Toggle): - """Discarded Panels will have items on them. - Solving certain Discarded Panels may still be necessary even if off!""" + """Add Discarded Panels into the location pool. + Solving certain Discarded Panels may still be necessary to beat the game, even if this is off.""" + display_name = "Shuffle Discarded Panels" @@ -52,9 +63,10 @@ class ShuffleUncommonLocations(Toggle): display_name = "Shuffle Uncommon Locations" -class ShuffleHardLocations(Toggle): - """Adds some harder locations into the game, e.g. Mountain Secret Area panels""" - display_name = "Shuffle Hard Locations" +class ShufflePostgame(Toggle): + """Adds locations into the pool that are guaranteed to be locked behind your goal. Use this if you don't play with + forfeit on victory.""" + display_name = "Shuffle Postgame" class VictoryCondition(Choice): @@ -103,16 +115,17 @@ class PuzzleSkipAmount(Range): the_witness_options: Dict[str, type] = { # "hard_mode": HardMode, + "shuffle_symbols": ShuffleSymbols, + "shuffle_doors": ShuffleDoors, + "shuffle_lasers": ShuffleLasers, "disable_non_randomized_puzzles": DisableNonRandomizedPuzzles, "shuffle_discarded_panels": ShuffleDiscardedPanels, "shuffle_vault_boxes": ShuffleVaultBoxes, "shuffle_uncommon": ShuffleUncommonLocations, - "shuffle_hard": ShuffleHardLocations, + "shuffle_postgame": ShufflePostgame, "victory_condition": VictoryCondition, "trap_percentage": TrapPercentage, "early_secret_area": EarlySecretArea, - # "shuffle_symbols": ShuffleSymbols, - # "shuffle_doors": ShuffleDoors, "mountain_lasers": MountainLasers, "challenge_lasers": ChallengeLasers, "puzzle_skip_amount": PuzzleSkipAmount, diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 9d8831bb10..5631ab2f41 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -1,6 +1,8 @@ Progression: 0 - Dots 1 - Colored Dots +2 - Full Dots +3 - Invisible Dots 5 - Sound Dots 10 - Symmetry 20 - Triangles @@ -12,6 +14,7 @@ Progression: 61 - Stars + Same Colored Symbol 71 - Black/White Squares 72 - Colored Squares +80 - Arrows Usefuls: 101 - Functioning Brain - False @@ -23,3 +26,173 @@ Boosts: Traps: 600 - Slowness 610 - Power Surge + +Doors: +1100 - Glass Factory Entry Door (Panel) - 0x01A54 +1105 - Door to Symmetry Island Lower (Panel) - 0x000B0 +1107 - Door to Symmetry Island Upper (Panel) - 0x1C349 +1110 - Door to Desert Flood Light Room (Panel) - 0x0C339 +1111 - Desert Flood Room Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B +1119 - Quarry Door to Mill (Panel) - 0x01E5A,0x01E59 +1120 - Quarry Mill Ramp Controls (Panel) - 0x03678,0x03676 +1122 - Quarry Mill Elevator Controls (Panel) - 0x03679,0x03675 +1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852 +1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858 +1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC +1150 - Monastery Entry Door Left (Panel) - 0x00B10 +1151 - Monastery Entry Door Right (Panel) - 0x00C92 +1162 - Town Door to RGB House (Panel) - 0x28998 +1163 - Town Door to Church (Panel) - 0x28A0D +1166 - Town Maze Panel (Drop-Down Staircase) (Panel) - 0x28A79 +1169 - Windmill Door (Panel) - 0x17F5F +1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886 +1202 - Treehouse Third Door (Panel) - 0x0A182 +1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x334DC +1208 - Treehouse Shortcut Drop-Down Bridge (Panel) - 0x17CBC +1175 - Jungle Popup Wall (Panel) - 0x17CAB +1180 - Bunker Entry Door (Panel) - 0x17C2E +1183 - Inside Bunker Door to Bunker Proper (Panel) - 0x0A099 +1186 - Bunker Elevator Control (Panel) - 0x0A079 +1190 - Swamp Entry Door (Panel) - 0x0056E +1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488 +1195 - Swamp Rotating Bridge (Panel) - 0x181F5 +1197 - Swamp Maze Control (Panel) - 0x17C0A +1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 + +1400 - Caves Mountain Shortcut - 0x2D73F + +1500 - Symmetry Laser - 0x00509 +1501 - Desert Laser - 0x012FB,0x01317 +1502 - Quarry Laser - 0x01539 +1503 - Shadows Laser - 0x181B3 +1504 - Keep Laser - 0x014BB +1505 - Monastery Laser - 0x17C65 +1506 - Town Laser - 0x032F9 +1507 - Jungle Laser - 0x00274 +1508 - Bunker Laser - 0x0C2B2 +1509 - Swamp Laser - 0x00BF6 +1510 - Treehouse Laser - 0x028A4 + +1600 - Outside Tutorial Optional Door - 0x03BA2 +1603 - Outside Tutorial Outpost Entry Door - 0x0A170 +1606 - Outside Tutorial Outpost Exit Door - 0x04CA3 +1609 - Glass Factory Entry Door - 0x01A29 +1612 - Glass Factory Back Wall - 0x0D7ED +1615 - Symmetry Island Lower Door - 0x17F3E +1618 - Symmetry Island Upper Door - 0x18269 +1619 - Orchard Middle Gate - 0x03307 +1620 - Orchard Final Gate - 0x03313 +1621 - Desert Door to Flood Light Room - 0x09FEE +1624 - Desert Door to Pond Room - 0x0C2C3 +1627 - Desert Door to Water Levels Room - 0x0A24B +1630 - Desert Door to Elevator Room - 0x0C316 +1633 - Quarry Main Entry 1 - 0x09D6F +1636 - Quarry Main Entry 2 - 0x17C07 +1639 - Quarry Door to Mill - 0x02010 +1642 - Quarry Mill Side Door - 0x275FF +1645 - Quarry Mill Rooftop Shortcut - 0x17CE8 +1648 - Quarry Mill Stairs - 0x0368A +1651 - Quarry Boathouse Boat Staircase - 0x2769B,0x27163 +1653 - Quarry Boathouse First Barrier - 0x17C50 +1654 - Quarry Boathouse Shortcut - 0x3865F +1656 - Shadows Timed Door - 0x19B24 +1657 - Shadows Laser Room Right Door - 0x194B2 +1660 - Shadows Laser Room Left Door - 0x19665 +1663 - Shadows Barrier to Quarry - 0x19865,0x0A2DF +1666 - Shadows Barrier to Ledge - 0x1855B,0x19ADE +1669 - Keep Hedge Maze 1 Exit Door - 0x01954 +1672 - Keep Pressure Plates 1 Exit Door - 0x01BEC +1675 - Keep Hedge Maze 2 Shortcut - 0x018CE +1678 - Keep Hedge Maze 2 Exit Door - 0x019D8 +1681 - Keep Hedge Maze 3 Shortcut - 0x019B5 +1684 - Keep Hedge Maze 3 Exit Door - 0x019E6 +1687 - Keep Hedge Maze 4 Shortcut - 0x0199A +1690 - Keep Hedge Maze 4 Exit Door - 0x01A0E +1693 - Keep Pressure Plates 2 Exit Door - 0x01BEA +1696 - Keep Pressure Plates 3 Exit Door - 0x01CD5 +1699 - Keep Pressure Plates 4 Exit Door - 0x01D40 +1702 - Keep Shortcut to Shadows - 0x09E3D +1705 - Keep Tower Shortcut - 0x04F8F +1708 - Monastery Shortcut - 0x0364E +1711 - Monastery Inner Door - 0x0C128 +1714 - Monastery Outer Door - 0x0C153 +1717 - Monastery Door to Garden - 0x03750 +1718 - Town Cargo Box Door - 0x0A0C9 +1720 - Town Wooden Roof Staircase - 0x034F5 +1723 - Town Tinted Door to RGB House - 0x28A61 +1726 - Town Door to Church - 0x03BB0 +1729 - Town Maze Staircase - 0x28AA2 +1732 - Town Windmill Door - 0x1845B +1735 - Town RGB House Staircase - 0x2897B +1738 - Town Tower Blue Panels Door - 0x27798 +1741 - Town Tower Lattice Door - 0x27799 +1744 - Town Tower Environmental Set Door - 0x2779A +1747 - Town Tower Wooden Roof Set Door - 0x2779C +1750 - Theater Entry Door - 0x17F88 +1753 - Theater Exit Door Left - 0x0A16D +1756 - Theater Exit Door Right - 0x3CCDF +1759 - Jungle Bamboo Shortcut to River - 0x3873B +1760 - Jungle Popup Wall - 0x1475B +1762 - River Shortcut to Monastery Garden - 0x0CF2A +1765 - Bunker Bunker Entry Door - 0x0C2A4 +1768 - Bunker Tinted Glass Door - 0x17C79 +1771 - Bunker Door to Ultraviolet Room - 0x0C2A3 +1774 - Bunker Door to Elevator - 0x0A08D +1777 - Swamp Entry Door - 0x00C1C +1780 - Swamp Door to Broken Shapers - 0x184B7 +1783 - Swamp Platform Shortcut Door - 0x38AE6 +1786 - Swamp Cyan Water Pump - 0x04B7F +1789 - Swamp Door to Rotated Shapers - 0x18507 +1792 - Swamp Red Water Pump - 0x183F2 +1795 - Swamp Red Underwater Exit - 0x305D5 +1798 - Swamp Blue Water Pump - 0x18482 +1801 - Swamp Purple Water Pump - 0x0A1D6 +1804 - Swamp Near Laser Shortcut - 0x2D880 +1807 - Treehouse First Door - 0x0C309 +1810 - Treehouse Second Door - 0x0C310 +1813 - Treehouse Beyond Yellow Bridge Door - 0x0A181 +1816 - Treehouse Drawbridge - 0x0C32D +1819 - Treehouse Timed Door to Laser House - 0x0C323 +1822 - Inside Mountain First Layer Exit Door - 0x09E54 +1825 - Inside Mountain Second Layer Staircase Near - 0x09FFB +1828 - Inside Mountain Second Layer Exit Door - 0x09EDD +1831 - Inside Mountain Second Layer Staircase Far - 0x09E07 +1834 - Inside Mountain Giant Puzzle Exit Door - 0x09F89 +1840 - Inside Mountain Door to Final Room - 0x0C141 +1843 - Inside Mountain Bottom Layer Rock - 0x17F33 +1846 - Inside Mountain Door to Secret Area - 0x2D77D +1849 - Caves Pillar Door - 0x019A5 +1855 - Caves Swamp Shortcut - 0x2D859 +1858 - Challenge Entry Door - 0x0A19A +1861 - Challenge Door to Theater Walkway - 0x0348A +1864 - Theater Walkway Door to Windmill Interior - 0x27739 +1867 - Theater Walkway Door to Desert Elevator Room - 0x27263 +1870 - Theater Walkway Door to Town - 0x09E87 + +1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3 +1906 - Symmetry Island Doors - 0x17F3E,0x18269 +1909 - Orchard Gates - 0x03313,0x03307 +1912 - Desert Doors - 0x09FEE,0x0C2C3,0x0A24B,0x0C316 +1915 - Quarry Main Entry - 0x09D6F +1918 - Quarry Mill Shortcuts - 0x17C07,0x17CE8,0x0368A +1921 - Quarry Boathouse Barriers - 0x17C50,0x3865F +1924 - Shadows Laser Room Door - 0x194B2,0x19665 +1927 - Shadows Barriers - 0x19865,0x0A2DF,0x1855B,0x19ADE +1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E +1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40 +1936 - Keep Shortcuts - 0x09E3D,0x04F8F +1939 - Monastery Entry Door - 0x0C128,0x0C153 +1942 - Monastery Shortcuts - 0x0364E,0x03750 +1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B +1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C +1951 - Theater Exit Door - 0x0A16D,0x3CCDF +1954 - Jungle & River Shortcuts - 0x3873B,0x0CF2A +1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D +1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507 +1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6 +1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181 +1975 - Inside Mountain Second Layer Stairs & Doors - 0x09FFB,0x09EDD,0x09E07 +1978 - Inside Mountain Bottom Layer Doors to Caves - 0x17F33,0x2D77D +1981 - Caves Doors to Challenge - 0x019A5,0x0A19A +1984 - Caves Exits to Main Island - 0x2D859,0x2D73F +1987 - Theater Walkway Doors - 0x27739,0x27263,0x09E87 \ No newline at end of file diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index e4e63dc434..350f72b680 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -1,712 +1,924 @@ -First Hallway (First Hallway) - Entry - True: -0x00064 (Straight) - True - True -0x00182 (Bend) - 0x00064 - True +First Hallway (First Hallway) - Entry - True - Tutorial - 0x00182: +158000 - 0x00064 (Straight) - True - True +158001 - 0x00182 (Bend) - 0x00064 - True -Tutorial (Tutorial) - First Hallway - 0x00182: -0x00293 (Front Center) - True - True -0x00295 (Center Left) - 0x00293 - True -0x002C2 (Front Left) - 0x00295 - True -0x0A3B5 (Back Left) - True - True -0x0A3B2 (Back Right) - True - True -0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True -0x03505 (Gate Close) - 0x2FAF6 - True -0x0C335 (Pillar) - True - Triangles - True -0x0C373 (Patio Floor) - 0x0C335 - Dots +Tutorial (Tutorial) - Outside Tutorial - 0x03629: +158002 - 0x00293 (Front Center) - True - True +158003 - 0x00295 (Center Left) - 0x00293 - True +158004 - 0x002C2 (Front Left) - 0x00295 - True +158005 - 0x0A3B5 (Back Left) - True - True +158006 - 0x0A3B2 (Back Right) - True - True +158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True +158008 - 0x03505 (Gate Close) - 0x2FAF6 - True +158009 - 0x0C335 (Pillar) - True - Triangles - True +158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots -Outside Tutorial (Outside Tutorial) - Tutorial - 0x03629: -0x033D4 (Vault) - True - Dots & Squares & Black/White Squares -0x03481 (Vault Box) - 0x033D4 - True -0x0A171 (Optional Door 1) - 0x0A3B5 - Dots -0x17CFB (Discard) - 0x0A171 - Triangles -0x04CA4 (Optional Door 2) - 0x0A171 - Dots & Squares & Black/White Squares -0x0005D (Dots Introduction 1) - True - Dots -0x0005E (Dots Introduction 2) - 0x0005D - Dots -0x0005F (Dots Introduction 3) - 0x0005E - Dots -0x00060 (Dots Introduction 4) - 0x0005F - Dots -0x00061 (Dots Introduction 5) - 0x00060 - Dots -0x018AF (Squares Introduction 1) - True - Squares & Black/White Squares -0x0001B (Squares Introduction 2) - 0x018AF - Squares & Black/White Squares -0x012C9 (Squares Introduction 3) - 0x0001B - Squares & Black/White Squares -0x0001C (Squares Introduction 4) - 0x012C9 - Squares & Black/White Squares -0x0001D (Squares Introduction 5) - 0x0001C - Squares & Black/White Squares -0x0001E (Squares Introduction 6) - 0x0001D - Squares & Black/White Squares -0x0001F (Squares Introduction 7) - 0x0001E - Squares & Black/White Squares -0x00020 (Squares Introduction 8) - 0x0001F - Squares & Black/White Squares -0x00021 (Squares Introduction 9) - 0x00020 - Squares & Black/White Squares +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: +158650 - 0x033D4 (Vault) - True - Dots & Squares & Black/White Squares +158651 - 0x03481 (Vault Box) - 0x033D4 - True +158013 - 0x0005D (Dots Introduction 1) - True - Dots +158014 - 0x0005E (Dots Introduction 2) - 0x0005D - Dots +158015 - 0x0005F (Dots Introduction 3) - 0x0005E - Dots +158016 - 0x00060 (Dots Introduction 4) - 0x0005F - Dots +158017 - 0x00061 (Dots Introduction 5) - 0x00060 - Dots +158018 - 0x018AF (Squares Introduction 1) - True - Squares & Black/White Squares +158019 - 0x0001B (Squares Introduction 2) - 0x018AF - Squares & Black/White Squares +158020 - 0x012C9 (Squares Introduction 3) - 0x0001B - Squares & Black/White Squares +158021 - 0x0001C (Squares Introduction 4) - 0x012C9 - Squares & Black/White Squares +158022 - 0x0001D (Squares Introduction 5) - 0x0001C - Squares & Black/White Squares +158023 - 0x0001E (Squares Introduction 6) - 0x0001D - Squares & Black/White Squares +158024 - 0x0001F (Squares Introduction 7) - 0x0001E - Squares & Black/White Squares +158025 - 0x00020 (Squares Introduction 8) - 0x0001F - Squares & Black/White Squares +158026 - 0x00021 (Squares Introduction 9) - 0x00020 - Squares & Black/White Squares +Door - 0x03BA2 (Optional Door 1) - 0x0A3B5 + +Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170: +158011 - 0x0A171 (Door to Outpost Panel) - True - Dots +Door - 0x0A170 (Door to Outpost) - 0x0A171 + +Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3: +158012 - 0x04CA4 (Exit Door from Outpost Panel) - True - Dots & Squares & Black/White Squares +Door - 0x04CA3 (Exit Door from Outpost) - 0x04CA4 +158600 - 0x17CFB (Discard) - True - Triangles Main Island () - Outside Tutorial - True: -Outside Glass Factory (Glass Factory) - Main Island - True: -0x01A54 (Entry Door) - True - Symmetry -0x3C12B (Discard) - True - Triangles +Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: +158027 - 0x01A54 (Entry Door Panel) - True - Symmetry +Door - 0x01A29 (Entry Door) - 0x01A54 +158601 - 0x3C12B (Discard) - True - Triangles -Inside Glass Factory (Glass Factory) - Outside Glass Factory - 0x01A54: -0x00086 (Vertical Symmetry 1) - True - Symmetry -0x00087 (Vertical Symmetry 2) - 0x00086 - Symmetry -0x00059 (Vertical Symmetry 3) - 0x00087 - Symmetry -0x00062 (Vertical Symmetry 4) - 0x00059 - Symmetry -0x0005C (Vertical Symmetry 5) - 0x00062 - Symmetry -0x0008D (Rotational Symmetry 1) - 0x0005C - Symmetry -0x00081 (Rotational Symmetry 2) - 0x0008D - Symmetry -0x00083 (Rotational Symmetry 3) - 0x00081 - Symmetry -0x00084 (Melting 1) - 0x00083 - Symmetry -0x00082 (Melting 2) - 0x00084 - Symmetry -0x0343A (Melting 3) - 0x00082 - Symmetry -0x17CC8 (Boat Spawn) - 0x0005C - Boat +Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0x0D7ED: +158028 - 0x00086 (Vertical Symmetry 1) - True - Symmetry +158029 - 0x00087 (Vertical Symmetry 2) - 0x00086 - Symmetry +158030 - 0x00059 (Vertical Symmetry 3) - 0x00087 - Symmetry +158031 - 0x00062 (Vertical Symmetry 4) - 0x00059 - Symmetry +158032 - 0x0005C (Vertical Symmetry 5) - 0x00062 - Symmetry +158033 - 0x0008D (Rotational Symmetry 1) - 0x0005C - Symmetry +158034 - 0x00081 (Rotational Symmetry 2) - 0x0008D - Symmetry +158035 - 0x00083 (Rotational Symmetry 3) - 0x00081 - Symmetry +158036 - 0x00084 (Melting 1) - 0x00083 - Symmetry +158037 - 0x00082 (Melting 2) - 0x00084 - Symmetry +158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry +Door - 0x0D7ED (Back Wall) - 0x0005C -Outside Symmetry Island (Symmetry Island) - Main Island - True: -0x000B0 (Door to Symmetry Island Lower) - 0x0343A - Dots +Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: +158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat -Symmetry Island Lower (Symmetry Island) - Outside Symmetry Island - 0x000B0: -0x00022 (Black Dots 1) - True - Symmetry & Dots -0x00023 (Black Dots 2) - 0x00022 - Symmetry & Dots -0x00024 (Black Dots 3) - 0x00023 - Symmetry & Dots -0x00025 (Black Dots 4) - 0x00024 - Symmetry & Dots -0x00026 (Black Dots 5) - 0x00025 - Symmetry & Dots -0x0007C (Colored Dots 1) - 0x00026 - Symmetry & Colored Dots -0x0007E (Colored Dots 2) - 0x0007C - Symmetry & Colored Dots -0x00075 (Colored Dots 3) - 0x0007E - Symmetry & Colored Dots -0x00073 (Colored Dots 4) - 0x00075 - Symmetry & Colored Dots -0x00077 (Colored Dots 5) - 0x00073 - Symmetry & Colored Dots -0x00079 (Colored Dots 6) - 0x00077 - Symmetry & Colored Dots -0x00065 (Fading Lines 1) - 0x00079 - Symmetry & Colored Dots -0x0006D (Fading Lines 2) - 0x00065 - Symmetry & Colored Dots -0x00072 (Fading Lines 3) - 0x0006D - Symmetry & Colored Dots -0x0006F (Fading Lines 4) - 0x00072 - Symmetry & Colored Dots -0x00070 (Fading Lines 5) - 0x0006F - Symmetry & Colored Dots -0x00071 (Fading Lines 6) - 0x00070 - Symmetry & Colored Dots -0x00076 (Fading Lines 7) - 0x00071 - Symmetry & Colored Dots -0x009B8 (Scenery Outlines 1) - True - Symmetry & Environment -0x003E8 (Scenery Outlines 2) - 0x009B8 - Symmetry & Environment -0x00A15 (Scenery Outlines 3) - 0x003E8 - Symmetry & Environment -0x00B53 (Scenery Outlines 4) - 0x00A15 - Symmetry & Environment -0x00B8D (Scenery Outlines 5) - 0x00B53 - Symmetry & Environment -0x1C349 (Door to Symmetry Island Upper) - 0x00076 - Symmetry & Dots +Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: +158040 - 0x000B0 (Door to Symmetry Island Lower Panel) - 0x0343A - Dots +Door - 0x17F3E (Door to Symmetry Island Lower) - 0x000B0 -Symmetry Island Upper (Symmetry Island) - Symmetry Island Lower - 0x1C349: -0x00A52 (Yellow 1) - True - Symmetry & Colored Dots -0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots -0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots -0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots -0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots -0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots -0x0360D (Laser) - 0x00A68 - True +Symmetry Island Lower (Symmetry Island) - Symmetry Island Upper - 0x18269: +158041 - 0x00022 (Black Dots 1) - True - Symmetry & Dots +158042 - 0x00023 (Black Dots 2) - 0x00022 - Symmetry & Dots +158043 - 0x00024 (Black Dots 3) - 0x00023 - Symmetry & Dots +158044 - 0x00025 (Black Dots 4) - 0x00024 - Symmetry & Dots +158045 - 0x00026 (Black Dots 5) - 0x00025 - Symmetry & Dots +158046 - 0x0007C (Colored Dots 1) - 0x00026 - Symmetry & Colored Dots +158047 - 0x0007E (Colored Dots 2) - 0x0007C - Symmetry & Colored Dots +158048 - 0x00075 (Colored Dots 3) - 0x0007E - Symmetry & Colored Dots +158049 - 0x00073 (Colored Dots 4) - 0x00075 - Symmetry & Colored Dots +158050 - 0x00077 (Colored Dots 5) - 0x00073 - Symmetry & Colored Dots +158051 - 0x00079 (Colored Dots 6) - 0x00077 - Symmetry & Colored Dots +158052 - 0x00065 (Fading Lines 1) - 0x00079 - Symmetry & Colored Dots +158053 - 0x0006D (Fading Lines 2) - 0x00065 - Symmetry & Colored Dots +158054 - 0x00072 (Fading Lines 3) - 0x0006D - Symmetry & Colored Dots +158055 - 0x0006F (Fading Lines 4) - 0x00072 - Symmetry & Colored Dots +158056 - 0x00070 (Fading Lines 5) - 0x0006F - Symmetry & Colored Dots +158057 - 0x00071 (Fading Lines 6) - 0x00070 - Symmetry & Colored Dots +158058 - 0x00076 (Fading Lines 7) - 0x00071 - Symmetry & Colored Dots +158059 - 0x009B8 (Scenery Outlines 1) - True - Symmetry & Environment +158060 - 0x003E8 (Scenery Outlines 2) - 0x009B8 - Symmetry & Environment +158061 - 0x00A15 (Scenery Outlines 3) - 0x003E8 - Symmetry & Environment +158062 - 0x00B53 (Scenery Outlines 4) - 0x00A15 - Symmetry & Environment +158063 - 0x00B8D (Scenery Outlines 5) - 0x00B53 - Symmetry & Environment +158064 - 0x1C349 (Door to Symmetry Island Upper Panel) - 0x00076 - Symmetry & Dots +Door - 0x18269 (Door to Symmetry Island Upper) - 0x1C349 -Orchard (Orchard) - Main Island - True: -0x00143 (Apple Tree 1) - True - Environment -0x0003B (Apple Tree 2) - 0x00143 - Environment -0x00055 (Apple Tree 3) - 0x0003B - Environment -0x032F7 (Apple Tree 4) - 0x00055 - Environment -0x032FF (Apple Tree 5) - 0x032F7 - Environment +Symmetry Island Upper (Symmetry Island): +158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots +158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots +158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots +158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots +158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots +158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots +158700 - 0x0360D (Laser Panel) - 0x00A68 - True +Laser - 0x00509 (Laser) - 0x0360D - True -Desert Outside (Desert) - Main Island - True: -0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers -0x0339E (Vault Box) - 0x0CC7B - True -0x17CE7 (Discard) - True - Triangles -0x00698 (Sun Reflection 1) - True - Reflection -0x0048F (Sun Reflection 2) - 0x00698 - Reflection -0x09F92 (Sun Reflection 3) - 0x0048F & 0x09FA0 - Reflection -0x09FA0 (Reflection 3 Control) - 0x0048F - True -0x0A036 (Sun Reflection 4) - 0x09F92 - Reflection -0x09DA6 (Sun Reflection 5) - 0x09F92 - Reflection -0x0A049 (Sun Reflection 6) - 0x09F92 - Reflection -0x0A053 (Sun Reflection 7) - 0x0A036 & 0x09DA6 & 0x0A049 - Reflection -0x09F94 (Sun Reflection 8) - 0x0A053 & 0x09F86 - Reflection -0x09F86 (Reflection 8 Control) - 0x0A053 - True -0x0C339 (Door to Desert Flood Light Room) - 0x09F94 - True +Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307: +158071 - 0x00143 (Apple Tree 1) - True - Environment +158072 - 0x0003B (Apple Tree 2) - 0x00143 - Environment +158073 - 0x00055 (Apple Tree 3) - 0x0003B - Environment +Door - 0x03307 (Mid Gate) - 0x00055 -Desert Floodlight Room (Desert) - Desert Outside - 0x0C339: -0x09FAA (Light Control) - True - True -0x00422 (Artificial Light Reflection 1) - 0x09FAA - Reflection -0x006E3 (Artificial Light Reflection 2) - 0x09FAA - Reflection -0x0A02D (Artificial Light Reflection 3) - 0x09FAA & 0x00422 & 0x006E3 - Reflection +Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313: +158074 - 0x032F7 (Apple Tree 4) - 0x00055 - Environment +158075 - 0x032FF (Apple Tree 5) - 0x032F7 - Environment +Door - 0x03313 (Final Gate) - 0x032FF -Desert Pond Room (Desert) - Desert Floodlight Room - 0x0A02D: -0x00C72 (Pond Reflection 1) - True - Reflection -0x0129D (Pond Reflection 2) - 0x00C72 - Reflection -0x008BB (Pond Reflection 3) - 0x0129D - Reflection -0x0078D (Pond Reflection 4) - 0x008BB - Reflection -0x18313 (Pond Reflection 5) - 0x0078D - Reflection -0x0A249 (Door to Desert Water Levels Room) - 0x18313 - Reflection +Orchard End (Orchard): -Desert Water Levels Room (Desert) - Desert Pond Room - 0x0A249: -0x1C2DF (Reduce Water Level Far Left) - True - True -0x1831E (Reduce Water Level Far Right) - True - True -0x1C260 (Reduce Water Level Near Left) - True - True -0x1831C (Reduce Water Level Near Right) - True - True -0x1C2F3 (Raise Water Level Far Left) - True - True -0x1831D (Raise Water Level Far Right) - True - True -0x1C2B1 (Raise Water Level Near Left) - True - True -0x1831B (Raise Water Level Near Right) - True - True -0x04D18 (Flood Reflection 1) - 0x1C260 & 0x1831C - Reflection -0x01205 (Flood Reflection 2) - 0x04D18 & 0x1C260 & 0x1831C - Reflection -0x181AB (Flood Reflection 3) - 0x01205 & 0x1C260 & 0x1831C - Reflection -0x0117A (Flood Reflection 4) - 0x181AB & 0x1C260 & 0x1831C - Reflection -0x17ECA (Flood Reflection 5) - 0x0117A & 0x1C260 & 0x1831C - Reflection -0x18076 (Flood Reflection 6) - 0x17ECA & 0x1C260 & 0x1831C - Reflection +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: +158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers +158653 - 0x0339E (Vault Box) - 0x0CC7B - True +158602 - 0x17CE7 (Discard) - True - Triangles +158076 - 0x00698 (Sun Reflection 1) - True - Reflection +158077 - 0x0048F (Sun Reflection 2) - 0x00698 - Reflection +158078 - 0x09F92 (Sun Reflection 3) - 0x0048F & 0x09FA0 - Reflection +158079 - 0x09FA0 (Reflection 3 Control) - 0x0048F - True +158080 - 0x0A036 (Sun Reflection 4) - 0x09F92 - Reflection +158081 - 0x09DA6 (Sun Reflection 5) - 0x09F92 - Reflection +158082 - 0x0A049 (Sun Reflection 6) - 0x09F92 - Reflection +158083 - 0x0A053 (Sun Reflection 7) - 0x0A036 & 0x09DA6 & 0x0A049 - Reflection +158084 - 0x09F94 (Sun Reflection 8) - 0x0A053 & 0x09F86 - Reflection +158085 - 0x09F86 (Reflection 8 Control) - 0x0A053 - True +158086 - 0x0C339 (Door to Desert Flood Light Room Panel) - 0x09F94 - True +Door - 0x09FEE (Door to Desert Flood Light Room) - 0x0C339 - True -Desert Elevator Room (Desert) - Desert Water Levels Room - 0x18076: -0x17C31 (Final Transparent Reflection) - True - Reflection -0x012D7 (Final Reflection) - 0x17C31 & 0x0A015 - Reflection -0x0A015 (Final Reflection Control) - 0x17C31 - True -0x0A15C (Final Bent Reflection 1) - True - Reflection -0x09FFF (Final Bent Reflection 2) - 0x0A15C - Reflection -0x0A15F (Final Bent Reflection 3) - 0x09FFF - Reflection -0x03608 (Laser) - 0x012D7 & 0x0A15F - True +Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: +158087 - 0x09FAA (Light Control) - True - True +158088 - 0x00422 (Artificial Light Reflection 1) - 0x09FAA - Reflection +158089 - 0x006E3 (Artificial Light Reflection 2) - 0x09FAA - Reflection +158090 - 0x0A02D (Artificial Light Reflection 3) - 0x09FAA & 0x00422 & 0x006E3 - Reflection +Door - 0x0C2C3 (Door to Pond Room) - 0x0A02D -Outside Quarry (Quarry) - Main Island - True: -0x09E57 (Door to Quarry 1) - True - Squares & Black/White Squares -0x17C09 (Door to Quarry 2) - 0x09E57 - Shapers -0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser +Desert Pond Room (Desert) - Desert Water Levels Room - 0x0A24B: +158091 - 0x00C72 (Pond Reflection 1) - True - Reflection +158092 - 0x0129D (Pond Reflection 2) - 0x00C72 - Reflection +158093 - 0x008BB (Pond Reflection 3) - 0x0129D - Reflection +158094 - 0x0078D (Pond Reflection 4) - 0x008BB - Reflection +158095 - 0x18313 (Pond Reflection 5) - 0x0078D - Reflection +158096 - 0x0A249 (Door to Water Levels Room Panel) - 0x18313 - Reflection +Door - 0x0A24B (Door to Water Levels Room) - 0x0A249 -Quarry (Quarry) - Outside Quarry - 0x17C09 - Quarry Mill - 0x275ED - Quarry Mill - 0x17CAC - Shadows Ledge - 0x198BF: -0x01E5A (Door to Mill Left) - True - Squares & Black/White Squares -0x01E59 (Door to Mill Right) - True - Dots -0x17CF0 (Discard) - True - Triangles -0x03612 (Laser) - 0x0A3D0 & 0x0367C - Eraser & Shapers +Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: +158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True +158098 - 0x1831E (Reduce Water Level Far Right) - True - True +158099 - 0x1C260 (Reduce Water Level Near Left) - True - True +158100 - 0x1831C (Reduce Water Level Near Right) - True - True +158101 - 0x1C2F3 (Raise Water Level Far Left) - True - True +158102 - 0x1831D (Raise Water Level Far Right) - True - True +158103 - 0x1C2B1 (Raise Water Level Near Left) - True - True +158104 - 0x1831B (Raise Water Level Near Right) - True - True +158105 - 0x04D18 (Flood Reflection 1) - 0x1C260 & 0x1831C - Reflection +158106 - 0x01205 (Flood Reflection 2) - 0x04D18 & 0x1C260 & 0x1831C - Reflection +158107 - 0x181AB (Flood Reflection 3) - 0x01205 & 0x1C260 & 0x1831C - Reflection +158108 - 0x0117A (Flood Reflection 4) - 0x181AB & 0x1C260 & 0x1831C - Reflection +158109 - 0x17ECA (Flood Reflection 5) - 0x0117A & 0x1C260 & 0x1831C - Reflection +158110 - 0x18076 (Flood Reflection 6) - 0x17ECA & 0x1C260 & 0x1831C - Reflection +Door - 0x0C316 (Door to Elevator Room) - 0x18076 -Quarry Mill (Quarry Mill) - Quarry - 0x01E59 & 0x01E5A: -0x275ED (Ground Floor Shortcut Door) - True - True -0x03678 (Lower Ramp Control) - True - Dots & Eraser -0x00E0C (Eraser and Dots 1) - 0x03678 - Dots & Eraser -0x01489 (Eraser and Dots 2) - 0x00E0C - Dots & Eraser -0x0148A (Eraser and Dots 3) - 0x01489 - Dots & Eraser -0x014D9 (Eraser and Dots 4) - 0x0148A - Dots & Eraser -0x014E7 (Eraser and Dots 5) - 0x014D9 - Dots & Eraser -0x014E8 (Eraser and Dots 6) - 0x014E7 - Dots & Eraser -0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser -0x03675 (Upper Ramp Control) - 0x03679 - Dots & Eraser -0x03676 (Upper Lift Control) - 0x03679 - Dots & Eraser -0x00557 (Eraser and Squares 1) - 0x03679 - Squares & Colored Squares & Eraser -0x005F1 (Eraser and Squares 2) - 0x00557 - Squares & Colored Squares & Eraser -0x00620 (Eraser and Squares 3) - 0x005F1 - Squares & Colored Squares & Eraser -0x009F5 (Eraser and Squares 4) - 0x00620 - Squares & Colored Squares & Eraser -0x0146C (Eraser and Squares 5) - 0x009F5 - Squares & Colored Squares & Eraser -0x3C12D (Eraser and Squares 6) - 0x0146C - Squares & Colored Squares & Eraser -0x03686 (Eraser and Squares 7) - 0x3C12D - Squares & Colored Squares & Eraser -0x014E9 (Eraser and Squares 8) - 0x03686 - Squares & Colored Squares & Eraser -0x03677 (Stair Control) - 0x014E8 - Squares & Colored Squares & Eraser -0x3C125 (Big Squares & Dots & Eraser) - 0x0367C - Squares & Black/White Squares & Dots & Eraser -0x0367C (Small Squares & Dots & Eraser) - 0x014E9 - Squares & Colored Squares & Dots & Eraser -0x17CAC (Door to Outside Quarry Stairs) - True - True +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +158111 - 0x17C31 (Final Transparent Reflection) - True - Reflection +158113 - 0x012D7 (Final Reflection) - 0x17C31 & 0x0A015 - Reflection +158114 - 0x0A015 (Final Reflection Control) - 0x17C31 - True +158115 - 0x0A15C (Final Bent Reflection 1) - True - Reflection +158116 - 0x09FFF (Final Bent Reflection 2) - 0x0A15C - Reflection +158117 - 0x0A15F (Final Bent Reflection 3) - 0x09FFF - Reflection +158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True +Laser - 0x012FB (Laser) - 0x03608 -Quarry Boathouse (Quarry Boathouse) - Quarry - True: -0x034D4 (Intro Stars) - True - Stars -0x021D5 (Intro Shapers) - True - Shapers & Rotated Shapers -0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers -0x021B3 (Eraser and Shapers 1) - 0x03852 - Shapers & Eraser -0x021B4 (Eraser and Shapers 2) - 0x021B3 - Shapers & Eraser -0x021B0 (Eraser and Shapers 3) - 0x021B4 - Shapers & Eraser -0x021AF (Eraser and Shapers 4) - 0x021B0 - Shapers & Eraser -0x021AE (Eraser and Shapers 5) - 0x021AF - Shapers & Eraser & Broken Shapers -0x03858 (Ramp Horizontal Control) - 0x021AE - Shapers & Eraser -0x38663 (Shortcut Door) - 0x03858 - True -0x021B5 (Stars and Colored Eraser 1) - 0x03858 - Stars & Stars + Same Colored Symbol & Eraser -0x021B6 (Stars and Colored Eraser 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser -0x021B7 (Stars and Colored Eraser 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser -0x021BB (Stars and Colored Eraser 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser -0x09DB5 (Stars and Colored Eraser 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser -0x09DB1 (Stars and Colored Eraser 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser -0x3C124 (Stars and Colored Eraser 7) - 0x09DB1 - Stars & Stars + Same Colored Symbol & Eraser -0x09DB3 (Stars & Eraser & Shapers 1) - 0x3C124 - Stars & Eraser & Shapers -0x09DB4 (Stars & Eraser & Shapers 2) - 0x09DB3 - Stars & Eraser & Shapers -0x275FA (Hook Control) - 0x03858 - Shapers & Eraser -0x17CA6 (Boat Spawn) - True - Boat -0x0A3CB (Stars & Eraser & Shapers 3) - 0x09DB4 - Stars & Eraser & Shapers -0x0A3CC (Stars & Eraser & Shapers 4) - 0x0A3CB - Stars & Eraser & Shapers -0x0A3D0 (Stars & Eraser & Shapers 5) - 0x0A3CC - Stars & Eraser & Shapers +Desert Lowest Level Inbetween Shortcuts (Desert): -Shadows (Shadows) - Main Island - True - Keep Glass Plates - 0x09E49: -0x334DB (Door Timer Outside) - True - True -0x0AC74 (Lower Avoid 6) - 0x0A8DC - Shadows Avoid -0x0AC7A (Lower Avoid 7) - 0x0AC74 - Shadows Avoid -0x0A8E0 (Lower Avoid 8) - 0x0AC7A - Shadows Avoid -0x386FA (Environmental Avoid 1) - 0x0A8E0 - Shadows Avoid & Environment -0x1C33F (Environmental Avoid 2) - 0x386FA - Shadows Avoid & Environment -0x196E2 (Environmental Avoid 3) - 0x1C33F - Shadows Avoid & Environment -0x1972A (Environmental Avoid 4) - 0x196E2 - Shadows Avoid & Environment -0x19809 (Environmental Avoid 5) - 0x1972A - Shadows Avoid & Environment -0x19806 (Environmental Avoid 6) - 0x19809 - Shadows Avoid & Environment -0x196F8 (Environmental Avoid 7) - 0x19806 - Shadows Avoid & Environment -0x1972F (Environmental Avoid 8) - 0x196F8 - Shadows Avoid & Environment -0x19797 (Follow 1) - 0x0A8E0 - Shadows Follow -0x1979A (Follow 2) - 0x19797 - Shadows Follow -0x197E0 (Follow 3) - 0x1979A - Shadows Follow -0x197E8 (Follow 4) - 0x197E0 - Shadows Follow -0x197E5 (Follow 5) - 0x197E8 - Shadows Follow -0x19650 (Laser) - 0x197E5 & 0x196F8 - Shadows Avoid & Shadows Follow +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entry Doors - 0x09D6F: +158118 - 0x09E57 (Door to Quarry 1 Panel) - True - Squares & Black/White Squares +158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser +158603 - 0x17CF0 (Discard) - True - Triangles +158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers +Laser - 0x01539 (Laser) - 0x03612 +Door - 0x09D6F (Door to Quarry 1) - 0x09E57 -Shadows Ledge (Shadows) - Shadows - 0x334DB | 0x334DC | 0x0A8DC: -0x334DC (Door Timer Inside) - True - True -0x198B5 (Lower Avoid 1) - True - Shadows Avoid -0x198BD (Lower Avoid 2) - 0x198B5 - Shadows Avoid -0x198BF (Lower Avoid 3) - 0x198BD & 0x334DC - Shadows Avoid -0x19771 (Lower Avoid 4) - 0x198BF - Shadows Avoid -0x0A8DC (Lower Avoid 5) - 0x19771 - Shadows Avoid +Quarry Between Entry Doors (Quarry) - Quarry - 0x17C07: +158119 - 0x17C09 (Door to Quarry 2 Panel) - True - Shapers +Door - 0x17C07 (Door to Quarry 2) - 0x17C09 -Keep (Keep) - Main Island - True: +Quarry (Quarry) - Quarry Mill Ground Floor - 0x02010: +158121 - 0x01E5A (Door to Mill Left) - True - Squares & Black/White Squares +158122 - 0x01E59 (Door to Mill Right) - True - Dots +Door - 0x02010 (Door to Mill) - 0x01E59 & 0x01E5A -Keep Hedges (Keep) - Keep - True: -0x00139 (Hedge Maze 1) - True - Environment -0x019DC (Hedge Maze 2) - 0x00139 - Environment -0x019E7 (Hedge Maze 3) - 0x019DC - Environment & Sound -0x01A0F (Hedge Maze 4) - 0x019E7 - Environment +Quarry Mill Ground Floor (Quarry Mill) - Quarry - 0x275FF - Quarry Mill Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: +158123 - 0x275ED (Ground Floor Shortcut Door Panel) - True - True +Door - 0x275FF (Ground Floor Shortcut Door) - 0x275ED +158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser +158145 - 0x17CAC (Door to Outside Quarry Stairs Panel) - True - True +Door - 0x17CE8 (Door to Outside Quarry Stairs) - 0x17CAC -Keep Glass Plates (Keep) - Keep - True - Keep Tower - 0x0361B: -0x0A3A8 (Reset Pressure Plates 1) - True - True -0x033EA (Pressure Plates 1) - 0x0A3A8 - Pressure Plates & Dots -0x0A3B9 (Reset Pressure Plates 2) - 0x033EA - True -0x01BE9 (Pressure Plates 2) - 0x033EA & 0x0A3B9 - Pressure Plates & Stars & Stars + Same Colored Symbol & Squares & Black/White Squares -0x0A3BB (Reset Pressure Plates 3) - 0x0A3A8 - True -0x01CD3 (Pressure Plates 3) - 0x0A3A8 & 0x0A3BB - Pressure Plates & Shapers & Squares & Black/White Squares & Colored Squares -0x0A3AD (Reset Pressure Plates 4) - 0x01CD3 - True -0x01D3F (Pressure Plates 4) - 0x01CD3 & 0x0A3AD - Pressure Plates & Shapers & Dots & Symmetry -0x17D27 (Discard) - 0x01CD3 - Triangles -0x09E49 (Shortcut to Shadows) - 0x01CD3 - True +Quarry Mill Middle Floor (Quarry Mill) - Quarry Mill Ground Floor - 0x03675 - Quarry Mill Upper Floor - 0x03679: +158125 - 0x00E0C (Eraser and Dots 1) - True - Dots & Eraser +158126 - 0x01489 (Eraser and Dots 2) - 0x00E0C - Dots & Eraser +158127 - 0x0148A (Eraser and Dots 3) - 0x01489 - Dots & Eraser +158128 - 0x014D9 (Eraser and Dots 4) - 0x0148A - Dots & Eraser +158129 - 0x014E7 (Eraser and Dots 5) - 0x014D9 - Dots & Eraser +158130 - 0x014E8 (Eraser and Dots 6) - 0x014E7 - Dots & Eraser +158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser -Shipwreck (Shipwreck) - Keep Glass Plates - 0x033EA: -0x00AFB (Vault) - True - Symmetry & Sound & Sound Dots & Colored Dots -0x03535 (Vault Box) - 0x00AFB - True -0x17D28 (Discard) - True - Triangles +Quarry Mill Upper Floor (Quarry Mill) - Quarry Mill Middle Floor - 0x03676 & 0x03679 - Quarry Mill Ground Floor - 0x0368A: +158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser +158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser +158134 - 0x00557 (Eraser and Squares 1) - True - Squares & Colored Squares & Eraser +158135 - 0x005F1 (Eraser and Squares 2) - 0x00557 - Squares & Colored Squares & Eraser +158136 - 0x00620 (Eraser and Squares 3) - 0x005F1 - Squares & Colored Squares & Eraser +158137 - 0x009F5 (Eraser and Squares 4) - 0x00620 - Squares & Colored Squares & Eraser +158138 - 0x0146C (Eraser and Squares 5) - 0x009F5 - Squares & Colored Squares & Eraser +158139 - 0x3C12D (Eraser and Squares 6) - 0x0146C - Squares & Colored Squares & Eraser +158140 - 0x03686 (Eraser and Squares 7) - 0x3C12D - Squares & Colored Squares & Eraser +158141 - 0x014E9 (Eraser and Squares 8) - 0x03686 - Squares & Colored Squares & Eraser +158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser +Door - 0x0368A (Stairs) - 0x03677 +158143 - 0x3C125 (Big Squares & Dots & Eraser) - 0x0367C - Squares & Black/White Squares & Dots & Eraser +158144 - 0x0367C (Small Squares & Dots & Eraser) - 0x014E9 - Squares & Colored Squares & Dots & Eraser -Keep Tower (Keep) - Keep Hedges - 0x01A0F - Keep Glass Plates - 0x01D3F: -0x0361B (Shortcut to Keep Glass Plates) - True - True -0x0360E (Laser Hedges) - 0x01A0F - Environment & Sound -0x03317 (Laser Pressure Plates) - 0x01D3F - Shapers & Squares & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots +Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B: +158146 - 0x034D4 (Intro Stars) - True - Stars +158147 - 0x021D5 (Intro Shapers) - True - Shapers & Rotated Shapers +158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers +158166 - 0x17CA6 (Boat Spawn) - True - Boat +Door - 0x2769B (Boat Staircase) - 0x17CA6 +Door - 0x27163 (Boat Staircase Invis Barrier) - 0x17CA6 -Outside Monastery (Monastery) - Main Island - True: -0x03713 (Shortcut) - True - True -0x00B10 (Door Open Left) - True - True -0x00C92 (Door Open Right) - True - True -0x00290 (Rhombic Avoid 1) - 0x09D9B - Environment -0x00038 (Rhombic Avoid 2) - 0x09D9B & 0x00290 - Environment -0x00037 (Rhombic Avoid 3) - 0x09D9B & 0x00038 - Environment -0x17CA4 (Laser) - 0x193A6 - True +Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6: -Inside Monastery (Monastery) - Outside Monastery - 0x00B10 & 0x00C92: -0x09D9B (Overhead Door Control) - True - Dots -0x193A7 (Branch Avoid 1) - 0x00037 - Environment -0x193AA (Branch Avoid 2) - 0x193A7 - Environment -0x193AB (Branch Follow 1) - 0x193AA - Environment -0x193A6 (Branch Follow 2) - 0x193AB - Environment +Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50: +158149 - 0x021B3 (Eraser and Shapers 1) - True - Shapers & Eraser +158150 - 0x021B4 (Eraser and Shapers 2) - 0x021B3 - Shapers & Eraser +158151 - 0x021B0 (Eraser and Shapers 3) - 0x021B4 - Shapers & Eraser +158152 - 0x021AF (Eraser and Shapers 4) - 0x021B0 - Shapers & Eraser +158153 - 0x021AE (Eraser and Shapers 5) - 0x021AF - Shapers & Eraser & Broken Shapers +Door - 0x17C50 (Boathouse Barrier 1) - 0x021AE -Monastery Garden (Monastery) - Outside Monastery - 0x00037 - Outside Jungle River - 0x17CAA: +Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back - 0x03858: +158154 - 0x03858 (Ramp Horizontal Control) - True - Shapers & Eraser -Town (Town) - Main Island - True - Theater - 0x0A168 | 0x33AB2: -0x0A054 (Boat Summon) - True - Boat -0x0A0C8 (Cargo Box) - True - Squares & Black/White Squares & Shapers -0x17D01 (Cargo Box Discard) - 0x0A0C8 - Triangles -0x09F98 (Desert Laser Redirect) - True - True -0x18590 (Tree Outlines) - True - Symmetry & Environment -0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment -0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment -0x079DF (Triple Environmental Puzzle) - 0x28938 - Shadows Avoid & Environment & Reflection -0x28B39 (Hexagonal Reflection) - 0x079DF & 0x2896A - Reflection -0x28998 (Tinted Door to RGB House) - True - Stars & Rotated Shapers -0x28A0D (Door to Church) - 0x28998 - Stars & RGB & Environment -0x28A69 (Square Avoid) - 0x28A0D - Environment -0x28A79 (Maze Stair Control) - True - Environment -0x2896A (Maze Rooftop Bridge Control) - 0x28A79 - Shapers -0x17C71 (Rooftop Discard) - 0x2896A - Triangles -0x28AC7 (Symmetry Squares 1) - 0x2896A - Symmetry & Squares & Black/White Squares -0x28AC8 (Symmetry Squares 2) - 0x28AC7 - Symmetry & Squares & Black/White Squares -0x28ACA (Symmetry Squares 3 + Dots) - 0x28AC8 - Symmetry & Squares & Black/White Squares & Dots -0x28ACB (Symmetry Squares 4 + Dots) - 0x28ACA - Symmetry & Squares & Black/White Squares & Dots -0x28ACC (Symmetry Squares 5 + Dots) - 0x28ACB - Symmetry & Squares & Black/White Squares & Dots -0x2899C (Full Dot Grid Shapers 1) - True - Rotated Shapers & Dots -0x28A33 (Full Dot Grid Shapers 2) - 0x2899C - Shapers & Dots -0x28ABF (Full Dot Grid Shapers 3) - 0x28A33 - Shapers & Rotated Shapers & Dots -0x28AC0 (Full Dot Grid Shapers 4) - 0x28ABF - Rotated Shapers & Dots -0x28AC1 (Full Dot Grid Shapers 5) - 0x28AC0 - Rotated Shapers & Dots -0x28AD9 (Shapers & Dots & Eraser) - 0x28AC1 - Rotated Shapers & Dots & Eraser -0x17F5F (Windmill Door) - True - Dots +Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F: +158155 - 0x38663 (Shortcut Door Panel) - True - True +Door - 0x3865F (Shortcut Door) - 0x38663 +158156 - 0x021B5 (Stars and Colored Eraser 1) - True - Stars & Stars + Same Colored Symbol & Eraser +158157 - 0x021B6 (Stars and Colored Eraser 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser +158158 - 0x021B7 (Stars and Colored Eraser 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser +158159 - 0x021BB (Stars and Colored Eraser 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser +158160 - 0x09DB5 (Stars and Colored Eraser 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser +158161 - 0x09DB1 (Stars and Colored Eraser 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser +158162 - 0x3C124 (Stars and Colored Eraser 7) - 0x09DB1 - Stars & Stars + Same Colored Symbol & Eraser +158163 - 0x09DB3 (Stars & Eraser & Shapers 1) - 0x3C124 - Stars & Eraser & Shapers +158164 - 0x09DB4 (Stars & Eraser & Shapers 2) - 0x09DB3 - Stars & Eraser & Shapers +158165 - 0x275FA (Hook Control) - True - Shapers & Eraser +158167 - 0x0A3CB (Stars & Eraser & Shapers 3) - 0x09DB4 - Stars & Eraser & Shapers +158168 - 0x0A3CC (Stars & Eraser & Shapers 4) - 0x0A3CB - Stars & Eraser & Shapers +158169 - 0x0A3D0 (Stars & Eraser & Shapers 5) - 0x0A3CC - Stars & Eraser & Shapers -RGB House (Town) - Town - 0x28998: -0x034E4 (Sound Room Left) - True - Sound & Sound Waves -0x034E3 (Sound Room Right) - True - Sound & Sound Dots -0x334D8 (RGB Control) - 0x034E4 & 0x034E3 - Rotated Shapers & RGB & Squares & Colored Squares -0x03C0C (RGB Squares) - 0x334D8 - RGB & Squares & Colored Squares & Black/White Squares -0x03C08 (RGB Stars) - 0x334D8 - RGB & Stars +Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665: +158170 - 0x334DB (Door Timer Outside) - True - True +Door - 0x19B24 (Timed Door) - 0x334DB +158171 - 0x0AC74 (Lower Avoid 6) - 0x0A8DC - Shadows Avoid +158172 - 0x0AC7A (Lower Avoid 7) - 0x0AC74 - Shadows Avoid +158173 - 0x0A8E0 (Lower Avoid 8) - 0x0AC7A - Shadows Avoid +158174 - 0x386FA (Environmental Avoid 1) - 0x0A8E0 - Shadows Avoid & Environment +158175 - 0x1C33F (Environmental Avoid 2) - 0x386FA - Shadows Avoid & Environment +158176 - 0x196E2 (Environmental Avoid 3) - 0x1C33F - Shadows Avoid & Environment +158177 - 0x1972A (Environmental Avoid 4) - 0x196E2 - Shadows Avoid & Environment +158178 - 0x19809 (Environmental Avoid 5) - 0x1972A - Shadows Avoid & Environment +158179 - 0x19806 (Environmental Avoid 6) - 0x19809 - Shadows Avoid & Environment +158180 - 0x196F8 (Environmental Avoid 7) - 0x19806 - Shadows Avoid & Environment +158181 - 0x1972F (Environmental Avoid 8) - 0x196F8 - Shadows Avoid & Environment +Door - 0x194B2 (Laser Room Right Door) - 0x1972F +158182 - 0x19797 (Follow 1) - 0x0A8E0 - Shadows Follow +158183 - 0x1979A (Follow 2) - 0x19797 - Shadows Follow +158184 - 0x197E0 (Follow 3) - 0x1979A - Shadows Follow +158185 - 0x197E8 (Follow 4) - 0x197E0 - Shadows Follow +158186 - 0x197E5 (Follow 5) - 0x197E8 - Shadows Follow +Door - 0x19665 (Laser Room Left Door) - 0x197E5 -Town Tower Top (Town) - Town - 0x28A69 & 0x28B39 & 0x28ACC & 0x28AD9: -0x032F5 (Laser) - True - True +Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF: +158187 - 0x334DC (Door Timer Inside) - True - True +158188 - 0x198B5 (Lower Avoid 1) - True - Shadows Avoid +158189 - 0x198BD (Lower Avoid 2) - 0x198B5 - Shadows Avoid +158190 - 0x198BF (Lower Avoid 3) - 0x198BD & 0x334DC & 0x19B24 - Shadows Avoid +Door - 0x19865 (Barrier to Quarry) - 0x198BF +Door - 0x0A2DF (Barrier to Quarry 2) - 0x198BF +158191 - 0x19771 (Lower Avoid 4) - 0x198BF - Shadows Avoid +158192 - 0x0A8DC (Lower Avoid 5) - 0x19771 - Shadows Avoid +Door - 0x1855B (Barrier to Shadows) - 0x0A8DC +Door - 0x19ADE (Barrier to Shadows 2) - 0x0A8DC -Windmill Interior (Windmill) - Town - 0x17F5F: -0x17D02 (Turn Control) - True - Dots -0x17F89 (Door to Front of Theater) - True - Squares & Black/White Squares +Shadows Laser Room (Shadows): +158703 - 0x19650 (Laser Panel) - True - Shadows Avoid & Shadows Follow +Laser - 0x181B3 (Laser) - 0x19650 -Theater (Theater) - Windmill Interior - 0x17F89: -0x00815 (Video Input) - True - True -0x03553 (Tutorial Video) - 0x00815 & 0x03481 - True -0x03552 (Desert Video) - 0x00815 & 0x0339E - True -0x0354E (Jungle Video) - 0x00815 & 0x03702 - True -0x03549 (Challenge Video) - 0x00815 & 0x2FAF6 - True -0x0354F (Shipwreck Video) - 0x00815 & 0x03535 - True -0x03545 (Mountain Video) - 0x00815 & 0x03542 - True -0x0A168 (Door to Cargo Box Left) - True - Squares & Black/White Squares & Eraser -0x33AB2 (Door to Cargo Box Right) - True - Squares & Black/White Squares & Shapers -0x17CF7 (Discard) - True - Triangles +Keep (Keep) - Main Island - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC: +158193 - 0x00139 (Hedge Maze 1) - True - Environment +158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True +158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Pressure Plates & Dots +Door - 0x01954 (Hedge Maze 1 Exit Door) - 0x00139 +Door - 0x01BEC (Pressure Plates 1 Exit Door) - 0x033EA -Jungle (Jungle) - Main Island - True: -0x17CDF (Shore Boat Spawn) - True - Boat -0x17F9B (Discard) - True - Triangles -0x002C4 (Waves 1) - True - Sound & Sound Waves -0x00767 (Waves 2) - 0x002C4 - Sound & Sound Waves -0x002C6 (Waves 3) - 0x00767 - Sound & Sound Waves -0x0070E (Waves 4) - 0x002C6 - Sound & Sound Waves -0x0070F (Waves 5) - 0x0070E - Sound & Sound Waves -0x0087D (Waves 6) - 0x0070F - Sound & Sound Waves -0x002C7 (Waves 7) - 0x0087D - Sound & Sound Waves -0x17CAB (Popup Wall Control) - 0x002C7 - True -0x0026D (Popup Wall 1) - 0x17CAB - Sound & Sound Dots -0x0026E (Popup Wall 2) - 0x0026D - Sound & Sound Dots -0x0026F (Popup Wall 3) - 0x0026E - Sound & Sound Dots -0x00C3F (Popup Wall 4) - 0x0026F - Sound & Sound Dots -0x00C41 (Popup Wall 5) - 0x00C3F - Sound & Sound Dots -0x014B2 (Popup Wall 6) - 0x00C41 - Sound & Sound Dots -0x03616 (Laser) - 0x014B2 - True -0x337FA (Shortcut to River) - True - True +Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - 0x019D8: +Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139 +158194 - 0x019DC (Hedge Maze 2) - True - Environment +Door - 0x019D8 (Hedge Maze 2 Exit Door) - 0x019DC -Outside Jungle River (River) - Main Island - True - Jungle - 0x337FA: -0x17CAA (Rhombic Avoid to Monastery Garden) - True - Environment -0x15ADD (Vault) - True - Environment & Black/White Squares & Dots -0x03702 (Vault Box) - 0x15ADD - True +Keep 3rd Maze (Keep) - Keep - 0x019B5 - Keep 4th Maze - 0x019E6: +Door - 0x019B5 (Hedge Maze 3 Shortcut) - 0x019DC +158195 - 0x019E7 (Hedge Maze 3) - True - Environment & Sound +Door - 0x019E6 (Hedge Maze 3 Exit Door) - 0x019E7 -Outside Bunker (Bunker) - Main Island - True - Inside Bunker - 0x0A079: -0x17C2E (Door to Bunker) - True - Squares & Black/White Squares -0x09DE0 (Laser) - 0x0A079 - True +Keep 4th Maze (Keep) - Keep - 0x0199A - Keep Tower - 0x01A0E: +Door - 0x0199A (Hedge Maze 4 Shortcut) - 0x019E7 +158196 - 0x01A0F (Hedge Maze 4) - True - Environment +Door - 0x01A0E (Hedge Maze 4 Exit Door) - 0x01A0F -Inside Bunker (Bunker) - Outside Bunker - 0x17C2E: -0x09F7D (Drawn Squares 1) - True - Squares & Colored Squares -0x09FDC (Drawn Squares 2) - 0x09F7D - Squares & Colored Squares & Black/White Squares -0x09FF7 (Drawn Squares 3) - 0x09FDC - Squares & Colored Squares & Black/White Squares -0x09F82 (Drawn Squares 4) - 0x09FF7 - Squares & Colored Squares & Black/White Squares -0x09FF8 (Drawn Squares 5) - 0x09F82 - Squares & Colored Squares & Black/White Squares -0x09D9F (Drawn Squares 6) - 0x09FF8 - Squares & Colored Squares & Black/White Squares -0x09DA1 (Drawn Squares 7) - 0x09D9F - Squares & Colored Squares -0x09DA2 (Drawn Squares 8) - 0x09DA1 - Squares & Colored Squares -0x09DAF (Drawn Squares 9) - 0x09DA2 - Squares & Colored Squares -0x0A099 (Door to Bunker Proper) - 0x09DAF - True -0x0A010 (Drawn Squares through Tinted Glass 1) - 0x0A099 - Squares & Colored Squares & RGB & Environment -0x0A01B (Drawn Squares through Tinted Glass 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares & RGB & Environment -0x0A01F (Drawn Squares through Tinted Glass 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares & RGB & Environment -0x34BC5 (Drop-Down Door Open) - 0x0A01F - True -0x34BC6 (Drop-Down Door Close) - 0x34BC5 - True -0x17E63 (Drop-Down Door Squares 1) - 0x0A01F & 0x34BC5 - Squares & Colored Squares & RGB & Environment -0x17E67 (Drop-Down Door Squares 2) - 0x17E63 & 0x34BC6 - Squares & Colored Squares & Black/White Squares & RGB -0x0A079 (Elevator Control) - 0x17E67 - Squares & Colored Squares & Black/White Squares & RGB +Keep 2nd Pressure Plate (Keep) - Keep 3rd Pressure Plate - 0x01BEA: +158199 - 0x0A3B9 (Reset Pressure Plates 2) - True - True +158200 - 0x01BE9 (Pressure Plates 2) - 0x0A3B9 - Pressure Plates & Stars & Stars + Same Colored Symbol & Squares & Black/White Squares +Door - 0x01BEA (Pressure Plates 2 Exit Door) - 0x01BE9 -Outside Swamp (Swamp) - Main Island - True: -0x0056E (Entry Door) - True - Shapers +Keep 3rd Pressure Plate (Keep) - Keep 4th Pressure Plate - 0x01CD5: +158201 - 0x0A3BB (Reset Pressure Plates 3) - True - True +158202 - 0x01CD3 (Pressure Plates 3) - 0x0A3BB - Pressure Plates & Shapers & Squares & Black/White Squares & Colored Squares +Door - 0x01CD5 (Pressure Plates 3 Exit Door) - 0x01CD3 -Swamp Entry Area (Swamp) - Outside Swamp - 0x0056E: -0x00469 (Seperatable Shapers 1) - True - Shapers -0x00472 (Seperatable Shapers 2) - 0x00469 - Shapers -0x00262 (Seperatable Shapers 3) - 0x00472 - Shapers -0x00474 (Seperatable Shapers 4) - 0x00262 - Shapers -0x00553 (Seperatable Shapers 5) - 0x00474 - Shapers -0x0056F (Seperatable Shapers 6) - 0x00553 - Shapers -0x00390 (Combinable Shapers 1) - 0x0056F - Shapers -0x010CA (Combinable Shapers 2) - 0x00390 - Shapers -0x00983 (Combinable Shapers 3) - 0x010CA - Shapers -0x00984 (Combinable Shapers 4) - 0x00983 - Shapers -0x00986 (Combinable Shapers 5) - 0x00984 - Shapers -0x00985 (Combinable Shapers 6) - 0x00986 - Shapers -0x00987 (Combinable Shapers 7) - 0x00985 - Shapers -0x181A9 (Combinable Shapers 8) - 0x00987 - Shapers -0x00609 (Sliding Bridge) - 0x181A9 - Shapers +Keep 4th Pressure Plate (Keep) - Keep - 0x09E3D - Keep Tower - 0x01D40: +158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True +158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Pressure Plates & Shapers & Dots & Symmetry +Door - 0x01D40 (Pressure Plates 4 Exit Door) - 0x01D3F +158604 - 0x17D27 (Discard) - True - Triangles +158205 - 0x09E49 (Shortcut to Shadows Panel) - True - True +Door - 0x09E3D (Shortcut to Shadows) - 0x09E49 -Swamp Near Platform (Swamp) - Swamp Entry Area - 0x00609 | 0x18488: -0x00999 (Broken Shapers 1) - 0x00990 - Broken Shapers -0x0099D (Broken Shapers 2) - 0x00999 - Broken Shapers -0x009A0 (Broken Shapers 3) - 0x0099D - Broken Shapers -0x009A1 (Broken Shapers 4) - 0x009A0 - Broken Shapers -0x00002 (Cyan Underwater Negative Shapers 1) - 0x00006 - Shapers & Negative Shapers -0x00004 (Cyan Underwater Negative Shapers 2) - 0x00002 - Shapers & Negative Shapers -0x00005 (Cyan Underwater Negative Shapers 3) - 0x00004 - Shapers & Negative Shapers -0x013E6 (Cyan Underwater Negative Shapers 4) - 0x00005 - Shapers & Negative Shapers -0x00596 (Cyan Underwater Negative Shapers 5) - 0x013E6 - Shapers & Negative Shapers -0x18488 (Cyan Underwater Sliding Bridge Control) - 0x00006 - Shapers +Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: +158654 - 0x00AFB (Vault) - True - Symmetry & Sound & Sound Dots & Colored Dots +158655 - 0x03535 (Vault Box) - 0x00AFB - True +158605 - 0x17D28 (Discard) - True - Triangles -Swamp Platform (Swamp) - Swamp Near Platform - True: -0x00982 (Platform Shapers 1) - True - Shapers -0x0097F (Platform Shapers 2) - 0x00982 - Shapers -0x0098F (Platform Shapers 3) - 0x0097F - Shapers -0x00990 (Platform Shapers 4) - 0x0098F - Shapers -0x17C0D (Platform Shortcut Door Left) - True - Shapers -0x17C0E (Platform Shortcut Door Right) - True - Shapers +Keep Tower (Keep) - Keep - 0x04F8F: +158206 - 0x0361B (Tower Shortcut to Keep Panel) - True - True +Door - 0x04F8F (Tower Shortcut to Keep) - 0x0361B +158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F - Environment & Sound +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Squares & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots +Laser - 0x014BB (Laser) - 0x0360E | 0x03317 -Swamp Rotating Bridge Near Side (Swamp) - Swamp Near Platform - 0x009A1: -0x00007 (Rotated Shapers 1) - 0x009A1 - Rotated Shapers -0x00008 (Rotated Shapers 2) - 0x00007 - Rotated Shapers & Shapers -0x00009 (Rotated Shapers 3) - 0x00008 - Rotated Shapers -0x0000A (Rotated Shapers 4) - 0x00009 - Rotated Shapers -0x00001 (Red Underwater Negative Shapers 1) - 0x00596 - Shapers & Negative Shapers -0x014D2 (Red Underwater Negative Shapers 2) - 0x00596 - Shapers & Negative Shapers -0x014D4 (Red Underwater Negative Shapers 3) - 0x00596 - Shapers & Negative Shapers -0x014D1 (Red Underwater Negative Shapers 4) - 0x00596 - Shapers & Negative Shapers +Outside Monastery (Monastery) - Main Island - True - Main Island - 0x0364E - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: +158207 - 0x03713 (Shortcut Door Panel) - True - True +Door - 0x0364E (Shortcut) - 0x03713 +158208 - 0x00B10 (Door Open Left) - True - True +158209 - 0x00C92 (Door Open Right) - True - True +Door - 0x0C128 (Left Door) - 0x00B10 +Door - 0x0C153 (Right Door) - 0x00C92 +158210 - 0x00290 (Rhombic Avoid 1) - 0x09D9B - Environment +158211 - 0x00038 (Rhombic Avoid 2) - 0x09D9B & 0x00290 - Environment +158212 - 0x00037 (Rhombic Avoid 3) - 0x09D9B & 0x00038 - Environment +Door - 0x03750 (Door to Garden) - 0x00037 +158706 - 0x17CA4 (Laser Panel) - 0x193A6 - True +Laser - 0x17C65 (Laser) - 0x17CA4 -Swamp Near Boat (Swamp) - Swamp Rotating Bridge Near Side - 0x0000A - Swamp Platform - 0x17C0D & 0x17C0E: -0x181F5 (Rotating Bridge) - True - Rotated Shapers & Shapers -0x09DB8 (Boat Spawn) - True - Boat -0x003B2 (More Rotated Shapers 1) - 0x0000A - Rotated Shapers -0x00A1E (More Rotated Shapers 2) - 0x003B2 - Rotated Shapers -0x00C2E (More Rotated Shapers 3) - 0x00A1E - Rotated Shapers -0x00E3A (More Rotated Shapers 4) - 0x00C2E - Rotated Shapers -0x009A6 (Underwater Back Optional) - 0x00E3A & 0x181F5 - Shapers -0x009AB (Blue Underwater Negative Shapers 1) - 0x00E3A - Shapers & Negative Shapers -0x009AD (Blue Underwater Negative Shapers 2) - 0x009AB - Shapers & Negative Shapers -0x009AE (Blue Underwater Negetive Shapers 3) - 0x009AD - Shapers & Negative Shapers -0x009AF (Blue Underwater Negative Shapers 4) - 0x009AE - Shapers & Negative Shapers -0x00006 (Blue Underwater Negative Shapers 5) - 0x009AF - Shapers & Negative Shapers & Broken Negative Shapers -0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers +Inside Monastery (Monastery): +158213 - 0x09D9B (Overhead Door Control) - True - Dots +158214 - 0x193A7 (Branch Avoid 1) - 0x00037 - Environment +158215 - 0x193AA (Branch Avoid 2) - 0x193A7 - Environment +158216 - 0x193AB (Branch Follow 1) - 0x193AA - Environment +158217 - 0x193A6 (Branch Follow 2) - 0x193AB - Environment -Swamp Maze (Swamp) - Swamp Rotating Bridge Near Side - 0x00001 & 0x014D2 & 0x014D4 & 0x014D1 - Outside Swamp - 0x17C05 & 0x17C02: -0x17C0A (Maze Control) - True - Shapers & Negative Shapers & Rotated Shapers & Environment -0x17E07 (Maze Control Other Side) - True - Shapers & Negative Shapers & Rotated Shapers & Environment -0x03615 (Laser) - 0x17C0A & 0x17E07 - True -0x17C05 (Near Laser Shortcut Door Left) - True - Rotated Shapers -0x17C02 (Near Laser Shortcut Door Right) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers +Monastery Garden (Monastery): -Treehouse Entry Area (Treehouse): -0x17C95 (Boat Spawn) - True - Boat -0x0288C (First Door) - True - Stars -0x02886 (Second Door) - 0x0288C - Stars -0x17D72 (Yellow Bridge 1) - 0x02886 - Stars -0x17D8F (Yellow Bridge 2) - 0x17D72 - Stars -0x17D74 (Yellow Bridge 3) - 0x17D8F - Stars -0x17DAC (Yellow Bridge 4) - 0x17D74 - Stars -0x17D9E (Yellow Bridge 5) - 0x17DAC - Stars -0x17DB9 (Yellow Bridge 6) - 0x17D9E - Stars -0x17D9C (Yellow Bridge 7) - 0x17DB9 - Stars -0x17DC2 (Yellow Bridge 8) - 0x17D9C - Stars -0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars -0x0A182 (Beyond Yellow Bridge Door) - 0x17DC4 - Stars +Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: +158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat +158219 - 0x0A0C8 (Cargo Box Panel) - True - Squares & Black/White Squares & Shapers +Door - 0x0A0C9 (Cargo Box Door) - 0x0A0C8 +158707 - 0x09F98 (Desert Laser Redirect) - True - True +158220 - 0x18590 (Tree Outlines) - True - Symmetry & Environment +158221 - 0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment +158222 - 0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment +158223 - 0x079DF (Triple Environmental Puzzle) - 0x28938 - Shadows Avoid & Environment & Reflection +158235 - 0x2899C (Full Dot Grid Shapers 1) - True - Rotated Shapers & Dots +158236 - 0x28A33 (Full Dot Grid Shapers 2) - 0x2899C - Shapers & Dots +158237 - 0x28ABF (Full Dot Grid Shapers 3) - 0x28A33 - Shapers & Rotated Shapers & Dots +158238 - 0x28AC0 (Full Dot Grid Shapers 4) - 0x28ABF - Rotated Shapers & Dots +158239 - 0x28AC1 (Full Dot Grid Shapers 5) - 0x28AC0 - Rotated Shapers & Dots +Door - 0x034F5 (Wooden Roof Staircase) - 0x28AC1 +158225 - 0x28998 (Tinted Door Panel) - True - Stars & Rotated Shapers +Door - 0x28A61 (Tinted Door to RGB House) - 0x28998 +158226 - 0x28A0D (Door to Church Stars Panel) - 0x28998 - Stars & RGB & Environment +Door - 0x03BB0 (Door to Church) - 0x28A0D +158228 - 0x28A79 (Maze Stair Control) - True - Environment +Door - 0x28AA2 (Maze Staircase) - 0x28A79 +158241 - 0x17F5F (Windmill Door Panel) - True - Dots +Door - 0x1845B (Windmill Door) - 0x17F5F -Treehouse Beyond Yellow Bridge (Treehouse) - Treehouse Entry Area - 0x0A182: -0x2700B (Laser House Door Timer Outside Control) - True - True -0x17DC8 (First Purple Bridge 1) - True - Stars & Dots -0x17DC7 (First Purple Bridge 2) - 0x17DC8 - Stars & Dots -0x17CE4 (First Purple Bridge 3) - 0x17DC7 - Stars & Dots -0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Dots -0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Dots -0x17D9B (Second Purple Bridge 1) - 0x17D6C - Stars & Squares & Black/White Squares -0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Squares & Black/White Squares -0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Squares & Black/White Squares -0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Squares & Black/White Squares & Colored Squares -0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Squares & Colored Squares -0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Squares & Colored Squares -0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Squares & Colored Squares -0x17E3C (Green Bridge 1) - True - Stars & Shapers -0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers -0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Rotated Shapers -0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers & Environment -0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol -0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Colored Shapers & Negative Shapers & Colored Negative Shapers & Stars + Same Colored Symbol -0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers -0x17FA9 (Green Bridge Discard) - 0x17E61 - Triangles -0x17DB3 (Left Orange Bridge 1) - 0x17DC6 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol -0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol -0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol -0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Environment -0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17FA0 (Burned House Discard) - 0x17DDB - Triangles -0x17D88 (Right Orange Bridge 1) - True - Stars -0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars -0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars -0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Stars & Environment -0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars -0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars -0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars -0x17DCA (Right Orange Bridge 8) - 0x17DCC - Stars -0x17D8E (Right Orange Bridge 9) - 0x17DCA - Stars -0x17DB7 (Right Orange Bridge 10 & Directional) - 0x17D8E - Stars -0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars -0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars +Town Inside Cargo Box (Town): +158606 - 0x17D01 (Cargo Box Discard) - True - Triangles -Treehouse Laser Room (Treehouse) - Treehouse Beyond Yellow Bridge - 0x2700B & 0x17DA2 & 0x17DDB: -0x03613 (Laser) - True - True -0x17CBC (Laser House Door Timer Inside Control) - True - True +Town Maze Rooftop (Town) - Town Red Rooftop - 0x2896A: +158229 - 0x2896A (Maze Rooftop Bridge Control) - True - Shapers -Treehouse Bridge Platform (Treehouse) - Treehouse Beyond Yellow Bridge - 0x17DA2 - Main Island - 0x037FF: -0x037FF (Bridge Control) - True - Stars +Town Red Rooftop (Town): +158607 - 0x17C71 (Rooftop Discard) - True - Triangles +158230 - 0x28AC7 (Symmetry Squares 1) - True - Symmetry & Squares & Black/White Squares +158231 - 0x28AC8 (Symmetry Squares 2) - 0x28AC7 - Symmetry & Squares & Black/White Squares +158232 - 0x28ACA (Symmetry Squares 3 + Dots) - 0x28AC8 - Symmetry & Squares & Black/White Squares & Dots +158233 - 0x28ACB (Symmetry Squares 4 + Dots) - 0x28ACA - Symmetry & Squares & Black/White Squares & Dots +158234 - 0x28ACC (Symmetry Squares 5 + Dots) - 0x28ACB - Symmetry & Squares & Black/White Squares & Dots +158224 - 0x28B39 (Hexagonal Reflection) - 0x079DF - Reflection -Mountaintop (Mountaintop) - Main Island - True: -0x0042D (River Shape) - True - True -0x09F7F (Box Short) - 7 Lasers - True -0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True -0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17C42 (Discard) - True - Triangles -0x002A6 (Vault) - True - Symmetry & Colored Dots & Squares & Black/White Squares & Dots -0x03542 (Vault Box) - 0x002A6 - True +Town Wooden Rooftop (Town): +158240 - 0x28AD9 (Shapers & Dots & Eraser) - 0x28AC1 - Rotated Shapers & Dots & Eraser -Inside Mountain Top Layer (Inside Mountain) - Mountaintop - 0x17C34: -0x09E39 (Light Bridge Controller) - True - Squares & Black/White Squares & Colored Squares & Eraser & Colored Eraser +Town Church (Town): +158227 - 0x28A69 (Church Lattice) - 0x03BB0 - Environment -Inside Mountain Top Layer Bridge (Inside Mountain) - Inside Mountain Top Layer - 0x09E39: -0x09E7A (Obscured Vision 1) - True - Obscured & Squares & Black/White Squares & Dots -0x09E71 (Obscured Vision 2) - 0x09E7A - Obscured & Squares & Black/White Squares & Dots -0x09E72 (Obscured Vision 3) - 0x09E71 - Obscured & Squares & Black/White Squares & Shapers & Dots -0x09E69 (Obscured Vision 4) - 0x09E72 - Obscured & Squares & Black/White Squares & Dots -0x09E7B (Obscured Vision 5) - 0x09E69 - Obscured & Squares & Black/White Squares & Dots -0x09E73 (Moving Background 1) - True - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x09E75 (Moving Background 2) - 0x09E73 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x09E78 (Moving Background 3) - 0x09E75 - Moving & Shapers -0x09E79 (Moving Background 4) - 0x09E78 - Moving & Shapers & Rotated Shapers -0x09E6C (Moving Background 5) - 0x09E79 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x09E6F (Moving Background 6) - 0x09E6C - Moving & Stars & Rotated Shapers & Shapers -0x09E6B (Moving Background 7) - 0x09E6F - Moving & Stars & Dots -0x33AF5 (Physically Obstructed 1) - True - Squares & Black/White Squares & Environment & Symmetry -0x33AF7 (Physically Obstructed 2) - 0x33AF5 - Squares & Black/White Squares & Stars & Environment -0x09F6E (Physically Obstructed 3) - 0x33AF7 - Symmetry & Dots & Environment -0x09EAD (Angled Inside Trash 1) - True - Squares & Black/White Squares & Shapers & Angled -0x09EAF (Angled Inside Trash 2) - 0x09EAD - Squares & Black/White Squares & Shapers & Angled +RGB House (Town) - RGB Room - 0x2897B: +158242 - 0x034E4 (Sound Room Left) - True - Sound & Sound Waves +158243 - 0x034E3 (Sound Room Right) - True - Sound & Sound Dots +Door - 0x2897B (RGB House Staircase) - 0x034E4 & 0x034E3 -Inside Mountain Second Layer (Inside Mountain) - Inside Mountain Top Layer Bridge - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B: -0x09FD3 (Color Cycle 1) - True - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x09FD4 (Color Cycle 2) - 0x09FD3 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x09FD6 (Color Cycle 3) - 0x09FD4 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x09FD7 (Color Cycle 4) - 0x09FD6 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Shapers & Colored Shapers -0x09FD8 (Color Cycle 5) - 0x09FD7 - Color Cycle & RGB & Squares & Colored Squares & Symmetry & Colored Dots -0x09E86 (Light Bridge Controller 2) - 0x09FD8 - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines +RGB Room (Town): +158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & RGB & Squares & Colored Squares +158245 - 0x03C0C (RGB Squares) - 0x334D8 - RGB & Squares & Colored Squares & Black/White Squares +158246 - 0x03C08 (RGB Stars) - 0x334D8 - RGB & Stars -Inside Mountain Second Layer Beyond Bridge (Inside Mountain) - Inside Mountain Second Layer - 0x09E86: -0x09FCC (Same Solution 1) - True - Dots & Same Solution -0x09FCE (Same Solution 2) - 0x09FCC - Squares & Black/White Squares & Same Solution -0x09FCF (Same Solution 3) - 0x09FCE - Stars & Same Solution -0x09FD0 (Same Solution 4) - 0x09FCF - Rotated Shapers & Same Solution -0x09FD1 (Same Solution 5) - 0x09FD0 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Same Solution -0x09FD2 (Same Solution 6) - 0x09FD1 - Shapers & Same Solution -0x09ED8 (Light Bridge Controller 3) - 0x09FD2 - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines +Town Tower (Town Tower) - Town - True - Town Tower Top - 0x27798 & 0x27799 & 0x2779A & 0x2779C: +Door - 0x27798 (Blue Panels Door) - 0x28ACC +Door - 0x27799 (Church Lattice Door) - 0x28A69 +Door - 0x2779A (Environmental Set Door) - 0x28B39 +Door - 0x2779C (Eraser Set Door) - 0x28AD9 -Inside Mountain Second Layer Elevator (Inside Mountain) - Inside Mountain Second Layer - 0x09ED8 & 0x09E86: -0x09EEB (Elevator Control Panel) - True - Dots -0x17F93 (Elevator Discard) - True - Triangles +Town Tower Top (Town): +158708 - 0x032F5 (Laser Panel) - True - True +Laser - 0x032F9 (Laser) - 0x032F5 -Inside Mountain Third Layer (Inside Mountain) - Inside Mountain Second Layer Elevator - 0x09EEB: -0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser -0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser -0x09F01 (Giant Puzzle Top Right) - True - Rotated Shapers -0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser -0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +Windmill Interior (Windmill) - Theater - 0x17F88: +158247 - 0x17D02 (Turn Control) - True - Dots +158248 - 0x17F89 (Door to Front of Theater Panel) - True - Squares & Black/White Squares +Door - 0x17F88 (Door to Front of Theater) - 0x17F89 -Inside Mountain Bottom Layer (Inside Mountain) - Inside Mountain Third Layer - 0x09FDA - Inside Mountain Path to Secret Area - 0x334E1: -0x17FA2 (Bottom Layer Discard) - 0xFFF00 - Triangles & Environment -0x01983 (Door to Final Room Left) - True - Shapers & Stars -0x01987 (Door to Final Room Right) - True - Squares & Colored Squares & Dots +Theater (Theater) - Town - 0x0A16D | 0x3CCDF: +158656 - 0x00815 (Video Input) - True - True +158657 - 0x03553 (Tutorial Video) - 0x00815 & 0x03481 - True +158658 - 0x03552 (Desert Video) - 0x00815 & 0x0339E - True +158659 - 0x0354E (Jungle Video) - 0x00815 & 0x03702 - True +158660 - 0x03549 (Challenge Video) - 0x00815 & 0x0356B - True +158661 - 0x0354F (Shipwreck Video) - 0x00815 & 0x03535 - True +158662 - 0x03545 (Mountain Video) - 0x00815 & 0x03542 - True +158249 - 0x0A168 (Door to Cargo Box Left Panel) - True - Squares & Black/White Squares & Eraser +158250 - 0x33AB2 (Door to Cargo Box Right Panel) - True - Squares & Black/White Squares & Shapers +Door - 0x0A16D (Door to Cargo Box Left) - 0x0A168 +Door - 0x3CCDF (Door to Cargo Box Right) - 0x33AB2 +158608 - 0x17CF7 (Discard) - True - Triangles -Inside Mountain Path to Secret Area (Inside Mountain) - Inside Mountain Bottom Layer - 0x17FA2: -0x00FF8 (Door to Secret Area) - True - Triangles & Black/White Squares & Squares -0x334E1 (Rock Control) - True - True +Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF: +158251 - 0x17CDF (Shore Boat Spawn) - True - Boat +158609 - 0x17F9B (Discard) - True - Triangles +158252 - 0x002C4 (Waves 1) - True - Sound & Sound Waves +158253 - 0x00767 (Waves 2) - 0x002C4 - Sound & Sound Waves +158254 - 0x002C6 (Waves 3) - 0x00767 - Sound & Sound Waves +158255 - 0x0070E (Waves 4) - 0x002C6 - Sound & Sound Waves +158256 - 0x0070F (Waves 5) - 0x0070E - Sound & Sound Waves +158257 - 0x0087D (Waves 6) - 0x0070F - Sound & Sound Waves +158258 - 0x002C7 (Waves 7) - 0x0087D - Sound & Sound Waves +158259 - 0x17CAB (Popup Wall Control) - 0x002C7 - True +Door - 0x1475B (Popup Wall) - 0x17CAB +158260 - 0x0026D (Popup Wall 1) - 0x1475B - Sound & Sound Dots +158261 - 0x0026E (Popup Wall 2) - 0x0026D - Sound & Sound Dots +158262 - 0x0026F (Popup Wall 3) - 0x0026E - Sound & Sound Dots +158263 - 0x00C3F (Popup Wall 4) - 0x0026F - Sound & Sound Dots +158264 - 0x00C41 (Popup Wall 5) - 0x00C3F - Sound & Sound Dots +158265 - 0x014B2 (Popup Wall 6) - 0x00C41 - Sound & Sound Dots +158709 - 0x03616 (Laser Panel) - 0x014B2 - True +Laser - 0x00274 (Laser) - 0x03616 +158266 - 0x337FA (Shortcut to River Panel) - True - True +Door - 0x3873B (Shortcut to River) - 0x337FA -Inside Mountain Secret Area (Inside Mountain Secret Area) - Inside Mountain Path to Secret Area - 0x00FF8 - Main Island - 0x021D7 - Main Island - 0x17CF2: -0x021D7 (Shortcut to Mountain) - True - Triangles & Stars & Stars + Same Colored Symbol & Colored Triangles -0x17CF2 (Shortcut to Swamp) - True - Triangles -0x335AB (Elevator Inside Control) - True - Dots & Squares & Black/White Squares -0x335AC (Elevator Upper Outside Control) - 0x335AB - Squares & Black/White Squares -0x3369D (Elevator Lower Outside Control) - 0x335AB - Squares & Black/White Squares & Dots -0x00190 (Dot Grid Triangles 1) - True - Dots & Triangles -0x00558 (Dot Grid Triangles 2) - 0x00190 - Dots & Triangles -0x00567 (Dot Grid Triangles 3) - 0x00558 - Dots & Triangles -0x006FE (Dot Grid Triangles 4) - 0x00567 - Dots & Triangles -0x01A0D (Symmetry Triangles) - True - Symmetry & Triangles -0x008B8 (Squares and Triangles) - True - Squares & Black/White Squares & Triangles -0x00973 (Stars and Triangles) - 0x008B8 - Stars & Triangles -0x0097B (Stars and Triangles of same color) - 0x00973 - Stars & Triangles & Stars and Triangles of same color & Stars + Same Colored Symbol -0x0097D (Stars & Squares and Triangles) - 0x0097B - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Triangles -0x0097E (Stars & Squares and Triangles 2) - 0x0097D - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Stars and Triangles of same color -0x00994 (Rotated Shapers and Triangles 1) - True - Rotated Shapers & Triangles -0x334D5 (Rotated Shapers and Triangles 2) - 0x00994 - Rotated Shapers & Triangles -0x00995 (Rotated Shapers and Triangles 3) - 0x334D5 - Rotated Shapers & Triangles -0x00996 (Shapers and Triangles 1) - 0x00995 - Shapers & Triangles -0x00998 (Shapers and Triangles 2) - 0x00996 - Shapers & Triangles -0x009A4 (Broken Shapers) - True - Shapers & Broken Shapers -0x018A0 (Symmetry Shapers) - True - Shapers & Symmetry -0x00A72 (Broken and Negative Shapers) - True - Shapers & Broken Shapers & Negative Shapers -0x32962 (Rotated Broken Shapers) - True - Rotated Shapers & Broken Rotated Shapers -0x32966 (Stars and Squares) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x01A31 (Rainbow Squares) - True - Color Cycle & RGB & Squares & Colored Squares -0x00B71 (Squares & Stars and Colored Eraser) - True - Colored Eraser & Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Eraser -0x09DD5 (Lone Pillar) - True - Pillar & Triangles -0x0A16E (Door to Challenge) - 0x09DD5 - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol -0x288EA (Wooden Beam Shapers) - True - Environment & Shapers -0x288FC (Wooden Beam Squares and Shapers) - True - Environment & Squares & Black/White Squares & Shapers & Rotated Shapers -0x289E7 (Wooden Beam Stars and Squares) - True - Environment & Stars & Squares & Black/White Squares -0x288AA (Wooden Beam Shapers and Stars) - True - Environment & Stars & Shapers -0x17FB9 (Upstairs Dot Grid Negative Shapers) - True - Shapers & Dots & Negative Shapers -0x0A16B (Upstairs Dot Grid Gap Dots) - True - Dots -0x0A2CE (Upstairs Dot Grid Stars) - 0x0A16B - Stars & Dots -0x0A2D7 (Upstairs Dot Grid Stars & Squares) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars -0x0A2DD (Upstairs Dot Grid Shapers) - 0x0A2D7 - Shapers & Dots -0x0A2EA (Upstairs Dot Grid Rotated Shapers) - 0x0A2DD - Rotated Shapers & Dots -0x0008F (Upstairs Invisible Dots 1) - True - Dots & Invisible Dots -0x0006B (Upstairs Invisible Dots 2) - 0x0008F - Dots & Invisible Dots -0x0008B (Upstairs Invisible Dots 3) - 0x0006B - Dots & Invisible Dots -0x0008C (Upstairs Invisible Dots 4) - 0x0008B - Dots & Invisible Dots -0x0008A (Upstairs Invisible Dots 5) - 0x0008C - Dots & Invisible Dots -0x00089 (Upstairs Invisible Dots 6) - 0x0008A - Dots & Invisible Dots -0x0006A (Upstairs Invisible Dots 7) - 0x00089 - Dots & Invisible Dots -0x0006C (Upstairs Invisible Dots 8) - 0x0006A - Dots & Invisible Dots -0x00027 (Upstairs Invisible Dot Symmetry 1) - True - Dots & Invisible Dots & Symmetry -0x00028 (Upstairs Invisible Dot Symmetry 2) - 0x00027 - Dots & Invisible Dots & Symmetry -0x00029 (Upstairs Invisible Dot Symmetry 3) - 0x00028 - Dots & Invisible Dots & Symmetry +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: +158267 - 0x17CAA (Rhombic Avoid to Monastery Garden) - True - Environment +Door - 0x0CF2A (Shortcut to Monastery Garden) - 0x17CAA +158663 - 0x15ADD (Vault) - True - Environment & Black/White Squares & Dots +158664 - 0x03702 (Vault Box) - 0x15ADD - True -Challenge (Challenge) - Inside Mountain Secret Area - 0x0A16E: -0x0A332 (Start Timer) - 11 Lasers - True -0x0088E (Small Basic) - 0x0A332 - True -0x00BAF (Big Basic) - 0x0088E - True -0x00BF3 (Square) - 0x00BAF - Squares & Black/White Squares -0x00C09 (Maze Map) - 0x00BF3 - Dots -0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots -0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots -0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers -0x00CD4 (Big Basic 2) - 0x00524 - True -0x00CB9 (Choice Squares Right) - 0x00CD4 - Squares & Black/White Squares -0x00CA1 (Choice Squares Middle) - 0x00CD4 - Squares & Black/White Squares -0x00C80 (Choice Squares Left) - 0x00CD4 - Squares & Black/White Squares -0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles -0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles -0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry & Pillar -0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Squares & Black/White Squares & Symmetry & Pillar -0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True -0x039B4 (Door to Theater Walkway) - True - Triangles +Outside Bunker (Bunker) - Main Island - True - Inside Bunker - 0x0C2A4: +158268 - 0x17C2E (Bunker Entry Panel) - True - Squares & Black/White Squares & Colored Squares +Door - 0x0C2A4 (Bunker Entry Door) - 0x17C2E -Theater Walkway (Theater Walkway) - Challenge - 0x039B4 - Theater - 0x27732 - Desert Elevator Room - 0x2773D & 0x03608 - Town - 0x09E85: -0x2FAF6 (Vault Box) - True - True -0x27732 (Door to Back of Theater) - True - True -0x2773D (Door to Desert Elevator Room) - True - True -0x09E85 (Door to Town) - True - Triangles +Inside Bunker (Bunker) - Inside Bunker Glass Room - 0x17C79: +158269 - 0x09F7D (Drawn Squares 1) - True - Squares & Colored Squares +158270 - 0x09FDC (Drawn Squares 2) - 0x09F7D - Squares & Colored Squares & Black/White Squares +158271 - 0x09FF7 (Drawn Squares 3) - 0x09FDC - Squares & Colored Squares & Black/White Squares +158272 - 0x09F82 (Drawn Squares 4) - 0x09FF7 - Squares & Colored Squares & Black/White Squares +158273 - 0x09FF8 (Drawn Squares 5) - 0x09F82 - Squares & Colored Squares & Black/White Squares +158274 - 0x09D9F (Drawn Squares 6) - 0x09FF8 - Squares & Colored Squares & Black/White Squares +158275 - 0x09DA1 (Drawn Squares 7) - 0x09D9F - Squares & Colored Squares +158276 - 0x09DA2 (Drawn Squares 8) - 0x09DA1 - Squares & Colored Squares +158277 - 0x09DAF (Drawn Squares 9) - 0x09DA2 - Squares & Colored Squares +158278 - 0x0A099 (Door to Bunker Proper Panel) - 0x09DAF - True +Door - 0x17C79 (Door to Bunker Proper) - 0x0A099 -Final Room (Inside Mountain Final Room) - Inside Mountain Bottom Layer - 0x01983 & 0x01987: -0x0383A (Stars Pillar) - True - Stars & Pillar -0x09E56 (Stars and Dots Pillar) - 0x0383A - Stars & Dots & Pillar -0x09E5A (Dot Grid Pillar) - 0x09E56 - Dots & Pillar -0x33961 (Sparse Dots Pillar) - 0x09E5A - Dots & Symmetry & Pillar -0x0383D (Dot Maze Pillar) - True - Dots & Pillar -0x0383F (Squares Pillar) - 0x0383D - Squares & Black/White Squares & Pillar -0x03859 (Shapers Pillar) - 0x0383F - Shapers & Pillar -0x339BB (Squares and Stars) - 0x03859 - Squares & Black/White Squares & Stars & Symmetry & Pillar +Inside Bunker Glass Room (Bunker) - Inside Bunker Ultraviolet Room - 0x0C2A3: +158279 - 0x0A010 (Drawn Squares through Tinted Glass 1) - True - Squares & Colored Squares & RGB & Environment +158280 - 0x0A01B (Drawn Squares through Tinted Glass 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares & RGB & Environment +158281 - 0x0A01F (Drawn Squares through Tinted Glass 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares & RGB & Environment +Door - 0x0C2A3 (Door to Ultraviolet Room) - 0x0A01F -Elevator (Inside Mountain Final Room) - Final Room - 0x339BB & 0x33961: -0x3D9A6 (Elevator Door Closer Left) - True - True -0x3D9A7 (Elevator Door Close Right) - True - True -0x3C113 (Elevator Door Open Left) - 0x3D9A6 | 0x3D9A7 - True -0x3C114 (Elevator Door Open Right) - 0x3D9A6 | 0x3D9A7 - True -0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True -0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True -0x3D9A9 (Elevator Start) - 0x3D9AA | 0x3D9A8 - True +Inside Bunker Ultraviolet Room (Bunker) - Inside Bunker Elevator Section - 0x0A08D: +158282 - 0x34BC5 (Drop-Down Door Open) - True - True +158283 - 0x34BC6 (Drop-Down Door Close) - 0x34BC5 - True +158284 - 0x17E63 (Drop-Down Door Squares 1) - 0x34BC5 - Squares & Colored Squares & RGB & Environment +158285 - 0x17E67 (Drop-Down Door Squares 2) - 0x17E63 & 0x34BC6 - Squares & Colored Squares & Black/White Squares & RGB +Door - 0x0A08D (Door to Elevator) - 0x17E67 -Boat (Boat) - Main Island - 0x17CDF | 0x17CC8 & 0x0005C | 0x17CA6 | 0x09DB8 | 0x17C95 | 0x0A054 - Inside Glass Factory - 0x17CDF & 0x0005C | 0x17CC8 & 0x0005C | 0x17CA6 & 0x0005C | 0x09DB8 & 0x0005C | 0x17C95 & 0x0005C | 0x0A054 & 0x0005C - Quarry Boathouse - 0x17CA6 - Swamp Near Boat - 0x17CDF | 0x17CC8 & 0x0005C | 0x17CA6 | 0x09DB8 | 0x17C95 | 0x0A054 - Treehouse Entry Area - 0x17CDF | 0x17CC8 & 0x0005C | 0x17CA6 | 0x09DB8 | 0x17C95 | 0x0A054: +Inside Bunker Elevator Section (Bunker) - Bunker Laser Platform - 0x0A079: +158286 - 0x0A079 (Elevator Control) - True - Squares & Colored Squares & Black/White Squares & RGB + +Bunker Laser Platform (Bunker): +158710 - 0x09DE0 (Laser Panel) - True - True +Laser - 0x0C2B2 (Laser) - 0x09DE0 + +Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True: +158287 - 0x0056E (Entry Panel) - True - Shapers +Door - 0x00C1C (Entry Door) - 0x0056E + +Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: +158288 - 0x00469 (Seperatable Shapers 1) - True - Shapers +158289 - 0x00472 (Seperatable Shapers 2) - 0x00469 - Shapers +158290 - 0x00262 (Seperatable Shapers 3) - 0x00472 - Shapers +158291 - 0x00474 (Seperatable Shapers 4) - 0x00262 - Shapers +158292 - 0x00553 (Seperatable Shapers 5) - 0x00474 - Shapers +158293 - 0x0056F (Seperatable Shapers 6) - 0x00553 - Shapers +158294 - 0x00390 (Combinable Shapers 1) - 0x0056F - Shapers +158295 - 0x010CA (Combinable Shapers 2) - 0x00390 - Shapers +158296 - 0x00983 (Combinable Shapers 3) - 0x010CA - Shapers +158297 - 0x00984 (Combinable Shapers 4) - 0x00983 - Shapers +158298 - 0x00986 (Combinable Shapers 5) - 0x00984 - Shapers +158299 - 0x00985 (Combinable Shapers 6) - 0x00986 - Shapers +158300 - 0x00987 (Combinable Shapers 7) - 0x00985 - Shapers +158301 - 0x181A9 (Combinable Shapers 8) - 0x00987 - Shapers + +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +158302 - 0x00609 (Sliding Bridge) - True - Shapers + +Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Broken Shapers - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: +158313 - 0x00982 (Platform Shapers 1) - True - Shapers +158314 - 0x0097F (Platform Shapers 2) - 0x00982 - Shapers +158315 - 0x0098F (Platform Shapers 3) - 0x0097F - Shapers +158316 - 0x00990 (Platform Shapers 4) - 0x0098F - Shapers +Door - 0x184B7 (Door to Broken Shapers) - 0x00990 +158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Shapers +Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E +Door - 0x04B7F (Cyan Water Pump) - 0x00006 + +Swamp Cyan Underwater (Swamp): +158307 - 0x00002 (Cyan Underwater Negative Shapers 1) - True - Shapers & Negative Shapers +158308 - 0x00004 (Cyan Underwater Negative Shapers 2) - 0x00002 - Shapers & Negative Shapers +158309 - 0x00005 (Cyan Underwater Negative Shapers 3) - 0x00004 - Shapers & Negative Shapers +158310 - 0x013E6 (Cyan Underwater Negative Shapers 4) - 0x00005 - Shapers & Negative Shapers +158311 - 0x00596 (Cyan Underwater Negative Shapers 5) - 0x013E6 - Shapers & Negative Shapers +158312 - 0x18488 (Cyan Underwater Sliding Bridge Control) - True - Shapers + +Swamp Broken Shapers (Swamp) - Swamp Rotated Shapers - 0x18507: +158303 - 0x00999 (Broken Shapers 1) - 0x00990 - Shapers & Broken Shapers +158304 - 0x0099D (Broken Shapers 2) - 0x00999 - Shapers & Broken Shapers +158305 - 0x009A0 (Broken Shapers 3) - 0x0099D - Shapers & Broken Shapers +158306 - 0x009A1 (Broken Shapers 4) - 0x009A0 - Shapers & Broken Shapers +Door - 0x18507 (Door to Rotated Shapers) - 0x009A1 + +Swamp Rotated Shapers (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotating Bridge - TrueOneWay: +158319 - 0x00007 (Rotated Shapers 1) - 0x009A1 - Rotated Shapers +158320 - 0x00008 (Rotated Shapers 2) - 0x00007 - Rotated Shapers & Shapers +158321 - 0x00009 (Rotated Shapers 3) - 0x00008 - Rotated Shapers +158322 - 0x0000A (Rotated Shapers 4) - 0x00009 - Rotated Shapers +Door - 0x183F2 (Red Water Pump) - 0x00596 + +Swamp Red Underwater (Swamp) - Swamp Maze - 0x014D1: +158323 - 0x00001 (Red Underwater Negative Shapers 1) - True - Shapers & Negative Shapers +158324 - 0x014D2 (Red Underwater Negative Shapers 2) - True - Shapers & Negative Shapers +158325 - 0x014D4 (Red Underwater Negative Shapers 3) - True - Shapers & Negative Shapers +158326 - 0x014D1 (Red Underwater Negative Shapers 4) - True - Shapers & Negative Shapers +Door - 0x305D5 (Red Underwater Exit) - 0x014D1 + +Swamp Rotating Bridge (Swamp) - Swamp Rotated Shapers - 0x181F5 - Swamp Near Boat - 0x181F5 - Swamp Purple Area - 0x181F5: +158327 - 0x181F5 (Rotating Bridge) - True - Rotated Shapers & Shapers + +Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482: +158328 - 0x09DB8 (Boat Spawn) - True - Boat +158329 - 0x003B2 (More Rotated Shapers 1) - 0x0000A - Rotated Shapers +158330 - 0x00A1E (More Rotated Shapers 2) - 0x003B2 - Rotated Shapers +158331 - 0x00C2E (More Rotated Shapers 3) - 0x00A1E - Rotated Shapers +158332 - 0x00E3A (More Rotated Shapers 4) - 0x00C2E - Rotated Shapers +158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers +Door - 0x18482 (Blue Water Pump) - 0x00E3A + +Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6: +Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A + +Swamp Purple Underwater (Swamp): +158333 - 0x009A6 (Underwater Back Optional) - True - Shapers + +Swamp Blue Underwater (Swamp): +158334 - 0x009AB (Blue Underwater Negative Shapers 1) - True - Shapers & Negative Shapers +158335 - 0x009AD (Blue Underwater Negative Shapers 2) - 0x009AB - Shapers & Negative Shapers +158336 - 0x009AE (Blue Underwater Negetive Shapers 3) - 0x009AD - Shapers & Negative Shapers +158337 - 0x009AF (Blue Underwater Negative Shapers 4) - 0x009AE - Shapers & Negative Shapers +158338 - 0x00006 (Blue Underwater Negative Shapers 5) - 0x009AF - Shapers & Negative Shapers & Broken Negative Shapers + +Swamp Maze (Swamp) - Swamp Laser Area - 0x17C0A & 0x17E07: +158340 - 0x17C0A (Maze Control) - True - Shapers & Negative Shapers & Rotated Shapers & Environment +158112 - 0x17E07 (Maze Control Other Side) - True - Shapers & Negative Shapers & Rotated Shapers & Environment + +Swamp Laser Area (Swamp) - Outside Swamp - 0x2D880: +158711 - 0x03615 (Laser Panel) - True - True +Laser - 0x00BF6 (Laser) - 0x03615 +158341 - 0x17C05 (Near Laser Shortcut Left Panel) - True - Rotated Shapers +158342 - 0x17C02 (Near Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers +Door - 0x2D880 (Near Laser Shortcut) - 0x17C02 + +Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: +158343 - 0x17C95 (Boat Spawn) - True - Boat +158344 - 0x0288C (First Door Panel) - True - Stars +Door - 0x0C309 (First Door) - 0x0288C + +Treehouse Between Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310: +158345 - 0x02886 (Second Door Panel) - True - Stars +Door - 0x0C310 (Second Door) - 0x02886 + +Treehouse Yellow Bridge (Treehouse) - Treehouse After Yellow Bridge - 0x17DC4: +158346 - 0x17D72 (Yellow Bridge 1) - True - Stars +158347 - 0x17D8F (Yellow Bridge 2) - 0x17D72 - Stars +158348 - 0x17D74 (Yellow Bridge 3) - 0x17D8F - Stars +158349 - 0x17DAC (Yellow Bridge 4) - 0x17D74 - Stars +158350 - 0x17D9E (Yellow Bridge 5) - 0x17DAC - Stars +158351 - 0x17DB9 (Yellow Bridge 6) - 0x17D9E - Stars +158352 - 0x17D9C (Yellow Bridge 7) - 0x17DB9 - Stars +158353 - 0x17DC2 (Yellow Bridge 8) - 0x17D9C - Stars +158354 - 0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars + +Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: +158355 - 0x0A182 (Beyond Yellow Bridge Door Panel) - True - Stars +Door - 0x0A181 (Beyond Yellow Bridge Door) - 0x0A182 + +Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True: +158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True + +Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C: +158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots +158358 - 0x17DC7 (First Purple Bridge 2) - 0x17DC8 - Stars & Dots +158359 - 0x17CE4 (First Purple Bridge 3) - 0x17DC7 - Stars & Dots +158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Dots +158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Dots + +Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: +158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars +158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars +158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars +158394 - 0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Stars & Environment +158395 - 0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars +158396 - 0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars +158397 - 0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars +158398 - 0x17DCA (Right Orange Bridge 8) - 0x17DCC - Stars +158399 - 0x17D8E (Right Orange Bridge 9) - 0x17DCA - Stars +158400 - 0x17DB7 (Right Orange Bridge 10 & Directional) - 0x17D8E - Stars +158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars +158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars + +Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: +158404 - 0x037FF (Bridge Control) - True - Stars +Door - 0x0C32D (Drawbridge) - 0x037FF + +Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: +158362 - 0x17D9B (Second Purple Bridge 1) - True - Stars & Squares & Black/White Squares +158363 - 0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Squares & Black/White Squares +158364 - 0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Squares & Black/White Squares +158365 - 0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Squares & Black/White Squares & Colored Squares +158366 - 0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Squares & Colored Squares +158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Squares & Colored Squares +158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Squares & Colored Squares + +Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room - 0x0C323: +158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158379 - 0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158380 - 0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158381 - 0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158382 - 0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158383 - 0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158384 - 0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Environment +158385 - 0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158386 - 0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158387 - 0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158611 - 0x17FA0 (Burnt House Discard) - 0x17DDB - Triangles +Door - 0x0C323 (Door to Laser House) - 0x17DDB & 0x17DA2 & 0x2700B + +Treehouse Green Bridge (Treehouse): +158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers +158370 - 0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers +158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Rotated Shapers +158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers & Environment +158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol +158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Colored Shapers & Negative Shapers & Colored Negative Shapers & Stars + Same Colored Symbol +158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers +158610 - 0x17FA9 (Green Bridge Discard) - 0x17E61 - Triangles + +Treehouse Laser Room (Treehouse): +158712 - 0x03613 (Laser Panel) - True - True +158403 - 0x17CBC (Laser House Door Timer Inside Control) - True - True +Laser - 0x028A4 (Laser) - 0x03613 + +Mountaintop (Mountaintop) - Main Island - True - Inside Mountain Top Layer - 0x17C34: +158405 - 0x0042D (River Shape) - True - True +158406 - 0x09F7F (Box Short) - 7 Lasers - True +158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158612 - 0x17C42 (Discard) - True - Triangles +158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Squares & Black/White Squares & Dots +158666 - 0x03542 (Vault Box) - 0x002A6 - True +158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True + +Inside Mountain Top Layer (Inside Mountain) - Inside Mountain Top Layer Bridge - 0x09E39: +158408 - 0x09E39 (Light Bridge Controller) - True - Squares & Black/White Squares & Colored Squares & Eraser & Colored Eraser + +Inside Mountain Top Layer Bridge (Inside Mountain) - Inside Mountain Second Layer - 0x09E54: +158409 - 0x09E7A (Obscured Vision 1) - True - Obscured & Squares & Black/White Squares & Dots +158410 - 0x09E71 (Obscured Vision 2) - 0x09E7A - Obscured & Squares & Black/White Squares & Dots +158411 - 0x09E72 (Obscured Vision 3) - 0x09E71 - Obscured & Squares & Black/White Squares & Shapers & Dots +158412 - 0x09E69 (Obscured Vision 4) - 0x09E72 - Obscured & Squares & Black/White Squares & Dots +158413 - 0x09E7B (Obscured Vision 5) - 0x09E69 - Obscured & Squares & Black/White Squares & Dots +158414 - 0x09E73 (Moving Background 1) - True - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158415 - 0x09E75 (Moving Background 2) - 0x09E73 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158416 - 0x09E78 (Moving Background 3) - 0x09E75 - Moving & Shapers +158417 - 0x09E79 (Moving Background 4) - 0x09E78 - Moving & Shapers & Rotated Shapers +158418 - 0x09E6C (Moving Background 5) - 0x09E79 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158419 - 0x09E6F (Moving Background 6) - 0x09E6C - Moving & Stars & Rotated Shapers & Shapers +158420 - 0x09E6B (Moving Background 7) - 0x09E6F - Moving & Stars & Dots +158421 - 0x33AF5 (Physically Obstructed 1) - True - Squares & Black/White Squares & Environment & Symmetry +158422 - 0x33AF7 (Physically Obstructed 2) - 0x33AF5 - Squares & Black/White Squares & Stars & Environment +158423 - 0x09F6E (Physically Obstructed 3) - 0x33AF7 - Symmetry & Dots & Environment +158424 - 0x09EAD (Angled Inside Trash 1) - True - Squares & Black/White Squares & Shapers & Angled +158425 - 0x09EAF (Angled Inside Trash 2) - 0x09EAD - Squares & Black/White Squares & Shapers & Angled +Door - 0x09E54 (Door to Second Layer) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B + +Inside Mountain Second Layer (Inside Mountain) - Inside Mountain Second Layer Light Bridge Room Near - 0x09FFB - Inside Mountain Second Layer Blue Bridge - 0x09E86: +158426 - 0x09FD3 (Color Cycle 1) - True - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158427 - 0x09FD4 (Color Cycle 2) - 0x09FD3 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158428 - 0x09FD6 (Color Cycle 3) - 0x09FD4 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158429 - 0x09FD7 (Color Cycle 4) - 0x09FD6 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Shapers & Colored Shapers +158430 - 0x09FD8 (Color Cycle 5) - 0x09FD7 - Color Cycle & RGB & Squares & Colored Squares & Symmetry & Colored Dots +Door - 0x09FFB (Staircase Near) - 0x09FD8 + +Inside Mountain Second Layer Blue Bridge (Inside Mountain) - Inside Mountain Second Layer Beyond Bridge - TrueOneWay - Inside Mountain Second Layer Elevator Room - 0x09EDD: +Door - 0x09EDD (Door to Elevator) - 0x09ED8 & 0x09E86 + +Inside Mountain Second Layer Light Bridge Room Near (Inside Mountain): +158431 - 0x09E86 (Light Bridge Controller 2) - True - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines + +Inside Mountain Second Layer Beyond Bridge (Inside Mountain) - Inside Mountain Second Layer Light Bridge Room Far - 0x09E07: +158432 - 0x09FCC (Same Solution 1) - True - Dots & Same Solution +158433 - 0x09FCE (Same Solution 2) - 0x09FCC - Squares & Black/White Squares & Same Solution +158434 - 0x09FCF (Same Solution 3) - 0x09FCE - Stars & Same Solution +158435 - 0x09FD0 (Same Solution 4) - 0x09FCF - Rotated Shapers & Same Solution +158436 - 0x09FD1 (Same Solution 5) - 0x09FD0 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Same Solution +158437 - 0x09FD2 (Same Solution 6) - 0x09FD1 - Shapers & Same Solution +Door - 0x09E07 (Staircase Far) - 0x09FD2 + +Inside Mountain Second Layer Light Bridge Room Far (Inside Mountain): +158438 - 0x09ED8 (Light Bridge Controller 3) - True - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines + +Inside Mountain Second Layer Elevator Room (Inside Mountain) - Inside Mountain Second Layer Elevator - TrueOneWay: +158613 - 0x17F93 (Elevator Discard) - True - Triangles + +Inside Mountain Second Layer Elevator (Inside Mountain) - Inside Mountain Second Layer Elevator Room - 0x09EEB - Inside Mountain Third Layer - 0x09EEB: +158439 - 0x09EEB (Elevator Control Panel) - True - Dots + +Inside Mountain Third Layer (Inside Mountain) - Inside Mountain Second Layer Elevator - TrueOneWay - Inside Mountain Bottom Layer - 0x09F89: +158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser +158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser +158442 - 0x09F01 (Giant Puzzle Top Right) - True - Rotated Shapers +158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser +158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +Door - 0x09F89 (Glass Door) - 0x09FDA + +Inside Mountain Bottom Layer (Inside Mountain) - Inside Mountain Bottom Layer Rock - 0x17FA2 - Final Room - 0x0C141: +158614 - 0x17FA2 (Bottom Layer Discard) - 0xFFF00 - Triangles & Environment +158445 - 0x01983 (Door to Final Room Left) - True - Shapers & Stars +158446 - 0x01987 (Door to Final Room Right) - True - Squares & Colored Squares & Dots +Door - 0x0C141 (Door to Final Room) - 0x01983 & 0x01987 + +Inside Mountain Bottom Layer Rock (Inside Mountain) - Inside Mountain Bottom Layer - 0x17F33 - Inside Mountain Path to Secret Area - 0x17F33: +Door - 0x17F33 (Bottom Layer Rock Open) - True + +Inside Mountain Path to Secret Area (Inside Mountain) - Inside Mountain Bottom Layer Rock - 0x334E1 - Inside Mountain Caves - 0x2D77D: +158447 - 0x00FF8 (Secret Area Entry Panel) - True - Triangles & Black/White Squares & Squares +Door - 0x2D77D (Door to Secret Area) - 0x00FF8 +158448 - 0x334E1 (Rock Control) - True - True + +Inside Mountain Caves (Inside Mountain Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challenge - 0x019A5: +158451 - 0x335AB (Elevator Inside Control) - True - Dots & Squares & Black/White Squares +158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Squares & Black/White Squares +158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Squares & Black/White Squares & Dots +158454 - 0x00190 (Dot Grid Triangles 1) - True - Dots & Triangles +158455 - 0x00558 (Dot Grid Triangles 2) - 0x00190 - Dots & Triangles +158456 - 0x00567 (Dot Grid Triangles 3) - 0x00558 - Dots & Triangles +158457 - 0x006FE (Dot Grid Triangles 4) - 0x00567 - Dots & Triangles +158458 - 0x01A0D (Symmetry Triangles) - True - Symmetry & Triangles +158459 - 0x008B8 (Squares and Triangles) - True - Squares & Black/White Squares & Triangles +158460 - 0x00973 (Stars and Triangles) - 0x008B8 - Stars & Triangles +158461 - 0x0097B (Stars and Triangles of same color) - 0x00973 - Stars & Triangles & Stars and Triangles of same color & Stars + Same Colored Symbol +158462 - 0x0097D (Stars & Squares and Triangles) - 0x0097B - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Triangles +158463 - 0x0097E (Stars & Squares and Triangles 2) - 0x0097D - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Stars and Triangles of same color +158464 - 0x00994 (Rotated Shapers and Triangles 1) - True - Rotated Shapers & Triangles +158465 - 0x334D5 (Rotated Shapers and Triangles 2) - 0x00994 - Rotated Shapers & Triangles +158466 - 0x00995 (Rotated Shapers and Triangles 3) - 0x334D5 - Rotated Shapers & Triangles +158467 - 0x00996 (Shapers and Triangles 1) - 0x00995 - Shapers & Triangles +158468 - 0x00998 (Shapers and Triangles 2) - 0x00996 - Shapers & Triangles +158469 - 0x009A4 (Broken Shapers) - True - Shapers & Broken Shapers +158470 - 0x018A0 (Symmetry Shapers) - True - Shapers & Symmetry +158471 - 0x00A72 (Broken and Negative Shapers) - True - Shapers & Broken Shapers & Negative Shapers +158472 - 0x32962 (Rotated Broken Shapers) - True - Rotated Shapers & Broken Rotated Shapers +158473 - 0x32966 (Stars and Squares) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158474 - 0x01A31 (Rainbow Squares) - True - Color Cycle & RGB & Squares & Colored Squares +158475 - 0x00B71 (Squares & Stars and Colored Eraser) - True - Colored Eraser & Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Eraser +158478 - 0x288EA (Wooden Beam Shapers) - True - Environment & Shapers +158479 - 0x288FC (Wooden Beam Squares and Shapers) - True - Environment & Squares & Black/White Squares & Shapers & Rotated Shapers +158480 - 0x289E7 (Wooden Beam Stars and Squares) - True - Environment & Stars & Squares & Black/White Squares +158481 - 0x288AA (Wooden Beam Shapers and Stars) - True - Environment & Stars & Shapers +158482 - 0x17FB9 (Upstairs Dot Grid Negative Shapers) - True - Shapers & Dots & Negative Shapers +158483 - 0x0A16B (Upstairs Dot Grid Gap Dots) - True - Dots +158484 - 0x0A2CE (Upstairs Dot Grid Stars) - 0x0A16B - Stars & Dots +158485 - 0x0A2D7 (Upstairs Dot Grid Stars & Squares) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars +158486 - 0x0A2DD (Upstairs Dot Grid Shapers) - 0x0A2D7 - Shapers & Dots +158487 - 0x0A2EA (Upstairs Dot Grid Rotated Shapers) - 0x0A2DD - Rotated Shapers & Dots +158488 - 0x0008F (Upstairs Invisible Dots 1) - True - Dots & Invisible Dots +158489 - 0x0006B (Upstairs Invisible Dots 2) - 0x0008F - Dots & Invisible Dots +158490 - 0x0008B (Upstairs Invisible Dots 3) - 0x0006B - Dots & Invisible Dots +158491 - 0x0008C (Upstairs Invisible Dots 4) - 0x0008B - Dots & Invisible Dots +158492 - 0x0008A (Upstairs Invisible Dots 5) - 0x0008C - Dots & Invisible Dots +158493 - 0x00089 (Upstairs Invisible Dots 6) - 0x0008A - Dots & Invisible Dots +158494 - 0x0006A (Upstairs Invisible Dots 7) - 0x00089 - Dots & Invisible Dots +158495 - 0x0006C (Upstairs Invisible Dots 8) - 0x0006A - Dots & Invisible Dots +158496 - 0x00027 (Upstairs Invisible Dot Symmetry 1) - True - Dots & Invisible Dots & Symmetry +158497 - 0x00028 (Upstairs Invisible Dot Symmetry 2) - 0x00027 - Dots & Invisible Dots & Symmetry +158498 - 0x00029 (Upstairs Invisible Dot Symmetry 3) - 0x00028 - Dots & Invisible Dots & Symmetry +158476 - 0x09DD5 (Lone Pillar) - True - Pillar & Triangles +Door - 0x019A5 (Secret Black Door to Challenge) - 0x09DD5 +158449 - 0x021D7 (Shortcut to Mountain Panel) - True - Triangles & Stars & Stars + Same Colored Symbol & Colored Triangles +Door - 0x2D73F (Shortcut to Mountain Door) - 0x021D7 +158450 - 0x17CF2 (Shortcut to Swamp Panel) - True - Triangles +Door - 0x2D859 (Shortcut to Swamp Door) - 0x17CF2 + +Path to Challenge (Inside Mountain Caves) - Challenge - 0x0A19A: +158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol +Door - 0x0A19A (Challenge Entry Door) - 0x0A16E + +Challenge (Challenge) - Theater Walkway - 0x0348A: +158499 - 0x0A332 (Start Timer) - 11 Lasers - True +158500 - 0x0088E (Small Basic) - 0x0A332 - True +158501 - 0x00BAF (Big Basic) - 0x0088E - True +158502 - 0x00BF3 (Square) - 0x00BAF - Squares & Black/White Squares +158503 - 0x00C09 (Maze Map) - 0x00BF3 - Dots +158504 - 0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots +158505 - 0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots +158506 - 0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers +158507 - 0x00CD4 (Big Basic 2) - 0x00524 - True +158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Squares & Black/White Squares +158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Squares & Black/White Squares +158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Squares & Black/White Squares +158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares +158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares +158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares +158514 - 0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles +158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles +158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry & Pillar +158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Squares & Black/White Squares & Symmetry & Pillar +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True +158518 - 0x039B4 (Door to Theater Walkway Panel) - True - Triangles +Door - 0x0348A (Door to Theater Walkway) - 0x039B4 + +Theater Walkway (Theater Walkway) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: +158668 - 0x2FAF6 (Vault Box) - True - True +158519 - 0x27732 (Theater Shortcut Panel) - True - True +Door - 0x27739 (Door to Windmill Interior) - 0x27732 +158520 - 0x2773D (Desert Shortcut Panel) - True - True +Door - 0x27263 (Door to Desert Elevator Room) - 0x2773D +158521 - 0x09E85 (Town Shortcut Panel) - True - Triangles +Door - 0x09E87 (Door to Town) - 0x09E85 + +Final Room (Inside Mountain Final Room) - Elevator - 0x339BB & 0x33961: +158522 - 0x0383A (Right Pillar 1) - True - Stars & Pillar +158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots & Pillar +158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Pillar +158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry & Pillar +158526 - 0x0383D (Left Pillar 1) - True - Dots & Pillar +158527 - 0x0383F (Left Pillar 2) - 0x0383D - Squares & Black/White Squares & Pillar +158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers & Pillar +158529 - 0x339BB (Left Pillar 4) - 0x03859 - Squares & Black/White Squares & Stars & Symmetry & Pillar + +Elevator (Inside Mountain Final Room): +158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158531 - 0x3D9A7 (Elevator Door Close Right) - True - True +158532 - 0x3C113 (Elevator Door Open Left) - 0x3D9A6 | 0x3D9A7 - True +158533 - 0x3C114 (Elevator Door Open Right) - 0x3D9A6 | 0x3D9A7 - True +158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True +158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True +158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA | 0x3D9A8 - True + +Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 0857ef6b42..01669cffba 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -35,7 +35,7 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 2 + data_version = 5 static_logic = StaticWitnessLogic() static_locat = StaticWitnessLocations() @@ -53,11 +53,18 @@ class WitnessWorld(World): 'seed': self.world.random.randint(0, 1000000), 'victory_location': int(self.player_logic.VICTORY_LOCATION, 16), 'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID, - 'doorhex_to_id': self.player_logic.DOOR_DICT_FOR_CLIENT, - 'door_connections_to_sever': self.player_logic.DOOR_CONNECTIONS_TO_SEVER + 'item_id_to_door_hexes': self.items.ITEM_ID_TO_DOOR_HEX, + 'door_hexes': self.items.DOORS, + 'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME } def generate_early(self): + if not (is_option_enabled(self.world, self.player, "shuffle_symbols") + or get_option_value(self.world, self.player, "shuffle_doors") + or is_option_enabled(self.world, self.player, "shuffle_lasers")): + raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door" + " Shuffle or Laser Shuffle") + self.player_logic = WitnessPlayerLogic(self.world, self.player) self.locat = WitnessPlayerLocations(self.world, self.player, self.player_logic) self.items = WitnessPlayerItems(self.locat, self.world, self.player, self.player_logic) @@ -78,11 +85,11 @@ class WitnessWorld(World): less_junk = 0 # Put good item on first check if symbol shuffle is on - # symbols = is_option_enabled(self.world, self.player, "shuffle_symbols") - symbols = True + symbols = is_option_enabled(self.world, self.player, "shuffle_symbols") if symbols: random_good_item = self.world.random.choice(self.items.GOOD_ITEMS) + first_check = self.world.get_location( "Tutorial Gate Open", self.player ) @@ -91,6 +98,10 @@ class WitnessWorld(World): less_junk = 1 + for item in self.player_logic.STARTING_INVENTORY: + self.world.push_precollected(items_by_name[item]) + pool.remove(items_by_name[item]) + for item in self.items.EXTRA_AMOUNTS: witness_item = self.create_item(item) for i in range(0, self.items.EXTRA_AMOUNTS[item]): diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 0d7530988e..65a8326984 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -51,12 +51,15 @@ class StaticWitnessItems: def __init__(self): item_tab = dict() - for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS): + for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS: if item[0] == "11 Lasers" or item == "7 Lasers": continue item_tab[item[0]] = ItemData(158000 + item[1], True, False) + for item in StaticWitnessLogic.ALL_DOOR_ITEMS: + item_tab[item[0]] = ItemData(158000 + item[1], True, False) + for item in StaticWitnessLogic.ALL_TRAPS: item_tab[item[0]] = ItemData( 158000 + item[1], False, False, True @@ -89,23 +92,39 @@ class WitnessPlayerItems: self.ITEM_TABLE = copy.copy(StaticWitnessItems.ALL_ITEM_TABLE) self.PROGRESSION_TABLE = dict() + self.ITEM_ID_TO_DOOR_HEX = dict() + self.DOORS = set() + + self.SYMBOLS_NOT_IN_THE_GAME = set() + self.EXTRA_AMOUNTS = { "Functioning Brain": 1, "Puzzle Skip": get_option_value(world, player, "puzzle_skip_amount") } for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS): - if item not in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME: + if item[0] not in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME: del self.ITEM_TABLE[item[0]] + if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS: + self.SYMBOLS_NOT_IN_THE_GAME.add(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code) else: self.PROGRESSION_TABLE[item[0]] = self.ITEM_TABLE[item[0]] + for entity_hex, items in player_logic.DOOR_ITEMS_BY_ID.items(): + entity_hex_int = int(entity_hex, 16) + + self.DOORS.add(entity_hex_int) + + for item in items: + item_id = StaticWitnessItems.ALL_ITEM_TABLE[item].code + self.ITEM_ID_TO_DOOR_HEX.setdefault(item_id, set()).add(entity_hex_int) + symbols = is_option_enabled(world, player, "shuffle_symbols") if "shuffle_symbols" not in the_witness_options.keys(): symbols = True - doors = is_option_enabled(world, player, "shuffle_doors") + doors = get_option_value(world, player, "shuffle_doors") if doors and symbols: self.GOOD_ITEMS = [ @@ -117,10 +136,10 @@ class WitnessPlayerItems: "Shapers", "Symmetry" ] - if is_option_enabled(world, player, "shuffle_discarded_panels"): - self.GOOD_ITEMS.append("Triangles") - if not is_option_enabled(world, player, "disable_non_randomized_puzzles"): - self.GOOD_ITEMS.append("Colored Squares") + if is_option_enabled(world, player, "shuffle_discarded_panels"): + self.GOOD_ITEMS.append("Triangles") + if not is_option_enabled(world, player, "disable_non_randomized_puzzles"): + self.GOOD_ITEMS.append("Colored Squares") for event_location in locat.EVENT_LOCATION_TABLE: location = player_logic.EVENT_ITEM_PAIRS[event_location] diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index 380c64c069..f6fcad70ce 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -2,7 +2,7 @@ Defines constants for different types of locations in the game """ -from .Options import is_option_enabled +from .Options import is_option_enabled, get_option_value from .player_logic import StaticWitnessLogic, WitnessPlayerLogic @@ -42,7 +42,7 @@ class StaticWitnessLocations: "Symmetry Island Colored Dots 6", "Symmetry Island Fading Lines 7", "Symmetry Island Scenery Outlines 5", - "Symmetry Island Laser", + "Symmetry Island Laser Panel", "Orchard Apple Tree 5", @@ -52,7 +52,7 @@ class StaticWitnessLocations: "Desert Artificial Light Reflection 3", "Desert Pond Reflection 5", "Desert Flood Reflection 6", - "Desert Laser", + "Desert Laser Panel", "Quarry Mill Eraser and Dots 6", "Quarry Mill Eraser and Squares 8", @@ -63,34 +63,34 @@ class StaticWitnessLocations: "Quarry Boathouse Stars & Eraser & Shapers 2", "Quarry Boathouse Stars & Eraser & Shapers 5", "Quarry Discard", - "Quarry Laser", + "Quarry Laser Panel", "Shadows Lower Avoid 8", "Shadows Environmental Avoid 8", "Shadows Follow 5", - "Shadows Laser", + "Shadows Laser Panel", "Keep Hedge Maze 4", "Keep Pressure Plates 4", "Keep Discard", - "Keep Laser Hedges", - "Keep Laser Pressure Plates", + "Keep Laser Panel Hedges", + "Keep Laser Panel Pressure Plates", "Shipwreck Vault Box", "Shipwreck Discard", "Monastery Rhombic Avoid 3", "Monastery Branch Follow 2", - "Monastery Laser", + "Monastery Laser Panel", "Town Cargo Box Discard", "Town Hexagonal Reflection", - "Town Square Avoid", + "Town Church Lattice", "Town Rooftop Discard", "Town Symmetry Squares 5 + Dots", "Town Full Dot Grid Shapers 5", "Town Shapers & Dots & Eraser", - "Town Laser", + "Town Laser Panel", "Theater Discard", @@ -98,7 +98,7 @@ class StaticWitnessLocations: "Jungle Waves 3", "Jungle Waves 7", "Jungle Popup Wall 6", - "Jungle Laser", + "Jungle Laser Panel", "River Vault Box", @@ -106,7 +106,7 @@ class StaticWitnessLocations: "Bunker Drawn Squares 9", "Bunker Drawn Squares through Tinted Glass 3", "Bunker Drop-Down Door Squares 2", - "Bunker Laser", + "Bunker Laser Panel", "Swamp Seperatable Shapers 6", "Swamp Combinable Shapers 8", @@ -117,7 +117,7 @@ class StaticWitnessLocations: "Swamp Red Underwater Negative Shapers 4", "Swamp More Rotated Shapers 4", "Swamp Blue Underwater Negative Shapers 5", - "Swamp Laser", + "Swamp Laser Panel", "Treehouse Yellow Bridge 9", "Treehouse First Purple Bridge 5", @@ -125,21 +125,12 @@ class StaticWitnessLocations: "Treehouse Green Bridge 7", "Treehouse Green Bridge Discard", "Treehouse Left Orange Bridge 15", - "Treehouse Burned House Discard", + "Treehouse Burnt House Discard", "Treehouse Right Orange Bridge 12", - "Treehouse Laser", + "Treehouse Laser Panel", "Mountaintop Discard", "Mountaintop Vault Box", - - "Inside Mountain Obscured Vision 5", - "Inside Mountain Moving Background 7", - "Inside Mountain Physically Obstructed 3", - "Inside Mountain Angled Inside Trash 2", - "Inside Mountain Color Cycle 5", - "Inside Mountain Same Solution 6", - "Inside Mountain Elevator Discard", - "Inside Mountain Giant Puzzle", } UNCOMMON_LOCATIONS = { @@ -156,35 +147,53 @@ class StaticWitnessLocations: "Swamp Underwater Back Optional", } - HARD_LOCATIONS = { - "Inside Mountain Secret Area Dot Grid Triangles 4", - "Inside Mountain Secret Area Symmetry Triangles", - "Inside Mountain Secret Area Stars & Squares and Triangles 2", - "Inside Mountain Secret Area Shapers and Triangles 2", - "Inside Mountain Secret Area Symmetry Shapers", - "Inside Mountain Secret Area Broken and Negative Shapers", - "Inside Mountain Secret Area Broken Shapers", + CAVES_LOCATIONS = { + "Inside Mountain Caves Dot Grid Triangles 4", + "Inside Mountain Caves Symmetry Triangles", + "Inside Mountain Caves Stars & Squares and Triangles 2", + "Inside Mountain Caves Shapers and Triangles 2", + "Inside Mountain Caves Symmetry Shapers", + "Inside Mountain Caves Broken and Negative Shapers", + "Inside Mountain Caves Broken Shapers", - "Inside Mountain Secret Area Rainbow Squares", - "Inside Mountain Secret Area Squares & Stars and Colored Eraser", - "Inside Mountain Secret Area Rotated Broken Shapers", - "Inside Mountain Secret Area Stars and Squares", - "Inside Mountain Secret Area Lone Pillar", - "Inside Mountain Secret Area Wooden Beam Shapers", - "Inside Mountain Secret Area Wooden Beam Squares and Shapers", - "Inside Mountain Secret Area Wooden Beam Stars and Squares", - "Inside Mountain Secret Area Wooden Beam Shapers and Stars", - "Inside Mountain Secret Area Upstairs Invisible Dots 8", - "Inside Mountain Secret Area Upstairs Invisible Dot Symmetry 3", - "Inside Mountain Secret Area Upstairs Dot Grid Negative Shapers", - "Inside Mountain Secret Area Upstairs Dot Grid Rotated Shapers", + "Inside Mountain Caves Rainbow Squares", + "Inside Mountain Caves Squares & Stars and Colored Eraser", + "Inside Mountain Caves Rotated Broken Shapers", + "Inside Mountain Caves Stars and Squares", + "Inside Mountain Caves Lone Pillar", + "Inside Mountain Caves Wooden Beam Shapers", + "Inside Mountain Caves Wooden Beam Squares and Shapers", + "Inside Mountain Caves Wooden Beam Stars and Squares", + "Inside Mountain Caves Wooden Beam Shapers and Stars", + "Inside Mountain Caves Upstairs Invisible Dots 8", + "Inside Mountain Caves Upstairs Invisible Dot Symmetry 3", + "Inside Mountain Caves Upstairs Dot Grid Negative Shapers", + "Inside Mountain Caves Upstairs Dot Grid Rotated Shapers", - "Challenge Vault Box", "Theater Walkway Vault Box", "Inside Mountain Bottom Layer Discard", "Theater Challenge Video", } + MOUNTAIN_UNREACHABLE_FROM_BEHIND = { + "Mountaintop Trap Door Triple Exit", + + "Inside Mountain Obscured Vision 5", + "Inside Mountain Moving Background 7", + "Inside Mountain Physically Obstructed 3", + "Inside Mountain Angled Inside Trash 2", + "Inside Mountain Color Cycle 5", + "Inside Mountain Same Solution 6", + } + + MOUNTAIN_REACHABLE_FROM_BEHIND = { + "Inside Mountain Elevator Discard", + "Inside Mountain Giant Puzzle", + + "Inside Mountain Final Room Left Pillar 4", + "Inside Mountain Final Room Right Pillar 4", + } + ALL_LOCATIONS_TO_ID = dict() @staticmethod @@ -193,12 +202,7 @@ class StaticWitnessLocations: Calculates the location ID for any given location """ - panel_offset = StaticWitnessLogic.CHECKS_BY_HEX[chex]["idOffset"] - type_offset = StaticWitnessLocations.TYPE_OFFSETS[ - StaticWitnessLogic.CHECKS_BY_HEX[chex]["panelType"] - ] - - return StaticWitnessLocations.ID_START + panel_offset + type_offset + return StaticWitnessLogic.CHECKS_BY_HEX[chex]["id"] @staticmethod def get_event_name(panel_hex): @@ -213,6 +217,7 @@ class StaticWitnessLocations: all_loc_to_id = { panel_obj["checkName"]: self.get_id(chex) for chex, panel_obj in StaticWitnessLogic.CHECKS_BY_HEX.items() + if panel_obj["id"] } all_loc_to_id = dict( @@ -229,12 +234,34 @@ class WitnessPlayerLocations: """ def __init__(self, world, player, player_logic: WitnessPlayerLogic): + """Defines locations AFTER logic changes due to options""" + self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} self.CHECK_LOCATIONS = ( StaticWitnessLocations.GENERAL_LOCATIONS ) - """Defines locations AFTER logic changes due to options""" + doors = get_option_value(world, player, "shuffle_doors") + earlyutm = is_option_enabled(world, player, "early_secret_area") + victory = get_option_value(world, player, "victory_condition") + lasers = get_option_value(world, player, "challenge_lasers") + laser_shuffle = get_option_value(world, player, "shuffle_lasers") + + postgame = set() + postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS + postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND + postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND + + self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame + + if earlyutm or doors >= 2 or (victory == 1 and (lasers <= 11 or laser_shuffle)): + postgame -= StaticWitnessLocations.CAVES_LOCATIONS + + if doors >= 2: + postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND + + if victory != 2: + postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND if is_option_enabled(world, player, "shuffle_discarded_panels"): self.PANEL_TYPES_TO_SHUFFLE.add("Discard") @@ -245,18 +272,11 @@ class WitnessPlayerLocations: if is_option_enabled(world, player, "shuffle_uncommon"): self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.UNCOMMON_LOCATIONS - if is_option_enabled(world, player, "shuffle_hard"): - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.HARD_LOCATIONS - - if is_option_enabled(world, player, "shuffle_symbols") and is_option_enabled(world, player, "shuffle_doors"): - if is_option_enabled(world, player, "disable_non_randomized_puzzles"): - # This particular combination of logic settings leads to logic so restrictive that generation can fail - # Hence, we add some extra sphere 0 locations - - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.EXTRA_LOCATIONS - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS + if not is_option_enabled(world, player, "shuffle_postgame"): + self.CHECK_LOCATIONS -= postgame + self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - { StaticWitnessLogic.CHECKS_BY_HEX[check_hex]["checkName"] for check_hex in player_logic.COMPLETELY_DISABLED_CHECKS @@ -272,7 +292,7 @@ class WitnessPlayerLocations: ) event_locations = { - p for p in player_logic.NECESSARY_EVENT_PANELS + p for p in player_logic.EVENT_PANELS } self.EVENT_LOCATION_TABLE = { diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 689403dc22..eb57f2c6a0 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -18,22 +18,15 @@ When the world has parsed its options, a second function is called to finalize t import copy from BaseClasses import MultiWorld from .static_logic import StaticWitnessLogic -from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list +from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list, \ + get_symbol_shuffle_list, get_door_panel_shuffle_list, get_doors_complex_list, get_doors_max_list, \ + get_doors_simple_list, get_laser_shuffle from .Options import is_option_enabled, get_option_value, the_witness_options class WitnessPlayerLogic: """WITNESS LOGIC CLASS""" - def update_door_dict(self, panel_hex): - item_id = StaticWitnessLogic.ALL_DOOR_ITEM_IDS_BY_HEX.get(panel_hex) - - if item_id is None: - return - - self.DOOR_DICT_FOR_CLIENT[panel_hex] = item_id - self.DOOR_CONNECTIONS_TO_SEVER.update(StaticWitnessLogic.CONNECTIONS_TO_SEVER_BY_DOOR_HEX[panel_hex]) - def reduce_req_within_region(self, panel_hex): """ Panels in this game often only turn on when other panels are solved. @@ -43,35 +36,42 @@ class WitnessPlayerLogic: Panels outside of the same region will still be checked manually. """ - these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"] + check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex] - real_items = {item[0] for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME} + these_items = frozenset({frozenset()}) + + if check_obj["id"]: + these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"] these_items = frozenset({ - subset.intersection(real_items) + subset.intersection(self.PROG_ITEMS_ACTUALLY_IN_THE_GAME) for subset in these_items }) + if panel_hex in self.DOOR_ITEMS_BY_ID: + door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]}) + + all_options = set() + + for items_option in these_items: + for dependentItem in door_items: + all_options.add(items_option.union(dependentItem)) + + return frozenset(all_options) + these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"] - if StaticWitnessLogic.DOOR_NAMES_BY_HEX.get(panel_hex) in real_items: - self.update_door_dict(panel_hex) - - these_panels = frozenset({frozenset()}) - if these_panels == frozenset({frozenset()}): return these_items all_options = set() - check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex] - for option in these_panels: dependent_items_for_option = frozenset({frozenset()}) for option_panel in option: - new_items = set() dep_obj = StaticWitnessLogic.CHECKS_BY_HEX.get(option_panel) + if option_panel in {"7 Lasers", "11 Lasers"}: new_items = frozenset({frozenset([option_panel])}) # If a panel turns on when a panel in a different region turns on, @@ -101,8 +101,34 @@ class WitnessPlayerLogic: return frozenset(all_options) def make_single_adjustment(self, adj_type, line): + from . import StaticWitnessItems """Makes a single logic adjustment based on additional logic file""" + if adj_type == "Items": + if line not in StaticWitnessItems.ALL_ITEM_TABLE: + raise RuntimeError("Item \"" + line + "\" does not exit.") + + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(line) + + if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: + panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2] + for panel_hex in panel_hexes: + self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(line) + + return + + if adj_type == "Remove Items": + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.discard(line) + + if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: + panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2] + for panel_hex in panel_hexes: + if panel_hex in self.DOOR_ITEMS_BY_ID: + self.DOOR_ITEMS_BY_ID[panel_hex].discard(line) + + if adj_type == "Starting Inventory": + self.STARTING_INVENTORY.add(line) + if adj_type == "Event Items": line_split = line.split(" - ") hex_set = line_split[1].split(",") @@ -130,18 +156,20 @@ class WitnessPlayerLogic: if adj_type == "Requirement Changes": line_split = line.split(" - ") - required_items = parse_lambda(line_split[2]) - items_actually_in_the_game = {item[0] for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS} - required_items = frozenset( - subset.intersection(items_actually_in_the_game) - for subset in required_items - ) - requirement = { "panels": parse_lambda(line_split[1]), - "items": required_items } + if len(line_split) > 2: + required_items = parse_lambda(line_split[2]) + items_actually_in_the_game = {item[0] for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS} + required_items = frozenset( + subset.intersection(items_actually_in_the_game) + for subset in required_items + ) + + requirement["items"] = required_items + self.DEPENDENT_REQUIREMENTS_BY_HEX[line_split[0]] = requirement return @@ -151,11 +179,6 @@ class WitnessPlayerLogic: self.COMPLETELY_DISABLED_CHECKS.add(panel_hex) - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = { - item for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME - if item[0] != StaticWitnessLogic.DOOR_NAMES_BY_HEX.get(panel_hex) - } - return if adj_type == "Region Changes": @@ -189,18 +212,25 @@ class WitnessPlayerLogic: adjustment_linesets_in_order.append(get_disable_unrandomized_list()) if is_option_enabled(world, player, "shuffle_symbols") or "shuffle_symbols" not in the_witness_options.keys(): - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.update(StaticWitnessLogic.ALL_SYMBOL_ITEMS) + adjustment_linesets_in_order.append(get_symbol_shuffle_list()) - if is_option_enabled(world, player, "shuffle_doors"): - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.update(StaticWitnessLogic.ALL_DOOR_ITEMS) + if get_option_value(world, player, "shuffle_doors") == 1: + adjustment_linesets_in_order.append(get_door_panel_shuffle_list()) + + if get_option_value(world, player, "shuffle_doors") == 2: + adjustment_linesets_in_order.append(get_doors_simple_list()) + + if get_option_value(world, player, "shuffle_doors") == 3: + adjustment_linesets_in_order.append(get_doors_complex_list()) + + if get_option_value(world, player, "shuffle_doors") == 4: + adjustment_linesets_in_order.append(get_doors_max_list()) if is_option_enabled(world, player, "early_secret_area"): adjustment_linesets_in_order.append(get_early_utm_list()) - else: - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = { - item for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME - if item[0] != "Mountaintop River Shape Power On" - } + + if is_option_enabled(world, player, "shuffle_lasers"): + adjustment_linesets_in_order.append(get_laser_shuffle()) for adjustment_lineset in adjustment_linesets_in_order: current_adjustment_type = None @@ -233,62 +263,32 @@ class WitnessPlayerLogic: pair = (name, self.EVENT_ITEM_NAMES[panel]) return pair - def _regions_are_adjacent(self, region1, region2): - for connection in self.CONNECTIONS_BY_REGION_NAME[region1]: - if connection[0] == region2: - return True - - for connection in self.CONNECTIONS_BY_REGION_NAME[region2]: - if connection[0] == region1: - return True - - return False - def make_event_panel_lists(self): """ Special event panel data structures """ - for region_conn in self.CONNECTIONS_BY_REGION_NAME.values(): - for region_and_option in region_conn: - for panelset in region_and_option[1]: - for panel in panelset: - self.EVENT_PANELS_FROM_REGIONS.add(panel) - self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" - self.ORIGINAL_EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS) - self.ORIGINAL_EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS) + for region_name, connections in self.CONNECTIONS_BY_REGION_NAME.items(): + for connection in connections: + for panel_req in connection[1]: + for panel in panel_req: + if panel == "TrueOneWay": + continue - for panel in self.EVENT_PANELS_FROM_REGIONS: - for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items(): - for connection in self.CONNECTIONS_BY_REGION_NAME[region_name]: - connected_r = connection[0] - if connected_r not in StaticWitnessLogic.ALL_REGIONS_BY_NAME: - continue - if region_name == "Boat" or connected_r == "Boat": - continue - connected_r = StaticWitnessLogic.ALL_REGIONS_BY_NAME[connected_r] - if not any([panel in option for option in connection[1]]): - continue - if panel not in region["panels"] | connected_r["panels"]: - self.NECESSARY_EVENT_PANELS.add(panel) + if StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] != region_name: + self.EVENT_PANELS_FROM_REGIONS.add(panel) - for event_panel in self.EVENT_PANELS_FROM_PANELS: - for panel, panel_req in self.REQUIREMENTS_BY_HEX.items(): - if any([event_panel in item_set for item_set in panel_req]): - region1 = StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] - region2 = StaticWitnessLogic.CHECKS_BY_HEX[event_panel]["region"]["name"] - - if not self._regions_are_adjacent(region1, region2): - self.NECESSARY_EVENT_PANELS.add(event_panel) + self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS) + self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS) for always_hex, always_item in self.ALWAYS_EVENT_NAMES_BY_HEX.items(): self.ALWAYS_EVENT_HEX_CODES.add(always_hex) - self.NECESSARY_EVENT_PANELS.add(always_hex) + self.EVENT_PANELS.add(always_hex) self.EVENT_ITEM_NAMES[always_hex] = always_item - for panel in self.NECESSARY_EVENT_PANELS: + for panel in self.EVENT_PANELS: pair = self.make_event_item_pair(panel) self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] @@ -297,8 +297,8 @@ class WitnessPlayerLogic: self.EVENT_PANELS_FROM_REGIONS = set() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set() - self.DOOR_DICT_FOR_CLIENT = dict() - self.DOOR_CONNECTIONS_TO_SEVER = set() + self.DOOR_ITEMS_BY_ID = dict() + self.STARTING_INVENTORY = set() self.CONNECTIONS_BY_REGION_NAME = copy.copy(StaticWitnessLogic.STATIC_CONNECTIONS_BY_REGION_NAME) self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(StaticWitnessLogic.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) @@ -306,8 +306,7 @@ class WitnessPlayerLogic: # Determining which panels need to be events is a difficult process. # At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones. - self.ORIGINAL_EVENT_PANELS = set() - self.NECESSARY_EVENT_PANELS = set() + self.EVENT_PANELS = set() self.EVENT_ITEM_PAIRS = dict() self.ALWAYS_EVENT_HEX_CODES = set() self.COMPLETELY_DISABLED_CHECKS = set() @@ -320,42 +319,63 @@ class WitnessPlayerLogic: "0x00037": "Monastery Branch Panels Activate", "0x0A079": "Access to Bunker Laser", "0x0A3B5": "Door to Tutorial Discard Opens", + "0x00139": "Keep Hedges 2 Turns On", + "0x019DC": "Keep Hedges 3 Turns On", + "0x019E7": "Keep Hedges 4 Turns On", "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", "0x09F7F": "Mountain Access", "0x0367C": "Quarry Laser Mill Requirement Met", - "0x009A1": "Swamp Rotating Bridge Near Side", + "0x009A1": "Swamp Rotated Shapers 1 Activates", "0x00006": "Swamp Cyan Water Drains", "0x00990": "Swamp Broken Shapers 1 Activates", "0x0A8DC": "Lower Avoid 6 Activates", "0x0000A": "Swamp More Rotated Shapers 1 Access", - "0x09ED8": "Inside Mountain Second Layer Both Light Bridges Solved", + "0x09E86": "Inside Mountain Second Layer Blue Bridge Access", + "0x09ED8": "Inside Mountain Second Layer Yellow Bridge Access", "0x0A3D0": "Quarry Laser Boathouse Requirement Met", "0x00596": "Swamp Red Water Drains", - "0x28B39": "Town Tower 4th Door Opens", + "0x00E3A": "Swamp Purple Water Drains", "0x0343A": "Door to Symmetry Island Powers On", - "0xFFF00": "Inside Mountain Bottom Layer Discard Turns On" + "0xFFF00": "Inside Mountain Bottom Layer Discard Turns On", + "0x17CA6": "All Boat Panels Turn On", + "0x17CDF": "All Boat Panels Turn On", + "0x09DB8": "All Boat Panels Turn On", + "0x17C95": "All Boat Panels Turn On", + "0x03BB0": "Town Church Lattice Vision From Outside", + "0x28AC1": "Town Shapers & Dots & Eraser Turns On", + "0x28A69": "Town Tower 1st Door Opens", + "0x28ACC": "Town Tower 2nd Door Opens", + "0x28AD9": "Town Tower 3rd Door Opens", + "0x28B39": "Town Tower 4th Door Opens", + "0x03675": "Quarry Mill Ramp Activation From Above", + "0x03679": "Quarry Mill Lift Lowering While Standing On It", + "0x2FAF6": "Tutorial Gate Secret Solution Knowledge", + "0x079DF": "Town Hexagonal Reflection Turns On", + "0x17DA2": "Right Orange Bridge Fully Extended", + "0x19B24": "Shadows Lower Avoid Patterns Visible", + "0x2700B": "Open Door to Treehouse Laser House", + "0x00055": "Orchard Apple Trees 4 Turns On", } self.ALWAYS_EVENT_NAMES_BY_HEX = { - "0x0360D": "Symmetry Laser Activation", - "0x03608": "Desert Laser Activation", + "0x00509": "Symmetry Laser Activation", + "0x012FB": "Desert Laser Activation", "0x09F98": "Desert Laser Redirection", - "0x03612": "Quarry Laser Activation", - "0x19650": "Shadows Laser Activation", - "0x0360E": "Keep Laser Activation", - "0x03317": "Keep Laser Activation", - "0x17CA4": "Monastery Laser Activation", - "0x032F5": "Town Laser Activation", - "0x03616": "Jungle Laser Activation", - "0x09DE0": "Bunker Laser Activation", - "0x03615": "Swamp Laser Activation", - "0x03613": "Treehouse Laser Activation", + "0x01539": "Quarry Laser Activation", + "0x181B3": "Shadows Laser Activation", + "0x014BB": "Keep Laser Activation", + "0x17C65": "Monastery Laser Activation", + "0x032F9": "Town Laser Activation", + "0x00274": "Jungle Laser Activation", + "0x0C2B2": "Bunker Laser Activation", + "0x00BF6": "Swamp Laser Activation", + "0x028A4": "Treehouse Laser Activation", "0x03535": "Shipwreck Video Pattern Knowledge", "0x03542": "Mountain Video Pattern Knowledge", "0x0339E": "Desert Video Pattern Knowledge", "0x03481": "Tutorial Video Pattern Knowledge", "0x03702": "Jungle Video Pattern Knowledge", - "0x2FAF6": "Theater Walkway Video Pattern Knowledge", + "0x0356B": "Challenge Video Pattern Knowledge", "0x09F7F": "Mountaintop Trap Door Turns On", "0x17C34": "Mountain Access", } diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index a7d549e704..b5ee31b8ca 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -33,6 +33,10 @@ class WitnessRegions: source_region = world.get_region(source, player) target_region = world.get_region(target, player) + #print(source_region) + #print(target_region) + #print("---") + connection = Entrance( player, source + " to " + target + " via " + str(panel_hex_to_solve_set), @@ -76,10 +80,17 @@ class WitnessRegions: for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: if connection[0] == "Entry": continue - self.connect(world, player, region_name, - connection[0], player_logic, connection[1]) - self.connect(world, player, connection[0], - region_name, player_logic, connection[1]) + + if connection[1] == frozenset({frozenset(["TrueOneWay"])}): + self.connect(world, player, region_name, connection[0], player_logic, frozenset({frozenset()})) + continue + + for subset in connection[1]: + if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}): + if all({StaticWitnessLogic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}): + self.connect(world, player, connection[0], region_name, player_logic, frozenset({subset})) + + self.connect(world, player, region_name, connection[0], player_logic, connection[1]) world.get_entrance("The Splashscreen?", player).connect( world.get_region('First Hallway', player) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 1f13074a88..cd1fae1235 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -22,6 +22,21 @@ class WitnessLogic(LogicMixin): def _witness_has_lasers(self, world, player: int, amount: int) -> bool: lasers = 0 + if is_option_enabled(world, player, "shuffle_lasers"): + lasers += int(self.has("Symmetry Laser", player)) + lasers += int(self.has("Desert Laser", player) + and self.has("Desert Laser Redirection", player)) + lasers += int(self.has("Town Laser", player)) + lasers += int(self.has("Monastery Laser", player)) + lasers += int(self.has("Keep Laser", player)) + lasers += int(self.has("Quarry Laser", player)) + lasers += int(self.has("Treehouse Laser", player)) + lasers += int(self.has("Jungle Laser", player)) + lasers += int(self.has("Bunker Laser", player)) + lasers += int(self.has("Swamp Laser", player)) + lasers += int(self.has("Shadows Laser", player)) + return lasers >= amount + lasers += int(self.has("Symmetry Laser Activation", player)) lasers += int(self.has("Desert Laser Activation", player) and self.has("Desert Laser Redirection", player)) @@ -48,11 +63,8 @@ class WitnessLogic(LogicMixin): if (check_name + " Solved" in locat.EVENT_LOCATION_TABLE and not self.has(player_logic.EVENT_ITEM_PAIRS[check_name + " Solved"], player)): return False - if panel not in player_logic.ORIGINAL_EVENT_PANELS and not self.can_reach(check_name, "Location", player): - return False - if (panel in player_logic.ORIGINAL_EVENT_PANELS - and check_name + " Solved" not in locat.EVENT_LOCATION_TABLE - and not self._witness_safe_manual_panel_check(panel, world, player, player_logic, locat)): + if (check_name + " Solved" not in locat.EVENT_LOCATION_TABLE + and not self._witness_meets_item_requirements(panel, world, player, player_logic, locat)): return False return True @@ -79,8 +91,10 @@ class WitnessLogic(LogicMixin): if not self._witness_has_lasers(world, player, get_option_value(world, player, "challenge_lasers")): valid_option = False break - elif item in player_logic.ORIGINAL_EVENT_PANELS: - valid_option = self._witness_can_solve_panel(item, world, player, player_logic, locat) + elif item in player_logic.EVENT_PANELS: + if not self._witness_can_solve_panel(item, world, player, player_logic, locat): + valid_option = False + break elif not self.has(item, player): valid_option = False break @@ -90,24 +104,6 @@ class WitnessLogic(LogicMixin): return False - def _witness_safe_manual_panel_check(self, panel, world, player, player_logic: WitnessPlayerLogic, locat): - """ - nested can_reach can cause problems, but only if the region being - checked is neither of the two original regions from the first - can_reach. - A nested can_reach is okay here because the only panels this - function is called on are panels that exist on either side of all - connections they are required for. - The spoiler log looks so much nicer this way, - it gets rid of a bunch of event items, only leaving a couple. :) - """ - region = StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] - - return ( - self._witness_meets_item_requirements(panel, world, player, player_logic, locat) - and self.can_reach(region, "Region", player) - ) - def _witness_can_solve_panels(self, panel_hex_to_solve_set, world, player, player_logic: WitnessPlayerLogic, locat): """ Checks whether a set of panels can be solved. @@ -120,7 +116,12 @@ class WitnessLogic(LogicMixin): valid_option = True for panel in option: - if not self._witness_can_solve_panel(panel, world, player, player_logic, locat): + if panel in player_logic.DOOR_ITEMS_BY_ID: + if all({not self.has(item, player) for item in player_logic.DOOR_ITEMS_BY_ID[panel]}): + valid_option = False + break + + elif not self._witness_can_solve_panel(panel, world, player, player_logic, locat): valid_option = False break diff --git a/worlds/witness/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt similarity index 94% rename from worlds/witness/Disable_Unrandomized.txt rename to worlds/witness/settings/Disable_Unrandomized.txt index cad3804f34..6f957ada5c 100644 --- a/worlds/witness/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -1,18 +1,16 @@ Event Items: -Shadows Laser Activation - 0x00021,0x17D28,0x17C71 -Keep Laser Activation - 0x03317 -Bunker Laser Activation - 0x00061,0x17D01,0x17C42 -Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9,0x17CA4 Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x00B8D,0x17CF7 +Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9,0x17CA4 +Bunker Laser Activation - 0x00061,0x17D01,0x17C42 +Shadows Laser Activation - 0x00021,0x17D28,0x17C71 Requirement Changes: -0x17CA4 - True - True -0x28B39 - 0x2896A - Reflection +0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 | 0x17CA4 +0x0C2B2 - 0x00061 | 0x17D01 | 0x17C42 +0x181B3 - 0x00021 | 0x17D28 | 0x17C71 +0x28B39 - True - Reflection 0x17CAB - True - True -Region Changes: -Quarry (Quarry) - Outside Quarry - 0x17C09 - Quarry Mill - 0x275ED - Quarry Mill - 0x17CAC - Disabled Locations: 0x03505 (Tutorial Gate Close) 0x0C335 (Tutorial Pillar) diff --git a/worlds/witness/settings/Door_Panel_Shuffle.txt b/worlds/witness/settings/Door_Panel_Shuffle.txt new file mode 100644 index 0000000000..d6982f52e3 --- /dev/null +++ b/worlds/witness/settings/Door_Panel_Shuffle.txt @@ -0,0 +1,31 @@ +Items: +Glass Factory Entry Door (Panel) +Door to Symmetry Island Lower (Panel) +Door to Symmetry Island Upper (Panel) +Door to Desert Flood Light Room (Panel) +Desert Flood Room Flood Controls (Panel) +Quarry Door to Mill (Panel) +Quarry Mill Ramp Controls (Panel) +Quarry Mill Elevator Controls (Panel) +Quarry Boathouse Ramp Height Control (Panel) +Quarry Boathouse Ramp Horizontal Control (Panel) +Shadows Door Timer (Panel) +Monastery Entry Door Left (Panel) +Monastery Entry Door Right (Panel) +Town Door to RGB House (Panel) +Town Door to Church (Panel) +Town Maze Panel (Drop-Down Staircase) (Panel) +Windmill Door (Panel) +Treehouse First & Second Doors (Panel) +Treehouse Third Door (Panel) +Treehouse Laser House Door Timer (Panel) +Treehouse Shortcut Drop-Down Bridge (Panel) +Jungle Popup Wall (Panel) +Bunker Entry Door (Panel) +Inside Bunker Door to Bunker Proper (Panel) +Bunker Elevator Control (Panel) +Swamp Entry Door (Panel) +Swamp Sliding Bridge (Panel) +Swamp Rotating Bridge (Panel) +Swamp Maze Control (Panel) +Boat diff --git a/worlds/witness/settings/Doors_Complex.txt b/worlds/witness/settings/Doors_Complex.txt new file mode 100644 index 0000000000..c62562e32a --- /dev/null +++ b/worlds/witness/settings/Doors_Complex.txt @@ -0,0 +1,201 @@ +Items: +Outside Tutorial Optional Door +Outside Tutorial Outpost Entry Door +Outside Tutorial Outpost Exit Door +Glass Factory Entry Door +Glass Factory Back Wall +Symmetry Island Lower Door +Symmetry Island Upper Door +Orchard Middle Gate +Orchard Final Gate +Desert Door to Flood Light Room +Desert Door to Pond Room +Desert Door to Water Levels Room +Desert Door to Elevator Room +Quarry Main Entry 1 +Quarry Main Entry 2 +Quarry Door to Mill +Quarry Mill Side Door +Quarry Mill Rooftop Shortcut +Quarry Mill Stairs +Quarry Boathouse Boat Staircase +Quarry Boathouse First Barrier +Quarry Boathouse Shortcut +Shadows Timed Door +Shadows Laser Room Right Door +Shadows Laser Room Left Door +Shadows Barrier to Quarry +Shadows Barrier to Ledge +Keep Hedge Maze 1 Exit Door +Keep Pressure Plates 1 Exit Door +Keep Hedge Maze 2 Shortcut +Keep Hedge Maze 2 Exit Door +Keep Hedge Maze 3 Shortcut +Keep Hedge Maze 3 Exit Door +Keep Hedge Maze 4 Shortcut +Keep Hedge Maze 4 Exit Door +Keep Pressure Plates 2 Exit Door +Keep Pressure Plates 3 Exit Door +Keep Pressure Plates 4 Exit Door +Keep Shortcut to Shadows +Keep Tower Shortcut +Monastery Shortcut +Monastery Inner Door +Monastery Outer Door +Monastery Door to Garden +Town Cargo Box Door +Town Wooden Roof Staircase +Town Tinted Door to RGB House +Town Door to Church +Town Maze Staircase +Town Windmill Door +Town RGB House Staircase +Town Tower Blue Panels Door +Town Tower Lattice Door +Town Tower Environmental Set Door +Town Tower Wooden Roof Set Door +Theater Entry Door +Theater Exit Door Left +Theater Exit Door Right +Jungle Bamboo Shortcut to River +Jungle Popup Wall +River Shortcut to Monastery Garden +Bunker Bunker Entry Door +Bunker Tinted Glass Door +Bunker Door to Ultraviolet Room +Bunker Door to Elevator +Swamp Entry Door +Swamp Door to Broken Shapers +Swamp Platform Shortcut Door +Swamp Cyan Water Pump +Swamp Door to Rotated Shapers +Swamp Red Water Pump +Swamp Red Underwater Exit +Swamp Blue Water Pump +Swamp Purple Water Pump +Swamp Near Laser Shortcut +Treehouse First Door +Treehouse Second Door +Treehouse Beyond Yellow Bridge Door +Treehouse Drawbridge +Treehouse Timed Door to Laser House +Inside Mountain First Layer Exit Door +Inside Mountain Second Layer Staircase Near +Inside Mountain Second Layer Exit Door +Inside Mountain Second Layer Staircase Far +Inside Mountain Giant Puzzle Exit Door +Inside Mountain Door to Final Room +Inside Mountain Bottom Layer Rock +Inside Mountain Door to Secret Area +Caves Pillar Door +Caves Mountain Shortcut +Caves Swamp Shortcut +Challenge Entry Door +Challenge Door to Theater Walkway +Theater Walkway Door to Windmill Interior +Theater Walkway Door to Desert Elevator Room +Theater Walkway Door to Town + +Added Locations: +Outside Tutorial Door to Outpost Panel +Outside Tutorial Exit Door from Outpost Panel +Glass Factory Entry Door Panel +Glass Factory Vertical Symmetry 5 +Symmetry Island Door to Symmetry Island Lower Panel +Symmetry Island Door to Symmetry Island Upper Panel +Orchard Apple Tree 3 +Orchard Apple Tree 5 +Desert Door to Desert Flood Light Room Panel +Desert Artificial Light Reflection 3 +Desert Door to Water Levels Room Panel +Desert Flood Reflection 6 +Quarry Door to Quarry 1 Panel +Quarry Door to Quarry 2 Panel +Quarry Door to Mill Right +Quarry Door to Mill Left +Quarry Mill Ground Floor Shortcut Door Panel +Quarry Mill Door to Outside Quarry Stairs Panel +Quarry Mill Stair Control +Quarry Boathouse Shortcut Door Panel +Shadows Door Timer Inside +Shadows Door Timer Outside +Shadows Environmental Avoid 8 +Shadows Follow 5 +Shadows Lower Avoid 3 +Shadows Lower Avoid 5 +Keep Hedge Maze 1 +Keep Pressure Plates 1 +Keep Hedge Maze 2 +Keep Hedge Maze 3 +Keep Hedge Maze 4 +Keep Pressure Plates 2 +Keep Pressure Plates 3 +Keep Pressure Plates 4 +Keep Shortcut to Shadows Panel +Keep Tower Shortcut to Keep Panel +Monastery Shortcut Door Panel +Monastery Door Open Left +Monastery Door Open Right +Monastery Rhombic Avoid 3 +Town Cargo Box Panel +Town Full Dot Grid Shapers 5 +Town Tinted Door Panel +Town Door to Church Stars Panel +Town Maze Stair Control +Town Windmill Door Panel +Town Sound Room Left +Town Sound Room Right +Town Symmetry Squares 5 + Dots +Town Church Lattice +Town Hexagonal Reflection +Town Shapers & Dots & Eraser +Windmill Door to Front of Theater Panel +Theater Door to Cargo Box Left Panel +Theater Door to Cargo Box Right Panel +Jungle Shortcut to River Panel +Jungle Popup Wall Control +River Rhombic Avoid to Monastery Garden +Bunker Bunker Entry Panel +Bunker Door to Bunker Proper Panel +Bunker Drawn Squares through Tinted Glass 3 +Bunker Drop-Down Door Squares 2 +Swamp Entry Panel +Swamp Platform Shapers 4 +Swamp Platform Shortcut Right Panel +Swamp Blue Underwater Negative Shapers 5 +Swamp Broken Shapers 4 +Swamp Cyan Underwater Negative Shapers 5 +Swamp Red Underwater Negative Shapers 4 +Swamp More Rotated Shapers 4 +Swamp More Rotated Shapers 4 +Swamp Near Laser Shortcut Right Panel +Treehouse First Door Panel +Treehouse Second Door Panel +Treehouse Beyond Yellow Bridge Door Panel +Treehouse Bridge Control +Treehouse Left Orange Bridge 15 +Treehouse Right Orange Bridge 12 +Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Inside Control +Inside Mountain Moving Background 7 +Inside Mountain Obscured Vision 5 +Inside Mountain Physically Obstructed 3 +Inside Mountain Angled Inside Trash 2 +Inside Mountain Color Cycle 5 +Inside Mountain Light Bridge Controller 2 +Inside Mountain Light Bridge Controller 3 +Inside Mountain Same Solution 6 +Inside Mountain Giant Puzzle +Inside Mountain Door to Final Room Left +Inside Mountain Door to Final Room Right +Inside Mountain Bottom Layer Discard +Inside Mountain Rock Control +Inside Mountain Secret Area Entry Panel +Inside Mountain Caves Lone Pillar +Inside Mountain Caves Shortcut to Mountain Panel +Inside Mountain Caves Shortcut to Swamp Panel +Inside Mountain Caves Challenge Entry Panel +Challenge Door to Theater Walkway Panel +Theater Walkway Theater Shortcut Panel +Theater Walkway Desert Shortcut Panel +Theater Walkway Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Max.txt b/worlds/witness/settings/Doors_Max.txt new file mode 100644 index 0000000000..ec0a56a597 --- /dev/null +++ b/worlds/witness/settings/Doors_Max.txt @@ -0,0 +1,212 @@ +Items: +Outside Tutorial Optional Door +Outside Tutorial Outpost Entry Door +Outside Tutorial Outpost Exit Door +Glass Factory Entry Door +Glass Factory Back Wall +Symmetry Island Lower Door +Symmetry Island Upper Door +Orchard Middle Gate +Orchard Final Gate +Desert Door to Flood Light Room +Desert Door to Pond Room +Desert Door to Water Levels Room +Desert Door to Elevator Room +Quarry Main Entry 1 +Quarry Main Entry 2 +Quarry Door to Mill +Quarry Mill Side Door +Quarry Mill Rooftop Shortcut +Quarry Mill Stairs +Quarry Boathouse Boat Staircase +Quarry Boathouse First Barrier +Quarry Boathouse Shortcut +Shadows Timed Door +Shadows Laser Room Right Door +Shadows Laser Room Left Door +Shadows Barrier to Quarry +Shadows Barrier to Ledge +Keep Hedge Maze 1 Exit Door +Keep Pressure Plates 1 Exit Door +Keep Hedge Maze 2 Shortcut +Keep Hedge Maze 2 Exit Door +Keep Hedge Maze 3 Shortcut +Keep Hedge Maze 3 Exit Door +Keep Hedge Maze 4 Shortcut +Keep Hedge Maze 4 Exit Door +Keep Pressure Plates 2 Exit Door +Keep Pressure Plates 3 Exit Door +Keep Pressure Plates 4 Exit Door +Keep Shortcut to Shadows +Keep Tower Shortcut +Monastery Shortcut +Monastery Inner Door +Monastery Outer Door +Monastery Door to Garden +Town Cargo Box Door +Town Wooden Roof Staircase +Town Tinted Door to RGB House +Town Door to Church +Town Maze Staircase +Town Windmill Door +Town RGB House Staircase +Town Tower Blue Panels Door +Town Tower Lattice Door +Town Tower Environmental Set Door +Town Tower Wooden Roof Set Door +Theater Entry Door +Theater Exit Door Left +Theater Exit Door Right +Jungle Bamboo Shortcut to River +Jungle Popup Wall +River Shortcut to Monastery Garden +Bunker Bunker Entry Door +Bunker Tinted Glass Door +Bunker Door to Ultraviolet Room +Bunker Door to Elevator +Swamp Entry Door +Swamp Door to Broken Shapers +Swamp Platform Shortcut Door +Swamp Cyan Water Pump +Swamp Door to Rotated Shapers +Swamp Red Water Pump +Swamp Red Underwater Exit +Swamp Blue Water Pump +Swamp Purple Water Pump +Swamp Near Laser Shortcut +Treehouse First Door +Treehouse Second Door +Treehouse Beyond Yellow Bridge Door +Treehouse Drawbridge +Treehouse Timed Door to Laser House +Inside Mountain First Layer Exit Door +Inside Mountain Second Layer Staircase Near +Inside Mountain Second Layer Exit Door +Inside Mountain Second Layer Staircase Far +Inside Mountain Giant Puzzle Exit Door +Inside Mountain Door to Final Room +Inside Mountain Bottom Layer Rock +Inside Mountain Door to Secret Area +Caves Pillar Door +Caves Mountain Shortcut +Caves Swamp Shortcut +Challenge Entry Door +Challenge Door to Theater Walkway +Theater Walkway Door to Windmill Interior +Theater Walkway Door to Desert Elevator Room +Theater Walkway Door to Town + +Desert Flood Room Flood Controls (Panel) +Quarry Mill Ramp Controls (Panel) +Quarry Mill Elevator Controls (Panel) +Quarry Boathouse Ramp Height Control (Panel) +Quarry Boathouse Ramp Horizontal Control (Panel) +Bunker Elevator Control (Panel) +Swamp Sliding Bridge (Panel) +Swamp Rotating Bridge (Panel) +Swamp Maze Control (Panel) +Boat + +Added Locations: +Outside Tutorial Door to Outpost Panel +Outside Tutorial Exit Door from Outpost Panel +Glass Factory Entry Door Panel +Glass Factory Vertical Symmetry 5 +Symmetry Island Door to Symmetry Island Lower Panel +Symmetry Island Door to Symmetry Island Upper Panel +Orchard Apple Tree 3 +Orchard Apple Tree 5 +Desert Door to Desert Flood Light Room Panel +Desert Artificial Light Reflection 3 +Desert Door to Water Levels Room Panel +Desert Flood Reflection 6 +Quarry Door to Quarry 1 Panel +Quarry Door to Quarry 2 Panel +Quarry Door to Mill Right +Quarry Door to Mill Left +Quarry Mill Ground Floor Shortcut Door Panel +Quarry Mill Door to Outside Quarry Stairs Panel +Quarry Mill Stair Control +Quarry Boathouse Shortcut Door Panel +Shadows Door Timer Inside +Shadows Door Timer Outside +Shadows Environmental Avoid 8 +Shadows Follow 5 +Shadows Lower Avoid 3 +Shadows Lower Avoid 5 +Keep Hedge Maze 1 +Keep Pressure Plates 1 +Keep Hedge Maze 2 +Keep Hedge Maze 3 +Keep Hedge Maze 4 +Keep Pressure Plates 2 +Keep Pressure Plates 3 +Keep Pressure Plates 4 +Keep Shortcut to Shadows Panel +Keep Tower Shortcut to Keep Panel +Monastery Shortcut Door Panel +Monastery Door Open Left +Monastery Door Open Right +Monastery Rhombic Avoid 3 +Town Cargo Box Panel +Town Full Dot Grid Shapers 5 +Town Tinted Door Panel +Town Door to Church Stars Panel +Town Maze Stair Control +Town Windmill Door Panel +Town Sound Room Left +Town Sound Room Right +Town Symmetry Squares 5 + Dots +Town Church Lattice +Town Hexagonal Reflection +Town Shapers & Dots & Eraser +Windmill Door to Front of Theater Panel +Theater Door to Cargo Box Left Panel +Theater Door to Cargo Box Right Panel +Jungle Shortcut to River Panel +Jungle Popup Wall Control +River Rhombic Avoid to Monastery Garden +Bunker Bunker Entry Panel +Bunker Door to Bunker Proper Panel +Bunker Drawn Squares through Tinted Glass 3 +Bunker Drop-Down Door Squares 2 +Swamp Entry Panel +Swamp Platform Shapers 4 +Swamp Platform Shortcut Right Panel +Swamp Blue Underwater Negative Shapers 5 +Swamp Broken Shapers 4 +Swamp Cyan Underwater Negative Shapers 5 +Swamp Red Underwater Negative Shapers 4 +Swamp More Rotated Shapers 4 +Swamp More Rotated Shapers 4 +Swamp Near Laser Shortcut Right Panel +Treehouse First Door Panel +Treehouse Second Door Panel +Treehouse Beyond Yellow Bridge Door Panel +Treehouse Bridge Control +Treehouse Left Orange Bridge 15 +Treehouse Right Orange Bridge 12 +Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Inside Control +Inside Mountain Moving Background 7 +Inside Mountain Obscured Vision 5 +Inside Mountain Physically Obstructed 3 +Inside Mountain Angled Inside Trash 2 +Inside Mountain Color Cycle 5 +Inside Mountain Light Bridge Controller 2 +Inside Mountain Light Bridge Controller 3 +Inside Mountain Same Solution 6 +Inside Mountain Giant Puzzle +Inside Mountain Door to Final Room Left +Inside Mountain Door to Final Room Right +Inside Mountain Bottom Layer Discard +Inside Mountain Rock Control +Inside Mountain Secret Area Entry Panel +Inside Mountain Caves Lone Pillar +Inside Mountain Caves Shortcut to Mountain Panel +Inside Mountain Caves Shortcut to Swamp Panel +Inside Mountain Caves Challenge Entry Panel +Challenge Door to Theater Walkway Panel +Theater Walkway Theater Shortcut Panel +Theater Walkway Desert Shortcut Panel +Theater Walkway Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Simple.txt b/worlds/witness/settings/Doors_Simple.txt new file mode 100644 index 0000000000..1335456d95 --- /dev/null +++ b/worlds/witness/settings/Doors_Simple.txt @@ -0,0 +1,146 @@ +Items: +Glass Factory Back Wall +Quarry Boathouse Boat Staircase +Outside Tutorial Outpost Doors +Glass Factory Entry Door +Symmetry Island Doors +Orchard Gates +Desert Doors +Quarry Main Entry +Quarry Door to Mill +Quarry Mill Shortcuts +Quarry Boathouse Barriers +Shadows Timed Door +Shadows Laser Room Door +Shadows Barriers +Keep Hedge Maze Doors +Keep Pressure Plates Doors +Keep Shortcuts +Monastery Entry Door +Monastery Shortcuts +Town Doors +Town Tower Doors +Theater Entry Door +Theater Exit Door +Jungle & River Shortcuts +Jungle Popup Wall +Bunker Doors +Swamp Doors +Swamp Near Laser Shortcut +Swamp Water Pumps +Treehouse Entry Doors +Treehouse Drawbridge +Treehouse Timed Door to Laser House +Inside Mountain First Layer Exit Door +Inside Mountain Second Layer Stairs & Doors +Inside Mountain Giant Puzzle Exit Door +Inside Mountain Door to Final Room +Inside Mountain Bottom Layer Doors to Caves +Caves Doors to Challenge +Caves Exits to Main Island +Challenge Door to Theater Walkway +Theater Walkway Doors + +Added Locations: +Outside Tutorial Door to Outpost Panel +Outside Tutorial Exit Door from Outpost Panel +Glass Factory Entry Door Panel +Glass Factory Vertical Symmetry 5 +Symmetry Island Door to Symmetry Island Lower Panel +Symmetry Island Door to Symmetry Island Upper Panel +Orchard Apple Tree 3 +Orchard Apple Tree 5 +Desert Door to Desert Flood Light Room Panel +Desert Artificial Light Reflection 3 +Desert Door to Water Levels Room Panel +Desert Flood Reflection 6 +Quarry Door to Quarry 1 Panel +Quarry Door to Quarry 2 Panel +Quarry Door to Mill Right +Quarry Door to Mill Left +Quarry Mill Ground Floor Shortcut Door Panel +Quarry Mill Door to Outside Quarry Stairs Panel +Quarry Mill Stair Control +Quarry Boathouse Shortcut Door Panel +Shadows Door Timer Inside +Shadows Door Timer Outside +Shadows Environmental Avoid 8 +Shadows Follow 5 +Shadows Lower Avoid 3 +Shadows Lower Avoid 5 +Keep Hedge Maze 1 +Keep Pressure Plates 1 +Keep Hedge Maze 2 +Keep Hedge Maze 3 +Keep Hedge Maze 4 +Keep Pressure Plates 2 +Keep Pressure Plates 3 +Keep Pressure Plates 4 +Keep Shortcut to Shadows Panel +Keep Tower Shortcut to Keep Panel +Monastery Shortcut Door Panel +Monastery Door Open Left +Monastery Door Open Right +Monastery Rhombic Avoid 3 +Town Cargo Box Panel +Town Full Dot Grid Shapers 5 +Town Tinted Door Panel +Town Door to Church Stars Panel +Town Maze Stair Control +Town Windmill Door Panel +Town Sound Room Left +Town Sound Room Right +Town Symmetry Squares 5 + Dots +Town Church Lattice +Town Hexagonal Reflection +Town Shapers & Dots & Eraser +Windmill Door to Front of Theater Panel +Theater Door to Cargo Box Left Panel +Theater Door to Cargo Box Right Panel +Jungle Shortcut to River Panel +Jungle Popup Wall Control +River Rhombic Avoid to Monastery Garden +Bunker Bunker Entry Panel +Bunker Door to Bunker Proper Panel +Bunker Drawn Squares through Tinted Glass 3 +Bunker Drop-Down Door Squares 2 +Swamp Entry Panel +Swamp Platform Shapers 4 +Swamp Platform Shortcut Right Panel +Swamp Blue Underwater Negative Shapers 5 +Swamp Broken Shapers 4 +Swamp Cyan Underwater Negative Shapers 5 +Swamp Red Underwater Negative Shapers 4 +Swamp More Rotated Shapers 4 +Swamp More Rotated Shapers 4 +Swamp Near Laser Shortcut Right Panel +Treehouse First Door Panel +Treehouse Second Door Panel +Treehouse Beyond Yellow Bridge Door Panel +Treehouse Bridge Control +Treehouse Left Orange Bridge 15 +Treehouse Right Orange Bridge 12 +Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Inside Control +Inside Mountain Moving Background 7 +Inside Mountain Obscured Vision 5 +Inside Mountain Physically Obstructed 3 +Inside Mountain Angled Inside Trash 2 +Inside Mountain Color Cycle 5 +Inside Mountain Light Bridge Controller 2 +Inside Mountain Light Bridge Controller 3 +Inside Mountain Same Solution 6 +Inside Mountain Giant Puzzle +Inside Mountain Door to Final Room Left +Inside Mountain Door to Final Room Right +Inside Mountain Bottom Layer Discard +Inside Mountain Rock Control +Inside Mountain Secret Area Entry Panel +Inside Mountain Caves Lone Pillar +Inside Mountain Caves Shortcut to Mountain Panel +Inside Mountain Caves Shortcut to Swamp Panel +Inside Mountain Caves Challenge Entry Panel +Challenge Door to Theater Walkway Panel +Theater Walkway Theater Shortcut Panel +Theater Walkway Desert Shortcut Panel +Theater Walkway Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/Early_UTM.txt b/worlds/witness/settings/Early_UTM.txt new file mode 100644 index 0000000000..893f29d8bb --- /dev/null +++ b/worlds/witness/settings/Early_UTM.txt @@ -0,0 +1,9 @@ +Items: +Caves Exits to Main Island + +Starting Inventory: +Caves Exits to Main Island + +Remove Items: +Caves Mountain Shortcut +Caves Swamp Shortcut \ No newline at end of file diff --git a/worlds/witness/settings/Laser_Shuffle.txt b/worlds/witness/settings/Laser_Shuffle.txt new file mode 100644 index 0000000000..668a13f94a --- /dev/null +++ b/worlds/witness/settings/Laser_Shuffle.txt @@ -0,0 +1,12 @@ +Items: +Symmetry Laser +Desert Laser +Keep Laser +Shadows Laser +Quarry Laser +Town Laser +Swamp Laser +Jungle Laser +Bunker Laser +Monastery Laser +Treehouse Laser \ No newline at end of file diff --git a/worlds/witness/settings/Symbol_Shuffle.txt b/worlds/witness/settings/Symbol_Shuffle.txt new file mode 100644 index 0000000000..d03391f5c5 --- /dev/null +++ b/worlds/witness/settings/Symbol_Shuffle.txt @@ -0,0 +1,14 @@ +Items: +Dots +Colored Dots +Sound Dots +Symmetry +Triangles +Eraser +Shapers +Rotated Shapers +Negative Shapers +Stars +Stars + Same Colored Symbol +Black/White Squares +Colored Squares \ No newline at end of file diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py index 5f1d77d314..646957c462 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -5,12 +5,11 @@ from .utils import define_new_region, parse_lambda class StaticWitnessLogic: ALL_SYMBOL_ITEMS = set() + ALL_DOOR_ITEMS = set() + ALL_DOOR_ITEMS_AS_DICT = dict() ALL_USEFULS = set() ALL_TRAPS = set() ALL_BOOSTS = set() - ALL_DOOR_ITEM_IDS_BY_HEX = dict() - DOOR_NAMES_BY_HEX = dict() - ALL_DOOR_ITEMS = set() CONNECTIONS_TO_SEVER_BY_DOOR_HEX = dict() EVENT_PANELS_FROM_REGIONS = set() @@ -47,35 +46,23 @@ class StaticWitnessLogic: if line == "Usefuls:": current_set = self.ALL_USEFULS continue + if line == "Doors:": + current_set = self.ALL_DOOR_ITEMS + continue if line == "": continue line_split = line.split(" - ") - if current_set is not self.ALL_USEFULS: - current_set.add((line_split[1], int(line_split[0]))) - else: + if current_set is self.ALL_USEFULS: current_set.add((line_split[1], int(line_split[0]), line_split[2] == "True")) + elif current_set is self.ALL_DOOR_ITEMS: + new_door = (line_split[1], int(line_split[0]), frozenset(line_split[2].split(","))) + current_set.add(new_door) + self.ALL_DOOR_ITEMS_AS_DICT[line_split[1]] = new_door + else: + current_set.add((line_split[1], int(line_split[0]))) - path = os.path.join(os.path.dirname(__file__), "Door_Shuffle.txt") - with open(path, "r", encoding="utf-8") as file: - for line in file.readlines(): - line = line.strip() - - line_split = line.split(" - ") - - hex_set_split = line_split[1].split(",") - - sever_list = line_split[2].split(",") - sever_set = {sever_panel for sever_panel in sever_list if sever_panel != "None"} - - for door_hex in hex_set_split: - self.ALL_DOOR_ITEM_IDS_BY_HEX[door_hex] = int(line_split[0]) - self.CONNECTIONS_TO_SEVER_BY_DOOR_HEX[door_hex] = sever_set - - if len(line_split) > 3: - self.DOOR_NAMES_BY_HEX[door_hex] = line_split[3] - def read_logic_file(self): """ Reads the logic file and does the initial population of data structures @@ -84,10 +71,7 @@ class StaticWitnessLogic: with open(path, "r", encoding="utf-8") as file: current_region = dict() - discard_ids = 0 - normal_panel_ids = 0 - vault_ids = 0 - laser_ids = 0 + counter = 0 for line in file.readlines(): line = line.strip() @@ -95,7 +79,7 @@ class StaticWitnessLogic: if line == "": continue - if line[0] != "0": + if line[-1] == ":": new_region_and_connections = define_new_region(line) current_region = new_region_and_connections[0] region_name = current_region["name"] @@ -105,12 +89,33 @@ class StaticWitnessLogic: line_split = line.split(" - ") + location_id = line_split.pop(0) + check_name_full = line_split.pop(0) check_hex = check_name_full[0:7] check_name = check_name_full[9:-1] required_panel_lambda = line_split.pop(0) + + if location_id == "Door" or location_id == "Laser": + self.CHECKS_BY_HEX[check_hex] = { + "checkName": current_region["shortName"] + " " + check_name, + "checkHex": check_hex, + "region": current_region, + "id": None, + "panelType": location_id + } + + self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex] + + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = { + "panels": parse_lambda(required_panel_lambda) + } + + current_region["panels"].add(check_hex) + continue + required_item_lambda = line_split.pop(0) laser_names = { @@ -123,53 +128,14 @@ class StaticWitnessLogic: if "Discard" in check_name: location_type = "Discard" - location_id = discard_ids - discard_ids += 1 elif is_vault_or_video or check_name == "Tutorial Gate Close": location_type = "Vault" - location_id = vault_ids - vault_ids += 1 elif check_name in laser_names: location_type = "Laser" - location_id = laser_ids - laser_ids += 1 else: location_type = "General" - if check_hex == "0x012D7": # Compatibility - normal_panel_ids += 1 - - if check_hex == "0x17E07": # Compatibility - location_id = 112 - - elif check_hex == "0xFFF00": - location_id = 800 - - else: - location_id = normal_panel_ids - normal_panel_ids += 1 - required_items = parse_lambda(required_item_lambda) - items_actually_in_the_game = {item[0] for item in self.ALL_SYMBOL_ITEMS} - required_items = set( - subset.intersection(items_actually_in_the_game) - for subset in required_items - ) - - doors_in_the_game = self.ALL_DOOR_ITEM_IDS_BY_HEX.keys() - if check_hex in doors_in_the_game: - door_name = current_region["shortName"] + " " + check_name + " Power On" - if check_hex in self.DOOR_NAMES_BY_HEX.keys(): - door_name = self.DOOR_NAMES_BY_HEX[check_hex] - - required_items = set( - subset.union(frozenset({door_name})) - for subset in required_items - ) - - self.ALL_DOOR_ITEMS.add( - (door_name, self.ALL_DOOR_ITEM_IDS_BY_HEX[check_hex]) - ) required_items = frozenset(required_items) @@ -182,7 +148,7 @@ class StaticWitnessLogic: "checkName": current_region["shortName"] + " " + check_name, "checkHex": check_hex, "region": current_region, - "idOffset": location_id, + "id": int(location_id), "panelType": location_type } diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py index df4b43717e..809b2b1c3d 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/utils.py @@ -98,9 +98,39 @@ def get_adjustment_file(adjustment_file): @cache_argsless def get_disable_unrandomized_list(): - return get_adjustment_file("Disable_Unrandomized.txt") + return get_adjustment_file("settings/Disable_Unrandomized.txt") @cache_argsless def get_early_utm_list(): - return get_adjustment_file("Early_UTM.txt") \ No newline at end of file + return get_adjustment_file("settings/Early_UTM.txt") + + +@cache_argsless +def get_symbol_shuffle_list(): + return get_adjustment_file("settings/Symbol_Shuffle.txt") + + +@cache_argsless +def get_door_panel_shuffle_list(): + return get_adjustment_file("settings/Door_Panel_Shuffle.txt") + + +@cache_argsless +def get_doors_simple_list(): + return get_adjustment_file("settings/Doors_Simple.txt") + + +@cache_argsless +def get_doors_complex_list(): + return get_adjustment_file("settings/Doors_Complex.txt") + + +@cache_argsless +def get_doors_max_list(): + return get_adjustment_file("settings/Doors_Max.txt") + + +@cache_argsless +def get_laser_shuffle(): + return get_adjustment_file("settings/Laser_Shuffle.txt") From 025309ec64e4449ee82614059602d7f2447618a9 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 17 Jul 2022 20:40:23 -0400 Subject: [PATCH 033/138] SMZ3: Pedestal hint (#792) * - fixed missing pedestal and tablets hint text for foreign items (was "Don't waste yout time!", is now "A small victory!") - small precision to SMZ3 and SM docs about "What does another world's item look like in Super Metroid" --- worlds/sm/docs/en_Super Metroid.md | 3 ++- worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml | 4 ++++ worlds/smz3/docs/en_SMZ3.md | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/sm/docs/en_Super Metroid.md b/worlds/sm/docs/en_Super Metroid.md index 44a292f582..941cbf48cf 100644 --- a/worlds/sm/docs/en_Super Metroid.md +++ b/worlds/sm/docs/en_Super Metroid.md @@ -23,7 +23,8 @@ certain items to your own world. ## What does another world's item look like in Super Metroid? -A unique item sprite has been added to the game to represent items belonging to another world. +Two unique item sprites have been added to the game to represent items belonging to another world. Progression items have +a small up arrow on the sprite and non-progression don't. ## When the player receives an item, what happens? diff --git a/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml b/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml index c0eae5bbbe..12b5271eab 100644 --- a/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml +++ b/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml @@ -380,6 +380,10 @@ Items: Keycard: |- A key from the future? + + Something: |- + A small victory! + default: |- Don't waste your time! diff --git a/worlds/smz3/docs/en_SMZ3.md b/worlds/smz3/docs/en_SMZ3.md index 91dace7b4d..f0302d12f3 100644 --- a/worlds/smz3/docs/en_SMZ3.md +++ b/worlds/smz3/docs/en_SMZ3.md @@ -23,7 +23,8 @@ certain items to your own world. ## What does another world's item look like in Super Metroid? -A unique item sprite has been added to the game to represent items belonging to another world. +Two unique item sprites have been added to the game to represent items belonging to another world. Progression items have +a small up arrow on the sprite and non-progression don't. ## What does another world's item look like in LttP? From 9f5e40283ad5fb3c26bfacef424c15364a5cad9f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 18 Jul 2022 21:10:29 +0200 Subject: [PATCH 034/138] WebHost: reduce server uptime (#794) * WebHost: attempt to improve wording of server resume * WebHost: reduce default room timeout to 2 hours Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- WebHostLib/models.py | 2 +- WebHostLib/templates/hostRoom.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WebHostLib/models.py b/WebHostLib/models.py index 3d6de6812c..70f0318f85 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -27,7 +27,7 @@ class Room(db.Entity): seed = Required('Seed', index=True) multisave = Optional(buffer, lazy=True) show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always - timeout = Required(int, default=lambda: 6 * 60 * 60) # seconds since last activity to shutdown + timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown tracker = Optional(UUID, index=True) last_port = Optional(int, default=lambda: 0) diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index b5ec01f256..15429e7f8d 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -16,9 +16,9 @@ This room has a Multiworld Tracker enabled.
{% endif %} - This room will be closed after {{ room.timeout//60//60 }} hours of inactivity. Should you wish to continue - later, - you can simply refresh this page and the server will be started again.
+ The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. + Should you wish to continue later, + anyone can simply refresh this page and the server will resume.
{% if room.last_port %} You can connect to this room by using From 45aea2c8ffd728b9f02fbd05c2686284e49740bd Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Mon, 18 Jul 2022 22:44:04 -0700 Subject: [PATCH 035/138] ChecksFinder: Linux support via wine (#795) * ChecksFinder: Linux support via wine * ChecksFinder: account for custom $WINEPREFIX * ChecksFinder: wine detection --- ChecksFinderClient.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/ChecksFinderClient.py b/ChecksFinderClient.py index 3bb96ceb6c..e774b3faa7 100644 --- a/ChecksFinderClient.py +++ b/ChecksFinderClient.py @@ -1,6 +1,8 @@ from __future__ import annotations import os +import sys import asyncio +import shutil import ModuleUpdate ModuleUpdate.update() @@ -32,6 +34,24 @@ class ChecksFinderContext(CommonContext): self.send_index: int = 0 self.syncing = False self.awaiting_bridge = False + # self.game_communication_path: files go in this path to pass data between us and the actual game + if "localappdata" in os.environ: + self.game_communication_path = os.path.expandvars(r"%localappdata%/ChecksFinder") + else: + # not windows. game is an exe so let's see if wine might be around to run it + if "WINEPREFIX" in os.environ: + wineprefix = os.environ["WINEPREFIX"] + elif shutil.which("wine") or shutil.which("wine-stable"): + wineprefix = os.path.expanduser("~/.wine") # default root of wine system data, deep in which is app data + else: + msg = "ChecksFinderClient couldn't detect system type. Unable to infer required game_communication_path" + logger.error("Error: " + msg) + Utils.messagebox("Error", msg, error=True) + sys.exit(1) + self.game_communication_path = os.path.join( + wineprefix, + "drive_c", + os.path.expandvars("users/$USER/Local Settings/Application Data/ChecksFinder")) async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -41,8 +61,7 @@ class ChecksFinderContext(CommonContext): async def connection_closed(self): await super(ChecksFinderContext, self).connection_closed() - path = os.path.expandvars(r"%localappdata%/ChecksFinder") - for root, dirs, files in os.walk(path): + for root, dirs, files in os.walk(self.game_communication_path): for file in files: if file.find("obtain") <= -1: os.remove(root + "/" + file) @@ -56,26 +75,25 @@ class ChecksFinderContext(CommonContext): async def shutdown(self): await super(ChecksFinderContext, self).shutdown() - path = os.path.expandvars(r"%localappdata%/ChecksFinder") - for root, dirs, files in os.walk(path): + for root, dirs, files in os.walk(self.game_communication_path): for file in files: if file.find("obtain") <= -1: os.remove(root+"/"+file) def on_package(self, cmd: str, args: dict): if cmd in {"Connected"}: - if not os.path.exists(os.path.expandvars(r"%localappdata%/ChecksFinder")): - os.mkdir(os.path.expandvars(r"%localappdata%/ChecksFinder")) + if not os.path.exists(self.game_communication_path): + os.makedirs(self.game_communication_path) for ss in self.checked_locations: filename = f"send{ss}" - with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f: + with open(os.path.join(self.game_communication_path, filename), 'w') as f: f.close() if cmd in {"ReceivedItems"}: start_index = args["index"] if start_index != len(self.items_received): for item in args['items']: filename = f"AP_{str(NetworkItem(*item).location)}PLR{str(NetworkItem(*item).player)}.item" - with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f: + with open(os.path.join(self.game_communication_path, filename), 'w') as f: f.write(str(NetworkItem(*item).item)) f.close() @@ -83,7 +101,7 @@ class ChecksFinderContext(CommonContext): if "checked_locations" in args: for ss in self.checked_locations: filename = f"send{ss}" - with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f: + with open(os.path.join(self.game_communication_path, filename), 'w') as f: f.close() def run_gui(self): @@ -109,10 +127,9 @@ async def game_watcher(ctx: ChecksFinderContext): sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)}) await ctx.send_msgs(sync_msg) ctx.syncing = False - path = os.path.expandvars(r"%localappdata%/ChecksFinder") sending = [] victory = False - for root, dirs, files in os.walk(path): + for root, dirs, files in os.walk(ctx.game_communication_path): for file in files: if file.find("send") > -1: st = file.split("send", -1)[1] From 8ff2c1b6f3940613ac7e119649e9a1e61ab68ea1 Mon Sep 17 00:00:00 2001 From: Ludovic Marechal Date: Wed, 20 Jul 2022 12:48:14 +0200 Subject: [PATCH 036/138] DS3: Add the Dark Souls 3 World into Archipelago (#769) --- WebHostLib/downloads.py | 2 + WebHostLib/templates/macros.html | 3 + WebHostLib/upload.py | 5 + worlds/dark_souls_3/Options.py | 41 ++ worlds/dark_souls_3/__init__.py | 266 +++++++++++ worlds/dark_souls_3/data/items_data.py | 376 ++++++++++++++++ worlds/dark_souls_3/data/locations_data.py | 422 ++++++++++++++++++ worlds/dark_souls_3/docs/en_Dark Souls III.md | 22 + worlds/dark_souls_3/docs/setup_en.md | 35 ++ 9 files changed, 1172 insertions(+) create mode 100644 worlds/dark_souls_3/Options.py create mode 100644 worlds/dark_souls_3/__init__.py create mode 100644 worlds/dark_souls_3/data/items_data.py create mode 100644 worlds/dark_souls_3/data/locations_data.py create mode 100644 worlds/dark_souls_3/docs/en_Dark Souls III.md create mode 100644 worlds/dark_souls_3/docs/setup_en.md diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 9b93b82c54..0704f5d0ec 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -78,6 +78,8 @@ def download_slot_file(room_id, player_id: int): fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6" elif slot_data.game == "Super Mario 64": fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex" + elif slot_data.game == "Dark Souls III": + fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json" else: return "Game download not supported." return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname) diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 70b41fad9e..6ed2ca492a 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -43,6 +43,9 @@ {% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid", "SMZ3"] %} Download Patch File... + {% elif patch.game == "Dark Souls III" %} + + Download JSON File... {% else %} No file to download for this game. {% endif %} diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index e6b2c7de95..00825df47b 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -80,6 +80,11 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name, player_id=int(slot_id[1:]), game="Ocarina of Time")) + elif file.filename.endswith(".json"): + _, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('-', 3) + slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name, + player_id=int(slot_id[1:]), game="Dark Souls III")) + elif file.filename.endswith(".txt"): spoiler = zfile.open(file, "r").read().decode("utf-8-sig") diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py new file mode 100644 index 0000000000..6b52cf53b6 --- /dev/null +++ b/worlds/dark_souls_3/Options.py @@ -0,0 +1,41 @@ +import typing +from Options import Toggle, Option + + +class AutoEquipOption(Toggle): + """Automatically equips any received armor or left/right weapons.""" + display_name = "Auto-equip" + + +class LockEquipOption(Toggle): + """Lock the equipment slots so you cannot change your armor or your left/right weapons. Works great with the + Auto-equip option.""" + display_name = "Lock Equipement Slots" + + +class NoWeaponRequirementsOption(Toggle): + """Disable the weapon requirements by removing any movement or damage penalties. + Permitting you to use any weapon early""" + display_name = "No Weapon Requirements" + + +class RandomizeWeaponsLevelOption(Toggle): + """Enable this option to upgrade 33% ( based on the probability chance ) of the pool of weapons to a random value + between +1 and +5/+10""" + display_name = "Randomize weapons level" + + +class LateBasinOfVowsOption(Toggle): + """Force the Basin of Vows to be located as a reward of defeating Pontiff Sulyvahn. It permits to ease the + progression by preventing having to kill the Dancer of the Boreal Valley as the first boss""" + display_name = "Late Basin of Vows" + + +dark_souls_options: typing.Dict[str, type(Option)] = { + "auto_equip": AutoEquipOption, + "lock_equip": LockEquipOption, + "no_weapon_requirements": NoWeaponRequirementsOption, + "randomize_weapons_level": RandomizeWeaponsLevelOption, + "late_basin_of_vows": LateBasinOfVowsOption, +} + diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py new file mode 100644 index 0000000000..f8ed9fb0f8 --- /dev/null +++ b/worlds/dark_souls_3/__init__.py @@ -0,0 +1,266 @@ +# world/dark_souls_3/__init__.py +import json +import os + +from .Options import dark_souls_options +from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary_table, key_items_list +from .data.locations_data import location_dictionary_table, cemetery_of_ash_table, fire_link_shrine_table, \ + high_wall_of_lothric, \ + undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \ + farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \ + irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, grand_archives_table, \ + untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table +from ..AutoWorld import World, WebWorld +from BaseClasses import MultiWorld, Location, Region, Item, RegionType, Entrance, Tutorial, ItemClassification +from ..generic.Rules import set_rule + + +class DarkSouls3Web(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up the Archipelago Dark Souls III randomizer on your computer.", + "English", + "setup_en.md", + "setup/en", + ["Marech"] + )] + + +class DarkSouls3World(World): + """ + Dark souls III is an Action role-playing game and is part of the Souls series developed by FromSoftware. + Played in a third-person perspective, players have access to various weapons, armour, magic, and consumables that + they can use to fight their enemies. + """ + + game: str = "Dark Souls III" + options = dark_souls_options + topology_present: bool = True + remote_items: bool = False + remote_start_inventory: bool = False + web = DarkSouls3Web() + data_version = 1 + base_id = 100000 + item_name_to_id = {name: id for id, name in enumerate(item_dictionary_table, base_id)} + location_name_to_id = {name: id for id, name in enumerate(location_dictionary_table, base_id)} + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.locked_items = [] + self.locked_locations = [] + self.main_path_locations = [] + + def create_item(self, name: str) -> Item: + data = self.item_name_to_id[name] + + if name in key_items_list: + item_classification = ItemClassification.progression + elif name in weapons_upgrade_5_table or name in weapons_upgrade_10_table: + item_classification = ItemClassification.useful + else: + item_classification = ItemClassification.filler + + return DarkSouls3Item(name, item_classification, data, self.player) + + def create_regions(self): + menu_region = Region("Menu", RegionType.Generic, "Menu", self.player) + self.world.regions.append(menu_region) + + # Create all Vanilla regions of Dark Souls III + cemetery_of_ash_region = self.create_region("Cemetery Of Ash", cemetery_of_ash_table) + firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table) + firelink_shrine_bell_tower_region = self.create_region("Firelink Shrine Bell Tower", + firelink_shrine_bell_tower_table) + high_wall_of_lothric_region = self.create_region("High Wall of Lothric", high_wall_of_lothric) + undead_settlement_region = self.create_region("Undead Settlement", undead_settlement_table) + road_of_sacrifices_region = self.create_region("Road of Sacrifices", road_of_sacrifice_table) + consumed_king_garden_region = self.create_region("Consumed King's Garden", consumed_king_garden_table) + cathedral_of_the_deep_region = self.create_region("Cathedral of the Deep", cathedral_of_the_deep_table) + farron_keep_region = self.create_region("Farron Keep", farron_keep_table) + catacombs_of_carthus_region = self.create_region("Catacombs of Carthus", catacombs_of_carthus_table) + smouldering_lake_region = self.create_region("Smouldering Lake", smouldering_lake_table) + irithyll_of_the_boreal_valley_region = self.create_region("Irithyll of the Boreal Valley", + irithyll_of_the_boreal_valley_table) + irithyll_dungeon_region = self.create_region("Irithyll Dungeon", irithyll_dungeon_table) + profaned_capital_region = self.create_region("Profaned Capital", profaned_capital_table) + anor_londo_region = self.create_region("Anor Londo", anor_londo_table) + lothric_castle_region = self.create_region("Lothric Castle", lothric_castle_table) + grand_archives_region = self.create_region("Grand Archives", grand_archives_table) + untended_graves_region = self.create_region("Untended Graves", untended_graves_table) + archdragon_peak_region = self.create_region("Archdragon Peak", archdragon_peak_table) + kiln_of_the_first_flame_region = self.create_region("Kiln Of The First Flame", None) + + # Create the entrance to connect those regions + menu_region.exits.append(Entrance(self.player, "New Game", menu_region)) + self.world.get_entrance("New Game", self.player).connect(cemetery_of_ash_region) + cemetery_of_ash_region.exits.append(Entrance(self.player, "Goto Firelink Shrine", cemetery_of_ash_region)) + self.world.get_entrance("Goto Firelink Shrine", self.player).connect(firelink_shrine_region) + firelink_shrine_region.exits.append(Entrance(self.player, "Goto High Wall of Lothric", + firelink_shrine_region)) + firelink_shrine_region.exits.append(Entrance(self.player, "Goto Kiln Of The First Flame", + firelink_shrine_region)) + firelink_shrine_region.exits.append(Entrance(self.player, "Goto Bell Tower", + firelink_shrine_region)) + self.world.get_entrance("Goto High Wall of Lothric", self.player).connect(high_wall_of_lothric_region) + self.world.get_entrance("Goto Kiln Of The First Flame", self.player).connect(kiln_of_the_first_flame_region) + self.world.get_entrance("Goto Bell Tower", self.player).connect(firelink_shrine_bell_tower_region) + high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Undead Settlement", + high_wall_of_lothric_region)) + high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Lothric Castle", + high_wall_of_lothric_region)) + self.world.get_entrance("Goto Undead Settlement", self.player).connect(undead_settlement_region) + self.world.get_entrance("Goto Lothric Castle", self.player).connect(lothric_castle_region) + undead_settlement_region.exits.append(Entrance(self.player, "Goto Road Of Sacrifices", + undead_settlement_region)) + self.world.get_entrance("Goto Road Of Sacrifices", self.player).connect(road_of_sacrifices_region) + road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Cathedral", road_of_sacrifices_region)) + road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Farron keep", road_of_sacrifices_region)) + self.world.get_entrance("Goto Cathedral", self.player).connect(cathedral_of_the_deep_region) + self.world.get_entrance("Goto Farron keep", self.player).connect(farron_keep_region) + farron_keep_region.exits.append(Entrance(self.player, "Goto Carthus catacombs", farron_keep_region)) + self.world.get_entrance("Goto Carthus catacombs", self.player).connect(catacombs_of_carthus_region) + catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Irithyll of the boreal", + catacombs_of_carthus_region)) + catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Smouldering Lake", + catacombs_of_carthus_region)) + self.world.get_entrance("Goto Irithyll of the boreal", self.player).\ + connect(irithyll_of_the_boreal_valley_region) + self.world.get_entrance("Goto Smouldering Lake", self.player).connect(smouldering_lake_region) + irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Irithyll dungeon", + irithyll_of_the_boreal_valley_region)) + irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Anor Londo", + irithyll_of_the_boreal_valley_region)) + self.world.get_entrance("Goto Irithyll dungeon", self.player).connect(irithyll_dungeon_region) + self.world.get_entrance("Goto Anor Londo", self.player).connect(anor_londo_region) + irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Archdragon peak", irithyll_dungeon_region)) + irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Profaned capital", irithyll_dungeon_region)) + self.world.get_entrance("Goto Archdragon peak", self.player).connect(archdragon_peak_region) + self.world.get_entrance("Goto Profaned capital", self.player).connect(profaned_capital_region) + lothric_castle_region.exits.append(Entrance(self.player, "Goto Consumed King Garden", lothric_castle_region)) + lothric_castle_region.exits.append(Entrance(self.player, "Goto Grand Archives", lothric_castle_region)) + self.world.get_entrance("Goto Consumed King Garden", self.player).connect(consumed_king_garden_region) + self.world.get_entrance("Goto Grand Archives", self.player).connect(grand_archives_region) + consumed_king_garden_region.exits.append(Entrance(self.player, "Goto Untended Graves", + consumed_king_garden_region)) + self.world.get_entrance("Goto Untended Graves", self.player).connect(untended_graves_region) + + # For each region, add the associated locations retrieved from the corresponding location_table + def create_region(self, region_name, location_table) -> Region: + new_region = Region(region_name, RegionType.Generic, region_name, self.player) + if location_table: + for name, address in location_table.items(): + location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region) + new_region.locations.append(location) + self.world.regions.append(new_region) + return new_region + + def create_items(self): + for name, address in self.item_name_to_id.items(): + # Specific items will be included in the item pool under certain conditions. See generate_basic + if name != "Basin of Vows": + self.world.itempool += [self.create_item(name)] + + def generate_early(self): + pass + + def set_rules(self) -> None: + + # Define the access rules to the entrances + set_rule(self.world.get_entrance("Goto Bell Tower", self.player), + lambda state: state.has("Mortician's Ashes", self.player)) + set_rule(self.world.get_entrance("Goto Undead Settlement", self.player), + lambda state: state.has("Small Lothric Banner", self.player)) + set_rule(self.world.get_entrance("Goto Lothric Castle", self.player), + lambda state: state.has("Basin of Vows", self.player)) + set_rule(self.world.get_location("HWL: Soul of the Dancer", self.player), + lambda state: state.has("Basin of Vows", self.player)) + set_rule(self.world.get_entrance("Goto Irithyll of the boreal", self.player), + lambda state: state.has("Small Doll", self.player)) + set_rule(self.world.get_entrance("Goto Archdragon peak", self.player), + lambda state: state.has("Path of the Dragon Gesture", self.player)) + set_rule(self.world.get_entrance("Goto Profaned capital", self.player), + lambda state: state.has("Storm Ruler", self.player)) + set_rule(self.world.get_entrance("Goto Grand Archives", self.player), + lambda state: state.has("Grand Archives Key", self.player)) + set_rule(self.world.get_entrance("Goto Kiln Of The First Flame", self.player), + lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and + state.has("Cinders of a Lord - Yhorm the Giant", self.player) and + state.has("Cinders of a Lord - Aldrich", self.player) and + state.has("Cinders of a Lord - Lothric Prince", self.player)) + + self.world.completion_condition[self.player] = lambda state: \ + state.has("Cinders of a Lord - Abyss Watcher", self.player) and \ + state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \ + state.has("Cinders of a Lord - Aldrich", self.player) and \ + state.has("Cinders of a Lord - Lothric Prince", self.player) + + def generate_basic(self): + # Depending on the specified option, add the Basin of Vows to a specific location or to the item pool + item = self.create_item("Basin of Vows") + if self.world.late_basin_of_vows[self.player]: + self.world.get_location("IBV: Soul of Pontiff Sulyvahn", self.player).place_locked_item(item) + else: + self.world.itempool += [item] + + def generate_output(self, output_directory: str): + # Depending on the specified option, modify items hexadecimal value to add an upgrade level + item_dictionary = item_dictionary_table.copy() + if self.world.randomize_weapons_level[self.player]: + # Randomize some weapons upgrades + for name in weapons_upgrade_5_table.keys(): + if self.world.random.randint(0, 100) < 33: + value = self.world.random.randint(1, 5) + item_dictionary[name] += value + + for name in weapons_upgrade_10_table.keys(): + if self.world.random.randint(0, 100) < 33: + value = self.world.random.randint(1, 10) + item_dictionary[name] += value + + # Create the mandatory lists to generate the player's output file + items_id = [] + items_address = [] + locations_id = [] + locations_address = [] + locations_target = [] + for location in self.world.get_filled_locations(): + if location.item.player == self.player: + items_id.append(location.item.code) + items_address.append(item_dictionary[location.item.name]) + + if location.player == self.player: + locations_address.append(location_dictionary_table[location.name]) + locations_id.append(location.address) + if location.item.player == self.player: + locations_target.append(item_dictionary[location.item.name]) + else: + locations_target.append(0) + + data = { + "options": { + "auto_equip": self.world.auto_equip[self.player].value, + "lock_equip": self.world.lock_equip[self.player].value, + "no_weapon_requirements": self.world.no_weapon_requirements[self.player].value, + }, + "seed": self.world.seed_name, # to verify the server's multiworld + "slot": self.world.player_name[self.player], # to connect to server + "base_id": self.base_id, # to merge location and items lists + "locationsId": locations_id, + "locationsAddress": locations_address, + "locationsTarget": locations_target, + "itemsId": items_id, + "itemsAddress": items_address + } + + # generate the file + filename = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}.json" + with open(os.path.join(output_directory, filename), 'w') as outfile: + json.dump(data, outfile) + + +class DarkSouls3Location(Location): + game: str = "Dark Souls III" + + +class DarkSouls3Item(Item): + game: str = "Dark Souls III" diff --git a/worlds/dark_souls_3/data/items_data.py b/worlds/dark_souls_3/data/items_data.py new file mode 100644 index 0000000000..b7c5c3d186 --- /dev/null +++ b/worlds/dark_souls_3/data/items_data.py @@ -0,0 +1,376 @@ +""" +Tools used to create this list : +List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791 +Regular expression parser https://regex101.com/r/XdtiLR/2 +List of locations https://darksouls3.wiki.fextralife.com/Locations +""" + +weapons_upgrade_5_table = { + "Irithyll Straight Sword": 0x0020A760, + "Chaos Blade": 0x004C9960, + "Dragonrider Bow": 0x00D6B0F0, + "White Hair Talisman": 0x00CAF120, + "Izalith Staff": 0x00C96A80, + "Fume Ultra Greatsword": 0x0060E4B0, + "Black Knight Sword": 0x005F5E10, + + "Yorshka's Spear": 0x008C3A70, + "Smough's Great Hammer": 0x007E30B0, + "Dragonslayer Greatbow": 0x00CF8500, + "Golden Ritual Spear": 0x00C83200, + "Eleonora": 0x006CCB90, + "Witch's Locks": 0x00B7B740, + "Crystal Chime": 0x00CA2DD0, + "Black Knight Glaive": 0x009AE070, + "Dragonslayer Spear": 0x008CAFA0, + "Caitha's Chime": 0x00CA06C0, + "Sunlight Straight Sword": 0x00203230, + + "Firelink Greatsword": 0x0060BDA0, + "Hollowslayer Greatsword": 0x00604870, + "Arstor's Spear": 0x008BEC50, + "Vordt's Great Hammer": 0x007CD120, + "Crystal Sage's Rapier": 0x002E6300, + "Farron Greatsword": 0x005E9AC0, + "Wolf Knight's Greatsword": 0x00602160, + "Dancer's Enchanted Swords": 0x00F4C040, + "Wolnir's Holy Sword": 0x005FFA50, + "Demon's Greataxe": 0x006CA480, + "Demon's Fist": 0x00A84DF0, + + "Old King's Great Hammer": 0x007CF830, + "Greatsword of Judgment": 0x005E2590, + "Profaned Greatsword": 0x005E4CA0, + "Yhorm's Great Machete": 0x005F0FF0, + "Cleric's Candlestick": 0x0020F580, + "Dragonslayer Greataxe": 0x006C7D70, + "Moonlight Greatsword": 0x00606F80, + "Gundyr's Halberd": 0x009A1D20, + "Lothric's Holy Sword": 0x005FD340, + "Lorian's Greatsword": 0x005F8520, + "Twin Princes' Greatsword": 0x005FAC30, + "Storm Curved Sword": 0x003E4180, + "Dragonslayer Swordspear": 0x008BC540, + +} + +weapons_upgrade_10_table = { + "Broken Straight Sword": 0x001EF9B0, + "Deep Battle Axe": 0x0006AFA54, + "Club": 0x007A1200, + "Claymore": 0x005BDBA0, + "Longbow": 0x00D689E0, + "Mail Breaker": 0x002DEDD0, + "Broadsword": 0x001ED2A0, + "Astora's Straight Sword": 0x002191C0, + "Rapier": 0x002E14E0, + "Lucerne": 0x0098BD90, + "Whip": 0x00B71B00, + "Reinforced Club": 0x007A8730, + "Caestus": 0x00A7FFD0, + "Partizan": 0x0089C970, + "Red Hilted Halberd": 0x009AB960, + "Saint's Talisman": 0x00CACA10, + "Large Club": 0x007AFC60, + + "Brigand Twindaggers": 0x00F50E60, + "Butcher Knife": 0x006BE130, + "Brigand Axe": 0x006B1DE0, + "Heretic's Staff": 0x00C8F550, + "Great Club": 0x007B4A80, + "Exile Greatsword": 0x005DD770, + "Sellsword Twinblades": 0x00F42400, + "Notched Whip": 0x00B7DE50, + "Astora Greatsword": 0x005C9EF0, + "Executioner's Greatsword": 0x0021DFE0, + "Saint-tree Bellvine": 0x00C9DFB0, + "Saint Bident": 0x008C1360, + "Drang Hammers": 0x00F61FD0, + "Arbalest": 0x00D662D0, + "Sunlight Talisman": 0x00CA54E0, + "Greatsword": 0x005C50D0, + "Black Bow of Pharis": 0x00D7E970, + "Great Axe": 0x006B9310, + "Black Blade": 0x004CC070, + "Blacksmith Hammer": 0x007E57C0, + "Witchtree Branch": 0x00C94370, + "Painting Guardian's Curved Sword": 0x003E6890, + "Pickaxe": 0x007DE290, + "Court Sorcerer's Staff": 0x00C91C60, + "Avelyn": 0x00D6FF10, + "Onikiri and Ubadachi": 0x00F58390, + "Ricard's Rapier": 0x002E3BF0, + "Drakeblood Greatsword": 0x00609690, + "Greatlance": 0x008A8CC0, + "Sniper Crossbow": 0x00D83790, + + "Claw": 0x00A7D8C0, +} + +shields_table = { + "East-West Shield": 0x0142B930, + "Silver Eagle Kite Shield": 0x014418C0, + "Small Leather Shield": 0x01315410, + "Blue Wooden Shield": 0x0143F1B0, + "Plank Shield": 0x01346150, + "Caduceus Round Shield": 0x01341330, + "Wargod Wooden Shield": 0x0144DC10, + "Grass Crest Shield": 0x01437C80, + "Golden Falcon Shield": 0x01354BB0, + "Twin Dragon Greatshield": 0x01513820, + "Spider Shield": 0x01435570, + "Crest Shield": 0x01430750, + "Curse Ward Greatshield": 0x01518640, + "Stone Parma": 0x01443FD0, + "Dragon Crest Shield": 0x01432E60, + "Shield of Want": 0x0144B500, + "Black Iron Greatshield": 0x0150EA00, + "Great Magic Shield": 0x40144F38, + "Greatshield of Glory": 0x01515F30, + "Sacred Bloom Shield": 0x013572C0, + "Golden Wing Crest Shield": 0x0143CAA0, + "Ancient Dragon Greatshield": 0x013599D0, + "Spirit Tree Crest Shield": 0x014466E0, + +} + +goods_table = { + "Soul of an Intrepid Hero": 0x4000019D, + "Soul of the Nameless King": 0x400002D2, + "Soul of Champion Gundyr": 0x400002C8, + "Soul of the Twin Princes": 0x400002DB, + "Soul of Consumed Oceiros": 0x400002CE, + "Soul of Aldrich": 0x400002D5, + "Soul of Yhorm the Giant": 0x400002DC, + "Soul of Pontiff Sulyvahn": 0x400002D4, + "Soul of the Old Demon King": 0x400002D0, + "Soul of High Lord Wolnir": 0x400002D6, + "Soul of the Blood of the Wolf": 0x400002CD, + "Soul of the Deacons of the Deep": 0x400002D9, + "Soul of a Crystal Sage": 0x400002CB, + "Soul of Boreal Valley Vordt": 0x400002CF, + "Soul of a Stray Demon": 0x400002E7, + "Soul of a Demon": 0x400002E3, +} + +armor_table = { + "Fire Keeper Robe": 0x140D9CE8, + "Fire Keeper Gloves": 0x140DA0D0, + "Fire Keeper Skirt": 0x140DA4B8, + "Deserter Trousers": 0x126265B8, + "Cleric Hat": 0x11D905C0, + "Cleric Blue Robe": 0x11D909A8, + "Cleric Gloves": 0x11D90D90, + "Cleric Trousers": 0x11D91178, + "Northern Helm": 0x116E3600, + "Northern Armor": 0x116E39E8, + "Northern Gloves": 0x116E3DD0, + "Northern Trousers": 0x116E41B8, + "Loincloth": 0x148F57D8, + + "Brigand Hood": 0x148009E0, + "Brigand Armor": 0x14800DC8, + "Brigand Gauntlets": 0x148011B0, + "Brigand Trousers": 0x14801598, + "Sorcerer Hood": 0x11C9C380, + "Sorcerer Robe": 0x11C9C768, + "Sorcerer Gloves": 0x11C9CB50, + "Sorcerer Trousers": 0x11C9CF38, + "Fallen Knight Helm": 0x1121EAC0, + "Fallen Knight Armor": 0x1121EEA8, + "Fallen Knight Gauntlets": 0x1121F290, + "Fallen Knight Trousers": 0x1121F678, + "Conjurator Hood": 0x149E8E60, + "Conjurator Robe": 0x149E9248, + "Conjurator Manchettes": 0x149E9630, + "Conjurator Boots": 0x149E9A18, + + "Sellsword Helm": 0x11481060, + "Sellsword Armor": 0x11481448, + "Sellsword Gauntlet": 0x11481830, + "Sellsword Trousers": 0x11481C18, + "Herald Helm": 0x114FB180, + "Herald Armor": 0x114FB568, + "Herald Gloves": 0x114FB950, + "Herald Trousers": 0x114FBD38, + + "Maiden Hood": 0x14BD12E0, + "Maiden Robe": 0x14BD16C8, + "Maiden Gloves": 0x14BD1AB0, + "Maiden Skirt": 0x14BD1E98, + "Drang Armor": 0x154E0C28, + "Drang Gauntlets": 0x154E1010, + "Drang Shoes": 0x154E13F8, + "Archdeacon White Crown": 0x13EF1480, + "Archdeacon Holy Garb": 0x13EF1868, + "Archdeacon Skirt": 0x13EF2038, + "Antiquated Dress": 0x15D76068, + "Antiquated Gloves": 0x15D76450, + "Antiquated Skirt": 0x15D76838, + "Ragged Mask": 0x148F4C20, + "Crown of Dusk": 0x15D75C80, + "Pharis's Hat": 0x1487AB00, + "Old Sage's Blindfold": 0x11945BA0, + + "Painting Guardian Hood": 0x156C8CC0, + "Painting Guardian Gown": 0x156C90A8, + "Painting Guardian Gloves": 0x156C9490, + "Painting Guardian Waistcloth": 0x156C9878, + "Brass Helm": 0x1501BD00, + "Brass Armor": 0x1501C0E8, + "Brass Gauntlets": 0x1501C4D0, + "Brass Leggings": 0x1501C8B8, + "Old Sorcerer Hat": 0x1496ED40, + "Old Sorcerer Coat": 0x1496F128, + "Old Sorcerer Gauntlets": 0x1496F510, + "Old Sorcerer Boots": 0x1496F8F8, + "Court Sorcerer Hood": 0x11BA8140, + "Court Sorcerer Robe": 0x11BA8528, + "Court Sorcerer Gloves": 0x11BA8910, + "Court Sorcerer Trousers": 0x11BA8CF8, + "Dragonslayer Helm": 0x158B1140, + "Dragonslayer Armor": 0x158B1528, + "Dragonslayer Gauntlets": 0x158B1910, + "Dragonslayer Leggings": 0x158B1CF8, + + "Hood of Prayer": 0x13AA6A60, + "Robe of Prayer": 0x13AA6E48, + "Skirt of Prayer": 0x13AA7618, + "Winged Knight Helm": 0x12EBAE40, + "Winged Knight Armor": 0x12EBB228, + "Winged Knight Gauntlets": 0x12EBB610, + "Winged Knight Leggings": 0x12EBB9F8, + "Shadow Mask": 0x14D3F640, + "Shadow Garb": 0x14D3FA28, + "Shadow Gauntlets": 0x14D3FE10, + "Shadow Leggings": 0x14D401F8, +} + +rings_table = { + "Estus Ring": 0x200050DC, + "Covetous Silver Serpent Ring": 0x20004FB0, + "Fire Clutch Ring": 0x2000501E, + "Flame Stoneplate Ring": 0x20004E52, + "Flynn's Ring": 0x2000503C, + "Chloranthy Ring": 0x20004E2A, + + "Morne's Ring": 0x20004F1A, + "Sage Ring": 0x20004F38, + "Aldrich's Sapphire": 0x20005096, + "Lloyd's Sword Ring": 0x200050B4, + "Poisonbite Ring": 0x20004E8E, + "Deep Ring": 0x20004F60, + "Lingering Dragoncrest Ring": 0x20004F2E, + "Carthus Milkring": 0x20004FE2, + "Witch's Ring": 0x20004F11, + "Carthus Bloodring": 0x200050FA, + + "Speckled Stoneplate Ring": 0x20004E7A, + "Magic Clutch Ring": 0x2000500A, + "Ring of the Sun's First Born": 0x20004F1B, + "Pontiff's Right Eye": 0x2000510E, "Leo Ring": 0x20004EE8, + "Dark Stoneplate Ring": 0x20004E70, + "Reversal Ring": 0x20005104, + "Ring of Favor": 0x20004E3E, + "Bellowing Dragoncrest Ring": 0x20004F07, + "Covetous Gold Serpent Ring": 0x20004FA6, + "Dusk Crown Ring": 0x20004F4C, + "Dark Clutch Ring": 0x20005028, + "Cursebite Ring": 0x20004E98, + "Sun Princess Ring": 0x20004FBA, + "Aldrich's Ruby": 0x2000508C, + "Scholar Ring": 0x20004EB6, + "Fleshbite Ring": 0x20004EA2, + "Hunter's Ring": 0x20004FF6, + "Ashen Estus Ring": 0x200050E6, + "Hornet Ring": 0x20004F9C, + "Lightning Clutch Ring": 0x20005014, + "Ring of Steel Protection": 0x20004E48, + "Calamity Ring": 0x20005078, + "Thunder Stoneplate Ring": 0x20004E5C, + "Knight's Ring": 0x20004FEC, + "Red Tearstone Ring": 0x20004ECA, + "Dragonscale Ring": 0x2000515E, +} + +spells_table = { + "Seek Guidance": 0x40360420, + "Lightning Spear": 0x40362B30, + "Atonement": 0x4039ADA0, + "Great Magic Weapon": 0x40140118, + "Iron Flesh": 0x40251430, + "Lightning Stake": 0x40389C30, + "Toxic Mist": 0x4024F108, + "Sacred Flame": 0x40284880, + "Dorhys' Gnawing": 0x40363EB8, + "Great Heal": 0x40356FB0, + "Lightning Blade": 0x4036C770, + "Profaned Flame": 0x402575D8, + "Wrath of the Gods": 0x4035E0F8, + "Power Within": 0x40253B40, + "Soul Stream": 0x4018B820, + "Divine Pillars of Light": 0x4038C340, + "Great Magic Barrier": 0x40365628, + +} + +misc_items_table = { + "Cell Key": 0x400007DA, + "Small Lothric Banner": 0x40000836, + "Mortician's Ashes": 0x4000083B, + "Braille Divine Tome of Carim": 0x40000847, # Shop + "Great Swamp Pyromancy Tome": 0x4000084F, # Shop + "Farron Coal ": 0x40000837, # Shop + "Paladin's Ashes": 0x4000083D, #Shop + "Deep Braille Divine Tome": 0x40000860, # Shop + "Small Doll": 0x400007D5, + "Golden Scroll": 0x4000085C, + "Sage's Coal": 0x40000838, # Shop #Unique + "Sage's Scroll": 0x40000854, + "Dreamchaser's Ashes": 0x4000083C, # Shop #Unique + "Cinders of a Lord - Abyss Watcher": 0x4000084B, + "Cinders of a Lord - Yhorm the Giant": 0x4000084D, + "Cinders of a Lord - Aldrich": 0x4000084C, + "Grand Archives Key": 0x400007DE, + "Basin of Vows": 0x40000845, + "Cinders of a Lord - Lothric Prince": 0x4000084E, + "Carthus Pyromancy Tome": 0x40000850, + "Grave Warden's Ashes": 0x4000083E, + "Grave Warden Pyromancy Tome": 0x40000853, + "Quelana Pyromancy Tome": 0x40000852, + "Izalith Pyromancy Tome": 0x40000851, + "Greirat's Ashes": 0x4000083F, + "Excrement-covered Ashes": 0x40000862, + "Easterner's Ashes": 0x40000868, + "Prisoner Chief's Ashes": 0x40000863, + "Jailbreaker's Key": 0x400007D7, + "Dragon Torso Stone": 0x4000017A, + "Profaned Coal": 0x4000083A, + "Xanthous Ashes": 0x40000864, + "Old Cell Key": 0x400007DC, + "Jailer's Key Ring": 0x400007D8, + "Path of the Dragon Gesture": 0x40002346, + "Logan's Scroll": 0x40000855, + "Storm Ruler": 0x006132D0, + "Giant's Coal": 0x40000839, + "Coiled Sword Fragment": 0x4000015F, + "Dragon Chaser's Ashes": 0x40000867, + "Twinkling Dragon Torso Stone": 0x40000184, + "Braille Divine Tome of Lothric": 0x40000848, +} + +key_items_list = { + "Small Lothric Banner", + "Basin of Vows", + "Small Doll", + "Path of the Dragon Gesture", + "Storm Ruler", + "Grand Archives Key", + "Cinders of a Lord - Abyss Watcher", + "Cinders of a Lord - Yhorm the Giant", + "Cinders of a Lord - Aldrich", + "Cinders of a Lord - Lothric Prince", + "Mortician's Ashes" +} + +item_dictionary_table = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table} diff --git a/worlds/dark_souls_3/data/locations_data.py b/worlds/dark_souls_3/data/locations_data.py new file mode 100644 index 0000000000..bf85e6ebf1 --- /dev/null +++ b/worlds/dark_souls_3/data/locations_data.py @@ -0,0 +1,422 @@ +""" +Tools used to create this list : +List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791 +Regular expression parser https://regex101.com/r/XdtiLR/2 +List of locations https://darksouls3.wiki.fextralife.com/Locations +""" + +cemetery_of_ash_table = { +} + +fire_link_shrine_table = { + "FS: Broken Straight Sword": 0x001EF9B0, # Multiple + "FS: East-West Shield": 0x0142B930, +} + +firelink_shrine_bell_tower_table = { + "FSBT: Covetous Silver Serpent Ring": 0x20004FB0, + "FSBT: Fire Keeper Robe": 0x140D9CE8, + "FSBT: Fire Keeper Gloves": 0x140DA0D0, + "FSBT: Fire Keeper Skirt": 0x140DA4B8, + "FSBT: Estus Ring": 0x200050DC, + "FSBT: Fire Keeper Soul": 0x40000186 +} + +high_wall_of_lothric = { + "HWL: Deep Battle Axe": 0x0006AFA54, + "HWL: Club": 0x007A1200, + "HWL: Claymore": 0x005BDBA0, + "HWL: Binoculars": 0x40000173, + "HWL: Longbow": 0x00D689E0, + "HWL: Mail Breaker": 0x002DEDD0, + "HWL: Broadsword": 0x001ED2A0, + "HWL: Silver Eagle Kite Shield": 0x014418C0, + "HWL: Astora's Straight Sword": 0x002191C0, + "HWL: Cell Key": 0x400007DA, + "HWL: Rapier": 0x002E14E0, + "HWL: Lucerne": 0x0098BD90, + "HWL: Small Lothric Banner": 0x40000836, + "HWL: Basin of Vows": 0x40000845, + "HWL: Soul of Boreal Valley Vordt": 0x400002CF, + "HWL: Soul of the Dancer": 0x400002CA, + "HWL: Way of Blue Covenant": 0x2000274C, +} + +undead_settlement_table = { + "US: Small Leather Shield": 0x01315410, + "US: Whip": 0x00B71B00, + "US: Reinforced Club": 0x007A8730, + "US: Blue Wooden Shield": 0x0143F1B0, + + "US: Cleric Hat": 0x11D905C0, + "US: Cleric Blue Robe": 0x11D909A8, + "US: Cleric Gloves": 0x11D90D90, + "US: Cleric Trousers": 0x11D91178, + + "US: Mortician's Ashes": 0x4000083B, # Key item for Grave Key for Firelink Towerlocations + "US: Caestus": 0x00A7FFD0, + "US: Plank Shield": 0x01346150, + "US: Flame Stoneplate Ring": 0x20004E52, + "US: Caduceus Round Shield": 0x01341330, + "US: Fire Clutch Ring": 0x2000501E, + "US: Partizan": 0x0089C970, + "US: Bloodbite Ring": 0x20004E84, + + "US: Red Hilted Halberd": 0x009AB960, + "US: Saint's Talisman": 0x00CACA10, + "US: Irithyll Straight Sword": 0x0020A760, + "US: Large Club": 0x007AFC60, + "US: Northern Helm": 0x116E3600, + "US: Northern Armor": 0x116E39E8, + "US: Northern Gloves": 0x116E3DD0, + "US: Northern Trousers": 0x116E41B8, + "US: Flynn's Ring": 0x2000503C, + + "US: Mirrah Vest": 0x15204568, + "US: Mirrah Gloves": 0x15204950, + "US: Mirrah Trousers": 0x15204D38, + + "US: Chloranthy Ring": 0x20004E2A, + "US: Loincloth": 0x148F57D8, + "US: Wargod Wooden Shield": 0x0144DC10, + + "US: Loretta's Bone": 0x40000846, + + "US: Hand Axe": 0x006ACFC0, + "US: Great Scythe": 0x00989680, + "US: Soul of the Rotted Greatwood": 0x400002D7, + "US: Hawk Ring": 0x20004F92, + "US: Warrior of Sunlight Covenant": 0x20002738, +} + +road_of_sacrifice_table = { + "RS: Brigand Twindaggers": 0x00F50E60, + + "RS: Brigand Hood": 0x148009E0, + "RS: Brigand Armor": 0x14800DC8, + "RS: Brigand Gauntlets": 0x148011B0, + "RS: Brigand Trousers": 0x14801598, + + "RS: Butcher Knife": 0x006BE130, + "RS: Brigand Axe": 0x006B1DE0, + "RS: Braille Divine Tome of Carim": 0x40000847, # Shop + "RS: Morne's Ring": 0x20004F1A, + "RS: Twin Dragon Greatshield": 0x01513820, + "RS: Heretic's Staff": 0x00C8F550, + + "RS: Sorcerer Hood": 0x11C9C380, + "RS: Sorcerer Robe": 0x11C9C768, + "RS: Sorcerer Gloves": 0x11C9CB50, + "RS: Sorcerer Trousers": 0x11C9CF38, + + "RS: Sage Ring": 0x20004F38, + + "RS: Fallen Knight Helm": 0x1121EAC0, + "RS: Fallen Knight Armor": 0x1121EEA8, + "RS: Fallen Knight Gauntlets": 0x1121F290, + "RS: Fallen Knight Trousers": 0x1121F678, + + "RS: Conjurator Hood": 0x149E8E60, + "RS: Conjurator Robe": 0x149E9248, + "RS: Conjurator Manchettes": 0x149E9630, + "RS: Conjurator Boots": 0x149E9A18, + + "RS: Great Swamp Pyromancy Tome": 0x4000084F, # Shop + + "RS: Great Club": 0x007B4A80, + "RS: Exile Greatsword": 0x005DD770, + + "RS: Farron Coal ": 0x40000837, # Shop + + "RS: Sellsword Twinblades": 0x00F42400, + "RS: Sellsword Helm": 0x11481060, + "RS: Sellsword Armor": 0x11481448, + "RS: Sellsword Gauntlet": 0x11481830, + "RS: Sellsword Trousers": 0x11481C18, + + "RS: Golden Falcon Shield": 0x01354BB0, + + "RS: Herald Helm": 0x114FB180, + "RS: Herald Armor": 0x114FB568, + "RS: Herald Gloves": 0x114FB950, + "RS: Herald Trousers": 0x114FBD38, + + "RS: Grass Crest Shield": 0x01437C80, + "RS: Soul of a Crystal Sage": 0x400002CB, + "RS: Great Swamp Ring": 0x20004F10, +} + +cathedral_of_the_deep_table = { + "CD: Paladin's Ashes": 0x4000083D, #Shop + "CD: Spider Shield": 0x01435570, + "CD: Crest Shield": 0x01430750, + "CD: Notched Whip": 0x00B7DE50, + "CD: Astora Greatsword": 0x005C9EF0, + "CD: Executioner's Greatsword": 0x0021DFE0, + "CD: Curse Ward Greatshield": 0x01518640, + "CD: Saint-tree Bellvine": 0x00C9DFB0, + "CD: Poisonbite Ring": 0x20004E8E, + + "CD: Lloyd's Sword Ring": 0x200050B4, + "CD: Seek Guidance": 0x40360420, + + "CD: Aldrich's Sapphire": 0x20005096, + "CD: Deep Braille Divine Tome": 0x40000860, # Shop + + "CD: Saint Bident": 0x008C1360, + "CD: Maiden Hood": 0x14BD12E0, + "CD: Maiden Robe": 0x14BD16C8, + "CD: Maiden Gloves": 0x14BD1AB0, + "CD: Maiden Skirt": 0x14BD1E98, + "CD: Drang Armor": 0x154E0C28, + "CD: Drang Gauntlets": 0x154E1010, + "CD: Drang Shoes": 0x154E13F8, + "CD: Drang Hammers": 0x00F61FD0, + "CD: Deep Ring": 0x20004F60, + + "CD: Archdeacon White Crown": 0x13EF1480, + "CD: Archdeacon Holy Garb": 0x13EF1868, + "CD: Archdeacon Skirt": 0x13EF2038, + + "CD: Arbalest": 0x00D662D0, + "CD: Small Doll": 0x400007D5, + "CD: Soul of the Deacons of the Deep": 0x400002D9, + "CD: Rosaria's Fingers Covenant": 0x20002760, +} + +farron_keep_table = { + "FK: Ragged Mask": 0x148F4C20, + "FK: Iron Flesh": 0x40251430, + "FK: Golden Scroll": 0x4000085C, + + "FK: Antiquated Dress": 0x15D76068, + "FK: Antiquated Gloves": 0x15D76450, + "FK: Antiquated Skirt": 0x15D76838, + + "FK: Nameless Knight Helm": 0x143B5FC0, + "FK: Nameless Knight Armor": 0x143B63A8, + "FK: Nameless Knight Gauntlets": 0x143B6790, + "FK: Nameless Knight Leggings": 0x143B6B78, + + "FK: Sunlight Talisman": 0x00CA54E0, + "FK: Wolf's Blood Swordgrass": 0x4000016E, + "FK: Greatsword": 0x005C50D0, + + "FK: Sage's Coal": 0x40000838, # Shop #Unique + "FK: Stone Parma": 0x01443FD0, + "FK: Sage's Scroll": 0x40000854, + "FK: Crown of Dusk": 0x15D75C80, + + "FK: Lingering Dragoncrest Ring": 0x20004F2E, + "FK: Pharis's Hat": 0x1487AB00, + "FK: Black Bow of Pharis": 0x00D7E970, + + "FK: Dreamchaser's Ashes": 0x4000083C, # Shop #Unique + "FK: Great Axe": 0x006B9310, # Multiple + "FK: Dragon Crest Shield": 0x01432E60, + "FK: Lightning Spear": 0x40362B30, + "FK: Atonement": 0x4039ADA0, + "FK: Great Magic Weapon": 0x40140118, + "FK: Cinders of a Lord - Abyss Watcher": 0x4000084B, + "FK: Soul of the Blood of the Wolf": 0x400002CD, + "FK: Soul of a Stray Demon": 0x400002E7, + "FK: Watchdogs of Farron Covenant": 0x20002724, +} + +catacombs_of_carthus_table = { + "CC: Carthus Pyromancy Tome": 0x40000850, + "CC: Carthus Milkring": 0x20004FE2, + "CC: Grave Warden's Ashes": 0x4000083E, + "CC: Carthus Bloodring": 0x200050FA, + "CC: Grave Warden Pyromancy Tome": 0x40000853, + "CC: Old Sage's Blindfold": 0x11945BA0, + "CC: Witch's Ring": 0x20004F11, + "CC: Black Blade": 0x004CC070, + "CC: Soul of High Lord Wolnir": 0x400002D6, + "CC: Soul of a Demon": 0x400002E3, +} + +smouldering_lake_table = { + "SL: Shield of Want": 0x0144B500, + "SL: Speckled Stoneplate Ring": 0x20004E7A, + "SL: Dragonrider Bow": 0x00D6B0F0, + "SL: Lightning Stake": 0x40389C30, + "SL: Izalith Pyromancy Tome": 0x40000851, + "SL: Black Knight Sword": 0x005F5E10, + "SL: Quelana Pyromancy Tome": 0x40000852, + "SL: Toxic Mist": 0x4024F108, + "SL: White Hair Talisman": 0x00CAF120, + "SL: Izalith Staff": 0x00C96A80, + "SL: Sacred Flame": 0x40284880, + "SL: Fume Ultra Greatsword": 0x0060E4B0, + "SL: Black Iron Greatshield": 0x0150EA00, + "SL: Soul of the Old Demon King": 0x400002D0, +} + +irithyll_of_the_boreal_valley_table = { + "IBV: Dorhys' Gnawing": 0x40363EB8, + "IBV: Witchtree Branch": 0x00C94370, + "IBV: Magic Clutch Ring": 0x2000500A, + "IBV: Ring of the Sun's First Born": 0x20004F1B, + "IBV: Roster of Knights": 0x4000006C, + "IBV: Pontiff's Right Eye": 0x2000510E, + + "IBV: Yorshka's Spear": 0x008C3A70, + "IBV: Great Heal": 0x40356FB0, + + "IBV: Smough's Great Hammer": 0x007E30B0, + "IBV: Leo Ring": 0x20004EE8, + "IBV: Greirat's Ashes": 0x4000083F, + "IBV: Excrement-covered Ashes": 0x40000862, + + "IBV: Dark Stoneplate Ring": 0x20004E70, + "IBV: Easterner's Ashes": 0x40000868, + "IBV: Painting Guardian's Curved Sword": 0x003E6890, + "IBV: Painting Guardian Hood": 0x156C8CC0, + "IBV: Painting Guardian Gown": 0x156C90A8, + "IBV: Painting Guardian Gloves": 0x156C9490, + "IBV: Painting Guardian Waistcloth": 0x156C9878, + "IBV: Dragonslayer Greatbow": 0x00CF8500, + "IBV: Reversal Ring": 0x20005104, + "IBV: Brass Helm": 0x1501BD00, + "IBV: Brass Armor": 0x1501C0E8, + "IBV: Brass Gauntlets": 0x1501C4D0, + "IBV: Brass Leggings": 0x1501C8B8, + "IBV: Ring of Favor": 0x20004E3E, + "IBV: Golden Ritual Spear": 0x00C83200, + "IBV: Soul of Pontiff Sulyvahn": 0x400002D4, + "IBV: Aldrich Faithful Covenant": 0x2000272E, +} + +irithyll_dungeon_table = { + "ID: Bellowing Dragoncrest Ring": 0x20004F07, + "ID: Jailbreaker's Key": 0x400007D7, + "ID: Prisoner Chief's Ashes": 0x40000863, + "ID: Old Sorcerer Hat": 0x1496ED40, + "ID: Old Sorcerer Coat": 0x1496F128, + "ID: Old Sorcerer Gauntlets": 0x1496F510, + "ID: Old Sorcerer Boots": 0x1496F8F8, + "ID: Great Magic Shield": 0x40144F38, + + "ID: Dragon Torso Stone": 0x4000017A, + "ID: Lightning Blade": 0x4036C770, + "ID: Profaned Coal": 0x4000083A, + "ID: Xanthous Ashes": 0x40000864, + "ID: Old Cell Key": 0x400007DC, + "ID: Pickaxe": 0x007DE290, + "ID: Profaned Flame": 0x402575D8, + "ID: Covetous Gold Serpent Ring": 0x20004FA6, + "ID: Jailer's Key Ring": 0x400007D8, + "ID: Dusk Crown Ring": 0x20004F4C, + "ID: Dark Clutch Ring": 0x20005028, +} + +profaned_capital_table = { + "PC: Cursebite Ring": 0x20004E98, + "PC: Court Sorcerer Hood": 0x11BA8140, + "PC: Court Sorcerer Robe": 0x11BA8528, + "PC: Court Sorcerer Gloves": 0x11BA8910, + "PC: Court Sorcerer Trousers": 0x11BA8CF8, + "PC: Wrath of the Gods": 0x4035E0F8, + "PC: Logan's Scroll": 0x40000855, + "PC: Eleonora": 0x006CCB90, + "PC: Court Sorcerer's Staff": 0x00C91C60, + "PC: Greatshield of Glory": 0x01515F30, + "PC: Storm Ruler": 0x006132D0, + "PC: Cinders of a Lord - Yhorm the Giant": 0x4000084D, + "PC: Soul of Yhorm the Giant": 0x400002DC, +} + +anor_londo_table = { + "AL: Giant's Coal": 0x40000839, + "AL: Sun Princess Ring": 0x20004FBA, + "AL: Aldrich's Ruby": 0x2000508C, + "AL: Cinders of a Lord - Aldrich": 0x4000084C, + "AL: Soul of Aldrich": 0x400002D5, +} + +lothric_castle_table = { + "LC: Hood of Prayer": 0x13AA6A60, + "LC: Robe of Prayer": 0x13AA6E48, + "LC: Skirt of Prayer": 0x13AA7618, + + "LC: Sacred Bloom Shield": 0x013572C0, + "LC: Winged Knight Helm": 0x12EBAE40, + "LC: Winged Knight Armor": 0x12EBB228, + "LC: Winged Knight Gauntlets": 0x12EBB610, + "LC: Winged Knight Leggings": 0x12EBB9F8, + + "LC: Greatlance": 0x008A8CC0, + "LC: Sniper Crossbow": 0x00D83790, + "LC: Spirit Tree Crest Shield": 0x014466E0, + "LC: Red Tearstone Ring": 0x20004ECA, + "LC: Caitha's Chime": 0x00CA06C0, + "LC: Braille Divine Tome of Lothric": 0x40000848, + "LC: Knight's Ring": 0x20004FEC, + "LC: Sunlight Straight Sword": 0x00203230, + "LC: Grand Archives Key": 0x400007DE, + "LC: Soul of Dragonslayer Armour": 0x400002D1, +} + +consumed_king_garden_table = { + "CKG: Dragonscale Ring": 0x2000515E, + "CKG: Shadow Mask": 0x14D3F640, + "CKG: Shadow Garb": 0x14D3FA28, + "CKG: Shadow Gauntlets": 0x14D3FE10, + "CKG: Shadow Leggings": 0x14D401F8, + "CKG: Claw": 0x00A7D8C0, + "CKG: Soul of Consumed Oceiros": 0x400002CE, + "CKG: Path of the Dragon Gesture": 0x40002346, +} + +grand_archives_table = { + "GA: Avelyn": 0x00D6FF10, + "GA: Witch's Locks": 0x00B7B740, + "GA: Power Within": 0x40253B40, + "GA: Scholar Ring": 0x20004EB6, + "GA: Soul Stream": 0x4018B820, + "GA: Fleshbite Ring": 0x20004EA2, + "GA: Crystal Chime": 0x00CA2DD0, + "GA: Golden Wing Crest Shield": 0x0143CAA0, + "GA: Onikiri and Ubadachi": 0x00F58390, + "GA: Hunter's Ring": 0x20004FF6, + "GA: Divine Pillars of Light": 0x4038C340, + "GA: Cinders of a Lord - Lothric Prince": 0x4000084E, + "GA: Soul of the Twin Princes": 0x400002DB, +} + +untended_graves_table = { + "UG: Ashen Estus Ring": 0x200050E6, + "UG: Black Knight Glaive": 0x009AE070, + "UG: Hornet Ring": 0x20004F9C, + "UG: Chaos Blade": 0x004C9960, + "UG: Blacksmith Hammer": 0x007E57C0, + "UG: Eyes of a Fire Keeper": 0x4000085A, + "UG: Coiled Sword Fragment": 0x4000015F, + "UG: Soul of Champion Gundyr": 0x400002C8, +} + +archdragon_peak_table = { + "AP: Lightning Clutch Ring": 0x20005014, + "AP: Ancient Dragon Greatshield": 0x013599D0, + "AP: Ring of Steel Protection": 0x20004E48, + "AP: Calamity Ring": 0x20005078, + "AP: Drakeblood Greatsword": 0x00609690, + "AP: Dragonslayer Spear": 0x008CAFA0, + + "AP: Thunder Stoneplate Ring": 0x20004E5C, + "AP: Great Magic Barrier": 0x40365628, + "AP: Dragon Chaser's Ashes": 0x40000867, + "AP: Twinkling Dragon Torso Stone": 0x40000184, + "AP: Dragonslayer Helm": 0x158B1140, + "AP: Dragonslayer Armor": 0x158B1528, + "AP: Dragonslayer Gauntlets": 0x158B1910, + "AP: Dragonslayer Leggings": 0x158B1CF8, + "AP: Ricard's Rapier": 0x002E3BF0, + "AP: Soul of the Nameless King": 0x400002D2, +} + +location_dictionary_table = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, + **cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table, + **irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table, + **grand_archives_table, **untended_graves_table, **archdragon_peak_table} diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md new file mode 100644 index 0000000000..5860073c37 --- /dev/null +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -0,0 +1,22 @@ +# Dark Souls III + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized. +This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at +the same location. I also added an option available from the settings page to randomize the level of the generated +weapons( from +0 to +10/+5 ) + +## What Dark Souls III items can appear in other players' worlds? + +Every unique items from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon +or a key item. + +## What does another world's item look like in Dark Souls III? + +In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone. \ No newline at end of file diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md new file mode 100644 index 0000000000..e08029283a --- /dev/null +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -0,0 +1,35 @@ +# Dark Souls III Randomizer Setup Guide + +## Required Software + +- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) +- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client) + +## General Concept + +The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command +prompt where you can read information about your run and write any command to interact with the Archipelago server. + +The randomization is performed by the AP.json file, an output file generated by the Archipelago server. + +## Installation Procedures + +**This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed** + +Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client). +Then you need to add the two following files at the root folder of your game +( e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game" ): +- **dinput8.dll** +- **AP.json** (renamed from the generated file AP-{ROOM_ID}.json) + +## Joining a MultiWorld Game + +1. Run DarkSoulsIII.exe or run the game through Steam +2. Type in /connect {SERVER_IP}:{SERVER_PORT} in the "Windows Command Prompt" that opened +3. Once connected, create a new game, choose a class and wait for the others before starting +4. You can quit and launch at anytime during a game + +## Where do I get a config file? + +The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to +configure your personal settings and export them into a config file \ No newline at end of file From 17351021b37dd1f8437b7cddf707cbc9304e2ab4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 20 Jul 2022 12:45:03 +0200 Subject: [PATCH 037/138] Factorio: update rcon lib --- worlds/factorio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt index 00d9d20af1..c45fb771da 100644 --- a/worlds/factorio/requirements.txt +++ b/worlds/factorio/requirements.txt @@ -1 +1 @@ -factorio-rcon-py==1.2.1 +factorio-rcon-py>=2.0.1 From 53a995372f481ac6ecc7c419e60860a615d3eacb Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 21 Jul 2022 09:57:38 +0200 Subject: [PATCH 038/138] Subnautica: add missed PDA --- worlds/subnautica/Items.py | 2 +- worlds/subnautica/Locations.py | 8 ++++++-- worlds/subnautica/__init__.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index f3a6ded5aa..f0d236623a 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -115,7 +115,7 @@ item_table: Dict[int, ItemDict] = { 'name': 'Light Stick Fragment', 'tech_type': 'TechlightFragment'}, 35026: {'classification': ItemClassification.progression, - 'count': 3, + 'count': 4, 'name': 'Mobile Vehicle Bay Fragment', 'tech_type': 'ConstructorFragment'}, 35027: {'classification': ItemClassification.progression, diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index c437fbc9bf..2ce8cc1190 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -560,8 +560,12 @@ location_table: Dict[int, LocationDict] = { 33129: {'can_slip_through': False, 'name': 'Floating Island - Cave Entrance PDA', 'need_laser_cutter': False, - 'position': {'x': -748.9, 'y': 14.4, 'z': -1179.5}}} - + 'position': {'x': -748.9, 'y': 14.4, 'z': -1179.5}}, + 33130: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Outside PDA', + 'need_laser_cutter': False, + 'position': {'x': -83.2, 'y': -276.4, 'z': -345.5}}, +} if False: # turn to True to export for Subnautica mod payload = {location_id: location_data["position"] for location_id, location_data in location_table.items()} import json diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 9ad4feb1a4..2127fb0cda 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -41,7 +41,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options = Options.options - data_version = 3 + data_version = 4 required_client_version = (0, 3, 3) prefill_items: List[Item] From 9e972eafb26a7f814dda16eb072b213d829a88dc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 21 Jul 2022 15:39:34 +0200 Subject: [PATCH 039/138] Subnautica: Add DeathLink (#803) --- worlds/subnautica/Options.py | 10 ++++++++-- worlds/subnautica/__init__.py | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index b5dc2241fb..f9f3f56756 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, Range +from Options import Choice, Range, DeathLink from .Creatures import all_creatures @@ -39,8 +39,14 @@ class CreatureScans(Range): range_end = len(all_creatures) +class SubnauticaDeathLink(DeathLink): + """When you die, everyone dies. Of course the reverse is true too. + Note: can be toggled via in-game console command "deathlink".""" + + options = { "item_pool": ItemPool, "goal": Goal, - "creature_scans": CreatureScans + "creature_scans": CreatureScans, + "death_link": SubnauticaDeathLink, } diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 2127fb0cda..be709a1c30 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -115,7 +115,8 @@ class SubnauticaWorld(World): slot_data: Dict[str, Any] = { "goal": goal.current_key, "vanilla_tech": vanilla_tech, - "creatures_to_scan": self.creatures_to_scan + "creatures_to_scan": self.creatures_to_scan, + "death_link": self.world.death_link[self.player].value, } return slot_data From 79b851189f984b22ce4fedb5376ff16086858a55 Mon Sep 17 00:00:00 2001 From: KonoTyran Date: Wed, 20 Jul 2022 21:45:01 -0700 Subject: [PATCH 040/138] HK - Fix typos in option names Fixed max charm and max geo cost display names. --- worlds/hk/Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 6c3a9bf548..0f4bec8205 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -222,6 +222,7 @@ class MinimumCharmPrice(Range): class MaximumCharmPrice(MinimumCharmPrice): """The maximum charm price in the range of prices that an item should cost for Salubra's shop item which also carry a charm cost.""" + display_name = "Maximum Charm Requirement" default = 20 @@ -235,7 +236,7 @@ class MinimumGeoPrice(Range): class MaximumGeoPrice(Range): """The maximum geo price for items in geo shops.""" - display_name = "Minimum Geo Price" + display_name = "Maximum Geo Price" range_start = 1 range_end = 2000 default = 400 From a7787d87f95f10c92a43eac0ef58ded305772ee4 Mon Sep 17 00:00:00 2001 From: Jolteon0163 <63653478+jmabry0163@users.noreply.github.com> Date: Thu, 21 Jul 2022 17:08:07 -0500 Subject: [PATCH 041/138] Add to the ArchipIDLE items list (#807) * Add to the ArchipIDLE items list * Update Items.py * Update Items.py --- worlds/archipidle/Items.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/archipidle/Items.py b/worlds/archipidle/Items.py index 3100330d1b..945d3aae60 100644 --- a/worlds/archipidle/Items.py +++ b/worlds/archipidle/Items.py @@ -299,4 +299,5 @@ item_table = ( 'A Shrubbery', 'Roomba with a Knife', 'Wet Cat', + 'The missing moderator, Frostwares', ) From cabbe0aaf60f57809e284e7f7ff40db8793ca7d6 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 22 Jul 2022 01:02:25 -0400 Subject: [PATCH 042/138] Donkey Kong Country 3 Implementation (#798) * Baseline patching and logic for DKC3 * Client can send, but not yet receive * Alpha Test Baseline * Bug Fixes and Starting Lives Option * Finish BBH, add world hints * Add music shuffle * Boomer Costs Text * Stubbed in Collect behaviour * Adjust Gyrocopter option * Add Bonus Coin junk replacement and tracker support * Delete bad logs * Undo host.yaml change * Refactored SNIClient * Make Swanky Free * Fix Typo * Undo SNIClient run_game hack * Fix Typo * Remove Bosses from Level Shuffle * Remove duplicate kivy Data * Add DKC3 Docs and increment Data version * Remove dead code * Fix mislabeled region * Add Dark Souls 3 to README * Always force Cog on Rocket Rush Flag * Fix Single Ski lock and too many DK Coins * Update Retroarch version number * Don't send DKC3 through LttP Adjuster * Comment Location ROM Table * Change ROM Hash prefix to D3 * Remove redundant constructor * Add ROM Change Safeguards * Properly mark WRAM accesses * Remove outdated region connect * Fix syntax error * Fix Game description * Fix SNES Bank Access * Add isso_setup for DKC3 * Double Quote strings * Escape single quotes I guess --- Launcher.py | 2 +- Patch.py | 26 +- README.md | 2 + SNIClient.py | 84 +- host.yaml | 9 + inno_setup.iss | 35 + worlds/dkc3/Client.py | 220 +++++ worlds/dkc3/Items.py | 52 ++ worlds/dkc3/Levels.py | 115 +++ worlds/dkc3/Locations.py | 283 ++++++ worlds/dkc3/Names/ItemName.py | 21 + worlds/dkc3/Names/LocationName.py | 336 +++++++ worlds/dkc3/Options.py | 132 +++ worlds/dkc3/Regions.py | 879 +++++++++++++++++++ worlds/dkc3/Rom.py | 553 ++++++++++++ worlds/dkc3/Rules.py | 32 + worlds/dkc3/__init__.py | 204 +++++ worlds/dkc3/docs/en_Donkey Kong Country 3.md | 35 + worlds/dkc3/docs/setup_en.md | 161 ++++ 19 files changed, 3139 insertions(+), 42 deletions(-) create mode 100644 worlds/dkc3/Client.py create mode 100644 worlds/dkc3/Items.py create mode 100644 worlds/dkc3/Levels.py create mode 100644 worlds/dkc3/Locations.py create mode 100644 worlds/dkc3/Names/ItemName.py create mode 100644 worlds/dkc3/Names/LocationName.py create mode 100644 worlds/dkc3/Options.py create mode 100644 worlds/dkc3/Regions.py create mode 100644 worlds/dkc3/Rom.py create mode 100644 worlds/dkc3/Rules.py create mode 100644 worlds/dkc3/__init__.py create mode 100644 worlds/dkc3/docs/en_Donkey Kong Country 3.md create mode 100644 worlds/dkc3/docs/setup_en.md diff --git a/Launcher.py b/Launcher.py index 809a8937d7..53032ea251 100644 --- a/Launcher.py +++ b/Launcher.py @@ -126,7 +126,7 @@ components: Iterable[Component] = ( Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), # SNI Component('SNI Client', 'SNIClient', - file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3')), + file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3')), Component('LttP Adjuster', 'LttPAdjuster'), # Factorio Component('Factorio Client', 'FactorioClient'), diff --git a/Patch.py b/Patch.py index a2f29fdabc..f90e376656 100644 --- a/Patch.py +++ b/Patch.py @@ -166,13 +166,15 @@ GAME_ALTTP = "A Link to the Past" GAME_SM = "Super Metroid" GAME_SOE = "Secret of Evermore" GAME_SMZ3 = "SMZ3" -supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3"} +GAME_DKC3 = "Donkey Kong Country 3" +supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3", "Donkey Kong Country 3"} preferred_endings = { GAME_ALTTP: "apbp", GAME_SM: "apm3", GAME_SOE: "apsoe", - GAME_SMZ3: "apsmz" + GAME_SMZ3: "apsmz", + GAME_DKC3: "apdkc3" } @@ -187,6 +189,8 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAM from worlds.alttp.Rom import LTTPJPN10HASH as ALTTPHASH from worlds.sm.Rom import SMJUHASH as SMHASH HASH = ALTTPHASH + SMHASH + elif game == GAME_DKC3: + from worlds.dkc3.Rom import USHASH as HASH else: raise RuntimeError(f"Selected game {game} for base rom not found.") @@ -216,7 +220,10 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str meta, game) target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ( - ".apbp" if game == GAME_ALTTP else ".apsmz" if game == GAME_SMZ3 else ".apm3") + ".apbp" if game == GAME_ALTTP + else ".apsmz" if game == GAME_SMZ3 + else ".apdkc3" if game == GAME_DKC3 + else ".apm3") write_lzma(bytes, target) return target @@ -245,6 +252,8 @@ def get_base_rom_data(game: str): get_base_rom_bytes = lambda: bytes(read_rom(open(get_base_rom_path(), "rb"))) elif game == GAME_SMZ3: from worlds.smz3.Rom import get_base_rom_bytes + elif game == GAME_DKC3: + from worlds.dkc3.Rom import get_base_rom_bytes else: raise RuntimeError("Selected game for base rom not found.") return get_base_rom_bytes() @@ -389,6 +398,13 @@ if __name__ == "__main__": if 'server' in data: Utils.persistent_store("servers", data['hash'], data['server']) print(f"Host is {data['server']}") + elif rom.endswith(".apdkc3"): + print(f"Applying patch {rom}") + data, target = create_rom_file(rom) + print(f"Created rom {target}.") + if 'server' in data: + Utils.persistent_store("servers", data['hash'], data['server']) + print(f"Host is {data['server']}") elif rom.endswith(".zip"): print(f"Updating host in patch files contained in {rom}") @@ -396,7 +412,9 @@ if __name__ == "__main__": def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str): data = zfr.read(zfinfo) - if zfinfo.filename.endswith(".apbp") or zfinfo.filename.endswith(".apm3"): + if zfinfo.filename.endswith(".apbp") or \ + zfinfo.filename.endswith(".apm3") or \ + zfinfo.filename.endswith(".apdkc3"): data = update_patch_data(data, server) with ziplock: zfw.writestr(zfinfo, data) diff --git a/README.md b/README.md index 2b9cde4093..a3a06d480b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Currently, the following games are supported: * The Witness * Sonic Adventure 2: Battle * Starcraft 2: Wings of Liberty +* Donkey Kong Country 3 +* Dark Souls 3 For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/SNIClient.py b/SNIClient.py index 151a68da11..072d04cc99 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -33,7 +33,7 @@ from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT from worlds.smz3.Rom import ROM_PLAYER_LIMIT as SMZ3_ROM_PLAYER_LIMIT import Utils from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser -from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3 +from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3 snes_logger = logging.getLogger("SNES") @@ -251,6 +251,9 @@ async def deathlink_kill_player(ctx: Context): if not gamemode or gamemode[0] in SM_DEATH_MODES or ( ctx.death_link_allow_survive and health is not None and health > 0): ctx.death_state = DeathState.dead + elif ctx.game == GAME_DKC3: + from worlds.dkc3.Client import deathlink_kill_player as dkc3_deathlink_kill_player + await dkc3_deathlink_kill_player(ctx) ctx.last_death_link = time.time() @@ -1034,44 +1037,48 @@ async def game_watcher(ctx: Context): if not ctx.rom: ctx.finished_game = False ctx.death_link_allow_survive = False - game_name = await snes_read(ctx, SM_ROMNAME_START, 5) - if game_name is None: - continue - elif game_name[:2] == b"SM": - ctx.game = GAME_SM - # versions lower than 0.3.0 dont have item handling flag nor remote item support - romVersion = int(game_name[2:5].decode('UTF-8')) - if romVersion < 30: - ctx.items_handling = 0b001 # full local - else: - item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1) - ctx.items_handling = 0b001 if item_handling is None else item_handling[0] - else: - game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3) - if game_name == b"ZSM": - ctx.game = GAME_SMZ3 - ctx.items_handling = 0b101 # local items and remote start inventory - else: - ctx.game = GAME_ALTTP - ctx.items_handling = 0b001 # full local - rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else SMZ3_ROMNAME_START if ctx.game == GAME_SMZ3 else ROMNAME_START, ROMNAME_SIZE) - if rom is None or rom == bytes([0] * ROMNAME_SIZE): - continue + from worlds.dkc3.Client import dkc3_rom_init + init_handled = await dkc3_rom_init(ctx) + if not init_handled: + game_name = await snes_read(ctx, SM_ROMNAME_START, 5) + if game_name is None: + continue + elif game_name[:2] == b"SM": + ctx.game = GAME_SM + # versions lower than 0.3.0 dont have item handling flag nor remote item support + romVersion = int(game_name[2:5].decode('UTF-8')) + if romVersion < 30: + ctx.items_handling = 0b001 # full local + else: + item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1) + ctx.items_handling = 0b001 if item_handling is None else item_handling[0] + else: + game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3) + if game_name == b"ZSM": + ctx.game = GAME_SMZ3 + ctx.items_handling = 0b101 # local items and remote start inventory + else: + ctx.game = GAME_ALTTP + ctx.items_handling = 0b001 # full local - ctx.rom = rom - if ctx.game != GAME_SMZ3: - death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else - SM_DEATH_LINK_ACTIVE_ADDR, 1) - if death_link: - ctx.allow_collect = bool(death_link[0] & 0b100) - ctx.death_link_allow_survive = bool(death_link[0] & 0b10) - await ctx.update_death_link(bool(death_link[0] & 0b1)) - if not ctx.prev_rom or ctx.prev_rom != ctx.rom: - ctx.locations_checked = set() - ctx.locations_scouted = set() - ctx.locations_info = {} - ctx.prev_rom = ctx.rom + rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else SMZ3_ROMNAME_START if ctx.game == GAME_SMZ3 else ROMNAME_START, ROMNAME_SIZE) + if rom is None or rom == bytes([0] * ROMNAME_SIZE): + continue + + ctx.rom = rom + if ctx.game != GAME_SMZ3: + death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else + SM_DEATH_LINK_ACTIVE_ADDR, 1) + if death_link: + ctx.allow_collect = bool(death_link[0] & 0b100) + ctx.death_link_allow_survive = bool(death_link[0] & 0b10) + await ctx.update_death_link(bool(death_link[0] & 0b1)) + if not ctx.prev_rom or ctx.prev_rom != ctx.rom: + ctx.locations_checked = set() + ctx.locations_scouted = set() + ctx.locations_info = {} + ctx.prev_rom = ctx.rom if ctx.awaiting_rom: await ctx.server_auth(False) @@ -1279,6 +1286,9 @@ async def game_watcher(ctx: Context): color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names[item.location], itemOutPtr, len(ctx.items_received))) await snes_flush_writes(ctx) + elif ctx.game == GAME_DKC3: + from worlds.dkc3.Client import dkc3_game_watcher + await dkc3_game_watcher(ctx) async def run_game(romfile): diff --git a/host.yaml b/host.yaml index af16a8258e..86f88de024 100644 --- a/host.yaml +++ b/host.yaml @@ -127,3 +127,12 @@ smz3_options: # True for operating system default program # Alternatively, a path to a program to open the .sfc file with rom_start: true +dkc3_options: + # File name of the DKC3 US rom + rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc" + # Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found + sni: "SNI" + # Set this to false to never autostart a rom (such as after patching) + # True for operating system default program + # Alternatively, a path to a program to open the .sfc file with + rom_start: true diff --git a/inno_setup.iss b/inno_setup.iss index 1005cadad0..ff2da1211a 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -54,6 +54,7 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed Name: "generator"; Description: "Generator"; Types: full hosting Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning +Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning @@ -62,6 +63,7 @@ Name: "client"; Description: "Clients"; Types: full playing Name: "client/sni"; Description: "SNI Client"; Types: full playing Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning +Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing @@ -76,6 +78,7 @@ NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-mod [Files] Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm +Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3 Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs @@ -143,6 +146,11 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archi Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni + Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni @@ -206,6 +214,9 @@ var LttPROMFilePage: TInputFileWizardPage; var smrom: string; var SMRomFilePage: TInputFileWizardPage; +var dkc3rom: string; +var DKC3RomFilePage: TInputFileWizardPage; + var soerom: string; var SoERomFilePage: TInputFileWizardPage; @@ -295,6 +306,8 @@ begin Result := not (LttPROMFilePage.Values[0] = '') else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then Result := not (SMROMFilePage.Values[0] = '') + else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then + Result := not (DKC3ROMFilePage.Values[0] = '') else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then Result := not (SoEROMFilePage.Values[0] = '') else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then @@ -335,6 +348,22 @@ begin Result := ''; end; +function GetDKC3ROMPath(Param: string): string; +begin + if Length(dkc3rom) > 0 then + Result := dkc3rom + else if Assigned(DKC3RomFilePage) then + begin + R := CompareStr(GetSNESMD5OfFile(DKC3ROMFilePage.Values[0]), '120abf304f0c40fe059f6a192ed4f947') + if R <> 0 then + MsgBox('Donkey Kong Country 3 ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := DKC3ROMFilePage.Values[0] + end + else + Result := ''; + end; + function GetSoEROMPath(Param: string): string; begin if Length(soerom) > 0 then @@ -379,6 +408,10 @@ begin if Length(smrom) = 0 then SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc'); + dkc3rom := CheckRom('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc', '120abf304f0c40fe059f6a192ed4f947'); + if Length(dkc3rom) = 0 then + DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc'); + soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); if Length(soerom) = 0 then SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); @@ -392,6 +425,8 @@ begin Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp')); if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm')); + if (assigned(DKC3ROMFilePage)) and (PageID = DKC3ROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3')); if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/soe')); if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py new file mode 100644 index 0000000000..12643a5fcf --- /dev/null +++ b/worlds/dkc3/Client.py @@ -0,0 +1,220 @@ +import logging +import asyncio + +from NetUtils import ClientStatus, color +from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read +from Patch import GAME_DKC3 + +snes_logger = logging.getLogger("SNES") + +# DKC3 - DKC3_TODO: Check these values +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +SAVEDATA_START = WRAM_START + 0xF000 +SAVEDATA_SIZE = 0x500 + +DKC3_ROMNAME_START = 0x00FFC0 +DKC3_ROMHASH_START = 0x7FC0 +ROMNAME_SIZE = 0x15 +ROMHASH_SIZE = 0x15 + +DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632 # DKC3_TODO: Find a permanent home for this +DKC3_FILE_NAME_ADDR = WRAM_START + 0x5D9 +DEATH_LINK_ACTIVE_ADDR = DKC3_ROMNAME_START + 0x15 # DKC3_TODO: Find a permanent home for this + + +async def deathlink_kill_player(ctx: Context): + pass + #if ctx.game == GAME_DKC3: + # DKC3_TODO: Handle Receiving Deathlink + + +async def dkc3_rom_init(ctx: Context): + if not ctx.rom: + ctx.finished_game = False + ctx.death_link_allow_survive = False + game_name = await snes_read(ctx, DKC3_ROMNAME_START, 0x15) + if game_name is None or game_name != b"DONKEY KONG COUNTRY 3": + return False + else: + ctx.game = GAME_DKC3 + ctx.items_handling = 0b111 # remote items + + rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) + if rom is None or rom == bytes([0] * ROMHASH_SIZE): + return False + + ctx.rom = rom + + #death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1) + ## DKC3_TODO: Handle Deathlink + #if death_link: + # ctx.allow_collect = bool(death_link[0] & 0b100) + # await ctx.update_death_link(bool(death_link[0] & 0b1)) + return True + + +async def dkc3_game_watcher(ctx: Context): + if ctx.game == GAME_DKC3: + # DKC3_TODO: Handle Deathlink + save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) + if save_file_name is None or save_file_name[0] == 0x00: + # We haven't loaded a save file + return + + new_checks = [] + from worlds.dkc3.Rom import location_rom_data, item_rom_data + for loc_id, loc_data in location_rom_data.items(): + if loc_id not in ctx.locations_checked: + data = await snes_read(ctx, WRAM_START + loc_data[0], 1) + masked_data = data[0] & (1 << loc_data[1]) + bit_set = (masked_data != 0) + invert_bit = ((len(loc_data) >= 3) and loc_data[2]) + if bit_set != invert_bit: + # DKC3_TODO: Handle non-included checks + new_checks.append(loc_id) + + save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) + if save_file_name is None or save_file_name[0] == 0x00: + # We have somehow exited the save file + return + + rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) + if rom != ctx.rom: + ctx.rom = None + # We have somehow loaded a different ROM + return + + for new_check_id in new_checks: + ctx.locations_checked.add(new_check_id) + location = ctx.location_names[new_check_id] + snes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) + + # DKC3_TODO: Make this actually visually display new things received (ASM Hook required) + recv_count = await snes_read(ctx, DKC3_RECV_PROGRESS_ADDR, 1) + recv_index = recv_count[0] + + if recv_index < len(ctx.items_received): + item = ctx.items_received[recv_index] + recv_index += 1 + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names[item.location], recv_index, len(ctx.items_received))) + + snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index])) + if item.item in item_rom_data: + item_count = await snes_read(ctx, WRAM_START + item_rom_data[item.item][0], 0x1) + new_item_count = item_count[0] + 1 + for address in item_rom_data[item.item]: + snes_buffered_write(ctx, WRAM_START + address, bytes([new_item_count])) + + # Handle Coin Displays + current_level = await snes_read(ctx, WRAM_START + 0x5E3, 0x5) + if item.item == 0xDC3002 and (current_level[0] == 0x0A and current_level[2] == 0x00 and current_level[4] == 0x03): + # Bazaar and Barter + item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1) + new_item_count = item_count[0] + 1 + snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count])) + elif item.item == 0xDC3002 and current_level[0] == 0x04: + # Swanky + item_count = await snes_read(ctx, WRAM_START + 0xA26, 0x1) + new_item_count = item_count[0] + 1 + snes_buffered_write(ctx, WRAM_START + 0xA26, bytes([new_item_count])) + elif item.item == 0xDC3003 and (current_level[0] == 0x0A and current_level[2] == 0x08 and current_level[4] == 0x01): + # Boomer + item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1) + new_item_count = item_count[0] + 1 + snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count])) + else: + # Handle Patch and Skis + if item.item == 0xDC3007: + num_upgrades = 1 + inventory = await snes_read(ctx, WRAM_START + 0x605, 0xF) + + if (inventory[0] & 0x02): + num_upgrades = 3 + elif (inventory[13] & 0x08) or (inventory[0] & 0x01): + num_upgrades = 2 + + if num_upgrades == 1: + snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x01])) + if inventory[4] == 0: + snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x01])) + elif inventory[6] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x01])) + elif inventory[8] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x01])) + elif inventory[10] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x01])) + + cove_mekanos_progress = await snes_read(ctx, WRAM_START + 0x691, 0x2) + snes_buffered_write(ctx, WRAM_START + 0x691, bytes([cove_mekanos_progress[0] | 0x01])) + snes_buffered_write(ctx, WRAM_START + 0x692, bytes([cove_mekanos_progress[1] | 0x01])) + elif num_upgrades == 2: + snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x02])) + if inventory[4] == 0: + snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x02])) + elif inventory[6] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x02])) + elif inventory[8] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x02])) + elif inventory[10] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x02])) + elif num_upgrades == 3: + snes_buffered_write(ctx, WRAM_START + 0x606, bytes([inventory[1] | 0x20])) + + k3_ridge_progress = await snes_read(ctx, WRAM_START + 0x693, 0x2) + snes_buffered_write(ctx, WRAM_START + 0x693, bytes([k3_ridge_progress[0] | 0x01])) + snes_buffered_write(ctx, WRAM_START + 0x694, bytes([k3_ridge_progress[1] | 0x01])) + elif item.item == 0xDC3000: + # Handle Victory + if not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + else: + print("Item Not Recognized: ", item.item) + pass + + await snes_flush_writes(ctx) + + # DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged + # Handle Collected Locations + #for loc_id in ctx.checked_locations: + # if loc_id not in ctx.locations_checked: + # loc_data = location_rom_data[loc_id] + # data = await snes_read(ctx, WRAM_START + loc_data[0], 1) + # invert_bit = ((len(loc_data) >= 3) and loc_data[2]) + # if not invert_bit: + # masked_data = data[0] | (1 << loc_data[1]) + # print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1]) + # snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) + # await snes_flush_writes(ctx) + # else: + # masked_data = data[0] & ~(1 << loc_data[1]) + # print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1]) + # snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) + # await snes_flush_writes(ctx) + # ctx.locations_checked.add(loc_id) + + # Calculate Boomer Cost Text + boomer_cost_text = await snes_read(ctx, WRAM_START + 0xAAFD, 2) + if boomer_cost_text[0] == 0x31 and boomer_cost_text[1] == 0x35: + boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1) + boomer_cost_tens = int(boomer_cost[0]) // 10 + boomer_cost_ones = int(boomer_cost[0]) % 10 + snes_buffered_write(ctx, WRAM_START + 0xAAFD, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones])) + await snes_flush_writes(ctx) + + boomer_final_cost_text = await snes_read(ctx, WRAM_START + 0xAB9B, 2) + if boomer_final_cost_text[0] == 0x32 and boomer_final_cost_text[1] == 0x35: + boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1) + boomer_cost_tens = boomer_cost[0] // 10 + boomer_cost_ones = boomer_cost[0] % 10 + snes_buffered_write(ctx, WRAM_START + 0xAB9B, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones])) + await snes_flush_writes(ctx) diff --git a/worlds/dkc3/Items.py b/worlds/dkc3/Items.py new file mode 100644 index 0000000000..358873cd20 --- /dev/null +++ b/worlds/dkc3/Items.py @@ -0,0 +1,52 @@ +import typing + +from BaseClasses import Item, ItemClassification +from .Names import ItemName + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + progression: bool + quantity: int = 1 + event: bool = False + + +class DKC3Item(Item): + game: str = "Donkey Kong Country 3" + + +# Separate tables for each type of item. +junk_table = { + ItemName.one_up_balloon: ItemData(0xDC3001, False), + ItemName.bear_coin: ItemData(0xDC3002, False), +} + +collectable_table = { + ItemName.bonus_coin: ItemData(0xDC3003, True), + ItemName.dk_coin: ItemData(0xDC3004, True), + ItemName.banana_bird: ItemData(0xDC3005, True), + ItemName.krematoa_cog: ItemData(0xDC3006, True), + ItemName.progressive_boat: ItemData(0xDC3007, True), +} + +inventory_table = { + ItemName.present: ItemData(0xDC3008, True), + ItemName.bowling_ball: ItemData(0xDC3009, True), + ItemName.shell: ItemData(0xDC300A, True), + ItemName.mirror: ItemData(0xDC300B, True), + ItemName.flower: ItemData(0xDC300C, True), + ItemName.wrench: ItemData(0xDC300D, True), +} + +event_table = { + ItemName.victory: ItemData(0xDC3000, True), +} + +# Complete item table. +item_table = { + **junk_table, + **collectable_table, + **event_table, +} + +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} diff --git a/worlds/dkc3/Levels.py b/worlds/dkc3/Levels.py new file mode 100644 index 0000000000..c3983ab817 --- /dev/null +++ b/worlds/dkc3/Levels.py @@ -0,0 +1,115 @@ + +from .Names import LocationName + +class DKC3Level(): + nameIDAddress: int + levelIDAddress: int + nameID: int + levelID: int + + def __init__(self, nameIDAddress: int, levelIDAddress: int, nameID: int, levelID: int): + self.nameIDAddress = nameIDAddress + self.levelIDAddress = levelIDAddress + self.nameID = nameID + self.levelID = levelID + + +level_dict = { + LocationName.lakeside_limbo_region: DKC3Level(0x34D19C, 0x34D19D, 0x01, 0x25), + LocationName.doorstop_dash_region: DKC3Level(0x34D1A7, 0x34D1A8, 0x02, 0x28), + LocationName.tidal_trouble_region: DKC3Level(0x34D1BD, 0x34D1BE, 0x04, 0x27), + LocationName.skiddas_row_region: DKC3Level(0x34D1C8, 0x34D1C9, 0x05, 0x2B), + LocationName.murky_mill_region: DKC3Level(0x34D1D3, 0x34D1D4, 0x0D, 0x2A), + + LocationName.barrel_shield_bust_up_region: DKC3Level(0x34D217, 0x34D218, 0x0B, 0x30), + LocationName.riverside_race_region: DKC3Level(0x34D22D, 0x34D22E, 0x0C, 0x32), + LocationName.squeals_on_wheels_region: DKC3Level(0x34D238, 0x34D239, 0x06, 0x29), + LocationName.springin_spiders_region: DKC3Level(0x34D24E, 0x34D24F, 0x0E, 0x2F), + LocationName.bobbing_barrel_brawl_region: DKC3Level(0x34D264, 0x34D265, 0x37, 0x34), + + LocationName.bazzas_blockade_region: DKC3Level(0x34D29D, 0x34D29E, 0x14, 0x35), + LocationName.rocket_barrel_ride_region: DKC3Level(0x34D2A8, 0x34D2A9, 0x15, 0x38), + LocationName.kreeping_klasps_region: DKC3Level(0x34D2BE, 0x34D2BF, 0x16, 0x26), + LocationName.tracker_barrel_trek_region: DKC3Level(0x34D2D4, 0x34D2D5, 0x17, 0x39), + LocationName.fish_food_frenzy_region: DKC3Level(0x34D2DF, 0x34D2E0, 0x18, 0x36), + + LocationName.fire_ball_frenzy_region: DKC3Level(0x34D30D, 0x34D30E, 0x1B, 0x3B), + LocationName.demolition_drain_pipe_region: DKC3Level(0x34D323, 0x34D324, 0x1D, 0x40), + LocationName.ripsaw_rage_region: DKC3Level(0x34D339, 0x34D33A, 0x1E, 0x2E), + LocationName.blazing_bazookas_region: DKC3Level(0x34D34F, 0x34D350, 0x1F, 0x3C), + LocationName.low_g_labyrinth_region: DKC3Level(0x34D35A, 0x34D35B, 0x20, 0x3E), + + LocationName.krevice_kreepers_region: DKC3Level(0x34D388, 0x34D389, 0x23, 0x41), + LocationName.tearaway_toboggan_region: DKC3Level(0x34D393, 0x34D394, 0x24, 0x2D), + LocationName.barrel_drop_bounce_region: DKC3Level(0x34D39E, 0x34D39F, 0x25, 0x3A), + LocationName.krack_shot_kroc_region: DKC3Level(0x34D3A9, 0x34D3AA, 0x26, 0x3D), + LocationName.lemguin_lunge_region: DKC3Level(0x34D3B4, 0x34D3B5, 0x27, 0x2C), + + LocationName.buzzer_barrage_region: DKC3Level(0x34D40E, 0x34D40F, 0x2B, 0x44), + LocationName.kong_fused_cliffs_region: DKC3Level(0x34D424, 0x34D425, 0x2D, 0x42), + LocationName.floodlit_fish_region: DKC3Level(0x34D42F, 0x34D430, 0x2E, 0x37), + LocationName.pothole_panic_region: DKC3Level(0x34D43A, 0x34D43B, 0x2F, 0x45), + LocationName.ropey_rumpus_region: DKC3Level(0x34D450, 0x34D451, 0x30, 0x43), + + LocationName.konveyor_rope_clash_region: DKC3Level(0x34D489, 0x34D48A, 0x38, 0x48), + LocationName.creepy_caverns_region: DKC3Level(0x34D49F, 0x34D4A0, 0x36, 0x46), + LocationName.lightning_lookout_region: DKC3Level(0x34D4AA, 0x34D4AB, 0x10, 0x33), + LocationName.koindozer_klamber_region: DKC3Level(0x34D4C0, 0x34D4C1, 0x34, 0x47), + LocationName.poisonous_pipeline_region: DKC3Level(0x34D4D6, 0x34D4D7, 0x39, 0x3F), + + LocationName.stampede_sprint_region: DKC3Level(0x34D51A, 0x34D51B, 0x3D, 0x49), + LocationName.criss_cross_cliffs_region: DKC3Level(0x34D525, 0x34D526, 0x3E, 0x4A), + LocationName.tyrant_twin_tussle_region: DKC3Level(0x34D530, 0x34D531, 0x3F, 0x4B), + LocationName.swoopy_salvo_region: DKC3Level(0x34D53B, 0x34D53C, 0x40, 0x31), + #LocationName.rocket_rush_region: DKC3Level(0x34D546, 0x34D547, 0x05, 0x4C), # Rocket Rush is not getting shuffled +} + +level_list = [ + LocationName.lakeside_limbo_region, + LocationName.doorstop_dash_region, + LocationName.tidal_trouble_region, + LocationName.skiddas_row_region, + LocationName.murky_mill_region, + + LocationName.barrel_shield_bust_up_region, + LocationName.riverside_race_region, + LocationName.squeals_on_wheels_region, + LocationName.springin_spiders_region, + LocationName.bobbing_barrel_brawl_region, + + LocationName.bazzas_blockade_region, + LocationName.rocket_barrel_ride_region, + LocationName.kreeping_klasps_region, + LocationName.tracker_barrel_trek_region, + LocationName.fish_food_frenzy_region, + + LocationName.fire_ball_frenzy_region, + LocationName.demolition_drain_pipe_region, + LocationName.ripsaw_rage_region, + LocationName.blazing_bazookas_region, + LocationName.low_g_labyrinth_region, + + LocationName.krevice_kreepers_region, + LocationName.tearaway_toboggan_region, + LocationName.barrel_drop_bounce_region, + LocationName.krack_shot_kroc_region, + LocationName.lemguin_lunge_region, + + LocationName.buzzer_barrage_region, + LocationName.kong_fused_cliffs_region, + LocationName.floodlit_fish_region, + LocationName.pothole_panic_region, + LocationName.ropey_rumpus_region, + + LocationName.konveyor_rope_clash_region, + LocationName.creepy_caverns_region, + LocationName.lightning_lookout_region, + LocationName.koindozer_klamber_region, + LocationName.poisonous_pipeline_region, + + LocationName.stampede_sprint_region, + LocationName.criss_cross_cliffs_region, + LocationName.tyrant_twin_tussle_region, + LocationName.swoopy_salvo_region, + #LocationName.rocket_rush_region, +] diff --git a/worlds/dkc3/Locations.py b/worlds/dkc3/Locations.py new file mode 100644 index 0000000000..aa8acf729a --- /dev/null +++ b/worlds/dkc3/Locations.py @@ -0,0 +1,283 @@ +import typing + +from BaseClasses import Location +from .Names import LocationName + + +class DKC3Location(Location): + game: str = "Donkey Kong Country 3" + + progress_byte: int = 0x000000 + progress_bit: int = 0 + inverted_bit: bool = False + + def __init__(self, player: int, name: str = '', address: int = None, parent=None, prog_byte: int = None, prog_bit: int = None, invert: bool = False): + super().__init__(player, name, address, parent) + self.progress_byte = prog_byte + self.progress_bit = prog_bit + self.inverted_bit = invert + + +level_location_table = { + LocationName.lakeside_limbo_flag: 0xDC3000, + LocationName.lakeside_limbo_bonus_1: 0xDC3001, + LocationName.lakeside_limbo_bonus_2: 0xDC3002, + LocationName.lakeside_limbo_dk: 0xDC3003, + + LocationName.doorstop_dash_flag: 0xDC3004, + LocationName.doorstop_dash_bonus_1: 0xDC3005, + LocationName.doorstop_dash_bonus_2: 0xDC3006, + LocationName.doorstop_dash_dk: 0xDC3007, + + LocationName.tidal_trouble_flag: 0xDC3008, + LocationName.tidal_trouble_bonus_1: 0xDC3009, + LocationName.tidal_trouble_bonus_2: 0xDC300A, + LocationName.tidal_trouble_dk: 0xDC300B, + + LocationName.skiddas_row_flag: 0xDC300C, + LocationName.skiddas_row_bonus_1: 0xDC300D, + LocationName.skiddas_row_bonus_2: 0xDC300E, + LocationName.skiddas_row_dk: 0xDC300F, + + LocationName.murky_mill_flag: 0xDC3010, + LocationName.murky_mill_bonus_1: 0xDC3011, + LocationName.murky_mill_bonus_2: 0xDC3012, + LocationName.murky_mill_dk: 0xDC3013, + + LocationName.barrel_shield_bust_up_flag: 0xDC3014, + LocationName.barrel_shield_bust_up_bonus_1: 0xDC3015, + LocationName.barrel_shield_bust_up_bonus_2: 0xDC3016, + LocationName.barrel_shield_bust_up_dk: 0xDC3017, + + LocationName.riverside_race_flag: 0xDC3018, + LocationName.riverside_race_bonus_1: 0xDC3019, + LocationName.riverside_race_bonus_2: 0xDC301A, + LocationName.riverside_race_dk: 0xDC301B, + + LocationName.squeals_on_wheels_flag: 0xDC301C, + LocationName.squeals_on_wheels_bonus_1: 0xDC301D, + LocationName.squeals_on_wheels_bonus_2: 0xDC301E, + LocationName.squeals_on_wheels_dk: 0xDC301F, + + LocationName.springin_spiders_flag: 0xDC3020, + LocationName.springin_spiders_bonus_1: 0xDC3021, + LocationName.springin_spiders_bonus_2: 0xDC3022, + LocationName.springin_spiders_dk: 0xDC3023, + + LocationName.bobbing_barrel_brawl_flag: 0xDC3024, + LocationName.bobbing_barrel_brawl_bonus_1: 0xDC3025, + LocationName.bobbing_barrel_brawl_bonus_2: 0xDC3026, + LocationName.bobbing_barrel_brawl_dk: 0xDC3027, + + LocationName.bazzas_blockade_flag: 0xDC3028, + LocationName.bazzas_blockade_bonus_1: 0xDC3029, + LocationName.bazzas_blockade_bonus_2: 0xDC302A, + LocationName.bazzas_blockade_dk: 0xDC302B, + + LocationName.rocket_barrel_ride_flag: 0xDC302C, + LocationName.rocket_barrel_ride_bonus_1: 0xDC302D, + LocationName.rocket_barrel_ride_bonus_2: 0xDC302E, + LocationName.rocket_barrel_ride_dk: 0xDC302F, + + LocationName.kreeping_klasps_flag: 0xDC3030, + LocationName.kreeping_klasps_bonus_1: 0xDC3031, + LocationName.kreeping_klasps_bonus_2: 0xDC3032, + LocationName.kreeping_klasps_dk: 0xDC3033, + + LocationName.tracker_barrel_trek_flag: 0xDC3034, + LocationName.tracker_barrel_trek_bonus_1: 0xDC3035, + LocationName.tracker_barrel_trek_bonus_2: 0xDC3036, + LocationName.tracker_barrel_trek_dk: 0xDC3037, + + LocationName.fish_food_frenzy_flag: 0xDC3038, + LocationName.fish_food_frenzy_bonus_1: 0xDC3039, + LocationName.fish_food_frenzy_bonus_2: 0xDC303A, + LocationName.fish_food_frenzy_dk: 0xDC303B, + + LocationName.fire_ball_frenzy_flag: 0xDC303C, + LocationName.fire_ball_frenzy_bonus_1: 0xDC303D, + LocationName.fire_ball_frenzy_bonus_2: 0xDC303E, + LocationName.fire_ball_frenzy_dk: 0xDC303F, + + LocationName.demolition_drain_pipe_flag: 0xDC3040, + LocationName.demolition_drain_pipe_bonus_1: 0xDC3041, + LocationName.demolition_drain_pipe_bonus_2: 0xDC3042, + LocationName.demolition_drain_pipe_dk: 0xDC3043, + + LocationName.ripsaw_rage_flag: 0xDC3044, + LocationName.ripsaw_rage_bonus_1: 0xDC3045, + LocationName.ripsaw_rage_bonus_2: 0xDC3046, + LocationName.ripsaw_rage_dk: 0xDC3047, + + LocationName.blazing_bazookas_flag: 0xDC3048, + LocationName.blazing_bazookas_bonus_1: 0xDC3049, + LocationName.blazing_bazookas_bonus_2: 0xDC304A, + LocationName.blazing_bazookas_dk: 0xDC304B, + + LocationName.low_g_labyrinth_flag: 0xDC304C, + LocationName.low_g_labyrinth_bonus_1: 0xDC304D, + LocationName.low_g_labyrinth_bonus_2: 0xDC304E, + LocationName.low_g_labyrinth_dk: 0xDC304F, + + LocationName.krevice_kreepers_flag: 0xDC3050, + LocationName.krevice_kreepers_bonus_1: 0xDC3051, + LocationName.krevice_kreepers_bonus_2: 0xDC3052, + LocationName.krevice_kreepers_dk: 0xDC3053, + + LocationName.tearaway_toboggan_flag: 0xDC3054, + LocationName.tearaway_toboggan_bonus_1: 0xDC3055, + LocationName.tearaway_toboggan_bonus_2: 0xDC3056, + LocationName.tearaway_toboggan_dk: 0xDC3057, + + LocationName.barrel_drop_bounce_flag: 0xDC3058, + LocationName.barrel_drop_bounce_bonus_1: 0xDC3059, + LocationName.barrel_drop_bounce_bonus_2: 0xDC305A, + LocationName.barrel_drop_bounce_dk: 0xDC305B, + + LocationName.krack_shot_kroc_flag: 0xDC305C, + LocationName.krack_shot_kroc_bonus_1: 0xDC305D, + LocationName.krack_shot_kroc_bonus_2: 0xDC305E, + LocationName.krack_shot_kroc_dk: 0xDC305F, + + LocationName.lemguin_lunge_flag: 0xDC3060, + LocationName.lemguin_lunge_bonus_1: 0xDC3061, + LocationName.lemguin_lunge_bonus_2: 0xDC3062, + LocationName.lemguin_lunge_dk: 0xDC3063, + + LocationName.buzzer_barrage_flag: 0xDC3064, + LocationName.buzzer_barrage_bonus_1: 0xDC3065, + LocationName.buzzer_barrage_bonus_2: 0xDC3066, + LocationName.buzzer_barrage_dk: 0xDC3067, + + LocationName.kong_fused_cliffs_flag: 0xDC3068, + LocationName.kong_fused_cliffs_bonus_1: 0xDC3069, + LocationName.kong_fused_cliffs_bonus_2: 0xDC306A, + LocationName.kong_fused_cliffs_dk: 0xDC306B, + + LocationName.floodlit_fish_flag: 0xDC306C, + LocationName.floodlit_fish_bonus_1: 0xDC306D, + LocationName.floodlit_fish_bonus_2: 0xDC306E, + LocationName.floodlit_fish_dk: 0xDC306F, + + LocationName.pothole_panic_flag: 0xDC3070, + LocationName.pothole_panic_bonus_1: 0xDC3071, + LocationName.pothole_panic_bonus_2: 0xDC3072, + LocationName.pothole_panic_dk: 0xDC3073, + + LocationName.ropey_rumpus_flag: 0xDC3074, + LocationName.ropey_rumpus_bonus_1: 0xDC3075, + LocationName.ropey_rumpus_bonus_2: 0xDC3076, + LocationName.ropey_rumpus_dk: 0xDC3077, + + LocationName.konveyor_rope_clash_flag: 0xDC3078, + LocationName.konveyor_rope_clash_bonus_1: 0xDC3079, + LocationName.konveyor_rope_clash_bonus_2: 0xDC307A, + LocationName.konveyor_rope_clash_dk: 0xDC307B, + + LocationName.creepy_caverns_flag: 0xDC307C, + LocationName.creepy_caverns_bonus_1: 0xDC307D, + LocationName.creepy_caverns_bonus_2: 0xDC307E, + LocationName.creepy_caverns_dk: 0xDC307F, + + LocationName.lightning_lookout_flag: 0xDC3080, + LocationName.lightning_lookout_bonus_1: 0xDC3081, + LocationName.lightning_lookout_bonus_2: 0xDC3082, + LocationName.lightning_lookout_dk: 0xDC3083, + + LocationName.koindozer_klamber_flag: 0xDC3084, + LocationName.koindozer_klamber_bonus_1: 0xDC3085, + LocationName.koindozer_klamber_bonus_2: 0xDC3086, + LocationName.koindozer_klamber_dk: 0xDC3087, + + LocationName.poisonous_pipeline_flag: 0xDC3088, + LocationName.poisonous_pipeline_bonus_1: 0xDC3089, + LocationName.poisonous_pipeline_bonus_2: 0xDC308A, + LocationName.poisonous_pipeline_dk: 0xDC308B, + + LocationName.stampede_sprint_flag: 0xDC308C, + LocationName.stampede_sprint_bonus_1: 0xDC308D, + LocationName.stampede_sprint_bonus_2: 0xDC308E, + LocationName.stampede_sprint_bonus_3: 0xDC308F, + LocationName.stampede_sprint_dk: 0xDC3090, + + LocationName.criss_cross_cliffs_flag: 0xDC3091, + LocationName.criss_cross_cliffs_bonus_1: 0xDC3092, + LocationName.criss_cross_cliffs_bonus_2: 0xDC3093, + LocationName.criss_cross_cliffs_dk: 0xDC3094, + + LocationName.tyrant_twin_tussle_flag: 0xDC3095, + LocationName.tyrant_twin_tussle_bonus_1: 0xDC3096, + LocationName.tyrant_twin_tussle_bonus_2: 0xDC3097, + LocationName.tyrant_twin_tussle_bonus_3: 0xDC3098, + LocationName.tyrant_twin_tussle_dk: 0xDC3099, + + LocationName.swoopy_salvo_flag: 0xDC309A, + LocationName.swoopy_salvo_bonus_1: 0xDC309B, + LocationName.swoopy_salvo_bonus_2: 0xDC309C, + LocationName.swoopy_salvo_bonus_3: 0xDC309D, + LocationName.swoopy_salvo_dk: 0xDC309E, + + LocationName.rocket_rush_flag: 0xDC309F, + LocationName.rocket_rush_dk: 0xDC30A0, +} + + +boss_location_table = { + LocationName.belchas_barn: 0xDC30A1, + LocationName.arichs_ambush: 0xDC30A2, + LocationName.squirts_showdown: 0xDC30A3, + LocationName.kaos_karnage: 0xDC30A4, + LocationName.bleaks_house: 0xDC30A5, + LocationName.barboss_barrier: 0xDC30A6, + LocationName.kastle_kaos: 0xDC30A7, + LocationName.knautilus: 0xDC30A8, +} + +secret_cave_location_table = { + LocationName.belchas_burrow: 0xDC30A9, + LocationName.kong_cave: 0xDC30AA, + LocationName.undercover_cove: 0xDC30AB, + LocationName.ks_cache: 0xDC30AC, + LocationName.hill_top_hoard: 0xDC30AD, + LocationName.bounty_beach: 0xDC30AE, + LocationName.smugglers_cove: 0xDC30AF, + LocationName.arichs_hoard: 0xDC30B0, + LocationName.bounty_bay: 0xDC30B1, + LocationName.sky_high_secret: 0xDC30B2, + LocationName.glacial_grotto: 0xDC30B3, + LocationName.cifftop_cache: 0xDC30B4, + LocationName.sewer_stockpile: 0xDC30B5, + LocationName.banana_bird_mother: 0xDC30B6, +} + +brothers_bear_location_table = { + LocationName.bazaars_general_store_1: 0xDC30B7, + LocationName.bazaars_general_store_2: 0xDC30B8, + LocationName.brambles_bungalow: 0xDC30B9, + LocationName.flower_spot: 0xDC30BA, + LocationName.barters_swap_shop: 0xDC30BB, + LocationName.barnacles_island: 0xDC30BC, + LocationName.blues_beach_hut: 0xDC30BD, + LocationName.blizzards_basecamp: 0xDC30BE, +} + +all_locations = { + **level_location_table, + **boss_location_table, + **secret_cave_location_table, + **brothers_bear_location_table, +} + +location_table = {} + + +def setup_locations(world, player: int): + location_table = {**level_location_table, **boss_location_table, **secret_cave_location_table} + + if False:#world.include_trade_sequence[player].value: + location_table.update({**brothers_bear_location_table}) + + return location_table + + +lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in all_locations.items()} diff --git a/worlds/dkc3/Names/ItemName.py b/worlds/dkc3/Names/ItemName.py new file mode 100644 index 0000000000..d3395832f4 --- /dev/null +++ b/worlds/dkc3/Names/ItemName.py @@ -0,0 +1,21 @@ +# Junk Definitions +one_up_balloon = "1-Up Balloon" +bear_coin = "Bear Coin" + +# Collectable Definitions +bonus_coin = "Bonus Coin" +dk_coin = "DK Coin" +banana_bird = "Banana Bird" +krematoa_cog = "Krematoa Cog" + +# Inventory Definitions +progressive_boat = "Progressive Boat Upgrade" +present = "Present" +bowling_ball = "Bowling Ball" +shell = "Shell" +mirror = "Mirror" +flower = "Flupperius Petallus Pongus" +wrench = "No. 6 Wrench" + +# Other Definitions +victory = "Donkey Kong" diff --git a/worlds/dkc3/Names/LocationName.py b/worlds/dkc3/Names/LocationName.py new file mode 100644 index 0000000000..b3aca3b0f1 --- /dev/null +++ b/worlds/dkc3/Names/LocationName.py @@ -0,0 +1,336 @@ +# Level Definitions +lakeside_limbo_flag = "Lakeside Limbo - Flag" +lakeside_limbo_bonus_1 = "Lakeside Limbo - Bonus 1" +lakeside_limbo_bonus_2 = "Lakeside Limbo - Bonus 2" +lakeside_limbo_dk = "Lakeside Limbo - DK Coin" + +doorstop_dash_flag = "Doorstop Dash - Flag" +doorstop_dash_bonus_1 = "Doorstop Dash - Bonus 1" +doorstop_dash_bonus_2 = "Doorstop Dash - Bonus 2" +doorstop_dash_dk = "Doorstop Dash - DK Coin" + +tidal_trouble_flag = "Tidal Trouble - Flag" +tidal_trouble_bonus_1 = "Tidal Trouble - Bonus 1" +tidal_trouble_bonus_2 = "Tidal Trouble - Bonus 2" +tidal_trouble_dk = "Tidal Trouble - DK Coin" + +skiddas_row_flag = "Skidda's Row - Flag" +skiddas_row_bonus_1 = "Skidda's Row - Bonus 1" +skiddas_row_bonus_2 = "Skidda's Row - Bonus 2" +skiddas_row_dk = "Skidda's Row - DK Coin" + +murky_mill_flag = "Murky Mill - Flag" +murky_mill_bonus_1 = "Murky Mill - Bonus 1" +murky_mill_bonus_2 = "Murky Mill - Bonus 2" +murky_mill_dk = "Murky Mill - DK Coin" + +barrel_shield_bust_up_flag = "Barrel Shield Bust-Up - Flag" +barrel_shield_bust_up_bonus_1 = "Barrel Shield Bust-Up - Bonus 1" +barrel_shield_bust_up_bonus_2 = "Barrel Shield Bust-Up - Bonus 2" +barrel_shield_bust_up_dk = "Barrel Shield Bust-Up - DK Coin" + +riverside_race_flag = "Riverside Race - Flag" +riverside_race_bonus_1 = "Riverside Race - Bonus 1" +riverside_race_bonus_2 = "Riverside Race - Bonus 2" +riverside_race_dk = "Riverside Race - DK Coin" + +squeals_on_wheels_flag = "Squeals On Wheels - Flag" +squeals_on_wheels_bonus_1 = "Squeals On Wheels - Bonus 1" +squeals_on_wheels_bonus_2 = "Squeals On Wheels - Bonus 2" +squeals_on_wheels_dk = "Squeals On Wheels - DK Coin" + +springin_spiders_flag = "Springin' Spiders - Flag" +springin_spiders_bonus_1 = "Springin' Spiders - Bonus 1" +springin_spiders_bonus_2 = "Springin' Spiders - Bonus 2" +springin_spiders_dk = "Springin' Spiders - DK Coin" + +bobbing_barrel_brawl_flag = "Bobbing Barrel Brawl - Flag" +bobbing_barrel_brawl_bonus_1 = "Bobbing Barrel Brawl - Bonus 1" +bobbing_barrel_brawl_bonus_2 = "Bobbing Barrel Brawl - Bonus 2" +bobbing_barrel_brawl_dk = "Bobbing Barrel Brawl - DK Coin" + +bazzas_blockade_flag = "Bazza's Blockade - Flag" +bazzas_blockade_bonus_1 = "Bazza's Blockade - Bonus 1" +bazzas_blockade_bonus_2 = "Bazza's Blockade - Bonus 2" +bazzas_blockade_dk = "Bazza's Blockade - DK Coin" + +rocket_barrel_ride_flag = "Rocket Barrel Ride - Flag" +rocket_barrel_ride_bonus_1 = "Rocket Barrel Ride - Bonus 1" +rocket_barrel_ride_bonus_2 = "Rocket Barrel Ride - Bonus 2" +rocket_barrel_ride_dk = "Rocket Barrel Ride - DK Coin" + +kreeping_klasps_flag = "Kreeping Klasps - Flag" +kreeping_klasps_bonus_1 = "Kreeping Klasps - Bonus 1" +kreeping_klasps_bonus_2 = "Kreeping Klasps - Bonus 2" +kreeping_klasps_dk = "Kreeping Klasps - DK Coin" + +tracker_barrel_trek_flag = "Tracker Barrel Trek - Flag" +tracker_barrel_trek_bonus_1 = "Tracker Barrel Trek - Bonus 1" +tracker_barrel_trek_bonus_2 = "Tracker Barrel Trek - Bonus 2" +tracker_barrel_trek_dk = "Tracker Barrel Trek - DK Coin" + +fish_food_frenzy_flag = "Fish Food Frenzy - Flag" +fish_food_frenzy_bonus_1 = "Fish Food Frenzy - Bonus 1" +fish_food_frenzy_bonus_2 = "Fish Food Frenzy - Bonus 2" +fish_food_frenzy_dk = "Fish Food Frenzy - DK Coin" + +fire_ball_frenzy_flag = "Fire-Ball Frenzy - Flag" +fire_ball_frenzy_bonus_1 = "Fire-Ball Frenzy - Bonus 1" +fire_ball_frenzy_bonus_2 = "Fire-Ball Frenzy - Bonus 2" +fire_ball_frenzy_dk = "Fire-Ball Frenzy - DK Coin" + +demolition_drain_pipe_flag = "Demolition Drain-Pipe - Flag" +demolition_drain_pipe_bonus_1 = "Demolition Drain-Pipe - Bonus 1" +demolition_drain_pipe_bonus_2 = "Demolition Drain-Pipe - Bonus 2" +demolition_drain_pipe_dk = "Demolition Drain-Pipe - DK Coin" + +ripsaw_rage_flag = "Ripsaw Rage - Flag" +ripsaw_rage_bonus_1 = "Ripsaw Rage - Bonus 1" +ripsaw_rage_bonus_2 = "Ripsaw Rage - Bonus 2" +ripsaw_rage_dk = "Ripsaw Rage - DK Coin" + +blazing_bazookas_flag = "Blazing Bazookas - Flag" +blazing_bazookas_bonus_1 = "Blazing Bazookas - Bonus 1" +blazing_bazookas_bonus_2 = "Blazing Bazookas - Bonus 2" +blazing_bazookas_dk = "Blazing Bazookas - DK Coin" + +low_g_labyrinth_flag = "Low-G Labyrinth - Flag" +low_g_labyrinth_bonus_1 = "Low-G Labyrinth - Bonus 1" +low_g_labyrinth_bonus_2 = "Low-G Labyrinth - Bonus 2" +low_g_labyrinth_dk = "Low-G Labyrinth - DK Coin" + +krevice_kreepers_flag = "Krevice Kreepers - Flag" +krevice_kreepers_bonus_1 = "Krevice Kreepers - Bonus 1" +krevice_kreepers_bonus_2 = "Krevice Kreepers - Bonus 2" +krevice_kreepers_dk = "Krevice Kreepers - DK Coin" + +tearaway_toboggan_flag = "Tearaway Toboggan - Flag" +tearaway_toboggan_bonus_1 = "Tearaway Toboggan - Bonus 1" +tearaway_toboggan_bonus_2 = "Tearaway Toboggan - Bonus 2" +tearaway_toboggan_dk = "Tearaway Toboggan - DK Coin" + +barrel_drop_bounce_flag = "Barrel Drop Bounce - Flag" +barrel_drop_bounce_bonus_1 = "Barrel Drop Bounce - Bonus 1" +barrel_drop_bounce_bonus_2 = "Barrel Drop Bounce - Bonus 2" +barrel_drop_bounce_dk = "Barrel Drop Bounce - DK Coin" + +krack_shot_kroc_flag = "Krack-Shot Kroc - Flag" +krack_shot_kroc_bonus_1 = "Krack-Shot Kroc - Bonus 1" +krack_shot_kroc_bonus_2 = "Krack-Shot Kroc - Bonus 2" +krack_shot_kroc_dk = "Krack-Shot Kroc - DK Coin" + +lemguin_lunge_flag = "Lemguin Lunge - Flag" +lemguin_lunge_bonus_1 = "Lemguin Lunge - Bonus 1" +lemguin_lunge_bonus_2 = "Lemguin Lunge - Bonus 2" +lemguin_lunge_dk = "Lemguin Lunge - DK Coin" + +buzzer_barrage_flag = "Buzzer Barrage - Flag" +buzzer_barrage_bonus_1 = "Buzzer Barrage - Bonus 1" +buzzer_barrage_bonus_2 = "Buzzer Barrage - Bonus 2" +buzzer_barrage_dk = "Buzzer Barrage - DK Coin" + +kong_fused_cliffs_flag = "Kong-Fused Cliffs - Flag" +kong_fused_cliffs_bonus_1 = "Kong-Fused Cliffs - Bonus 1" +kong_fused_cliffs_bonus_2 = "Kong-Fused Cliffs - Bonus 2" +kong_fused_cliffs_dk = "Kong-Fused Cliffs - DK Coin" + +floodlit_fish_flag = "Floodlit Fish - Flag" +floodlit_fish_bonus_1 = "Floodlit Fish - Bonus 1" +floodlit_fish_bonus_2 = "Floodlit Fish - Bonus 2" +floodlit_fish_dk = "Floodlit Fish - DK Coin" + +pothole_panic_flag = "Pothole Panic - Flag" +pothole_panic_bonus_1 = "Pothole Panic - Bonus 1" +pothole_panic_bonus_2 = "Pothole Panic - Bonus 2" +pothole_panic_dk = "Pothole Panic - DK Coin" + +ropey_rumpus_flag = "Ropey Rumpus - Flag" +ropey_rumpus_bonus_1 = "Ropey Rumpus - Bonus 1" +ropey_rumpus_bonus_2 = "Ropey Rumpus - Bonus 2" +ropey_rumpus_dk = "Ropey Rumpus - DK Coin" + +konveyor_rope_clash_flag = "Konveyor Rope Klash - Flag" +konveyor_rope_clash_bonus_1 = "Konveyor Rope Klash - Bonus 1" +konveyor_rope_clash_bonus_2 = "Konveyor Rope Klash - Bonus 2" +konveyor_rope_clash_dk = "Konveyor Rope Klash - DK Coin" + +creepy_caverns_flag = "Creepy Caverns - Flag" +creepy_caverns_bonus_1 = "Creepy Caverns - Bonus 1" +creepy_caverns_bonus_2 = "Creepy Caverns - Bonus 2" +creepy_caverns_dk = "Creepy Caverns - DK Coin" + +lightning_lookout_flag = "Lightning Lookout - Flag" +lightning_lookout_bonus_1 = "Lightning Lookout - Bonus 1" +lightning_lookout_bonus_2 = "Lightning Lookout - Bonus 2" +lightning_lookout_dk = "Lightning Lookout - DK Coin" + +koindozer_klamber_flag = "Koindozer Klamber - Flag" +koindozer_klamber_bonus_1 = "Koindozer Klamber - Bonus 1" +koindozer_klamber_bonus_2 = "Koindozer Klamber - Bonus 2" +koindozer_klamber_dk = "Koindozer Klamber - DK Coin" + +poisonous_pipeline_flag = "Poisonous Pipeline - Flag" +poisonous_pipeline_bonus_1 = "Poisonous Pipeline - Bonus 1" +poisonous_pipeline_bonus_2 = "Poisonous Pipeline - Bonus 2" +poisonous_pipeline_dk = "Poisonous Pipeline - DK Coin" + +stampede_sprint_flag = "Stampede Sprint - Flag" +stampede_sprint_bonus_1 = "Stampede Sprint - Bonus 1" +stampede_sprint_bonus_2 = "Stampede Sprint - Bonus 2" +stampede_sprint_bonus_3 = "Stampede Sprint - Bonus 3" +stampede_sprint_dk = "Stampede Sprint - DK Coin" + +criss_cross_cliffs_flag = "Criss Kross Cliffs - Flag" +criss_cross_cliffs_bonus_1 = "Criss Kross Cliffs - Bonus 1" +criss_cross_cliffs_bonus_2 = "Criss Kross Cliffs - Bonus 2" +criss_cross_cliffs_dk = "Criss Kross Cliffs - DK Coin" + +tyrant_twin_tussle_flag = "Tyrant Twin Tussle - Flag" +tyrant_twin_tussle_bonus_1 = "Tyrant Twin Tussle - Bonus 1" +tyrant_twin_tussle_bonus_2 = "Tyrant Twin Tussle - Bonus 2" +tyrant_twin_tussle_bonus_3 = "Tyrant Twin Tussle - Bonus 3" +tyrant_twin_tussle_dk = "Tyrant Twin Tussle - DK Coin" + +swoopy_salvo_flag = "Swoopy Salvo - Flag" +swoopy_salvo_bonus_1 = "Swoopy Salvo - Bonus 1" +swoopy_salvo_bonus_2 = "Swoopy Salvo - Bonus 2" +swoopy_salvo_bonus_3 = "Swoopy Salvo - Bonus 3" +swoopy_salvo_dk = "Swoopy Salvo - DK Coin" + +rocket_rush_flag = "Rocket Rush - Flag" +rocket_rush_dk = "Rocket Rush - DK Coin" + +# Boss Definitions +belchas_barn = "Belcha's Barn" +arichs_ambush = "Arich's Ambush" +squirts_showdown = "Squirt's Showdown" +kaos_karnage = "KAOS Karnage" +bleaks_house = "Bleak's House" +barboss_barrier = "Barbos's Barrier" +kastle_kaos = "Kastle KAOS" +knautilus = "Knautilus" + + +# Banana Bird Cave Definitions +belchas_burrow = "Belcha's Burrow" +kong_cave = "Kong Cave" +undercover_cove = "Undercover Cove" +ks_cache = "K's Cache" +hill_top_hoard = "Hill-Top Hoard" +bounty_beach = "Bounty Beach" +smugglers_cove = "Smuggler's Cove" +arichs_hoard = "Arich's Hoard" +bounty_bay = "Bounty Bay" +sky_high_secret = "Sky-High Secret" +glacial_grotto = "Glacial Grotto" +cifftop_cache = "Clifftop Cache" +sewer_stockpile = "Sewer Stockpile" + +banana_bird_mother = "Banana Bird Mother" + + +# Brothers Bear Definitions +bazaars_general_store_1 = "Bazaar's General Store - 1" +bazaars_general_store_2 = "Bazaar's General Store - 2" +brambles_bungalow = "Bramble's Bungalow" +flower_spot = "Flower Spot" +barters_swap_shop = "Barter's Swap Shop" +barnacles_island = "Barnacle's Island" +blues_beach_hut = "Blue's Beach Hut" +blizzards_basecamp = "Bizzard's Basecamp" + + +# Region Definitions +menu_region = "Menu" +overworld_1_region = "Overworld 1" +overworld_2_region = "Overworld 2" +overworld_3_region = "Overworld 3" +overworld_4_region = "Overworld 4" + +bazaar_region = "Bazaar's General Store Region" +bramble_region = "Bramble's Bungalow Region" +flower_spot_region = "Flower Spot Region" +barter_region = "Barter's Swap Shop Region" +barnacle_region = "Barnacle's Island Region" +blue_region = "Blue's Beach Hut Region" +blizzard_region = "Bizzard's Basecamp Region" + +lake_orangatanga_region = "Lake_Orangatanga" +kremwood_forest_region = "Kremwood Forest" +cotton_top_cove_region = "Cotton-Top Cove" +mekanos_region = "Mekanos" +k3_region = "K3" +razor_ridge_region = "Razor Ridge" +kaos_kore_region = "KAOS Kore" +krematoa_region = "Krematoa" + +belchas_barn_region = "Belcha's Barn Region" +arichs_ambush_region = "Arich's Ambush Region" +squirts_showdown_region = "Squirt's Showdown Region" +kaos_karnage_region = "KAOS Karnage Region" +bleaks_house_region = "Bleak's House Region" +barboss_barrier_region = "Barbos's Barrier Region" +kastle_kaos_region = "Kastle KAOS Region" +knautilus_region = "Knautilus Region" + +belchas_burrow_region = "Belcha's Burrow Region" +kong_cave_region = "Kong Cave Region" +undercover_cove_region = "Undercover Cove Region" +ks_cache_region = "K's Cache Region" +hill_top_hoard_region = "Hill-Top Hoard Region" +bounty_beach_region = "Bounty Beach Region" +smugglers_cove_region = "Smuggler's Cove Region" +arichs_hoard_region = "Arich's Hoard Region" +bounty_bay_region = "Bounty Bay Region" +sky_high_secret_region = "Sky-High Secret Region" +glacial_grotto_region = "Glacial Grotto Region" +cifftop_cache_region = "Clifftop Cache Region" +sewer_stockpile_region = "Sewer Stockpile Region" + +lakeside_limbo_region = "Lakeside Limbo" +doorstop_dash_region = "Doorstop Dash" +tidal_trouble_region = "Tidal Trouble" +skiddas_row_region = "Skidda's Row" +murky_mill_region = "Murky Mill" + +barrel_shield_bust_up_region = "Barrel Shield Bust-Up" +riverside_race_region = "Riverside Race" +squeals_on_wheels_region = "Squeals On Wheels" +springin_spiders_region = "Springin' Spiders" +bobbing_barrel_brawl_region = "Bobbing Barrel Brawl" + +bazzas_blockade_region = "Bazza's Blockade" +rocket_barrel_ride_region = "Rocket Barrel Ride" +kreeping_klasps_region = "Kreeping Klasps" +tracker_barrel_trek_region = "Tracker Barrel Trek" +fish_food_frenzy_region = "Fish Food Frenzy" + +fire_ball_frenzy_region = "Fire-Ball Frenzy" +demolition_drain_pipe_region = "Demolition Drain-Pipe" +ripsaw_rage_region = "Ripsaw Rage" +blazing_bazookas_region = "Blazing Bazukas" +low_g_labyrinth_region = "Low-G Labyrinth" + +krevice_kreepers_region = "Krevice Kreepers" +tearaway_toboggan_region = "Tearaway Toboggan" +barrel_drop_bounce_region = "Barrel Drop Bounce" +krack_shot_kroc_region = "Krack-Shot Kroc" +lemguin_lunge_region = "Lemguin Lunge" + +buzzer_barrage_region = "Buzzer Barrage" +kong_fused_cliffs_region = "Kong-Fused Cliffs" +floodlit_fish_region = "Floodlit Fish" +pothole_panic_region = "Pothole Panic" +ropey_rumpus_region = "Ropey Rumpus" + +konveyor_rope_clash_region = "Konveyor Rope Klash" +creepy_caverns_region = "Creepy Caverns" +lightning_lookout_region = "Lightning Lookout" +koindozer_klamber_region = "Koindozer Klamber" +poisonous_pipeline_region = "Poisonous Pipeline" + +stampede_sprint_region = "Stampede Sprint" +criss_cross_cliffs_region = "Criss Kross Cliffs" +tyrant_twin_tussle_region = "Tyrant Twin Tussle" +swoopy_salvo_region = "Swoopy Salvo" +rocket_rush_region = "Rocket Rush" diff --git a/worlds/dkc3/Options.py b/worlds/dkc3/Options.py new file mode 100644 index 0000000000..9e00014933 --- /dev/null +++ b/worlds/dkc3/Options.py @@ -0,0 +1,132 @@ +import typing + +from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList + + +class Goal(Choice): + """ + Determines the goal of the seed + Knautilus: Reach the Knautilus and defeat Baron K. Roolenstein + Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother + """ + display_name = "Goal" + option_knautilus = 0 + option_banana_bird_hunt = 1 + default = 0 + + +class IncludeTradeSequence(Toggle): + """ + Allows logic to place items at the various steps of the trade sequence + """ + display_name = "Include Trade Sequence" + + +class DKCoinsForGyrocopter(Range): + """ + How many DK Coins are needed to unlock the Gyrocopter + Note: Achieving this number before unlocking the Turbo Ski will cause the game to grant you a + one-time upgrade to the next non-unlocked boat, until you return to Funky. Logic does not assume + that you will use this. + """ + display_name = "DK Coins for Gyrocopter" + range_start = 10 + range_end = 41 + default = 30 + + +class KrematoaBonusCoinCost(Range): + """ + How many Bonus Coins are needed to unlock each level in Krematoa + """ + display_name = "Krematoa Bonus Coins Cost" + range_start = 1 + range_end = 17 + default = 15 + + +class PercentageOfExtraBonusCoins(Range): + """ + What Percentage of unneeded Bonus Coins are included in the item pool + """ + display_name = "Percentage of Extra Bonus Coins" + range_start = 0 + range_end = 100 + default = 100 + + +class NumberOfBananaBirds(Range): + """ + How many Banana Birds are put into the item pool + """ + display_name = "Number of Banana Birds" + range_start = 5 + range_end = 15 + default = 15 + + +class PercentageOfBananaBirds(Range): + """ + What Percentage of Banana Birds in the item pool are required for Banana Bird Hunt + """ + display_name = "Percentage of Banana Birds" + range_start = 20 + range_end = 100 + default = 100 + + +class LevelShuffle(Toggle): + """ + Whether levels are shuffled + """ + display_name = "Level Shuffle" + + +class MusicShuffle(Toggle): + """ + Whether music is shuffled + """ + display_name = "Music Shuffle" + + +class KongPaletteSwap(Choice): + """ + Which Palette to use for the Kongs + """ + display_name = "Kong Palette Swap" + option_default = 0 + option_purple = 1 + option_spooky = 2 + option_dark = 3 + option_chocolate = 4 + option_shadow = 5 + option_red_gold = 6 + option_gbc = 7 + option_halloween = 8 + default = 0 + + +class StartingLifeCount(Range): + """ + How many extra lives to start the game with + """ + display_name = "Starting Life Count" + range_start = 1 + range_end = 99 + default = 5 + + +dkc3_options: typing.Dict[str, type(Option)] = { + #"death_link": DeathLink, # Disabled + "goal": Goal, + #"include_trade_sequence": IncludeTradeSequence, # Disabled + "dk_coins_for_gyrocopter": DKCoinsForGyrocopter, + "krematoa_bonus_coin_cost": KrematoaBonusCoinCost, + "percentage_of_extra_bonus_coins": PercentageOfExtraBonusCoins, + "number_of_banana_birds": NumberOfBananaBirds, + "percentage_of_banana_birds": PercentageOfBananaBirds, + "level_shuffle": LevelShuffle, + "music_shuffle": MusicShuffle, + "kong_palette_swap": KongPaletteSwap, + "starting_life_count": StartingLifeCount, +} diff --git a/worlds/dkc3/Regions.py b/worlds/dkc3/Regions.py new file mode 100644 index 0000000000..6cb01e4f18 --- /dev/null +++ b/worlds/dkc3/Regions.py @@ -0,0 +1,879 @@ +import typing + +from BaseClasses import MultiWorld, Region, Entrance +from .Items import DKC3Item +from .Locations import DKC3Location +from .Names import LocationName, ItemName + + +def create_regions(world, player: int, active_locations): + menu_region = create_region(world, player, active_locations, 'Menu', None, None) + + overworld_1_region_locations = {} + if world.goal[player] != "knautilus": + overworld_1_region_locations.update({LocationName.banana_bird_mother: []}) + overworld_1_region = create_region(world, player, active_locations, LocationName.overworld_1_region, + overworld_1_region_locations, None) + + overworld_2_region_locations = {} + overworld_2_region = create_region(world, player, active_locations, LocationName.overworld_2_region, + overworld_2_region_locations, None) + + overworld_3_region_locations = {} + overworld_3_region = create_region(world, player, active_locations, LocationName.overworld_3_region, + overworld_3_region_locations, None) + + overworld_4_region_locations = {} + overworld_4_region = create_region(world, player, active_locations, LocationName.overworld_4_region, + overworld_4_region_locations, None) + + + lake_orangatanga_region = create_region(world, player, active_locations, LocationName.lake_orangatanga_region, None, None) + kremwood_forest_region = create_region(world, player, active_locations, LocationName.kremwood_forest_region, None, None) + cotton_top_cove_region = create_region(world, player, active_locations, LocationName.cotton_top_cove_region, None, None) + mekanos_region = create_region(world, player, active_locations, LocationName.mekanos_region, None, None) + k3_region = create_region(world, player, active_locations, LocationName.k3_region, None, None) + razor_ridge_region = create_region(world, player, active_locations, LocationName.razor_ridge_region, None, None) + kaos_kore_region = create_region(world, player, active_locations, LocationName.kaos_kore_region, None, None) + krematoa_region = create_region(world, player, active_locations, LocationName.krematoa_region, None, None) + + + lakeside_limbo_region_locations = { + LocationName.lakeside_limbo_flag : [0x657, 1], + LocationName.lakeside_limbo_bonus_1 : [0x657, 2], + LocationName.lakeside_limbo_bonus_2 : [0x657, 3], + LocationName.lakeside_limbo_dk : [0x657, 5], + } + lakeside_limbo_region = create_region(world, player, active_locations, LocationName.lakeside_limbo_region, + lakeside_limbo_region_locations, None) + + doorstop_dash_region_locations = { + LocationName.doorstop_dash_flag : [0x65A, 1], + LocationName.doorstop_dash_bonus_1 : [0x65A, 2], + LocationName.doorstop_dash_bonus_2 : [0x65A, 3], + LocationName.doorstop_dash_dk : [0x65A, 5], + } + doorstop_dash_region = create_region(world, player, active_locations, LocationName.doorstop_dash_region, + doorstop_dash_region_locations, None) + + tidal_trouble_region_locations = { + LocationName.tidal_trouble_flag : [0x659, 1], + LocationName.tidal_trouble_bonus_1 : [0x659, 2], + LocationName.tidal_trouble_bonus_2 : [0x659, 3], + LocationName.tidal_trouble_dk : [0x659, 5], + } + tidal_trouble_region = create_region(world, player, active_locations, LocationName.tidal_trouble_region, + tidal_trouble_region_locations, None) + + skiddas_row_region_locations = { + LocationName.skiddas_row_flag : [0x65D, 1], + LocationName.skiddas_row_bonus_1 : [0x65D, 2], + LocationName.skiddas_row_bonus_2 : [0x65D, 3], + LocationName.skiddas_row_dk : [0x65D, 5], + } + skiddas_row_region = create_region(world, player, active_locations, LocationName.skiddas_row_region, + skiddas_row_region_locations, None) + + murky_mill_region_locations = { + LocationName.murky_mill_flag : [0x65C, 1], + LocationName.murky_mill_bonus_1 : [0x65C, 2], + LocationName.murky_mill_bonus_2 : [0x65C, 3], + LocationName.murky_mill_dk : [0x65C, 5], + } + murky_mill_region = create_region(world, player, active_locations, LocationName.murky_mill_region, + murky_mill_region_locations, None) + + barrel_shield_bust_up_region_locations = { + LocationName.barrel_shield_bust_up_flag : [0x662, 1], + LocationName.barrel_shield_bust_up_bonus_1 : [0x662, 2], + LocationName.barrel_shield_bust_up_bonus_2 : [0x662, 3], + LocationName.barrel_shield_bust_up_dk : [0x662, 5], + } + barrel_shield_bust_up_region = create_region(world, player, active_locations, LocationName.barrel_shield_bust_up_region, + barrel_shield_bust_up_region_locations, None) + + riverside_race_region_locations = { + LocationName.riverside_race_flag : [0x664, 1], + LocationName.riverside_race_bonus_1 : [0x664, 2], + LocationName.riverside_race_bonus_2 : [0x664, 3], + LocationName.riverside_race_dk : [0x664, 5], + } + riverside_race_region = create_region(world, player, active_locations, LocationName.riverside_race_region, + riverside_race_region_locations, None) + + squeals_on_wheels_region_locations = { + LocationName.squeals_on_wheels_flag : [0x65B, 1], + LocationName.squeals_on_wheels_bonus_1 : [0x65B, 2], + LocationName.squeals_on_wheels_bonus_2 : [0x65B, 3], + LocationName.squeals_on_wheels_dk : [0x65B, 5], + } + squeals_on_wheels_region = create_region(world, player, active_locations, LocationName.squeals_on_wheels_region, + squeals_on_wheels_region_locations, None) + + springin_spiders_region_locations = { + LocationName.springin_spiders_flag : [0x661, 1], + LocationName.springin_spiders_bonus_1 : [0x661, 2], + LocationName.springin_spiders_bonus_2 : [0x661, 3], + LocationName.springin_spiders_dk : [0x661, 5], + } + springin_spiders_region = create_region(world, player, active_locations, LocationName.springin_spiders_region, + springin_spiders_region_locations, None) + + bobbing_barrel_brawl_region_locations = { + LocationName.bobbing_barrel_brawl_flag : [0x666, 1], + LocationName.bobbing_barrel_brawl_bonus_1 : [0x666, 2], + LocationName.bobbing_barrel_brawl_bonus_2 : [0x666, 3], + LocationName.bobbing_barrel_brawl_dk : [0x666, 5], + } + bobbing_barrel_brawl_region = create_region(world, player, active_locations, LocationName.bobbing_barrel_brawl_region, + bobbing_barrel_brawl_region_locations, None) + + bazzas_blockade_region_locations = { + LocationName.bazzas_blockade_flag : [0x667, 1], + LocationName.bazzas_blockade_bonus_1 : [0x667, 2], + LocationName.bazzas_blockade_bonus_2 : [0x667, 3], + LocationName.bazzas_blockade_dk : [0x667, 5], + } + bazzas_blockade_region = create_region(world, player, active_locations, LocationName.bazzas_blockade_region, + bazzas_blockade_region_locations, None) + + rocket_barrel_ride_region_locations = { + LocationName.rocket_barrel_ride_flag : [0x66A, 1], + LocationName.rocket_barrel_ride_bonus_1 : [0x66A, 2], + LocationName.rocket_barrel_ride_bonus_2 : [0x66A, 3], + LocationName.rocket_barrel_ride_dk : [0x66A, 5], + } + rocket_barrel_ride_region = create_region(world, player, active_locations, LocationName.rocket_barrel_ride_region, + rocket_barrel_ride_region_locations, None) + + kreeping_klasps_region_locations = { + LocationName.kreeping_klasps_flag : [0x658, 1], + LocationName.kreeping_klasps_bonus_1 : [0x658, 2], + LocationName.kreeping_klasps_bonus_2 : [0x658, 3], + LocationName.kreeping_klasps_dk : [0x658, 5], + } + kreeping_klasps_region = create_region(world, player, active_locations, LocationName.kreeping_klasps_region, + kreeping_klasps_region_locations, None) + + tracker_barrel_trek_region_locations = { + LocationName.tracker_barrel_trek_flag : [0x66B, 1], + LocationName.tracker_barrel_trek_bonus_1 : [0x66B, 2], + LocationName.tracker_barrel_trek_bonus_2 : [0x66B, 3], + LocationName.tracker_barrel_trek_dk : [0x66B, 5], + } + tracker_barrel_trek_region = create_region(world, player, active_locations, LocationName.tracker_barrel_trek_region, + tracker_barrel_trek_region_locations, None) + + fish_food_frenzy_region_locations = { + LocationName.fish_food_frenzy_flag : [0x668, 1], + LocationName.fish_food_frenzy_bonus_1 : [0x668, 2], + LocationName.fish_food_frenzy_bonus_2 : [0x668, 3], + LocationName.fish_food_frenzy_dk : [0x668, 5], + } + fish_food_frenzy_region = create_region(world, player, active_locations, LocationName.fish_food_frenzy_region, + fish_food_frenzy_region_locations, None) + + fire_ball_frenzy_region_locations = { + LocationName.fire_ball_frenzy_flag : [0x66D, 1], + LocationName.fire_ball_frenzy_bonus_1 : [0x66D, 2], + LocationName.fire_ball_frenzy_bonus_2 : [0x66D, 3], + LocationName.fire_ball_frenzy_dk : [0x66D, 5], + } + fire_ball_frenzy_region = create_region(world, player, active_locations, LocationName.fire_ball_frenzy_region, + fire_ball_frenzy_region_locations, None) + + demolition_drain_pipe_region_locations = { + LocationName.demolition_drain_pipe_flag : [0x672, 1], + LocationName.demolition_drain_pipe_bonus_1 : [0x672, 2], + LocationName.demolition_drain_pipe_bonus_2 : [0x672, 3], + LocationName.demolition_drain_pipe_dk : [0x672, 5], + } + demolition_drain_pipe_region = create_region(world, player, active_locations, LocationName.demolition_drain_pipe_region, + demolition_drain_pipe_region_locations, None) + + ripsaw_rage_region_locations = { + LocationName.ripsaw_rage_flag : [0x660, 1], + LocationName.ripsaw_rage_bonus_1 : [0x660, 2], + LocationName.ripsaw_rage_bonus_2 : [0x660, 3], + LocationName.ripsaw_rage_dk : [0x660, 5], + } + ripsaw_rage_region = create_region(world, player, active_locations, LocationName.ripsaw_rage_region, + ripsaw_rage_region_locations, None) + + blazing_bazookas_region_locations = { + LocationName.blazing_bazookas_flag : [0x66E, 1], + LocationName.blazing_bazookas_bonus_1 : [0x66E, 2], + LocationName.blazing_bazookas_bonus_2 : [0x66E, 3], + LocationName.blazing_bazookas_dk : [0x66E, 5], + } + blazing_bazookas_region = create_region(world, player, active_locations, LocationName.blazing_bazookas_region, + blazing_bazookas_region_locations, None) + + low_g_labyrinth_region_locations = { + LocationName.low_g_labyrinth_flag : [0x670, 1], + LocationName.low_g_labyrinth_bonus_1 : [0x670, 2], + LocationName.low_g_labyrinth_bonus_2 : [0x670, 3], + LocationName.low_g_labyrinth_dk : [0x670, 5], + } + low_g_labyrinth_region = create_region(world, player, active_locations, LocationName.low_g_labyrinth_region, + low_g_labyrinth_region_locations, None) + + krevice_kreepers_region_locations = { + LocationName.krevice_kreepers_flag : [0x673, 1], + LocationName.krevice_kreepers_bonus_1 : [0x673, 2], + LocationName.krevice_kreepers_bonus_2 : [0x673, 3], + LocationName.krevice_kreepers_dk : [0x673, 5], + } + krevice_kreepers_region = create_region(world, player, active_locations, LocationName.krevice_kreepers_region, + krevice_kreepers_region_locations, None) + + tearaway_toboggan_region_locations = { + LocationName.tearaway_toboggan_flag : [0x65F, 1], + LocationName.tearaway_toboggan_bonus_1 : [0x65F, 2], + LocationName.tearaway_toboggan_bonus_2 : [0x65F, 3], + LocationName.tearaway_toboggan_dk : [0x65F, 5], + } + tearaway_toboggan_region = create_region(world, player, active_locations, LocationName.tearaway_toboggan_region, + tearaway_toboggan_region_locations, None) + + barrel_drop_bounce_region_locations = { + LocationName.barrel_drop_bounce_flag : [0x66C, 1], + LocationName.barrel_drop_bounce_bonus_1 : [0x66C, 2], + LocationName.barrel_drop_bounce_bonus_2 : [0x66C, 3], + LocationName.barrel_drop_bounce_dk : [0x66C, 5], + } + barrel_drop_bounce_region = create_region(world, player, active_locations, LocationName.barrel_drop_bounce_region, + barrel_drop_bounce_region_locations, None) + + krack_shot_kroc_region_locations = { + LocationName.krack_shot_kroc_flag : [0x66F, 1], + LocationName.krack_shot_kroc_bonus_1 : [0x66F, 2], + LocationName.krack_shot_kroc_bonus_2 : [0x66F, 3], + LocationName.krack_shot_kroc_dk : [0x66F, 5], + } + krack_shot_kroc_region = create_region(world, player, active_locations, LocationName.krack_shot_kroc_region, + krack_shot_kroc_region_locations, None) + + lemguin_lunge_region_locations = { + LocationName.lemguin_lunge_flag : [0x65E, 1], + LocationName.lemguin_lunge_bonus_1 : [0x65E, 2], + LocationName.lemguin_lunge_bonus_2 : [0x65E, 3], + LocationName.lemguin_lunge_dk : [0x65E, 5], + } + lemguin_lunge_region = create_region(world, player, active_locations, LocationName.lemguin_lunge_region, + lemguin_lunge_region_locations, None) + + buzzer_barrage_region_locations = { + LocationName.buzzer_barrage_flag : [0x676, 1], + LocationName.buzzer_barrage_bonus_1 : [0x676, 2], + LocationName.buzzer_barrage_bonus_2 : [0x676, 3], + LocationName.buzzer_barrage_dk : [0x676, 5], + } + buzzer_barrage_region = create_region(world, player, active_locations, LocationName.buzzer_barrage_region, + buzzer_barrage_region_locations, None) + + kong_fused_cliffs_region_locations = { + LocationName.kong_fused_cliffs_flag : [0x674, 1], + LocationName.kong_fused_cliffs_bonus_1 : [0x674, 2], + LocationName.kong_fused_cliffs_bonus_2 : [0x674, 3], + LocationName.kong_fused_cliffs_dk : [0x674, 5], + } + kong_fused_cliffs_region = create_region(world, player, active_locations, LocationName.kong_fused_cliffs_region, + kong_fused_cliffs_region_locations, None) + + floodlit_fish_region_locations = { + LocationName.floodlit_fish_flag : [0x669, 1], + LocationName.floodlit_fish_bonus_1 : [0x669, 2], + LocationName.floodlit_fish_bonus_2 : [0x669, 3], + LocationName.floodlit_fish_dk : [0x669, 5], + } + floodlit_fish_region = create_region(world, player, active_locations, LocationName.floodlit_fish_region, + floodlit_fish_region_locations, None) + + pothole_panic_region_locations = { + LocationName.pothole_panic_flag : [0x677, 1], + LocationName.pothole_panic_bonus_1 : [0x677, 2], + LocationName.pothole_panic_bonus_2 : [0x677, 3], + LocationName.pothole_panic_dk : [0x677, 5], + } + pothole_panic_region = create_region(world, player, active_locations, LocationName.pothole_panic_region, + pothole_panic_region_locations, None) + + ropey_rumpus_region_locations = { + LocationName.ropey_rumpus_flag : [0x675, 1], + LocationName.ropey_rumpus_bonus_1 : [0x675, 2], + LocationName.ropey_rumpus_bonus_2 : [0x675, 3], + LocationName.ropey_rumpus_dk : [0x675, 5], + } + ropey_rumpus_region = create_region(world, player, active_locations, LocationName.ropey_rumpus_region, + ropey_rumpus_region_locations, None) + + konveyor_rope_clash_region_locations = { + LocationName.konveyor_rope_clash_flag : [0x657, 1], + LocationName.konveyor_rope_clash_bonus_1 : [0x657, 2], + LocationName.konveyor_rope_clash_bonus_2 : [0x657, 3], + LocationName.konveyor_rope_clash_dk : [0x657, 5], + } + konveyor_rope_clash_region = create_region(world, player, active_locations, LocationName.konveyor_rope_clash_region, + konveyor_rope_clash_region_locations, None) + + creepy_caverns_region_locations = { + LocationName.creepy_caverns_flag : [0x678, 1], + LocationName.creepy_caverns_bonus_1 : [0x678, 2], + LocationName.creepy_caverns_bonus_2 : [0x678, 3], + LocationName.creepy_caverns_dk : [0x678, 5], + } + creepy_caverns_region = create_region(world, player, active_locations, LocationName.creepy_caverns_region, + creepy_caverns_region_locations, None) + + lightning_lookout_region_locations = { + LocationName.lightning_lookout_flag : [0x665, 1], + LocationName.lightning_lookout_bonus_1 : [0x665, 2], + LocationName.lightning_lookout_bonus_2 : [0x665, 3], + LocationName.lightning_lookout_dk : [0x665, 5], + } + lightning_lookout_region = create_region(world, player, active_locations, LocationName.lightning_lookout_region, + lightning_lookout_region_locations, None) + + koindozer_klamber_region_locations = { + LocationName.koindozer_klamber_flag : [0x679, 1], + LocationName.koindozer_klamber_bonus_1 : [0x679, 2], + LocationName.koindozer_klamber_bonus_2 : [0x679, 3], + LocationName.koindozer_klamber_dk : [0x679, 5], + } + koindozer_klamber_region = create_region(world, player, active_locations, LocationName.koindozer_klamber_region, + koindozer_klamber_region_locations, None) + + poisonous_pipeline_region_locations = { + LocationName.poisonous_pipeline_flag : [0x671, 1], + LocationName.poisonous_pipeline_bonus_1 : [0x671, 2], + LocationName.poisonous_pipeline_bonus_2 : [0x671, 3], + LocationName.poisonous_pipeline_dk : [0x671, 5], + } + poisonous_pipeline_region = create_region(world, player, active_locations, LocationName.poisonous_pipeline_region, + poisonous_pipeline_region_locations, None) + + stampede_sprint_region_locations = { + LocationName.stampede_sprint_flag : [0x67B, 1], + LocationName.stampede_sprint_bonus_1 : [0x67B, 2], + LocationName.stampede_sprint_bonus_2 : [0x67B, 3], + LocationName.stampede_sprint_bonus_3 : [0x67B, 4], + LocationName.stampede_sprint_dk : [0x67B, 5], + } + stampede_sprint_region = create_region(world, player, active_locations, LocationName.stampede_sprint_region, + stampede_sprint_region_locations, None) + + criss_cross_cliffs_region_locations = { + LocationName.criss_cross_cliffs_flag : [0x67C, 1], + LocationName.criss_cross_cliffs_bonus_1 : [0x67C, 2], + LocationName.criss_cross_cliffs_bonus_2 : [0x67C, 3], + LocationName.criss_cross_cliffs_dk : [0x67C, 5], + } + criss_cross_cliffs_region = create_region(world, player, active_locations, LocationName.criss_cross_cliffs_region, + criss_cross_cliffs_region_locations, None) + + tyrant_twin_tussle_region_locations = { + LocationName.tyrant_twin_tussle_flag : [0x67D, 1], + LocationName.tyrant_twin_tussle_bonus_1 : [0x67D, 2], + LocationName.tyrant_twin_tussle_bonus_2 : [0x67D, 3], + LocationName.tyrant_twin_tussle_bonus_3 : [0x67D, 4], + LocationName.tyrant_twin_tussle_dk : [0x67D, 5], + } + tyrant_twin_tussle_region = create_region(world, player, active_locations, LocationName.tyrant_twin_tussle_region, + tyrant_twin_tussle_region_locations, None) + + swoopy_salvo_region_locations = { + LocationName.swoopy_salvo_flag : [0x663, 1], + LocationName.swoopy_salvo_bonus_1 : [0x663, 2], + LocationName.swoopy_salvo_bonus_2 : [0x663, 3], + LocationName.swoopy_salvo_bonus_3 : [0x663, 4], + LocationName.swoopy_salvo_dk : [0x663, 5], + } + swoopy_salvo_region = create_region(world, player, active_locations, LocationName.swoopy_salvo_region, + swoopy_salvo_region_locations, None) + + rocket_rush_region_locations = { + LocationName.rocket_rush_flag : [0x67E, 1], + LocationName.rocket_rush_dk : [0x67E, 5], + } + rocket_rush_region = create_region(world, player, active_locations, LocationName.rocket_rush_region, + rocket_rush_region_locations, None) + + belchas_barn_region_locations = { + LocationName.belchas_barn: [0x64F, 1], + } + belchas_barn_region = create_region(world, player, active_locations, LocationName.belchas_barn_region, + belchas_barn_region_locations, None) + + arichs_ambush_region_locations = { + LocationName.arichs_ambush: [0x650, 1], + } + arichs_ambush_region = create_region(world, player, active_locations, LocationName.arichs_ambush_region, + arichs_ambush_region_locations, None) + + squirts_showdown_region_locations = { + LocationName.squirts_showdown: [0x651, 1], + } + squirts_showdown_region = create_region(world, player, active_locations, LocationName.squirts_showdown_region, + squirts_showdown_region_locations, None) + + kaos_karnage_region_locations = { + LocationName.kaos_karnage: [0x652, 1], + } + kaos_karnage_region = create_region(world, player, active_locations, LocationName.kaos_karnage_region, + kaos_karnage_region_locations, None) + + bleaks_house_region_locations = { + LocationName.bleaks_house: [0x653, 1], + } + bleaks_house_region = create_region(world, player, active_locations, LocationName.bleaks_house_region, + bleaks_house_region_locations, None) + + barboss_barrier_region_locations = { + LocationName.barboss_barrier: [0x654, 1], + } + barboss_barrier_region = create_region(world, player, active_locations, LocationName.barboss_barrier_region, + barboss_barrier_region_locations, None) + + kastle_kaos_region_locations = { + LocationName.kastle_kaos: [0x655, 1], + } + kastle_kaos_region = create_region(world, player, active_locations, LocationName.kastle_kaos_region, + kastle_kaos_region_locations, None) + + knautilus_region_locations = { + LocationName.knautilus: [0x656, 1], + } + knautilus_region = create_region(world, player, active_locations, LocationName.knautilus_region, + knautilus_region_locations, None) + + belchas_burrow_region_locations = { + LocationName.belchas_burrow: [0x647, 1], + } + belchas_burrow_region = create_region(world, player, active_locations, LocationName.belchas_burrow_region, + belchas_burrow_region_locations, None) + + kong_cave_region_locations = { + LocationName.kong_cave: [0x645, 1], + } + kong_cave_region = create_region(world, player, active_locations, LocationName.kong_cave_region, + kong_cave_region_locations, None) + + undercover_cove_region_locations = { + LocationName.undercover_cove: [0x644, 1], + } + undercover_cove_region = create_region(world, player, active_locations, LocationName.undercover_cove_region, + undercover_cove_region_locations, None) + + ks_cache_region_locations = { + LocationName.ks_cache: [0x642, 1], + } + ks_cache_region = create_region(world, player, active_locations, LocationName.ks_cache_region, + ks_cache_region_locations, None) + + hill_top_hoard_region_locations = { + LocationName.hill_top_hoard: [0x643, 1], + } + hill_top_hoard_region = create_region(world, player, active_locations, LocationName.hill_top_hoard_region, + hill_top_hoard_region_locations, None) + + bounty_beach_region_locations = { + LocationName.bounty_beach: [0x646, 1], + } + bounty_beach_region = create_region(world, player, active_locations, LocationName.bounty_beach_region, + bounty_beach_region_locations, None) + + smugglers_cove_region_locations = { + LocationName.smugglers_cove: [0x648, 1], + } + smugglers_cove_region = create_region(world, player, active_locations, LocationName.smugglers_cove_region, + smugglers_cove_region_locations, None) + + arichs_hoard_region_locations = { + LocationName.arichs_hoard: [0x649, 1], + } + arichs_hoard_region = create_region(world, player, active_locations, LocationName.arichs_hoard_region, + arichs_hoard_region_locations, None) + + bounty_bay_region_locations = { + LocationName.bounty_bay: [0x64A, 1], + } + bounty_bay_region = create_region(world, player, active_locations, LocationName.bounty_bay_region, + bounty_bay_region_locations, None) + + sky_high_secret_region_locations = { + LocationName.sky_high_secret: [0x64B, 1], + } + sky_high_secret_region = create_region(world, player, active_locations, LocationName.sky_high_secret_region, + sky_high_secret_region_locations, None) + + glacial_grotto_region_locations = { + LocationName.glacial_grotto: [0x64C, 1], + } + glacial_grotto_region = create_region(world, player, active_locations, LocationName.glacial_grotto_region, + glacial_grotto_region_locations, None) + + cifftop_cache_region_locations = { + LocationName.cifftop_cache: [0x64D, 1], + } + cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region, + cifftop_cache_region_locations, None) + + sewer_stockpile_region_locations = { + LocationName.sewer_stockpile: [0x64E, 1], + } + sewer_stockpile_region = create_region(world, player, active_locations, LocationName.sewer_stockpile_region, + sewer_stockpile_region_locations, None) + + + # Set up the regions correctly. + world.regions += [ + menu_region, + overworld_1_region, + overworld_2_region, + overworld_3_region, + overworld_4_region, + lake_orangatanga_region, + kremwood_forest_region, + cotton_top_cove_region, + mekanos_region, + k3_region, + razor_ridge_region, + kaos_kore_region, + krematoa_region, + lakeside_limbo_region, + doorstop_dash_region, + tidal_trouble_region, + skiddas_row_region, + murky_mill_region, + barrel_shield_bust_up_region, + riverside_race_region, + squeals_on_wheels_region, + springin_spiders_region, + bobbing_barrel_brawl_region, + bazzas_blockade_region, + rocket_barrel_ride_region, + kreeping_klasps_region, + tracker_barrel_trek_region, + fish_food_frenzy_region, + fire_ball_frenzy_region, + demolition_drain_pipe_region, + ripsaw_rage_region, + blazing_bazookas_region, + low_g_labyrinth_region, + krevice_kreepers_region, + tearaway_toboggan_region, + barrel_drop_bounce_region, + krack_shot_kroc_region, + lemguin_lunge_region, + buzzer_barrage_region, + kong_fused_cliffs_region, + floodlit_fish_region, + pothole_panic_region, + ropey_rumpus_region, + konveyor_rope_clash_region, + creepy_caverns_region, + lightning_lookout_region, + koindozer_klamber_region, + poisonous_pipeline_region, + stampede_sprint_region, + criss_cross_cliffs_region, + tyrant_twin_tussle_region, + swoopy_salvo_region, + rocket_rush_region, + belchas_barn_region, + arichs_ambush_region, + squirts_showdown_region, + kaos_karnage_region, + bleaks_house_region, + barboss_barrier_region, + kastle_kaos_region, + knautilus_region, + belchas_burrow_region, + kong_cave_region, + undercover_cove_region, + ks_cache_region, + hill_top_hoard_region, + bounty_beach_region, + smugglers_cove_region, + arichs_hoard_region, + bounty_bay_region, + sky_high_secret_region, + glacial_grotto_region, + cifftop_cache_region, + sewer_stockpile_region, + ] + + bazaar_region_locations = {} + bramble_region_locations = {} + flower_spot_region_locations = {} + barter_region_locations = {} + barnacle_region_locations = {} + blue_region_locations = {} + blizzard_region_locations = {} + + if False:#world.include_trade_sequence[player]: + bazaar_region_locations.update({ + LocationName.bazaars_general_store_1: [0x615, 2, True], + LocationName.bazaars_general_store_2: [0x615, 3, True], + }) + + bramble_region_locations.update({ + LocationName.brambles_bungalow: [0x619, 2], + }) + + #flower_spot_region_locations.update({ + # LocationName.flower_spot: [0x615, 3, True], + #}) + + barter_region_locations.update({ + LocationName.barters_swap_shop: [0x61B, 3], + }) + + barnacle_region_locations.update({ + LocationName.barnacles_island: [0x61D, 2], + }) + + blue_region_locations.update({ + LocationName.blues_beach_hut: [0x621, 4], + }) + + blizzard_region_locations.update({ + LocationName.blizzards_basecamp: [0x625, 4, True], + }) + + bazaar_region = create_region(world, player, active_locations, LocationName.bazaar_region, + bazaar_region_locations, None) + bramble_region = create_region(world, player, active_locations, LocationName.bramble_region, + bramble_region_locations, None) + flower_spot_region = create_region(world, player, active_locations, LocationName.flower_spot_region, + flower_spot_region_locations, None) + barter_region = create_region(world, player, active_locations, LocationName.barter_region, + barter_region_locations, None) + barnacle_region = create_region(world, player, active_locations, LocationName.barnacle_region, + barnacle_region_locations, None) + blue_region = create_region(world, player, active_locations, LocationName.blue_region, + blue_region_locations, None) + blizzard_region = create_region(world, player, active_locations, LocationName.blizzard_region, + blizzard_region_locations, None) + + world.regions += [ + bazaar_region, + bramble_region, + flower_spot_region, + barter_region, + barnacle_region, + blue_region, + blizzard_region, + ] + + +def connect_regions(world, player, level_list): + names: typing.Dict[str, int] = {} + + # Overworld + connect(world, player, names, 'Menu', LocationName.overworld_1_region) + connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_2_region, + lambda state: (state.has(ItemName.progressive_boat, player, 1))) + connect(world, player, names, LocationName.overworld_2_region, LocationName.overworld_3_region, + lambda state: (state.has(ItemName.progressive_boat, player, 3))) + connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_4_region, + lambda state: (state.has(ItemName.dk_coin, player, world.dk_coins_for_gyrocopter[player].value) and + state.has(ItemName.progressive_boat, player, 3))) + + # World Connections + connect(world, player, names, LocationName.overworld_1_region, LocationName.lake_orangatanga_region) + connect(world, player, names, LocationName.overworld_1_region, LocationName.kremwood_forest_region) + connect(world, player, names, LocationName.overworld_1_region, LocationName.bounty_beach_region) + connect(world, player, names, LocationName.overworld_1_region, LocationName.bazaar_region) + + connect(world, player, names, LocationName.overworld_2_region, LocationName.cotton_top_cove_region) + connect(world, player, names, LocationName.overworld_2_region, LocationName.mekanos_region) + connect(world, player, names, LocationName.overworld_2_region, LocationName.kong_cave_region) + connect(world, player, names, LocationName.overworld_2_region, LocationName.bramble_region) + + connect(world, player, names, LocationName.overworld_3_region, LocationName.k3_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.razor_ridge_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.kaos_kore_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.krematoa_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.undercover_cove_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.flower_spot_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.barter_region) + + connect(world, player, names, LocationName.overworld_4_region, LocationName.belchas_burrow_region) + connect(world, player, names, LocationName.overworld_4_region, LocationName.ks_cache_region) + connect(world, player, names, LocationName.overworld_4_region, LocationName.hill_top_hoard_region) + + + # Lake Orangatanga Connections + lake_orangatanga_levels = [ + level_list[0], + level_list[1], + level_list[2], + level_list[3], + level_list[4], + LocationName.belchas_barn_region, + LocationName.barnacle_region, + LocationName.smugglers_cove_region, + ] + + for i in range(0, len(lake_orangatanga_levels)): + connect(world, player, names, LocationName.lake_orangatanga_region, lake_orangatanga_levels[i]) + + # Kremwood Forest Connections + kremwood_forest_levels = [ + level_list[5], + level_list[6], + level_list[7], + level_list[8], + level_list[9], + LocationName.arichs_ambush_region, + LocationName.arichs_hoard_region, + ] + + for i in range(0, len(kremwood_forest_levels) - 1): + connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i]) + + connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1], + lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", player))) + + # Cotton-Top Cove Connections + cotton_top_cove_levels = [ + LocationName.blue_region, + level_list[10], + level_list[11], + level_list[12], + level_list[13], + level_list[14], + LocationName.squirts_showdown_region, + LocationName.bounty_bay_region, + ] + + for i in range(0, len(cotton_top_cove_levels)): + connect(world, player, names, LocationName.cotton_top_cove_region, cotton_top_cove_levels[i]) + + # Mekanos Connections + mekanos_levels = [ + level_list[15], + level_list[16], + level_list[17], + level_list[18], + level_list[19], + LocationName.kaos_karnage_region, + ] + + for i in range(0, len(mekanos_levels)): + connect(world, player, names, LocationName.mekanos_region, mekanos_levels[i]) + + if False:#world.include_trade_sequence[player]: + connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region, + lambda state: (state.has(ItemName.bowling_ball, player, 1))) + else: + connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region, + lambda state: (state.can_reach(LocationName.bleaks_house, "Location", player))) + + # K3 Connections + k3_levels = [ + level_list[20], + level_list[21], + level_list[22], + level_list[23], + level_list[24], + LocationName.bleaks_house_region, + LocationName.blizzard_region, + LocationName.glacial_grotto_region, + ] + + for i in range(0, len(k3_levels)): + connect(world, player, names, LocationName.k3_region, k3_levels[i]) + + # Razor Ridge Connections + razor_ridge_levels = [ + level_list[25], + level_list[26], + level_list[27], + level_list[28], + level_list[29], + LocationName.barboss_barrier_region, + ] + + for i in range(0, len(razor_ridge_levels)): + connect(world, player, names, LocationName.razor_ridge_region, razor_ridge_levels[i]) + + if False:#world.include_trade_sequence[player]: + connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region, + lambda state: (state.has(ItemName.wrench, player, 1))) + else: + connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region) + + # KAOS Kore Connections + kaos_kore_levels = [ + level_list[30], + level_list[31], + level_list[32], + level_list[33], + level_list[34], + LocationName.kastle_kaos_region, + LocationName.sewer_stockpile_region, + ] + + for i in range(0, len(kaos_kore_levels)): + connect(world, player, names, LocationName.kaos_kore_region, kaos_kore_levels[i]) + + # Krematoa Connections + krematoa_levels = [ + level_list[35], + level_list[36], + level_list[37], + level_list[38], + LocationName.rocket_rush_region, + ] + + for i in range(0, len(krematoa_levels)): + connect(world, player, names, LocationName.krematoa_region, krematoa_levels[i], + lambda state: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1)))) + + connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region, + lambda state: (state.has(ItemName.krematoa_cog, player, 5))) + + +def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None, exits=None): + # Shamelessly stolen from the ROR2 definition + ret = Region(name, None, name, player) + ret.world = world + if locations: + for locationName, locationData in locations.items(): + loc_id = active_locations.get(locationName, 0) + if loc_id: + loc_byte = locationData[0] if (len(locationData) > 0) else 0 + loc_bit = locationData[1] if (len(locationData) > 1) else 0 + loc_invert = locationData[2] if (len(locationData) > 2) else False + + location = DKC3Location(player, locationName, loc_id, ret, loc_byte, loc_bit, loc_invert) + ret.locations.append(location) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + + return ret + + +def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str, + rule: typing.Optional[typing.Callable] = None): + source_region = world.get_region(source, player) + target_region = world.get_region(target, player) + + if target not in used_names: + used_names[target] = 1 + name = target + else: + used_names[target] += 1 + name = target + (' ' * used_names[target]) + + connection = Entrance(player, name, source_region) + + if rule: + connection.access_rule = rule + + source_region.exits.append(connection) + connection.connect(target_region) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py new file mode 100644 index 0000000000..761161ee83 --- /dev/null +++ b/worlds/dkc3/Rom.py @@ -0,0 +1,553 @@ +import Utils +from Patch import read_rom +from .Locations import lookup_id_to_name, all_locations +from .Levels import level_list, level_dict + +USHASH = '120abf304f0c40fe059f6a192ed4f947' +ROM_PLAYER_LIMIT = 65535 + +import hashlib +import os +import math + + +location_rom_data = { + 0xDC3000: [0x657, 1], # Lakeside Limbo + 0xDC3001: [0x657, 2], + 0xDC3002: [0x657, 3], + 0xDC3003: [0x657, 5], + + 0xDC3004: [0x65A, 1], # Doorstop Dash + 0xDC3005: [0x65A, 2], + 0xDC3006: [0x65A, 3], + 0xDC3007: [0x65A, 5], + + 0xDC3008: [0x659, 1], # Tidal Trouble + 0xDC3009: [0x659, 2], + 0xDC300A: [0x659, 3], + 0xDC300B: [0x659, 5], + + 0xDC300C: [0x65D, 1], # Skidda's Row + 0xDC300D: [0x65D, 2], + 0xDC300E: [0x65D, 3], + 0xDC300F: [0x65D, 5], + + 0xDC3010: [0x65C, 1], # Murky Mill + 0xDC3011: [0x65C, 2], + 0xDC3012: [0x65C, 3], + 0xDC3013: [0x65C, 5], + + + 0xDC3014: [0x662, 1], # Barrel Shield Bust-Up + 0xDC3015: [0x662, 2], + 0xDC3016: [0x662, 3], + 0xDC3017: [0x662, 5], + + 0xDC3018: [0x664, 1], # Riverside Race + 0xDC3019: [0x664, 2], + 0xDC301A: [0x664, 3], + 0xDC301B: [0x664, 5], + + 0xDC301C: [0x65B, 1], # Squeals on Wheels + 0xDC301D: [0x65B, 2], + 0xDC301E: [0x65B, 3], + 0xDC301F: [0x65B, 5], + + 0xDC3020: [0x661, 1], # Springin' Spiders + 0xDC3021: [0x661, 2], + 0xDC3022: [0x661, 3], + 0xDC3023: [0x661, 5], + + 0xDC3024: [0x666, 1], # Bobbing Barrel Brawl + 0xDC3025: [0x666, 2], + 0xDC3026: [0x666, 3], + 0xDC3027: [0x666, 5], + + + 0xDC3028: [0x667, 1], # Bazza's Blockade + 0xDC3029: [0x667, 2], + 0xDC302A: [0x667, 3], + 0xDC302B: [0x667, 5], + + 0xDC302C: [0x66A, 1], # Rocket Barrel Ride + 0xDC302D: [0x66A, 2], + 0xDC302E: [0x66A, 3], + 0xDC302F: [0x66A, 5], + + 0xDC3030: [0x658, 1], # Kreeping Klasps + 0xDC3031: [0x658, 2], + 0xDC3032: [0x658, 3], + 0xDC3033: [0x658, 5], + + 0xDC3034: [0x66B, 1], # Tracker Barrel Trek + 0xDC3035: [0x66B, 2], + 0xDC3036: [0x66B, 3], + 0xDC3037: [0x66B, 5], + + 0xDC3038: [0x668, 1], # Fish Food Frenzy + 0xDC3039: [0x668, 2], + 0xDC303A: [0x668, 3], + 0xDC303B: [0x668, 5], + + + 0xDC303C: [0x66D, 1], # Fire-ball Frenzy + 0xDC303D: [0x66D, 2], + 0xDC303E: [0x66D, 3], + 0xDC303F: [0x66D, 5], + + 0xDC3040: [0x672, 1], # Demolition Drainpipe + 0xDC3041: [0x672, 2], + 0xDC3042: [0x672, 3], + 0xDC3043: [0x672, 5], + + 0xDC3044: [0x660, 1], # Ripsaw Rage + 0xDC3045: [0x660, 2], + 0xDC3046: [0x660, 3], + 0xDC3047: [0x660, 5], + + 0xDC3048: [0x66E, 1], # Blazing Bazukas + 0xDC3049: [0x66E, 2], + 0xDC304A: [0x66E, 3], + 0xDC304B: [0x66E, 5], + + 0xDC304C: [0x670, 1], # Low-G Labyrinth + 0xDC304D: [0x670, 2], + 0xDC304E: [0x670, 3], + 0xDC304F: [0x670, 5], + + + 0xDC3050: [0x673, 1], # Krevice Kreepers + 0xDC3051: [0x673, 2], + 0xDC3052: [0x673, 3], + 0xDC3053: [0x673, 5], + + 0xDC3054: [0x65F, 1], # Tearaway Toboggan + 0xDC3055: [0x65F, 2], + 0xDC3056: [0x65F, 3], + 0xDC3057: [0x65F, 5], + + 0xDC3058: [0x66C, 1], # Barrel Drop Bounce + 0xDC3059: [0x66C, 2], + 0xDC305A: [0x66C, 3], + 0xDC305B: [0x66C, 5], + + 0xDC305C: [0x66F, 1], # Krack-Shot Kroc + 0xDC305D: [0x66F, 2], + 0xDC305E: [0x66F, 3], + 0xDC305F: [0x66F, 5], + + 0xDC3060: [0x65E, 1], # Lemguin Lunge + 0xDC3061: [0x65E, 2], + 0xDC3062: [0x65E, 3], + 0xDC3063: [0x65E, 5], + + + 0xDC3064: [0x676, 1], # Buzzer Barrage + 0xDC3065: [0x676, 2], + 0xDC3066: [0x676, 3], + 0xDC3067: [0x676, 5], + + 0xDC3068: [0x674, 1], # Kong-Fused Cliffs + 0xDC3069: [0x674, 2], + 0xDC306A: [0x674, 3], + 0xDC306B: [0x674, 5], + + 0xDC306C: [0x669, 1], # Floodlit Fish + 0xDC306D: [0x669, 2], + 0xDC306E: [0x669, 3], + 0xDC306F: [0x669, 5], + + 0xDC3070: [0x677, 1], # Pothole Panic + 0xDC3071: [0x677, 2], + 0xDC3072: [0x677, 3], + 0xDC3073: [0x677, 5], + + 0xDC3074: [0x675, 1], # Ropey Rumpus + 0xDC3075: [0x675, 2], + 0xDC3076: [0x675, 3], + 0xDC3077: [0x675, 5], + + + 0xDC3078: [0x67A, 1], # Konveyor Rope Klash + 0xDC3079: [0x67A, 2], + 0xDC307A: [0x67A, 3], + 0xDC307B: [0x67A, 5], + + 0xDC307C: [0x678, 1], # Creepy Caverns + 0xDC307D: [0x678, 2], + 0xDC307E: [0x678, 3], + 0xDC307F: [0x678, 5], + + 0xDC3080: [0x665, 1], # Lightning Lookout + 0xDC3081: [0x665, 2], + 0xDC3082: [0x665, 3], + 0xDC3083: [0x665, 5], + + 0xDC3084: [0x679, 1], # Koindozer Klamber + 0xDC3085: [0x679, 2], + 0xDC3086: [0x679, 3], + 0xDC3087: [0x679, 5], + + 0xDC3088: [0x671, 1], # Poisonous Pipeline + 0xDC3089: [0x671, 2], + 0xDC308A: [0x671, 3], + 0xDC308B: [0x671, 5], + + + 0xDC308C: [0x67B, 1], # Stampede Sprint + 0xDC308D: [0x67B, 2], + 0xDC308E: [0x67B, 3], + 0xDC308F: [0x67B, 4], + 0xDC3090: [0x67B, 5], + + 0xDC3091: [0x67C, 1], # Criss Kross Cliffs + 0xDC3092: [0x67C, 2], + 0xDC3093: [0x67C, 3], + 0xDC3094: [0x67C, 5], + + 0xDC3095: [0x67D, 1], # Tyrant Twin Tussle + 0xDC3096: [0x67D, 2], + 0xDC3097: [0x67D, 3], + 0xDC3098: [0x67D, 4], + 0xDC3099: [0x67D, 5], + + 0xDC309A: [0x663, 1], # Swoopy Salvo + 0xDC309B: [0x663, 2], + 0xDC309C: [0x663, 3], + 0xDC309D: [0x663, 4], + 0xDC309E: [0x663, 5], + + 0xDC309F: [0x67E, 1], # Rocket Rush + 0xDC30A0: [0x67E, 5], + + 0xDC30A1: [0x64F, 1], # Bosses + 0xDC30A2: [0x650, 1], + 0xDC30A3: [0x651, 1], + 0xDC30A4: [0x652, 1], + 0xDC30A5: [0x653, 1], + 0xDC30A6: [0x654, 1], + 0xDC30A7: [0x655, 1], + 0xDC30A8: [0x656, 1], + + 0xDC30A9: [0x647, 1], # Banana Bird Caves + 0xDC30AA: [0x645, 1], + 0xDC30AB: [0x644, 1], + 0xDC30AC: [0x642, 1], + 0xDC30AD: [0x643, 1], + 0xDC30AE: [0x646, 1], + 0xDC30AF: [0x648, 1], + 0xDC30B0: [0x649, 1], + 0xDC30B1: [0x64A, 1], + 0xDC30B2: [0x64B, 1], + 0xDC30B3: [0x64C, 1], + 0xDC30B4: [0x64D, 1], + 0xDC30B5: [0x64E, 1], + + 0xDC30B6: [0x5FD, 4], # Banana Bird Mother + + # DKC3_TODO: Disabled until Trade Sequence + #0xDC30B7: [0x615, 2, True], + #0xDC30B8: [0x615, 3, True], + #0xDC30B9: [0x619, 2], + ##0xDC30BA: + #0xDC30BB: [0x61B, 3], + #0xDC30BC: [0x61D, 2], + #0xDC30BD: [0x621, 4], + #0xDC30BE: [0x625, 4, True], +} + + +item_rom_data = { + 0xDC3001: [0x5D5], # 1-Up Balloon + 0xDC3002: [0x5C9], # Bear Coin + 0xDC3003: [0x5CB], # Bonus Coin + 0xDC3004: [0x5CF], # DK Coin + 0xDC3005: [0x5CD], # Banana Bird + 0xDC3006: [0x5D1, 0x603], # Cog +} + +music_rom_data = [ + 0x3D06B1, + 0x3D0753, + 0x3D071D, + 0x3D07FA, + 0x3D07C4, + + 0x3D08FE, + 0x3D096C, + 0x3D078E, + 0x3D08CD, + 0x3D09DD, + + 0x3D0A0E, + 0x3D0AB3, + 0x3D06E7, + 0x3D0AE4, + 0x3D0A45, + + 0x3D0B46, + 0x3D0C40, + 0x3D0897, + 0x3D0B77, + 0x3D0BD9, + + 0x3D0C71, + 0x3D0866, + 0x3D0B15, + 0x3D0BA8, + 0x3D0830, + + 0x3D0D04, + 0x3D0CA2, + 0x3D0A7C, + 0x3D0D35, + 0x3D0CD3, + + 0x3D0DC8, + 0x3D0D66, + 0x3D09AC, + 0x3D0D97, + 0x3D0C0F, + + 0x3D0DF9, + 0x3D0E31, + 0x3D0E62, + 0x3D0934, + 0x3D0E9A, +] + +level_music_ids = [ + 0x06, + 0x07, + 0x08, + 0x0A, + 0x0B, + 0x0E, + 0x0F, + 0x10, + 0x17, + 0x19, + 0x1C, + 0x1D, + 0x1E, + 0x21, +] + +class LocalRom(object): + + def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None): + self.name = name + self.hash = hash + self.orig_buffer = None + + with open(file, 'rb') as stream: + self.buffer = read_rom(stream) + #if patch: + # self.patch_rom() + # self.orig_buffer = self.buffer.copy() + #if vanillaRom: + # with open(vanillaRom, 'rb') as vanillaStream: + # self.orig_buffer = read_rom(vanillaStream) + + def read_bit(self, address: int, bit_number: int) -> bool: + bitflag = (1 << bit_number) + return ((self.buffer[address] & bitflag) != 0) + + def read_byte(self, address: int) -> int: + return self.buffer[address] + + def read_bytes(self, startaddress: int, length: int) -> bytes: + return self.buffer[startaddress:startaddress + length] + + def write_byte(self, address: int, value: int): + self.buffer[address] = value + + def write_bytes(self, startaddress: int, values): + self.buffer[startaddress:startaddress + len(values)] = values + + def write_to_file(self, file): + with open(file, 'wb') as outfile: + outfile.write(self.buffer) + + def read_from_file(self, file): + with open(file, 'rb') as stream: + self.buffer = bytearray(stream.read()) + + + +def patch_rom(world, rom, player, active_level_list): + local_random = world.slot_seeds[player] + + # Boomer Costs + bonus_coin_cost = world.krematoa_bonus_coin_cost[player] + inverted_bonus_coin_cost = 0x100 - bonus_coin_cost + rom.write_byte(0x3498B9, inverted_bonus_coin_cost) + rom.write_byte(0x3498BA, inverted_bonus_coin_cost) + rom.write_byte(0x3498BB, inverted_bonus_coin_cost) + rom.write_byte(0x3498BC, inverted_bonus_coin_cost) + rom.write_byte(0x3498BD, inverted_bonus_coin_cost) + + rom.write_byte(0x349857, bonus_coin_cost) + rom.write_byte(0x349862, bonus_coin_cost) + + # Gyrocopter Costs + dk_coin_cost = world.dk_coins_for_gyrocopter[player] + rom.write_byte(0x3484A6, dk_coin_cost) + rom.write_byte(0x3484D5, dk_coin_cost) + rom.write_byte(0x3484D7, 0x90) + rom.write_byte(0x3484DC, 0xEA) + rom.write_byte(0x3484DD, 0xEA) + rom.write_byte(0x3484DE, 0xEA) + rom.write_byte(0x348528, 0x80) # Prevent Single-Ski Lock + + + # Make Swanky free + rom.write_byte(0x348C48, 0x00) + + # Banana Bird Costs + if world.goal[player] == "banana_bird_hunt": + banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0) + rom.write_byte(0x34AB85, banana_bird_cost) + rom.write_byte(0x329FD8, banana_bird_cost) + rom.write_byte(0x32A025, banana_bird_cost) + rom.write_byte(0x329FDA, 0xB0) + else: + # rom.write_byte(0x34AB84, 0x20) # These cause hangs at Wrinkly's + # rom.write_byte(0x329FD8, 0x20) + # rom.write_byte(0x32A025, 0x20) + rom.write_byte(0x329FDA, 0xB0) + + # Baffle Mirror Fix + rom.write_byte(0x9133, 0x08) + rom.write_byte(0x9135, 0x0C) + rom.write_byte(0x9136, 0x2B) + rom.write_byte(0x9137, 0x06) + + # Palette Swap + rom.write_byte(0x3B96A5, 0xD0) + if world.kong_palette_swap[player] == "default": + rom.write_byte(0x3B96A9, 0x00) + rom.write_byte(0x3B96A8, 0x00) + elif world.kong_palette_swap[player] == "purple": + rom.write_byte(0x3B96A9, 0x00) + rom.write_byte(0x3B96A8, 0x3C) + elif world.kong_palette_swap[player] == "spooky": + rom.write_byte(0x3B96A9, 0x00) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "dark": + rom.write_byte(0x3B96A9, 0x05) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "chocolate": + rom.write_byte(0x3B96A9, 0x1D) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "shadow": + rom.write_byte(0x3B96A9, 0x45) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "red_gold": + rom.write_byte(0x3B96A9, 0x5D) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "gbc": + rom.write_byte(0x3B96A9, 0x20) + rom.write_byte(0x3B96A8, 0x3C) + elif world.kong_palette_swap[player] == "halloween": + rom.write_byte(0x3B96A9, 0x70) + rom.write_byte(0x3B96A8, 0x3C) + + if world.music_shuffle[player]: + for address in music_rom_data: + rand_song = local_random.choice(level_music_ids) + rom.write_byte(address, rand_song) + + # Starting Lives + rom.write_byte(0x9130, world.starting_life_count[player].value) + rom.write_byte(0x913B, world.starting_life_count[player].value) + + + # Handle Level Shuffle Here + if world.level_shuffle[player]: + for i in range(len(active_level_list)): + rom.write_byte(level_dict[level_list[i]].nameIDAddress, level_dict[active_level_list[i]].nameID) + rom.write_byte(level_dict[level_list[i]].levelIDAddress, level_dict[active_level_list[i]].levelID) + + # First levels of each world + rom.write_byte(0x34BC3E, (0x32 + level_dict[active_level_list[0]].levelID)) + rom.write_byte(0x34BC47, (0x32 + level_dict[active_level_list[5]].levelID)) + rom.write_byte(0x34BC4A, (0x32 + level_dict[active_level_list[10]].levelID)) + rom.write_byte(0x34BC53, (0x32 + level_dict[active_level_list[15]].levelID)) + rom.write_byte(0x34BC59, (0x32 + level_dict[active_level_list[20]].levelID)) + rom.write_byte(0x34BC5C, (0x32 + level_dict[active_level_list[25]].levelID)) + rom.write_byte(0x34BC65, (0x32 + level_dict[active_level_list[30]].levelID)) + rom.write_byte(0x34BC6E, (0x32 + level_dict[active_level_list[35]].levelID)) + + # Cotton-Top Cove Boss Unlock + rom.write_byte(0x34C02A, (0x32 + level_dict[active_level_list[14]].levelID)) + + # Kong-Fused Cliffs Unlock + rom.write_byte(0x34C213, (0x32 + level_dict[active_level_list[25]].levelID)) + rom.write_byte(0x34C21B, (0x32 + level_dict[active_level_list[26]].levelID)) + + if world.goal[player] == "knautilus": + # Swap Kastle KAOS and Knautilus + rom.write_byte(0x34D4E1, 0xC2) + rom.write_byte(0x34D4E2, 0x24) + rom.write_byte(0x34D551, 0xBA) + rom.write_byte(0x34D552, 0x23) + + rom.write_byte(0x32F339, 0x55) + + + from Main import __version__ + rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21] + rom.name.extend([0] * (21 - len(rom.name))) + rom.write_bytes(0x7FC0, rom.name) + + # DKC3_TODO: This is a hack, reconsider + # Don't grant (DK, Bonus, Bear) Coins + rom.write_byte(0x3BD454, 0xEA) + rom.write_byte(0x3BD455, 0xEA) + + # Don't grant Cogs + rom.write_byte(0x3BD574, 0xEA) + rom.write_byte(0x3BD575, 0xEA) + rom.write_byte(0x3BD576, 0xEA) + + # Don't grant Banana Birds at their caves + rom.write_byte(0x32DD62, 0xEA) + rom.write_byte(0x32DD63, 0xEA) + rom.write_byte(0x32DD64, 0xEA) + + # Don't grant Patch and Skis from their bosses + rom.write_byte(0x3F3762, 0x00) + rom.write_byte(0x3F377B, 0x00) + rom.write_byte(0x3F3797, 0x00) + + # Always allow Start+Select + rom.write_byte(0x8BAB, 0x01) + + # Handle Alt Palettes in Krematoa + rom.write_byte(0x3B97E9, 0x80) + rom.write_byte(0x3B97EA, 0xEA) + + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if USHASH != basemd5.hexdigest(): + raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["dkc3_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.local_path(file_name) + return file_name diff --git a/worlds/dkc3/Rules.py b/worlds/dkc3/Rules.py new file mode 100644 index 0000000000..dcfc912448 --- /dev/null +++ b/worlds/dkc3/Rules.py @@ -0,0 +1,32 @@ +import math + +from BaseClasses import MultiWorld +from .Names import LocationName, ItemName +from ..AutoWorld import LogicMixin +from ..generic.Rules import add_rule, set_rule + + +def set_rules(world: MultiWorld, player: int): + + if False:#world.include_trade_sequence[player]: + add_rule(world.get_location(LocationName.barnacles_island, player), + lambda state: state.has(ItemName.shell, player)) + + add_rule(world.get_location(LocationName.blues_beach_hut, player), + lambda state: state.has(ItemName.present, player)) + + add_rule(world.get_location(LocationName.brambles_bungalow, player), + lambda state: state.has(ItemName.flower, player)) + + add_rule(world.get_location(LocationName.barters_swap_shop, player), + lambda state: state.has(ItemName.mirror, player)) + + + if world.goal[player] != "knautilus": + required_banana_birds = math.floor( + world.number_of_banana_birds[player].value * (world.percentage_of_banana_birds[player].value / 100.0)) + + add_rule(world.get_location(LocationName.banana_bird_mother, player), + lambda state: state.has(ItemName.banana_bird, player, required_banana_birds)) + + world.completion_condition[player] = lambda state: state.has(ItemName.victory, player) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py new file mode 100644 index 0000000000..54087db9aa --- /dev/null +++ b/worlds/dkc3/__init__.py @@ -0,0 +1,204 @@ +import os +import typing +import math +import threading + +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from .Items import DKC3Item, ItemData, item_table, inventory_table +from .Locations import DKC3Location, all_locations, setup_locations +from .Options import dkc3_options +from .Regions import create_regions, connect_regions +from .Levels import level_list +from .Rules import set_rules +from .Names import ItemName, LocationName +from ..AutoWorld import WebWorld, World +from .Rom import LocalRom, patch_rom, get_base_rom_path +import Patch + + +class DKC3Web(WebWorld): + theme = "jungle" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Donkey Kong Country 3 randomizer connected to an Archipelago Multiworld.", + "English", + "setup_en.md", + "setup/en", + ["PoryGone"] + ) + + tutorials = [setup_en] + + +class DKC3World(World): + """ + Donkey Kong Country 3 is an action platforming game. + Play as Dixie Kong and her baby cousin Kiddy as they try to solve the + mystery of why Donkey Kong and Diddy disappeared while on vacation. + """ + game: str = "Donkey Kong Country 3" + options = dkc3_options + topology_present = False + data_version = 1 + #hint_blacklist = {LocationName.rocket_rush_flag} + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = all_locations + + active_level_list: typing.List[str] + web = DKC3Web() + + def __init__(self, world: MultiWorld, player: int): + self.rom_name_available_event = threading.Event() + super().__init__(world, player) + + @classmethod + def stage_assert_generate(cls, world): + rom_file = get_base_rom_path() + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + + def _get_slot_data(self): + return { + #"death_link": self.world.death_link[self.player].value, + "active_levels": self.active_level_list, + } + + def _create_items(self, name: str): + data = item_table[name] + return [self.create_item(name)] * data.quantity + + def fill_slot_data(self) -> dict: + slot_data = self._get_slot_data() + for option_name in dkc3_options: + option = getattr(self.world, option_name)[self.player] + slot_data[option_name] = option.value + + return slot_data + + def generate_basic(self): + self.topology_present = self.world.level_shuffle[self.player].value + itempool: typing.List[DKC3Item] = [] + + # Levels + total_required_locations = 161 + + number_of_banana_birds = 0 + # Rocket Rush Cog + total_required_locations -= 1 + number_of_cogs = 4 + self.world.get_location(LocationName.rocket_rush_flag, self.player).place_locked_item(self.create_item(ItemName.krematoa_cog)) + number_of_bosses = 8 + if self.world.goal[self.player] == "knautilus": + self.world.get_location(LocationName.kastle_kaos, self.player).place_locked_item(self.create_item(ItemName.victory)) + number_of_bosses = 7 + else: + self.world.get_location(LocationName.banana_bird_mother, self.player).place_locked_item(self.create_item(ItemName.victory)) + number_of_banana_birds = self.world.number_of_banana_birds[self.player] + + # Bosses + total_required_locations += number_of_bosses + + # Secret Caves + total_required_locations += 13 + + ## Brothers Bear + if False:#self.world.include_trade_sequence[self.player]: + total_required_locations += 8 + + number_of_bonus_coins = (self.world.krematoa_bonus_coin_cost[self.player] * 5) + number_of_bonus_coins += math.ceil((85 - number_of_bonus_coins) * self.world.percentage_of_extra_bonus_coins[self.player] / 100) + + itempool += [self.create_item(ItemName.bonus_coin)] * number_of_bonus_coins + itempool += [self.create_item(ItemName.dk_coin)] * 41 + itempool += [self.create_item(ItemName.banana_bird)] * number_of_banana_birds + itempool += [self.create_item(ItemName.krematoa_cog)] * number_of_cogs + itempool += [self.create_item(ItemName.progressive_boat)] * 3 + + total_junk_count = total_required_locations - len(itempool) + + itempool += [self.create_item(ItemName.bear_coin)] * total_junk_count + + self.active_level_list = level_list.copy() + + if self.world.level_shuffle[self.player]: + self.world.random.shuffle(self.active_level_list) + + connect_regions(self.world, self.player, self.active_level_list) + + self.world.itempool += itempool + + def generate_output(self, output_directory: str): + try: + world = self.world + player = self.player + + rom = LocalRom(get_base_rom_path()) + patch_rom(self.world, rom, self.player, self.active_level_list) + + self.active_level_list.append(LocationName.rocket_rush_region) + + outfilepname = f'_P{player}' + outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ + if world.player_name[player] != 'Player%d' % player else '' + + rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') + rom.write_to_file(rompath) + Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player], game=Patch.GAME_DKC3) + os.unlink(rompath) + self.rom_name = rom.name + except: + raise + finally: + self.rom_name_available_event.set() # make sure threading continues and errors are collected + + def modify_multidata(self, multidata: dict): + import base64 + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + rom_name = getattr(self, "rom_name", None) + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]] + + if self.topology_present: + world_names = [ + LocationName.lake_orangatanga_region, + LocationName.kremwood_forest_region, + LocationName.cotton_top_cove_region, + LocationName.mekanos_region, + LocationName.k3_region, + LocationName.razor_ridge_region, + LocationName.kaos_kore_region, + LocationName.krematoa_region, + ] + er_hint_data = {} + for world_index in range(len(world_names)): + for level_index in range(5): + level_region = self.world.get_region(self.active_level_list[world_index * 5 + level_index], self.player) + for location in level_region.locations: + er_hint_data[location.address] = world_names[world_index] + multidata['er_hint_data'][self.player] = er_hint_data + + def create_regions(self): + location_table = setup_locations(self.world, self.player) + create_regions(self.world, self.player, location_table) + + def create_item(self, name: str, force_non_progression=False) -> Item: + data = item_table[name] + + if force_non_progression: + classification = ItemClassification.filler + elif data.progression: + classification = ItemClassification.progression + else: + classification = ItemClassification.filler + + created_item = DKC3Item(name, classification, data.code, self.player) + + return created_item + + def set_rules(self): + set_rules(self.world, self.player) diff --git a/worlds/dkc3/docs/en_Donkey Kong Country 3.md b/worlds/dkc3/docs/en_Donkey Kong Country 3.md new file mode 100644 index 0000000000..2041f0a41b --- /dev/null +++ b/worlds/dkc3/docs/en_Donkey Kong Country 3.md @@ -0,0 +1,35 @@ +# Donkey Kong Country 3 + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? + +Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is +always able to be completed, but because of the item shuffle the player may need to access certain areas before they +would in the vanilla game. + +## What is the goal of Donkey Kong Country 3 when randomized? + +There are two goals which can be chosen: +- `Knautilus`: Collect Bonus Coins and Krematoa Cogs to reach K. Rool's submarine in Krematoa +- `Banana Bird Hunt`: Collect Banana Birds to free the Banana Bird Mother + +## What items and locations get shuffled? + +All Bonus Coins, DK Coins, and Banana Birds (if on a `Banana Bird Hunt` goal) are randomized. Additionally, level clears award a location check. +The Patch and two Skis for upgrading the boat are included. Bear Coins are provided if additional items are needed for the item pool. +Four of the Five Krematoa Cogs are randomized, but the final one is always in its vanilla location at the Flag of Rocket Rush in Krematoa + +## Which items can be in another player's world? + +Any shuffled item can be in other players' worlds. + +## What does another world's item look like in Donkey Kong Country 3 + +Items pickups all retain their original appearance. You won't know if an item belongs to another player until you collect. + +## When the player receives an item, what happens? + +Currently, the items are silently added to the player's inventory, which can be seen when saving the game. diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md new file mode 100644 index 0000000000..34a297eab0 --- /dev/null +++ b/worlds/dkc3/docs/setup_en.md @@ -0,0 +1,161 @@ +# Donkey Kong Country 3 Randomizer Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Donkey Kong Country 3 Patch Setup` + + +- Hardware or software capable of loading and playing SNES ROM files + - An emulator capable of connecting to SNI such as: + - snes9x Multitroid + from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html) + - RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, + - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other + compatible hardware +- Your legally obtained Donkey Kong Country 3 ROM file, probably named `Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc` + +## Installation Procedures + +### Windows Setup + +1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this, + or you are on an older version, you may run the installer again to install the SNI Client. +2. During setup, you will be asked to locate your base ROM file. This is your Donkey Kong Country 3 ROM file. +3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .sfc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Generating a Single-Player Game + +1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. + - Player Settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings) +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your patch file. +5. Double-click on your patch file, and the Donkey Kong Country 3 Client will launch automatically, create your ROM from the + patch file, and open your emulator for you. +6. Since this is a single-player game, you will no longer need the client, so feel free to close it. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.apsm` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + +### Connect to the client + +#### With an emulator + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +##### snes9x Multitroid + +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the connector lua file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of +the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. + +##### BizHawk + +1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these + menu options: + `Config --> Cores --> SNES --> BSNES` + Once you have changed the loaded core, you must restart BizHawk. +2. Load your ROM file if it hasn't already been loaded. +3. Click on the Tools menu and click on **Lua Console** +4. Click the button to open a new Lua script. +5. Select the `Connector.lua` file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. + +##### RetroArch 1.10.3 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to +read ROM data. + +#### With hardware + +This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do +this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES +releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases) + +Other hardware may find helpful information on the usb2snes platforms +page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms) + +1. Close your emulator, which may have auto-launched. +2. Power on your device and load the ROM. + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! + +## Hosting a MultiWorld game + +The recommended way to host a game is to use our hosting service. The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the Generate page above. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, you will be redirected to a "Seed Info" page. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. From 04c34298390313afd1b482b7e4a480962f368ff1 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 22 Jul 2022 00:04:41 -0500 Subject: [PATCH 043/138] LttP: Fix scam options (#806) --- worlds/alttp/Options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index b42a5eb377..183f3eda91 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -215,9 +215,11 @@ class Scams(Choice): option_all = 3 alias_false = 0 + @property def gives_king_zora_hint(self): return self.value in {0, 2} + @property def gives_bottle_merchant_hint(self): return self.value in {0, 1} From fe2c3557392652089f2cb0d2de5c304e30b9e36d Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Fri, 22 Jul 2022 03:44:58 -0400 Subject: [PATCH 044/138] Sm beam door speedkeep fun accessibility (#785) added speedkeep option now forces accessibility to "minimal" instead of (to be deprecated) "item" when "fun" settings is used --- worlds/sm/Options.py | 12 ++++++++---- worlds/sm/__init__.py | 4 ++-- .../patches/common/ips/beam_doors_gfx.ips | Bin 13919 -> 10353 bytes 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 07f79f17bd..814b19f4d4 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -143,15 +143,15 @@ class BossRandomization(Toggle): display_name = "Boss Randomization" class FunCombat(Toggle): - """Forces removal of Plasma Beam and Screw Attack if the preset and settings allow it. In addition, can randomly remove Spazer and Wave Beam from the Combat set. If used, might force 'items' accessibility.""" + """Forces removal of Plasma Beam and Screw Attack if the preset and settings allow it. In addition, can randomly remove Spazer and Wave Beam from the Combat set. If used, might force 'minimal' accessibility.""" display_name = "Fun Combat" class FunMovement(Toggle): - """Forces removal of Space Jump if the preset allows it. In addition, can randomly remove High Jump, Grappling Beam, Spring Ball, Speed Booster, and Bombs from the Movement set. If used, might force 'items' accessibility.""" + """Forces removal of Space Jump if the preset allows it. In addition, can randomly remove High Jump, Grappling Beam, Spring Ball, Speed Booster, and Bombs from the Movement set. If used, might force 'minimal' accessibility.""" display_name = "Fun Movement" class FunSuits(Toggle): - """If the preset and seed layout allow it, will force removal of at least one of Varia Suit and/or Gravity Suit. If used, might force 'items' accessibility.""" + """If the preset and seed layout allow it, will force removal of at least one of Varia Suit and/or Gravity Suit. If used, might force 'minimal' accessibility.""" display_name = "Fun Suits" class LayoutPatches(DefaultOnToggle): @@ -182,6 +182,10 @@ class SpinJumpRestart(Toggle): """Allows Samus to start spinning in mid air after jumping or falling.""" display_name = "Spin Jump Restart" +class SpeedKeep(Toggle): + """Let Samus keeps her momentum when landing from a fall or from jumping.""" + display_name = "Momentum conservation (a.k.a. Speedkeep)" + class InfiniteSpaceJump(Toggle): """Space jumps can be done quicker and at any time in air, water or lava, even after falling long distances.""" display_name = "Infinite Space Jump" @@ -266,7 +270,7 @@ sm_options: typing.Dict[str, type(Option)] = { #"item_sounds": "on", "elevators_doors_speed": ElevatorsDoorsSpeed, "spin_jump_restart": SpinJumpRestart, - #"rando_speed": "off", + "rando_speed": SpeedKeep, "infinite_space_jump": InfiniteSpaceJump, "refill_before_save": RefillBeforeSave, "hud": Hud, diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index fe1323caec..59c3c463f7 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -125,8 +125,8 @@ class SMWorld(World): self.remote_items = self.world.remote_items[self.player] if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0): - self.world.accessibility[self.player] = self.world.accessibility[self.player].from_text("items") - logger.warning(f"accessibility forced to 'items' for player {self.world.get_player_name(self.player)} because of 'fun' settings") + self.world.accessibility[self.player] = self.world.accessibility[self.player].from_text("minimal") + logger.warning(f"accessibility forced to 'minimal' for player {self.world.get_player_name(self.player)} because of 'fun' settings") def generate_basic(self): itemPool = self.variaRando.container.itemPool diff --git a/worlds/sm/variaRandomizer/patches/common/ips/beam_doors_gfx.ips b/worlds/sm/variaRandomizer/patches/common/ips/beam_doors_gfx.ips index 00239095e2b2412eb0b085bb510f476aa4f2c372..509bbbf8e5d2fd2f98633be32132c6e773271aa2 100644 GIT binary patch delta 11 Scmcbg^D$sUo(8k4zZ(D|yaccS delta 3605 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 Date: Sat, 23 Jul 2022 12:42:14 +0200 Subject: [PATCH 045/138] The Witness: Small changes in response to beta tests (#801) * Option order and better tooltip * Logic fix: Hedge Laser requires access to all Hedges * Add item groups: Lasers, Symbols, Doors * Update worlds/witness/items.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Comment for clarity * Logic fix * Another logic fix Co-authored-by: metzner Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/witness/Options.py | 7 ++++--- worlds/witness/WitnessItems.txt | 4 ++-- worlds/witness/WitnessLogic.txt | 2 +- worlds/witness/__init__.py | 1 + worlds/witness/items.py | 12 +++++++++++- worlds/witness/settings/Disable_Unrandomized.txt | 3 ++- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index 2bfe3f41e1..631a5bc076 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -36,7 +36,8 @@ class ShuffleLasers(Toggle): class ShuffleDoors(Choice): """If on, opening doors will require their respective "keys". If set to "panels", those keys will unlock the panels on doors. - In "doors_simple" and "doors_complex", the doors will magically open by themselves upon receiving the key.""" + In "doors_simple" and "doors_complex", the doors will magically open by themselves upon receiving the key. + The last option, "max", is a combination of "doors_complex" and "panels".""" display_name = "Shuffle Doors" option_none = 0 option_panels = 1 @@ -124,10 +125,10 @@ the_witness_options: Dict[str, type] = { "shuffle_uncommon": ShuffleUncommonLocations, "shuffle_postgame": ShufflePostgame, "victory_condition": VictoryCondition, - "trap_percentage": TrapPercentage, - "early_secret_area": EarlySecretArea, "mountain_lasers": MountainLasers, "challenge_lasers": ChallengeLasers, + "early_secret_area": EarlySecretArea, + "trap_percentage": TrapPercentage, "puzzle_skip_amount": PuzzleSkipAmount, } diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 5631ab2f41..4449602529 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -173,8 +173,8 @@ Doors: 1906 - Symmetry Island Doors - 0x17F3E,0x18269 1909 - Orchard Gates - 0x03313,0x03307 1912 - Desert Doors - 0x09FEE,0x0C2C3,0x0A24B,0x0C316 -1915 - Quarry Main Entry - 0x09D6F -1918 - Quarry Mill Shortcuts - 0x17C07,0x17CE8,0x0368A +1915 - Quarry Main Entry - 0x09D6F,0x17C07 +1918 - Quarry Mill Shortcuts - 0x17CE8,0x0368A,0x275FF 1921 - Quarry Boathouse Barriers - 0x17C50,0x3865F 1924 - Shadows Laser Room Door - 0x194B2,0x19665 1927 - Shadows Barriers - 0x19865,0x0A2DF,0x1855B,0x19ADE diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 350f72b680..a4cdd9f1c4 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -354,7 +354,7 @@ Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut to Keep Panel) - True - True Door - 0x04F8F (Tower Shortcut to Keep) - 0x0361B -158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F - Environment & Sound +158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - Environment & Sound 158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Squares & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots Laser - 0x014BB (Laser) - 0x0360E | 0x03317 diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 01669cffba..da6683b51c 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -47,6 +47,7 @@ class WitnessWorld(World): name: data.code for name, data in static_items.ALL_ITEM_TABLE.items() } location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID + item_name_groups = StaticWitnessItems.ITEM_NAME_GROUPS def _get_slot_data(self): return { diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 65a8326984..9ffd5a1173 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -2,7 +2,7 @@ Defines progression, junk and event items for The Witness """ import copy -from typing import Dict, NamedTuple, Optional +from typing import Dict, NamedTuple, Optional, Set from BaseClasses import Item, MultiWorld from . import StaticWitnessLogic, WitnessPlayerLocations, WitnessPlayerLogic @@ -35,6 +35,8 @@ class StaticWitnessItems: ALL_ITEM_TABLE: Dict[str, ItemData] = {} + ITEM_NAME_GROUPS: Dict[str, Set[str]] = dict() + # These should always add up to 1!!! BONUS_WEIGHTS = { "Speed Boost": Fraction(1, 1), @@ -57,9 +59,17 @@ class StaticWitnessItems: item_tab[item[0]] = ItemData(158000 + item[1], True, False) + self.ITEM_NAME_GROUPS.setdefault("Symbols", set()).add(item[0]) + for item in StaticWitnessLogic.ALL_DOOR_ITEMS: item_tab[item[0]] = ItemData(158000 + item[1], True, False) + # 1500 - 1510 are the laser items, which are handled like doors but should be their own separate group. + if item[1] in range(1500, 1511): + self.ITEM_NAME_GROUPS.setdefault("Lasers", set()).add(item[0]) + else: + self.ITEM_NAME_GROUPS.setdefault("Doors", set()).add(item[0]) + for item in StaticWitnessLogic.ALL_TRAPS: item_tab[item[0]] = ItemData( 158000 + item[1], False, False, True diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index 6f957ada5c..43c2596405 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -82,7 +82,8 @@ Disabled Locations: 0x002C7 (Waves 7) 0x15ADD (River Rhombic Avoid Vault) 0x03702 (River Vault Box) -0x17C2E (Door to Bunker) - True - Squares & Black/White Squares +0x17CAA (Rhombic Avoid to Monastery Garden) +0x17C2E (Door to Bunker) 0x09F7D (Bunker Drawn Squares 1) 0x09FDC (Bunker Drawn Squares 2) 0x09FF7 (Bunker Drawn Squares 3) From e6635cdd773aac7c37101e324e9a90d5c9d156c6 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Sun, 24 Jul 2022 20:07:22 -0400 Subject: [PATCH 046/138] OOT updates (#821) * oot: remove all escape characters in LogicTricks.py * only attempt to connect to client once * oot: don't kill player outside ToT or in market entrance fixed camera makes the game crash outside ToT. added market entrance to be safe, it doesn't matter if you don't die there --- OoTClient.py | 4 ++-- data/lua/OOT/oot_connector.lua | 13 +++++++--- worlds/oot/LogicTricks.py | 44 +++++++++++++++++----------------- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/OoTClient.py b/OoTClient.py index e455efdcd3..fbe2b35d1a 100644 --- a/OoTClient.py +++ b/OoTClient.py @@ -48,7 +48,7 @@ deathlink_sent_this_death: we interacted with the multiworld on this death, wait oot_loc_name_to_id = network_data_package["games"]["Ocarina of Time"]["location_name_to_id"] -script_version: int = 1 +script_version: int = 2 def get_item_value(ap_id): return ap_id - 66000 @@ -186,7 +186,7 @@ async def n64_sync_task(ctx: OoTContext): data = await asyncio.wait_for(reader.readline(), timeout=10) data_decoded = json.loads(data.decode()) reported_version = data_decoded.get('scriptVersion', 0) - if reported_version == script_version: + if reported_version >= script_version: if ctx.game is not None and 'locations' in data_decoded: # Not just a keep alive ping, parse asyncio.create_task(parse_payload(data_decoded, ctx, False)) diff --git a/data/lua/OOT/oot_connector.lua b/data/lua/OOT/oot_connector.lua index 96eee4f78f..a82bcdcb83 100644 --- a/data/lua/OOT/oot_connector.lua +++ b/data/lua/OOT/oot_connector.lua @@ -2,8 +2,8 @@ local socket = require("socket") local json = require('json') local math = require('math') -local last_modified_date = '2022-05-25' -- Should be the last modified date -local script_version = 1 +local last_modified_date = '2022-07-24' -- Should be the last modified date +local script_version = 2 -------------------------------------------------- -- Heavily modified form of RiptideSage's tracker @@ -1723,6 +1723,11 @@ function get_death_state() end function kill_link() + -- market entrance: 27/28/29 + -- outside ToT: 35/36/37. + -- if killed on these scenes the game crashes, so we wait until not on this screen. + local scene = global_context:rawget('cur_scene'):rawget() + if scene == 27 or scene == 28 or scene == 29 or scene == 35 or scene == 36 or scene == 37 then return end mainmemory.write_u16_be(0x11A600, 0) end @@ -1824,13 +1829,15 @@ function main() elseif (curstate == STATE_UNINITIALIZED) then if (frame % 60 == 0) then server:settimeout(2) - print("Attempting to connect") local client, timeout = server:accept() if timeout == nil then print('Initial Connection Made') curstate = STATE_INITIAL_CONNECTION_MADE ootSocket = client ootSocket:settimeout(0) + else + print('Connection failed, ensure OoTClient is running and rerun oot_connector.lua') + return end end end diff --git a/worlds/oot/LogicTricks.py b/worlds/oot/LogicTricks.py index 6950bc2124..548b7b969f 100644 --- a/worlds/oot/LogicTricks.py +++ b/worlds/oot/LogicTricks.py @@ -51,20 +51,20 @@ known_logic_tricks = { Can be reached by side-hopping off the watchtower. '''}, - 'Dodongo\'s Cavern Staircase with Bow': { + "Dodongo's Cavern Staircase with Bow": { 'name' : 'logic_dc_staircase', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ The Bow can be used to knock down the stairs with two well-timed shots. '''}, - 'Dodongo\'s Cavern Spike Trap Room Jump without Hover Boots': { + "Dodongo's Cavern Spike Trap Room Jump without Hover Boots": { 'name' : 'logic_dc_jump', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ Jump is adult only. '''}, - 'Dodongo\'s Cavern Vines GS from Below with Longshot': { + "Dodongo's Cavern Vines GS from Below with Longshot": { 'name' : 'logic_dc_vines_gs', 'tags' : ("Dodongo's Cavern", "Skulltulas",), 'tooltip' : '''\ @@ -73,7 +73,7 @@ known_logic_tricks = { from below, by shooting it through the vines, bypassing the need to lower the staircase. '''}, - 'Thieves\' Hideout "Kitchen" with No Additional Items': { + '''Thieves' Hideout "Kitchen" with No Additional Items''': { 'name' : 'logic_gerudo_kitchen', 'tags' : ("Thieves' Hideout", "Gerudo's Fortress"), 'tooltip' : '''\ @@ -157,7 +157,7 @@ known_logic_tricks = { Can jump up to the spinning platform from below as adult. '''}, - 'Crater\'s Bean PoH with Hover Boots': { + "Crater's Bean PoH with Hover Boots": { 'name' : 'logic_crater_bean_poh_with_hovers', 'tags' : ("Death Mountain Crater",), 'tooltip' : '''\ @@ -165,7 +165,7 @@ known_logic_tricks = { near Goron City and walk up the very steep slope. '''}, - 'Zora\'s Domain Entry with Cucco': { + "Zora's Domain Entry with Cucco": { 'name' : 'logic_zora_with_cucco', 'tags' : ("Zora's River",), 'tooltip' : '''\ @@ -404,7 +404,7 @@ known_logic_tricks = { Longshot can be shot through the ceiling to obtain the token with two fewer small keys than normal. '''}, - 'Zora\'s River Lower Freestanding PoH as Adult with Nothing': { + "Zora's River Lower Freestanding PoH as Adult with Nothing": { 'name' : 'logic_zora_river_lower', 'tags' : ("Zora's River",), 'tooltip' : '''\ @@ -502,7 +502,7 @@ known_logic_tricks = { you can get enough of a break to take some time to aim more carefully. '''}, - 'Dodongo\'s Cavern Scarecrow GS with Armos Statue': { + "Dodongo's Cavern Scarecrow GS with Armos Statue": { 'name' : 'logic_dc_scarecrow_gs', 'tags' : ("Dodongo's Cavern", "Skulltulas",), 'tooltip' : '''\ @@ -541,7 +541,7 @@ known_logic_tricks = { 'name' : 'logic_spirit_mq_lower_adult', 'tags' : ("Spirit Temple",), 'tooltip' : '''\ - It can be done with Din\'s Fire and Bow. + It can be done with Din's Fire and Bow. Whenever an arrow passes through a lit torch, it resets the timer. It's finicky but it's also possible to stand on the pillar next to the center @@ -704,13 +704,13 @@ known_logic_tricks = { in the same jump in order to destroy it before you fall into the lava. '''}, - 'Zora\'s Domain Entry with Hover Boots': { + "Zora's Domain Entry with Hover Boots": { 'name' : 'logic_zora_with_hovers', 'tags' : ("Zora's River",), 'tooltip' : '''\ Can hover behind the waterfall as adult. '''}, - 'Zora\'s Domain GS with No Additional Items': { + "Zora's Domain GS with No Additional Items": { 'name' : 'logic_domain_gs', 'tags' : ("Zora's Domain", "Skulltulas",), 'tooltip' : '''\ @@ -736,7 +736,7 @@ known_logic_tricks = { needing a Bow. Applies in both vanilla and MQ Shadow. '''}, - 'Stop Link the Goron with Din\'s Fire': { + "Stop Link the Goron with Din's Fire": { 'name' : 'logic_link_goron_dins', 'tags' : ("Goron City",), 'tooltip' : '''\ @@ -825,7 +825,7 @@ known_logic_tricks = { Link will not be expected to do anything at Gerudo's Fortress. '''}, - 'Zora\'s River Upper Freestanding PoH as Adult with Nothing': { + "Zora's River Upper Freestanding PoH as Adult with Nothing": { 'name' : 'logic_zora_river_upper', 'tags' : ("Zora's River",), 'tooltip' : '''\ @@ -971,7 +971,7 @@ known_logic_tricks = { in the Water Temple are not going to be relevant unless this trick is first enabled. '''}, - 'Water Temple Central Pillar GS with Farore\'s Wind': { + "Water Temple Central Pillar GS with Farore's Wind": { 'name' : 'logic_water_central_gs_fw', 'tags' : ("Water Temple", "Skulltulas",), 'tooltip' : '''\ @@ -1104,7 +1104,7 @@ known_logic_tricks = { this allows you to obtain the GS on the door frame as adult without Hookshot or Song of Time. '''}, - 'Dodongo\'s Cavern MQ Early Bomb Bag Area as Child': { + "Dodongo's Cavern MQ Early Bomb Bag Area as Child": { 'name' : 'logic_dc_mq_child_bombs', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1113,7 +1113,7 @@ known_logic_tricks = { without needing a Slingshot. You will take fall damage. '''}, - 'Dodongo\'s Cavern Two Scrub Room with Strength': { + "Dodongo's Cavern Two Scrub Room with Strength": { 'name' : 'logic_dc_scrub_room', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1122,7 +1122,7 @@ known_logic_tricks = { destroy the mud wall blocking the room with two Deku Scrubs. '''}, - 'Dodongo\'s Cavern Child Slingshot Skips': { + "Dodongo's Cavern Child Slingshot Skips": { 'name' : 'logic_dc_slingshot_skip', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1132,7 +1132,7 @@ known_logic_tricks = { you also enable the Adult variant: "Dodongo's Cavern Spike Trap Room Jump without Hover Boots". '''}, - 'Dodongo\'s Cavern MQ Light the Eyes with Strength': { + "Dodongo's Cavern MQ Light the Eyes with Strength": { 'name' : 'logic_dc_mq_eyes', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1145,7 +1145,7 @@ known_logic_tricks = { Also, the bombable floor before King Dodongo can be destroyed with Hammer if hit in the very center. '''}, - 'Dodongo\'s Cavern MQ Back Areas as Child without Explosives': { + "Dodongo's Cavern MQ Back Areas as Child without Explosives": { 'name' : 'logic_dc_mq_child_back', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1232,7 +1232,7 @@ known_logic_tricks = { It can also be done as child, using the Slingshot instead of the Bow. '''}, - 'Fire Temple East Tower without Scarecrow\'s Song': { + "Fire Temple East Tower without Scarecrow's Song": { 'name' : 'logic_fire_scarecrow', 'tags' : ("Fire Temple",), 'tooltip' : '''\ @@ -1277,14 +1277,14 @@ known_logic_tricks = { Removes the requirements for the Lens of Truth in Bottom of the Well. '''}, - 'Ganon\'s Castle MQ without Lens of Truth': { + "Ganon's Castle MQ without Lens of Truth": { 'name' : 'logic_lens_castle_mq', 'tags' : ("Lens of Truth","Ganon's Castle",), 'tooltip' : '''\ Removes the requirements for the Lens of Truth in Ganon's Castle MQ. '''}, - 'Ganon\'s Castle without Lens of Truth': { + "Ganon's Castle without Lens of Truth": { 'name' : 'logic_lens_castle', 'tags' : ("Lens of Truth","Ganon's Castle",), 'tooltip' : '''\ From c3ff201b9054bde4eb3c79199648413d27f81ba1 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Mon, 25 Jul 2022 18:39:31 +0200 Subject: [PATCH 047/138] sm64ex: Various Features (#790) * sm64ex: Course and Secret Randomizer * sm64ex: Allow higher star door costs, raise minimum amount of stars, deprecate ExtraStars * sm64ex: Support setting MIPS costs * sm64ex: Safeguard MIPS Costs --- worlds/generic/docs/advanced_settings_en.md | 2 +- worlds/sm64ex/Locations.py | 53 ++++--- worlds/sm64ex/Options.py | 44 ++++-- worlds/sm64ex/Regions.py | 155 ++++++++++++-------- worlds/sm64ex/Rules.py | 106 +++++++------ worlds/sm64ex/__init__.py | 25 ++-- 6 files changed, 235 insertions(+), 150 deletions(-) diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index a0808fb444..d19c9d5ee6 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -272,7 +272,7 @@ Super Mario 64: StrictCapRequirements: true StrictCannonRequirements: true StarsToFinish: 70 - ExtraStars: 30 + AmountOfStars: 70 DeathLink: true BuddyChecks: true AreaRandomizer: true diff --git a/worlds/sm64ex/Locations.py b/worlds/sm64ex/Locations.py index 5aee0189bf..f33c4306a1 100644 --- a/worlds/sm64ex/Locations.py +++ b/worlds/sm64ex/Locations.py @@ -182,26 +182,50 @@ loc100Coin_table = { "RR: 100 Coins": 3626104 } +locPSS_table = { + "The Princess's Secret Slide Block": 3626126, + "The Princess's Secret Slide Fast": 3626127, +} + +locSA_table = { + "The Secret Aquarium": 3626161 +} + locBitDW_table = { "Bowser in the Dark World Red Coins": 3626105, "Bowser in the Dark World Key": 3626178 } +locTotWC_table = { + "Tower of the Wing Cap Switch": 3626181, + "Tower of the Wing Cap Red Coins": 3626140 +} + +locCotMC_table = { + "Cavern of the Metal Cap Switch": 3626182, + "Cavern of the Metal Cap Red Coins": 3626133 +} + +locVCutM_table = { + "Vanish Cap Under the Moat Switch": 3626183, + "Vanish Cap Under the Moat Red Coins": 3626147 +} + locBitFS_table = { "Bowser in the Fire Sea Red Coins": 3626112, "Bowser in the Fire Sea Key": 3626179 } -#Secret Stars and Stages +locWMotR_table = { + "Wing Mario Over the Rainbow": 3626154 +} + +locBitS_table = { + "Bowser in the Sky Red Coins": 3626119 +} + +#Secret Stars found inside the Castle locSS_table = { - "Bowser in the Sky Red Coins": 3626119, - "The Princess's Secret Slide Block": 3626126, - "The Princess's Secret Slide Fast": 3626127, - "Cavern of the Metal Cap Red Coins": 3626133, - "Tower of the Wing Cap Red Coins": 3626140, - "Vanish Cap Under the Moat Red Coins": 3626147, - "Wing Mario Over the Rainbow": 3626154, - "The Secret Aquarium": 3626161, "Toad (Basement)": 3626168, "Toad (Second Floor)": 3626169, "Toad (Third Floor)": 3626170, @@ -209,15 +233,10 @@ locSS_table = { "MIPS 2": 3626172 } -#Caps -locCap_table = { - "Tower of the Wing Cap Switch": 3626181, - "Cavern of the Metal Cap Switch": 3626182, - "Vanish Cap Under the Moat Switch": 3626183 -} - # Correspond to 3626000 + course index * 7 + star index, then secret stars, then keys, then 100 Coin Stars location_table = {**locBoB_table,**locWhomp_table,**locJRB_table,**locCCM_table,**locBBH_table, \ **locHMC_table,**locLLL_table,**locSSL_table,**locDDD_table,**locSL_table, \ **locWDW_table,**locTTM_table,**locTHI_table,**locTTC_table,**locRR_table, \ - **loc100Coin_table,**locBitDW_table,**locBitFS_table,**locSS_table,**locCap_table} \ No newline at end of file + **loc100Coin_table,**locPSS_table,**locSA_table,**locBitDW_table,**locTotWC_table, \ + **locCotMC_table, **locVCutM_table, **locBitFS_table, **locWMotR_table, **locBitS_table, \ + **locSS_table} \ No newline at end of file diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index bddfc3fb31..594b0561c0 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink +from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice class EnableCoinStars(DefaultOnToggle): """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything""" @@ -16,19 +16,31 @@ class StrictCannonRequirements(DefaultOnToggle): class FirstBowserStarDoorCost(Range): """How many stars are required at the Star Door to Bowser in the Dark World""" range_start = 0 - range_end = 20 + range_end = 50 default = 8 class BasementStarDoorCost(Range): """How many stars are required at the Star Door in the Basement""" range_start = 0 - range_end = 50 + range_end = 70 default = 30 class SecondFloorStarDoorCost(Range): """How many stars are required to access the third floor""" range_start = 0 - range_end = 50 + range_end = 90 + default = 50 + +class MIPS1Cost(Range): + """How many stars are required to spawn MIPS the first time""" + range_start = 0 + range_end = 40 + default = 15 + +class MIPS2Cost(Range): + """How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost""" + range_start = 0 + range_end = 80 default = 50 class StarsToFinish(Range): @@ -38,15 +50,19 @@ class StarsToFinish(Range): range_end = 100 default = 70 -class ExtraStars(Range): - """How many stars exist beyond those set for StarsToFinish""" - range_start = 0 - range_end = 50 - default = 50 +class AmountOfStars(Range): + """How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set""" + range_start = 35 + range_end = 120 + default = 120 -class AreaRandomizer(Toggle): - """Randomize Entrances to Courses""" - display_name = "Course Randomizer" +class AreaRandomizer(Choice): + """Randomize Entrances""" + display_name = "Entrance Randomizer" + alias_false = 0 + option_Off = 0 + option_Courses_Only = 1 + option_Courses_and_Secrets = 2 class BuddyChecks(Toggle): """Bob-omb Buddies are checks, Cannon Unlocks are items""" @@ -60,13 +76,15 @@ sm64_options: typing.Dict[str,type(Option)] = { "AreaRandomizer": AreaRandomizer, "ProgressiveKeys": ProgressiveKeys, "EnableCoinStars": EnableCoinStars, + "AmountOfStars": AmountOfStars, "StrictCapRequirements": StrictCapRequirements, "StrictCannonRequirements": StrictCannonRequirements, "FirstBowserStarDoorCost": FirstBowserStarDoorCost, "BasementStarDoorCost": BasementStarDoorCost, "SecondFloorStarDoorCost": SecondFloorStarDoorCost, + "MIPS1Cost": MIPS1Cost, + "MIPS2Cost": MIPS2Cost, "StarsToFinish": StarsToFinish, - "ExtraStars": ExtraStars, "death_link": DeathLink, "BuddyChecks": BuddyChecks, } \ No newline at end of file diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index d9a314dfff..f8e856a9f6 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -4,150 +4,168 @@ from .Locations import SM64Location, location_table, locBoB_table, locWhomp_tabl locBBH_table, \ locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \ locWDW_table, locTTM_table, locTHI_table, locTTC_table, locRR_table, \ - locBitDW_table, locBitFS_table, locSS_table, locCap_table + locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \ + locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table +# List of all courses, including secrets, without BitS as that one is static sm64courses = ["Bob-omb Battlefield", "Whomp's Fortress", "Jolly Roger Bay", "Cool, Cool Mountain", "Big Boo's Haunt", "Hazy Maze Cave", "Lethal Lava Land", "Shifting Sand Land", "Dire, Dire Docks", "Snowman's Land", - "Wet-Dry World", - "Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride"] + "Wet-Dry World", "Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride", + "The Princess's Secret Slide", "The Secret Aquarium", "Bowser in the Dark World", "Tower of the Wing Cap", + "Cavern of the Metal Cap", "Vanish Cap under the Moat", "Bowser in the Fire Sea", "Wing Mario over the Rainbow"] -# sm64paintings is list of strings for quick reference for Painting IDs (NOT warp node IDs!) -sm64paintings = ["BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", "WDW", "TTM", "THI Tiny", "THI Huge", "TTC", "RR"] +# sm64paintings is list of entrances, format LEVEL | AREA. String Reference below +sm64paintings = [91,241,121,51,41,71,221,81,231,101,111,361,132,131,141,151] +sm64paintings_s = ["BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", "WDW", "TTM", "THI Tiny", "THI Huge", "TTC", "RR"] +# sm64secrets is list of secret areas +sm64secrets = [271, 201, 171, 291, 281, 181, 191, 311] +sm64secrets_s = ["PSS", "SA", "BitDW", "TOTWC", "COTMC", "VCUTM", "BitFS", "WMOTR"] + +sm64entrances = sm64paintings + sm64secrets +sm64entrances_s = sm64paintings_s + sm64secrets_s +sm64_internalloc_to_string = dict(zip(sm64paintings+sm64secrets, sm64entrances_s)) +sm64_internalloc_to_regionid = dict(zip(sm64paintings+sm64secrets, list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))))) def create_regions(world: MultiWorld, player: int): regSS = Region("Menu", RegionType.Generic, "Castle Area", player, world) - locSS_names = [name for name, id in locSS_table.items()] - locSS_names += [name for name, id in locCap_table.items()] - regSS.locations += [SM64Location(player, loc_name, location_table[loc_name], regSS) for loc_name in locSS_names] + create_default_locs(regSS, locSS_table, player) world.regions.append(regSS) - regBoB = Region("Bob-omb Battlefield", RegionType.Generic, "Bob-omb Battlefield", player, world) - locBoB_names = [name for name, id in locBoB_table.items()] - regBoB.locations += [SM64Location(player, loc_name, location_table[loc_name], regBoB) for loc_name in locBoB_names] + regBoB = create_region("Bob-omb Battlefield", player, world) + create_default_locs(regBoB, locBoB_table, player) if (world.EnableCoinStars[player].value): regBoB.locations.append(SM64Location(player, "BoB: 100 Coins", location_table["BoB: 100 Coins"], regBoB)) world.regions.append(regBoB) - regWhomp = Region("Whomp's Fortress", RegionType.Generic, "Whomp's Fortress", player, world) - locWhomp_names = [name for name, id in locWhomp_table.items()] - regWhomp.locations += [SM64Location(player, loc_name, location_table[loc_name], regWhomp) for loc_name in - locWhomp_names] + regWhomp = create_region("Whomp's Fortress", player, world) + create_default_locs(regWhomp, locWhomp_table, player) if (world.EnableCoinStars[player].value): regWhomp.locations.append(SM64Location(player, "WF: 100 Coins", location_table["WF: 100 Coins"], regWhomp)) world.regions.append(regWhomp) - regJRB = Region("Jolly Roger Bay", RegionType.Generic, "Jolly Roger Bay", player, world) - locJRB_names = [name for name, id in locJRB_table.items()] - regJRB.locations += [SM64Location(player, loc_name, location_table[loc_name], regJRB) for loc_name in locJRB_names] + regJRB = create_region("Jolly Roger Bay", player, world) + create_default_locs(regJRB, locJRB_table, player) if (world.EnableCoinStars[player].value): regJRB.locations.append(SM64Location(player, "JRB: 100 Coins", location_table["JRB: 100 Coins"], regJRB)) world.regions.append(regJRB) - regCCM = Region("Cool, Cool Mountain", RegionType.Generic, "Cool, Cool Mountain", player, world) - locCCM_names = [name for name, id in locCCM_table.items()] - regCCM.locations += [SM64Location(player, loc_name, location_table[loc_name], regCCM) for loc_name in locCCM_names] + regCCM = create_region("Cool, Cool Mountain", player, world) + create_default_locs(regCCM, locCCM_table, player) if (world.EnableCoinStars[player].value): regCCM.locations.append(SM64Location(player, "CCM: 100 Coins", location_table["CCM: 100 Coins"], regCCM)) world.regions.append(regCCM) - regBBH = Region("Big Boo's Haunt", RegionType.Generic, "Big Boo's Haunt", player, world) - locBBH_names = [name for name, id in locBBH_table.items()] - regBBH.locations += [SM64Location(player, loc_name, location_table[loc_name], regBBH) for loc_name in locBBH_names] + regBBH = create_region("Big Boo's Haunt", player, world) + create_default_locs(regBBH, locBBH_table, player) if (world.EnableCoinStars[player].value): regBBH.locations.append(SM64Location(player, "BBH: 100 Coins", location_table["BBH: 100 Coins"], regBBH)) world.regions.append(regBBH) - regBitDW = Region("Bowser in the Dark World", RegionType.Generic, "Bowser in the Dark World", player, world) - locBitDW_names = [name for name, id in locBitDW_table.items()] - regBitDW.locations += [SM64Location(player, loc_name, location_table[loc_name], regBitDW) for loc_name in - locBitDW_names] + regPSS = create_region("The Princess's Secret Slide", player, world) + create_default_locs(regPSS, locPSS_table, player) + world.regions.append(regPSS) + + regSA = create_region("The Secret Aquarium", player, world) + create_default_locs(regSA, locSA_table, player) + world.regions.append(regSA) + + regTotWC = create_region("Tower of the Wing Cap", player, world) + create_default_locs(regTotWC, locTotWC_table, player) + world.regions.append(regTotWC) + + regBitDW = create_region("Bowser in the Dark World", player, world) + create_default_locs(regBitDW, locBitDW_table, player) world.regions.append(regBitDW) - regBasement = Region("Basement", RegionType.Generic, "Basement", player, world) + regBasement = create_region("Basement", player, world) world.regions.append(regBasement) - regHMC = Region("Hazy Maze Cave", RegionType.Generic, "Hazy Maze Cave", player, world) - locHMC_names = [name for name, id in locHMC_table.items()] - regHMC.locations += [SM64Location(player, loc_name, location_table[loc_name], regHMC) for loc_name in locHMC_names] + regHMC = create_region("Hazy Maze Cave", player, world) + create_default_locs(regHMC, locHMC_table, player) if (world.EnableCoinStars[player].value): regHMC.locations.append(SM64Location(player, "HMC: 100 Coins", location_table["HMC: 100 Coins"], regHMC)) world.regions.append(regHMC) - regLLL = Region("Lethal Lava Land", RegionType.Generic, "Lethal Lava Land", player, world) - locLLL_names = [name for name, id in locLLL_table.items()] - regLLL.locations += [SM64Location(player, loc_name, location_table[loc_name], regLLL) for loc_name in locLLL_names] + regLLL = create_region("Lethal Lava Land", player, world) + create_default_locs(regLLL, locLLL_table, player) if (world.EnableCoinStars[player].value): regLLL.locations.append(SM64Location(player, "LLL: 100 Coins", location_table["LLL: 100 Coins"], regLLL)) world.regions.append(regLLL) - regSSL = Region("Shifting Sand Land", RegionType.Generic, "Shifting Sand Land", player, world) - locSSL_names = [name for name, id in locSSL_table.items()] - regSSL.locations += [SM64Location(player, loc_name, location_table[loc_name], regSSL) for loc_name in locSSL_names] + regSSL = create_region("Shifting Sand Land", player, world) + create_default_locs(regSSL, locSSL_table, player) if (world.EnableCoinStars[player].value): regSSL.locations.append(SM64Location(player, "SSL: 100 Coins", location_table["SSL: 100 Coins"], regSSL)) world.regions.append(regSSL) - regDDD = Region("Dire, Dire Docks", RegionType.Generic, "Dire, Dire Docks", player, world) - locDDD_names = [name for name, id in locDDD_table.items()] - regDDD.locations += [SM64Location(player, loc_name, location_table[loc_name], regDDD) for loc_name in locDDD_names] + regDDD = create_region("Dire, Dire Docks", player, world) + create_default_locs(regDDD, locDDD_table, player) if (world.EnableCoinStars[player].value): regDDD.locations.append(SM64Location(player, "DDD: 100 Coins", location_table["DDD: 100 Coins"], regDDD)) world.regions.append(regDDD) - regBitFS = Region("Bowser in the Fire Sea", RegionType.Generic, "Bowser in the Fire Sea", player, world) - locBitFS_names = [name for name, id in locBitFS_table.items()] - regBitFS.locations += [SM64Location(player, loc_name, location_table[loc_name], regBitFS) for loc_name in - locBitFS_names] + regCotMC = create_region("Cavern of the Metal Cap", player, world) + create_default_locs(regCotMC, locCotMC_table, player) + world.regions.append(regCotMC) + + regVCutM = create_region("Vanish Cap under the Moat", player, world) + create_default_locs(regVCutM, locVCutM_table, player) + world.regions.append(regVCutM) + + regBitFS = create_region("Bowser in the Fire Sea", player, world) + create_default_locs(regBitFS, locBitFS_table, player) world.regions.append(regBitFS) - regFloor2 = Region("Second Floor", RegionType.Generic, "Second Floor", player, world) + regFloor2 = create_region("Second Floor", player, world) world.regions.append(regFloor2) - regSL = Region("Snowman's Land", RegionType.Generic, "Snowman's Land", player, world) - locSL_names = [name for name, id in locSL_table.items()] - regSL.locations += [SM64Location(player, loc_name, location_table[loc_name], regSL) for loc_name in locSL_names] + regSL = create_region("Snowman's Land", player, world) + create_default_locs(regSL, locSL_table, player) if (world.EnableCoinStars[player].value): regSL.locations.append(SM64Location(player, "SL: 100 Coins", location_table["SL: 100 Coins"], regSL)) world.regions.append(regSL) - regWDW = Region("Wet-Dry World", RegionType.Generic, "Wet-Dry World", player, world) - locWDW_names = [name for name, id in locWDW_table.items()] - regWDW.locations += [SM64Location(player, loc_name, location_table[loc_name], regWDW) for loc_name in locWDW_names] + regWDW = create_region("Wet-Dry World", player, world) + create_default_locs(regWDW, locWDW_table, player) if (world.EnableCoinStars[player].value): regWDW.locations.append(SM64Location(player, "WDW: 100 Coins", location_table["WDW: 100 Coins"], regWDW)) world.regions.append(regWDW) - regTTM = Region("Tall, Tall Mountain", RegionType.Generic, "Tall, Tall Mountain", player, world) - locTTM_names = [name for name, id in locTTM_table.items()] - regTTM.locations += [SM64Location(player, loc_name, location_table[loc_name], regTTM) for loc_name in locTTM_names] + regTTM = create_region("Tall, Tall Mountain", player, world) + create_default_locs(regTTM, locTTM_table, player) if (world.EnableCoinStars[player].value): regTTM.locations.append(SM64Location(player, "TTM: 100 Coins", location_table["TTM: 100 Coins"], regTTM)) world.regions.append(regTTM) - regTHI = Region("Tiny-Huge Island", RegionType.Generic, "Tiny-Huge Island", player, world) - locTHI_names = [name for name, id in locTHI_table.items()] - regTHI.locations += [SM64Location(player, loc_name, location_table[loc_name], regTHI) for loc_name in locTHI_names] + regTHI = create_region("Tiny-Huge Island", player, world) + create_default_locs(regTHI, locTHI_table, player) if (world.EnableCoinStars[player].value): regTHI.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHI)) world.regions.append(regTHI) - regFloor3 = Region("Third Floor", RegionType.Generic, "Third Floor", player, world) + regFloor3 = create_region("Third Floor", player, world) world.regions.append(regFloor3) - regTTC = Region("Tick Tock Clock", RegionType.Generic, "Tick Tock Clock", player, world) - locTTC_names = [name for name, id in locTTC_table.items()] - regTTC.locations += [SM64Location(player, loc_name, location_table[loc_name], regTTC) for loc_name in locTTC_names] + regTTC = create_region("Tick Tock Clock", player, world) + create_default_locs(regTTC, locTTC_table, player) if (world.EnableCoinStars[player].value): regTTC.locations.append(SM64Location(player, "TTC: 100 Coins", location_table["TTC: 100 Coins"], regTTC)) world.regions.append(regTTC) - regRR = Region("Rainbow Ride", RegionType.Generic, "Rainbow Ride", player, world) - locRR_names = [name for name, id in locRR_table.items()] - regRR.locations += [SM64Location(player, loc_name, location_table[loc_name], regRR) for loc_name in locRR_names] + regRR = create_region("Rainbow Ride", player, world) + create_default_locs(regRR, locRR_table, player) if (world.EnableCoinStars[player].value): regRR.locations.append(SM64Location(player, "RR: 100 Coins", location_table["RR: 100 Coins"], regRR)) world.regions.append(regRR) + regWMotR = create_region("Wing Mario over the Rainbow", player, world) + create_default_locs(regWMotR, locWMotR_table, player) + world.regions.append(regWMotR) + + regBitS = create_region("Bowser in the Sky", player, world) + create_default_locs(regBitS, locBitS_table, player) + world.regions.append(regBitS) + def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None): sourceRegion = world.get_region(source, player) @@ -159,3 +177,10 @@ def connect_regions(world: MultiWorld, player: int, source: str, target: str, ru sourceRegion.exits.append(connection) connection.connect(targetRegion) + +def create_region(name: str, player: int, world: MultiWorld) -> Region: + return Region(name, RegionType.Generic, name, player, world) + +def create_default_locs(reg: Region, locs, player): + reg_names = [name for name, id in locs.items()] + reg.locations += [SM64Location(player, loc_name, location_table[loc_name], reg) for loc_name in locs] diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 6dc6e84964..a4a82b2737 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -1,56 +1,76 @@ from ..generic.Rules import add_rule -from .Regions import connect_regions, sm64courses, sm64paintings +from .Regions import connect_regions, sm64courses, sm64paintings, sm64secrets, sm64entrances +def fix_reg(entrance_ids, reg, invalidspot, swaplist, world): + if entrance_ids.index(reg) == invalidspot: # Unlucky :C + swaplist.remove(invalidspot) + rand = world.random.choice(swaplist) + entrance_ids[invalidspot], entrance_ids[rand] = entrance_ids[rand], entrance_ids[invalidspot] + swaplist.append(invalidspot) + swaplist.remove(rand) def set_rules(world, player: int, area_connections): - entrance_ids = list(range(len(sm64paintings))) - destination_courses = list(range(13)) + [12,13,14] # Two instances of Destination Course THI - if world.AreaRandomizer[player]: + destination_regions = list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))) # Two instances of Destination Course THI. Past normal course idx are secret regions + if world.AreaRandomizer[player].value == 0: + entrance_ids = list(range(len(sm64paintings + sm64secrets))) + if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses + entrance_ids = list(range(len(sm64paintings))) world.random.shuffle(entrance_ids) - temp_assign = dict(zip(entrance_ids,destination_courses)) # Used for Rules only + entrance_ids = entrance_ids + list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets))) + if world.AreaRandomizer[player].value == 2: # Secret Regions as well + world.random.shuffle(entrance_ids) + # Guarantee first entrance is a course + swaplist = list(range(len(entrance_ids))) + if entrance_ids.index(0) > 15: # Unlucky :C + rand = world.random.randint(0,15) + entrance_ids[entrance_ids.index(0)], entrance_ids[rand] = entrance_ids[rand], entrance_ids[entrance_ids.index(0)] + swaplist.remove(entrance_ids.index(0)) + # Guarantee COTMC is not mapped to HMC, cuz thats impossible + fix_reg(entrance_ids, 20, 5, swaplist, world) + # Guarantee BITFS is not mapped to DDD + fix_reg(entrance_ids, 22, 8, swaplist, world) + temp_assign = dict(zip(entrance_ids,destination_regions)) # Used for Rules only - # Destination Format: LVL | AREA with LVL = Course ID, 0-indexed, AREA = Area as used in sm64 code - area_connections.update({entrance: (destination_course*10 + 1) for entrance, destination_course in temp_assign.items()}) - for i in range(len(area_connections)): - if (int(area_connections[i]/10) == 12): - # Change first occurence of course 12 (THI) to Area 2 (THI Tiny) - area_connections[i] = 12*10 + 2 - break - - connect_regions(world, player, "Menu", sm64courses[temp_assign[0]]) - connect_regions(world, player, "Menu", sm64courses[temp_assign[1]], lambda state: state.has("Power Star", player, 1)) - connect_regions(world, player, "Menu", sm64courses[temp_assign[2]], lambda state: state.has("Power Star", player, 3)) - connect_regions(world, player, "Menu", sm64courses[temp_assign[3]], lambda state: state.has("Power Star", player, 3)) - connect_regions(world, player, "Menu", "Bowser in the Dark World", lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) - connect_regions(world, player, "Menu", sm64courses[temp_assign[4]], lambda state: state.has("Power Star", player, 12)) + # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code + area_connections.update({sm64entrances[entrance]: destination for entrance, destination in zip(entrance_ids,sm64entrances)}) + + connect_regions(world, player, "Menu", sm64courses[temp_assign[0]]) # BOB + connect_regions(world, player, "Menu", sm64courses[temp_assign[1]], lambda state: state.has("Power Star", player, 1)) # WF + connect_regions(world, player, "Menu", sm64courses[temp_assign[2]], lambda state: state.has("Power Star", player, 3)) # JRB + connect_regions(world, player, "Menu", sm64courses[temp_assign[3]], lambda state: state.has("Power Star", player, 3)) # CCM + connect_regions(world, player, "Menu", sm64courses[temp_assign[4]], lambda state: state.has("Power Star", player, 12)) # BBH + connect_regions(world, player, "Menu", sm64courses[temp_assign[16]], lambda state: state.has("Power Star", player, 1)) # PSS + connect_regions(world, player, "Menu", sm64courses[temp_assign[17]], lambda state: state.has("Power Star", player, 3)) # SA + connect_regions(world, player, "Menu", sm64courses[temp_assign[19]], lambda state: state.has("Power Star", player, 10)) # TOTWC + connect_regions(world, player, "Menu", sm64courses[temp_assign[18]], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) # BITDW connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1)) - connect_regions(world, player, "Basement", sm64courses[temp_assign[5]]) - connect_regions(world, player, "Basement", sm64courses[temp_assign[6]]) - connect_regions(world, player, "Basement", sm64courses[temp_assign[7]]) - connect_regions(world, player, "Basement", sm64courses[temp_assign[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) - connect_regions(world, player, "Basement", "Bowser in the Fire Sea", lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and - state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + connect_regions(world, player, "Basement", sm64courses[temp_assign[5]]) # HMC + connect_regions(world, player, "Basement", sm64courses[temp_assign[6]]) # LLL + connect_regions(world, player, "Basement", sm64courses[temp_assign[7]]) # SSL + connect_regions(world, player, "Basement", sm64courses[temp_assign[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) # DDD + connect_regions(world, player, "Hazy Maze Cave", sm64courses[temp_assign[20]]) # COTMC + connect_regions(world, player, "Basement", sm64courses[temp_assign[21]]) # VCUTM + connect_regions(world, player, "Basement", sm64courses[temp_assign[22]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and + state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) # BITFS connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) - connect_regions(world, player, "Second Floor", sm64courses[temp_assign[9]]) - connect_regions(world, player, "Second Floor", sm64courses[temp_assign[10]]) - connect_regions(world, player, "Second Floor", sm64courses[temp_assign[11]]) + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[9]]) # SL + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[10]]) # WDW + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[11]]) # TTM connect_regions(world, player, "Second Floor", sm64courses[temp_assign[12]]) # THI Tiny connect_regions(world, player, "Second Floor", sm64courses[temp_assign[13]]) # THI Huge connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value)) - connect_regions(world, player, "Third Floor", sm64courses[temp_assign[14]]) - connect_regions(world, player, "Third Floor", sm64courses[temp_assign[15]]) + connect_regions(world, player, "Third Floor", sm64courses[temp_assign[14]]) # TTC + connect_regions(world, player, "Third Floor", sm64courses[temp_assign[15]]) # RR + connect_regions(world, player, "Third Floor", sm64courses[temp_assign[23]]) # WMOTR + connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value)) # BITS #Special Rules for some Locations - add_rule(world.get_location("Tower of the Wing Cap Switch", player), lambda state: state.has("Power Star", player, 10)) - add_rule(world.get_location("Cavern of the Metal Cap Switch", player), lambda state: state.can_reach("Hazy Maze Cave", 'Region', player)) - add_rule(world.get_location("Vanish Cap Under the Moat Switch", player), lambda state: state.can_reach("Basement", 'Region', player)) - add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player)) add_rule(world.get_location("BBH: Eye to Eye in the Secret Room", player), lambda state: state.has("Vanish Cap", player)) add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Vanish Cap", player)) @@ -89,18 +109,14 @@ def set_rules(world, player: int, area_connections): add_rule(world.get_location("BoB: 100 Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) #Rules for Secret Stars - add_rule(world.get_location("Bowser in the Sky Red Coins", player), lambda state: state.can_reach("Third Floor", 'Region',player) and state.has("Power Star", player, world.StarsToFinish[player].value)) - add_rule(world.get_location("The Princess's Secret Slide Block", player), lambda state: state.has("Power Star", player, 1)) - add_rule(world.get_location("The Princess's Secret Slide Fast", player), lambda state: state.has("Power Star", player, 1)) - add_rule(world.get_location("Cavern of the Metal Cap Red Coins", player), lambda state: state.can_reach("Cavern of the Metal Cap Switch", 'Location', player)) - add_rule(world.get_location("Tower of the Wing Cap Red Coins", player), lambda state: state.can_reach("Tower of the Wing Cap Switch", 'Location', player)) - add_rule(world.get_location("Vanish Cap Under the Moat Red Coins", player), lambda state: state.can_reach("Vanish Cap Under the Moat Switch", 'Location', player)) - add_rule(world.get_location("Wing Mario Over the Rainbow", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Wing Cap", player)) - add_rule(world.get_location("The Secret Aquarium", player), lambda state: state.has("Power Star", player, 3)) + add_rule(world.get_location("Wing Mario Over the Rainbow", player), lambda state: state.has("Wing Cap", player)) add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 12)) add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor", 'Region', player) and state.has("Power Star", player, 25)) add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35)) - add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 15)) - add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 50)) - world.completion_condition[player] = lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, world.StarsToFinish[player].value) + if world.MIPS1Cost[player].value > world.MIPS2Cost[player].value: + world.MIPS2Cost[player].value = world.MIPS1Cost[player].value + add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value)) + add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value)) + + world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index bcf1bf2a8a..401a2d683b 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -5,7 +5,7 @@ from .Items import item_table, cannon_item_table, SM64Item from .Locations import location_table, SM64Location from .Options import sm64_options from .Rules import set_rules -from .Regions import create_regions, sm64courses, sm64paintings +from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internalloc_to_string, sm64_internalloc_to_regionid from BaseClasses import Item, Tutorial, ItemClassification from ..AutoWorld import World, WebWorld @@ -54,10 +54,10 @@ class SM64World(World): set_rules(self.world, self.player, self.area_connections) if self.topology_present: # Write area_connections to spoiler log - for painting_id, destination in self.area_connections.items(): + for entrance, destination in self.area_connections.items(): self.world.spoiler.set_entrance( - sm64paintings[painting_id] + " Painting", - sm64courses[destination // 10], + sm64_internalloc_to_string[entrance] + " Entrance", + sm64_internalloc_to_string[destination], 'entrance', self.player) def create_item(self, name: str) -> Item: @@ -74,9 +74,13 @@ class SM64World(World): def generate_basic(self): staritem = self.create_item("Power Star") - starcount = min(self.world.StarsToFinish[self.player].value + self.world.ExtraStars[self.player].value,120) + starcount = self.world.AmountOfStars[self.player].value if (not self.world.EnableCoinStars[self.player].value): - starcount = max(starcount - 15,self.world.StarsToFinish[self.player].value) + starcount = max(35,self.world.AmountOfStars[self.player].value-15) + starcount = max(starcount, self.world.FirstBowserStarDoorCost[self.player].value, + self.world.BasementStarDoorCost[self.player].value, self.world.SecondFloorStarDoorCost[self.player].value, + self.world.MIPS1Cost[self.player].value, self.world.MIPS2Cost[self.player].value, + self.world.StarsToFinish[self.player].value) self.world.itempool += [staritem for i in range(0,starcount)] mushroomitem = self.create_item("1Up Mushroom") self.world.itempool += [mushroomitem for i in range(starcount,120 - (15 if not self.world.EnableCoinStars[self.player].value else 0))] @@ -117,6 +121,8 @@ class SM64World(World): "FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[self.player].value, "BasementDoorCost": self.world.BasementStarDoorCost[self.player].value, "SecondFloorCost": self.world.SecondFloorStarDoorCost[self.player].value, + "MIPS1Cost": self.world.MIPS1Cost[self.player].value, + "MIPS2Cost": self.world.MIPS2Cost[self.player].value, "StarsToFinish": self.world.StarsToFinish[self.player].value, "DeathLink": self.world.death_link[self.player].value, } @@ -145,8 +151,9 @@ class SM64World(World): def modify_multidata(self, multidata): if self.topology_present: er_hint_data = {} - for painting_id, destination in self.area_connections.items(): - region = self.world.get_region(sm64courses[destination // 10], self.player) + for entrance, destination in self.area_connections.items(): + regionid = sm64_internalloc_to_regionid[destination] + region = self.world.get_region(sm64courses[regionid], self.player) for location in region.locations: - er_hint_data[location.address] = sm64paintings[painting_id] + er_hint_data[location.address] = sm64_internalloc_to_string[entrance] multidata['er_hint_data'][self.player] = er_hint_data From 41883e44e7b2141e38e8bbcfb280adb0dd06836c Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Mon, 25 Jul 2022 15:34:31 -0400 Subject: [PATCH 048/138] DKC3 - Logic Softlock Fix (#817) * Add two locations to Trade Sequence List * Remove trace sequence locations from ROM data dict --- worlds/dkc3/Regions.py | 16 ++++++++++------ worlds/dkc3/Rom.py | 4 ++-- worlds/dkc3/__init__.py | 4 ++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/worlds/dkc3/Regions.py b/worlds/dkc3/Regions.py index 6cb01e4f18..501f1a0ea4 100644 --- a/worlds/dkc3/Regions.py +++ b/worlds/dkc3/Regions.py @@ -501,9 +501,11 @@ def create_regions(world, player: int, active_locations): bounty_bay_region = create_region(world, player, active_locations, LocationName.bounty_bay_region, bounty_bay_region_locations, None) - sky_high_secret_region_locations = { - LocationName.sky_high_secret: [0x64B, 1], - } + sky_high_secret_region_locations = {} + if False:#world.include_trade_sequence[player]: + sky_high_secret_region_locations.update({ + LocationName.sky_high_secret: [0x64B, 1], + }) sky_high_secret_region = create_region(world, player, active_locations, LocationName.sky_high_secret_region, sky_high_secret_region_locations, None) @@ -513,9 +515,11 @@ def create_regions(world, player: int, active_locations): glacial_grotto_region = create_region(world, player, active_locations, LocationName.glacial_grotto_region, glacial_grotto_region_locations, None) - cifftop_cache_region_locations = { - LocationName.cifftop_cache: [0x64D, 1], - } + cifftop_cache_region_locations = {} + if False:#world.include_trade_sequence[player]: + cifftop_cache_region_locations.update({ + LocationName.cifftop_cache: [0x64D, 1], + }) cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region, cifftop_cache_region_locations, None) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 761161ee83..821143090b 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -238,9 +238,9 @@ location_rom_data = { 0xDC30AF: [0x648, 1], 0xDC30B0: [0x649, 1], 0xDC30B1: [0x64A, 1], - 0xDC30B2: [0x64B, 1], + #0xDC30B2: [0x64B, 1], # Disabled until Trade Sequence 0xDC30B3: [0x64C, 1], - 0xDC30B4: [0x64D, 1], + #0xDC30B4: [0x64D, 1], # Disabled until Trade Sequence 0xDC30B5: [0x64E, 1], 0xDC30B6: [0x5FD, 4], # Banana Bird Mother diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index 54087db9aa..d9e73a7ec3 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -82,7 +82,7 @@ class DKC3World(World): itempool: typing.List[DKC3Item] = [] # Levels - total_required_locations = 161 + total_required_locations = 159 number_of_banana_birds = 0 # Rocket Rush Cog @@ -105,7 +105,7 @@ class DKC3World(World): ## Brothers Bear if False:#self.world.include_trade_sequence[self.player]: - total_required_locations += 8 + total_required_locations += 10 number_of_bonus_coins = (self.world.krematoa_bonus_coin_cost[self.player] * 5) number_of_bonus_coins += math.ceil((85 - number_of_bonus_coins) * self.world.percentage_of_extra_bonus_coins[self.player] / 100) From ce536fa3ac2a2e2d80e68ff548c33ec42015a7cc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 21 Jul 2022 13:38:56 +0200 Subject: [PATCH 049/138] Subnautica: fix Multipurpose Room not acquirable in valuable item pool BaseRoomFragment doesn't exist in vanilla, so when valuable item pool marked it as scannable in vanilla location it did not work, as it's technically BaseRoom BaseRoom is also required to install other modules into, modules that are already marked as useful, so logically if it's required for other useful stuff it should also be marked as useful By switching from Fragment to non-fragment one now needs 1 out of 2 instead of 2 out of 2 items, which I consider a plus as well. --- worlds/subnautica/Items.py | 6 +++--- worlds/subnautica/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index f0d236623a..0f05d5e31a 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -222,10 +222,10 @@ item_table: Dict[int, ItemDict] = { 'count': 2, 'name': 'Observatory Fragment', 'tech_type': 'BaseObservatoryFragment'}, - 35053: {'classification': ItemClassification.filler, + 35053: {'classification': ItemClassification.useful, 'count': 2, - 'name': 'Multipurpose Room Fragment', - 'tech_type': 'BaseRoomFragment'}, + 'name': 'Multipurpose Room', + 'tech_type': 'BaseRoom'}, 35054: {'classification': ItemClassification.useful, 'count': 2, 'name': 'Bulkhead Fragment', diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index be709a1c30..f36149b5ad 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -41,7 +41,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options = Options.options - data_version = 4 + data_version = 5 required_client_version = (0, 3, 3) prefill_items: List[Item] From 3b2037a2d4020ffc7fdcc1842a594479bb99239f Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 25 Jul 2022 16:19:07 -0400 Subject: [PATCH 050/138] HK - focus location (#778) --- worlds/hk/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index af0e54e237..c07d995ecd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -254,6 +254,9 @@ class HKWorld(World): if location_name == "Start": if item_name in randomized_starting_items: + if item_name == "Focus": + self.create_location("Focus") + unfilled_locations += 1 pool.append(item) else: self.world.push_precollected(item) @@ -502,6 +505,7 @@ class HKWorld(World): location.place_locked_item(item) if costs: location.costs = costs.pop() + return location def collect(self, state, item: HKItem) -> bool: change = super(HKWorld, self).collect(state, item) From 288a623ab6061852b8d5b55e481f4067c4015042 Mon Sep 17 00:00:00 2001 From: Ludovic Marechal Date: Tue, 26 Jul 2022 09:31:16 +0200 Subject: [PATCH 051/138] Update ds3 locations and items (#819) * DS3: Add more rules to avoid softlocks, remove Path of the Dragon gesture location/item and remove useless comments * DS3: Add more Hostile NPCs locations/items * DS3: Add missing key items to the key items list --- worlds/dark_souls_3/__init__.py | 31 +++++++++++++++--- worlds/dark_souls_3/data/items_data.py | 19 +++++++---- worlds/dark_souls_3/data/locations_data.py | 38 ++++++++++++++-------- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index f8ed9fb0f8..0ff27acc43 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -39,7 +39,7 @@ class DarkSouls3World(World): remote_items: bool = False remote_start_inventory: bool = False web = DarkSouls3Web() - data_version = 1 + data_version = 2 base_id = 100000 item_name_to_id = {name: id for id, name in enumerate(item_dictionary_table, base_id)} location_name_to_id = {name: id for id, name in enumerate(location_dictionary_table, base_id)} @@ -167,17 +167,15 @@ class DarkSouls3World(World): # Define the access rules to the entrances set_rule(self.world.get_entrance("Goto Bell Tower", self.player), - lambda state: state.has("Mortician's Ashes", self.player)) + lambda state: state.has("Tower Key", self.player)) set_rule(self.world.get_entrance("Goto Undead Settlement", self.player), lambda state: state.has("Small Lothric Banner", self.player)) set_rule(self.world.get_entrance("Goto Lothric Castle", self.player), lambda state: state.has("Basin of Vows", self.player)) - set_rule(self.world.get_location("HWL: Soul of the Dancer", self.player), - lambda state: state.has("Basin of Vows", self.player)) set_rule(self.world.get_entrance("Goto Irithyll of the boreal", self.player), lambda state: state.has("Small Doll", self.player)) set_rule(self.world.get_entrance("Goto Archdragon peak", self.player), - lambda state: state.has("Path of the Dragon Gesture", self.player)) + lambda state: state.can_reach("CKG: Soul of Consumed Oceiros", "Location", self.player)) set_rule(self.world.get_entrance("Goto Profaned capital", self.player), lambda state: state.has("Storm Ruler", self.player)) set_rule(self.world.get_entrance("Goto Grand Archives", self.player), @@ -188,6 +186,23 @@ class DarkSouls3World(World): state.has("Cinders of a Lord - Aldrich", self.player) and state.has("Cinders of a Lord - Lothric Prince", self.player)) + # Define the access rules to some specific locations + set_rule(self.world.get_location("HWL: Soul of the Dancer", self.player), + lambda state: state.has("Basin of Vows", self.player)) + set_rule(self.world.get_location("HWL: Greirat's Ashes", self.player), + lambda state: state.has("Cell Key", self.player)) + set_rule(self.world.get_location("ID: Bellowing Dragoncrest Ring", self.player), + lambda state: state.has("Jailbreaker's Key", self.player)) + set_rule(self.world.get_location("ID: Prisoner Chief's Ashes", self.player), + lambda state: state.has("Jailer's Key Ring", self.player)) + set_rule(self.world.get_location("ID: Covetous Gold Serpent Ring", self.player), + lambda state: state.has("Old Cell Key", self.player)) + black_hand_gotthard_corpse_rule = lambda state: \ + (state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and + state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player)) + set_rule(self.world.get_location("LC: Grand Archives Key", self.player), black_hand_gotthard_corpse_rule) + set_rule(self.world.get_location("LC: Gotthard Twinswords", self.player), black_hand_gotthard_corpse_rule) + self.world.completion_condition[self.player] = lambda state: \ state.has("Cinders of a Lord - Abyss Watcher", self.player) and \ state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \ @@ -202,6 +217,12 @@ class DarkSouls3World(World): else: self.world.itempool += [item] + # Fill item pool with additional items + item_pool_len = self.item_name_to_id.__len__() + total_required_locations = self.location_name_to_id.__len__() + for i in range(item_pool_len, total_required_locations): + self.world.itempool += [self.create_item("Soul of an Intrepid Hero")] + def generate_output(self, output_directory: str): # Depending on the specified option, modify items hexadecimal value to add an upgrade level item_dictionary = item_dictionary_table.copy() diff --git a/worlds/dark_souls_3/data/items_data.py b/worlds/dark_souls_3/data/items_data.py index b7c5c3d186..9add18820b 100644 --- a/worlds/dark_souls_3/data/items_data.py +++ b/worlds/dark_souls_3/data/items_data.py @@ -51,7 +51,7 @@ weapons_upgrade_5_table = { "Twin Princes' Greatsword": 0x005FAC30, "Storm Curved Sword": 0x003E4180, "Dragonslayer Swordspear": 0x008BC540, - + "Sage's Crystal Staff": 0x00C8CE40, } weapons_upgrade_10_table = { @@ -105,6 +105,7 @@ weapons_upgrade_10_table = { "Sniper Crossbow": 0x00D83790, "Claw": 0x00A7D8C0, + "Drang Twinspears": 0x00F5AAA0, } shields_table = { @@ -125,7 +126,6 @@ shields_table = { "Dragon Crest Shield": 0x01432E60, "Shield of Want": 0x0144B500, "Black Iron Greatshield": 0x0150EA00, - "Great Magic Shield": 0x40144F38, "Greatshield of Glory": 0x01515F30, "Sacred Bloom Shield": 0x013572C0, "Golden Wing Crest Shield": 0x0143CAA0, @@ -291,6 +291,7 @@ rings_table = { "Knight's Ring": 0x20004FEC, "Red Tearstone Ring": 0x20004ECA, "Dragonscale Ring": 0x2000515E, + "Knight Slayer's Ring": 0x20005000, } spells_table = { @@ -311,10 +312,12 @@ spells_table = { "Soul Stream": 0x4018B820, "Divine Pillars of Light": 0x4038C340, "Great Magic Barrier": 0x40365628, - + "Great Magic Shield": 0x40144F38, } misc_items_table = { + "Tower Key": 0x400007DF, + "Grave Key": 0x400007D9, "Cell Key": 0x400007DA, "Small Lothric Banner": 0x40000836, "Mortician's Ashes": 0x4000083B, @@ -349,7 +352,6 @@ misc_items_table = { "Xanthous Ashes": 0x40000864, "Old Cell Key": 0x400007DC, "Jailer's Key Ring": 0x400007D8, - "Path of the Dragon Gesture": 0x40002346, "Logan's Scroll": 0x40000855, "Storm Ruler": 0x006132D0, "Giant's Coal": 0x40000839, @@ -363,14 +365,19 @@ key_items_list = { "Small Lothric Banner", "Basin of Vows", "Small Doll", - "Path of the Dragon Gesture", "Storm Ruler", "Grand Archives Key", "Cinders of a Lord - Abyss Watcher", "Cinders of a Lord - Yhorm the Giant", "Cinders of a Lord - Aldrich", "Cinders of a Lord - Lothric Prince", - "Mortician's Ashes" + "Mortician's Ashes", + "Cell Key", + "Tower Key", + "Jailbreaker's Key", + "Prisoner Chief's Ashes", + "Old Cell Key", + "Jailer's Key Ring", } item_dictionary_table = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table} diff --git a/worlds/dark_souls_3/data/locations_data.py b/worlds/dark_souls_3/data/locations_data.py index bf85e6ebf1..384da049ac 100644 --- a/worlds/dark_souls_3/data/locations_data.py +++ b/worlds/dark_souls_3/data/locations_data.py @@ -9,8 +9,12 @@ cemetery_of_ash_table = { } fire_link_shrine_table = { - "FS: Broken Straight Sword": 0x001EF9B0, # Multiple + # "FS: Coiled Sword": 0x40000859, You can still light the Firelink Shrine fire whether you have it or not, useless + "FS: Broken Straight Sword": 0x001EF9B0, "FS: East-West Shield": 0x0142B930, + "FS: Uchigatana": 0x004C4B40, + "FS: Master's Attire": 0x148F5008, + "FS: Master's Gloves": 0x148F53F0, } firelink_shrine_bell_tower_table = { @@ -40,6 +44,7 @@ high_wall_of_lothric = { "HWL: Soul of Boreal Valley Vordt": 0x400002CF, "HWL: Soul of the Dancer": 0x400002CA, "HWL: Way of Blue Covenant": 0x2000274C, + "HWL: Greirat's Ashes": 0x4000083F, } undead_settlement_table = { @@ -53,7 +58,7 @@ undead_settlement_table = { "US: Cleric Gloves": 0x11D90D90, "US: Cleric Trousers": 0x11D91178, - "US: Mortician's Ashes": 0x4000083B, # Key item for Grave Key for Firelink Towerlocations + "US: Mortician's Ashes": 0x4000083B, "US: Caestus": 0x00A7FFD0, "US: Plank Shield": 0x01346150, "US: Flame Stoneplate Ring": 0x20004E52, @@ -99,7 +104,7 @@ road_of_sacrifice_table = { "RS: Butcher Knife": 0x006BE130, "RS: Brigand Axe": 0x006B1DE0, - "RS: Braille Divine Tome of Carim": 0x40000847, # Shop + "RS: Braille Divine Tome of Carim": 0x40000847, "RS: Morne's Ring": 0x20004F1A, "RS: Twin Dragon Greatshield": 0x01513820, "RS: Heretic's Staff": 0x00C8F550, @@ -121,12 +126,12 @@ road_of_sacrifice_table = { "RS: Conjurator Manchettes": 0x149E9630, "RS: Conjurator Boots": 0x149E9A18, - "RS: Great Swamp Pyromancy Tome": 0x4000084F, # Shop + "RS: Great Swamp Pyromancy Tome": 0x4000084F, "RS: Great Club": 0x007B4A80, "RS: Exile Greatsword": 0x005DD770, - "RS: Farron Coal ": 0x40000837, # Shop + "RS: Farron Coal ": 0x40000837, "RS: Sellsword Twinblades": 0x00F42400, "RS: Sellsword Helm": 0x11481060, @@ -147,7 +152,7 @@ road_of_sacrifice_table = { } cathedral_of_the_deep_table = { - "CD: Paladin's Ashes": 0x4000083D, #Shop + "CD: Paladin's Ashes": 0x4000083D, "CD: Spider Shield": 0x01435570, "CD: Crest Shield": 0x01430750, "CD: Notched Whip": 0x00B7DE50, @@ -161,7 +166,7 @@ cathedral_of_the_deep_table = { "CD: Seek Guidance": 0x40360420, "CD: Aldrich's Sapphire": 0x20005096, - "CD: Deep Braille Divine Tome": 0x40000860, # Shop + "CD: Deep Braille Divine Tome": 0x40000860, "CD: Saint Bident": 0x008C1360, "CD: Maiden Hood": 0x14BD12E0, @@ -202,7 +207,7 @@ farron_keep_table = { "FK: Wolf's Blood Swordgrass": 0x4000016E, "FK: Greatsword": 0x005C50D0, - "FK: Sage's Coal": 0x40000838, # Shop #Unique + "FK: Sage's Coal": 0x40000838, "FK: Stone Parma": 0x01443FD0, "FK: Sage's Scroll": 0x40000854, "FK: Crown of Dusk": 0x15D75C80, @@ -211,8 +216,8 @@ farron_keep_table = { "FK: Pharis's Hat": 0x1487AB00, "FK: Black Bow of Pharis": 0x00D7E970, - "FK: Dreamchaser's Ashes": 0x4000083C, # Shop #Unique - "FK: Great Axe": 0x006B9310, # Multiple + "FK: Dreamchaser's Ashes": 0x4000083C, + "FK: Great Axe": 0x006B9310, "FK: Dragon Crest Shield": 0x01432E60, "FK: Lightning Spear": 0x40362B30, "FK: Atonement": 0x4039ADA0, @@ -251,6 +256,7 @@ smouldering_lake_table = { "SL: Fume Ultra Greatsword": 0x0060E4B0, "SL: Black Iron Greatshield": 0x0150EA00, "SL: Soul of the Old Demon King": 0x400002D0, + "SL: Knight Slayer's Ring": 0x20005000, } irithyll_of_the_boreal_valley_table = { @@ -266,7 +272,6 @@ irithyll_of_the_boreal_valley_table = { "IBV: Smough's Great Hammer": 0x007E30B0, "IBV: Leo Ring": 0x20004EE8, - "IBV: Greirat's Ashes": 0x4000083F, "IBV: Excrement-covered Ashes": 0x40000862, "IBV: Dark Stoneplate Ring": 0x20004E70, @@ -286,6 +291,7 @@ irithyll_of_the_boreal_valley_table = { "IBV: Golden Ritual Spear": 0x00C83200, "IBV: Soul of Pontiff Sulyvahn": 0x400002D4, "IBV: Aldrich Faithful Covenant": 0x2000272E, + "IBV: Drang Twinspears": 0x00F5AAA0, } irithyll_dungeon_table = { @@ -354,8 +360,11 @@ lothric_castle_table = { "LC: Braille Divine Tome of Lothric": 0x40000848, "LC: Knight's Ring": 0x20004FEC, "LC: Sunlight Straight Sword": 0x00203230, - "LC: Grand Archives Key": 0x400007DE, "LC: Soul of Dragonslayer Armour": 0x400002D1, + + # The Black Hand Gotthard corpse appears when you have defeated Yhorm and Aldrich and triggered the cutscene + "LC: Grand Archives Key": 0x400007DE, # On Black Hand Gotthard corpse + "LC: Gotthard Twinswords": 0x00F53570 # On Black Hand Gotthard corpse } consumed_king_garden_table = { @@ -366,7 +375,7 @@ consumed_king_garden_table = { "CKG: Shadow Leggings": 0x14D401F8, "CKG: Claw": 0x00A7D8C0, "CKG: Soul of Consumed Oceiros": 0x400002CE, - "CKG: Path of the Dragon Gesture": 0x40002346, + # "CKG: Path of the Dragon Gesture": 0x40002346, I can't technically randomize it as it is a gesture and not an item } grand_archives_table = { @@ -383,6 +392,7 @@ grand_archives_table = { "GA: Divine Pillars of Light": 0x4038C340, "GA: Cinders of a Lord - Lothric Prince": 0x4000084E, "GA: Soul of the Twin Princes": 0x400002DB, + "GA: Sage's Crystal Staff": 0x00C8CE40, } untended_graves_table = { @@ -414,6 +424,8 @@ archdragon_peak_table = { "AP: Dragonslayer Leggings": 0x158B1CF8, "AP: Ricard's Rapier": 0x002E3BF0, "AP: Soul of the Nameless King": 0x400002D2, + "AP: Dragon Tooth": 0x007E09A0, + "AP: Havel's Greatshield": 0x013376F0, } location_dictionary_table = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, From a0482cf27e46fc81a930b4ea3bc7852e387c280d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 22 Jul 2022 00:11:47 +0200 Subject: [PATCH 052/138] Archipidle: Fix forgotten version increment when a new item was added --- worlds/archipidle/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py index 6afcf4aa30..0ddb8248fb 100644 --- a/worlds/archipidle/__init__.py +++ b/worlds/archipidle/__init__.py @@ -25,7 +25,7 @@ class ArchipIDLEWorld(World): """ game = "ArchipIDLE" topology_present = False - data_version = 3 + data_version = 4 hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April web = ArchipIDLEWebWorld() From 1e366ff66f700ca90f95989cb35375a552ff7686 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Tue, 26 Jul 2022 00:43:39 -0700 Subject: [PATCH 053/138] SM: smoother co-op, basepatch internal improvements (#793) * SM: remote touch instantly, pull ips refactor and symbols * SM: remove hard-coded ROM address writes * SM: Full length player table, incl. receive-only player ids + apply PR feedback (correct graphic offset, readable data file paths) --- .gitignore | 3 + worlds/sm/Rom.py | 49 +- worlds/sm/__init__.py | 221 ++++-- .../multiworld-basepatch.ips | Bin 0 -> 17952 bytes .../data/SMBasepatch_prebuilt/multiworld.sym | 689 ++++++++++++++++++ .../sm-basepatch-symbols.json | 141 ++++ .../SMBasepatch_prebuilt/variapatches.ips} | Bin 45423 -> 27590 bytes worlds/sm/data/sourceinfo.txt | 3 + worlds/sm/variaRandomizer/rom/rompatcher.py | 2 - 9 files changed, 1027 insertions(+), 81 deletions(-) create mode 100644 worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips create mode 100644 worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym create mode 100644 worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json rename worlds/sm/{variaRandomizer/patches/common/ips/basepatch.ips => data/SMBasepatch_prebuilt/variapatches.ips} (54%) create mode 100644 worlds/sm/data/sourceinfo.txt diff --git a/.gitignore b/.gitignore index 3733723427..58122d64a2 100644 --- a/.gitignore +++ b/.gitignore @@ -116,6 +116,9 @@ target/ profile_default/ ipython_config.py +# vim editor +*.swp + # SageMath parsed files *.sage.py diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index a01fcbe3a8..e2957fe00f 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -1,11 +1,12 @@ import hashlib import os +import json import Utils from Patch import read_rom, APDeltaPatch SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' -ROM_PLAYER_LIMIT = 65535 +ROM_PLAYER_LIMIT = 65535 # max archipelago player ID. note, SM ROM itself will only store 201 names+ids max class SMDeltaPatch(APDeltaPatch): @@ -17,7 +18,6 @@ class SMDeltaPatch(APDeltaPatch): def get_source_data(cls) -> bytes: return get_base_rom_bytes() - def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: @@ -40,3 +40,48 @@ def get_base_rom_path(file_name: str = "") -> str: if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name + +def get_sm_symbols(sym_json_path) -> dict: + with open(sym_json_path, "r") as stream: + symbols = json.load(stream) + symboltable = {} + for name, sixdigitaddr in symbols.items(): + (bank, addr_within_bank) = sixdigitaddr.split(":") + bank = int(bank, 16) + addr_within_bank = int(addr_within_bank, 16) + # categorize addresses using snes lorom mapping: + # (reference: https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map) + if (bank >= 0x70 and bank <= 0x7d): + offset_within_rom_file = None + # SRAM is not continuous, but callers may want it in continuous terms + # SRAM @ data bank $70-$7D, addr_within_bank $0000-$7FFF + # + # symbol aka snes offestwithincontinuousSRAM + # --------------- -------------------------- + # $70:0000-7FFF -> 0x0000- 7FFF + # $71:0000-7FFF -> 0x8000- FFFF + # $72:0000-7FFF -> 0x10000-17FFF + # etc... + offset_within_continuous_sram = (bank - 0x70) * 0x8000 + addr_within_bank + offset_within_wram = None + elif bank == 0x7e or bank == 0x7f or (bank == 0x00 and addr_within_bank <= 0x1fff): + offset_within_rom_file = None + offset_within_continuous_sram = None + offset_within_wram = addr_within_bank + if bank == 0x7f: + offset_within_wram += 0x10000 + elif bank >= 0x80: + offset_within_rom_file = ((bank - 0x80) * 0x8000) + (addr_within_bank % 0x8000) + offset_within_continuous_sram = None + offset_within_wram = None + else: + offset_within_rom_file = None + offset_within_continuous_sram = None + offset_within_wram = None + symboltable[name] = {"bank": bank, + "addr_within_bank": addr_within_bank, + "offset_within_rom_file": offset_within_rom_file, + "offset_within_continuous_sram": offset_within_continuous_sram, + "offset_within_wram": offset_within_wram + } + return symboltable diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 59c3c463f7..1de316269a 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -16,7 +16,7 @@ from .Items import lookup_name_to_id as items_lookup_name_to_id from .Regions import create_regions from .Rules import set_rules, add_entrance_rule from .Options import sm_options -from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch +from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch, get_sm_symbols import Utils from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, RegionType, CollectionState, Tutorial @@ -201,10 +201,7 @@ class SMWorld(World): create_locations(self, self.player) create_regions(self, self.world, self.player) - def getWord(self, w): - return (w & 0x00FF, (w & 0xFF00) >> 8) - - def getWordArray(self, w): + def getWordArray(self, w): # little-endian convert a 16-bit number to an array of numbers <= 255 each return [w & 0x00FF, (w & 0xFF00) >> 8] # used for remote location Credits Spoiler of local items @@ -269,109 +266,175 @@ class SMWorld(World): itemName = "___" + itemName + "___" for char in itemName: - (w0, w1) = self.getWord(charMap.get(char, 0x3C4E)) + [w0, w1] = self.getWordArray(charMap.get(char, 0x3C4E)) data.append(w0) data.append(w1) return data def APPatchRom(self, romPatcher): - multiWorldLocations = {} - multiWorldItems = {} + # first apply the sm multiworld code patch named 'basepatch' (also has empty tables that we'll overwrite), + # + apply some patches from varia that we want to be always-on. + # basepatch and variapatches are both generated from https://github.com/lordlou/SMBasepatch + romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__), + "data", "SMBasepatch_prebuilt", "multiworld-basepatch.ips")) + romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__), + "data", "SMBasepatch_prebuilt", "variapatches.ips")) + symbols = get_sm_symbols(os.path.join(os.path.dirname(__file__), + "data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json")) + multiWorldLocations = [] + multiWorldItems = [] idx = 0 self.playerIDMap = {} - playerIDCount = 0 # 0 is for "Archipelago" server + playerIDCount = 0 # 0 is for "Archipelago" server; highest possible = 200 (201 entries) + vanillaItemTypesCount = 21 for itemLoc in self.world.get_locations(): - romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0 if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None: + # this SM world can find this item: write full item data to tables and assign player data for writing + romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0 if itemLoc.item.type in ItemManager.Items: itemId = ItemManager.Items[itemLoc.item.type].Id else: itemId = ItemManager.Items['ArchipelagoItem'].Id + idx - multiWorldItems[0x029EA3 + idx*64] = self.convertToROMItemName(itemLoc.item.name) + multiWorldItems.append({"sym": symbols["message_item_names"], + "offset": (vanillaItemTypesCount + idx)*64, + "values": self.convertToROMItemName(itemLoc.item.name)}) idx += 1 if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): playerIDCount += 1 self.playerIDMap[romPlayerID] = playerIDCount - (w0, w1) = self.getWord(0 if itemLoc.item.player == self.player else 1) - (w2, w3) = self.getWord(itemId) - (w4, w5) = self.getWord(romPlayerID) - (w6, w7) = self.getWord(0 if itemLoc.item.advancement else 1) - multiWorldLocations[0x1C6000 + locationsDict[itemLoc.name].Id*8] = [w0, w1, w2, w3, w4, w5, w6, w7] + [w0, w1] = self.getWordArray(0 if itemLoc.item.player == self.player else 1) + [w2, w3] = self.getWordArray(itemId) + [w4, w5] = self.getWordArray(romPlayerID) + [w6, w7] = self.getWordArray(0 if itemLoc.item.advancement else 1) + multiWorldLocations.append({"sym": symbols["rando_item_table"], + "offset": locationsDict[itemLoc.name].Id*8, + "values": [w0, w1, w2, w3, w4, w5, w6, w7]}) - if itemLoc.item.player == self.player: + elif itemLoc.item.player == self.player: + # this SM world owns the item: so in case the sending player might not have anything placed in this + # world to receive from it, assign them space in playerIDMap so that the ROM can display their name + # (SM item name not needed, as SM item type id will be in the message they send to this world live) + romPlayerID = itemLoc.player if itemLoc.player <= ROM_PLAYER_LIMIT else 0 if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): playerIDCount += 1 self.playerIDMap[romPlayerID] = playerIDCount - itemSprites = ["off_world_prog_item.bin", "off_world_item.bin"] + itemSprites = [{"fileName": "off_world_prog_item.bin", + "paletteSymbolName": "prog_item_eight_palette_indices", + "dataSymbolName": "offworld_graphics_data_progression_item"}, + + {"fileName": "off_world_item.bin", + "paletteSymbolName": "nonprog_item_eight_palette_indices", + "dataSymbolName": "offworld_graphics_data_item"}] idx = 0 - offworldSprites = {} - for fileName in itemSprites: - with open(Utils.local_path("lib", "worlds", "sm", "data", "custom_sprite", fileName) if Utils.is_frozen() else Utils.local_path("worlds", "sm", "data", "custom_sprite", fileName), 'rb') as stream: + offworldSprites = [] + for itemSprite in itemSprites: + with open(os.path.join(os.path.dirname(__file__), "data", "custom_sprite", itemSprite["fileName"]), 'rb') as stream: buffer = bytearray(stream.read()) - offworldSprites[0x027882 + 10*(21 + idx) + 2] = buffer[0:8] - offworldSprites[0x049100 + idx*256] = buffer[8:264] + offworldSprites.append({"sym": symbols[itemSprite["paletteSymbolName"]], + "offset": 0, + "values": buffer[0:8]}) + offworldSprites.append({"sym": symbols[itemSprite["dataSymbolName"]], + "offset": 0, + "values": buffer[8:264]}) idx += 1 - - openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]} - deathLink = {0x277f04: [self.world.death_link[self.player].value]} - remoteItem = {0x277f06: self.getWordArray(0b001 + (0b010 if self.remote_items else 0b000))} + deathLink = [{"sym": symbols["config_deathlink"], + "offset": 0, + "values": [self.world.death_link[self.player].value]}] + remoteItem = [{"sym": symbols["config_remote_items"], + "offset": 0, + "values": self.getWordArray(0b001 + (0b010 if self.remote_items else 0b000))}] + ownPlayerId = [{"sym": symbols["config_player_id"], + "offset": 0, + "values": self.getWordArray(self.player)}] - playerNames = {} - playerNameIDMap = {} - playerNames[0x1C5000] = "Archipelago".upper().center(16).encode() - playerNameIDMap[0x1C5800] = self.getWordArray(0) + playerNames = [] + playerNameIDMap = [] + playerNames.append({"sym": symbols["rando_player_table"], + "offset": 0, + "values": "Archipelago".upper().center(16).encode()}) + playerNameIDMap.append({"sym": symbols["rando_player_id_table"], + "offset": 0, + "values": self.getWordArray(0)}) for key,value in self.playerIDMap.items(): - playerNames[0x1C5000 + value * 16] = self.world.player_name[key][:16].upper().center(16).encode() - playerNameIDMap[0x1C5800 + value * 2] = self.getWordArray(key) + playerNames.append({"sym": symbols["rando_player_table"], + "offset": value * 16, + "values": self.world.player_name[key][:16].upper().center(16).encode()}) + playerNameIDMap.append({"sym": symbols["rando_player_id_table"], + "offset": value * 2, + "values": self.getWordArray(key)}) patchDict = { 'MultiWorldLocations': multiWorldLocations, 'MultiWorldItems': multiWorldItems, 'offworldSprites': offworldSprites, - 'openTourianGreyDoors': openTourianGreyDoors, 'deathLink': deathLink, 'remoteItem': remoteItem, + 'ownPlayerId': ownPlayerId, 'PlayerName': playerNames, 'PlayerNameIDMap': playerNameIDMap} + + # convert an array of symbolic byte_edit dicts like {"sym": symobj, "offset": 0, "values": [1, 0]} + # to a single rom patch dict like {0x438c: [1, 0], 0xa4a5: [0, 0, 0]} which varia will understand and apply + def resolve_symbols_to_file_offset_based_dict(byte_edits_arr) -> dict: + this_patch_as_dict = {} + for byte_edit in byte_edits_arr: + offset_within_rom_file = byte_edit["sym"]["offset_within_rom_file"] + byte_edit["offset"] + this_patch_as_dict[offset_within_rom_file] = byte_edit["values"] + return this_patch_as_dict + + for patchname, byte_edits_arr in patchDict.items(): + patchDict[patchname] = resolve_symbols_to_file_offset_based_dict(byte_edits_arr) + romPatcher.applyIPSPatchDict(patchDict) + openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]} + romPatcher.applyIPSPatchDict({'openTourianGreyDoors': openTourianGreyDoors}) + + # set rom name # 21 bytes from Main import __version__ - self.romName = bytearray(f'SM{__version__.replace(".", "")[0:3]}_{self.player}_{self.world.seed:11}\0', 'utf8')[:21] + self.romName = bytearray(f'SM{__version__.replace(".", "")[0:3]}_{self.player}_{self.world.seed:11}', 'utf8')[:21] self.romName.extend([0] * (21 - len(self.romName))) # clients should read from 0x7FC0, the location of the rom title in the SNES header. # duplicative ROM name at 0x1C4F00 is still written here for now, since people with archipelago pre-0.3.0 client installed will still be depending on this location for connecting to SM romPatcher.applyIPSPatch('ROMName', { 'ROMName': {0x1C4F00 : self.romName, 0x007FC0 : self.romName} }) - startItemROMAddressBase = 0x2FD8B9 + startItemROMAddressBase = symbols["start_item_data_major"]["offset_within_rom_file"] - # current, base value or bitmask, max, base value or bitmask - startItemROMDict = {'ETank': [0x8, 0x64, 0xA, 0x64], - 'Missile': [0xC, 0x5, 0xE, 0x5], - 'Super': [0x10, 0x5, 0x12, 0x5], - 'PowerBomb': [0x14, 0x5, 0x16, 0x5], - 'Reserve': [0x1A, 0x64, 0x18, 0x64], - 'Morph': [0x2, 0x4, 0x0, 0x4], - 'Bomb': [0x3, 0x10, 0x1, 0x10], - 'SpringBall': [0x2, 0x2, 0x0, 0x2], - 'HiJump': [0x3, 0x1, 0x1, 0x1], - 'Varia': [0x2, 0x1, 0x0, 0x1], - 'Gravity': [0x2, 0x20, 0x0, 0x20], - 'SpeedBooster': [0x3, 0x20, 0x1, 0x20], - 'SpaceJump': [0x3, 0x2, 0x1, 0x2], - 'ScrewAttack': [0x2, 0x8, 0x0, 0x8], - 'Charge': [0x7, 0x10, 0x5, 0x10], - 'Ice': [0x6, 0x2, 0x4, 0x2], - 'Wave': [0x6, 0x1, 0x4, 0x1], - 'Spazer': [0x6, 0x4, 0x4, 0x4], - 'Plasma': [0x6, 0x8, 0x4, 0x8], - 'Grapple': [0x3, 0x40, 0x1, 0x40], - 'XRayScope': [0x3, 0x80, 0x1, 0x80] + # array for each item: + # offset within ROM table "start_item_data_major" of this item"s info (starting status) + # item bitmask or amount per pickup (BVOB = base value or bitmask), + # offset within ROM table "start_item_data_major" of this item"s info (starting maximum/starting collected items) + # current BVOB max + # ------- ---- --- + startItemROMDict = {"ETank": [ 0x8, 0x64, 0xA], + "Missile": [ 0xC, 0x5, 0xE], + "Super": [0x10, 0x5, 0x12], + "PowerBomb": [0x14, 0x5, 0x16], + "Reserve": [0x1A, 0x64, 0x18], + "Morph": [ 0x2, 0x4, 0x0], + "Bomb": [ 0x3, 0x10, 0x1], + "SpringBall": [ 0x2, 0x2, 0x0], + "HiJump": [ 0x3, 0x1, 0x1], + "Varia": [ 0x2, 0x1, 0x0], + "Gravity": [ 0x2, 0x20, 0x0], + "SpeedBooster": [ 0x3, 0x20, 0x1], + "SpaceJump": [ 0x3, 0x2, 0x1], + "ScrewAttack": [ 0x2, 0x8, 0x0], + "Charge": [ 0x7, 0x10, 0x5], + "Ice": [ 0x6, 0x2, 0x4], + "Wave": [ 0x6, 0x1, 0x4], + "Spazer": [ 0x6, 0x4, 0x4], + "Plasma": [ 0x6, 0x8, 0x4], + "Grapple": [ 0x3, 0x40, 0x1], + "XRayScope": [ 0x3, 0x80, 0x1] + + # BVOB = base value or bitmask } mergedData = {} hasETank = False @@ -379,48 +442,52 @@ class SMWorld(World): hasPlasma = False for startItem in self.startItems: item = startItem.Type - if item == 'ETank': hasETank = True - if item == 'Spazer': hasSpazer = True - if item == 'Plasma': hasPlasma = True - if (item in ['ETank', 'Missile', 'Super', 'PowerBomb', 'Reserve']): - (currentValue, currentBase, maxValue, maxBase) = startItemROMDict[item] + if item == "ETank": hasETank = True + if item == "Spazer": hasSpazer = True + if item == "Plasma": hasPlasma = True + if (item in ["ETank", "Missile", "Super", "PowerBomb", "Reserve"]): + (currentValue, amountPerItem, maxValue) = startItemROMDict[item] if (startItemROMAddressBase + currentValue) in mergedData: - mergedData[startItemROMAddressBase + currentValue] += currentBase - mergedData[startItemROMAddressBase + maxValue] += maxBase + mergedData[startItemROMAddressBase + currentValue] += amountPerItem + mergedData[startItemROMAddressBase + maxValue] += amountPerItem else: - mergedData[startItemROMAddressBase + currentValue] = currentBase - mergedData[startItemROMAddressBase + maxValue] = maxBase + mergedData[startItemROMAddressBase + currentValue] = amountPerItem + mergedData[startItemROMAddressBase + maxValue] = amountPerItem else: - (collected, currentBitmask, equipped, maxBitmask) = startItemROMDict[item] + (collected, bitmask, equipped) = startItemROMDict[item] if (startItemROMAddressBase + collected) in mergedData: - mergedData[startItemROMAddressBase + collected] |= currentBitmask - mergedData[startItemROMAddressBase + equipped] |= maxBitmask + mergedData[startItemROMAddressBase + collected] |= bitmask + mergedData[startItemROMAddressBase + equipped] |= bitmask else: - mergedData[startItemROMAddressBase + collected] = currentBitmask - mergedData[startItemROMAddressBase + equipped] = maxBitmask + mergedData[startItemROMAddressBase + collected] = bitmask + mergedData[startItemROMAddressBase + equipped] = bitmask if hasETank: + # we are overwriting the starting energy, so add up the E from 99 (normal starting energy) rather than from 0 mergedData[startItemROMAddressBase + 0x8] += 99 mergedData[startItemROMAddressBase + 0xA] += 99 if hasSpazer and hasPlasma: + # de-equip spazer. + # otherwise, firing the unintended spazer+plasma combo would cause massive game glitches and crashes mergedData[startItemROMAddressBase + 0x4] &= ~0x4 for key, value in mergedData.items(): if (key - startItemROMAddressBase > 7): - (w0, w1) = self.getWord(value) + [w0, w1] = self.getWordArray(value) mergedData[key] = [w0, w1] else: mergedData[key] = [value] - - startItemPatch = { 'startItemPatch': mergedData } - romPatcher.applyIPSPatch('startItemPatch', startItemPatch) + startItemPatch = { "startItemPatch": mergedData } + romPatcher.applyIPSPatch("startItemPatch", startItemPatch) + + # commit all the changes we've made here to the ROM romPatcher.commitIPS() itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.world.get_locations() if itemLoc.player == self.player] - romPatcher.writeItemsLocs(itemLocs) + romPatcher.writeItemsLocs(itemLocs) itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player] progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player and itemLoc.item.advancement == True] diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips new file mode 100644 index 0000000000000000000000000000000000000000..d7fd17613e8f2af2b5cc6ed5db11efbe9d227f07 GIT binary patch literal 17952 zcmeHNdrVu`8UOqMi~|YPl9ooKyS=5vl`2X|TFxqKBbN2O)6_8S7ECwYn8XZ zHX1n>hGQT~AW=J&{sRJ0VOYkbuG=NFBZNO<6GWIaDTq|!`ay7b+7N<$)5QDEHRf5< zZknbkJNN3|bH4AM-}m^P2NwE{9s0>DC4iSI2B)t;4yRlvj&R2hnSOlqNNL%t$2qP* zouLI4fUp;|yiE{6SOV=^W0fL&rh!yG$1P4ysYY5)LChiO)Z6B$D zoK*kDrhfK>+z>e-UyPK=A4cE;^{QMK6P-^Zmm+_T^hEAQu19u6Z$+MoPDP5Mu2kUO zkXV~{Bk9VWl#Bag-SwvLCj%x0XRTNajCHx{qou#yq&*-l;$_0LZ(MYd%unfn&wZVX8v@gXdXTLMrUt+jo8udc7F}fDO(q6 z0G?C}wV)Q%>Z;cumHX&&gUYO~ih_pw;mZH%C_V5Hi~HKR<%IC3*SZfV*WG|uB4G)|$N`E|(n zh~)Tr<6YyN;@xCSZYMe3gy>o0y=4nt>`xoLn_}1sPa{l*OE8rj?=wyyUzEFmVkdyo z^d<(9glljLd+YD5;$K7zwLl}Ft1*cA zuSai{|82z5{3(j=o^Q60-2W2J9<>n1`f&Zy-SFN0&tbL_dYx9{_agRRS%2~9=g{ZW z8V)zxNFG1Ucn*EWSMcgOT%SR5{5&GV81+5OSwar!>EqjQ`fSDjG%^;(2d0jH)e+Qpc$446_iw|`8K%&GMDd)!tZkFy6S>ofefR_oi}>EiKr;M)GXF3#K$-le zJDLB)*zcDAXjxky7D(XL-U5IH3f(6gn}kApFz9wm2VpqW+18IAq>HMo%wTZr{8!@e z!oWbCpus8&1a&k6^unEC*hGzx(0u>fNSl>SP$>1E@>O|lzuRr(Kh4(O(@f&(u8!Dm-UVV z`fM{-R;A&}Sln=RD|LOo2s2u5WNlZxU7{HsCZl-wD5ZaE^ z@Jsc=rNuP9uUttl5?$5JB@#2hSJ3SBKwDnYdTV}j@_aMfE&M$2@le3)nRJRAt+qIsOL zCg5yMkc*E2pXY7Cpak0CJFbTV^*GQ_AzKhPTGCaOXQT7@aTDydS(?=JrQK_z>fQDD zqQj_h$g|aJ5oe(@9#^4Qk7|R(dZw?s-&Oj7W6%W}$B^qu;03`^-dEmVK3G14Zaj}e zXrJc^2tBXwKpq@}&4Y0EuHD|*Nwe*A6)7B)55{e%&<1aptILPV2W5-7r0e|=`|f&t zS@`pn0-B@2+)(G)LcgSyJ(#DXzgLa#zOK~RP%X~D!M6Szy4cV#Y3q9qVjhTv@?T%R zMoXxXUfYQMg^3hL-vkNcBAeeI@c3biyLj0ef?y|zbL z{JmO9Y)_OHX|S}2FGWKjiJ@&&+reKHL}BOZfSsW4pqUAP{9F0=E;K7MnbGxex}DYw zX}FkR?K+-r&weip_}>X)g`C}K&C%h;)PWu{u+rxR@7vz9-fr)NcbD%K->-bFz7b!h z|7E|$|B=7nANGGga4K>p@)zlr1m0)8$Gw90kKV7mdLQxq!uO%i?R(%W@W1Up>lgjE z{W}9c3N*+Ka@lAPW@M+!AQ9_IofIV)>z*`ng;lQTp(Rh&94w!!gyriboJ@3AT3mg- z;1-)&((xFPFt0Gx=umT2(PS-avQhI7MWaOx_fxmk0}x81JLO8cy$j*Ba4@9h^FI;d zf`GygKN0wIe`ql090l05;&N>#=NN3~IquluLvLa&-#(6`A6MkU4f6gC@_`NViyP#Z zHpoRAWD=M674ARq;!8#32L<~IsedLjKG=F-`+=PY>^x+D57^&B_IrT+9$>!*{(pWC z&@-Q1|4R(ym>~<}Q7y;xXtXN)CqevrH1c`W=7;6O)+O7YlmWI5*g9bAfb9cz4zP28 dodfI~VCMik2mbr#z~0kX#*$ci0Iwc>?VpVOFhKwS literal 0 HcmV?d00001 diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym new file mode 100644 index 0000000000..751f470f53 --- /dev/null +++ b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym @@ -0,0 +1,689 @@ +; wla symbolic information file +; generated by asar + +[labels] +B8:8026 :neg_1_1 +85:B9B4 :neg_1_2 +85:B9E6 :neg_1_3 +B8:C81F :neg_1_4 +B8:C831 :neg_1_5 +B8:C843 :neg_1_6 +B8:800C :pos_1_0 +B8:81DE :pos_1_1 +84:FA6B :pos_1_2 +84:FA75 :pos_1_3 +B8:C862 :pos_1_4 +B8:C86F :pos_1_5 +B8:C87C :pos_1_6 +85:FF00 CLIPCHECK +85:9900 CLIPLEN +85:990F CLIPLEN_end +85:990C CLIPLEN_no_multi +85:FF1D CLIPSET +B8:80EF COLLECTTANK +85:FF45 MISCFX +84:8BF2 NORMAL +85:FF4E SETFX +85:FF30 SOUNDFX +84:F9E0 SOUNDFX_84 +85:FF3C SPECIALFX +84:F896 ammo_loop_table +84:F874 archipelago_chozo_item_plm +84:F878 archipelago_hidden_item_plm +84:F870 archipelago_visible_item_plm +84:F892 c_item +CE:FF04 config_deathlink +CE:FF00 config_flags +CE:FF00 config_multiworld +CE:FF08 config_player_id +CE:FF06 config_remote_items +CE:FF02 config_sprite +84:F894 h_item +84:F8AD i_chozo_item +84:F8B4 i_hidden_item +84:FA5A i_hidden_item_setup +B8:885C i_item_setup_shared +B8:8878 i_item_setup_shared_all_items +B8:8883 i_item_setup_shared_alwaysloaded +84:FA79 i_live_pickup +B8:817F i_live_pickup_multiworld +B8:81C4 i_live_pickup_multiworld_end +B8:819B i_live_pickup_multiworld_local_item_or_offworld +B8:81B0 i_live_pickup_multiworld_own_item +B8:81BC i_live_pickup_multiworld_own_item1 +84:FA1E i_load_custom_graphics +84:FA39 i_load_custom_graphics_all_items +84:FA49 i_load_custom_graphics_alwaysloaded +84:FA61 i_load_rando_item +84:FA78 i_load_rando_item_end +84:F9F1 i_start_draw_loop +84:FA0A i_start_draw_loop_all_items +84:F9EC i_start_draw_loop_hidden +84:FA1C i_start_draw_loop_non_ammo_item +84:F9E5 i_start_draw_loop_visible_or_chozo +84:F8A6 i_visible_item +84:FA53 i_visible_item_setup +85:BA8A message_PlaceholderBig +85:BA0A message_char_table +85:BABC message_hook_tilemap_calc +85:BADC message_hook_tilemap_calc_msgbox_mwrecv +85:BACE message_hook_tilemap_calc_msgbox_mwsend +85:824C message_hook_tilemap_calc_normal +85:BAC9 message_hook_tilemap_calc_vanilla +85:9963 message_item_names +85:B8A3 message_item_received +85:B9A3 message_item_received_end +85:B7A3 message_item_sent +85:B8A3 message_item_sent_end +85:BA95 message_multiworld_init_new_messagebox_if_needed +85:BAB1 message_multiworld_init_new_messagebox_if_needed_msgbox_mwrecv +85:BAB1 message_multiworld_init_new_messagebox_if_needed_msgbox_mwsend +85:BAA9 message_multiworld_init_new_messagebox_if_needed_vanilla +85:B9A3 message_write_placeholders +85:B9A5 message_write_placeholders_adjust +85:BA04 message_write_placeholders_end +85:B9CA message_write_placeholders_loop +85:B9DC message_write_placeholders_notfound +85:B9DF message_write_placeholders_value_ok +B8:8092 mw_display_item_sent +B8:80FF mw_handle_queue +B8:8178 mw_handle_queue_end +B8:8101 mw_handle_queue_loop +B8:8151 mw_handle_queue_new_remote_item +B8:816D mw_handle_queue_next +B8:8163 mw_handle_queue_perform_receive +B8:81C8 mw_hook_main_game +B8:8011 mw_init +B8:8044 mw_init_end +B8:8000 mw_init_memory +B8:8083 mw_load_sram +B8:80B0 mw_receive_item +B8:80E8 mw_receive_item_end +B8:8070 mw_save_sram +B8:8049 mw_write_message +84:F888 nonprog_item_eight_palette_indices +89:9200 offworld_graphics_data_item +89:9100 offworld_graphics_data_progression_item +84:F972 p_chozo_item +84:F9A0 p_chozo_item_end +84:F98D p_chozo_item_loop +84:F999 p_chozo_item_trigger +84:F8FB p_etank_hloop +84:F8BB p_etank_loop +84:F9A6 p_hidden_item +84:F9D8 p_hidden_item_end +84:F9BD p_hidden_item_loop +84:F9A8 p_hidden_item_loop2 +84:F9D1 p_hidden_item_trigger +84:F90F p_missile_hloop +84:F8CB p_missile_loop +84:F937 p_pb_hloop +84:F8EB p_pb_loop +84:F923 p_super_hloop +84:F8DB p_super_loop +84:F94B p_visible_item +84:F96E p_visible_item_end +84:F95B p_visible_item_loop +84:F967 p_visible_item_trigger +B8:81DF patch_load_multiworld +84:FA7E perform_item_pickup +84:F886 plm_graphics_entry_offworld_item +84:F87C plm_graphics_entry_offworld_progression_item +84:FA90 plm_sequence_generic_item_0_bitmask +84:F87E prog_item_eight_palette_indices +B8:E000 rando_item_table +B8:DC90 rando_player_id_table +B8:DE22 rando_player_id_table_end +B8:D000 rando_player_table +B8:CF00 rando_seed_data +B8:8800 sm_item_graphics +B8:882E sm_item_plm_pickup_sequence_pointers +B8:C81C start_item +B8:C800 start_item_data_major +B8:C808 start_item_data_minor +B8:C818 start_item_data_reserve +B8:C856 update_graphic +84:F890 v_item + +[source files] +0000 e25029c5 main.asm +0001 06780555 ../common/nofanfare.asm +0002 e76d1f83 ../common/multiworld.asm +0003 613d24e1 ../common/itemextras.asm +0004 d6616c0c ../common/items.asm +0005 440b54fe ../common/startitem.asm + +[rom checksum] +09b134c5 + +[addr-to-line mapping] +ff:ffff 0000:00000001 +85:ff00 0001:0000010b +85:ff03 0001:0000010c +85:ff06 0001:0000010d +85:ff08 0001:0000010e +85:ff0b 0001:0000010f +85:ff0f 0001:00000110 +85:ff12 0001:00000111 +85:ff16 0001:00000112 +85:ff19 0001:00000113 +85:ff1c 0001:00000114 +85:ff1d 0001:00000117 +85:ff20 0001:00000118 +85:ff24 0001:00000119 +85:ff28 0001:0000011a +85:ff2b 0001:0000011c +85:ff2f 0001:0000011d +85:ff30 0001:00000120 +85:ff34 0001:00000121 +85:ff37 0001:00000122 +85:ff3b 0001:00000123 +85:ff3c 0001:00000126 +85:ff40 0001:00000127 +85:ff44 0001:00000128 +85:ff45 0001:0000012b +85:ff49 0001:0000012c +85:ff4d 0001:0000012d +85:ff4e 0001:00000131 +85:ff51 0001:00000132 +85:ff54 0001:00000134 +85:ff57 0001:00000135 +85:ff58 0001:00000136 +85:8490 0001:0000013a +85:9900 0001:0000013e +85:9901 0001:0000013f +85:9905 0001:00000140 +85:9907 0001:00000141 +85:990a 0001:00000142 +85:990c 0001:00000144 +85:990f 0001:00000146 +85:9910 0001:00000147 +82:e126 0001:0000014a +82:e12a 0001:0000014b +85:8089 0001:0000014e +84:8bf2 0001:00000152 +84:8bf6 0001:00000153 +84:8bf7 0001:00000153 +b8:8000 0002:00000019 +b8:8002 0002:0000001a +b8:8006 0002:0000001b +b8:8008 0002:0000001c +b8:800c 0002:00000020 +b8:800e 0002:00000021 +b8:8010 0002:00000022 +b8:8011 0002:00000025 +b8:8012 0002:00000025 +b8:8013 0002:00000025 +b8:8014 0002:00000025 +b8:8015 0000:00000013 +b8:8017 0002:00000029 +b8:801b 0002:0000002a +b8:801e 0002:0000002b +b8:8020 0002:0000002d +b8:8023 0002:0000002e +b8:8026 0002:00000031 +b8:802a 0002:00000032 +b8:802e 0002:00000033 +b8:8032 0002:00000034 +b8:8036 0002:00000035 +b8:8037 0002:00000035 +b8:8038 0002:00000036 +b8:803b 0002:00000037 +b8:803d 0002:00000039 +b8:8040 0002:0000003a +b8:8044 0002:0000003d +b8:8045 0002:0000003d +b8:8046 0002:0000003d +b8:8047 0002:0000003d +b8:8048 0002:0000003e +b8:8049 0002:00000043 +b8:804a 0002:00000043 +b8:804b 0002:00000044 +b8:804c 0002:00000044 +b8:804d 0002:00000045 +b8:8051 0002:00000046 +b8:8054 0002:00000046 +b8:8055 0002:00000047 +b8:8056 0002:00000048 +b8:805a 0002:00000049 +b8:805b 0002:0000004a +b8:805f 0002:0000004b +b8:8060 0002:0000004c +b8:8064 0002:0000004e +b8:8068 0002:0000004f +b8:8069 0002:00000050 +b8:806d 0002:00000051 +b8:806e 0002:00000051 +b8:806f 0002:00000052 +b8:8070 0002:00000055 +b8:8071 0002:00000055 +b8:8072 0000:00000013 +b8:8074 0002:00000057 +b8:8078 0002:00000058 +b8:807c 0002:00000059 +b8:807d 0002:00000059 +b8:807e 0002:0000005b +b8:807f 0002:0000005c +b8:8082 0002:0000005d +b8:8083 0002:00000060 +b8:8084 0002:00000060 +b8:8085 0000:00000013 +b8:8087 0002:00000062 +b8:808b 0002:00000063 +b8:808f 0002:00000064 +b8:8090 0002:00000064 +b8:8091 0002:00000065 +b8:8092 0002:0000006a +b8:8094 0002:0000006b +b8:8096 0002:0000006e +b8:8099 0002:0000006f +b8:809b 0002:00000070 +b8:809e 0002:00000071 +b8:80a0 0002:00000072 +b8:80a3 0002:00000073 +b8:80a7 0002:00000074 +b8:80a9 0002:00000075 +b8:80ab 0002:00000076 +b8:80ad 0002:00000077 +b8:80af 0002:00000078 +b8:80b0 0002:0000007c +b8:80b1 0002:0000007c +b8:80b2 0002:0000007d +b8:80b5 0002:0000007e +b8:80b7 0002:0000007f +b8:80ba 0002:00000080 +b8:80bc 0002:00000081 +b8:80bd 0002:00000082 +b8:80be 0002:00000084 +b8:80c1 0002:00000085 +b8:80c3 0002:00000086 +b8:80c6 0002:00000087 +b8:80c7 0002:00000088 +b8:80ca 0002:00000089 +b8:80cb 0002:00000089 +b8:80cc 0002:0000008a +b8:80d0 0002:0000008b +b8:80d1 0002:0000008c +b8:80d4 0002:0000008d +b8:80d8 0002:0000008e +b8:80da 0002:00000090 +b8:80dd 0002:00000091 +b8:80df 0002:00000092 +b8:80e2 0002:00000093 +b8:80e4 0002:00000095 +b8:80e8 0002:00000097 +b8:80ea 0002:00000098 +b8:80ec 0002:00000099 +b8:80ed 0002:00000099 +b8:80ee 0002:0000009a +b8:80ef 0002:000000a5 +b8:80f0 0002:000000a6 +b8:80f4 0002:000000a7 +b8:80f5 0002:000000a8 +b8:80f9 0002:000000a9 +b8:80fa 0002:000000ab +b8:80fe 0002:000000ac +b8:80ff 0002:000000de +b8:8100 0002:000000de +b8:8101 0002:000000e1 +b8:8105 0002:000000e2 +b8:8109 0002:000000e3 +b8:810b 0002:000000e5 +b8:810d 0002:000000e5 +b8:810e 0002:000000e8 +b8:8112 0002:000000e9 +b8:8114 0002:000000ea +b8:8118 0002:000000eb +b8:811a 0002:000000ec +b8:811e 0002:000000ed +b8:8121 0002:000000ee +b8:8123 0002:000000ef +b8:8125 0002:000000f0 +b8:8129 0002:000000f1 +b8:812b 0002:000000f2 +b8:812d 0002:000000f3 +b8:8130 0002:000000f4 +b8:8133 0002:000000f5 +b8:8135 0002:000000f6 +b8:813d 0002:000000fa +b8:813e 0002:000000fb +b8:813f 0002:000000fc +b8:8143 0002:000000ff +b8:8147 0002:00000100 +b8:814b 0002:00000101 +b8:814d 0002:00000103 +b8:814e 0002:00000104 +b8:814f 0002:00000105 +b8:8151 0002:0000010a +b8:8152 0002:0000010b +b8:8156 0002:0000010e +b8:815a 0002:0000010f +b8:815e 0002:00000110 +b8:8162 0002:00000111 +b8:8163 0002:00000115 +b8:8165 0002:00000116 +b8:8168 0002:00000117 +b8:816a 0002:00000118 +b8:816d 0002:0000011b +b8:8171 0002:0000011c +b8:8172 0002:0000011d +b8:8176 0002:0000011f +b8:8178 0002:00000122 +b8:817a 0002:00000123 +b8:817c 0002:00000124 +b8:817d 0002:00000124 +b8:817e 0002:00000125 +b8:817f 0002:00000129 +b8:8180 0002:00000129 +b8:8181 0002:00000129 +b8:8182 0002:0000012a +b8:8186 0002:0000012b +b8:8189 0002:0000012b +b8:818a 0002:0000012d +b8:818e 0002:0000012e +b8:818f 0002:0000012f +b8:8193 0002:00000130 +b8:8196 0002:00000131 +b8:8198 0002:00000133 +b8:819b 0002:00000136 +b8:819f 0002:00000137 +b8:81a3 0002:00000138 +b8:81a5 0002:0000013a +b8:81a9 0002:0000013b +b8:81aa 0002:0000013d +b8:81ae 0002:0000013e +b8:81b0 0002:00000141 +b8:81b4 0002:00000142 +b8:81b7 0002:00000143 +b8:81b9 0002:00000144 +b8:81bc 0002:00000147 +b8:81bd 0002:00000148 +b8:81be 0002:00000149 +b8:81c2 0002:0000014a +b8:81c4 0002:0000014d +b8:81c5 0002:0000014d +b8:81c6 0002:0000014d +b8:81c7 0002:0000014e +b8:81c8 0002:00000152 +b8:81cc 0002:00000153 +b8:81d0 0002:00000154 +b8:81d2 0002:00000155 +b8:81d6 0002:00000156 +b8:81d9 0002:00000157 +b8:81db 0002:00000158 +b8:81de 0002:0000015a +b8:81df 0002:0000015d +b8:81e3 0002:0000015e +b8:81e4 0002:0000015f +b8:81e7 0002:00000160 +b8:81eb 0002:00000162 +b8:81ec 0002:00000163 +b8:81ed 0002:00000164 +b8:81ee 0002:00000165 +b8:81ef 0002:00000166 +8b:914a 0002:0000016b +81:80f7 0002:0000016e +81:8027 0002:00000171 +82:8bb3 0002:00000174 +85:b9a3 0002:0000020e +85:b9a4 0002:0000020e +85:b9a5 0002:00000211 +85:b9a7 0002:00000212 +85:b9ad 0002:00000212 +85:b9ae 0002:00000213 +85:b9b1 0002:00000214 +85:b9b2 0002:00000215 +85:b9b3 0002:00000215 +85:b9b4 0002:00000219 +85:b9b7 0002:0000021a +85:b9bb 0002:0000021b +85:b9bd 0002:0000021b +85:b9bf 0002:0000021c +85:b9c2 0002:0000021d +85:b9c4 0002:0000021f +85:b9c5 0002:00000220 +85:b9c7 0002:00000224 +85:b9ca 0002:00000226 +85:b9cd 0002:00000227 +85:b9cf 0002:00000228 +85:b9d1 0002:00000229 +85:b9d5 0002:0000022a +85:b9d7 0002:0000022b +85:b9d9 0002:0000022c +85:b9da 0002:0000022d +85:b9dc 0002:0000022f +85:b9df 0002:00000231 +85:b9e2 0002:00000231 +85:b9e3 0002:00000232 +85:b9e6 0002:00000234 +85:b9ea 0002:00000235 +85:b9ed 0002:00000236 +85:b9ee 0002:00000237 +85:b9ef 0002:00000237 +85:b9f0 0002:00000238 +85:b9f4 0002:00000239 +85:b9f5 0002:0000023a +85:b9f9 0002:0000023b +85:b9fb 0002:0000023c +85:b9fc 0002:0000023d +85:b9fd 0002:0000023e +85:ba00 0002:0000023f +85:ba02 0002:00000240 +85:ba04 0002:00000243 +85:ba05 0002:00000243 +85:ba06 0002:00000244 +85:ba09 0002:00000245 +85:ba8a 0002:00000253 +85:ba8c 0002:00000254 +85:ba8f 0002:00000255 +85:ba92 0002:00000256 +85:ba95 0002:0000025e +85:ba96 0002:0000025f +85:ba98 0002:00000260 +85:ba9b 0002:00000261 +85:ba9d 0002:00000262 +85:ba9f 0002:00000263 +85:baa2 0002:00000264 +85:baa4 0002:00000265 +85:baa7 0002:00000266 +85:baa9 0002:00000269 +85:baaa 0002:0000026a +85:baab 0002:0000026b +85:baac 0002:0000026c +85:baae 0002:0000026d +85:baaf 0002:0000026e +85:bab0 0002:0000026f +85:bab1 0002:00000274 +85:bab4 0002:00000275 +85:bab5 0002:00000276 +85:bab8 0002:00000277 +85:bab9 0002:00000278 +85:baba 0002:00000279 +85:babb 0002:0000027a +85:babc 0002:00000285 +85:babd 0002:00000286 +85:babf 0002:00000287 +85:bac2 0002:00000288 +85:bac4 0002:00000289 +85:bac7 0002:0000028a +85:bac9 0002:0000028d +85:baca 0002:0000028e +85:bacb 0002:0000028f +85:bacd 0002:00000290 +85:bace 0002:00000292 +85:bacf 0002:00000293 +85:bad1 0002:00000294 +85:bad4 0002:00000295 +85:bad6 0002:00000296 +85:bad9 0002:00000297 +85:badb 0002:00000298 +85:badc 0002:0000029a +85:badd 0002:0000029b +85:badf 0002:0000029c +85:bae2 0002:0000029d +85:bae4 0002:0000029e +85:bae7 0002:0000029f +85:bae9 0002:000002a0 +85:8246 0002:000002a5 +85:8249 0002:000002a6 +85:824b 0002:000002a7 +85:82f9 0002:000002ab +b8:885c 0003:00000045 +b8:885d 0003:00000045 +b8:885e 0003:00000046 +b8:885f 0003:00000047 +b8:8863 0003:00000048 +b8:8866 0003:00000049 +b8:8867 0003:0000004a +b8:886b 0003:0000004b +b8:886e 0003:0000004c +b8:8870 0003:0000004e +b8:8873 0003:0000004f +b8:8874 0003:0000004f +b8:8878 0003:00000051 +b8:8879 0003:00000052 +b8:887a 0003:00000053 +b8:887e 0003:00000054 +b8:8880 0003:00000056 +b8:8881 0003:00000056 +b8:8882 0003:00000057 +b8:8883 0003:0000005a +b8:8884 0003:0000005a +b8:8885 0003:0000005b +b8:8886 0003:0000005c +b8:888a 0003:0000005d +84:f8a6 0004:00000051 +84:f8a9 0004:00000052 +84:f8ac 0004:00000053 +84:f8ad 0004:00000056 +84:f8b0 0004:00000057 +84:f8b3 0004:00000058 +84:f8b4 0004:0000005b +84:f8b7 0004:0000005c +84:f8ba 0004:0000005d +84:f9e0 0004:000000d4 +84:f9e4 0004:000000d5 +84:f9e5 0004:000000d8 +84:f9e8 0004:000000d9 +84:f9ea 0004:000000da +84:f9ec 0004:000000dd +84:f9ef 0004:000000de +84:f9f1 0004:000000e5 +84:f9f2 0004:000000e6 +84:f9f5 0004:000000e7 +84:f9f8 0004:000000e7 +84:f9f9 0004:000000e8 +84:f9fd 0004:000000e9 +84:fa00 0004:000000ea +84:fa02 0004:000000ec +84:fa05 0004:000000ed +84:fa06 0004:000000ee +84:fa0a 0004:000000f1 +84:fa0d 0004:000000f2 +84:fa0f 0004:000000f4 +84:fa11 0004:000000f5 +84:fa12 0004:000000f6 +84:fa14 0004:000000f7 +84:fa15 0004:000000f8 +84:fa19 0004:000000f9 +84:fa1a 0004:000000fa +84:fa1b 0004:000000fb +84:fa1c 0004:000000fe +84:fa1d 0004:000000ff +84:fa1e 0004:00000103 +84:fa1f 0004:00000103 +84:fa20 0004:00000103 +84:fa21 0004:00000104 +84:fa24 0004:00000105 +84:fa27 0004:00000106 +84:fa28 0004:00000107 +84:fa2c 0004:00000108 +84:fa2f 0004:00000109 +84:fa31 0004:0000010b +84:fa34 0004:0000010c +84:fa35 0004:0000010c +84:fa39 0004:0000010e +84:fa3a 0004:00000110 +84:fa3b 0004:00000111 +84:fa3c 0004:00000112 +84:fa40 0004:00000113 +84:fa42 0004:00000114 +84:fa43 0004:00000115 +84:fa44 0004:00000116 +84:fa47 0004:00000117 +84:fa48 0004:00000118 +84:fa49 0004:0000011b +84:fa4a 0004:0000011c +84:fa4c 0004:0000011d +84:fa4d 0004:0000011e +84:fa51 0004:0000011f +84:fa52 0004:00000120 +84:fa53 0004:00000123 +84:fa57 0004:00000124 +84:fa5a 0004:00000127 +84:fa5e 0004:00000128 +84:fa61 0004:0000012c +84:fa64 0004:0000012c +84:fa66 0004:0000012d +84:fa69 0004:0000012e +84:fa6b 0004:0000012f +84:fa6e 0004:0000012f +84:fa70 0004:00000130 +84:fa73 0004:00000131 +84:fa75 0004:00000132 +84:fa78 0004:00000135 +84:fa79 0004:00000139 +84:fa7d 0004:0000013a +84:fa7e 0004:0000013f +84:fa7f 0004:00000140 +84:fa80 0004:00000141 +84:fa81 0004:00000141 +84:fa82 0004:00000145 +84:fa86 0004:00000146 +84:fa87 0004:00000147 +84:fa88 0004:00000148 +84:fa89 0004:00000148 +84:fa8a 0004:00000149 +84:fa8d 0004:0000014a +84:fa8e 0004:0000014b +84:fa8f 0004:0000014c +81:b303 0005:00000003 +81:b307 0005:00000004 +81:b308 0005:00000005 +b8:c81c 0005:00000016 +b8:c81f 0005:00000018 +b8:c823 0005:00000019 +b8:c827 0005:0000001a +b8:c828 0005:0000001b +b8:c829 0005:0000001c +b8:c82c 0005:0000001d +b8:c82e 0005:0000001e +b8:c831 0005:00000020 +b8:c835 0005:00000021 +b8:c839 0005:00000022 +b8:c83a 0005:00000023 +b8:c83b 0005:00000024 +b8:c83e 0005:00000025 +b8:c840 0005:00000026 +b8:c843 0005:00000028 +b8:c847 0005:00000029 +b8:c84b 0005:0000002a +b8:c84c 0005:0000002b +b8:c84d 0005:0000002c +b8:c850 0005:0000002d +b8:c852 0005:0000002e +b8:c855 0005:00000031 +b8:c856 0005:00000034 +b8:c859 0005:00000035 +b8:c85b 0005:00000036 +b8:c85e 0005:00000037 +b8:c862 0005:00000039 +b8:c866 0005:0000003a +b8:c869 0005:0000003b +b8:c86b 0005:0000003c +b8:c86f 0005:0000003e +b8:c873 0005:0000003f +b8:c876 0005:00000040 +b8:c878 0005:00000041 +b8:c87c 0005:00000043 +b8:c880 0005:00000044 diff --git a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json new file mode 100644 index 0000000000..63198cde72 --- /dev/null +++ b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json @@ -0,0 +1,141 @@ +{ + "CLIPCHECK": "85:FF00", + "CLIPLEN": "85:9900", + "CLIPLEN_end": "85:990F", + "CLIPLEN_no_multi": "85:990C", + "CLIPSET": "85:FF1D", + "COLLECTTANK": "B8:80EF", + "MISCFX": "85:FF45", + "NORMAL": "84:8BF2", + "SETFX": "85:FF4E", + "SOUNDFX": "85:FF30", + "SOUNDFX_84": "84:F9E0", + "SPECIALFX": "85:FF3C", + "ammo_loop_table": "84:F896", + "archipelago_chozo_item_plm": "84:F874", + "archipelago_hidden_item_plm": "84:F878", + "archipelago_visible_item_plm": "84:F870", + "c_item": "84:F892", + "config_deathlink": "CE:FF04", + "config_flags": "CE:FF00", + "config_multiworld": "CE:FF00", + "config_player_id": "CE:FF08", + "config_remote_items": "CE:FF06", + "config_sprite": "CE:FF02", + "h_item": "84:F894", + "i_chozo_item": "84:F8AD", + "i_hidden_item": "84:F8B4", + "i_hidden_item_setup": "84:FA5A", + "i_item_setup_shared": "B8:885C", + "i_item_setup_shared_all_items": "B8:8878", + "i_item_setup_shared_alwaysloaded": "B8:8883", + "i_live_pickup": "84:FA79", + "i_live_pickup_multiworld": "B8:817F", + "i_live_pickup_multiworld_end": "B8:81C4", + "i_live_pickup_multiworld_local_item_or_offworld": "B8:819B", + "i_live_pickup_multiworld_own_item": "B8:81B0", + "i_live_pickup_multiworld_own_item1": "B8:81BC", + "i_load_custom_graphics": "84:FA1E", + "i_load_custom_graphics_all_items": "84:FA39", + "i_load_custom_graphics_alwaysloaded": "84:FA49", + "i_load_rando_item": "84:FA61", + "i_load_rando_item_end": "84:FA78", + "i_start_draw_loop": "84:F9F1", + "i_start_draw_loop_all_items": "84:FA0A", + "i_start_draw_loop_hidden": "84:F9EC", + "i_start_draw_loop_non_ammo_item": "84:FA1C", + "i_start_draw_loop_visible_or_chozo": "84:F9E5", + "i_visible_item": "84:F8A6", + "i_visible_item_setup": "84:FA53", + "message_PlaceholderBig": "85:BA8A", + "message_char_table": "85:BA0A", + "message_hook_tilemap_calc": "85:BABC", + "message_hook_tilemap_calc_msgbox_mwrecv": "85:BADC", + "message_hook_tilemap_calc_msgbox_mwsend": "85:BACE", + "message_hook_tilemap_calc_normal": "85:824C", + "message_hook_tilemap_calc_vanilla": "85:BAC9", + "message_item_names": "85:9963", + "message_item_received": "85:B8A3", + "message_item_received_end": "85:B9A3", + "message_item_sent": "85:B7A3", + "message_item_sent_end": "85:B8A3", + "message_multiworld_init_new_messagebox_if_needed": "85:BA95", + "message_multiworld_init_new_messagebox_if_needed_msgbox_mwrecv": "85:BAB1", + "message_multiworld_init_new_messagebox_if_needed_msgbox_mwsend": "85:BAB1", + "message_multiworld_init_new_messagebox_if_needed_vanilla": "85:BAA9", + "message_write_placeholders": "85:B9A3", + "message_write_placeholders_adjust": "85:B9A5", + "message_write_placeholders_end": "85:BA04", + "message_write_placeholders_loop": "85:B9CA", + "message_write_placeholders_notfound": "85:B9DC", + "message_write_placeholders_value_ok": "85:B9DF", + "mw_display_item_sent": "B8:8092", + "mw_handle_queue": "B8:80FF", + "mw_handle_queue_end": "B8:8178", + "mw_handle_queue_loop": "B8:8101", + "mw_handle_queue_new_remote_item": "B8:8151", + "mw_handle_queue_next": "B8:816D", + "mw_handle_queue_perform_receive": "B8:8163", + "mw_hook_main_game": "B8:81C8", + "mw_init": "B8:8011", + "mw_init_end": "B8:8044", + "mw_init_memory": "B8:8000", + "mw_load_sram": "B8:8083", + "mw_receive_item": "B8:80B0", + "mw_receive_item_end": "B8:80E8", + "mw_save_sram": "B8:8070", + "mw_write_message": "B8:8049", + "nonprog_item_eight_palette_indices": "84:F888", + "offworld_graphics_data_item": "89:9200", + "offworld_graphics_data_progression_item": "89:9100", + "p_chozo_item": "84:F972", + "p_chozo_item_end": "84:F9A0", + "p_chozo_item_loop": "84:F98D", + "p_chozo_item_trigger": "84:F999", + "p_etank_hloop": "84:F8FB", + "p_etank_loop": "84:F8BB", + "p_hidden_item": "84:F9A6", + "p_hidden_item_end": "84:F9D8", + "p_hidden_item_loop": "84:F9BD", + "p_hidden_item_loop2": "84:F9A8", + "p_hidden_item_trigger": "84:F9D1", + "p_missile_hloop": "84:F90F", + "p_missile_loop": "84:F8CB", + "p_pb_hloop": "84:F937", + "p_pb_loop": "84:F8EB", + "p_super_hloop": "84:F923", + "p_super_loop": "84:F8DB", + "p_visible_item": "84:F94B", + "p_visible_item_end": "84:F96E", + "p_visible_item_loop": "84:F95B", + "p_visible_item_trigger": "84:F967", + "patch_load_multiworld": "B8:81DF", + "perform_item_pickup": "84:FA7E", + "plm_graphics_entry_offworld_item": "84:F886", + "plm_graphics_entry_offworld_progression_item": "84:F87C", + "plm_sequence_generic_item_0_bitmask": "84:FA90", + "prog_item_eight_palette_indices": "84:F87E", + "rando_item_table": "B8:E000", + "rando_player_id_table": "B8:DC90", + "rando_player_id_table_end": "B8:DE22", + "rando_player_table": "B8:D000", + "rando_seed_data": "B8:CF00", + "sm_item_graphics": "B8:8800", + "sm_item_plm_pickup_sequence_pointers": "B8:882E", + "start_item": "B8:C81C", + "start_item_data_major": "B8:C800", + "start_item_data_minor": "B8:C808", + "start_item_data_reserve": "B8:C818", + "update_graphic": "B8:C856", + "v_item": "84:F890", + "ITEM_RAM": "7E:09A2", + "SRAM_MW_ITEMS_RECV": "70:2000", + "SRAM_MW_ITEMS_RECV_RPTR": "70:2600", + "SRAM_MW_ITEMS_RECV_WPTR": "70:2602", + "SRAM_MW_ITEMS_RECV_SPTR": "70:2604", + "SRAM_MW_ITEMS_SENT_RPTR": "70:2680", + "SRAM_MW_ITEMS_SENT_WPTR": "70:2682", + "SRAM_MW_ITEMS_SENT": "70:2700", + "SRAM_MW_INITIALIZED": "70:26fe", + "CollectedItems": "7E:D86E" +} \ No newline at end of file diff --git a/worlds/sm/variaRandomizer/patches/common/ips/basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips similarity index 54% rename from worlds/sm/variaRandomizer/patches/common/ips/basepatch.ips rename to worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips index b0b34963e8363eeef803c38a86c2410729039342..c1285e1653a11592b890289558b6f8a711681537 100644 GIT binary patch delta 433 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 diff --git a/worlds/sm/data/sourceinfo.txt b/worlds/sm/data/sourceinfo.txt new file mode 100644 index 0000000000..8facb6b249 --- /dev/null +++ b/worlds/sm/data/sourceinfo.txt @@ -0,0 +1,3 @@ +SMBasepatch_prebuilt: +- comes exactly from build/vanilla/ directory of https://github.com/lordlou/SMBasepatch +- keep it in sync with the basepatch repo; do not modify the contents in this repo alone! diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index 22b83ceb5d..9dae1cea87 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -24,8 +24,6 @@ class RomPatcher: 'Removes_Gravity_Suit_heat_protection', # door ASM to skip G4 cutscene when all 4 bosses are dead 'g4_skip.ips', - # basepatch is generated from https://github.com/lordlou/SMBasepatch - 'basepatch.ips' ], # VARIA tweaks 'VariaTweaks' : ['WS_Etank', 'LN_Chozo_SpaceJump_Check_Disable', 'ln_chozo_platform.ips', 'bomb_torizo.ips'], From 79702aba652e4c8d81f93bbf43d0ad50f8f7e2d5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 25 Jul 2022 22:50:07 +0200 Subject: [PATCH 054/138] WebHost: flask caching did a rename --- WebHostLib/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 6280ecdfc6..c9ee8620ce 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,7 +1,7 @@ flask>=2.1.2 pony>=0.7.16 waitress>=2.1.1 -flask-caching>=2.0.0 +Flask-Caching>=2.0.0 Flask-Compress>=1.12 Flask-Limiter>=2.5.0 bokeh>=2.4.3 \ No newline at end of file From c61f77029b4e13993c2688299db4f443057f73d5 Mon Sep 17 00:00:00 2001 From: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Date: Tue, 26 Jul 2022 10:53:30 -0400 Subject: [PATCH 055/138] SC2 docs: Extensive reworks and rewordings. (#809) --- .../docs/en_Starcraft 2 Wings of Liberty.md | 47 +++++----- worlds/sc2wol/docs/setup_en.md | 94 ++++++++++--------- 2 files changed, 74 insertions(+), 67 deletions(-) diff --git a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md index aba882c7d6..8fa20c86f9 100644 --- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md +++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md @@ -1,37 +1,34 @@ # Starcraft 2 Wings of Liberty -## Where is the settings page? - -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a -config file. - ## What does randomization do to this game? -Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is -always able to be completed. Options exist to also shuffle around the mission order of the campaign. +The following unlocks are randomized as items: +1. Your ability to build any non-worker unit (including Marines!). +2. Your ability to upgrade infantry weapons, infantry armor, vehicle weapons, etc. +3. All armory upgrades +4. All laboratory upgrades +5. All mercenaries +6. Small boosts to your starting mineral and vespene gas totals on each mission -## What is the goal of Starcraft 2 when randomized? +You find items by making progress in bonus objectives (like by rescuing allies in 'Zero Hour') and by completing +missions. When you receive items, they will immediately become available, even during a mission, and you will be +notified via a text box in the top-right corner of the game screen. (The text client for StarCraft 2 also records all +items in all worlds.) -The goal remains unchanged. Beat the final mission All In. +Missions are launched only through the text client. The Hyperion is never visited. Aditionally, credits are not used. -## What items and locations get shuffled? +## What is the goal of this game when randomized? -Unit unlocks, upgrade unlocks, armory upgrades, laboratory researches, and mercenary unlocks can be shuffled, and all -bonus objectives, side missions, mission completions are now locations that can contain these items. +The goal is to beat the final mission: 'All In'. The config file determines which variant you must complete. -## What has been changed from vanilla Starcraft 2? +## What non-randomized changes are there from vanilla Starcraft 2? -Some missions have been given more vespene gas available to mine to allow for a wider variety of unit compositions on -those missions. Starports no longer require Factories in order to be built. In 'A Sinister Turn' and 'Echoes -of the Future', you can research protoss air armor and weapon upgrades. +1. Some missions have more vespene geysers available to allow a wider variety of units. +2. Starports no longer require Factories in order to be built. +3. In 'A Sinister Turn' and 'Echoes of the Future', you can research Protoss air weapon/armor upgrades. -## Which items can be in another player's world? - -Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit -certain items to your own world. - -## When the player receives an item, what happens? - -When the player receives an item, they will receive a message through their text client and in game if currently playing - a mission. They will immediately be able to use that unlock/upgrade. +## Which of my items can be in another player's world? +By default, any of StarCraft 2's items (specified above) can be in another player's world. See the +[Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) +for more information on how to change this. \ No newline at end of file diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md index 6697277d9f..1539a21291 100644 --- a/worlds/sc2wol/docs/setup_en.md +++ b/worlds/sc2wol/docs/setup_en.md @@ -1,62 +1,69 @@ -# Starcraft 2 Wings of Liberty Randomizer Setup Guide +# StarCraft 2 Wings of Liberty Randomizer Setup Guide + +This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as where +to obtain a config file for StarCraft 2. ## Required Software -- [Starcraft 2](https://starcraft2.com/en-us/) -- [Starcraft 2 AP Client](https://github.com/ArchipelagoMW/Archipelago) -- [Starcraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData) +- [StarCraft 2](https://starcraft2.com/en-us/) +- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) +- [StarCraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData) -## General Concept +## How do I install this randomizer? -Starcraft 2 AP Client launches a custom version of Starcraft 2 running modified Wings of Liberty campaign maps - to allow for randomization of the items +1. Install StarCraft 2 and Archipelago using the first two links above. (The StarCraft 2 client for Archipelago is + included by default.) +2. Click the third link above and follow the instructions there. +3. Linux users should also follow the instructions found at the bottom of this page + (["Running in Linux"](#running-in-linux)). -## Installation Procedures +## Where do I get a config file (aka "YAML") for this game? -Follow the installation directions at the -[Starcraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData) page you can find the .zip -files on the releases page. After it is installed, just run ArchipelagoStarcraft2Client.exe to start the client and connect -to a Multiworld Game. +The [Player Settings](https://archipelago.gg/games/Starcraft%202%20Wings%20of%20Liberty/player-settings) page on this +website allows you to choose your personal settings for the randomizer and download them into a config file. Remember +the name you type in the `Player Name` box; that's the "slot name" the client will ask you for when you attempt to +connect! -## Joining a MultiWorld Game +### And why do I need a config file? + +Config files tell Archipelago how you'd like your game to be randomized, even if you're only using default settings. +When you're setting up a multiworld, every world needs its own config file. +Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en#creating-a-yaml) for more information. + +## How do I join a MultiWorld game? 1. Run ArchipelagoStarcraft2Client.exe. -2. Type in `/connect [server ip]`. -3. Insert slot name and password as prompted. -4. Once connected, use `/unfinished` to find what missions you can play and `/play [mission id]` to launch a mission. - For new games under default settings the first mission available will always be Liberation Day[1] playable using the - command `/play 1`. +2. Type `/connect [server ip]`. +3. Type your slot name and the server's password when prompted. +4. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see every mission. By default, + only 'Liberation Day' will be available at the beginning. Just click on a mission to start it! -## Where do I get a config file? +## The game isn't launching when I try to start a mission. -The [Player Settings](/games/Starcraft%202%20Wings%20of%20Liberty/player-settings) page on the website allows you to -configure your personal settings and export them into a config file. +First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If the below fix doesn't +work for you, and you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) +tech-support channel for help. Please include a specific description of what's going wrong and attach your log file to +your message. -## Game isn't launching when I type /play +### Check your installation -First check the log file for issues (stored at [Archipelago Directory]/logs/SC2Client.txt. There is sometimes an issue -where the client can not find Starcraft 2. Usually 'Documents/StarCraft II/ExecuteInfo.txt' is checked to find where -Starcraft 2 is installed. On some computers particularly if you have OneDrive running this may fail. The following -directions may help you in this case if you are on Windows. - -1. Navigate to '%userprofile%'. Easiest way to do this is to hit Windows key+R type in `%userprofile%` and hit run or -type in `%userprofile%` in the navigation bar of your file explorer. -2. If it does not exist create a folder in her named 'Documents'. -3. Locate your 'My Documents' folder on your PC. If you navigate to 'My PC' on the sidebar of file explorer should be a -link to this folder there labeled 'Documents'. -4. Find a folder labeled 'StarCraft II' and copy it. -5. Paste this 'StarCraft II' folder into the folder created or found in step 2. - -These steps have been shown to work for some people for some people having issues with launching the game. If you are -still having issues check out our [Discord](https://discord.com/invite/8Z65BR2) for help. +Make sure you've followed the installation instructions completely. Specifically, make sure that you've placed the Maps +and Mods folders directly inside the StarCraft II installation folder. They should be in the same location as the +SC2Data, Support, Support64, and Versions folders. ## Running in Linux -To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine then run the Linux build of the Archipelago client. +To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine, then run the Linux build +of the Archipelago client. -Make sure you have StarCraft 2 installed using Wine and you have followed the [Installation Procedures](#installation-procedures) to add the Archipelago maps to the correct location. You will not need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the Lutris installer. +Make sure you have StarCraft 2 installed using Wine, and that you have followed the +[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. You will not +need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the +Lutris installer. -Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables to the relevant locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same folder as the script. +Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant +locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same +folder as the script. ```sh # Let the client know we're running SC2 in Wine @@ -81,8 +88,11 @@ ARCHIPELAGO="$(ls ${PATH_TO_ARCHIPELAGO:-$(dirname $0)}/Archipelago_*.AppImage | $ARCHIPELAGO Starcraft2Client ``` -For Lutris installs, you can run `lutris -l` to get the numerical ID of your StarCraft II install, then run the command below, replacing **${ID}** with the numerical ID. +For Lutris installs, you can run `lutris -l` to get the numerical ID of your StarCraft II install, then run the command +below, replacing **${ID}** with the numerical ID. lutris lutris:rungameid/${ID} --output-script sc2.sh -This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code above into the existing script. +This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path +to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code +above into the existing script. From 73afab67c802b9c5085258254ed1bc07ce283dba Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Jul 2022 22:21:06 +0200 Subject: [PATCH 056/138] LttP: fix deprecated use of isSet() (#831) --- LttPAdjuster.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 3368620e6c..3de6e3b13a 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -289,7 +289,7 @@ def run_sprite_update(): else: top.withdraw() task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set()) - while not done.isSet(): + while not done.is_set(): task.do_events() logging.info("Done updating sprites") @@ -300,6 +300,7 @@ def update_sprites(task, on_finish=None): sprite_dir = user_path("data", "sprites", "alttpr") os.makedirs(sprite_dir, exist_ok=True) ctx = get_cert_none_ssl_context() + def finished(): task.close_window() if on_finish: From 489450d3fabe3ca639199d865ad26b8f35f64e7b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 26 Jul 2022 16:44:32 +0200 Subject: [PATCH 057/138] SNIClient: fix program not exiting if SNI does not exist nor is running --- SNIClient.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index 072d04cc99..aad231691b 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -188,7 +188,10 @@ class Context(CommonContext): async def shutdown(self): await super(Context, self).shutdown() if self.snes_connect_task: - await self.snes_connect_task + try: + await asyncio.wait_for(self.snes_connect_task, 1) + except asyncio.TimeoutError: + self.snes_connect_task.cancel() def on_package(self, cmd: str, args: dict): if cmd in {"Connected", "RoomUpdate"}: @@ -598,7 +601,7 @@ class SNESState(enum.IntEnum): SNES_ATTACHED = 3 -def launch_sni(ctx: Context): +def launch_sni(): sni_path = Utils.get_options()["lttp_options"]["sni"] if not os.path.isdir(sni_path): @@ -636,11 +639,9 @@ async def _snes_connect(ctx: Context, address: str): address = f"ws://{address}" if "://" not in address else address snes_logger.info("Connecting to SNI at %s ..." % address) seen_problems = set() - succesful = False - while not succesful: + while 1: try: snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None) - succesful = True except Exception as e: problem = "%s" % e # only tell the user about new problems, otherwise silently lay in wait for a working connection @@ -650,7 +651,7 @@ async def _snes_connect(ctx: Context, address: str): if len(seen_problems) == 1: # this is the first problem. Let's try launching SNI if it isn't already running - launch_sni(ctx) + launch_sni() await asyncio.sleep(1) else: From e5b868e0e974ec480e61ed547cc2af8cdf2154cd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Jul 2022 23:09:40 +0200 Subject: [PATCH 058/138] WebHost: fix 30 days cutoff for stats (#826) --- WebHostLib/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/stats.py b/WebHostLib/stats.py index 9a164d02cb..131f807d2d 100644 --- a/WebHostLib/stats.py +++ b/WebHostLib/stats.py @@ -17,7 +17,7 @@ from .models import Room def get_db_data(): games_played = defaultdict(Counter) total_games = Counter() - cutoff = date.today()-timedelta(days=30000) + cutoff = date.today()-timedelta(days=30) room: Room for room in select(room for room in Room if room.creation_time >= cutoff): for slot in room.seed.slots: From 4565b3af8dc8bccefc417bdc9724190db8495b73 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 26 Jul 2022 19:25:49 +0200 Subject: [PATCH 059/138] DKC3: fix missing default options in Utils.py --- Utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index e13a5fe773..423bb3b469 100644 --- a/Utils.py +++ b/Utils.py @@ -277,7 +277,12 @@ def get_default_options() -> dict: }, "oot_options": { "rom_file": "The Legend of Zelda - Ocarina of Time.z64", - } + }, + "dkc3_options": { + "rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc", + "sni": "SNI", + "rom_start": True, + }, } return options From e849e4792dda4d97e657a8b542149d0eb1fc32d1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Jul 2022 23:36:20 +0200 Subject: [PATCH 060/138] WebHost: games played per day plot per game on stats page (#827) * WebHost: generate stats page palette for maximum hue difference between neighbours. * WebHost: add per game played stats --- WebHostLib/stats.py | 74 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/WebHostLib/stats.py b/WebHostLib/stats.py index 131f807d2d..a647be5ee5 100644 --- a/WebHostLib/stats.py +++ b/WebHostLib/stats.py @@ -1,20 +1,24 @@ from collections import Counter, defaultdict -from itertools import cycle +from colorsys import hsv_to_rgb from datetime import datetime, timedelta, date from math import tau +import typing from bokeh.embed import components -from bokeh.palettes import Dark2_8 as palette +from bokeh.models import HoverTool from bokeh.plotting import figure, ColumnDataSource from bokeh.resources import INLINE +from bokeh.colors import RGB from flask import render_template from pony.orm import select from . import app, cache from .models import Room +PLOT_WIDTH = 600 -def get_db_data(): + +def get_db_data() -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]: games_played = defaultdict(Counter) total_games = Counter() cutoff = date.today()-timedelta(days=30) @@ -26,29 +30,72 @@ def get_db_data(): return total_games, games_played +def get_color_palette(colors_needed: int) -> typing.List[RGB]: + colors = [] + # colors_needed +1 to prevent first and last color being too close to each other + colors_needed += 1 + + for x in range(0, 361, 360 // colors_needed): + # a bit of noise on value to add some luminosity difference + colors.append(RGB(*(val * 255 for val in hsv_to_rgb(x / 360, 0.8, 0.8 + (x / 1800))))) + + # splice colors for maximum hue contrast. + colors = colors[::2] + colors[1::2] + + return colors + + +def create_game_played_figure(all_games_data: typing.Dict[datetime.date, typing.Dict[str, int]], + game: str, color: RGB) -> figure: + occurences = [] + days = [day for day, game_data in all_games_data.items() if game_data[game]] + for day in days: + occurences.append(all_games_data[day][game]) + data = { + "days": [datetime.combine(day, datetime.min.time()) for day in days], + "played": occurences + } + + plot = figure( + title=f"{game} Played Per Day", x_axis_type='datetime', x_axis_label="Date", + y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500, + toolbar_location=None, tools="", + # setting legend to False seems broken in bokeh currently? + # legend=False + ) + + hover = HoverTool(tooltips=[("Date:", "@days{%F}"), ("Played:", "@played")], formatters={"@days": "datetime"}) + plot.add_tools(hover) + plot.vbar(x="days", top="played", legend_label=game, color=color, source=ColumnDataSource(data=data), width=1) + return plot + + @app.route('/stats') -@cache.memoize(timeout=60*60) # regen once per hour should be plenty +@cache.memoize(timeout=60 * 60) # regen once per hour should be plenty def stats(): plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date", - y_axis_label="Games Played", sizing_mode="scale_both", width=500, height=500) + y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500) total_games, games_played = get_db_data() days = sorted(games_played) - cyc_palette = cycle(palette) + color_palette = get_color_palette(len(total_games)) + game_to_color: typing.Dict[str, RGB] = {game: color for game, color in zip(total_games, color_palette)} for game in sorted(total_games): occurences = [] for day in days: occurences.append(games_played[day][game]) plot.line([datetime.combine(day, datetime.min.time()) for day in days], - occurences, legend_label=game, line_width=2, color=next(cyc_palette)) + occurences, legend_label=game, line_width=2, color=game_to_color[game]) total = sum(total_games.values()) pie = figure(plot_height=350, title=f"Games Played in the Last 30 Days (Total: {total})", toolbar_location=None, tools="hover", tooltips=[("Game:", "@games"), ("Played:", "@count")], - sizing_mode="scale_both", width=500, height=500) + sizing_mode="scale_both", width=PLOT_WIDTH, height=500, x_range=(-0.5, 1.2)) pie.axis.visible = False + pie.xgrid.visible = False + pie.ygrid.visible = False data = { "games": [], @@ -65,12 +112,15 @@ def stats(): current_angle += angle data["end_angles"].append(current_angle) - data["colors"] = [element[1] for element in sorted((game, color) for game, color in - zip(data["games"], cycle(palette)))] - pie.wedge(x=0.5, y=0.5, radius=0.5, + data["colors"] = [game_to_color[game] for game in data["games"]] + + pie.wedge(x=0, y=0, radius=0.5, start_angle="start_angles", end_angle="end_angles", fill_color="colors", source=ColumnDataSource(data=data), legend_field="games") - script, charts = components((plot, pie)) + per_game_charts = [create_game_played_figure(games_played, game, game_to_color[game]) for game in total_games + if total_games[game] > 1] + + script, charts = components((plot, pie, *per_game_charts)) return render_template("stats.html", js_resources=INLINE.render_js(), css_resources=INLINE.render_css(), chart_data=script, charts=charts) From 7d9203ef84699955defab95fa4a65502edcf4636 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 28 Jul 2022 00:05:47 +0200 Subject: [PATCH 061/138] CI: update SNI to 0.0.82 --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb9fda69b4..4138f93f04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: python-version: '3.8' - name: Download run-time dependencies run: | - Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-windows-amd64.zip -OutFile sni.zip + Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-windows-amd64.zip -OutFile sni.zip Expand-Archive -Path sni.zip -DestinationPath SNI -Force Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force @@ -63,7 +63,7 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e5095cff2..aa82883ff1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI From c02f355479e85acaa2b9e85e19a52f67637c533d Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 28 Jul 2022 06:04:48 -0400 Subject: [PATCH 062/138] Smz3 no progression gt fix (#818) --- worlds/smz3/__init__.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index e440eab2c9..77168d5674 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -10,6 +10,7 @@ from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassi from worlds.generic.Rules import set_rule import worlds.smz3.TotalSMZ3.Item as TotalSMZ3Item from worlds.smz3.TotalSMZ3.World import World as TotalSMZ3World +from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic from worlds.smz3.TotalSMZ3.Location import LocationType, locations_start_id, Location as TotalSMZ3Location from worlds.smz3.TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray @@ -68,6 +69,8 @@ class SMZ3World(World): remote_items: bool = False remote_start_inventory: bool = False + locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations} + # first added for 0.2.6 required_client_version = (0, 2, 6) @@ -364,6 +367,7 @@ class SMZ3World(World): item.item.Progression = False item.location.event = False self.unreachable.append(item.location) + self.JunkFillGT() def get_pre_fill_items(self): if (not self.smz3World.Config.Keysanity): @@ -377,6 +381,23 @@ class SMZ3World(World): def write_spoiler(self, spoiler_handle: TextIO): self.world.spoiler.unreachables.update(self.unreachable) + def JunkFillGT(self): + for loc in self.locations.values(): + if loc.name in self.locationNamesGT and loc.item is None: + poolLength = len(self.world.itempool) + # start looking at a random starting index and loop at start if no match found + for i in range(self.world.random.randint(0, poolLength), poolLength): + if not self.world.itempool[i].advancement: + itemFromPool = self.world.itempool.pop(i) + break + else: + for i in range(0, poolLength): + if not self.world.itempool[i].advancement: + itemFromPool = self.world.itempool.pop(i) + break + self.world.push_item(loc, itemFromPool, False) + loc.event = False + def FillItemAtLocation(self, itemPool, itemType, location): itemToPlace = TotalSMZ3Item.Item.Get(itemPool, itemType, self.smz3World) if (itemToPlace == None): @@ -401,7 +422,7 @@ class SMZ3World(World): raise Exception(f"Tried to front fill {item.Name} in, but no location was available") location.Item = item - itemFromPool = next((i for i in self.world.itempool if i.player == self.player and i.name == item.Type.name), None) + itemFromPool = next((i for i in self.world.itempool if i.player == self.player and i.name == item.Type.name and i.advancement == item.Progression), None) if itemFromPool is not None: self.world.get_location(location.Name, self.player).place_locked_item(itemFromPool) self.world.itempool.remove(itemFromPool) From fd6a0b547fc0d9925a8cd16ececd44e16b69835f Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 28 Jul 2022 23:43:35 +0200 Subject: [PATCH 063/138] Witness: Fatal logic bug fix (#837) * Renamed some event items * Fatal logic bug: Door panels did not check their symbol items --- worlds/witness/player_logic.py | 6 +++--- worlds/witness/rules.py | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index eb57f2c6a0..3639e836df 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -319,9 +319,9 @@ class WitnessPlayerLogic: "0x00037": "Monastery Branch Panels Activate", "0x0A079": "Access to Bunker Laser", "0x0A3B5": "Door to Tutorial Discard Opens", - "0x00139": "Keep Hedges 2 Turns On", - "0x019DC": "Keep Hedges 3 Turns On", - "0x019E7": "Keep Hedges 4 Turns On", + "0x00139": "Keep Hedges 1 Knowledge", + "0x019DC": "Keep Hedges 2 Knowledge", + "0x019E7": "Keep Hedges 3 Knowledge", "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", "0x09F7F": "Mountain Access", "0x0367C": "Quarry Laser Mill Requirement Met", diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index cd1fae1235..2b9888b361 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -116,12 +116,7 @@ class WitnessLogic(LogicMixin): valid_option = True for panel in option: - if panel in player_logic.DOOR_ITEMS_BY_ID: - if all({not self.has(item, player) for item in player_logic.DOOR_ITEMS_BY_ID[panel]}): - valid_option = False - break - - elif not self._witness_can_solve_panel(panel, world, player, player_logic, locat): + if not self._witness_can_solve_panel(panel, world, player, player_logic, locat): valid_option = False break From 9acaf1c27938cba28d46530cc70a13318afc7577 Mon Sep 17 00:00:00 2001 From: Jarno Date: Fri, 29 Jul 2022 01:11:52 +0200 Subject: [PATCH 064/138] [Docs] Further explained the mythical `InvalidPacket` (#828) * [Docs] Further explained the mythical `InvalidPacket` * Fixed header category * Update docs/network protocol.md Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update docs/network protocol.md Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Hussein Farran Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Hussein Farran --- docs/network protocol.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index b5553c372c..05a58d0598 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -191,8 +191,23 @@ Sent to clients after a client requested this message be sent to them, more info ### InvalidPacket Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for. +#### Arguments +| Name | Type | Notes | +| ---- | ---- | ----- | +| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. | +| original_cmd | Optional[str] | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. | +| text | str | A descriptive message of the problem at hand. | + +##### PacketProblemType +`PacketProblemType` indicates the type of problem that was detected in the faulty packet, the known problem types are below but others may be added in the future. + +| Type | Notes | +| ---- | ----- | +| cmd | `cmd` argument of the faulty packet that could not be parsed correctly. | +| arguments | Arguments of the faulty packet which were not correct. | + ### Retrieved -Sent to clients as a response the a [Get](#Get) package +Sent to clients as a response the a [Get](#Get) package. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | From f3d966897f3bf4111bd548fc562d9ea3004a4027 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Thu, 28 Jul 2022 19:13:00 -0400 Subject: [PATCH 065/138] Prevent Krematoa Crash (#832) * Prevent Krematoa Crash, add crash robustness * Remove print statements * Don't remove ctx.rom if save file dies * Consolidate logic for readability --- worlds/dkc3/Client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index 12643a5fcf..8ac20131f3 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -77,9 +77,9 @@ async def dkc3_game_watcher(ctx: Context): # DKC3_TODO: Handle non-included checks new_checks.append(loc_id) - save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) - if save_file_name is None or save_file_name[0] == 0x00: - # We have somehow exited the save file + verify_save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) + if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name != save_file_name: + # We have somehow exited the save file (or worse) return rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) @@ -116,17 +116,18 @@ async def dkc3_game_watcher(ctx: Context): # Handle Coin Displays current_level = await snes_read(ctx, WRAM_START + 0x5E3, 0x5) - if item.item == 0xDC3002 and (current_level[0] == 0x0A and current_level[2] == 0x00 and current_level[4] == 0x03): + overworld_locked = ((await snes_read(ctx, WRAM_START + 0x5FC, 0x1))[0] == 0x01) + if item.item == 0xDC3002 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x00 and current_level[4] == 0x03): # Bazaar and Barter item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1) new_item_count = item_count[0] + 1 snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count])) - elif item.item == 0xDC3002 and current_level[0] == 0x04: + elif item.item == 0xDC3002 and not overworld_locked and current_level[0] == 0x04: # Swanky item_count = await snes_read(ctx, WRAM_START + 0xA26, 0x1) new_item_count = item_count[0] + 1 snes_buffered_write(ctx, WRAM_START + 0xA26, bytes([new_item_count])) - elif item.item == 0xDC3003 and (current_level[0] == 0x0A and current_level[2] == 0x08 and current_level[4] == 0x01): + elif item.item == 0xDC3003 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x08 and current_level[4] == 0x01): # Boomer item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1) new_item_count = item_count[0] + 1 From d817fdcfdb65138ec589d4c2627a98f9aa1fe397 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 29 Jul 2022 01:18:59 +0200 Subject: [PATCH 066/138] Doc: move Running from source from wiki to docs (#797) * Doc: move "Running from source" from wiki to docs/ * Doc: update links and reformat running from source * Doc: implement suggestions in "Running from source" thanks @alwaysintreble * Doc: update link to "Running from source" also link docs/ folder * Doc: Running from source: Apply suggestions from code review Co-authored-by: KonoTyran Co-authored-by: KonoTyran --- README.md | 4 +-- docs/running from source.md | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 docs/running from source.md diff --git a/README.md b/README.md index a3a06d480b..9403159c74 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEnt ## Running Archipelago For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only. -If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/ArchipelagoMW/Archipelago/wiki/Running-from-source). +If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md). ## Related Repositories This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present. @@ -68,7 +68,7 @@ Contributions are welcome. We have a few asks of any new contributors. Otherwise, we tend to judge code on a case to case basis. It is a generally good idea to stick to PEP-8 guidelines to ensure consistency with existing code. (And to make the linter happy.) -For adding a new game to Archipelago and other documentation on how Archipelago functions, please see the docs folder for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord. +For adding a new game to Archipelago and other documentation on how Archipelago functions, please see [the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord. ## FAQ For frequently asked questions see the website's [FAQ Page](https://archipelago.gg/faq/en/) diff --git a/docs/running from source.md b/docs/running from source.md new file mode 100644 index 0000000000..4360b28c16 --- /dev/null +++ b/docs/running from source.md @@ -0,0 +1,58 @@ +# Running From Source + +If you just want to play and there is a compiled version available on the +[Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases), +use that version. These steps are for developers or platforms without compiled releases available. + +## General + +What you'll need: + * Python 3.8.7 or newer + * pip (Depending on platform may come included) + * A C compiler + * possibly optional, read OS-specific sections + +Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the +required modules and after pressing enter proceed to install everything automatically. +After this, you should be able to run the programs. + + +## Windows + +Recommended steps + * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) + * Download and install full Visual Studio from + [Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/) + or an older "Build Tools for Visual Studio" from + [Visual Studio Older Downloads](https://visualstudio.microsoft.com/vs/older-downloads/). + + * Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details + * This step is optional. Pre-compiled modules are pinned on + [Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808) + + * It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/) + * Run Generate.py which will prompt installation of missing modules, press enter to confirm + + +## macOS + +Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md). + + +## Optional: A Link to the Past Enemizer + +Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an +error if it is required. + +You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases). +It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer +setting in host.yaml at your Enemizer executable. + + +## Optional: SNI + +SNI is required to use SNIClient. If not integrated into the project, it has to be started manually. + +You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases). +It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in +host.yaml at your SNI folder. From 2ff7e83ad970235c8106a120898661da1b9eeddd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Jul 2022 12:01:26 +0200 Subject: [PATCH 067/138] WebHost: make a deeply buried if tree for games a bit more automatic --- WebHostLib/__init__.py | 4 ++++ WebHostLib/templates/macros.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index c0179b2238..a44afc744a 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -69,6 +69,10 @@ class B64UUIDConverter(BaseConverter): app.url_map.converters["suuid"] = B64UUIDConverter app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') +# has automatic patch integration +import Patch +app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types + def get_world_theme(game_name: str): if game_name in AutoWorldRegister.world_types: diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 6ed2ca492a..9a92edbbf1 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -40,7 +40,7 @@ {% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %} Download APSM64EX File... - {% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid", "SMZ3"] %} + {% elif patch.game | supports_apdeltapatch %} Download Patch File... {% elif patch.game == "Dark Souls III" %} From 07450bb83dd0db8828f51a88a7275aa2d0a42423 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Thu, 28 Jul 2022 19:51:22 -0400 Subject: [PATCH 068/138] Migrate DKC3 to APDeltaPatch (#838) * Add DKC3 to APDeltaPatch * Undo unintended commit * More undoing * Remove else clause --- worlds/dkc3/Rom.py | 11 ++++++++++- worlds/dkc3/__init__.py | 12 ++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 821143090b..7e83589ffa 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -1,5 +1,5 @@ import Utils -from Patch import read_rom +from Patch import read_rom, APDeltaPatch from .Locations import lookup_id_to_name, all_locations from .Levels import level_list, level_dict @@ -529,6 +529,15 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x3B97EA, 0xEA) +class DKC3DeltaPatch(APDeltaPatch): + hash = USHASH + game = "Donkey Kong Country 3" + patch_file_ending = ".apdkc3" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index d9e73a7ec3..423693470d 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -12,7 +12,7 @@ from .Levels import level_list from .Rules import set_rules from .Names import ItemName, LocationName from ..AutoWorld import WebWorld, World -from .Rom import LocalRom, patch_rom, get_base_rom_path +from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch import Patch @@ -138,19 +138,23 @@ class DKC3World(World): patch_rom(self.world, rom, self.player, self.active_level_list) self.active_level_list.append(LocationName.rocket_rush_region) - + outfilepname = f'_P{player}' outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ if world.player_name[player] != 'Player%d' % player else '' rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') rom.write_to_file(rompath) - Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player], game=Patch.GAME_DKC3) - os.unlink(rompath) self.rom_name = rom.name + + patch = DKC3DeltaPatch(os.path.splitext(rompath)[0]+DKC3DeltaPatch.patch_file_ending, player=player, + player_name=world.player_name[player], patched_path=rompath) + patch.write() except: raise finally: + if os.path.exists(rompath): + os.unlink(rompath) self.rom_name_available_event.set() # make sure threading continues and errors are collected def modify_multidata(self, multidata: dict): From afc9c772be12241b0559511b47063ff7dfae2030 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 30 Jul 2022 12:42:02 -0400 Subject: [PATCH 069/138] Sm broken start location fix (#841) * - fixed basepatches application order breaking (at least) starting location --- worlds/sm/__init__.py | 6 ++++-- worlds/sm/variaRandomizer/randomizer.py | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 1de316269a..bdb4b11eeb 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -271,7 +271,7 @@ class SMWorld(World): data.append(w1) return data - def APPatchRom(self, romPatcher): + def APPrePatchRom(self, romPatcher): # first apply the sm multiworld code patch named 'basepatch' (also has empty tables that we'll overwrite), # + apply some patches from varia that we want to be always-on. # basepatch and variapatches are both generated from https://github.com/lordlou/SMBasepatch @@ -279,6 +279,8 @@ class SMWorld(World): "data", "SMBasepatch_prebuilt", "multiworld-basepatch.ips")) romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__), "data", "SMBasepatch_prebuilt", "variapatches.ips")) + + def APPostPatchRom(self, romPatcher): symbols = get_sm_symbols(os.path.join(os.path.dirname(__file__), "data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json")) multiWorldLocations = [] @@ -504,7 +506,7 @@ class SMWorld(World): outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc') try: - self.variaRando.PatchRom(outputFilename, self.APPatchRom) + self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom) self.write_crc(outputFilename) self.rom_name = self.romName except: diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index ebb87c520b..3f6a9cc5fa 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -697,7 +697,7 @@ class VariaRandomizer: #if args.patchOnly == False: # randoExec.postProcessItemLocs(itemLocs, args.hideItems) - def PatchRom(self, outputFilename, customPatchApply = None): + def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None): args = self.args optErrMsgs = self.optErrMsgs @@ -749,6 +749,9 @@ class VariaRandomizer: else: romPatcher = RomPatcher(magic=args.raceMagic) + if customPrePatchApply != None: + customPrePatchApply(romPatcher) + if args.hud == True or args.majorsSplit == "FullWithHUD": args.patches.append("varia_hud.ips") if args.patchOnly == False: @@ -767,8 +770,8 @@ class VariaRandomizer: # don't color randomize custom ships args.shift_ship_palette = False - if customPatchApply != None: - customPatchApply(romPatcher) + if customPostPatchApply != None: + customPostPatchApply(romPatcher) # we have to write ips to ROM before doing our direct modifications which will rewrite some parts (like in credits), # but in web mode we only want to generate a global ips at the end From 75165803a061db7eb15b61623d0dbedfc3257d13 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 31 Jul 2022 05:08:41 -0400 Subject: [PATCH 070/138] Sm smz3 create item fix (#844) --- worlds/sm/__init__.py | 2 +- worlds/smz3/__init__.py | 97 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index bdb4b11eeb..a7445c01a5 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -598,7 +598,7 @@ class SMWorld(World): def create_item(self, name: str) -> Item: item = next(x for x in ItemManager.Items.values() if x.Name == name) - return SMItem(item.Name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Name], + return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], player=self.player) def get_filler_item_name(self) -> str: diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 77168d5674..7c519ec068 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -8,6 +8,7 @@ from typing import Dict, Set, TextIO from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, RegionType, CollectionState, \ Tutorial from worlds.generic.Rules import set_rule +from worlds.smz3.TotalSMZ3.Item import ItemType import worlds.smz3.TotalSMZ3.Item as TotalSMZ3Item from worlds.smz3.TotalSMZ3.World import World as TotalSMZ3World from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower @@ -80,6 +81,99 @@ class SMZ3World(World): self.unreachable = [] super().__init__(world, player) + @classmethod + def isProgression(cls, itemType): + progressionTypes = { + ItemType.ProgressiveShield, + ItemType.ProgressiveSword, + ItemType.Bow, + ItemType.Hookshot, + ItemType.Mushroom, + ItemType.Powder, + ItemType.Firerod, + ItemType.Icerod, + ItemType.Bombos, + ItemType.Ether, + ItemType.Quake, + ItemType.Lamp, + ItemType.Hammer, + ItemType.Shovel, + ItemType.Flute, + ItemType.Bugnet, + ItemType.Book, + ItemType.Bottle, + ItemType.Somaria, + ItemType.Byrna, + ItemType.Cape, + ItemType.Mirror, + ItemType.Boots, + ItemType.ProgressiveGlove, + ItemType.Flippers, + ItemType.MoonPearl, + ItemType.HalfMagic, + + ItemType.Grapple, + ItemType.Charge, + ItemType.Ice, + ItemType.Wave, + ItemType.Plasma, + ItemType.Varia, + ItemType.Gravity, + ItemType.Morph, + ItemType.Bombs, + ItemType.SpringBall, + ItemType.ScrewAttack, + ItemType.HiJump, + ItemType.SpaceJump, + ItemType.SpeedBooster, + + ItemType.ETank, + ItemType.ReserveTank, + + ItemType.BigKeyGT, + ItemType.KeyGT, + ItemType.BigKeyEP, + ItemType.BigKeyDP, + ItemType.BigKeyTH, + ItemType.BigKeyPD, + ItemType.BigKeySP, + ItemType.BigKeySW, + ItemType.BigKeyTT, + ItemType.BigKeyIP, + ItemType.BigKeyMM, + ItemType.BigKeyTR, + + ItemType.KeyHC, + ItemType.KeyCT, + ItemType.KeyDP, + ItemType.KeyTH, + ItemType.KeyPD, + ItemType.KeySP, + ItemType.KeySW, + ItemType.KeyTT, + ItemType.KeyIP, + ItemType.KeyMM, + ItemType.KeyTR, + + ItemType.CardCrateriaL1, + ItemType.CardCrateriaL2, + ItemType.CardCrateriaBoss, + ItemType.CardBrinstarL1, + ItemType.CardBrinstarL2, + ItemType.CardBrinstarBoss, + ItemType.CardNorfairL1, + ItemType.CardNorfairL2, + ItemType.CardNorfairBoss, + ItemType.CardMaridiaL1, + ItemType.CardMaridiaL2, + ItemType.CardMaridiaBoss, + ItemType.CardWreckedShipL1, + ItemType.CardWreckedShipBoss, + ItemType.CardLowerNorfairL1, + ItemType.CardLowerNorfairBoss, + } + return itemType in progressionTypes + @classmethod def stage_assert_generate(cls, world): base_combined_rom = get_base_rom_bytes() @@ -337,7 +431,8 @@ class SMZ3World(World): return False def create_item(self, name: str) -> Item: - return SMZ3Item(name, ItemClassification.progression, + return SMZ3Item(name, + ItemClassification.progression if SMZ3World.isProgression(TotalSMZ3Item.ItemType[name]) else ItemClassification.filler, TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], self.player, TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[name], self)) From 3bc9392e5b9eb7d4a66a93ef55fe861894a3fec4 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 31 Jul 2022 05:02:36 -0500 Subject: [PATCH 071/138] Core: have generation print plando settings as string instead of numbers (#843) * have generation print plando settings as string instead of numbers * Change to __str__ * Make to_string not a class method * Suggested fix Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Fix the fix * Better quotes Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Generate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Generate.py b/Generate.py index 125fab4163..7cfe7a504c 100644 --- a/Generate.py +++ b/Generate.py @@ -61,6 +61,11 @@ class PlandoSettings(enum.IntFlag): else: return base | part + def __str__(self) -> str: + if self.value: + return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value)) + return "Off" + def mystery_argparse(): options = get_options() From d1f34d088b3b4b36f26b41df0357d62781e0d3c4 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sun, 31 Jul 2022 15:17:26 +0000 Subject: [PATCH 072/138] WebHost: Add links to "Setup Guides" in Supported Games page (#847) * WebHost: Add links to "Setup Guides" in Supported Games page * Remove a hanging console.log() I left in --- WebHostLib/static/assets/tutorialLanding.js | 10 ++++++++++ WebHostLib/templates/supportedGames.html | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/WebHostLib/static/assets/tutorialLanding.js b/WebHostLib/static/assets/tutorialLanding.js index d00eefa307..b820cc3465 100644 --- a/WebHostLib/static/assets/tutorialLanding.js +++ b/WebHostLib/static/assets/tutorialLanding.js @@ -23,6 +23,7 @@ window.addEventListener('load', () => { games.forEach((game) => { const gameTitle = document.createElement('h2'); gameTitle.innerText = game.gameTitle; + gameTitle.id = `${encodeURIComponent(game.gameTitle)}`; tutorialDiv.appendChild(gameTitle); game.tutorials.forEach((tutorial) => { @@ -65,6 +66,15 @@ window.addEventListener('load', () => { showError(); console.error(error); } + + // Check if we are on an anchor when coming in, and scroll to it. + const hash = window.location.hash; + if (hash) { + const offset = 128; // To account for navbar banner at top of page. + window.scrollTo(0, 0); + const rect = document.getElementById(hash.slice(1)).getBoundingClientRect(); + window.scrollTo(rect.left, rect.top - offset); + } }; ajax.open('GET', `${window.location.origin}/static/generated/tutorials.json`, true); ajax.send(); diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 00f49d6d18..125551fbb9 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -15,10 +15,15 @@

{{ world.__doc__ | default("No description provided.", true) }}
Game Page + {% if world.web.tutorials %} | + Setup Guides + {% endif %} {% if world.web.settings_page is string %} + | Settings Page {% elif world.web.settings_page %} + | Settings Page {% endif %} {% if world.web.bug_report_page %} From 4b8500096009b230ca81778bc74d0643620408c8 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 31 Jul 2022 14:01:39 -0700 Subject: [PATCH 073/138] Fixed a crafting category bug related to fluids. (#848) --- worlds/factorio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 33f1809cf7..9dc1febcba 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -221,7 +221,7 @@ class Factorio(World): # Return the liquid to the pool and get a new ingredient. pool.append(new_ingredient) new_ingredient = pool.pop(0) - liquids_used += 1 + liquids_used += 1 if new_ingredient in fluids else 0 new_ingredients[new_ingredient] = 1 return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients, original.products, original.energy) From 57979b92875814291cf0b49d0c2cf8a3c27681df Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 1 Aug 2022 12:41:15 +0200 Subject: [PATCH 074/138] WebHost: update flask (#804) --- WebHostLib/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index c9ee8620ce..52d0316b2a 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,7 +1,7 @@ -flask>=2.1.2 +flask>=2.1.3 pony>=0.7.16 waitress>=2.1.1 -Flask-Caching>=2.0.0 +Flask-Caching>=2.0.1 Flask-Compress>=1.12 Flask-Limiter>=2.5.0 -bokeh>=2.4.3 \ No newline at end of file +bokeh>=2.4.3 From 0b228834c2c3398fca15d193028206c5d5619fb9 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 1 Aug 2022 20:09:34 +0200 Subject: [PATCH 075/138] The Witness: Logic fix (unbeatable seed) (#850) --- worlds/witness/WitnessLogic.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index a4cdd9f1c4..71ca7c819f 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -765,7 +765,9 @@ Inside Mountain Second Layer (Inside Mountain) - Inside Mountain Second Layer Li 158430 - 0x09FD8 (Color Cycle 5) - 0x09FD7 - Color Cycle & RGB & Squares & Colored Squares & Symmetry & Colored Dots Door - 0x09FFB (Staircase Near) - 0x09FD8 -Inside Mountain Second Layer Blue Bridge (Inside Mountain) - Inside Mountain Second Layer Beyond Bridge - TrueOneWay - Inside Mountain Second Layer Elevator Room - 0x09EDD: +Inside Mountain Second Layer Blue Bridge (Inside Mountain) - Inside Mountain Second Layer Beyond Bridge - TrueOneWay - Inside Mountain Second Layer At Door - TrueOneWay: + +Inside Mountain Second Layer At Door (Inside Mountain) - Inside Mountain Second Layer Elevator Room - 0x09EDD: Door - 0x09EDD (Door to Elevator) - 0x09ED8 & 0x09E86 Inside Mountain Second Layer Light Bridge Room Near (Inside Mountain): From 5f27019855bc0ac5ca8b4d852edad40006b3e858 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Mon, 1 Aug 2022 14:57:30 -0700 Subject: [PATCH 076/138] Add an optional path to factorio server-settings.json (#851) * Add an optional path to factorio server-settings.json * factorio: changes * use forward slashs in host.yaml going forward. (works on all OSes.) * comment out the host.yaml server_settings option. * assume that server_settings is NOT provided and explicitly check for its existence in factorio_client. --- FactorioClient.py | 9 ++++++++- host.yaml | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/FactorioClient.py b/FactorioClient.py index b7bf324311..2fa8ba9c15 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -400,6 +400,7 @@ if __name__ == '__main__': "Refer to Factorio --help for those.") parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio') parser.add_argument('--rcon-password', help='Password to authenticate with RCON.') + parser.add_argument('--server-settings', help='Factorio server settings configuration file.') args, rest = parser.parse_known_args() colorama.init() @@ -410,6 +411,9 @@ if __name__ == '__main__': factorio_server_logger = logging.getLogger("FactorioServer") options = Utils.get_options() executable = options["factorio_options"]["executable"] + server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None) + if server_settings: + server_settings = os.path.abspath(server_settings) if not os.path.exists(os.path.dirname(executable)): raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.") @@ -421,7 +425,10 @@ if __name__ == '__main__': else: raise FileNotFoundError(f"Path {executable} is not an executable file.") - server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest) + if server_settings and os.path.isfile(server_settings): + server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, "--server-settings", server_settings, *rest) + else: + server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest) asyncio.run(main(args)) colorama.deinit() diff --git a/host.yaml b/host.yaml index 86f88de024..901e6cd727 100644 --- a/host.yaml +++ b/host.yaml @@ -101,7 +101,9 @@ sm_options: # Alternatively, a path to a program to open the .sfc file with rom_start: true factorio_options: - executable: "factorio\\bin\\x64\\factorio" + executable: "factorio/bin/x64/factorio" + # by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used. + # server_settings: "factorio\\data\\server-settings.json" minecraft_options: forge_directory: "Minecraft Forge server" max_heap_size: "2G" From b47cca451567bdfcb196984baec9e86f74064757 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 3 Aug 2022 07:41:27 -0500 Subject: [PATCH 077/138] HK: Add bug report link (#824) Co-authored-by: Hussein Farran --- worlds/hk/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index c07d995ecd..bc9b29519e 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -121,6 +121,7 @@ shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = { "Leg_Eater": ("GEO",), } + class HKWeb(WebWorld): tutorials = [Tutorial( "Mod Setup and Use Guide", @@ -131,6 +132,8 @@ class HKWeb(WebWorld): ["Ijwu"] )] + bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title=" + class HKWorld(World): """Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface, From 59918b9dbc5ce68641273ea3f1cec31c8c77466d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 3 Aug 2022 14:53:14 +0200 Subject: [PATCH 078/138] Core: patch stream_input to ignore non-parsable input (such as EOF encoded as 0xff) (#854) --- Utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Utils.py b/Utils.py index 423bb3b469..4d3d6b134b 100644 --- a/Utils.py +++ b/Utils.py @@ -479,9 +479,13 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri def stream_input(stream, queue): def queuer(): while 1: - text = stream.readline().strip() - if text: - queue.put_nowait(text) + try: + text = stream.readline().strip() + except UnicodeDecodeError as e: + logging.exception(e) + else: + if text: + queue.put_nowait(text) from threading import Thread thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True) From 95012c004f2e3196ff380917e82da73be8d06f98 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 5 Aug 2022 14:23:21 +0200 Subject: [PATCH 079/138] Subnautica: update docs with resume info (#853) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: strotlog <49286967+strotlog@users.noreply.github.com> --- worlds/subnautica/docs/setup_en.md | 32 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/worlds/subnautica/docs/setup_en.md b/worlds/subnautica/docs/setup_en.md index 39d292938d..665cb8b336 100644 --- a/worlds/subnautica/docs/setup_en.md +++ b/worlds/subnautica/docs/setup_en.md @@ -18,20 +18,26 @@ 4. Start Subnautica. You should see a Connect Menu in the topleft of your main Menu. +## Connecting + +Using the Connect Menu in Subnautica's Main Menu you enter your connection info to connect to an Archipelago Multiworld. +Menu points: + - Host: the full url that you're trying to connect to, such as `archipelago.gg:38281`. + - PlayerName: your name in the multiworld. Can also be called Slot Name and is the name you entered when creating your settings. + - Password: optional password, leave blank if no password was set. + +After the connection is made, start a new game. You should start to see Archipelago chat messages to appear, such as a message announcing that you joined the multiworld. + +## Resuming + +When loading a savegame it will automatically attempt to resume the connection that was active when the savegame was made. +If that connection information is no longer valid, such as if the server's IP and/or port has changed, the Connect Menu will reappear after loading. Use the Connect Menu before or after loading the savegame to connect to the new instance. + +Warning: Currently it is not checked if this is the correct multiworld belonging to that savegame, please ensure that yourself beforehand. + ## Troubleshooting -If you don't see the connect window check that you see a qmodmanager_log-Subnautica.txt in Subnautica, if not +If you don't see the Connect Menu within the Main Menu, check that you see a file named `qmodmanager_log-Subnautica.txt` in the Subnautica game directory. If not, QModManager4 is not correctly installed, otherwise open it and look -for `[Info : BepInEx] Loading [Archipelago 1.0.0.0]`, version number doesn't matter. If it doesn't show this, then +for `[Info : BepInEx] Loading [Archipelago`. If it doesn't show this, then QModManager4 didn't find the Archipelago mod, so check your paths. - -## Joining a MultiWorld Game - -1. In Host, enter the address of the server, such as archipelago.gg:38281, your server host should be able to tell you - this. - -2. In Password enter the server password if one exists, otherwise leave blank. - -3. In PlayerName enter your "name" field from the yaml, or website config. - -4. Hit Connect. If it says successfully authenticated you can now create a new savegame or resume the correct savegame. \ No newline at end of file From 530b6cc36006b0565328a6e72a52798adbec6f3c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 4 Aug 2022 01:49:15 +0200 Subject: [PATCH 080/138] SMZ3: FixJunkFillGT making invalid placements --- worlds/smz3/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 7c519ec068..0cd9a0e264 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -77,7 +77,7 @@ class SMZ3World(World): def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() - self.locations = {} + self.locations: Dict[str, Location] = {} self.unreachable = [] super().__init__(world, player) @@ -481,15 +481,13 @@ class SMZ3World(World): if loc.name in self.locationNamesGT and loc.item is None: poolLength = len(self.world.itempool) # start looking at a random starting index and loop at start if no match found - for i in range(self.world.random.randint(0, poolLength), poolLength): - if not self.world.itempool[i].advancement: + start = self.world.random.randint(0, poolLength) + for off in range(0, poolLength): + i = (start + off) % poolLength + if self.world.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) \ + and loc.can_fill(self.world.state, self.world.itempool[i], False): itemFromPool = self.world.itempool.pop(i) break - else: - for i in range(0, poolLength): - if not self.world.itempool[i].advancement: - itemFromPool = self.world.itempool.pop(i) - break self.world.push_item(loc, itemFromPool, False) loc.event = False From 7c808bb03b3bd9042c543c56f48f7f3fa14286dc Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 4 Aug 2022 12:39:31 +0200 Subject: [PATCH 081/138] SMZ3: Fix Swamp Palace Entrace for minimal accessibility --- worlds/smz3/TotalSMZ3/Config.py | 1 + worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py | 3 ++- worlds/smz3/__init__.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/smz3/TotalSMZ3/Config.py b/worlds/smz3/TotalSMZ3/Config.py index bfcd541b98..1c3bddb188 100644 --- a/worlds/smz3/TotalSMZ3/Config.py +++ b/worlds/smz3/TotalSMZ3/Config.py @@ -48,6 +48,7 @@ class Config: Keysanity: bool = KeyShuffle != KeyShuffle.Null Race: bool = False GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals + MinimalAccessibility: bool = False # AP specific accessibility: minimal def __init__(self, options: Dict[str, str]): self.GameMode = self.ParseOption(options, GameMode.Multiworld) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py index 1805e74dca..ab7c86a631 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py @@ -14,7 +14,8 @@ class SwampPalace(Z3Region, IReward): self.Reward = RewardType.Null self.Locations = [ Location(self, 256+135, 0x1EA9D, LocationType.Regular, "Swamp Palace - Entrance") - .Allow(lambda item, items: self.Config.Keysanity or item.Is(ItemType.KeySP, self.world)), + .Allow(lambda item, items: self.Config.Keysanity or self.Config.MinimalAccessibility or + item.Is(ItemType.KeySP, self.world)), Location(self, 256+136, 0x1E986, LocationType.Regular, "Swamp Palace - Map Chest", lambda items: items.KeySP), Location(self, 256+137, 0x1E989, LocationType.Regular, "Swamp Palace - Big Chest", diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 0cd9a0e264..58e8b65603 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -19,6 +19,7 @@ from ..AutoWorld import World, AutoLogicRegister, WebWorld from .Rom import get_base_rom_bytes, SMZ3DeltaPatch from .ips import IPS_Patch from .Options import smz3_options +from Options import Accessibility world_folder = os.path.dirname(__file__) logger = logging.getLogger("SMZ3") @@ -189,6 +190,7 @@ class SMZ3World(World): config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value) config.Keysanity = config.KeyShuffle != KeyShuffle.Null config.GanonInvincible = GanonInvincible.BeforeCrystals + config.MinimalAccessibility = self.world.accessibility[self.player] == Accessibility.option_minimal self.local_random = random.Random(self.world.random.randint(0, 1000)) self.smz3World = TotalSMZ3World(config, self.world.get_player_name(self.player), self.player, self.world.seed_name) From db5b7e5db9687874f1678134c091dc762c9d5da5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 1 Aug 2022 23:32:48 +0200 Subject: [PATCH 082/138] Core: update version --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index 4d3d6b134b..e38b13560d 100644 --- a/Utils.py +++ b/Utils.py @@ -30,7 +30,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.3" +__version__ = "0.3.4" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith('linux') From d15c30f63b82b5e56de9e9c874e465c58e4f82dc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 5 Aug 2022 15:35:46 +0200 Subject: [PATCH 083/138] Stats: limit to recognized games --- WebHostLib/stats.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/WebHostLib/stats.py b/WebHostLib/stats.py index a647be5ee5..54f5e598d1 100644 --- a/WebHostLib/stats.py +++ b/WebHostLib/stats.py @@ -18,15 +18,16 @@ from .models import Room PLOT_WIDTH = 600 -def get_db_data() -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]: +def get_db_data(known_games: str) -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]: games_played = defaultdict(Counter) total_games = Counter() cutoff = date.today()-timedelta(days=30) room: Room for room in select(room for room in Room if room.creation_time >= cutoff): for slot in room.seed.slots: - total_games[slot.game] += 1 - games_played[room.creation_time.date()][slot.game] += 1 + if slot.game in known_games: + total_games[slot.game] += 1 + games_played[room.creation_time.date()][slot.game] += 1 return total_games, games_played @@ -73,10 +74,12 @@ def create_game_played_figure(all_games_data: typing.Dict[datetime.date, typing. @app.route('/stats') @cache.memoize(timeout=60 * 60) # regen once per hour should be plenty def stats(): + from worlds import network_data_package + known_games = set(network_data_package["games"]) plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date", y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500) - total_games, games_played = get_db_data() + total_games, games_played = get_db_data(known_games) days = sorted(games_played) color_palette = get_color_palette(len(total_games)) From 21f7c6c0adcdf85d81bba72fe967fa72cb92bb4d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 5 Aug 2022 17:09:21 +0200 Subject: [PATCH 084/138] Core: optimize away Item.world (#840) * Core: optimize away Item.world * Update test/general/TestFill.py * Test: undo unnecessary changes * lttp: remove two more Item.world writes Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- BaseClasses.py | 18 ++++++++---------- Main.py | 3 --- test/general/TestFill.py | 3 +-- worlds/alttp/Dungeons.py | 1 - worlds/alttp/Rules.py | 1 - worlds/alttp/Shops.py | 1 - 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 6816617279..66e96824a7 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -384,7 +384,6 @@ class MultiWorld(): return self.worlds[player].create_item(item_name) def push_precollected(self, item: Item): - item.world = self self.precollected_items[item.player].append(item) self.state.collect(item, True) @@ -392,7 +391,6 @@ class MultiWorld(): assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}." location.item = item item.location = location - item.world = self # try to not have this here anymore and create it with item? if collect: self.state.collect(item, location.event, location) @@ -1102,7 +1100,6 @@ class Location: self.item = item item.location = self self.event = item.advancement - self.item.world = self.parent_region.world self.locked = True def __repr__(self): @@ -1148,7 +1145,6 @@ class ItemClassification(IntFlag): class Item: location: Optional[Location] = None - world: Optional[MultiWorld] = None code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata name: str game: str = "Generic" @@ -1175,11 +1171,11 @@ class Item: self.code = code @property - def hint_text(self): + def hint_text(self) -> str: return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " ")) @property - def pedestal_hint_text(self): + def pedestal_hint_text(self) -> str: return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " ")) @property @@ -1205,7 +1201,7 @@ class Item: def __eq__(self, other): return self.name == other.name and self.player == other.player - def __lt__(self, other: Item): + def __lt__(self, other: Item) -> bool: if other.player != self.player: return other.player < self.player return self.name < other.name @@ -1213,11 +1209,13 @@ class Item: def __hash__(self): return hash((self.name, self.player)) - def __repr__(self): + def __repr__(self) -> str: return self.__str__() - def __str__(self): - return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' + def __str__(self) -> str: + if self.location: + return self.location.parent_region.world.get_name_string_for_object(self) + return f"{self.name} (Player {self.player})" class Spoiler(): diff --git a/Main.py b/Main.py index 3912e65c99..3b094326f2 100644 --- a/Main.py +++ b/Main.py @@ -217,9 +217,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger.info("Running Item Plando") - for item in world.itempool: - item.world = world - distribute_planned(world) logger.info('Running Pre Main Fill.') diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 5cec895221..189aafafb2 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -49,8 +49,7 @@ class PlayerDefinition(object): region_name = "player" + str(self.id) + region_tag region = Region("player" + str(self.id) + region_tag, RegionType.Generic, "Region Hint", self.id, self.world) - self.locations += generate_locations(size, - self.id, None, region, region_tag) + self.locations += generate_locations(size, self.id, None, region, region_tag) entrance = Entrance(self.id, region_name + "_entrance", parent) parent.exits.append(entrance) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 861dce9ac0..8850786be6 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -15,7 +15,6 @@ def create_dungeons(world, player): dungeon_items, player) for item in dungeon.all_items: item.dungeon = dungeon - item.world = world dungeon.boss = BossFactory(default_boss, player) if default_boss else None for region in dungeon.regions: world.get_region(region, player).dungeon = dungeon diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 7e16d6e566..36e16cf57c 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -935,7 +935,6 @@ def set_trock_key_rules(world, player): else: # A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works item = ItemFactory('Small Key (Turtle Rock)', player) - item.world = world location = world.get_location('Turtle Rock - Big Key Chest', player) location.place_locked_item(item) location.event = True diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 5abbdd07bc..9a77e7d11a 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -335,7 +335,6 @@ def create_shops(world, player: int): else: loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player) loc.shop_slot_disabled = True - loc.item.world = world shop.region.locations.append(loc) world.clear_location_cache() From 95bba50223aaa9d4ad197fa13ce9907a2866e872 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 5 Aug 2022 15:53:28 +0200 Subject: [PATCH 085/138] WebHost: fix filename rename in flask update --- WebHostLib/downloads.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 0704f5d0ec..528cbe5ec0 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -36,14 +36,14 @@ def download_patch(room_id, patch_id): fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \ f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}" new_file.seek(0) - return send_file(new_file, as_attachment=True, attachment_filename=fname) + return send_file(new_file, as_attachment=True, download_name=fname) else: patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}") patch_data = BytesIO(patch_data) fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \ f"{preferred_endings[patch.game]}" - return send_file(patch_data, as_attachment=True, attachment_filename=fname) + return send_file(patch_data, as_attachment=True, download_name=fname) @app.route("/dl_spoiler/") @@ -66,7 +66,7 @@ def download_slot_file(room_id, player_id: int): from worlds.minecraft import mc_update_output fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc" data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port) - return send_file(io.BytesIO(data), as_attachment=True, attachment_filename=fname) + return send_file(io.BytesIO(data), as_attachment=True, download_name=fname) elif slot_data.game == "Factorio": with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf: for name in zf.namelist(): @@ -82,7 +82,7 @@ def download_slot_file(room_id, player_id: int): fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json" else: return "Game download not supported." - return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname) + return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname) @app.route("/templates") From dd6e21251971734a0419d05d943008070fdab21a Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Thu, 4 Aug 2022 21:26:52 +0200 Subject: [PATCH 086/138] [Core] Colorama fix --- NetUtils.py | 2 +- docs/network protocol.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NetUtils.py b/NetUtils.py index 6cf3ab6d41..1e7d66d824 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -270,7 +270,7 @@ class RawJSONtoTextParser(JSONtoTextParser): color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, - 'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47} + 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47} def color_code(*args): diff --git a/docs/network protocol.md b/docs/network protocol.md index 05a58d0598..b12768e2c9 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -501,7 +501,7 @@ Color options: * green_bg * yellow_bg * blue_bg -* purple_bg +* magenta_bg * cyan_bg * white_bg From f6da81ac7044cb0d40ddc89c9e7008a408cdbe23 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 6 Aug 2022 00:49:54 +0200 Subject: [PATCH 087/138] Core: cleanup Item classes (#849) --- BaseClasses.py | 24 +++++++----------------- worlds/alttp/Items.py | 7 ++++++- worlds/alttp/Rom.py | 25 ++++++++++++++++--------- worlds/alttp/SubClasses.py | 13 +++++-------- worlds/alttp/__init__.py | 15 +++++++++++++-- worlds/archipidle/__init__.py | 7 +++++-- worlds/hk/__init__.py | 3 ++- worlds/meritous/Items.py | 23 +++++++++-------------- worlds/oot/Items.py | 15 +-------------- worlds/sa2b/Items.py | 7 ++++--- worlds/sm/__init__.py | 5 +++-- worlds/smz3/__init__.py | 6 ++++-- 12 files changed, 75 insertions(+), 75 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 66e96824a7..b550569e48 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1144,31 +1144,21 @@ class ItemClassification(IntFlag): class Item: - location: Optional[Location] = None - code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata - name: str game: str = "Generic" - type: str = None + __slots__ = ("name", "classification", "code", "player", "location") + name: str classification: ItemClassification - - # need to find a decent place for these to live and to allow other games to register texts if they want. - pedestal_credit_text: str = "and the Unknown Item" - sickkid_credit_text: Optional[str] = None - magicshop_credit_text: Optional[str] = None - zora_credit_text: Optional[str] = None - fluteboy_credit_text: Optional[str] = None - - # hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem - smallkey: bool = False - bigkey: bool = False - map: bool = False - compass: bool = False + code: Optional[int] + """an item with code None is called an Event, and does not get written to multidata""" + player: int + location: Optional[Location] def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int): self.name = name self.classification = classification self.player = player self.code = code + self.location = None @property def hint_text(self) -> str: diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index d1d372bf4b..3663db5cf4 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -51,6 +51,11 @@ class ItemData(typing.NamedTuple): flute_boy_credit: typing.Optional[str] hint_text: typing.Optional[str] + def as_init_dict(self) -> typing.Dict[str, typing.Any]: + return {key: getattr(self, key) for key in + ('classification', 'type', 'item_code', 'pedestal_hint', 'hint_text')} + + # Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), 'Progressive Bow': ItemData(IC.progression, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), @@ -218,7 +223,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\ 'Open Floodgate': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None), } -as_dict_item_table = {name: data._asdict() for name, data in item_table.items()} +item_init_table = {name: data.as_init_dict() for name, data in item_table.items()} progression_mapping = { "Golden Sword": ("Progressive Sword", 4), diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index c16bbf5322..dd5cc8c4dc 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -2091,7 +2091,9 @@ def write_string_to_rom(rom, target, string): def write_strings(rom, world, player): + from . import ALTTPWorld local_random = world.slot_seeds[player] + w: ALTTPWorld = world.worlds[player] tt = TextTable() tt.removeUnwantedText() @@ -2420,7 +2422,8 @@ def write_strings(rom, world, player): pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem, True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item' tt['mastersword_pedestal_translated'] = pedestal_text - pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else pedestalitem.pedestal_credit_text if pedestalitem.pedestal_credit_text is not None else 'and the Unknown Item' + pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \ + w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item') etheritem = world.get_location('Ether Tablet', player).item ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem, @@ -2448,20 +2451,24 @@ def write_strings(rom, world, player): credits = Credits() sickkiditem = world.get_location('Sick Kid', player).item - sickkiditem_text = local_random.choice( - SickKid_texts) if sickkiditem is None or sickkiditem.sickkid_credit_text is None else sickkiditem.sickkid_credit_text + sickkiditem_text = local_random.choice(SickKid_texts) \ + if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \ + else w.sickkid_credit_texts[sickkiditem.code] zoraitem = world.get_location('King Zora', player).item - zoraitem_text = local_random.choice( - Zora_texts) if zoraitem is None or zoraitem.zora_credit_text is None else zoraitem.zora_credit_text + zoraitem_text = local_random.choice(Zora_texts) \ + if zoraitem is None or zoraitem.code not in w.zora_credit_texts \ + else w.zora_credit_texts[zoraitem.code] magicshopitem = world.get_location('Potion Shop', player).item - magicshopitem_text = local_random.choice( - MagicShop_texts) if magicshopitem is None or magicshopitem.magicshop_credit_text is None else magicshopitem.magicshop_credit_text + magicshopitem_text = local_random.choice(MagicShop_texts) \ + if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \ + else w.magicshop_credit_texts[magicshopitem.code] fluteboyitem = world.get_location('Flute Spot', player).item - fluteboyitem_text = local_random.choice( - FluteBoy_texts) if fluteboyitem is None or fluteboyitem.fluteboy_credit_text is None else fluteboyitem.fluteboy_credit_text + fluteboyitem_text = local_random.choice(FluteBoy_texts) \ + if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \ + else w.fluteboy_credit_texts[fluteboyitem.code] credits.update_credits_line('castle', 0, local_random.choice(KingsReturn_texts)) credits.update_credits_line('sanctuary', 0, local_random.choice(Sanctuary_texts)) diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 87f10f48e0..b933c0740d 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -18,19 +18,16 @@ class ALttPLocation(Location): class ALttPItem(Item): game: str = "A Link to the Past" + type: Optional[str] + _pedestal_hint_text: Optional[str] + _hint_text: Optional[str] dungeon = None - def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None, pedestal_hint=None, - pedestal_credit=None, sick_kid_credit=None, zora_credit=None, witch_credit=None, - flute_boy_credit=None, hint_text=None): + def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None, + pedestal_hint=None, hint_text=None): super(ALttPItem, self).__init__(name, classification, item_code, player) self.type = type self._pedestal_hint_text = pedestal_hint - self.pedestal_credit_text = pedestal_credit - self.sickkid_credit_text = sick_kid_credit - self.zora_credit_text = zora_credit - self.magicshop_credit_text = witch_credit - self.fluteboy_credit_text = flute_boy_credit self._hint_text = hint_text @property diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 871c44684d..64b1bf8db5 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -8,7 +8,7 @@ from BaseClasses import Item, CollectionState, Tutorial from .SubClasses import ALttPItem from ..AutoWorld import World, WebWorld, LogicMixin from .Options import alttp_options, smallkey_shuffle -from .Items import as_dict_item_table, item_name_groups, item_table, GetBeemizerItem +from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions from .Rules import set_rules from .ItemPool import generate_itempool, difficulties @@ -124,6 +124,17 @@ class ALTTPWorld(World): required_client_version = (0, 3, 2) web = ALTTPWeb() + pedestal_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.pedestal_credit for data in item_table.values() if data.pedestal_credit} + sickkid_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.sick_kid_credit for data in item_table.values() if data.sick_kid_credit} + zora_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.zora_credit for data in item_table.values() if data.zora_credit} + magicshop_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.witch_credit for data in item_table.values() if data.witch_credit} + fluteboy_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.flute_boy_credit for data in item_table.values() if data.flute_boy_credit} + set_rules = set_rules create_items = generate_itempool @@ -400,7 +411,7 @@ class ALTTPWorld(World): multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]] def create_item(self, name: str) -> Item: - return ALttPItem(name, self.player, **as_dict_item_table[name]) + return ALttPItem(name, self.player, **item_init_table[name]) @classmethod def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py index 0ddb8248fb..8b1061b5d1 100644 --- a/worlds/archipidle/__init__.py +++ b/worlds/archipidle/__init__.py @@ -47,13 +47,12 @@ class ArchipIDLEWorld(World): item_pool = [] for i in range(100): - item = Item( + item = ArchipIDLEItem( item_table_copy[i], ItemClassification.progression if i < 20 else ItemClassification.filler, self.item_name_to_id[item_table_copy[i]], self.player ) - item.game = 'ArchipIDLE' item_pool.append(item) self.world.itempool += item_pool @@ -93,6 +92,10 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi return region +class ArchipIDLEItem(Item): + game = "ArchipIDLE" + + class ArchipIDLELocation(Location): game: str = "ArchipIDLE" diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index bc9b29519e..6869e14b67 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -632,8 +632,9 @@ class HKLocation(Location): class HKItem(Item): game = "Hollow Knight" + type: str - def __init__(self, name, advancement, code, type, player: int = None): + def __init__(self, name, advancement, code, type: str, player: int = None): if name == "Mimic_Grub": classification = ItemClassification.trap elif type in ("Grub", "DreamWarrior", "Root", "Egg"): diff --git a/worlds/meritous/Items.py b/worlds/meritous/Items.py index 1b5228e5ce..9f28c5d178 100644 --- a/worlds/meritous/Items.py +++ b/worlds/meritous/Items.py @@ -6,14 +6,9 @@ import typing from BaseClasses import Item, ItemClassification +from worlds.alttp import ALTTPWorld -# pedestal_credit_text: str = "and the Unknown Item" -# sickkid_credit_text: Optional[str] = None -# magicshop_credit_text: Optional[str] = None -# zora_credit_text: Optional[str] = None -# fluteboy_credit_text: Optional[str] = None - class MeritousLttPText(typing.NamedTuple): pedestal: typing.Optional[str] sickkid: typing.Optional[str] @@ -143,6 +138,7 @@ LttPCreditsText = { class MeritousItem(Item): game: str = "Meritous" + type: str def __init__(self, name, advancement, code, player): super(MeritousItem, self).__init__(name, @@ -171,14 +167,6 @@ class MeritousItem(Item): self.type = "Artifact" self.classification = ItemClassification.useful - if name in LttPCreditsText: - lttp = LttPCreditsText[name] - self.pedestal_credit_text = f"and the {lttp.pedestal}" - self.sickkid_credit_text = lttp.sickkid - self.magicshop_credit_text = lttp.magicshop - self.zora_credit_text = lttp.zora - self.fluteboy_credit_text = lttp.fluteboy - offset = 593_000 @@ -217,3 +205,10 @@ item_groups = { "Important Artifacts": ["Shield Boost", "Circuit Booster", "Metabolism", "Dodge Enhancer"], "Crystals": ["Crystals x500", "Crystals x1000", "Crystals x2000"] } + +ALTTPWorld.pedestal_credit_texts.update({item_table[name]: f"and the {texts.pedestal}" + for name, texts in LttPCreditsText.items()}) +ALTTPWorld.sickkid_credit_texts.update({item_table[name]: texts.sickkid for name, texts in LttPCreditsText.items()}) +ALTTPWorld.magicshop_credit_texts.update({item_table[name]: texts.magicshop for name, texts in LttPCreditsText.items()}) +ALTTPWorld.zora_credit_texts.update({item_table[name]: texts.zora for name, texts in LttPCreditsText.items()}) +ALTTPWorld.fluteboy_credit_texts.update({item_table[name]: texts.fluteboy for name, texts in LttPCreditsText.items()}) diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py index f07d91a85e..b4c0719700 100644 --- a/worlds/oot/Items.py +++ b/worlds/oot/Items.py @@ -24,6 +24,7 @@ def ap_id_to_oot_data(ap_id): class OOTItem(Item): game: str = "Ocarina of Time" + type: str def __init__(self, name, player, data, event, force_not_advancement): (type, advancement, index, special) = data @@ -38,7 +39,6 @@ class OOTItem(Item): classification = ItemClassification.progression else: classification = ItemClassification.filler - adv = bool(advancement) and not force_not_advancement super(OOTItem, self).__init__(name, classification, oot_data_to_ap_id(data, event), player) self.type = type self.index = index @@ -46,25 +46,12 @@ class OOTItem(Item): self.looks_like_item = None self.price = special.get('price', None) if special else None self.internal = False - - # The playthrough calculation calls a function that uses "sweep_for_events(key_only=True)" - # This checks if the item it's looking for is a small key, using the small key property. - # Because of overlapping item fields, this means that OoT small keys are technically counted, unless we do this. - # This causes them to be double-collected during playthrough and generation. - @property - def smallkey(self) -> bool: - return False - - @property - def bigkey(self) -> bool: - return False @property def dungeonitem(self) -> bool: return self.type in ['SmallKey', 'HideoutSmallKey', 'BossKey', 'GanonBossKey', 'Map', 'Compass'] - # Progressive: True -> Advancement # False -> Priority # None -> Normal diff --git a/worlds/sa2b/Items.py b/worlds/sa2b/Items.py index bebfa44cd4..d11178f575 100644 --- a/worlds/sa2b/Items.py +++ b/worlds/sa2b/Items.py @@ -2,6 +2,7 @@ import typing from BaseClasses import Item, ItemClassification from .Names import ItemName +from worlds.alttp import ALTTPWorld class ItemData(typing.NamedTuple): @@ -18,9 +19,6 @@ class SA2BItem(Item): def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None): super(SA2BItem, self).__init__(name, classification, code, player) - if self.name == ItemName.sonic_light_shoes or self.name == ItemName.shadow_air_shoes: - self.pedestal_credit_text = "and the Soap Shoes" - # Separate tables for each type of item. emblems_table = { @@ -94,3 +92,6 @@ item_table = { } lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} + +ALTTPWorld.pedestal_credit_texts[item_table[ItemName.sonic_light_shoes].code] = "and the Soap Shoes" +ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes" diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index a7445c01a5..d2728132a3 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -5,7 +5,7 @@ import copy import os import threading import base64 -from typing import Set, List, TextIO +from typing import Set, TextIO from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils @@ -735,7 +735,8 @@ class SMLocation(Location): class SMItem(Item): game = "Super Metroid" + type: str - def __init__(self, name, classification, type, code, player: int = None): + def __init__(self, name, classification, type: str, code, player: int): super(SMItem, self).__init__(name, classification, code, player) self.type = type diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 58e8b65603..fcedfd45db 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -573,8 +573,10 @@ class SMZ3Location(Location): class SMZ3Item(Item): game = "SMZ3" + type: ItemType + item: Item - def __init__(self, name, classification, type, code, player: int = None, item=None): + def __init__(self, name, classification, type: ItemType, code, player: int, item: Item): + super(SMZ3Item, self).__init__(name, classification, code, player) self.type = type self.item = item - super(SMZ3Item, self).__init__(name, classification, code, player) From ae3e6c29e3cc68849ae7ba07ffc3e278de14a1e4 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 5 Aug 2022 18:53:48 -0400 Subject: [PATCH 088/138] DKC3: Add Link to Tracker from Setup Guide (#871) --- worlds/dkc3/docs/setup_en.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md index 34a297eab0..3cb3db0a30 100644 --- a/worlds/dkc3/docs/setup_en.md +++ b/worlds/dkc3/docs/setup_en.md @@ -15,6 +15,11 @@ compatible hardware - Your legally obtained Donkey Kong Country 3 ROM file, probably named `Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc` +## Optional Software +- Donkey Kong Country 3 Tracker + - PopTracker from: [PopTracker Releases Page](https://github.com/black-sliver/PopTracker/releases/) + - Donkey Kong Country 3 Archipelago PopTracker pack from: [DKC3 AP Tracker Releases Page](https://github.com/PoryGone/DKC3_AP_Tracker/releases/) + ## Installation Procedures ### Windows Setup @@ -57,7 +62,6 @@ validator page: [YAML Validation page](/mysterycheck) 4. You will be presented with a server page, from which you can download your patch file. 5. Double-click on your patch file, and the Donkey Kong Country 3 Client will launch automatically, create your ROM from the patch file, and open your emulator for you. -6. Since this is a single-player game, you will no longer need the client, so feel free to close it. ## Joining a MultiWorld Game From 69e5627cd7e5fd3306cc9dca23343d56368bef29 Mon Sep 17 00:00:00 2001 From: Joethepic <60947591+Joethepic@users.noreply.github.com> Date: Fri, 5 Aug 2022 19:11:10 -0500 Subject: [PATCH 089/138] HK: fix indentation on mimic grubs (#868) --- worlds/hk/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 0f4bec8205..fd8d036175 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -50,7 +50,7 @@ option_docstrings = { "pool and open their locations for randomization.", "RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.", "RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization." - "Mimic Grubs are always placed in your own game.", + "Mimic Grubs are always placed in your own game.", "RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see" " and buy an item that is randomized into that location as well.", "RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items " From f1c5c9a14868e3faa3432769a1b02b81f01b697d Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 6 Aug 2022 11:25:37 +0000 Subject: [PATCH 090/138] WebHost: Fix `OptionDict`s that define `valid_keys` from outputting as `[]` on Weighted Settings export. (#874) * WebHost: Fix OptionDicts that define valid_keys from outputting as [] on Weighted Settings export --- WebHostLib/options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 2cab7728da..ccd1b27b3c 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -110,7 +110,7 @@ def create(): if option.default == "random": this_option["defaultValue"] = "random" - elif hasattr(option, "range_start") and hasattr(option, "range_end"): + elif issubclass(option, Options.Range): game_options[option_name] = { "type": "range", "displayName": option.display_name if hasattr(option, "display_name") else option_name, @@ -121,7 +121,7 @@ def create(): "max": option.range_end, } - if hasattr(option, "special_range_names"): + if issubclass(option, Options.SpecialRange): game_options[option_name]["type"] = 'special_range' game_options[option_name]["value_names"] = {} for key, val in option.special_range_names.items(): @@ -141,7 +141,7 @@ def create(): "description": option.__doc__ if option.__doc__ else "Please document me!", } - elif hasattr(option, "valid_keys"): + elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet): if option.valid_keys: game_options[option_name] = { "type": "custom-list", From 9167e5363d6775351a276035e0254c68f37f7a7a Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sat, 6 Aug 2022 07:26:02 -0400 Subject: [PATCH 091/138] DKC3: Correct File Extension in Setup Guide (#872) --- worlds/dkc3/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md index 3cb3db0a30..471248deb8 100644 --- a/worlds/dkc3/docs/setup_en.md +++ b/worlds/dkc3/docs/setup_en.md @@ -69,7 +69,7 @@ validator page: [YAML Validation page](/mysterycheck) When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch -files. Your patch file should have a `.apsm` extension. +files. Your patch file should have a `.apdkc3` extension. Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the client, and will also create your ROM in the same place as your patch file. From 04eef669f910532ae3e33334096bac5d31f98b64 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 6 Aug 2022 21:36:32 -0500 Subject: [PATCH 092/138] StS: Add a description for the game. (#876) --- worlds/spire/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 594605cd34..4d2917aab9 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -22,6 +22,11 @@ class SpireWeb(WebWorld): class SpireWorld(World): + """ + A deck-building roguelike where you must craft a unique deck, encounter bizarre creatures, discover relics of + immense power, and Slay the Spire! + """ + options = spire_options game = "Slay the Spire" topology_present = False From 181cc470795fcd5a3460333bfd3c397467bd7c7d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 4 Aug 2022 14:10:58 +0200 Subject: [PATCH 093/138] Core: cleanup BaseClasses.Location This is just cleanup and has virtually no performance impact. --- BaseClasses.py | 19 +++++++++---------- worlds/alttp/Shops.py | 4 ++-- worlds/alttp/SubClasses.py | 13 +++++++++---- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b550569e48..02a194eef5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1064,26 +1064,25 @@ class LocationProgressType(IntEnum): class Location: - # If given as integer, then this is the shop's inventory index - shop_slot: Optional[int] = None - shop_slot_disabled: bool = False + game: str = "Generic" + player: int + name: str + address: Optional[int] + parent_region: Optional[Region] event: bool = False locked: bool = False - game: str = "Generic" show_in_spoiler: bool = True - crystal: bool = False progress_type: LocationProgressType = LocationProgressType.DEFAULT always_allow = staticmethod(lambda item, state: False) access_rule = staticmethod(lambda state: True) item_rule = staticmethod(lambda item: True) item: Optional[Item] = None - parent_region: Optional[Region] - def __init__(self, player: int, name: str = '', address: int = None, parent=None): - self.name: str = name - self.address: Optional[int] = address + def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None): + self.player = player + self.name = name + self.address = address self.parent_region = parent - self.player: int = player def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state))) diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 9a77e7d11a..f6233286f0 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -207,10 +207,10 @@ def ShopSlotFill(world): shops_per_sphere.append(current_shops_slots) candidates_per_sphere.append(current_candidates) for location in sphere: - if location.shop_slot is not None: + if isinstance(location, ALttPLocation) and location.shop_slot is not None: if not location.shop_slot_disabled: current_shops_slots.append(location) - elif not location.locked and not location.item.name in blacklist_words: + elif not location.locked and location.item.name not in blacklist_words: current_candidates.append(location) if cumu_weights: x = cumu_weights[-1] diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index b933c0740d..f54ab16e92 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -6,14 +6,19 @@ from BaseClasses import Location, Item, ItemClassification class ALttPLocation(Location): game: str = "A Link to the Past" + crystal: bool + player_address: Optional[int] + _hint_text: Optional[str] + shop_slot: Optional[int] = None + """If given as integer, shop_slot is the shop's inventory index.""" + shop_slot_disabled: bool = False - def __init__(self, player: int, name: str = '', address=None, crystal: bool = False, - hint_text: Optional[str] = None, parent=None, - player_address=None): + def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False, + hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None): super(ALttPLocation, self).__init__(player, name, address, parent) self.crystal = crystal self.player_address = player_address - self._hint_text: str = hint_text + self._hint_text = hint_text class ALttPItem(Item): From c1e9d0ab4fb3609b5f2d774af7dc8994c479fbe8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 7 Aug 2022 18:28:50 +0200 Subject: [PATCH 094/138] WebHost: allow customserver to skip importing worlds subsystem for hosting a Room (#877) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- MultiServer.py | 165 ++++++++++++++++++++------------ Utils.py | 10 -- WebHost.py | 6 +- WebHostLib/__init__.py | 184 +++--------------------------------- WebHostLib/autolauncher.py | 4 +- WebHostLib/customserver.py | 39 ++++++-- WebHostLib/misc.py | 170 +++++++++++++++++++++++++++++++++ WebHostLib/tracker.py | 4 +- worlds/meritous/__init__.py | 1 - worlds/sm64ex/__init__.py | 4 +- worlds/v6/__init__.py | 1 - 11 files changed, 326 insertions(+), 262 deletions(-) create mode 100644 WebHostLib/misc.py diff --git a/MultiServer.py b/MultiServer.py index e8e1cc8d4c..3e502f649f 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -30,13 +30,8 @@ except ImportError: OperationalError = ConnectionError import NetUtils -from worlds.AutoWorld import AutoWorldRegister - -proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()} -from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name import Utils -from Utils import get_item_name_from_id, get_location_name_from_id, \ - version_tuple, restricted_loads, Version +from Utils import version_tuple, restricted_loads, Version from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ SlotType @@ -126,6 +121,11 @@ class Context: stored_data: typing.Dict[str, object] stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] + item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') + location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + all_item_and_group_names: typing.Dict[str, typing.Set[str]] + forced_auto_forfeits: typing.Dict[str, bool] + def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -190,8 +190,43 @@ class Context: self.stored_data = {} self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet) - # General networking + # init empty to satisfy linter, I suppose + self.gamespackage = {} + self.item_name_groups = {} + self.all_item_and_group_names = {} + self.forced_auto_forfeits = collections.defaultdict(lambda: False) + self.non_hintable_names = {} + self._load_game_data() + self._init_game_data() + + # Datapackage retrieval + def _load_game_data(self): + import worlds + self.gamespackage = worlds.network_data_package["games"] + + self.item_name_groups = {world_name: world.item_name_groups for world_name, world in + worlds.AutoWorldRegister.world_types.items()} + for world_name, world in worlds.AutoWorldRegister.world_types.items(): + self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit + self.non_hintable_names[world_name] = world.hint_blacklist + + def _init_game_data(self): + for game_name, game_package in self.gamespackage.items(): + for item_name, item_id in game_package["item_name_to_id"].items(): + self.item_names[item_id] = item_name + for location_name, location_id in game_package["location_name_to_id"].items(): + self.location_names[location_id] = location_name + self.all_item_and_group_names[game_name] = \ + set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) + + def item_names_for_game(self, game: str) -> typing.Dict[str, int]: + return self.gamespackage[game]["item_name_to_id"] + + def location_names_for_game(self, game: str) -> typing.Dict[str, int]: + return self.gamespackage[game]["location_name_to_id"] + + # General networking async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: if not endpoint.socket or not endpoint.socket.open: return False @@ -546,7 +581,7 @@ class Context: self.notify_all(finished_msg) if "auto" in self.forfeit_mode: forfeit_player(self, client.team, client.slot) - elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit: + elif self.forced_auto_forfeits[self.games[client.slot]]: forfeit_player(self, client.team, client.slot) if "auto" in self.collect_mode: collect_player(self, client.team, client.slot) @@ -642,9 +677,10 @@ async def on_client_connected(ctx: Context, client: Client): 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, - 'datapackage_version': network_data_package["version"], + 'datapackage_version': sum(game_data["version"] for game_data in ctx.gamespackage.values()) + if all(game_data["version"] for game_data in ctx.gamespackage.values()) else 0, 'datapackage_versions': {game: game_data["version"] for game, game_data - in network_data_package["games"].items()}, + in ctx.gamespackage.items()}, 'seed_name': ctx.seed_name, 'time': time.time(), }]) @@ -822,8 +858,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi send_items_to(ctx, team, target_player, new_item) logging.info('(Team #%d) %s sent %s to %s (%s)' % ( - team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id), - ctx.player_names[(team, target_player)], get_location_name_from_id(location))) + team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id], + ctx.player_names[(team, target_player)], ctx.location_names[location])) info_text = json_format_send_event(new_item, target_player) ctx.broadcast_team(team, [info_text]) @@ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi ctx.save() -def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]: +def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]: hints = [] slots: typing.Set[int] = {slot} for group_id, group in ctx.groups.items(): if slot in group: slots.add(group_id) - seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item] + + seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name] for finding_player, check_data in ctx.locations.items(): for location_id, (item_id, receiving_player, item_flags) in check_data.items(): if receiving_player in slots and item_id == seeked_item_id: @@ -857,7 +894,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[ def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: - seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location] + seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location] return collect_hint_location_id(ctx, team, slot, seeked_location) @@ -874,8 +911,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ - f"{lookup_any_item_id_to_name[hint.item]} is " \ - f"at {get_location_name_from_id(hint.location)} " \ + f"{ctx.item_names[hint.item]} is " \ + f"at {ctx.location_names[hint.location]} " \ f"in {ctx.player_names[team, hint.finding_player]}'s World" if hint.entrance: @@ -1133,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor): forfeit_player(self.ctx, self.client.team, self.client.slot) return True elif "disabled" in self.ctx.forfeit_mode: - self.output( - "Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release") + self.output("Sorry, client item releasing has been disabled on this server. " + "You can ask the server admin for a /release") return False else: # is auto or goal if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: @@ -1170,7 +1207,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1183,7 +1220,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1199,7 +1236,7 @@ class ClientMessageProcessor(CommonCommandProcessor): locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations] + texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations] texts.append(f"Found {len(locations)} missing location checks") self.ctx.notify_client_multiple(self.client, texts) else: @@ -1212,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor): locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations] + texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations] texts.append(f"Found {len(locations)} done location checks") self.ctx.notify_client_multiple(self.client, texts) else: @@ -1241,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor): def _cmd_getitem(self, item_name: str) -> bool: """Cheat in an item, if it is enabled on this server""" if self.ctx.item_cheat: - world = proxy_worlds[self.ctx.games[self.client.slot]] - item_name, usable, response = get_intended_text(item_name, - world.item_names) + names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot]) + item_name, usable, response = get_intended_text( + item_name, + names + ) if usable: - new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot) + new_item = NetworkItem(names[item_name], -1, self.client.slot) get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item) get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item) self.ctx.notify_all( @@ -1271,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor): f"You have {points_available} points.") return True else: - world = proxy_worlds[self.ctx.games[self.client.slot]] - names = world.location_names if for_location else world.all_item_and_group_names + game = self.ctx.games[self.client.slot] + names = self.ctx.location_names_for_game(game) \ + if for_location else \ + self.ctx.all_item_and_group_names[game] hint_name, usable, response = get_intended_text(input_text, names) if usable: - if hint_name in world.hint_blacklist: + if hint_name in self.ctx.non_hintable_names[game]: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") hints = [] - elif not for_location and hint_name in world.item_name_groups: # item group name + elif not for_location and hint_name in self.ctx.item_name_groups[game]: # item group name hints = [] - for item in world.item_name_groups[hint_name]: - if item in world.item_name_to_id: # ensure item has an ID - hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item)) - elif not for_location and hint_name in world.item_names: # item name + for item_name in self.ctx.item_name_groups[game][hint_name]: + if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID + hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) + elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) else: # location name hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) @@ -1346,12 +1387,12 @@ class ClientMessageProcessor(CommonCommandProcessor): return False @mark_raw - def _cmd_hint(self, item: str = "") -> bool: + def _cmd_hint(self, item_name: str = "") -> bool: """Use !hint {item_name}, for example !hint Lamp to get a spoiler peek for that item. If hint costs are on, this will only give you one new result, you can rerun the command to get more in that case.""" - return self.get_hints(item) + return self.get_hints(item_name) @mark_raw def _cmd_hint_location(self, location: str = "") -> bool: @@ -1477,23 +1518,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): elif cmd == "GetDataPackage": exclusions = args.get("exclusions", []) if "games" in args: - games = {name: game_data for name, game_data in network_data_package["games"].items() + games = {name: game_data for name, game_data in ctx.gamespackage.items() if name in set(args.get("games", []))} await ctx.send_msgs(client, [{"cmd": "DataPackage", "data": {"games": games}}]) # TODO: remove exclusions behaviour around 0.5.0 elif exclusions: exclusions = set(exclusions) - games = {name: game_data for name, game_data in network_data_package["games"].items() + games = {name: game_data for name, game_data in ctx.gamespackage.items() if name not in exclusions} - package = network_data_package.copy() - package["games"] = games + + package = {"games": games} await ctx.send_msgs(client, [{"cmd": "DataPackage", "data": package}]) else: await ctx.send_msgs(client, [{"cmd": "DataPackage", - "data": network_data_package}]) + "data": {"games": ctx.gamespackage}}]) elif client.auth: if cmd == "ConnectUpdate": @@ -1549,7 +1590,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): create_as_hint: int = int(args.get("create_as_hint", 0)) hints = [] for location in args["locations"]: - if type(location) is not int or location not in lookup_any_location_id_to_name: + if type(location) is not int: await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts', "original_cmd": cmd}]) @@ -1763,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor): seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(item_name) - world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.item_names) + item_name = " ".join(item_name) + names = self.ctx.item_names_for_game(self.ctx.games[slot]) + item_name, usable, response = get_intended_text(item_name, names) if usable: amount: int = int(amount) - new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))] + new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))] send_items_to(self.ctx, team, slot, *new_items) send_new_items(self.ctx) self.ctx.notify_all( 'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') + - f'"{item}" to {self.ctx.get_aliased_name(team, slot)}') + f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}') return True else: self.output(response) @@ -1787,22 +1828,22 @@ class ServerCommandProcessor(CommonCommandProcessor): """Sends an item to the specified player""" return self._cmd_send_multiple(1, player_name, *item_name) - def _cmd_hint(self, player_name: str, *item: str) -> bool: + def _cmd_hint(self, player_name: str, *item_name: str) -> bool: """Send out a hint for a player's item to their team""" seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(item) - world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.all_item_and_group_names) + item_name = " ".join(item_name) + game = self.ctx.games[slot] + item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game]) if usable: - if item in world.item_name_groups: + if item_name in self.ctx.item_name_groups[game]: hints = [] - for item in world.item_name_groups[item]: - if item in world.item_name_to_id: # ensure item has an ID - hints.extend(collect_hints(self.ctx, team, slot, item)) + for item_name_from_group in self.ctx.item_name_groups[game][item_name]: + if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID + hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) else: # item name - hints = collect_hints(self.ctx, team, slot, item) + hints = collect_hints(self.ctx, team, slot, item_name) if hints: notify_hints(self.ctx, team, hints) @@ -1818,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor): self.output(response) return False - def _cmd_hint_location(self, player_name: str, *location: str) -> bool: + def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool: """Send out a hint for a player's location to their team""" seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(location) - world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.location_names) + location_name = " ".join(location_name) + location_name, usable, response = get_intended_text(location_name, + self.ctx.location_names_for_game(self.ctx.games[slot])) if usable: - hints = collect_hint_location_name(self.ctx, team, slot, item) + hints = collect_hint_location_name(self.ctx, team, slot, location_name) if hints: notify_hints(self.ctx, team, hints) else: diff --git a/Utils.py b/Utils.py index e38b13560d..8f00a91047 100644 --- a/Utils.py +++ b/Utils.py @@ -328,16 +328,6 @@ def get_options() -> dict: return get_options.options -def get_item_name_from_id(code: int) -> str: - from worlds import lookup_any_item_id_to_name - return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})') - - -def get_location_name_from_id(code: int) -> str: - from worlds import lookup_any_location_id_to_name - return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})') - - def persistent_store(category: str, key: typing.Any, value: typing.Any): path = user_path("_persistent_storage.yaml") storage: dict = persistent_load() diff --git a/WebHost.py b/WebHost.py index eb575df3e9..5d3c44592b 100644 --- a/WebHost.py +++ b/WebHost.py @@ -14,7 +14,7 @@ import Utils Utils.local_path.cached_path = os.path.dirname(__file__) -from WebHostLib import app as raw_app +from WebHostLib import register, app as raw_app from waitress import serve from WebHostLib.models import db @@ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen from WebHostLib.lttpsprites import update_sprites_lttp from WebHostLib.options import create as create_options_files -from worlds.AutoWorld import AutoWorldRegister - configpath = os.path.abspath("config.yaml") if not os.path.exists(configpath): # fall back to config.yaml in home configpath = os.path.abspath(Utils.user_path('config.yaml')) def get_app(): + register() app = raw_app if os.path.exists(configpath): import yaml @@ -43,6 +42,7 @@ def get_app(): def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]: import json import shutil + from worlds.AutoWorld import AutoWorldRegister worlds = {} data = [] for game, world in AutoWorldRegister.world_types.items(): diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index a44afc744a..b2d243c913 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -3,12 +3,11 @@ import uuid import base64 import socket -import jinja2.exceptions from pony.flask import Pony -from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory +from flask import Flask from flask_caching import Cache from flask_compress import Compress -from worlds.AutoWorld import AutoWorldRegister +from werkzeug.routing import BaseConverter from .models import * @@ -53,8 +52,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg" cache = Cache(app) Compress(app) -from werkzeug.routing import BaseConverter - class B64UUIDConverter(BaseConverter): @@ -69,173 +66,16 @@ class B64UUIDConverter(BaseConverter): app.url_map.converters["suuid"] = B64UUIDConverter app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') -# has automatic patch integration -import Patch -app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types +def register(): + """Import submodules, triggering their registering on flask routing. + Note: initializes worlds subsystem.""" + # has automatic patch integration + import Patch + app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types -def get_world_theme(game_name: str): - if game_name in AutoWorldRegister.world_types: - return AutoWorldRegister.world_types[game_name].web.theme - return 'grass' + from WebHostLib.customserver import run_server_process + # to trigger app routing picking up on it + from . import tracker, upload, landing, check, generate, downloads, api, stats, misc - -@app.before_request -def register_session(): - session.permanent = True # technically 31 days after the last visit - if not session.get("_id", None): - session["_id"] = uuid4() # uniquely identify each session without needing a login - - -@app.errorhandler(404) -@app.errorhandler(jinja2.exceptions.TemplateNotFound) -def page_not_found(err): - return render_template('404.html'), 404 - - -# Start Playing Page -@app.route('/start-playing') -def start_playing(): - return render_template(f"startPlaying.html") - - -@app.route('/weighted-settings') -def weighted_settings(): - return render_template(f"weighted-settings.html") - - -# Player settings pages -@app.route('/games//player-settings') -def player_settings(game): - return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) - - -# Game Info Pages -@app.route('/games//info/') -def game_info(game, lang): - return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) - - -# List of supported games -@app.route('/games') -def games(): - worlds = {} - for game, world in AutoWorldRegister.world_types.items(): - if not world.hidden: - worlds[game] = world - return render_template("supportedGames.html", worlds=worlds) - - -@app.route('/tutorial///') -def tutorial(game, file, lang): - return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) - - -@app.route('/tutorial/') -def tutorial_landing(): - worlds = {} - for game, world in AutoWorldRegister.world_types.items(): - if not world.hidden: - worlds[game] = world - return render_template("tutorialLanding.html") - - -@app.route('/faq//') -def faq(lang): - return render_template("faq.html", lang=lang) - - -@app.route('/glossary//') -def terms(lang): - return render_template("glossary.html", lang=lang) - - -@app.route('/seed/') -def view_seed(seed: UUID): - seed = Seed.get(id=seed) - if not seed: - abort(404) - return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots)) - - -@app.route('/new_room/') -def new_room(seed: UUID): - seed = Seed.get(id=seed) - if not seed: - abort(404) - room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) - commit() - return redirect(url_for("host_room", room=room.id)) - - -def _read_log(path: str): - if os.path.exists(path): - with open(path, encoding="utf-8-sig") as log: - yield from log - else: - yield f"Logfile {path} does not exist. " \ - f"Likely a crash during spinup of multiworld instance or it is still spinning up." - - -@app.route('/log/') -def display_log(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - if room.owner == session["_id"]: - return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") - return "Access Denied", 403 - - -@app.route('/room/', methods=['GET', 'POST']) -def host_room(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - if request.method == "POST": - if room.owner == session["_id"]: - cmd = request.form["cmd"] - if cmd: - Command(room=room, commandtext=cmd) - commit() - - with db_session: - room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running - - return render_template("hostRoom.html", room=room) - - -@app.route('/favicon.ico') -def favicon(): - return send_from_directory(os.path.join(app.root_path, 'static/static'), - 'favicon.ico', mimetype='image/vnd.microsoft.icon') - - -@app.route('/discord') -def discord(): - return redirect("https://discord.gg/archipelago") - - -@app.route('/datapackage') -@cache.cached() -def get_datapackge(): - """A pretty print version of /api/datapackage""" - from worlds import network_data_package - import json - return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain") - - -@app.route('/index') -@app.route('/sitemap') -def get_sitemap(): - available_games = [] - for game, world in AutoWorldRegister.world_types.items(): - if not world.hidden: - available_games.append(game) - return render_template("siteMap.html", games=available_games) - - -from WebHostLib.customserver import run_server_process -from . import tracker, upload, landing, check, generate, downloads, api, stats # to trigger app routing picking up on it - -app.register_blueprint(api.api_endpoints) + app.register_blueprint(api.api_endpoints) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 6f978211fb..77445eadea 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -184,7 +184,7 @@ class MultiworldInstance(): logging.info(f"Spinning up {self.room_id}") process = multiprocessing.Process(group=None, target=run_server_process, - args=(self.room_id, self.ponyconfig), + args=(self.room_id, self.ponyconfig, get_static_server_data()), name="MultiHost") process.start() # bind after start to prevent thread sync issues with guardian. @@ -238,5 +238,5 @@ def run_guardian(): from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed -from .customserver import run_server_process +from .customserver import run_server_process, get_static_server_data from .generate import gen_game diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index f78a8eb24d..01f1fd25e5 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -9,12 +9,13 @@ import time import random import pickle import logging +import datetime import Utils -from .models import * +from .models import db_session, Room, select, commit, Command, db from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor -from Utils import get_public_ipv4, get_public_ipv6, restricted_loads +from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless class CustomClientMessageProcessor(ClientMessageProcessor): @@ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor): import MultiServer MultiServer.client_message_processor = CustomClientMessageProcessor -del (MultiServer) +del MultiServer class DBCommandProcessor(ServerCommandProcessor): @@ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor): class WebHostContext(Context): - def __init__(self): + def __init__(self, static_server_data: dict): + # static server data is used during _load_game_data to load required data, + # without needing to import worlds system, which takes quite a bit of memory + self.static_server_data = static_server_data super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2) + del self.static_server_data self.main_loop = asyncio.get_running_loop() self.video = {} self.tags = ["AP", "WebHost"] + def _load_game_data(self): + for key, value in self.static_server_data.items(): + setattr(self, key, value) + def listen_to_db_commands(self): cmdprocessor = DBCommandProcessor(self) @@ -107,14 +116,32 @@ def get_random_port(): return random.randint(49152, 65535) -def run_server_process(room_id, ponyconfig: dict): +@cache_argsless +def get_static_server_data() -> dict: + import worlds + data = { + "forced_auto_forfeits": {}, + "non_hintable_names": {}, + "gamespackage": worlds.network_data_package["games"], + "item_name_groups": {world_name: world.item_name_groups for world_name, world in + worlds.AutoWorldRegister.world_types.items()}, + } + + for world_name, world in worlds.AutoWorldRegister.world_types.items(): + data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit + data["non_hintable_names"][world_name] = world.hint_blacklist + + return data + + +def run_server_process(room_id, ponyconfig: dict, static_server_data: dict): # establish DB connection for multidata and multisave db.bind(**ponyconfig) db.generate_mapping(check_tables=False) async def main(): Utils.init_logging(str(room_id), write_mode="a") - ctx = WebHostContext() + ctx = WebHostContext(static_server_data) ctx.load(room_id) ctx.init_save() diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py new file mode 100644 index 0000000000..f113c04645 --- /dev/null +++ b/WebHostLib/misc.py @@ -0,0 +1,170 @@ +import datetime +import os + +import jinja2.exceptions +from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory + +from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4 +from worlds.AutoWorld import AutoWorldRegister +from . import app, cache + + +def get_world_theme(game_name: str): + if game_name in AutoWorldRegister.world_types: + return AutoWorldRegister.world_types[game_name].web.theme + return 'grass' + + +@app.before_request +def register_session(): + session.permanent = True # technically 31 days after the last visit + if not session.get("_id", None): + session["_id"] = uuid4() # uniquely identify each session without needing a login + + +@app.errorhandler(404) +@app.errorhandler(jinja2.exceptions.TemplateNotFound) +def page_not_found(err): + return render_template('404.html'), 404 + + +# Start Playing Page +@app.route('/start-playing') +def start_playing(): + return render_template(f"startPlaying.html") + + +@app.route('/weighted-settings') +def weighted_settings(): + return render_template(f"weighted-settings.html") + + +# Player settings pages +@app.route('/games//player-settings') +def player_settings(game): + return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) + + +# Game Info Pages +@app.route('/games//info/') +def game_info(game, lang): + return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) + + +# List of supported games +@app.route('/games') +def games(): + worlds = {} + for game, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + worlds[game] = world + return render_template("supportedGames.html", worlds=worlds) + + +@app.route('/tutorial///') +def tutorial(game, file, lang): + return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) + + +@app.route('/tutorial/') +def tutorial_landing(): + worlds = {} + for game, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + worlds[game] = world + return render_template("tutorialLanding.html") + + +@app.route('/faq//') +def faq(lang): + return render_template("faq.html", lang=lang) + + +@app.route('/glossary//') +def terms(lang): + return render_template("glossary.html", lang=lang) + + +@app.route('/seed/') +def view_seed(seed: UUID): + seed = Seed.get(id=seed) + if not seed: + abort(404) + return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots)) + + +@app.route('/new_room/') +def new_room(seed: UUID): + seed = Seed.get(id=seed) + if not seed: + abort(404) + room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) + commit() + return redirect(url_for("host_room", room=room.id)) + + +def _read_log(path: str): + if os.path.exists(path): + with open(path, encoding="utf-8-sig") as log: + yield from log + else: + yield f"Logfile {path} does not exist. " \ + f"Likely a crash during spinup of multiworld instance or it is still spinning up." + + +@app.route('/log/') +def display_log(room: UUID): + room = Room.get(id=room) + if room is None: + return abort(404) + if room.owner == session["_id"]: + return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") + return "Access Denied", 403 + + +@app.route('/room/', methods=['GET', 'POST']) +def host_room(room: UUID): + room = Room.get(id=room) + if room is None: + return abort(404) + if request.method == "POST": + if room.owner == session["_id"]: + cmd = request.form["cmd"] + if cmd: + Command(room=room, commandtext=cmd) + commit() + + with db_session: + room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running + + return render_template("hostRoom.html", room=room) + + +@app.route('/favicon.ico') +def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static/static'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + + +@app.route('/discord') +def discord(): + return redirect("https://discord.gg/archipelago") + + +@app.route('/datapackage') +@cache.cached() +def get_datapackge(): + """A pretty print version of /api/datapackage""" + from worlds import network_data_package + import json + return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain") + + +@app.route('/index') +@app.route('/sitemap') +def get_sitemap(): + available_games = [] + for game, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + available_games.append(game) + return render_template("siteMap.html", games=available_games) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 5e249c19ea..4179478985 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -11,7 +11,7 @@ from worlds.alttp import Items from WebHostLib import app, cache, Room from Utils import restricted_loads from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name -from MultiServer import get_item_name_from_id, Context +from MultiServer import Context from NetUtils import SlotType alttp_icons = { @@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID): for (team, player), data in multisave.get("video", []): video[(team, player)] = data - return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, + return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas, diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 3c29032aa5..3a98bfe562 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -45,7 +45,6 @@ class MeritousWorld(World): item_name_groups = item_groups data_version = 2 - forced_auto_forfeit = False # NOTE: Remember to change this before this game goes live required_client_version = (0, 2, 4) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 401a2d683b..46282fe316 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -35,9 +35,7 @@ class SM64World(World): location_name_to_id = location_table data_version = 6 - required_client_version = (0,3,0) - - forced_auto_forfeit = False + required_client_version = (0, 3, 0) area_connections: typing.Dict[int, int] diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 04947716d3..4959ddca1b 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -35,7 +35,6 @@ class V6World(World): location_name_to_id = location_table data_version = 1 - forced_auto_forfeit = False area_connections: typing.Dict[int, int] area_cost_map: typing.Dict[int,int] From eb5ba72cfc95e17b148c267fc24f94eeee3e5ba2 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:23:22 -0400 Subject: [PATCH 095/138] Smz3 min accessibility fix (#880) --- worlds/smz3/TotalSMZ3/Config.py | 1 - worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py | 3 +-- worlds/smz3/__init__.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/worlds/smz3/TotalSMZ3/Config.py b/worlds/smz3/TotalSMZ3/Config.py index 1c3bddb188..bfcd541b98 100644 --- a/worlds/smz3/TotalSMZ3/Config.py +++ b/worlds/smz3/TotalSMZ3/Config.py @@ -48,7 +48,6 @@ class Config: Keysanity: bool = KeyShuffle != KeyShuffle.Null Race: bool = False GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals - MinimalAccessibility: bool = False # AP specific accessibility: minimal def __init__(self, options: Dict[str, str]): self.GameMode = self.ParseOption(options, GameMode.Multiworld) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py index ab7c86a631..1805e74dca 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py @@ -14,8 +14,7 @@ class SwampPalace(Z3Region, IReward): self.Reward = RewardType.Null self.Locations = [ Location(self, 256+135, 0x1EA9D, LocationType.Regular, "Swamp Palace - Entrance") - .Allow(lambda item, items: self.Config.Keysanity or self.Config.MinimalAccessibility or - item.Is(ItemType.KeySP, self.world)), + .Allow(lambda item, items: self.Config.Keysanity or item.Is(ItemType.KeySP, self.world)), Location(self, 256+136, 0x1E986, LocationType.Regular, "Swamp Palace - Map Chest", lambda items: items.KeySP), Location(self, 256+137, 0x1E989, LocationType.Regular, "Swamp Palace - Big Chest", diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index fcedfd45db..9a0fcad90e 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -190,7 +190,6 @@ class SMZ3World(World): config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value) config.Keysanity = config.KeyShuffle != KeyShuffle.Null config.GanonInvincible = GanonInvincible.BeforeCrystals - config.MinimalAccessibility = self.world.accessibility[self.player] == Accessibility.option_minimal self.local_random = random.Random(self.world.random.randint(0, 1000)) self.smz3World = TotalSMZ3World(config, self.world.get_player_name(self.player), self.player, self.world.seed_name) @@ -525,6 +524,7 @@ class SMZ3World(World): def InitialFillInOwnWorld(self): self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySW, self.smz3World.GetLocation("Skull Woods - Pinball Room")) + self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySP, self.smz3World.GetLocation("Swamp Palace - Entrance")) # /* Check Swords option and place as needed */ if self.smz3World.Config.SwordLocation == SwordLocation.Uncle: From a378d62dfd2a93d14b14253e46fed7fab57ab969 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 8 Aug 2022 23:20:18 +0200 Subject: [PATCH 096/138] SC2: fix Moebius Factor rescue condition (#882) --- worlds/sc2wol/Locations.py | 12 ++++++------ worlds/sc2wol/LogicMixin.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index dc2ec74a4b..92dfb033c0 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -103,19 +103,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, lambda state: state._sc2wol_has_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001, - lambda state: state._sc2wol_has_air(world, player) or True), + lambda state: True), LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, lambda state: state._sc2wol_has_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, lambda state: state._sc2wol_has_air(world, player)), LocationData("The Moebius Factor", "Beat The Moebius Factor", None, diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index 7e2fc2f0e8..baf77dc677 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -37,7 +37,7 @@ class SC2WoLLogic(LogicMixin): self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Medivac', 'Hercules', 'Raven', 'Orbital Strike'}, player) + return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) From fb2979d9ef084bc02d630e7b0966bfdd525919c6 Mon Sep 17 00:00:00 2001 From: TheCondor07 Date: Mon, 8 Aug 2022 15:20:51 -0700 Subject: [PATCH 097/138] SC2: Added Difficulty Override to Client (#863) --- Starcraft2Client.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index e9e06335ac..dc63e9a456 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -44,11 +44,38 @@ nest_asyncio.apply() class StarcraftClientProcessor(ClientCommandProcessor): ctx: SC2Context + def _cmd_difficulty(self, difficulty: str = "") -> bool: + """Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal""" + options = difficulty.split() + num_options = len(options) + difficulty_choice = options[0].lower() + + if num_options > 0: + if difficulty_choice == "casual": + self.ctx.difficulty_override = 0 + elif difficulty_choice == "normal": + self.ctx.difficulty_override = 1 + elif difficulty_choice == "hard": + self.ctx.difficulty_override = 2 + elif difficulty_choice == "brutal": + self.ctx.difficulty_override = 3 + else: + self.output("Unable to parse difficulty '" + options[0] + "'") + return False + + self.output("Difficulty set to " + options[0]) + return True + + else: + self.output("Difficulty needs to be specified in the command.") + return False + def _cmd_disable_mission_check(self) -> bool: """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play the next mission in a chain the other player is doing.""" self.ctx.missions_unlocked = True sc2_logger.info("Mission check has been disabled") + return True def _cmd_play(self, mission_id: str = "") -> bool: """Start a Starcraft 2 mission""" @@ -64,6 +91,7 @@ class StarcraftClientProcessor(ClientCommandProcessor): else: sc2_logger.info( "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.") + return False return True @@ -108,6 +136,7 @@ class SC2Context(CommonContext): missions_unlocked = False current_tooltip = None last_loc_list = None + difficulty_override = -1 async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -470,7 +499,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): game_state = 0 if iteration == 0: start_items = calculate_items(self.ctx.items_received) - difficulty = calc_difficulty(self.ctx.difficulty) + if self.ctx.difficulty_override >= 0: + difficulty = calc_difficulty(self.ctx.difficulty_override) + else: + difficulty = calc_difficulty(self.ctx.difficulty) await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format( difficulty, start_items[0], start_items[1], start_items[2], start_items[3], start_items[4], From b3700dabf20ba1890ef49130fbd5c1bfe7b86e5d Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 8 Aug 2022 19:29:00 -0500 Subject: [PATCH 098/138] Core: Fix meta.yaml and allow the `None` game category for common options (#845) --- Generate.py | 54 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/Generate.py b/Generate.py index 7cfe7a504c..70a8eaf667 100644 --- a/Generate.py +++ b/Generate.py @@ -7,7 +7,7 @@ import urllib.request import urllib.parse from typing import Set, Dict, Tuple, Callable, Any, Union import os -from collections import Counter +from collections import Counter, ChainMap import string import enum @@ -133,12 +133,14 @@ def main(args=None, callback=ERmain): if args.meta_file_path and os.path.exists(args.meta_file_path): try: - weights_cache[args.meta_file_path] = read_weights_yamls(args.meta_file_path) + meta_weights = read_weights_yamls(args.meta_file_path)[-1] except Exception as e: raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e - meta_weights = weights_cache[args.meta_file_path][-1] print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}") - del(meta_weights["meta_description"]) + try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file + del(meta_weights["meta_description"]) + except Exception as e: + raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e if args.samesettings: raise Exception("Cannot mix --samesettings with --meta") else: @@ -164,7 +166,7 @@ def main(args=None, callback=ERmain): player_files[player_id] = filename player_id += 1 - args.multi = max(player_id-1, args.multi) + args.multi = max(player_id - 1, args.multi) print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: " f"{args.plando}") @@ -186,26 +188,28 @@ def main(args=None, callback=ERmain): erargs.enemizercli = args.enemizercli settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ - {fname: ( tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) - for fname, yamls in weights_cache.items()} - player_path_cache = {} - for player in range(1, args.multi + 1): - player_path_cache[player] = player_files.get(player, args.weights_file_path) + {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) + for fname, yamls in weights_cache.items()} if meta_weights: for category_name, category_dict in meta_weights.items(): for key in category_dict: - option = get_choice(key, category_dict) + option = roll_meta_option(key, category_name, category_dict) if option is not None: - for player, path in player_path_cache.items(): + for path in weights_cache: for yaml in weights_cache[path]: if category_name is None: - yaml[key] = option + for category in yaml: + if category in AutoWorldRegister.world_types and key in Options.common_options: + yaml[category][key] = option elif category_name not in yaml: logging.warning(f"Meta: Category {category_name} is not present in {path}.") else: - yaml[category_name][key] = option + yaml[category_name][key] = option + player_path_cache = {} + for player in range(1, args.multi + 1): + player_path_cache[player] = player_files.get(player, args.weights_file_path) name_counter = Counter() erargs.player_settings = {} @@ -387,6 +391,28 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di return weights +def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: + if not game: + return get_choice(option_key, category_dict) + if game in AutoWorldRegister.world_types: + game_world = AutoWorldRegister.world_types[game] + options = ChainMap(game_world.options, Options.per_game_common_options) + if option_key in options: + if options[option_key].supports_weighting: + return get_choice(option_key, category_dict) + return options[option_key] + if game == "A Link to the Past": # TODO wow i hate this + if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode", + "triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra", + "triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality", + "boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time", + "red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes", + "misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite", + "random_sprite_on_event"}: + return get_choice(option_key, category_dict) + raise Exception(f"Error generating meta option {option_key} for {game}.") + + def roll_linked_options(weights: dict) -> dict: weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings for option_set in weights["linked_options"]: From 2c4e819010df33be20262b707a56636492d9d866 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 9 Aug 2022 03:47:01 -0500 Subject: [PATCH 099/138] docs: plando update (#861) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/generic/docs/plando_en.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index c73fa8d398..fa3edd1fa1 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -7,8 +7,7 @@ changes it up by allowing you to plan out certain aspects of the game by placing certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`, and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported -by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss -plando. +by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary. ### Enabling Plando From debda5d11149d18a5a278c2704f0ab7959e95344 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 9 Aug 2022 06:21:05 +0200 Subject: [PATCH 100/138] MultiServer: swap auto-forfeit with auto-collect order That way the forfeit for items for players that are still playing appear last in the log, which is the visible text in at least the py clients --- MultiServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 3e502f649f..8a1844bf92 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -579,12 +579,12 @@ class Context: finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \ f' has completed their goal.' self.notify_all(finished_msg) + if "auto" in self.collect_mode: + collect_player(self, client.team, client.slot) if "auto" in self.forfeit_mode: forfeit_player(self, client.team, client.slot) elif self.forced_auto_forfeits[self.games[client.slot]]: forfeit_player(self, client.team, client.slot) - if "auto" in self.collect_mode: - collect_player(self, client.team, client.slot) def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False): From f2e83c37e9e4e4fdd1de9d63e9d7692859bbcb75 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 9 Aug 2022 22:21:45 +0200 Subject: [PATCH 101/138] WebHost: use title-typical sorting for game titles (#883) --- Utils.py | 11 +++++++++++ WebHost.py | 2 +- WebHostLib/__init__.py | 2 ++ WebHostLib/templates/supportedGames.html | 3 ++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Utils.py b/Utils.py index 8f00a91047..82d13b3f84 100644 --- a/Utils.py +++ b/Utils.py @@ -603,3 +603,14 @@ def messagebox(title: str, text: str, error: bool = False) -> None: root.withdraw() showerror(title, text) if error else showinfo(title, text) root.update() + + +def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))): + """Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning.""" + def sorter(element: str) -> str: + parts = element.split(maxsplit=1) + if parts[0].lower() in ignore: + return parts[1] + else: + return element + return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i)) diff --git a/WebHost.py b/WebHost.py index 5d3c44592b..09f8d8235a 100644 --- a/WebHost.py +++ b/WebHost.py @@ -85,7 +85,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] for games in data: if 'Archipelago' in games['gameTitle']: generic_data = data.pop(data.index(games)) - sorted_data = [generic_data] + sorted(data, key=lambda entry: entry["gameTitle"].lower()) + sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower()) json.dump(sorted_data, json_target, indent=2, ensure_ascii=False) return sorted_data diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index b2d243c913..b7bf4e38d1 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -9,6 +9,7 @@ from flask_caching import Cache from flask_compress import Compress from werkzeug.routing import BaseConverter +from Utils import title_sorted from .models import * UPLOAD_FOLDER = os.path.relpath('uploads') @@ -65,6 +66,7 @@ class B64UUIDConverter(BaseConverter): # short UUID app.url_map.converters["suuid"] = B64UUIDConverter app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') +app.jinja_env.filters["title_sorted"] = title_sorted def register(): diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 125551fbb9..fe81463a46 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -10,7 +10,8 @@ {% include 'header/oceanHeader.html' %}

Currently Supported Games

- {% for game_name, world in worlds.items() | sort(attribute=0) %} + {% for game_name in worlds | title_sorted %} + {% set world = worlds[game_name] %}

{{ game_name }}

{{ world.__doc__ | default("No description provided.", true) }}
From e1e2526322aeaf6b59bf37ffa892c547762faf3d Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 10 Aug 2022 13:21:52 -0700 Subject: [PATCH 102/138] LttP: Do a check for enemizer much earlier in generation. (#875) --- worlds/alttp/__init__.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 64b1bf8db5..8f39b606e4 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -14,8 +14,8 @@ from .Rules import set_rules from .ItemPool import generate_itempool, difficulties from .Shops import create_shops, ShopSlotFill from .Dungeons import create_dungeons -from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string, \ - get_base_rom_path, LttPDeltaPatch +from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ + get_hash_string, get_base_rom_path, LttPDeltaPatch import Patch from itertools import chain @@ -156,6 +156,9 @@ class ALTTPWorld(World): player = self.player world = self.world + if self.use_enemizer(): + check_enemizer(world.enemizer) + # system for sharing ER layouts self.er_seed = str(world.random.randint(0, 2 ** 64)) @@ -341,14 +344,19 @@ class ALTTPWorld(World): def stage_post_fill(cls, world): ShopSlotFill(world) + def use_enemizer(self): + world = self.world + player = self.player + return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] + or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' + or world.pot_shuffle[player] or world.bush_shuffle[player] + or world.killable_thieves[player]) + def generate_output(self, output_directory: str): world = self.world player = self.player try: - use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] - or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' - or world.pot_shuffle[player] or world.bush_shuffle[player] - or world.killable_thieves[player]) + use_enemizer = self.use_enemizer() rom = LocalRom(get_base_rom_path()) From 29e09758324a757abc4fac126c2ee5bdb37001c3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 10 Aug 2022 22:20:14 +0200 Subject: [PATCH 103/138] Clients: prepare for removal of players key in RoomInfo --- CommonClient.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 76623ff3f2..e0a9c30784 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -562,18 +562,21 @@ async def process_server_cmd(ctx: CommonContext, args: dict): f" for each location checked. Use !hint for more information.") ctx.hint_cost = int(args['hint_cost']) ctx.check_points = int(args['location_check_points']) - players = args.get("players", []) - if len(players) < 1: - logger.info('No player connected') - else: - players.sort() - current_team = -1 - logger.info('Connected Players:') - for network_player in players: - if network_player.team != current_team: - logger.info(f' Team #{network_player.team + 1}') - current_team = network_player.team - logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) + + if "players" in args: # TODO remove when servers sending this are outdated + players = args.get("players", []) + if len(players) < 1: + logger.info('No player connected') + else: + players.sort() + current_team = -1 + logger.info('Connected Players:') + for network_player in players: + if network_player.team != current_team: + logger.info(f' Team #{network_player.team + 1}') + current_team = network_player.team + logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) + # update datapackage await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"]) From b98969874019e14baaeb3793d420a6b40e84c404 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 11 Aug 2022 00:58:08 +0200 Subject: [PATCH 104/138] WebHost: fix datapackage typo --- WebHostLib/api/__init__.py | 4 ++-- WebHostLib/misc.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index c2f9b3840f..80c60a093a 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -32,14 +32,14 @@ def room_info(room: UUID): @api_endpoints.route('/datapackage') @cache.cached() -def get_datapackge(): +def get_datapackage(): from worlds import network_data_package return network_data_package @api_endpoints.route('/datapackage_version') @cache.cached() -def get_datapackge_versions(): +def get_datapackage_versions(): from worlds import network_data_package, AutoWorldRegister version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()} version_package["version"] = network_data_package["version"] diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index f113c04645..44377cf445 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -153,7 +153,7 @@ def discord(): @app.route('/datapackage') @cache.cached() -def get_datapackge(): +def get_datapackage(): """A pretty print version of /api/datapackage""" from worlds import network_data_package import json From ffe528467e4735d858d00c412166638671703c5a Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 11 Aug 2022 01:02:06 +0200 Subject: [PATCH 105/138] Generate: remove period for easy copy&paste Double-clicking in terminal may select the period, resulting in a bad filename in clipboard. Also fixing quotes. --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 3b094326f2..48095e06bd 100644 --- a/Main.py +++ b/Main.py @@ -423,7 +423,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) zipfilename = output_path(f"AP_{world.seed_name}.zip") - logger.info(f'Creating final archive at {zipfilename}.') + logger.info(f"Creating final archive at {zipfilename}") with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf: for file in os.scandir(temp_dir): From c96acbfa2379f8a977fb3ecaa681799b65fb90a7 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 10 Aug 2022 23:05:36 +0200 Subject: [PATCH 106/138] TextClient: receive all items By popular demand, this makes /received work again. Closes #887 --- CommonClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CommonClient.py b/CommonClient.py index e0a9c30784..0b2c22cfd8 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -726,7 +726,7 @@ if __name__ == '__main__': class TextContext(CommonContext): tags = {"AP", "IgnoreGame", "TextOnly"} game = "" # empty matches any game since 0.3.2 - items_handling = 0 # don't receive any NetworkItems + items_handling = 0b111 # receive all items for /received async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: From b32d0efe6dcb69f6bbd887efdbf195ca8eb5784a Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 11 Aug 2022 15:57:33 +0200 Subject: [PATCH 107/138] Witness: Logic fix for Treehouse in Doors (#892) --- worlds/witness/WitnessLogic.txt | 10 +++++++--- worlds/witness/player_logic.py | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 71ca7c819f..c98257fb73 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -691,7 +691,7 @@ Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17 158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Squares & Colored Squares 158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Squares & Colored Squares -Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room - 0x0C323: +Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDB - Treehouse Laser Room Back Platform - 0x17DDB: 158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol 158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol 158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol @@ -707,8 +707,6 @@ Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room - 0x0C323: 158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol 158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol 158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158611 - 0x17FA0 (Burnt House Discard) - 0x17DDB - Triangles -Door - 0x0C323 (Door to Laser House) - 0x17DDB & 0x17DA2 & 0x2700B Treehouse Green Bridge (Treehouse): 158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers @@ -720,6 +718,12 @@ Treehouse Green Bridge (Treehouse): 158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers 158610 - 0x17FA9 (Green Bridge Discard) - 0x17E61 - Triangles +Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: +Door - 0x0C323 (Door to Laser House) - 0x17DA2 & 0x2700B & 0x17DDB + +Treehouse Laser Room Back Platform (Treehouse): +158611 - 0x17FA0 (Burnt House Discard) - True - Triangles + Treehouse Laser Room (Treehouse): 158712 - 0x03613 (Laser Panel) - True - True 158403 - 0x17CBC (Laser House Door Timer Inside Control) - True - True diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 3639e836df..efbb177f00 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -355,6 +355,7 @@ class WitnessPlayerLogic: "0x19B24": "Shadows Lower Avoid Patterns Visible", "0x2700B": "Open Door to Treehouse Laser House", "0x00055": "Orchard Apple Trees 4 Turns On", + "0x17DDB": "Left Orange Bridge Fully Extended", } self.ALWAYS_EVENT_NAMES_BY_HEX = { From adc16fdd3dad1827e9df769561ce8c3b8023d70a Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Thu, 11 Aug 2022 09:11:34 -0700 Subject: [PATCH 108/138] Factorio: Don't send researches completed by editor extensions testing forces. (#894) --- worlds/factorio/data/mod_template/control.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index e7774fef00..63473808c6 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -286,6 +286,12 @@ end) -- hook into researches done script.on_event(defines.events.on_research_finished, function(event) local technology = event.research + if string.find(technology.force.name, "EE_TESTFORCE") == 1 then + --Don't acknowledge AP research as an Editor Extensions test force + --Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces + --are worked on exclusively in editor mode. + return + end if technology.researched and string.find(technology.name, "ap%-") == 1 then -- check if it came from the server anyway, then we don't need to double send. dumpInfo(technology.force) --is sendable From b8ca41b45fc8c2ffccdb979859eafafedf1eaabe Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 12 Aug 2022 00:46:11 +0200 Subject: [PATCH 109/138] Utils: SI: fix rounding problems (#895) * Utils: SI: fix rounding problems 999.999 would give 1000.00 instead of 1.00k * Tests: add Utils: SI tests --- Utils.py | 5 +++-- test/utils/TestSIPrefix.py | 44 ++++++++++++++++++++++++++++++++++++++ test/utils/__init__.py | 0 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 test/utils/TestSIPrefix.py create mode 100644 test/utils/__init__.py diff --git a/Utils.py b/Utils.py index 82d13b3f84..cd1956a805 100644 --- a/Utils.py +++ b/Utils.py @@ -504,11 +504,12 @@ def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str: # noinspection PyPep8Naming -def format_SI_prefix(value, power=1000, power_labels=('', 'k', 'M', 'G', 'T', "P", "E", "Z", "Y")) -> str: +def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P", "E", "Z", "Y")) -> str: """Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix""" n = 0 value = decimal.Decimal(value) - while value >= power: + limit = power - decimal.Decimal("0.005") + while value >= limit: value /= power n += 1 diff --git a/test/utils/TestSIPrefix.py b/test/utils/TestSIPrefix.py new file mode 100644 index 0000000000..81c7e0daf0 --- /dev/null +++ b/test/utils/TestSIPrefix.py @@ -0,0 +1,44 @@ +# Tests for SI prefix in Utils.py + +import unittest +from decimal import Decimal +from Utils import format_SI_prefix + + +class TestGenerateMain(unittest.TestCase): + """This tests SI prefix formatting in Utils.py""" + def assertEqual(self, first, second, msg=None): + # we strip spaces everywhere because that is an undefined implementation detail + super().assertEqual(first.replace(" ", ""), second.replace(" ", ""), msg) + + def test_rounding(self): + # we don't care if float(999.995) would fail due to error in precision + self.assertEqual(format_SI_prefix(999.999), "1.00k") + self.assertEqual(format_SI_prefix(1000.001), "1.00k") + self.assertEqual(format_SI_prefix(Decimal("999.995")), "1.00k") + self.assertEqual(format_SI_prefix(Decimal("1000.004")), "1.00k") + + def test_letters(self): + self.assertEqual(format_SI_prefix(0e0), "0.00") + self.assertEqual(format_SI_prefix(1e3), "1.00k") + self.assertEqual(format_SI_prefix(2e6), "2.00M") + self.assertEqual(format_SI_prefix(3e9), "3.00G") + self.assertEqual(format_SI_prefix(4e12), "4.00T") + self.assertEqual(format_SI_prefix(5e15), "5.00P") + self.assertEqual(format_SI_prefix(6e18), "6.00E") + self.assertEqual(format_SI_prefix(7e21), "7.00Z") + self.assertEqual(format_SI_prefix(8e24), "8.00Y") + + def test_multiple_letters(self): + self.assertEqual(format_SI_prefix(9e27), "9.00kY") + + def test_custom_power(self): + self.assertEqual(format_SI_prefix(1023.99, 1024), "1023.99") + self.assertEqual(format_SI_prefix(1034.24, 1024), "1.01k") + + def test_custom_labels(self): + labels = ("E", "da", "h", "k") + self.assertEqual(format_SI_prefix(1, 10, labels), "1.00E") + self.assertEqual(format_SI_prefix(10, 10, labels), "1.00da") + self.assertEqual(format_SI_prefix(100, 10, labels), "1.00h") + self.assertEqual(format_SI_prefix(1000, 10, labels), "1.00k") diff --git a/test/utils/__init__.py b/test/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From b702ae482ba14822ecf0f56f7ea3820450c39c63 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 12 Aug 2022 00:32:37 +0200 Subject: [PATCH 110/138] Core: clean up Utils.py * fix import order * lazy import shutil * lazy import jellyfish (also speed-up by 0.8%, probably because of inlining) * yaml: * explicitely call Loader UnsafeLoader * use CDumper, twice as fast * stop leaking leak imported names load and load_all * open_file: use absolute path * replace quotes in touched code * add some typing in touched code * stringify type hinting for non-imports * %s/.format -> f * freeze safe_builtins * remove double-caching in get_options() * get rid of some warnings --- Utils.py | 141 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/Utils.py b/Utils.py index cd1956a805..b94e2862a3 100644 --- a/Utils.py +++ b/Utils.py @@ -1,6 +1,5 @@ from __future__ import annotations -import shutil import typing import builtins import os @@ -13,11 +12,18 @@ import collections import importlib import logging import decimal +from yaml import load, load_all, dump, SafeLoader + +try: + from yaml import CLoader as UnsafeLoader + from yaml import CDumper as Dumper +except ImportError: + from yaml import Loader as UnsafeLoader + from yaml import Dumper if typing.TYPE_CHECKING: - from tkinter import Tk -else: - Tk = typing.Any + import tkinter + import pathlib def tuplize_version(version: str) -> Version: @@ -33,18 +39,10 @@ class Version(typing.NamedTuple): __version__ = "0.3.4" version_tuple = tuplize_version(__version__) -is_linux = sys.platform.startswith('linux') -is_macos = sys.platform == 'darwin' +is_linux = sys.platform.startswith("linux") +is_macos = sys.platform == "darwin" is_windows = sys.platform in ("win32", "cygwin", "msys") -import jellyfish -from yaml import load, load_all, dump, SafeLoader - -try: - from yaml import CLoader as Loader -except ImportError: - from yaml import Loader - def int16_as_bytes(value: int) -> typing.List[int]: value = value & 0xFFFF @@ -125,17 +123,18 @@ def home_path(*path: str) -> str: def user_path(*path: str) -> str: """Returns either local_path or home_path based on write permissions.""" - if hasattr(user_path, 'cached_path'): + if hasattr(user_path, "cached_path"): pass elif os.access(local_path(), os.W_OK): user_path.cached_path = local_path() else: user_path.cached_path = home_path() # populate home from local - TODO: upgrade feature - if user_path.cached_path != local_path() and not os.path.exists(user_path('host.yaml')): - for dn in ('Players', 'data/sprites'): + if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")): + import shutil + for dn in ("Players", "data/sprites"): shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True) - for fn in ('manifest.json', 'host.yaml'): + for fn in ("manifest.json", "host.yaml"): shutil.copy2(local_path(fn), user_path(fn)) return os.path.join(user_path.cached_path, *path) @@ -150,11 +149,12 @@ def output_path(*path: str): return path -def open_file(filename): - if sys.platform == 'win32': +def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None: + if is_windows: os.startfile(filename) else: - open_command = 'open' if sys.platform == 'darwin' else 'xdg-open' + from shutil import which + open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open")) subprocess.call([open_command, filename]) @@ -173,7 +173,9 @@ class UniqueKeyLoader(SafeLoader): parse_yaml = functools.partial(load, Loader=UniqueKeyLoader) parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader) -unsafe_parse_yaml = functools.partial(load, Loader=Loader) +unsafe_parse_yaml = functools.partial(load, Loader=UnsafeLoader) + +del load, load_all # should not be used. don't leak their names def get_cert_none_ssl_context(): @@ -191,11 +193,12 @@ def get_public_ipv4() -> str: ip = socket.gethostbyname(socket.gethostname()) ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip() + ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip() except Exception as e: + # noinspection PyBroadException try: - ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip() - except: + ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip() + except Exception: logging.exception(e) pass # we could be offline, in a local game, so no point in erroring out return ip @@ -208,7 +211,7 @@ def get_public_ipv6() -> str: ip = socket.gethostbyname(socket.gethostname()) ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip() + ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip() except Exception as e: logging.exception(e) pass # we could be offline, in a local game, or ipv6 may not be available @@ -309,23 +312,19 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict: @cache_argsless def get_options() -> dict: - if not hasattr(get_options, "options"): - filenames = ("options.yaml", "host.yaml") - locations = [] - if os.path.join(os.getcwd()) != local_path(): - locations += filenames # use files from cwd only if it's not the local_path - locations += [user_path(filename) for filename in filenames] + filenames = ("options.yaml", "host.yaml") + locations = [] + if os.path.join(os.getcwd()) != local_path(): + locations += filenames # use files from cwd only if it's not the local_path + locations += [user_path(filename) for filename in filenames] - for location in locations: - if os.path.exists(location): - with open(location) as f: - options = parse_yaml(f.read()) + for location in locations: + if os.path.exists(location): + with open(location) as f: + options = parse_yaml(f.read()) + return update_options(get_default_options(), options, location, list()) - get_options.options = update_options(get_default_options(), options, location, list()) - break - else: - raise FileNotFoundError(f"Could not find {filenames[1]} to load options.") - return get_options.options + raise FileNotFoundError(f"Could not find {filenames[1]} to load options.") def persistent_store(category: str, key: typing.Any, value: typing.Any): @@ -334,10 +333,10 @@ def persistent_store(category: str, key: typing.Any, value: typing.Any): category = storage.setdefault(category, {}) category[key] = value with open(path, "wt") as f: - f.write(dump(storage)) + f.write(dump(storage, Dumper=Dumper)) -def persistent_load() -> typing.Dict[dict]: +def persistent_load() -> typing.Dict[str, dict]: storage = getattr(persistent_load, "storage", None) if storage: return storage @@ -355,8 +354,8 @@ def persistent_load() -> typing.Dict[dict]: return storage -def get_adjuster_settings(gameName: str): - adjuster_settings = persistent_load().get("adjuster", {}).get(gameName, {}) +def get_adjuster_settings(game_name: str): + adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {}) return adjuster_settings @@ -372,10 +371,10 @@ def get_unique_identifier(): return uuid -safe_builtins = { +safe_builtins = frozenset(( 'set', 'frozenset', -} +)) class RestrictedUnpickler(pickle.Unpickler): @@ -403,8 +402,7 @@ class RestrictedUnpickler(pickle.Unpickler): if issubclass(obj, self.options_module.Option): return obj # Forbid everything else. - raise pickle.UnpicklingError("global '%s.%s' is forbidden" % - (module, name)) + raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") def restricted_loads(s): @@ -483,11 +481,11 @@ def stream_input(stream, queue): return thread -def tkinter_center_window(window: Tk): +def tkinter_center_window(window: "tkinter.Tk") -> None: window.update() - xPos = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2) - yPos = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2) - window.geometry("+{}+{}".format(xPos, yPos)) + x = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2) + y = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2) + window.geometry(f"+{x}+{y}") class VersionException(Exception): @@ -516,13 +514,14 @@ def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}" -def get_fuzzy_ratio(word1: str, word2: str) -> float: - return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower()) - / max(len(word1), len(word2))) - - def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \ -> typing.List[typing.Tuple[str, int]]: + import jellyfish + + def get_fuzzy_ratio(word1: str, word2: str) -> float: + return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower()) + / max(len(word1), len(word2))) + limit: int = limit if limit else len(wordlist) return list( map( @@ -540,18 +539,19 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \ -> typing.Optional[str]: def run(*args: str): - return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None + return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None if is_linux: # prefer native dialog - kdialog = shutil.which('kdialog') + from shutil import which + kdialog = which("kdialog") if kdialog: k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes)) - return run(kdialog, f'--title={title}', '--getopenfilename', '.', k_filters) - zenity = shutil.which('zenity') + return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters) + zenity = which("zenity") if zenity: z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes) - return run(zenity, f'--title={title}', '--file-selection', *z_filters) + return run(zenity, f"--title={title}", "--file-selection", *z_filters) # fall back to tk try: @@ -569,10 +569,10 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin def messagebox(title: str, text: str, error: bool = False) -> None: def run(*args: str): - return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None + return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None def is_kivy_running(): - if 'kivy' in sys.modules: + if "kivy" in sys.modules: from kivy.app import App return App.get_running_app() is not None return False @@ -582,14 +582,15 @@ def messagebox(title: str, text: str, error: bool = False) -> None: MessageBox(title, text, error).open() return - if is_linux and not 'tkinter' in sys.modules: + if is_linux and "tkinter" not in sys.modules: # prefer native dialog - kdialog = shutil.which('kdialog') + from shutil import which + kdialog = which("kdialog") if kdialog: - return run(kdialog, f'--title={title}', '--error' if error else '--msgbox', text) - zenity = shutil.which('zenity') + return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text) + zenity = which("zenity") if zenity: - return run(zenity, f'--title={title}', f'--text={text}', '--error' if error else '--info') + return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info") # fall back to tk try: From 2e428f906c922dd033470f4c94fafa37978942dd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 12 Aug 2022 06:52:01 +0200 Subject: [PATCH 111/138] Core: document KeyedDefaultDict --- Utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Utils.py b/Utils.py index b94e2862a3..aafd6014cf 100644 --- a/Utils.py +++ b/Utils.py @@ -411,6 +411,9 @@ def restricted_loads(s): class KeyedDefaultDict(collections.defaultdict): + """defaultdict variant that uses the missing key as argument to default_factory""" + default_factory: typing.Callable[[typing.Any], typing.Any] + def __missing__(self, key): self[key] = value = self.default_factory(key) return value From 9bd035a19d9667cebec3090a7c57dc58b5f9cacc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 12 Aug 2022 04:55:40 +0200 Subject: [PATCH 112/138] WebHost: make a fresh Room reload page once if port is not assigned yet --- WebHostLib/misc.py | 9 ++++++--- WebHostLib/templates/hostRoom.html | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 44377cf445..03cd03b624 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -124,7 +124,7 @@ def display_log(room: UUID): @app.route('/room/', methods=['GET', 'POST']) def host_room(room: UUID): - room = Room.get(id=room) + room: Room = Room.get(id=room) if room is None: return abort(404) if request.method == "POST": @@ -134,10 +134,13 @@ def host_room(room: UUID): Command(room=room, commandtext=cmd) commit() + now = datetime.datetime.utcnow() + # indicate that the page should reload to get the assigned port + should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3) with db_session: - room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running + room.last_activity = now # will trigger a spinup, if it's not already running - return render_template("hostRoom.html", room=room) + return render_template("hostRoom.html", room=room, should_refresh=should_refresh) @app.route('/favicon.ico') diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 15429e7f8d..8981de9b7a 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -2,6 +2,7 @@ {% import "macros.html" as macros %} {% block head %} Multiworld {{ room.id|suuid }} + {% if should_refresh %}{% endif %} {% endblock %} From f5e48c850d6a62d480a525da71ae692f3ae2fed4 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 12 Aug 2022 23:02:56 +0200 Subject: [PATCH 113/138] Utils: lazy decimal import decimal is kinda big, there is no noticable difference in performance and the import is unused by webhost's customserver --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index aafd6014cf..bb29166f75 100644 --- a/Utils.py +++ b/Utils.py @@ -11,7 +11,6 @@ import io import collections import importlib import logging -import decimal from yaml import load, load_all, dump, SafeLoader try: @@ -507,6 +506,7 @@ def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str: # noinspection PyPep8Naming def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P", "E", "Z", "Y")) -> str: """Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix""" + import decimal n = 0 value = decimal.Decimal(value) limit = power - decimal.Decimal("0.005") From 645ede869f18ff3b8bbb3cf0f265ba8d0ab1bc26 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Fri, 12 Aug 2022 19:36:06 -0700 Subject: [PATCH 114/138] OoT: Fix blind item.type reference (#905) * oot: remove blind reference to item.type * oot: logical reasoning is hard * oot: fix blind item.type reference --- worlds/oot/Hints.py | 3 ++- worlds/oot/Patches.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index 4250c5590e..b8ae7dfafc 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -10,6 +10,7 @@ from urllib.error import URLError, HTTPError import json from enum import Enum +from .Items import OOTItem from .HintList import getHint, getHintGroup, Hint, hintExclusions from .Messages import COLOR_MAP, update_message_by_id from .TextBox import line_wrap @@ -480,7 +481,7 @@ def get_specific_item_hint(world, checked): def get_random_location_hint(world, checked): locations = list(filter(lambda location: is_not_checked(location, checked) - and location.item.type not in ('Drop', 'Event', 'Shop', 'DungeonReward') + and not (isinstance(location.item, OOTItem) and location.item.type in ('Drop', 'Event', 'Shop', 'DungeonReward')) # and not (location.parent_region.dungeon and isRestrictedDungeonItem(location.parent_region.dungeon, location.item)) # AP already locks dungeon items and not location.locked and location.name not in world.hint_exclusions diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 177a4c6165..91f656b4e9 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -5,6 +5,7 @@ import zlib from collections import defaultdict from functools import partial +from .Items import OOTItem from .LocationList import business_scrubs from .Hints import writeGossipStoneHints, buildAltarHints, \ buildGanonText, getSimpleHintNoPrefix @@ -1881,9 +1882,9 @@ def get_override_entry(player_id, location): type = 2 elif location.type == 'GS Token': type = 3 - elif location.type == 'Shop' and location.item.type != 'Shop': + elif location.type == 'Shop' and not (isinstance(location.item, OOTItem) and location.item.type == 'Shop'): type = 0 - elif location.type == 'GrottoNPC' and location.item.type != 'Shop': + elif location.type == 'GrottoNPC' and not (isinstance(location.item, OOTItem) and location.item.type == 'Shop'): type = 4 elif location.type in ['Song', 'Cutscene']: type = 5 From 0ed0d17f38fbd63c3737605aa237f33c673c8e81 Mon Sep 17 00:00:00 2001 From: Ludovic Marechal Date: Sun, 14 Aug 2022 00:07:36 +0200 Subject: [PATCH 115/138] DS3: Update the setup guide (#878) * Merge pull request #1 from eudaimonistic/patch-2 Update setup_en.md (cherry picked from commit 41567697fb89e74301afe651fbde0bafca5946e0) * DS3: Update english documentation * DS3: Add French setup guide * DS3: Fix space formatting in doc * DS3: Resolve comment --- worlds/dark_souls_3/__init__.py | 16 +++++++- worlds/dark_souls_3/docs/en_Dark Souls III.md | 5 ++- worlds/dark_souls_3/docs/setup_en.md | 18 +++++---- worlds/dark_souls_3/docs/setup_fr.md | 38 +++++++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 worlds/dark_souls_3/docs/setup_fr.md diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 0ff27acc43..3a5c26ccce 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -16,14 +16,26 @@ from ..generic.Rules import set_rule class DarkSouls3Web(WebWorld): - tutorials = [Tutorial( + bug_report_page = "https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/issues" + setup_en = Tutorial( "Multiworld Setup Tutorial", "A guide to setting up the Archipelago Dark Souls III randomizer on your computer.", "English", "setup_en.md", "setup/en", ["Marech"] - )] + ) + + setup_fr = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Français", + "setup_fr.md", + "setup/fr", + ["Marech"] + ) + + tutorials = [setup_en, setup_fr] class DarkSouls3World(World): diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md index 5860073c37..2effa5f124 100644 --- a/worlds/dark_souls_3/docs/en_Dark Souls III.md +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -10,7 +10,10 @@ config file. In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized. This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at the same location. I also added an option available from the settings page to randomize the level of the generated -weapons( from +0 to +10/+5 ) +weapons(from +0 to +10/+5) + +To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld +and kill the final boss "Soul of Cinder" ## What Dark Souls III items can appear in other players' worlds? diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index e08029283a..3d8606a5cf 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -3,7 +3,7 @@ ## Required Software - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) -- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client) +- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) ## General Concept @@ -14,22 +14,24 @@ The randomization is performed by the AP.json file, an output file generated by ## Installation Procedures -**This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed** + +**This mod can ban you permanently from the FromSoftware servers if used online.** + +This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed. -Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client). -Then you need to add the two following files at the root folder of your game -( e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game" ): +Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases). +Then you need to add the two following files at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game"): - **dinput8.dll** -- **AP.json** (renamed from the generated file AP-{ROOM_ID}.json) +- **AP.json** : The .json file downloaded from the multiworld room or provided by the host, named AP-{ROOM_ID}.json, has to be renamed to AP.json. ## Joining a MultiWorld Game 1. Run DarkSoulsIII.exe or run the game through Steam -2. Type in /connect {SERVER_IP}:{SERVER_PORT} in the "Windows Command Prompt" that opened +2. Type in "/connect {SERVER_IP}:{SERVER_PORT}" in the "Windows Command Prompt" that opened 3. Once connected, create a new game, choose a class and wait for the others before starting 4. You can quit and launch at anytime during a game ## Where do I get a config file? The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to -configure your personal settings and export them into a config file \ No newline at end of file +configure your personal settings and export them into a config file diff --git a/worlds/dark_souls_3/docs/setup_fr.md b/worlds/dark_souls_3/docs/setup_fr.md new file mode 100644 index 0000000000..f33b6951c5 --- /dev/null +++ b/worlds/dark_souls_3/docs/setup_fr.md @@ -0,0 +1,38 @@ +# Guide d'installation de Dark Souls III Randomizer + +## Logiciels requis + +- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) +- [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) + +## Concept général + +Le client Archipelago de Dark Souls III est un fichier dinput8.dll. Cette .dll va lancer une invite de commande Windows +permettant de lire des informations de la partie et écrire des commandes pour intéragir avec le serveur Archipelago. + +Le mélange des objets est réalisé par le fichier AP.json, un fichier généré par le serveur Archipelago. + +## Procédures d'installation + + +**Il y a des risques de bannissement permanent des serveurs FromSoftware si ce mod est utilisé en ligne.** + +Ce client a été testé sur la version Steam officielle du jeu (v1.15/1.35), peu importe les DLCs actuellement installés. + +Télécharger le fichier dinput8.dll disponible dans le [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases). +Vous devez ensuite ajouter les deux fichiers suivants à la racine du jeu +(ex: "SteamLibrary\steamapps\common\DARK SOULS III\Game"): +- **dinput8.dll** +- **AP.json** : Le fichier .json téléchargé depuis la room ou donné par l'hôte de la partie, nommé AP-{ROOM_ID}.json, doit être renommé en AP.json. + +## Rejoindre une partie Multiworld + +1. Lancer DarkSoulsIII.exe ou lancer le jeu depuis Steam +2. Ecrire "/connect {SERVER_IP}:{SERVER_PORT}" dans l'invite de commande Windows ouverte au lancement du jeu +3. Une fois connecté, créez une nouvelle partie, choisissez une classe et attendez que les autres soient prêts avant de lancer +4. Vous pouvez quitter et lancer le jeu n'importe quand pendant une partie + +## Où trouver le fichier de configuration ? + +La [Page de configuration](/games/Dark%20Souls%20III/player-settings) sur le site vous permez de configurer vos +paramètres et de les exporter sous la forme d'un fichier. From 23b04b5069edd3b1672915d37946da7ce1b80d10 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 13 Aug 2022 02:58:34 +0200 Subject: [PATCH 116/138] SM: correctly check if items are SM items --- worlds/sm/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index d2728132a3..8954e2b5f7 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -293,15 +293,15 @@ class SMWorld(World): if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None: # this SM world can find this item: write full item data to tables and assign player data for writing romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0 - if itemLoc.item.type in ItemManager.Items: - itemId = ItemManager.Items[itemLoc.item.type].Id + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items: + itemId = ItemManager.Items[itemLoc.item.type].Id else: itemId = ItemManager.Items['ArchipelagoItem'].Id + idx multiWorldItems.append({"sym": symbols["message_item_names"], "offset": (vanillaItemTypesCount + idx)*64, "values": self.convertToROMItemName(itemLoc.item.name)}) idx += 1 - + if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): playerIDCount += 1 self.playerIDMap[romPlayerID] = playerIDCount @@ -488,7 +488,13 @@ class SMWorld(World): # commit all the changes we've made here to the ROM romPatcher.commitIPS() - itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.world.get_locations() if itemLoc.player == self.player] + itemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], True) + for itemLoc in self.world.get_locations() if itemLoc.player == self.player + ] romPatcher.writeItemsLocs(itemLocs) itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player] From c02c6ee58ca8b0e324f9ff922ba2616802cf35d8 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 14 Aug 2022 12:34:46 -0700 Subject: [PATCH 117/138] Fix generation failure for Final Fantasy 1 and Dark Souls 3. (#907) * Fix generation failure for Final Fantasy 1. * Fix spoiler log giving "Location (Player x): Item (Player y)" for FF1. * Dark Soul 3 Items/Locations now get player names in spoiler log. --- BaseClasses.py | 2 +- worlds/dark_souls_3/__init__.py | 2 +- worlds/ff1/__init__.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 02a194eef5..aa37a097a6 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1202,7 +1202,7 @@ class Item: return self.__str__() def __str__(self) -> str: - if self.location: + if self.location and self.location.parent_region and self.location.parent_region.world: return self.location.parent_region.world.get_name_string_for_object(self) return f"{self.name} (Player {self.player})" diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 3a5c26ccce..7245499e28 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -158,7 +158,7 @@ class DarkSouls3World(World): # For each region, add the associated locations retrieved from the corresponding location_table def create_region(self, region_name, location_table) -> Region: - new_region = Region(region_name, RegionType.Generic, region_name, self.player) + new_region = Region(region_name, RegionType.Generic, region_name, self.player, self.world) if location_table: for name, address in location_table.items(): location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region) diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 9818bed974..d5a8dd30aa 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -54,6 +54,7 @@ class FF1World(World): locations = get_options(self.world, 'locations', self.player) rules = get_options(self.world, 'rules', self.player) menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules) + menu_region.world = self.world terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region) terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player) terminated_event.place_locked_item(terminated_item) From 898fa203ad852edeb4a9339d3291c51ff8206548 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Mon, 15 Aug 2022 10:48:13 -0400 Subject: [PATCH 118/138] Smz3 updated to version 11.3 (#886) --- worlds/smz3/Options.py | 86 +++- worlds/smz3/TotalSMZ3/Config.py | 105 ++--- worlds/smz3/TotalSMZ3/Item.py | 30 +- worlds/smz3/TotalSMZ3/Patch.py | 424 +++++++++--------- worlds/smz3/TotalSMZ3/Region.py | 21 +- .../Regions/SuperMetroid/Brinstar/Kraid.py | 2 +- .../Regions/SuperMetroid/Brinstar/Pink.py | 2 +- .../Regions/SuperMetroid/Crateria/East.py | 4 +- .../Regions/SuperMetroid/Maridia/Inner.py | 19 +- .../Regions/SuperMetroid/NorfairLower/East.py | 6 +- .../Regions/SuperMetroid/NorfairLower/West.py | 29 +- .../SuperMetroid/NorfairUpper/Crocomire.py | 9 +- .../Regions/SuperMetroid/WreckedShip.py | 7 +- .../Regions/Zelda/DarkWorld/NorthEast.py | 10 +- .../Regions/Zelda/DarkWorld/NorthWest.py | 2 +- .../Regions/Zelda/DarkWorld/South.py | 2 +- .../TotalSMZ3/Regions/Zelda/GanonsTower.py | 26 +- .../smz3/TotalSMZ3/Regions/Zelda/IcePalace.py | 3 +- .../Regions/Zelda/LightWorld/NorthEast.py | 2 +- .../Regions/Zelda/LightWorld/NorthWest.py | 4 +- .../TotalSMZ3/Regions/Zelda/MiseryMire.py | 8 +- .../TotalSMZ3/Regions/Zelda/SwampPalace.py | 1 + .../TotalSMZ3/Regions/Zelda/TurtleRock.py | 8 +- worlds/smz3/TotalSMZ3/Text/Dialog.py | 103 +++-- .../smz3/TotalSMZ3/Text/Scripts/General.yaml | 73 ++- .../TotalSMZ3/Text/Scripts/StringTable.yaml | 29 +- worlds/smz3/TotalSMZ3/Text/StringTable.py | 16 +- worlds/smz3/TotalSMZ3/Text/Texts.py | 6 +- worlds/smz3/TotalSMZ3/World.py | 73 +-- worlds/smz3/TotalSMZ3/WorldState.py | 170 +++++++ worlds/smz3/__init__.py | 109 ++++- worlds/smz3/data/zsm.ips | Bin 1460427 -> 1470833 bytes 32 files changed, 907 insertions(+), 482 deletions(-) create mode 100644 worlds/smz3/TotalSMZ3/WorldState.py diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index ad59bc6d42..2bbddf7ab3 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Choice, Option +from Options import Choice, Option, Toggle, DefaultOnToggle, Range class SMLogic(Choice): """This option selects what kind of logic to use for item placement inside @@ -45,6 +45,22 @@ class MorphLocation(Choice): option_Original = 2 default = 0 + +class Goal(Choice): + """This option decides what goal is required to finish the randomizer. + Defeat Ganon and Mother Brain - Find the required crystals and boss tokens kill both bosses. + Fast Ganon and Defeat Mother Brain - The hole to ganon is open without having to defeat Agahnim in + Ganon's Tower and Ganon can be defeat as soon you have the required + crystals to make Ganon vulnerable. For keysanity, this mode also removes + the Crateria Boss Key requirement from Tourian to allow faster access. + All Dungeons and Defeat Mother Brain - Similar to "Defeat Ganon and Mother Brain", but also requires all dungeons + to be beaten including Castle Tower and Agahnim.""" + display_name = "Goal" + option_DefeatBoth = 0 + option_FastGanonDefeatMotherBrain = 1 + option_AllDungeonsDefeatMotherBrain = 2 + default = 0 + class KeyShuffle(Choice): """This option decides how dungeon items such as keys are shuffled. None - A Link to the Past dungeon items can only be placed inside the @@ -55,9 +71,75 @@ class KeyShuffle(Choice): option_Keysanity = 1 default = 0 +class OpenTower(Range): + """The amount of crystals required to be able to enter Ganon's Tower. + If this is set to Random, the amount can be found in-game on a sign next to Ganon's Tower.""" + display_name = "Open Tower" + range_start = 0 + range_end = 7 + default = 7 + +class GanonVulnerable(Range): + """The amount of crystals required to be able to harm Ganon. The amount can be found + in-game on a sign near the top of the Pyramid.""" + display_name = "Ganon Vulnerable" + range_start = 0 + range_end = 7 + default = 7 + +class OpenTourian(Range): + """The amount of boss tokens required to enter Tourian. The amount can be found in-game + on a sign above the door leading to the Tourian entrance.""" + display_name = "Open Tourian" + range_start = 0 + range_end = 4 + default = 4 + +class SpinJumpsAnimation(Toggle): + """Enable separate space/screw jump animations""" + display_name = "Spin Jumps Animation" + +class HeartBeepSpeed(Choice): + """Sets the speed of the heart beep sound in A Link to the Past.""" + display_name = "Heart Beep Speed" + option_Off = 0 + option_Quarter = 1 + option_Half = 2 + option_Normal = 3 + option_Double = 4 + alias_false = 0 + default = 3 + +class HeartColor(Choice): + """Changes the color of the hearts in the HUD for A Link to the Past.""" + display_name = "Heart Color" + option_Red = 0 + option_Green = 1 + option_Blue = 2 + option_Yellow = 3 + default = 0 + +class QuickSwap(Toggle): + """When enabled, lets you switch items in ALTTP with L/R""" + display_name = "Quick Swap" + +class EnergyBeep(DefaultOnToggle): + """Toggles the low health energy beep in Super Metroid.""" + display_name = "Energy Beep" + + smz3_options: typing.Dict[str, type(Option)] = { "sm_logic": SMLogic, "sword_location": SwordLocation, "morph_location": MorphLocation, - "key_shuffle": KeyShuffle + "goal": Goal, + "key_shuffle": KeyShuffle, + "open_tower": OpenTower, + "ganon_vulnerable": GanonVulnerable, + "open_tourian": OpenTourian, + "spin_jumps_animation": SpinJumpsAnimation, + "heart_beep_speed": HeartBeepSpeed, + "heart_color": HeartColor, + "quick_swap": QuickSwap, + "energy_beep": EnergyBeep } diff --git a/worlds/smz3/TotalSMZ3/Config.py b/worlds/smz3/TotalSMZ3/Config.py index bfcd541b98..23dde1c88e 100644 --- a/worlds/smz3/TotalSMZ3/Config.py +++ b/worlds/smz3/TotalSMZ3/Config.py @@ -26,16 +26,42 @@ class MorphLocation(Enum): class Goal(Enum): DefeatBoth = 0 + FastGanonDefeatMotherBrain = 1 + AllDungeonsDefeatMotherBrain = 2 class KeyShuffle(Enum): Null = 0 Keysanity = 1 -class GanonInvincible(Enum): - Never = 0 - BeforeCrystals = 1 - BeforeAllDungeons = 2 - Always = 3 +class OpenTower(Enum): + Random = -1 + NoCrystals = 0 + OneCrystal = 1 + TwoCrystals = 2 + ThreeCrystals = 3 + FourCrystals = 4 + FiveCrystals = 5 + SixCrystals = 6 + SevenCrystals = 7 + +class GanonVulnerable(Enum): + Random = -1 + NoCrystals = 0 + OneCrystal = 1 + TwoCrystals = 2 + ThreeCrystals = 3 + FourCrystals = 4 + FiveCrystals = 5 + SixCrystals = 6 + SevenCrystals = 7 + +class OpenTourian(Enum): + Random = -1 + NoBosses = 0 + OneBoss = 1 + TwoBosses = 2 + ThreeBosses = 3 + FourBosses = 4 class Config: GameMode: GameMode = GameMode.Multiworld @@ -45,63 +71,20 @@ class Config: MorphLocation: MorphLocation = MorphLocation.Randomized Goal: Goal = Goal.DefeatBoth KeyShuffle: KeyShuffle = KeyShuffle.Null - Keysanity: bool = KeyShuffle != KeyShuffle.Null Race: bool = False - GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals - def __init__(self, options: Dict[str, str]): - self.GameMode = self.ParseOption(options, GameMode.Multiworld) - self.Z3Logic = self.ParseOption(options, Z3Logic.Normal) - self.SMLogic = self.ParseOption(options, SMLogic.Normal) - self.SwordLocation = self.ParseOption(options, SwordLocation.Randomized) - self.MorphLocation = self.ParseOption(options, MorphLocation.Randomized) - self.Goal = self.ParseOption(options, Goal.DefeatBoth) - self.GanonInvincible = self.ParseOption(options, GanonInvincible.BeforeCrystals) - self.KeyShuffle = self.ParseOption(options, KeyShuffle.Null) - self.Keysanity = self.KeyShuffle != KeyShuffle.Null - self.Race = self.ParseOptionWith(options, "Race", False) + OpenTower: OpenTower = OpenTower.SevenCrystals + GanonVulnerable: GanonVulnerable = GanonVulnerable.SevenCrystals + OpenTourian: OpenTourian = OpenTourian.FourBosses - def ParseOption(self, options:Dict[str, str], defaultValue:Enum): - enumKey = defaultValue.__class__.__name__.lower() - if (enumKey in options): - return defaultValue.__class__[options[enumKey]] - return defaultValue + @property + def SingleWorld(self) -> bool: + return self.GameMode == GameMode.Normal + + @property + def Multiworld(self) -> bool: + return self.GameMode == GameMode.Multiworld - def ParseOptionWith(self, options:Dict[str, str], option:str, defaultValue:bool): - if (option.lower() in options): - return options[option.lower()] - return defaultValue - - """ public static RandomizerOption GetRandomizerOption(string description, string defaultOption = "") where T : Enum { - var enumType = typeof(T); - var values = Enum.GetValues(enumType).Cast(); - - return new RandomizerOption { - Key = enumType.Name.ToLower(), - Description = description, - Type = RandomizerOptionType.Dropdown, - Default = string.IsNullOrEmpty(defaultOption) ? GetDefaultValue().ToLString() : defaultOption, - Values = values.ToDictionary(k => k.ToLString(), v => v.GetDescription()) - }; - } - - public static RandomizerOption GetRandomizerOption(string name, string description, bool defaultOption = false) { - return new RandomizerOption { - Key = name.ToLower(), - Description = description, - Type = RandomizerOptionType.Checkbox, - Default = defaultOption.ToString().ToLower(), - Values = new Dictionary() - }; - } - - public static TEnum GetDefaultValue() where TEnum : Enum { - Type t = typeof(TEnum); - var attributes = (DefaultValueAttribute[])t.GetCustomAttributes(typeof(DefaultValueAttribute), false); - if ((attributes?.Length ?? 0) > 0) { - return (TEnum)attributes.First().Value; - } - else { - return default; - } - } """ + @property + def Keysanity(self) -> bool: + return self.KeyShuffle != KeyShuffle.Null \ No newline at end of file diff --git a/worlds/smz3/TotalSMZ3/Item.py b/worlds/smz3/TotalSMZ3/Item.py index bad16ce9d0..2aced8bfac 100644 --- a/worlds/smz3/TotalSMZ3/Item.py +++ b/worlds/smz3/TotalSMZ3/Item.py @@ -130,6 +130,11 @@ class ItemType(Enum): CardLowerNorfairL1 = 0xDE CardLowerNorfairBoss = 0xDF + SmMapBrinstar = 0xCA + SmMapWreckedShip = 0xCB + SmMapMaridia = 0xCC + SmMapLowerNorfair = 0xCD + Missile = 0xC2 Super = 0xC3 PowerBomb = 0xC4 @@ -174,6 +179,7 @@ class Item: map = re.compile("^Map") compass = re.compile("^Compass") keycard = re.compile("^Card") + smMap = re.compile("^SmMap") def IsDungeonItem(self): return self.dungeon.match(self.Type.name) def IsBigKey(self): return self.bigKey.match(self.Type.name) @@ -181,6 +187,7 @@ class Item: def IsMap(self): return self.map.match(self.Type.name) def IsCompass(self): return self.compass.match(self.Type.name) def IsKeycard(self): return self.keycard.match(self.Type.name) + def IsSmMap(self): return self.smMap.match(self.Type.name) def Is(self, type: ItemType, world): return self.Type == type and self.World == world @@ -313,7 +320,7 @@ class Item: Item.AddRange(itemPool, 4, Item(ItemType.BombUpgrade5)) Item.AddRange(itemPool, 2, Item(ItemType.OneRupee)) Item.AddRange(itemPool, 4, Item(ItemType.FiveRupees)) - Item.AddRange(itemPool, 25 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees)) + Item.AddRange(itemPool, 21 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees)) Item.AddRange(itemPool, 7, Item(ItemType.FiftyRupees)) Item.AddRange(itemPool, 5, Item(ItemType.ThreeHundredRupees)) @@ -421,6 +428,21 @@ class Item: return itemPool + @staticmethod + def CreateSmMaps(world): + itemPool = [ + Item(ItemType.SmMapBrinstar, world), + Item(ItemType.SmMapWreckedShip, world), + Item(ItemType.SmMapMaridia, world), + Item(ItemType.SmMapLowerNorfair, world) + ] + + for item in itemPool: + item.Progression = True + item.World = world + + return itemPool + @staticmethod def Get(items, itemType:ItemType): item = next((i for i in items if i.Type == itemType), None) @@ -725,7 +747,7 @@ class Progression: def CanAccessMiseryMirePortal(self, config: Config): if (config.SMLogic == SMLogic.Normal): - return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and (self.Gravity and self.SpaceJump) and self.CanUsePowerBombs() + return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and self.Gravity and self.SpaceJump and self.CanUsePowerBombs() else: return (self.CardNorfairL2 or self.SpeedBooster) and self.Varia and self.Super and \ (self.CanFly() or self.HiJump or self.SpeedBooster or self.CanSpringBallJump() or self.Ice) \ @@ -769,11 +791,11 @@ class Progression: if (world.Config.SMLogic == SMLogic.Normal): return self.MoonPearl and self.Flippers and \ self.Gravity and self.Morph and \ - (world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) + (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) else: return self.MoonPearl and self.Flippers and \ (self.CanSpringBallJump() or self.HiJump or self.Gravity) and self.Morph and \ - (world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) + (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) # Start of AP integration items_start_id = 84000 diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index d029e58473..2b8d278d49 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -6,7 +6,7 @@ import typing from BaseClasses import Location from worlds.smz3.TotalSMZ3.Item import Item, ItemType from worlds.smz3.TotalSMZ3.Location import LocationType -from worlds.smz3.TotalSMZ3.Region import IMedallionAccess, IReward, RewardType, SMRegion, Z3Region +from worlds.smz3.TotalSMZ3.Region import IReward, RewardType, SMRegion, Z3Region from worlds.smz3.TotalSMZ3.Regions.Zelda.EasternPalace import EasternPalace from worlds.smz3.TotalSMZ3.Regions.Zelda.DesertPalace import DesertPalace from worlds.smz3.TotalSMZ3.Regions.Zelda.TowerOfHera import TowerOfHera @@ -18,10 +18,14 @@ from worlds.smz3.TotalSMZ3.Regions.Zelda.IcePalace import IcePalace from worlds.smz3.TotalSMZ3.Regions.Zelda.MiseryMire import MiseryMire from worlds.smz3.TotalSMZ3.Regions.Zelda.TurtleRock import TurtleRock from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Kraid import Kraid +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.WreckedShip import WreckedShip +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Maridia.Inner import Inner +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairLower.East import East from worlds.smz3.TotalSMZ3.Text.StringTable import StringTable from worlds.smz3.TotalSMZ3.World import World -from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible +from worlds.smz3.TotalSMZ3.Config import Config, OpenTourian, Goal from worlds.smz3.TotalSMZ3.Text.Texts import Texts from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog @@ -30,6 +34,11 @@ class KeycardPlaque: Level2 = 0xe1 Boss = 0xe2 Null = 0x00 + Zero = 0xe3 + One = 0xe4 + Two = 0xe5 + Three = 0xe6 + Four = 0xe7 class KeycardDoors: Left = 0xd414 @@ -73,8 +82,8 @@ class DropPrize(Enum): Fairy = 0xE3 class Patch: - Major = 0 - Minor = 1 + Major = 11 + Minor = 3 allWorlds: List[World] myWorld: World seedGuid: str @@ -105,13 +114,16 @@ class Patch: self.WriteDiggingGameRng() - self.WritePrizeShuffle() + self.WritePrizeShuffle(self.myWorld.WorldState.DropPrizes) self.WriteRemoveEquipmentFromUncle( self.myWorld.GetLocation("Link's Uncle").APLocation.item.item if self.myWorld.GetLocation("Link's Uncle").APLocation.item.game == "SMZ3" else Item(ItemType.Something)) - self.WriteGanonInvicible(config.GanonInvincible) + self.WriteGanonInvicible(config.Goal) + self.WritePreOpenPyramid(config.Goal) + self.WriteCrystalsNeeded(self.myWorld.TowerCrystals, self.myWorld.GanonCrystals) + self.WriteBossesNeeded(self.myWorld.TourianBossTokens) self.WriteRngBlock() self.WriteSaveAndQuitFromBossRoom() @@ -135,26 +147,27 @@ class Patch: return {patch[0]:patch[1] for patch in self.patches} def WriteMedallions(self): + from worlds.smz3.TotalSMZ3.WorldState import Medallion turtleRock = next(region for region in self.myWorld.Regions if isinstance(region, TurtleRock)) miseryMire = next(region for region in self.myWorld.Regions if isinstance(region, MiseryMire)) turtleRockAddresses = [0x308023, 0xD020, 0xD0FF, 0xD1DE ] miseryMireAddresses = [ 0x308022, 0xCFF2, 0xD0D1, 0xD1B0 ] - if turtleRock.Medallion == ItemType.Bombos: + if turtleRock.Medallion == Medallion.Bombos: turtleRockValues = [0x00, 0x51, 0x10, 0x00] - elif turtleRock.Medallion == ItemType.Ether: + elif turtleRock.Medallion == Medallion.Ether: turtleRockValues = [0x01, 0x51, 0x18, 0x00] - elif turtleRock.Medallion == ItemType.Quake: + elif turtleRock.Medallion == Medallion.Quake: turtleRockValues = [0x02, 0x14, 0xEF, 0xC4] else: raise exception(f"Tried using {turtleRock.Medallion} in place of Turtle Rock medallion") - if miseryMire.Medallion == ItemType.Bombos: + if miseryMire.Medallion == Medallion.Bombos: miseryMireValues = [0x00, 0x51, 0x00, 0x00] - elif miseryMire.Medallion == ItemType.Ether: + elif miseryMire.Medallion == Medallion.Ether: miseryMireValues = [0x01, 0x13, 0x9F, 0xF1] - elif miseryMire.Medallion == ItemType.Quake: + elif miseryMire.Medallion == Medallion.Quake: miseryMireValues = [0x02, 0x51, 0x08, 0x00] else: raise exception(f"Tried using {miseryMire.Medallion} in place of Misery Mire medallion") @@ -174,12 +187,19 @@ class Patch: self.rnd.shuffle(pendantsBlueRed) pendantRewards = pendantsGreen + pendantsBlueRed + bossTokens = [ 1, 2, 3, 4 ] + regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)] crystalRegions = [region for region in regions if region.Reward == RewardType.CrystalBlue] + [region for region in regions if region.Reward == RewardType.CrystalRed] pendantRegions = [region for region in regions if region.Reward == RewardType.PendantGreen] + [region for region in regions if region.Reward == RewardType.PendantNonGreen] + bossRegions = [region for region in regions if region.Reward == RewardType.BossTokenKraid] + \ + [region for region in regions if region.Reward == RewardType.BossTokenPhantoon] + \ + [region for region in regions if region.Reward == RewardType.BossTokenDraygon] + \ + [region for region in regions if region.Reward == RewardType.BossTokenRidley] self.patches += self.RewardPatches(crystalRegions, crystalRewards, self.CrystalValues) self.patches += self.RewardPatches(pendantRegions, pendantRewards, self.PendantValues) + self.patches += self.RewardPatches(bossRegions, bossTokens, self.BossTokenValues) def RewardPatches(self, regions: List[IReward], rewards: List[int], rewardValues: Callable): addresses = [self.RewardAddresses(region) for region in regions] @@ -189,17 +209,22 @@ class Patch: def RewardAddresses(self, region: IReward): regionType = { - EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE ], - DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF ], - TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706 ], - PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702 ], - SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701 ], - SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704 ], - ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707 ], - IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705 ], - MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703 ], - TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708 ] + EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE, 0x30D100], + DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF, 0x30D101 ], + TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706, 0x30D102 ], + PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702, 0x30D103 ], + SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701, 0x30D104 ], + SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704, 0x30D105 ], + ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707, 0x30D106 ], + IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705, 0x30D107 ], + MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703, 0x30D108 ], + TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708, 0x30D109 ], + Kraid : [ 0xF26002, 0xF26004, 0xF26005, 0xF26000, 0xF26006, 0xF26007, 0x82FD36 ], + WreckedShip : [ 0xF2600A, 0xF2600C, 0xF2600D, 0xF26008, 0xF2600E, 0xF2600F, 0x82FE26 ], + Inner : [ 0xF26012, 0xF26014, 0xF26015, 0xF26010, 0xF26016, 0xF26017, 0x82FE76 ], + East : [ 0xF2601A, 0xF2601C, 0xF2601D, 0xF26018, 0xF2601E, 0xF2601F, 0x82FDD6 ] } + result = regionType.get(type(region), None) if result is None: raise exception(f"Region {region} should not be a dungeon reward region") @@ -208,13 +233,13 @@ class Patch: def CrystalValues(self, crystal: int): crystalMap = { - 1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06 ], - 2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06 ], - 3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06 ], - 4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06 ], - 5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06 ], - 6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06 ], - 7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06 ], + 1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06, 0x10 ], + 2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x10 ], + 3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x10 ], + 4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06, 0x10 ], + 5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x11 ], + 6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x11 ], + 7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x10 ], } result = crystalMap.get(crystal, None) if result is None: @@ -224,15 +249,28 @@ class Patch: def PendantValues(self, pendant: int): pendantMap = { - 1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01 ], - 2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03 ], - 3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02 ], + 1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01, 0x12 ], + 2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x14 ], + 3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x13 ] } result = pendantMap.get(pendant, None) if result is None: raise exception(f"Tried using {pendant} as a pendant number") else: return result + + def BossTokenValues(self, token: int): + tokenMap = { + 1 : [ 0x01, 0x38, 0x40, 0x80, 0x69, 0x80, 0x15 ], + 2 : [ 0x02, 0x34, 0x42, 0x80, 0x69, 0x81, 0x16 ], + 3 : [ 0x04, 0x34, 0x44, 0x80, 0x69, 0x82, 0x17 ], + 4 : [ 0x08, 0x32, 0x46, 0x80, 0x69, 0x83, 0x18 ] + } + result = tokenMap.get(token, None) + if result is None: + raise exception(f"Tried using {token} as a boss token number") + else: + return result def WriteSMLocations(self, locations: List[Location]): def GetSMItemPLM(location:Location): @@ -259,7 +297,7 @@ class Patch: ItemType.SpaceJump : 0xEF1B, ItemType.ScrewAttack : 0xEF1F } - plmId = 0xEFE0 if self.myWorld.Config.GameMode == GameMode.Multiworld else \ + plmId = 0xEFE0 if self.myWorld.Config.Multiworld else \ itemMap.get(location.APLocation.item.item.Type, 0xEFE0) if (plmId == 0xEFE0): plmId += 4 if location.Type == LocationType.Chozo else 8 if location.Type == LocationType.Hidden else 0 @@ -268,7 +306,7 @@ class Patch: return plmId for location in locations: - if (self.myWorld.Config.GameMode == GameMode.Multiworld): + if (self.myWorld.Config.Multiworld): self.patches.append((Snes(location.Address), getWordArray(GetSMItemPLM(location)))) self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location))) else: @@ -283,18 +321,14 @@ class Patch: self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB])) elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]): text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something)) - dialog = Dialog.Simple(text) if (location.Type == LocationType.Pedestal): self.stringTable.SetPedestalText(text) - self.patches.append((Snes(0x308300), dialog)) elif (location.Type == LocationType.Ether): self.stringTable.SetEtherText(text) - self.patches.append((Snes(0x308F00), dialog)) elif (location.Type == LocationType.Bombos): self.stringTable.SetBombosText(text) - self.patches.append((Snes(0x309000), dialog)) - if (self.myWorld.Config.GameMode == GameMode.Multiworld): + if (self.myWorld.Config.Multiworld): self.patches.append((Snes(location.Address), [(location.Id - 256)])) self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location))) else: @@ -305,11 +339,11 @@ class Patch: item = location.APLocation.item.item itemDungeon = None if item.IsKey(): - itemDungeon = ItemType.Key if (not item.World.Config.Keysanity or item.Type != ItemType.KeyHC) else ItemType.KeyHC + itemDungeon = ItemType.Key elif item.IsBigKey(): itemDungeon = ItemType.BigKey elif item.IsMap(): - itemDungeon = ItemType.Map if (not item.World.Config.Keysanity or item.Type != ItemType.MapHC) else ItemType.MapHC + itemDungeon = ItemType.Map elif item.IsCompass(): itemDungeon = ItemType.Compass @@ -327,15 +361,11 @@ class Patch: def WriteDungeonMusic(self, keysanity: bool): if (not keysanity): - regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)] - music = [] + regions = [region for region in self.myWorld.Regions if isinstance(region, Z3Region) and isinstance(region, IReward) and + region.Reward != None and region.Reward != RewardType.Agahnim] pendantRegions = [region for region in regions if region.Reward in [RewardType.PendantGreen, RewardType.PendantNonGreen]] crystalRegions = [region for region in regions if region.Reward in [RewardType.CrystalBlue, RewardType.CrystalRed]] - regions = pendantRegions + crystalRegions - music = [ - 0x11, 0x11, 0x11, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, - ] + music = [0x11 if (region.Reward == RewardType.PendantGreen or region.Reward == RewardType.PendantNonGreen) else 0x16 for region in regions] self.patches += self.MusicPatches(regions, music) #IEnumerable RandomDungeonMusic() { @@ -366,51 +396,13 @@ class Patch: else: return result - def WritePrizeShuffle(self): - prizePackItems = 56 - treePullItems = 3 - - bytes = [] - drop = 0 - final = 0 - - pool = [ - DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, #// pack 1 - DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Red, DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Blue, #// pack 2 - DropPrize.FullMagic, DropPrize.Magic, DropPrize.Magic, DropPrize.Blue, DropPrize.FullMagic, DropPrize.Magic, DropPrize.Heart, DropPrize.Magic, #// pack 3 - DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb4, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb8, DropPrize.Bomb1, #// pack 4 - DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10, DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10,#// pack 5 - DropPrize.Magic, DropPrize.Green, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Magic, DropPrize.Bomb1, DropPrize.Green, DropPrize.Heart, #// pack 6 - DropPrize.Heart, DropPrize.Fairy, DropPrize.FullMagic, DropPrize.Red, DropPrize.Bomb8, DropPrize.Heart, DropPrize.Red, DropPrize.Arrow10, #// pack 7 - DropPrize.Green, DropPrize.Blue, DropPrize.Red,#// from pull trees - DropPrize.Green, DropPrize.Red,#// from prize crab - DropPrize.Green, #// stunned prize - DropPrize.Red,#// saved fish prize - ] - - prizes = pool - self.rnd.shuffle(prizes) - - #/* prize pack drop order */ - (bytes, prizes) = SplitOff(prizes, prizePackItems) - self.patches.append((Snes(0x6FA78), [byte.value for byte in bytes])) - - #/* tree pull prizes */ - (bytes, prizes) = SplitOff(prizes, treePullItems) - self.patches.append((Snes(0x1DFBD4), [byte.value for byte in bytes])) - - #/* crab prizes */ - (drop, final, prizes) = (prizes[0], prizes[1], prizes[2:]) - self.patches.append((Snes(0x6A9C8), [ drop.value ])) - self.patches.append((Snes(0x6A9C4), [ final.value ])) - - #/* stun prize */ - (drop, prizes) = (prizes[0], prizes[1:]) - self.patches.append((Snes(0x6F993), [ drop.value ])) - - #/* fish prize */ - drop = prizes[0] - self.patches.append((Snes(0x1D82CC), [ drop.value ])) + def WritePrizeShuffle(self, dropPrizes): + self.patches.append((Snes(0x6FA78), [e.value for e in dropPrizes.Packs])) + self.patches.append((Snes(0x1DFBD4), [e.value for e in dropPrizes.TreePulls])) + self.patches.append((Snes(0x6A9C8), [dropPrizes.CrabContinous.value])) + self.patches.append((Snes(0x6A9C4), [dropPrizes.CrabFinal.value])) + self.patches.append((Snes(0x6F993), [dropPrizes.Stun.value])) + self.patches.append((Snes(0x1D82CC), [dropPrizes.Fish.value])) self.patches += self.EnemyPrizePackDistribution() @@ -524,46 +516,29 @@ class Patch: redCrystalDungeons = [region for region in regions if region.Reward == RewardType.CrystalRed] sahasrahla = Texts.SahasrahlaReveal(greenPendantDungeon) - self.patches.append((Snes(0x308A00), Dialog.Simple(sahasrahla))) self.stringTable.SetSahasrahlaRevealText(sahasrahla) bombShop = Texts.BombShopReveal(redCrystalDungeons) - self.patches.append((Snes(0x308E00), Dialog.Simple(bombShop))) self.stringTable.SetBombShopRevealText(bombShop) blind = Texts.Blind(self.rnd) - self.patches.append((Snes(0x308800), Dialog.Simple(blind))) self.stringTable.SetBlindText(blind) tavernMan = Texts.TavernMan(self.rnd) - self.patches.append((Snes(0x308C00), Dialog.Simple(tavernMan))) self.stringTable.SetTavernManText(tavernMan) ganon = Texts.GanonFirstPhase(self.rnd) - self.patches.append((Snes(0x308600), Dialog.Simple(ganon))) self.stringTable.SetGanonFirstPhaseText(ganon) - #// Todo: Verify these two are correct if ganon invincible patch is ever added - #// ganon_fall_in_alt in v30 - ganonFirstPhaseInvincible = "You think you\nare ready to\nface me?\n\nI will not die\n\nunless you\ncomplete your\ngoals. Dingus!" - self.patches.append((Snes(0x309100), Dialog.Simple(ganonFirstPhaseInvincible))) - - #// ganon_phase_3_alt in v30 - ganonThirdPhaseInvincible = "Got wax in\nyour ears?\nI cannot die!" - self.patches.append((Snes(0x309200), Dialog.Simple(ganonThirdPhaseInvincible))) - #// --- - silversLocation = [loc for world in self.allWorlds for loc in world.Locations if loc.ItemIs(ItemType.SilverArrows, self.myWorld)] if len(silversLocation) == 0: silvers = Texts.GanonThirdPhaseMulti(None, self.myWorld, self.silversWorldID, self.playerIDToNames[self.silversWorldID]) else: - silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.GameMode == GameMode.Multiworld else \ + silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.Multiworld else \ Texts.GanonThirdPhaseSingle(silversLocation[0].Region) - self.patches.append((Snes(0x308700), Dialog.Simple(silvers))) self.stringTable.SetGanonThirdPhaseText(silvers) triforceRoom = Texts.TriforceRoom(self.rnd) - self.patches.append((Snes(0x308400), Dialog.Simple(triforceRoom))) self.stringTable.SetTriforceRoomText(triforceRoom) def WriteStringTable(self): @@ -579,26 +554,32 @@ class Patch: return bytearray(name, 'utf8') def WriteSeedData(self): - configField = \ + configField1 = \ ((1 if self.myWorld.Config.Race else 0) << 15) | \ ((1 if self.myWorld.Config.Keysanity else 0) << 13) | \ - ((1 if self.myWorld.Config.GameMode == Config.GameMode.Multiworld else 0) << 12) | \ + ((1 if self.myWorld.Config.Multiworld else 0) << 12) | \ (self.myWorld.Config.Z3Logic.value << 10) | \ (self.myWorld.Config.SMLogic.value << 8) | \ (Patch.Major << 4) | \ (Patch.Minor << 0) + configField2 = \ + ((1 if self.myWorld.Config.SwordLocation else 0) << 14) | \ + ((1 if self.myWorld.Config.MorphLocation else 0) << 12) | \ + ((1 if self.myWorld.Config.Goal else 0) << 8) + self.patches.append((Snes(0x80FF50), getWordArray(self.myWorld.Id))) - self.patches.append((Snes(0x80FF52), getWordArray(configField))) + self.patches.append((Snes(0x80FF52), getWordArray(configField1))) self.patches.append((Snes(0x80FF54), getDoubleWordArray(self.seed))) + self.patches.append((Snes(0x80FF58), getWordArray(configField2))) #/* Reserve the rest of the space for future use */ - self.patches.append((Snes(0x80FF58), [0x00] * 8)) + self.patches.append((Snes(0x80FF5A), [0x00] * 6)) self.patches.append((Snes(0x80FF60), bytearray(self.seedGuid, 'utf8'))) self.patches.append((Snes(0x80FF80), bytearray(self.myWorld.Guid, 'utf8'))) def WriteCommonFlags(self): #/* Common Combo Configuration flags at [asm]/config.asm */ - if (self.myWorld.Config.GameMode == GameMode.Multiworld): + if (self.myWorld.Config.Multiworld): self.patches.append((Snes(0xF47000), getWordArray(0x0001))) if (self.myWorld.Config.Keysanity): self.patches.append((Snes(0xF47006), getWordArray(0x0001))) @@ -619,97 +600,104 @@ class Patch: if (self.myWorld.Config.Keysanity): self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass - self.patches.append((Snes(0x40016A), [ 0x01 ])) #// enable local item dialog boxes for dungeon and keycard items + self.patches.append((Snes(0x40016A), [ 0x01 ])) #// FreeItemText: db #$01 ; #00 = Off (default) - #$01 = On def WriteSMKeyCardDoors(self): - if (not self.myWorld.Config.Keysanity): - return - - plaquePLm = 0xd410 - - doorList = [ - #// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created) - #// Crateria - [ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet - [ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB - [ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door) - [ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4 - [ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo - - #// Brinstar - [ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room - - [ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area - [ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn - [ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank - [ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super - - [ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid - - #// Upper Norfair - [ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice - [ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice - - [ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain - [ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain - [ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain - [ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain - - [ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire - - #// Lower Norfair - [ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre - [ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry" - [ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley - - #// Maridia - [ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia - [ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach - - [ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon - [ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon - - [ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon - - [ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking) - - #// Wrecked Ship - [ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check - [ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley - [ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley - - [ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon - ] - - doorId = 0x0000 + plaquePlm = 0xd410 plmTablePos = 0xf800 - for door in doorList: - doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3] - if (door[6] == 0): - #// Write dynamic door - doorData = [] - for x in door[0:3]: - doorData += getWordArray(x) - doorData += getWordArray(doorArgs) - self.patches.append((Snes(0x8f0000 + plmTablePos), doorData)) - plmTablePos += 0x08 - else: - #// Overwrite existing door - doorData = [] - for x in door[1:3]: - doorData += getWordArray(x) - doorData += getWordArray(doorArgs) - self.patches.append((Snes(0x8f0000 + door[6]), doorData)) - if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss): - #// Overwrite the extra parts of the Gadora with a PLM that just deletes itself - self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ])) - #// Plaque data - if (door[4] != KeycardPlaque.Null): - plaqueData = getWordArray(door[0]) + getWordArray(plaquePLm) + getWordArray(door[5]) + getWordArray(door[4]) - self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) - plmTablePos += 0x08 - doorId += 1 + if ( self.myWorld.Config.Keysanity): + doorList = [ + #// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created) + #// Crateria + [ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet + [ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB + [ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door) + [ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4 + [ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo + + #// Brinstar + [ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room + + [ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area + [ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn + [ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank + [ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super + + [ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid + + #// Upper Norfair + [ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice + [ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice + + [ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain + [ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain + [ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain + [ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain + + [ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire + + #// Lower Norfair + [ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre + [ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry" + [ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley + + #// Maridia + [ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia + [ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach + + [ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon + [ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon + + [ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon + + [ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking) + + #// Wrecked Ship + [ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check + [ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley + [ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley + + [ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon + ] + + doorId = 0x0000 + for door in doorList: + #/* When "Fast Ganon" is set, don't place the G4 Boss key door to enable faster games */ + if (door[0] == 0x99BD and self.myWorld.Config.Goal == Goal.FastGanonDefeatMotherBrain): + continue + doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3] + if (door[6] == 0): + #// Write dynamic door + doorData = [] + for x in door[0:3]: + doorData += getWordArray(x) + doorData += getWordArray(doorArgs) + self.patches.append((Snes(0x8f0000 + plmTablePos), doorData)) + plmTablePos += 0x08 + else: + #// Overwrite existing door + doorData = [] + for x in door[1:3]: + doorData += getWordArray(x) + doorData += getWordArray(doorArgs) + self.patches.append((Snes(0x8f0000 + door[6]), doorData)) + if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss): + #// Overwrite the extra parts of the Gadora with a PLM that just deletes itself + self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ])) + + #// Plaque data + if (door[4] != KeycardPlaque.Null): + plaqueData = getWordArray(door[0]) + getWordArray(plaquePlm) + getWordArray(door[5]) + getWordArray(door[4]) + self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) + plmTablePos += 0x08 + doorId += 1 + + #/* Write plaque showing SM bosses that needs to be killed */ + if (self.myWorld.Config.OpenTourian != OpenTourian.FourBosses): + plaqueData = getWordArray(0xA5ED) + getWordArray(plaquePlm) + getWordArray(0x044F) + getWordArray(KeycardPlaque.Zero + self.myWorld.TourianBossTokens) + self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) + plmTablePos += 0x08 self.patches.append((Snes(0x8f0000 + plmTablePos), [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ])) @@ -745,20 +733,32 @@ class Patch: (Snes(0xDD313), [ 0x00, 0x00, 0xE4, 0xFF, 0x08, 0x0E ]), ] - def WriteGanonInvicible(self, invincible: GanonInvincible): + def WritePreOpenPyramid(self, goal: Goal): + if (goal == Goal.FastGanonDefeatMotherBrain): + self.patches.append((Snes(0x30808B), [0x01])) + + def WriteGanonInvicible(self, goal: Goal): #/* Defaults to $00 (never) at [asm]/z3/randomizer/tables.asm */ - invincibleMap = { - GanonInvincible.Never : 0x00, - GanonInvincible.Always : 0x01, - GanonInvincible.BeforeAllDungeons : 0x02, - GanonInvincible.BeforeCrystals : 0x03, - } - value = invincibleMap.get(invincible, None) + valueMap = { + Goal.DefeatBoth : 0x03, + Goal.FastGanonDefeatMotherBrain : 0x04, + Goal.AllDungeonsDefeatMotherBrain : 0x02 + } + value = valueMap.get(goal, None) if (value is None): - raise exception(f"Unknown Ganon invincible value {invincible}") + raise exception(f"Unknown Ganon invincible value {goal}") else: self.patches.append((Snes(0x30803E), [value])) + def WriteBossesNeeded(self, tourianBossTokens): + self.patches.append((Snes(0xF47200), getWordArray(tourianBossTokens))) + + def WriteCrystalsNeeded(self, towerCrystals, ganonCrystals): + self.patches.append((Snes(0x30805E), [towerCrystals])) + self.patches.append((Snes(0x30805F), [ganonCrystals])) + + self.stringTable.SetTowerRequirementText(f"You need {towerCrystals} crystals to enter Ganon's Tower.") + self.stringTable.SetGanonRequirementText(f"You need {ganonCrystals} crystals to defeat Ganon.") def WriteRngBlock(self): #/* Repoint RNG Block */ diff --git a/worlds/smz3/TotalSMZ3/Region.py b/worlds/smz3/TotalSMZ3/Region.py index f352247c80..00e209ce45 100644 --- a/worlds/smz3/TotalSMZ3/Region.py +++ b/worlds/smz3/TotalSMZ3/Region.py @@ -5,12 +5,19 @@ from worlds.smz3.TotalSMZ3.Item import Item, ItemType class RewardType(Enum): Null = 0 - Agahnim = 1 - PendantGreen = 2 - PendantNonGreen = 3 - CrystalBlue = 4 - CrystalRed = 5 - GoldenFourBoss = 6 + Agahnim = 1 << 0 + PendantGreen = 1 << 1 + PendantNonGreen = 1 << 2 + CrystalBlue = 1 << 3 + CrystalRed = 1 << 4 + BossTokenKraid = 1 << 5 + BossTokenPhantoon = 1 << 6 + BossTokenDraygon = 1 << 7 + BossTokenRidley = 1 << 8 + + AnyPendant = PendantGreen | PendantNonGreen + AnyCrystal = CrystalBlue | CrystalRed + AnyBossToken = BossTokenKraid | BossTokenPhantoon | BossTokenDraygon | BossTokenRidley class IReward: Reward: RewardType @@ -18,7 +25,7 @@ class IReward: pass class IMedallionAccess: - Medallion: object + Medallion = None class Region: import worlds.smz3.TotalSMZ3.Location as Location diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py index fe3f804da9..2b99081dd3 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py @@ -7,7 +7,7 @@ class Kraid(SMRegion, IReward): Name = "Brinstar Kraid" Area = "Brinstar" - Reward = RewardType.GoldenFourBoss + Reward = RewardType.Null def __init__(self, world, config: Config): super().__init__(world, config) diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py index 465f885b11..bb1036fb81 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py @@ -40,5 +40,5 @@ class Pink(SMRegion): else: return items.CanOpenRedDoors() and (items.CanDestroyBombWalls() or items.SpeedBooster) or \ items.CanUsePowerBombs() or \ - items.CanAccessNorfairUpperPortal() and items.Morph and (items.CanOpenRedDoors() or items.Wave) and \ + items.CanAccessNorfairUpperPortal() and items.Morph and (items.Missile or items.Super or items.Wave ) and \ (items.Ice or items.HiJump or items.CanSpringBallJump() or items.CanFly()) diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py index d223fd82a2..72d10a4496 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py @@ -17,9 +17,9 @@ class East(SMRegion): self.world.CanEnter("Wrecked Ship", items)) if self.Logic == SMLogic.Normal else \ lambda items: items.Morph), Location(self, 2, 0x8F81EE, LocationType.Hidden, "Missile (outside Wrecked Ship top)", - lambda items: self.world.CanEnter("Wrecked Ship", items) and (not self.Config.Keysanity or items.CardWreckedShipBoss) and items.CanPassBombPassages()), + lambda items: self.world.CanEnter("Wrecked Ship", items) and items.CardWreckedShipBoss and items.CanPassBombPassages()), Location(self, 3, 0x8F81F4, LocationType.Visible, "Missile (outside Wrecked Ship middle)", - lambda items: self.world.CanEnter("Wrecked Ship", items) and (not self.Config.Keysanity or items.CardWreckedShipBoss) and items.CanPassBombPassages()), + lambda items: self.world.CanEnter("Wrecked Ship", items) and items.CardWreckedShipBoss and items.CanPassBombPassages()), Location(self, 4, 0x8F8248, LocationType.Visible, "Missile (Crateria moat)", lambda items: True) ] diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py index 280f7e5b28..7de0798bae 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py @@ -9,20 +9,17 @@ class Inner(SMRegion, IReward): def __init__(self, world, config: Config): super().__init__(world, config) - self.Reward = RewardType.GoldenFourBoss + self.Reward = RewardType.Null self.Locations = [ Location(self, 140, 0x8FC4AF, LocationType.Visible, "Super Missile (yellow Maridia)", - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and - (items.Gravity or items.Ice or items.HiJump and items.CanSpringBallJump())), + lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and self.CanReachAqueduct(items) and + (items.Gravity or items.Ice or items.HiJump and items.SpringBall)), Location(self, 141, 0x8FC4B5, LocationType.Visible, "Missile (yellow Maridia super missile)", - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and - (items.Gravity or items.Ice or items.HiJump and items.CanSpringBallJump())), + (items.Gravity or items.Ice or items.HiJump and items.SpringBall)), Location(self, 142, 0x8FC533, LocationType.Visible, "Missile (yellow Maridia false wall)", - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and - (items.Gravity or items.Ice or items.HiJump and items.CanSpringBallJump())), + (items.Gravity or items.Ice or items.HiJump and items.SpringBall)), Location(self, 143, 0x8FC559, LocationType.Chozo, "Plasma Beam", lambda items: self.CanDefeatDraygon(items) and (items.ScrewAttack or items.Plasma) and (items.HiJump or items.CanFly()) if self.Logic == SMLogic.Normal else \ lambda items: self.CanDefeatDraygon(items) and @@ -31,17 +28,17 @@ class Inner(SMRegion, IReward): Location(self, 144, 0x8FC5DD, LocationType.Visible, "Missile (left Maridia sand pit room)", lambda items: self.CanReachAqueduct(items) and items.Super and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Super and - (items.HiJump and (items.SpaceJump or items.CanSpringBallJump()) or items.Gravity)), + (items.Gravity or items.HiJump and (items.SpaceJump or items.CanSpringBallJump()))), Location(self, 145, 0x8FC5E3, LocationType.Chozo, "Reserve Tank, Maridia", lambda items: self.CanReachAqueduct(items) and items.Super and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Super and - (items.HiJump and (items.SpaceJump or items.CanSpringBallJump()) or items.Gravity)), + (items.Gravity or items.HiJump and (items.SpaceJump or items.CanSpringBallJump()))), Location(self, 146, 0x8FC5EB, LocationType.Visible, "Missile (right Maridia sand pit room)", lambda items: self.CanReachAqueduct(items) and items.Super) if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Super and (items.HiJump or items.Gravity), Location(self, 147, 0x8FC5F1, LocationType.Visible, "Power Bomb (right Maridia sand pit room)", lambda items: self.CanReachAqueduct(items) and items.Super) if self.Logic == SMLogic.Normal else \ - lambda items: self.CanReachAqueduct(items) and items.Super and (items.HiJump and items.CanSpringBallJump() or items.Gravity), + lambda items: self.CanReachAqueduct(items) and items.Super and (items.Gravity or items.HiJump and items.CanSpringBallJump()), Location(self, 148, 0x8FC603, LocationType.Visible, "Missile (pink Maridia)", lambda items: self.CanReachAqueduct(items) and items.SpeedBooster if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Gravity), diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py index 0cd577f78d..f1a325a12b 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py @@ -9,7 +9,7 @@ class East(SMRegion, IReward): def __init__(self, world, config: Config): super().__init__(world, config) - self.Reward = RewardType.GoldenFourBoss + self.Reward = RewardType.Null self.Locations = [ Location(self, 74, 0x8F8FCA, LocationType.Visible, "Missile (lower Norfair above fire flea room)", lambda items: self.CanExit(items)), @@ -30,11 +30,11 @@ class East(SMRegion, IReward): def CanExit(self, items:Progression): if self.Logic == SMLogic.Normal: # /*Bubble Mountain*/ - return items.CardNorfairL2 or ( + return items.Morph and (items.CardNorfairL2 or ( # /* Volcano Room and Blue Gate */ items.Gravity) and items.Wave and ( # /*Spikey Acid Snakes and Croc Escape*/ - items.Grapple or items.SpaceJump) + items.Grapple or items.SpaceJump)) else: # /*Vanilla LN Escape*/ return (items.Morph and (items.CardNorfairL2 or # /*Bubble Mountain*/ diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py index 8740c545e3..4e44d28ca5 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py @@ -17,13 +17,13 @@ class West(SMRegion): items.CanAccessNorfairLowerPortal() and (items.CanFly() or items.CanSpringBallJump() or items.SpeedBooster) and items.Super)), Location(self, 71, 0x8F8E74, LocationType.Hidden, "Super Missile (Gold Torizo)", lambda items: items.CanDestroyBombWalls() and (items.Super or items.Charge) and - (items.CanAccessNorfairLowerPortal() or items.SpaceJump and items.CanUsePowerBombs()) if self.Logic == SMLogic.Normal else \ + (items.CanAccessNorfairLowerPortal() or items.CanUsePowerBombs() and items.SpaceJump) if self.Logic == SMLogic.Normal else \ lambda items: items.CanDestroyBombWalls() and items.Varia and (items.Super or items.Charge)), Location(self, 79, 0x8F9110, LocationType.Chozo, "Screw Attack", - lambda items: items.CanDestroyBombWalls() and (items.SpaceJump and items.CanUsePowerBombs() or items.CanAccessNorfairLowerPortal()) if self.Logic == SMLogic.Normal else \ - lambda items: items.CanDestroyBombWalls() and (items.Varia or items.CanAccessNorfairLowerPortal())), + lambda items: items.CanDestroyBombWalls() and (items.CanAccessNorfairLowerPortal() or items.CanUsePowerBombs() and items.SpaceJump) if self.Logic == SMLogic.Normal else \ + lambda items: items.CanDestroyBombWalls() and (items.CanAccessNorfairLowerPortal() or items.Varia)), Location(self, 73, 0x8F8F30, LocationType.Visible, "Missile (Mickey Mouse room)", - lambda items: items.CanFly() and items.Morph and items.Super and ( + lambda items: items.Morph and items.Super and items.CanFly() and items.CanUsePowerBombs() and ( # /*Exit to Upper Norfair*/ (items.CardLowerNorfairL1 or # /*Vanilla or Reverse Lava Dive*/ @@ -33,17 +33,20 @@ class West(SMRegion): # /* Volcano Room and Blue Gate */ items.Gravity and items.Wave and # /*Spikey Acid Snakes and Croc Escape*/ - (items.Grapple or items.SpaceJump) or + (items.Grapple or items.SpaceJump) or # /*Exit via GT fight and Portal*/ - (items.CanUsePowerBombs() and items.SpaceJump and (items.Super or items.Charge))) if self.Logic == SMLogic.Normal else \ + items.CanUsePowerBombs() and items.SpaceJump and (items.Super or items.Charge)) if self.Logic == SMLogic.Normal else \ lambda items: - items.Morph and items.Varia and items.Super and ((items.CanFly() or items.CanSpringBallJump() or - items.SpeedBooster and (items.HiJump and items.CanUsePowerBombs() or items.Charge and items.Ice)) and - # /*Exit to Upper Norfair*/ - (items.CardNorfairL2 or (items.SpeedBooster or items.CanFly() or items.Grapple or items.HiJump and - (items.CanSpringBallJump() or items.Ice))) or - # /*Return to Portal*/ - items.CanUsePowerBombs())) + items.Varia and items.Morph and items.Super and + #/* Climb worst room (from LN East CanEnter) */ + (items.CanFly() or items.HiJump or items.CanSpringBallJump() or items.Ice and items.Charge) and + (items.CanPassBombPassages() or items.ScrewAttack and items.SpaceJump) and ( + #/* Exit to Upper Norfair */ + items.CardNorfairL2 or items.SpeedBooster or items.CanFly() or items.Grapple or + items.HiJump and (items.CanSpringBallJump() or items.Ice) or + #/* Portal (with GGG) */ + items.CanUsePowerBombs() + )) ] # // Todo: account for Croc Speedway once Norfair Upper East also do so, otherwise it would be inconsistent to do so here diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py index 914d07c3be..b38bbe70c6 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py @@ -45,11 +45,10 @@ class Crocomire(SMRegion): # /* Cathedral -> through the floor or Vulcano */ items.CanOpenRedDoors() and (items.CardNorfairL2 if self.Config.Keysanity else items.Super) and (items.CanFly() or items.HiJump or items.SpeedBooster) and - (items.CanPassBombPassages() or items.Gravity and items.Morph) and items.Wave - or + (items.CanPassBombPassages() or items.Gravity and items.Morph) and items.Wave) or ( # /* Reverse Lava Dive */ - items.CanAccessNorfairLowerPortal() and items.ScrewAttack and items.SpaceJump and items.Super and - items.Gravity and items.Wave and (items.CardNorfairL2 or items.Morph)) + items.Varia) and items.CanAccessNorfairLowerPortal() and items.ScrewAttack and items.SpaceJump and items.Super and ( + items.Gravity) and items.Wave and (items.CardNorfairL2 or items.Morph) else: return ((items.CanDestroyBombWalls() or items.SpeedBooster) and items.Super and items.Morph or items.CanAccessNorfairUpperPortal()) and ( # /* Ice Beam -> Croc Speedway */ @@ -65,5 +64,5 @@ class Crocomire(SMRegion): (items.Missile or items.Super or items.Wave) # /* Blue Gate */ ) or ( # /* Reverse Lava Dive */ - items.CanAccessNorfairLowerPortal()) and items.ScrewAttack and items.SpaceJump and items.Varia and items.Super and ( + items.Varia and items.CanAccessNorfairLowerPortal()) and items.ScrewAttack and items.SpaceJump and items.Super and ( items.HasEnergyReserves(2)) and (items.CardNorfairL2 or items.Morph) diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py index 053de3d1a6..e83c6f539c 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py @@ -9,12 +9,13 @@ class WreckedShip(SMRegion, IReward): def __init__(self, world, config: Config): super().__init__(world, config) - self.Reward = RewardType.GoldenFourBoss + self.Weight = 4 + self.Reward = RewardType.Null self.Locations = [ Location(self, 128, 0x8FC265, LocationType.Visible, "Missile (Wrecked Ship middle)", lambda items: items.CanPassBombPassages()), Location(self, 129, 0x8FC2E9, LocationType.Chozo, "Reserve Tank, Wrecked Ship", - lambda items: self.CanUnlockShip(items) and items.CardWreckedShipL1 and items.SpeedBooster and items.CanUsePowerBombs() and + lambda items: self.CanUnlockShip(items) and items.CardWreckedShipL1 and items.CanUsePowerBombs() and items.SpeedBooster and (items.Grapple or items.SpaceJump or items.Varia and items.HasEnergyReserves(2) or items.HasEnergyReserves(3)) if self.Logic == SMLogic.Normal else \ lambda items: self.CanUnlockShip(items) and items.CardWreckedShipL1 and items.CanUsePowerBombs() and items.SpeedBooster and (items.Varia or items.HasEnergyReserves(2))), @@ -27,7 +28,7 @@ class WreckedShip(SMRegion, IReward): Location(self, 132, 0x8FC337, LocationType.Visible, "Energy Tank, Wrecked Ship", lambda items: self.CanUnlockShip(items) and (items.HiJump or items.SpaceJump or items.SpeedBooster or items.Gravity) if self.Logic == SMLogic.Normal else \ - lambda items: self.CanUnlockShip(items) and (items.Bombs or items.PowerBomb or items.CanSpringBallJump() or + lambda items: self.CanUnlockShip(items) and (items.Morph and (items.Bombs or items.PowerBomb) or items.CanSpringBallJump() or items.HiJump or items.SpaceJump or items.SpeedBooster or items.Gravity)), Location(self, 133, 0x8FC357, LocationType.Visible, "Super Missile (Wrecked Ship left)", lambda items: self.CanUnlockShip(items)), diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py index 5bc581c6d4..7ca34cb031 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py @@ -14,15 +14,15 @@ class NorthEast(Z3Region): lambda items: items.MoonPearl and items.CanLiftLight()), Location(self, 256+79, 0x308147, LocationType.Regular, "Pyramid"), Location(self, 256+80, 0x1E980, LocationType.Regular, "Pyramid Fairy - Left", - lambda items: self.world.CanAquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and - (items.Hammer or items.Mirror and self.world.CanAquire(items, RewardType.Agahnim))), + lambda items: self.world.CanAcquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and + (items.Hammer or items.Mirror and self.world.CanAcquire(items, RewardType.Agahnim))), Location(self, 256+81, 0x1E983, LocationType.Regular, "Pyramid Fairy - Right", - lambda items: self.world.CanAquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and - (items.Hammer or items.Mirror and self.world.CanAquire(items, RewardType.Agahnim))) + lambda items: self.world.CanAcquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and + (items.Hammer or items.Mirror and self.world.CanAcquire(items, RewardType.Agahnim))) ] def CanEnter(self, items: Progression): - return self.world.CanAquire(items, RewardType.Agahnim) or items.MoonPearl and ( + return self.world.CanAcquire(items, RewardType.Agahnim) or items.MoonPearl and ( items.Hammer and items.CanLiftLight() or items.CanLiftHeavy() and items.Flippers or items.CanAccessDarkWorldPortal(self.Config) and items.Flippers) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py index 57b5ece194..28a318e80d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py @@ -25,7 +25,7 @@ class NorthWest(Z3Region): def CanEnter(self, items: Progression): return items.MoonPearl and (( - self.world.CanAquire(items, RewardType.Agahnim) or + self.world.CanAcquire(items, RewardType.Agahnim) or items.CanAccessDarkWorldPortal(self.Config) and items.Flippers ) and items.Hookshot and (items.Flippers or items.CanLiftLight() or items.Hammer) or items.Hammer and items.CanLiftLight() or diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py index f43cb8886b..14f4515c6d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py @@ -21,7 +21,7 @@ class South(Z3Region): def CanEnter(self, items: Progression): return items.MoonPearl and (( - self.world.CanAquire(items, RewardType.Agahnim) or + self.world.CanAcquire(items, RewardType.Agahnim) or items.CanAccessDarkWorldPortal(self.Config) and items.Flippers ) and (items.Hammer or items.Hookshot and (items.Flippers or items.CanLiftLight())) or items.Hammer and items.CanLiftLight() or diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py index 68fbd3b8db..0f21d7e284 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py @@ -34,33 +34,33 @@ class GanonsTower(Z3Region): self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ]) or self.GetLocation("Ganon's Tower - Firesnake Room").ItemIs(ItemType.KeyGT, self.world) else 3)), - Location(self, 256+196, 0x1EAC4, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Left", + Location(self, 256+230, 0x1EAC4, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Left", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Right"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ])), - Location(self, 256+197, 0x1EAC7, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Right", + Location(self, 256+231, 0x1EAC7, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Right", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ])), - Location(self, 256+198, 0x1EACA, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Left", + Location(self, 256+232, 0x1EACA, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Left", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Right"), self.GetLocation("Ganon's Tower - Randomizer Room - Top Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ])), - Location(self, 256+199, 0x1EACD, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Right", + Location(self, 256+233, 0x1EACD, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Right", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Right"), self.GetLocation("Ganon's Tower - Randomizer Room - Top Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left") ])), - Location(self, 256+200, 0x1EAD9, LocationType.Regular, "Ganon's Tower - Hope Room - Left"), - Location(self, 256+201, 0x1EADC, LocationType.Regular, "Ganon's Tower - Hope Room - Right"), - Location(self, 256+202, 0x1EAE2, LocationType.Regular, "Ganon's Tower - Tile Room", + Location(self, 256+234, 0x1EAD9, LocationType.Regular, "Ganon's Tower - Hope Room - Left"), + Location(self, 256+235, 0x1EADC, LocationType.Regular, "Ganon's Tower - Hope Room - Right"), + Location(self, 256+236, 0x1EAE2, LocationType.Regular, "Ganon's Tower - Tile Room", lambda items: items.Somaria), Location(self, 256+203, 0x1EAE5, LocationType.Regular, "Ganon's Tower - Compass Room - Top Left", lambda items: self.RightSide(items, [ @@ -118,8 +118,9 @@ class GanonsTower(Z3Region): return items.Somaria and items.Firerod and items.KeyGT >= (3 if any(l.ItemIs(ItemType.BigKeyGT, self.world) for l in locations) else 4) def BigKeyRoom(self, items: Progression): - return items.KeyGT >= 3 and self.CanBeatArmos(items) \ - and (items.Hammer and items.Hookshot or items.Firerod and items.Somaria) + return items.KeyGT >= 3 and \ + (items.Hammer and items.Hookshot or items.Firerod and items.Somaria) \ + and self.CanBeatArmos(items) def TowerAscend(self, items: Progression): return items.BigKeyGT and items.KeyGT >= 3 and items.Bow and items.CanLightTorches() @@ -134,13 +135,14 @@ class GanonsTower(Z3Region): def CanEnter(self, items: Progression): return items.MoonPearl and self.world.CanEnter("Dark World Death Mountain East", items) and \ - self.world.CanAquireAll(items, RewardType.CrystalBlue, RewardType.CrystalRed, RewardType.GoldenFourBoss) + self.world.CanAcquireAtLeast(self.world.TowerCrystals, items, RewardType.AnyCrystal) and \ + self.world.CanAcquireAtLeast(self.world.TourianBossTokens * (self.world.TowerCrystals / 7), items, RewardType.AnyBossToken) def CanFill(self, item: Item): - if (self.Config.GameMode == GameMode.Multiworld): + if (self.Config.Multiworld): if (item.World != self.world or item.Progression): return False - if (self.Config.KeyShuffle == KeyShuffle.Keysanity and not ((item.Type == ItemType.BigKeyGT or item.Type == ItemType.KeyGT) and item.World == self.world) and (item.IsKey() or item.IsBigKey() or item.IsKeycard())): + if (self.Config.Keysanity and not ((item.Type == ItemType.BigKeyGT or item.Type == ItemType.KeyGT) and item.World == self.world) and (item.IsKey() or item.IsBigKey() or item.IsKeycard())): return False return super().CanFill(item) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py index 6543017c6f..9b16a08b4d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py @@ -10,6 +10,7 @@ class IcePalace(Z3Region, IReward): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 4 self.RegionItems = [ ItemType.KeyIP, ItemType.BigKeyIP, ItemType.MapIP, ItemType.CompassIP] self.Reward = RewardType.Null self.Locations = [ @@ -43,7 +44,7 @@ class IcePalace(Z3Region, IReward): ] def CanNotWasteKeysBeforeAccessible(self, items: Progression, locations: List[Location]): - return not items.BigKeyIP or any(l.ItemIs(ItemType.BigKeyIP, self.world) for l in locations) + return self.world.ForwardSearch or not items.BigKeyIP or any(l.ItemIs(ItemType.BigKeyIP, self.world) for l in locations) def CanEnter(self, items: Progression): return items.MoonPearl and items.Flippers and items.CanLiftHeavy() and items.CanMeltFreezors() diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py index 0edf93f302..c111b07dfd 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py @@ -24,5 +24,5 @@ class NorthEast(Z3Region): Location(self, 256+42, 0x1EA85, LocationType.Regular, "Sahasrahla's Hut - Middle").Weighted(sphereOne), Location(self, 256+43, 0x1EA88, LocationType.Regular, "Sahasrahla's Hut - Right").Weighted(sphereOne), Location(self, 256+44, 0x5F1FC, LocationType.Regular, "Sahasrahla", - lambda items: self.world.CanAquire(items, RewardType.PendantGreen)) + lambda items: self.world.CanAcquire(items, RewardType.PendantGreen)) ] diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py index f35cbad33e..46f830dc8b 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py @@ -11,11 +11,11 @@ class NorthWest(Z3Region): sphereOne = -14 self.Locations = [ Location(self, 256+14, 0x589B0, LocationType.Pedestal, "Master Sword Pedestal", - lambda items: self.world.CanAquireAll(items, RewardType.PendantGreen, RewardType.PendantNonGreen)), + lambda items: self.world.CanAcquireAll(items, RewardType.AnyPendant)), Location(self, 256+15, 0x308013, LocationType.Regular, "Mushroom").Weighted(sphereOne), Location(self, 256+16, 0x308000, LocationType.Regular, "Lost Woods Hideout").Weighted(sphereOne), Location(self, 256+17, 0x308001, LocationType.Regular, "Lumberjack Tree", - lambda items: self.world.CanAquire(items, RewardType.Agahnim) and items.Boots), + lambda items: self.world.CanAcquire(items, RewardType.Agahnim) and items.Boots), Location(self, 256+18, 0x1EB3F, LocationType.Regular, "Pegasus Rocks", lambda items: items.Boots), Location(self, 256+19, 0x308004, LocationType.Regular, "Graveyard Ledge", diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py index 855c326f23..b1746184d3 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py @@ -10,9 +10,10 @@ class MiseryMire(Z3Region, IReward, IMedallionAccess): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 2 self.RegionItems = [ ItemType.KeyMM, ItemType.BigKeyMM, ItemType.MapMM, ItemType.CompassMM] self.Reward = RewardType.Null - self.Medallion = ItemType.Nothing + self.Medallion = None self.Locations = [ Location(self, 256+169, 0x1EA5E, LocationType.Regular, "Misery Mire - Main Lobby", lambda items: items.BigKeyMM or items.KeyMM >= 1), @@ -34,8 +35,9 @@ class MiseryMire(Z3Region, IReward, IMedallionAccess): # // Need "CanKillManyEnemies" if implementing swordless def CanEnter(self, items: Progression): - return (items.Bombos if self.Medallion == ItemType.Bombos else ( - items.Ether if self.Medallion == ItemType.Ether else items.Quake)) and items.Sword and \ + from worlds.smz3.TotalSMZ3.WorldState import Medallion + return (items.Bombos if self.Medallion == Medallion.Bombos else ( + items.Ether if self.Medallion == Medallion.Ether else items.Quake)) and items.Sword and \ items.MoonPearl and (items.Boots or items.Hookshot) and \ self.world.CanEnter("Dark World Mire", items) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py index 1805e74dca..27b5a1db43 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py @@ -10,6 +10,7 @@ class SwampPalace(Z3Region, IReward): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 3 self.RegionItems = [ ItemType.KeySP, ItemType.BigKeySP, ItemType.MapSP, ItemType.CompassSP] self.Reward = RewardType.Null self.Locations = [ diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py index c4d19bcda1..45546e9e99 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py @@ -10,9 +10,10 @@ class TurtleRock(Z3Region, IReward, IMedallionAccess): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 6 self.RegionItems = [ ItemType.KeyTR, ItemType.BigKeyTR, ItemType.MapTR, ItemType.CompassTR] self.Reward = RewardType.Null - self.Medallion = ItemType.Nothing + self.Medallion = None self.Locations = [ Location(self, 256+177, 0x1EA22, LocationType.Regular, "Turtle Rock - Compass Chest"), Location(self, 256+178, 0x1EA1C, LocationType.Regular, "Turtle Rock - Roller Room - Left", @@ -46,8 +47,9 @@ class TurtleRock(Z3Region, IReward, IMedallionAccess): return items.Firerod and items.Icerod def CanEnter(self, items: Progression): - return (items.Bombos if self.Medallion == ItemType.Bombos else ( - items.Ether if self.Medallion == ItemType.Ether else items.Quake)) and items.Sword and \ + from worlds.smz3.TotalSMZ3.WorldState import Medallion + return (items.Bombos if self.Medallion == Medallion.Bombos else ( + items.Ether if self.Medallion == Medallion.Ether else items.Quake)) and items.Sword and \ items.MoonPearl and items.CanLiftHeavy() and items.Hammer and items.Somaria and \ self.world.CanEnter("Light World Death Mountain East", items) diff --git a/worlds/smz3/TotalSMZ3/Text/Dialog.py b/worlds/smz3/TotalSMZ3/Text/Dialog.py index d465e1e201..92e034af67 100644 --- a/worlds/smz3/TotalSMZ3/Text/Dialog.py +++ b/worlds/smz3/TotalSMZ3/Text/Dialog.py @@ -4,9 +4,7 @@ class Dialog: command = re.compile(r"^\{[^}]*\}") invalid = re.compile(r"(?[0-9])|(?P[A-Z])|(?P[a-z])") @staticmethod def Simple(text: str): @@ -29,19 +27,16 @@ class Dialog: lineIndex += 1 - if (lineIndex % 3 == 0 and lineIndex < len(lines)): - bytes.append(0x7E) - if (lineIndex >= 3 and lineIndex < len(lines)): - bytes.append(0x73) + if (lineIndex < len(lines)): + if (lineIndex % 3 == 0): + bytes.append(0x7E) # pause for input + if (lineIndex >= 3): + bytes.append(0x73) # scroll - bytes.append(0x7F) - if (len(bytes) > maxBytes): - return bytes[:maxBytes - 1].append(0x7F) - - return bytes + return bytes[:maxBytes - 1].append(0x7F) @staticmethod - def Compiled(text: str, pause = True): + def Compiled(text: str): maxBytes = 2046 wrap = 19 @@ -49,6 +44,7 @@ class Dialog: raise Exception("Dialog commands must be placed on separate lines", text) padOut = False + pause = True bytes = [ 0xFB ] lines = Dialog.Wordwrap(text, wrap) @@ -61,33 +57,11 @@ class Dialog: return [ 0xFB, 0xFE, 0x6E, 0x00, 0xFE, 0x6B, 0x04 ] if (match.string == "{INTRO}"): padOut = True + if (match.string == "{NOPAUSE}"): + pause = False + continue - bytesMap = { - "{SPEED0}" : [ 0xFC, 0x00 ], - "{SPEED2}" : [ 0xFC, 0x02 ], - "{SPEED6}" : [ 0xFC, 0x06 ], - "{PAUSE1}" : [ 0xFE, 0x78, 0x01 ], - "{PAUSE3}" : [ 0xFE, 0x78, 0x03 ], - "{PAUSE5}" : [ 0xFE, 0x78, 0x05 ], - "{PAUSE7}" : [ 0xFE, 0x78, 0x07 ], - "{PAUSE9}" : [ 0xFE, 0x78, 0x09 ], - "{INPUT}" : [ 0xFA ], - "{CHOICE}" : [ 0xFE, 0x68 ], - "{ITEMSELECT}" : [ 0xFE, 0x69 ], - "{CHOICE2}" : [ 0xFE, 0x71 ], - "{CHOICE3}" : [ 0xFE, 0x72 ], - "{C:GREEN}" : [ 0xFE, 0x77, 0x07 ], - "{C:YELLOW}" : [ 0xFE, 0x77, 0x02 ], - "{HARP}" : [ 0xFE, 0x79, 0x2D ], - "{MENU}" : [ 0xFE, 0x6D, 0x00 ], - "{BOTTOM}" : [ 0xFE, 0x6D, 0x01 ], - "{NOBORDER}" : [ 0xFE, 0x6B, 0x02 ], - "{CHANGEPIC}" : [ 0xFE, 0x67, 0xFE, 0x67 ], - "{CHANGEMUSIC}" : [ 0xFE, 0x67 ], - "{INTRO}" : [ 0xFE, 0x6E, 0x00, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xFE, 0x6B, 0x02, 0xFE, 0x67 ], - "{IBOX}" : [ 0xFE, 0x6B, 0x02, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xF7 ], - } - result = bytesMap.get(match.string, None) + result = Dialog.CommandBytesFor(match.string) if (result is None): raise Exception(f"Dialog text contained unknown command {match.string}", text) else: @@ -98,12 +72,10 @@ class Dialog: continue - if (lineIndex == 1): - bytes.append(0xF8); #// row 2 - elif (lineIndex >= 3 and lineIndex < lineCount): - bytes.append(0xF6); #// scroll - elif (lineIndex >= 2): - bytes.append(0xF9); #// row 3 + if (lineIndex > 0): + bytes.append(0xF8 if lineIndex == 1 else #// row 2 + 0xF9 if lineIndex == 2 else #// row 3 + 0xF6) #// scroll #// The first box needs to fill the full width with spaces as the palette is loaded weird. letters = line + (" " * wrap) if padOut and lineIndex < 3 else line @@ -113,10 +85,39 @@ class Dialog: lineIndex += 1 if (pause and lineIndex % 3 == 0 and lineIndex < lineCount): - bytes.append(0xFA) #// wait for input + bytes.append(0xFA) #// pause for input return bytes[:maxBytes] + @staticmethod + def CommandBytesFor(text: str): + bytesMap = { + "{SPEED0}" : [ 0xFC, 0x00 ], + "{SPEED2}" : [ 0xFC, 0x02 ], + "{SPEED6}" : [ 0xFC, 0x06 ], + "{PAUSE1}" : [ 0xFE, 0x78, 0x01 ], + "{PAUSE3}" : [ 0xFE, 0x78, 0x03 ], + "{PAUSE5}" : [ 0xFE, 0x78, 0x05 ], + "{PAUSE7}" : [ 0xFE, 0x78, 0x07 ], + "{PAUSE9}" : [ 0xFE, 0x78, 0x09 ], + "{INPUT}" : [ 0xFA ], + "{CHOICE}" : [ 0xFE, 0x68 ], + "{ITEMSELECT}" : [ 0xFE, 0x69 ], + "{CHOICE2}" : [ 0xFE, 0x71 ], + "{CHOICE3}" : [ 0xFE, 0x72 ], + "{C:GREEN}" : [ 0xFE, 0x77, 0x07 ], + "{C:YELLOW}" : [ 0xFE, 0x77, 0x02 ], + "{HARP}" : [ 0xFE, 0x79, 0x2D ], + "{MENU}" : [ 0xFE, 0x6D, 0x00 ], + "{BOTTOM}" : [ 0xFE, 0x6D, 0x01 ], + "{NOBORDER}" : [ 0xFE, 0x6B, 0x02 ], + "{CHANGEPIC}" : [ 0xFE, 0x67, 0xFE, 0x67 ], + "{CHANGEMUSIC}" : [ 0xFE, 0x67 ], + "{INTRO}" : [ 0xFE, 0x6E, 0x00, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xFE, 0x6B, 0x02, 0xFE, 0x67 ], + "{IBOX}" : [ 0xFE, 0x6B, 0x02, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xF7 ], + } + return bytesMap.get(text, None) + @staticmethod def Wordwrap(text: str, width: int): result = [] @@ -146,9 +147,13 @@ class Dialog: @staticmethod def LetterToBytes(c: str): - if Dialog.digit.match(c): return [(ord(c) - ord('0') + 0xA0) ] - elif Dialog.uppercaseLetter.match(c): return [ (ord(c) - ord('A') + 0xAA) ] - elif Dialog.lowercaseLetter.match(c): return [ (ord(c) - ord('a') + 0x30) ] + match = Dialog.character.match(c) + if match is None: + value = Dialog.letters.get(c, None) + return value if value else [ 0xFF ] + elif match.group("digit") != None: return [(ord(c) - ord('0') + 0xA0) ] + elif match.group("upper") != None: return [ (ord(c) - ord('A') + 0xAA) ] + elif match.group("lower") != None: return [ (ord(c) - ord('a') + 0x30) ] else: value = Dialog.letters.get(c, None) return value if value else [ 0xFF ] diff --git a/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml b/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml index 12b5271eab..065e7a9e93 100644 --- a/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml +++ b/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml @@ -377,9 +377,76 @@ Items: THE GREEN BOOMERANG IS THE FASTEST! - Keycard: |- - A key from - the future? + + CardCrateriaL1: |- + An Alien Key! + It says On top + of the world! + CardCrateriaL2: |- + An Alien Key! + It says Lower + the drawbridge + CardCrateriaBoss: |- + An Alien Key! + It says The First + and The Last + CardBrinstarL1: |- + An Alien Key! + It says But wait + there's more! + CardBrinstarL2: |- + An Alien Key! + It says + Green Monkeys + CardBrinstarBoss: |- + An Alien Key! + It says + Metroid DLC + CardNorfairL1: |- + An Alien Key! + It says ice? + In this heat? + CardNorfairL2: |- + An Alien Key! + It says + THE BUBBLES! + CardNorfairBoss: |- + An Alien Key! + It says + Place your bets + CardMaridiaL1: |- + An Alien Key! + It says A + Day at the Beach + CardMaridiaL2: |- + An Alien Key! + It says + That's a Moray + CardMaridiaBoss: |- + An Alien Key! + It says Shrimp + for dinner? + CardWreckedShipL1: |- + An Alien Key! + It says + Gutter Ball + CardWreckedShipBoss: |- + An Alien Key! + It says The + Ghost of Arrghus + CardLowerNorfairL1: |- + An Alien Key! + It says Worst + Key in the Game + CardLowerNorfairBoss: |- + An Alien Key! + It says + This guy again? + + SmMap: |- + You can now + find your way + to the stars! Something: |- A small victory! diff --git a/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml b/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml index d1e5b9c364..918ef3f63d 100644 --- a/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml +++ b/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml @@ -4,8 +4,8 @@ # The order of the dialog entries is significant - set_cursor: [0xFB, 0xFC, 0x00, 0xF9, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xE4, 0xFE, 0x68] - set_cursor2: [0xFB, 0xFC, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xE4, 0xFE, 0x68] -- game_over_menu: { NoPause: "{SPEED0}\nSave and Continue\nSave and Quit\nContinue" } -- var_test: { NoPause: "0= ᚋ, 1= ᚌ\n2= ᚍ, 3= ᚎ" } +- game_over_menu: "{NOPAUSE}\n{SPEED0}\nSave and Continue\nSave and Quit\nContinue" +- var_test: "{NOPAUSE}\n0= ᚋ, 1= ᚌ\n2= ᚍ, 3= ᚎ" - follower_no_enter: "Can't you take me some place nice." - choice_1_3: [0xFB, 0xFC, 0x00, 0xF7, 0xE4, 0xF8, 0xFF, 0xF9, 0xFF, 0xFE, 0x71] - choice_2_3: [0xFB, 0xFC, 0x00, 0xF7, 0xFF, 0xF8, 0xE4, 0xF9, 0xFF, 0xFE, 0x71] @@ -290,10 +290,10 @@ # $110 - magic_bat_wake: "You bum! I was sleeping! Where's my magic bolts?" - magic_bat_give_half_magic: "How you like me now?" -- intro_main: { NoPause: "{INTRO}\n Episode III\n{PAUSE3}\n A Link to\n the Past\n{PAUSE3}\n Randomizer\n{PAUSE3}\nAfter mostly disregarding what happened in the first two games.\n{PAUSE3}\nLink awakens to his uncle leaving the house.\n{PAUSE3}\nHe just runs out the door,\n{PAUSE3}\ninto the rainy night.\n{PAUSE3}\n{CHANGEPIC}\nGanon has moved around all the items in Hyrule.\n{PAUSE7}\nYou will have to find all the items necessary to beat Ganon.\n{PAUSE7}\nThis is your chance to be a hero.\n{PAUSE3}\n{CHANGEPIC}\nYou must get the 7 crystals to beat Ganon.\n{PAUSE9}\n{CHANGEPIC}" } -- intro_throne_room: { NoPause: "{IBOX}\nLook at this Stalfos on the throne." } -- intro_zelda_cell: { NoPause: "{IBOX}\nIt is your time to shine!" } -- intro_agahnim: { NoPause: "{IBOX}\nAlso, you need to defeat this guy!" } +- intro_main: "{NOPAUSE}\n{INTRO}\n Episode III\n{PAUSE3}\n A Link to\n the Past\n{PAUSE3}\n Randomizer\n{PAUSE3}\nAfter mostly disregarding what happened in the first two games.\n{PAUSE3}\nLink awakens to his uncle leaving the house.\n{PAUSE3}\nHe just runs out the door,\n{PAUSE3}\ninto the rainy night.\n{PAUSE3}\n{CHANGEPIC}\nGanon has moved around all the items in Hyrule.\n{PAUSE7}\nYou will have to find all the items necessary to beat Ganon.\n{PAUSE7}\nThis is your chance to be a hero.\n{PAUSE3}\n{CHANGEPIC}\nYou must get the 7 crystals to beat Ganon.\n{PAUSE9}\n{CHANGEPIC}" +- intro_throne_room: "{NOPAUSE}\n{IBOX}\nLook at this Stalfos on the throne." +- intro_zelda_cell: "{NOPAUSE}\n{IBOX}\nIt is your time to shine!" +- intro_agahnim: "{NOPAUSE}\n{IBOX}\nAlso, you need to defeat this guy!" - pickup_purple_chest: "A curious box. Let's take it with us!" - bomb_shop: "30 bombs for 100 rupees. Good deals all day!" - bomb_shop_big_bomb: "30 bombs for 100 rupees, 100 rupees 1 BIG bomb. Good deals all day!" @@ -341,7 +341,6 @@ # $140 - agahnim_defeated: "Arrrgggghhh. Well you're coming with me!" - agahnim_final_meeting: "You have done well to come this far. Now, die!" -# $142 - zora_meeting: "What do you want?\n ≥ Flippers\n _Nothin'\n{CHOICE}" - zora_tells_cost: "Fine! But they aren't cheap. You got 500 rupees?\n ≥ Duh\n _Oh carp\n{CHOICE}" - zora_get_flippers: "Here's some Flippers for you! Swim little fish, swim." @@ -396,14 +395,12 @@ - lost_woods_thief: "Have you seen Andy?\n\nHe was out looking for our prized Ether medallion.\nI wonder when he will be back?" - blinds_hut_dude: "I'm just some dude. This is Blind's hut." - end_triforce: "{SPEED2}\n{MENU}\n{NOBORDER}\n G G" -# $174 - toppi_fallen: "Ouch!\n\nYou Jerk!" - kakariko_tavern_fisherman: "Don't argue\nwith a frozen\nDeadrock.\nHe'll never\nchange his\nposition!" - thief_money: "It's a secret to everyone." - thief_desert_rupee_cave: "So you, like, busted down my door, and are being a jerk by talking to me? Normally I would be angry and make you pay for it, but I bet you're just going to break all my pots and steal my 50 rupees." - thief_ice_rupee_cave: "I'm a rupee pot farmer. One day I will take over the world with my skillz. Have you met my brother in the desert? He's way richer than I am." - telepathic_tile_south_east_darkworld_cave: "~~ Dev cave ~~\n No farming\n required" -# $17A - cukeman: "Did you hear that Veetorp beat ajneb174 in a 1 on 1 race at AGDQ?" - cukeman_2: "You found Shabadoo, huh?\nNiiiiice." - potion_shop_no_cash: "Yo! I'm not running a charity here." @@ -415,19 +412,25 @@ - game_chest_lost_woods: "Pay 100 rupees, open 1 chest. Are you lucky?\nSo, Play game?\n ≥ Play\n Never!\n{CHOICE}" - kakariko_flophouse_man_no_flippers: "I sure do have a lot of beds.\n\nZora is a cheapskate and will try to sell you his trash for 500 rupees…" - kakariko_flophouse_man: "I sure do have a lot of beds.\n\nDid you know if you played the flute in the center of town things could happen?" -- menu_start_2: { NoPause: "{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n{CHOICE3}" } -- menu_start_3: { NoPause: "{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n_Mountain Cave\n{CHOICE2}" } -- menu_pause: { NoPause: "{SPEED0}\n≥Continue Game\n_Save and Quit\n{CHOICE3}" } +- menu_start_2: "{NOPAUSE}\n{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n{CHOICE3}" +- menu_start_3: "{NOPAUSE}\n{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n_Mountain Cave\n{CHOICE2}" +- menu_pause: "{NOPAUSE}\n{SPEED0}\n≥Continue Game\n_Save and Quit\n{CHOICE3}" - game_digging_choice: "Have 80 Rupees? Want to play digging game?\n ≥Yes\n _No\n{CHOICE}" - game_digging_start: "Okay, use the shovel with Y!" - game_digging_no_cash: "Shovel rental is 80 rupees.\nI have all day" - game_digging_end_time: "Time's up!\nTime for you to go." - game_digging_come_back_later: "Come back later, I have to bury things." - game_digging_no_follower: "Something is following you. I don't like." -- menu_start_4: { NoPause: "{MENU}\n{SPEED0}\n≥£'s House\n_Mountain Cave\n{CHOICE3}" } +- menu_start_4: "{NOPAUSE}\n{MENU}\n{SPEED0}\n≥£'s House\n_Mountain Cave\n{CHOICE3}" - ganon_fall_in_alt: "You think you\nare ready to\nface me?\n\nI will not die\n\nunless you\ncomplete your\ngoals. Dingus!" - ganon_phase_3_alt: "Got wax in your ears? I cannot die!" # $190 - sign_east_death_mountain_bridge: "How did you get up here?" - fish_money: "It's a secret to everyone." +- sign_ganons_tower: "You need all 7 crystals to enter." +- sign_ganon: "You need all 7 crystals to beat Ganon." +- ganon_phase_3_no_bow: "You have no bow. Dingus!" +- ganon_phase_3_no_silvers_alt: "You can't best me without silver arrows!" +- ganon_phase_3_no_silvers: "You can't best me without silver arrows!" +- ganon_phase_3_silvers: "Oh no! Silver! My one true weakness!" - end_pad_data: "" diff --git a/worlds/smz3/TotalSMZ3/Text/StringTable.py b/worlds/smz3/TotalSMZ3/Text/StringTable.py index 4c1986993a..13f3f5edb5 100644 --- a/worlds/smz3/TotalSMZ3/Text/StringTable.py +++ b/worlds/smz3/TotalSMZ3/Text/StringTable.py @@ -3,7 +3,7 @@ from typing import Any, List import copy from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog from worlds.smz3.TotalSMZ3.Text.Texts import text_folder -from yaml import load, Loader +from Utils import unsafe_parse_yaml class StringTable: @@ -11,7 +11,7 @@ class StringTable: def ParseEntries(resource: str): with open(resource, 'rb') as f: yaml = str(f.read(), "utf-8") - content = load(yaml, Loader) + content = unsafe_parse_yaml(yaml) result = [] for entryValue in content: @@ -20,8 +20,6 @@ class StringTable: result.append((key, value)) elif isinstance(value, str): result.append((key, Dialog.Compiled(value))) - elif isinstance(value, dict): - result.append((key, Dialog.Compiled(value["NoPause"], False))) else: raise Exception(f"Did not expect an object of type {type(value)}") return result @@ -47,9 +45,11 @@ class StringTable: def SetGanonThirdPhaseText(self, text: str): self.SetText("ganon_phase_3", text) + self.SetText("ganon_phase_3_no_silvers", text) + self.SetText("ganon_phase_3_no_silvers_alt", text) def SetTriforceRoomText(self, text: str): - self.SetText("end_triforce", "{NOBORDER}\n" + text) + self.SetText("end_triforce", f"{{NOBORDER}}\n{text}") def SetPedestalText(self, text: str): self.SetText("mastersword_pedestal_translated", text) @@ -60,6 +60,12 @@ class StringTable: def SetBombosText(self, text: str): self.SetText("tablet_bombos_book", text) + def SetTowerRequirementText(self, text: str): + self.SetText("sign_ganons_tower", text) + + def SetGanonRequirementText(self, text: str): + self.SetText("sign_ganon", text) + def SetText(self, name: str, text: str): count = 0 for key, value in self.entries: diff --git a/worlds/smz3/TotalSMZ3/Text/Texts.py b/worlds/smz3/TotalSMZ3/Text/Texts.py index 247ff92b1a..dfaeee06da 100644 --- a/worlds/smz3/TotalSMZ3/Text/Texts.py +++ b/worlds/smz3/TotalSMZ3/Text/Texts.py @@ -2,7 +2,7 @@ from worlds.smz3.TotalSMZ3.Region import Region from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower from worlds.smz3.TotalSMZ3.Item import Item, ItemType -from yaml import load, Loader +from Utils import unsafe_parse_yaml import random import os @@ -13,7 +13,7 @@ class Texts: def ParseYamlScripts(resource: str): with open(resource, 'rb') as f: yaml = str(f.read(), "utf-8") - return load(yaml, Loader) + return unsafe_parse_yaml(yaml) @staticmethod def ParseTextScript(resource: str): @@ -75,7 +75,7 @@ class Texts: } if item.IsMap(): name = "Map" elif item.IsCompass(): name = "Compass" - elif item.IsKeycard(): name = "Keycard" + elif item.IsSmMap(): name = "SmMap" else: name = nameMap[item.Type] items = Texts.scripts["Items"] diff --git a/worlds/smz3/TotalSMZ3/World.py b/worlds/smz3/TotalSMZ3/World.py index 14d685167f..722d5858e6 100644 --- a/worlds/smz3/TotalSMZ3/World.py +++ b/worlds/smz3/TotalSMZ3/World.py @@ -54,10 +54,26 @@ class World: Player: str Guid: str Id: int + WorldState = None + + @property + def TowerCrystals(self): + return 7 if self.WorldState is None else self.WorldState.TowerCrystals + + @property + def GanonCrystals(self): + return 7 if self.WorldState is None else self.WorldState.GanonCrystals + + @property + def TourianBossTokens(self): + return 4 if self.WorldState is None else self.WorldState.TourianBossTokens def Items(self): return [l.Item for l in self.Locations if l.Item != None] + ForwardSearch: bool = False + + rewardLookup: Dict[int, List[Region.IReward]] locationLookup: Dict[str, Location.Location] regionLookup: Dict[str, Region.Region] @@ -95,22 +111,22 @@ class World: DarkWorldNorthEast(self, self.Config), DarkWorldSouth(self, self.Config), DarkWorldMire(self, self.Config), - Central(self, self.Config), CrateriaWest(self, self.Config), + Central(self, self.Config), CrateriaEast(self, self.Config), Blue(self, self.Config), Green(self, self.Config), - Kraid(self, self.Config), Pink(self, self.Config), Red(self, self.Config), + Kraid(self, self.Config), + WreckedShip(self, self.Config), Outer(self, self.Config), Inner(self, self.Config), NorfairUpperWest(self, self.Config), NorfairUpperEast(self, self.Config), Crocomire(self, self.Config), NorfairLowerWest(self, self.Config), - NorfairLowerEast(self, self.Config), - WreckedShip(self, self.Config) + NorfairLowerEast(self, self.Config) ] self.Locations = [] @@ -130,37 +146,32 @@ class World: raise Exception(f"World.CanEnter: Invalid region name {regionName}", f'{regionName=}'.partition('=')[0]) return region.CanEnter(items) - def CanAquire(self, items: Item.Progression, reward: Region.RewardType): + def CanAcquire(self, items: Item.Progression, reward: Region.RewardType): return next(iter([region for region in self.Regions if isinstance(region, Region.IReward) and region.Reward == reward])).CanComplete(items) - def CanAquireAll(self, items: Item.Progression, *rewards: Region.RewardType): - for region in self.Regions: - if issubclass(type(region), Region.IReward): - if (region.Reward in rewards): - if not region.CanComplete(items): - return False - return True + def CanAcquireAll(self, items: Item.Progression, rewardsMask: Region.RewardType): + return all(region.CanComplete(items) for region in self.rewardLookup[rewardsMask.value]) - # return all(region.CanComplete(items) for region in self.Regions if (isinstance(region, Region.IReward) and region.Reward in rewards)) + def CanAcquireAtLeast(self, amount, items: Item.Progression, rewardsMask: Region.RewardType): + return len([region for region in self.rewardLookup[rewardsMask.value] if region.CanComplete(items)]) >= amount - def Setup(self, rnd: random): - self.SetMedallions(rnd) - self.SetRewards(rnd) + def Setup(self, state): + self.WorldState = state + self.SetMedallions(state.Medallions) + self.SetRewards(state.Rewards) + self.SetRewardLookup() - def SetMedallions(self, rnd: random): - medallionMap = {0: Item.ItemType.Bombos, 1: Item.ItemType.Ether, 2: Item.ItemType.Quake} - regionList = [region for region in self.Regions if isinstance(region, Region.IMedallionAccess)] - for region in regionList: - region.Medallion = medallionMap[rnd.randint(0, 2)] + def SetRewards(self, rewards: List): + regions = [region for region in self.Regions if isinstance(region, Region.IReward) and region.Reward == Region.RewardType.Null] + for (region, reward) in zip(regions, rewards): + region.Reward = reward - def SetRewards(self, rnd: random): - rewards = [ - Region.RewardType.PendantGreen, Region.RewardType.PendantNonGreen, Region.RewardType.PendantNonGreen, Region.RewardType.CrystalRed, Region.RewardType.CrystalRed, - Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue - ] - rnd.shuffle(rewards) - regionList = [region for region in self.Regions if isinstance(region, Region.IReward) and region.Reward == Region.RewardType.Null] - for region in regionList: - region.Reward = rewards[0] - rewards.remove(region.Reward) + def SetMedallions(self, medallions: List): + self.GetRegion("Misery Mire").Medallion = medallions[0] + self.GetRegion("Turtle Rock").Medallion = medallions[1] + def SetRewardLookup(self): + #/* Generate a lookup of all possible regions for any given reward combination for faster lookup later */ + self.rewardLookup: Dict[int, Region.IReward] = {} + for i in range(0, 512): + self.rewardLookup[i] = [region for region in self.Regions if isinstance(region, Region.IReward) and (region.Reward.value & i) != 0] diff --git a/worlds/smz3/TotalSMZ3/WorldState.py b/worlds/smz3/TotalSMZ3/WorldState.py new file mode 100644 index 0000000000..c857b539d1 --- /dev/null +++ b/worlds/smz3/TotalSMZ3/WorldState.py @@ -0,0 +1,170 @@ +from enum import Enum +from typing import List +from copy import copy + +from worlds.smz3.TotalSMZ3.Patch import DropPrize +from worlds.smz3.TotalSMZ3.Region import RewardType +from worlds.smz3.TotalSMZ3.Config import OpenTower, GanonVulnerable, OpenTourian + +class Medallion(Enum): + Bombos = 0 + Ether = 1 + Quake = 2 + +class DropPrizeRecord: + Packs: List[DropPrize] + TreePulls: List[DropPrize] + CrabContinous: DropPrize + CrabFinal: DropPrize + Stun: DropPrize + Fish: DropPrize + + def __init__(self, Packs, TreePulls, CrabContinous, CrabFinal, Stun, Fish): + self.Packs = Packs + self.TreePulls = TreePulls + self.CrabContinous = CrabContinous + self.CrabFinal = CrabFinal + self.Stun = Stun + self.Fish = Fish + +class WorldState: + Rewards: List[RewardType] + Medallions: List[Medallion] + + TowerCrystals: int + GanonCrystals: int + TourianBossTokens: int + + DropPrizes: DropPrizeRecord + + def __init__(self, config, rnd): + self.Rewards = self.DistributeRewards(rnd) + self.Medallions = self.GenerateMedallions(rnd) + self.TowerCrystals = rnd.randint(0, 7) if config.OpenTower == OpenTower.Random else config.OpenTower.value + self.GanonCrystals = rnd.randint(0, 7) if config.GanonVulnerable == GanonVulnerable.Random else config.GanonVulnerable.value + self.TourianBossTokens = rnd.randint(0, 4) if config.OpenTourian == OpenTourian.Random else config.OpenTourian.value + self.DropPrizes = self.ShuffleDropPrizes(rnd) + + @staticmethod + def Generate(config, rnd): + return WorldState(config, rnd) + + BaseRewards = [ + RewardType.PendantGreen, RewardType.PendantNonGreen, RewardType.PendantNonGreen, RewardType.CrystalRed, RewardType.CrystalRed, + RewardType.CrystalBlue, RewardType.CrystalBlue, RewardType.CrystalBlue, RewardType.CrystalBlue, RewardType.CrystalBlue, + RewardType.AnyBossToken, RewardType.AnyBossToken, RewardType.AnyBossToken, RewardType.AnyBossToken, + ] + + BossTokens = [ + RewardType.BossTokenKraid, RewardType.BossTokenPhantoon, RewardType.BossTokenDraygon, RewardType.BossTokenRidley + ] + + @staticmethod + def DistributeRewards(rnd): + #// Assign four rewards for SM using a "loot table", randomized result + gen = WorldState.Distribution().Generate(lambda dist: dist.Hit(rnd.randrange(dist.Sum))) + smRewards = [next(gen) for x in range(4)] + + #// Exclude the SM rewards to get the Z3 lineup + z3Rewards = WorldState.BaseRewards[:] + for reward in smRewards: + z3Rewards.remove(reward) + + rnd.shuffle(z3Rewards) + #// Replace "any token" with random specific tokens + rewards = z3Rewards + smRewards + tokens = WorldState.BossTokens[:] + rnd.shuffle(tokens) + rewards = [tokens.pop() if reward == RewardType.AnyBossToken else reward for reward in rewards] + + return rewards + + + class Distribution: + factor = 3 + + def __init__(self, distribution = None, boss = None, blue = None, red = None, pend = None, green = None): + self.Boss = 4 * self.factor + self.Blue = 5 * self.factor + self.Red = 2 * self.factor + self.Pend = 2 + self.Green = 1 + + if (distribution is not None): + self = copy(distribution) + if (boss is not None): + self.Boss = boss + if (blue is not None): + self.Blue = blue + if (red is not None): + self.Red = red + if (pend is not None): + self.Pend = pend + if (green is not None): + self.Green = green + + @property + def Sum(self): + return self.Boss + self.Blue + self.Red + self.Pend + self.Green + + def Hit(self, p): + p -= self.Boss + if (p < 0): return (RewardType.AnyBossToken, WorldState.Distribution(self, boss = self.Boss - WorldState.Distribution.factor)) + p -= self.Blue + if (p - self.Blue < 0): return (RewardType.CrystalBlue, WorldState.Distribution(self, blue = self.Blue - WorldState.Distribution.factor)) + p -= self.Red + if (p - self.Red < 0): return (RewardType.CrystalRed, WorldState.Distribution(self, red = self.Red - WorldState.Distribution.factor)) + p -= self.Pend + if (p - self.Pend < 0): return (RewardType.PendantNonGreen, WorldState.Distribution(self, pend = self.Pend - 1)) + return (RewardType.PendantGreen, WorldState.Distribution(self, green = self.Green - 1)) + + def Generate(self, func): + result = None + while (True): + (result, newSelf) = func(self) + self.Boss = newSelf.Boss + self.Blue = newSelf.Blue + self.Red = newSelf.Red + self.Pend = newSelf.Pend + self.Green = newSelf.Green + yield result + + @staticmethod + def GenerateMedallions(rnd): + return [ + Medallion(rnd.randrange(3)), + Medallion(rnd.randrange(3)), + ] + + BaseDropPrizes = [ + DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, #// pack 1 + DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Red, DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Blue, #// pack 2 + DropPrize.FullMagic, DropPrize.Magic, DropPrize.Magic, DropPrize.Blue, DropPrize.FullMagic, DropPrize.Magic, DropPrize.Heart, DropPrize.Magic, #// pack 3 + DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb4, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb8, DropPrize.Bomb1, #// pack 4 + DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10, DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10,#// pack 5 + DropPrize.Magic, DropPrize.Green, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Magic, DropPrize.Bomb1, DropPrize.Green, DropPrize.Heart, #// pack 6 + DropPrize.Heart, DropPrize.Fairy, DropPrize.FullMagic, DropPrize.Red, DropPrize.Bomb8, DropPrize.Heart, DropPrize.Red, DropPrize.Arrow10, #// pack 7 + DropPrize.Green, DropPrize.Blue, DropPrize.Red,#// from pull trees + DropPrize.Green, DropPrize.Red,#// from prize crab + DropPrize.Green, #// stunned prize + DropPrize.Red,#// saved fish prize + ] + + @staticmethod + def ShuffleDropPrizes(rnd): + nrPackDrops = 8 * 7 + nrTreePullDrops = 3 + + prizes = WorldState.BaseDropPrizes[:] + rnd.shuffle(prizes) + + (packs, prizes) = (prizes[:nrPackDrops], prizes[nrPackDrops:]) + (treePulls, prizes) = (prizes[:nrTreePullDrops], prizes[nrTreePullDrops:]) + (crabContinous, crabFinalDrop, prizes) = (prizes[0], prizes[1], prizes[2:]) + (stun, prizes) = (prizes[0], prizes[1:]) + fish = prizes[0] + return DropPrizeRecord(packs, treePulls, crabContinous, crabFinalDrop, stun, fish) + + @staticmethod + def SplitOff(source, count): + return (source[:count], source[count:]) \ No newline at end of file diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 9a0fcad90e..2849567d33 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -12,9 +12,10 @@ from worlds.smz3.TotalSMZ3.Item import ItemType import worlds.smz3.TotalSMZ3.Item as TotalSMZ3Item from worlds.smz3.TotalSMZ3.World import World as TotalSMZ3World from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower -from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic +from worlds.smz3.TotalSMZ3.Config import Config, GameMode, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic, OpenTower, GanonVulnerable, OpenTourian from worlds.smz3.TotalSMZ3.Location import LocationType, locations_start_id, Location as TotalSMZ3Location from worlds.smz3.TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray +from worlds.smz3.TotalSMZ3.WorldState import WorldState from ..AutoWorld import World, AutoLogicRegister, WebWorld from .Rom import get_base_rom_bytes, SMZ3DeltaPatch from .ips import IPS_Patch @@ -60,12 +61,12 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - data_version = 1 + data_version = 2 options = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] item_name_to_id = TotalSMZ3Item.lookup_name_to_id - location_name_to_id: Dict[str, int] = {key : locations_start_id + value.Id for key, value in TotalSMZ3World(Config({}), "", 0, "").locationLookup.items()} + location_name_to_id: Dict[str, int] = {key : locations_start_id + value.Id for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()} web = SMZ3Web() remote_items: bool = False @@ -180,30 +181,32 @@ class SMZ3World(World): base_combined_rom = get_base_rom_bytes() def generate_early(self): - config = Config({}) - config.GameMode = GameMode.Multiworld - config.Z3Logic = Z3Logic.Normal - config.SMLogic = SMLogic(self.world.sm_logic[self.player].value) - config.SwordLocation = SwordLocation(self.world.sword_location[self.player].value) - config.MorphLocation = MorphLocation(self.world.morph_location[self.player].value) - config.Goal = Goal.DefeatBoth - config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value) - config.Keysanity = config.KeyShuffle != KeyShuffle.Null - config.GanonInvincible = GanonInvincible.BeforeCrystals + self.config = Config() + self.config.GameMode = GameMode.Multiworld + self.config.Z3Logic = Z3Logic.Normal + self.config.SMLogic = SMLogic(self.world.sm_logic[self.player].value) + self.config.SwordLocation = SwordLocation(self.world.sword_location[self.player].value) + self.config.MorphLocation = MorphLocation(self.world.morph_location[self.player].value) + self.config.Goal = Goal(self.world.goal[self.player].value) + self.config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value) + self.config.OpenTower = OpenTower(self.world.open_tower[self.player].value) + self.config.GanonVulnerable = GanonVulnerable(self.world.ganon_vulnerable[self.player].value) + self.config.OpenTourian = OpenTourian(self.world.open_tourian[self.player].value) self.local_random = random.Random(self.world.random.randint(0, 1000)) - self.smz3World = TotalSMZ3World(config, self.world.get_player_name(self.player), self.player, self.world.seed_name) + self.smz3World = TotalSMZ3World(self.config, self.world.get_player_name(self.player), self.player, self.world.seed_name) self.smz3DungeonItems = [] SMZ3World.location_names = frozenset(self.smz3World.locationLookup.keys()) self.world.state.smz3state[self.player] = TotalSMZ3Item.Progression([]) def generate_basic(self): - self.smz3World.Setup(self.world.random) + self.smz3World.Setup(WorldState.Generate(self.config, self.world.random)) self.dungeon = TotalSMZ3Item.Item.CreateDungeonPool(self.smz3World) self.dungeon.reverse() self.progression = TotalSMZ3Item.Item.CreateProgressionPool(self.smz3World) self.keyCardsItems = TotalSMZ3Item.Item.CreateKeycards(self.smz3World) + self.SmMapsItems = TotalSMZ3Item.Item.CreateSmMaps(self.smz3World) niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World) junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World) @@ -211,7 +214,7 @@ class SMZ3World(World): self.junkItemsNames = [item.Type.name for item in junkItems] if (self.smz3World.Config.Keysanity): - progressionItems = self.progression + self.dungeon + self.keyCardsItems + progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems else: progressionItems = self.progression for item in self.keyCardsItems: @@ -352,6 +355,49 @@ class SMZ3World(World): return patch + def SnesCustomization(self, addr: int): + addr = (0x400000 if addr < 0x800000 else 0)| (addr & 0x3FFFFF) + return addr + + def apply_customization(self): + patch = {} + + # smSpinjumps + if (self.world.spin_jumps_animation[self.player].value == 1): + patch[self.SnesCustomization(0x9B93FE)] = bytearray([0x01]) + + # z3HeartBeep + values = [ 0x00, 0x80, 0x40, 0x20, 0x10] + index = self.world.heart_beep_speed[self.player].value + patch[0x400033] = bytearray([values[index if index < len(values) else 2]]) + + # z3HeartColor + values = [ + [0x24, [0x18, 0x00]], + [0x3C, [0x04, 0x17]], + [0x2C, [0xC9, 0x69]], + [0x28, [0xBC, 0x02]] + ] + index = self.world.heart_color[self.player].value + (hud, fileSelect) = values[index if index < len(values) else 0] + for i in range(0, 20, 2): + patch[self.SnesCustomization(0xDFA1E + i)] = bytearray([hud]) + patch[self.SnesCustomization(0x1BD6AA)] = bytearray(fileSelect) + + # z3QuickSwap + patch[0x40004B] = bytearray([0x01 if self.world.quick_swap[self.player].value else 0x00]) + + # smEnergyBeepOff + if (self.world.energy_beep[self.player].value == 0): + for ([addr, value]) in [ + [0x90EA9B, 0x80], + [0x90F337, 0x80], + [0x91E6D5, 0x80] + ]: + patch[self.SnesCustomization(addr)] = bytearray([value]) + + return patch + def generate_output(self, output_directory: str): try: base_combined_rom = get_base_rom_bytes() @@ -368,6 +414,7 @@ class SMZ3World(World): patches = patcher.Create(self.smz3World.Config) patches.update(self.apply_sm_custom_sprite()) patches.update(self.apply_item_names()) + patches.update(self.apply_customization()) for addr, bytes in patches.items(): offset = 0 for byte in bytes: @@ -463,7 +510,7 @@ class SMZ3World(World): item.item.Progression = False item.location.event = False self.unreachable.append(item.location) - self.JunkFillGT() + self.JunkFillGT(0.5) def get_pre_fill_items(self): if (not self.smz3World.Config.Keysanity): @@ -477,21 +524,34 @@ class SMZ3World(World): def write_spoiler(self, spoiler_handle: TextIO): self.world.spoiler.unreachables.update(self.unreachable) - def JunkFillGT(self): + def JunkFillGT(self, factor): + poolLength = len(self.world.itempool) + junkPoolIdx = [i for i in range(0, poolLength) + if self.world.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) and + self.world.itempool[i].player == self.player] + toRemove = [] for loc in self.locations.values(): + # commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT + # which isnt desirable (SMZ3 logic only filters for SMZ3 items). Having progression in GT can only happen in Single Player. + # if len(toRemove) >= int(len(self.locationNamesGT) * factor * self.smz3World.TowerCrystals / 7): + # break if loc.name in self.locationNamesGT and loc.item is None: - poolLength = len(self.world.itempool) + poolLength = len(junkPoolIdx) # start looking at a random starting index and loop at start if no match found start = self.world.random.randint(0, poolLength) for off in range(0, poolLength): i = (start + off) % poolLength - if self.world.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) \ - and loc.can_fill(self.world.state, self.world.itempool[i], False): - itemFromPool = self.world.itempool.pop(i) + candidate = self.world.itempool[junkPoolIdx[i]] + if junkPoolIdx[i] not in toRemove and loc.can_fill(self.world.state, candidate, False): + itemFromPool = candidate + toRemove.append(junkPoolIdx[i]) break self.world.push_item(loc, itemFromPool, False) loc.event = False - + toRemove.sort(reverse = True) + for i in toRemove: + self.world.itempool.pop(i) + def FillItemAtLocation(self, itemPool, itemType, location): itemToPlace = TotalSMZ3Item.Item.Get(itemPool, itemType, self.smz3World) if (itemToPlace == None): @@ -524,7 +584,8 @@ class SMZ3World(World): def InitialFillInOwnWorld(self): self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySW, self.smz3World.GetLocation("Skull Woods - Pinball Room")) - self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySP, self.smz3World.GetLocation("Swamp Palace - Entrance")) + if (not self.smz3World.Config.Keysanity): + self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySP, self.smz3World.GetLocation("Swamp Palace - Entrance")) # /* Check Swords option and place as needed */ if self.smz3World.Config.SwordLocation == SwordLocation.Uncle: diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index 6faeeaa2fb4bb4f2b29203806e3d43e91be85d55..2d6027d5e5875bc3fead9306d855e3f0f77ee168 100644 GIT binary patch delta 21133 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 eG2?cuxzMBJgpaWA)gEbH1+^FIJ;u#`{$ delta 16841 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 From 97b388747ade39b48ba64c98300499d0768955ae Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 14 Aug 2022 21:16:38 +0200 Subject: [PATCH 119/138] Docs: Added DS3 & DK3 to network graph --- docs/network diagram.jpg | Bin 252329 -> 0 bytes docs/network diagram.svg | 1 - docs/network diagram/network diagram.jpg | Bin 0 -> 538820 bytes docs/{ => network diagram}/network diagram.md | 8 ++++++++ docs/network diagram/network diagram.svg | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 docs/network diagram.jpg delete mode 100644 docs/network diagram.svg create mode 100644 docs/network diagram/network diagram.jpg rename docs/{ => network diagram}/network diagram.md (95%) create mode 100644 docs/network diagram/network diagram.svg diff --git a/docs/network diagram.jpg b/docs/network diagram.jpg deleted file mode 100644 index e738778d262eb6a61f9d6cac18322db013649121..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 252329 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 WM&KBMV+4*7I7Z

Secret of Evermore
WebHost (archipelago.gg)
.NET
Java
Native
SMZ3
Super Metroid
Ocarina of Time
Final Fantasy 1
A Link to the Past
ChecksFinder
Starcraft 2
FNA/XNA
Unity
Minecraft
Secret of Evermore
WebSockets
WebSockets
Integrated
Integrated
Various, depending on SNES device
LuaSockets
Integrated
LuaSockets
Integrated
Integrated
WebSockets
Various, depending on SNES device
Various, depending on SNES device
The Witness Randomizer
Various, depending on SNES device
WebSockets
WebSockets
Mod the Spire
TCP
Forge Mod Loader
WebSockets
TsRandomizer
RogueLegacyRandomizer
BepInEx
QModLoader (BepInEx)
HK Modding API
WebSockets
SQL
Subprocesses
SQL
Deposit Generated Worlds
Provide Generation Instructions
Subprocesses
Subprocesses
RCON
UDP
Integrated
Factorio Server
FactorioClient
Factorio Games
Factorio Mod Generated by AP
Factorio Modding API
SNES
Configurable (waitress, gunicorn, flask)
AutoHoster
PonyORM DB
WebHost
Flask WebContent
AutoGenerator
Mod with Archipelago.MultiClient.Net
Risk of Rain 2
Subnautica
Hollow Knight
Raft
Timespinner
Rogue Legacy
Mod with Archipelago.MultiClient.Java
Slay the Spire
Minecraft Forge Server
Any Java Minecraft Clients
Game using apclientpp Client Library
Game using Apcpp Client Library
Super Mario 64 Ex
VVVVVV
Meritous
The Witness
Sonic Adventure 2: Battle
ap-soeclient
SNES
SNES
OoTClient
Lua Connector
BizHawk with Ocarina of Time Loaded
FF1Client
Lua Connector
BizHawk with Final Fantasy Loaded
SNES
ChecksFinderClient
ChecksFinder
Starcraft 2 Game Client
Starcraft2Client.py
apsc2 Python Package
Archipelago Server
CommonClient.py
Super Nintendo Interface (SNI)
SNIClient
\ No newline at end of file diff --git a/docs/network diagram/network diagram.jpg b/docs/network diagram/network diagram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..15495e27246af113f335977137042555fb723716 GIT binary patch literal 538820 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 z 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ξ)2 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

Qae&f;CSa29vpI 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%0As)sSa~r<88ynUgoxil%b9`XJ8pCe?`XdY-f6V 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 Q*>7%3_n&glLcetX1>p1z?*IS* literal 0 HcmV?d00001 diff --git a/docs/network diagram.md b/docs/network diagram/network diagram.md similarity index 95% rename from docs/network diagram.md rename to docs/network diagram/network diagram.md index fc04b87f32..2bffd9f295 100644 --- a/docs/network diagram.md +++ b/docs/network diagram/network diagram.md @@ -69,6 +69,12 @@ flowchart LR end SNI <-- Various, depending on SNES device --> SMZ + %% Donkey Kong Country 3 + subgraph Donkey Kong Country 3 + DK3[SNES] + end + SNI <-- Various, depending on SNES device --> DK3 + %% Native Clients or Games %% Games or clients which compile to native or which the client is integrated in the game. subgraph "Native" @@ -82,10 +88,12 @@ flowchart LR MT[Meritous] TW[The Witness] SA2B[Sonic Adventure 2: Battle] + DS3[Dark Souls 3] APCLIENTPP <--> SOE APCLIENTPP <--> MT APCLIENTPP <-- The Witness Randomizer --> TW + APCLIENTPP <--> DS3 APCPP <--> SM64 APCPP <--> V6 APCPP <--> SA2B diff --git a/docs/network diagram/network diagram.svg b/docs/network diagram/network diagram.svg new file mode 100644 index 0000000000..38d3cc0713 --- /dev/null +++ b/docs/network diagram/network diagram.svg @@ -0,0 +1 @@ +
Factorio
Secret of Evermore
WebHost (archipelago.gg)
.NET
Java
Native
Donkey Kong Country 3
Super Metroid/A Link to the Past Combo Randomizer
Super Metroid
Ocarina of Time
Final Fantasy 1
A Link to the Past
ChecksFinder
Starcraft 2
FNA/XNA
Unity
Minecraft
Secret of Evermore
WebSockets
WebSockets
Integrated
Integrated
Various, depending on SNES device
LuaSockets
Integrated
LuaSockets
Integrated
Integrated
WebSockets
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
The Witness Randomizer
Various, depending on SNES device
WebSockets
WebSockets
Mod the Spire
TCP
Forge Mod Loader
WebSockets
TsRandomizer
RogueLegacyRandomizer
BepInEx
QModLoader (BepInEx)
HK Modding API
WebSockets
SQL
Subprocesses
SQL
Deposit Generated Worlds
Provide Generation Instructions
Subprocesses
Subprocesses
RCON
UDP
Integrated
Factorio Server
FactorioClient
Factorio Games
Factorio Mod Generated by AP
Factorio Modding API
SNES
Configurable (waitress, gunicorn, flask)
AutoHoster
PonyORM DB
WebHost
Flask WebContent
AutoGenerator
Mod with Archipelago.MultiClient.Net
Risk of Rain 2
Subnautica
Hollow Knight
Raft
Timespinner
Rogue Legacy
Mod with Archipelago.MultiClient.Java
Slay the Spire
Minecraft Forge Server
Any Java Minecraft Clients
Game using apclientpp Client Library
Game using Apcpp Client Library
Super Mario 64 Ex
VVVVVV
Meritous
The Witness
Sonic Adventure 2: Battle
Dark Souls 3
ap-soeclient
SNES
SNES
SNES
OoTClient
Lua Connector
BizHawk with Ocarina of Time Loaded
FF1Client
Lua Connector
BizHawk with Final Fantasy Loaded
SNES
ChecksFinderClient
ChecksFinder
Starcraft 2 Game Client
Starcraft2Client.py
apsc2 Python Package
Archipelago Server
CommonClient.py
Super Nintendo Interface (SNI)
SNIClient
\ No newline at end of file From f716bfc58faf406ee2a6020eb9d86a59cc7657f4 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Mon, 15 Aug 2022 17:29:35 +0200 Subject: [PATCH 120/138] sm64ex: Fix Second Floor Door Cost (#909) --- worlds/sm64ex/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 46282fe316..d40c60a163 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -118,7 +118,7 @@ class SM64World(World): "AreaRando": self.area_connections, "FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[self.player].value, "BasementDoorCost": self.world.BasementStarDoorCost[self.player].value, - "SecondFloorCost": self.world.SecondFloorStarDoorCost[self.player].value, + "SecondFloorDoorCost": self.world.SecondFloorStarDoorCost[self.player].value, "MIPS1Cost": self.world.MIPS1Cost[self.player].value, "MIPS2Cost": self.world.MIPS2Cost[self.player].value, "StarsToFinish": self.world.StarsToFinish[self.player].value, From d48d775a594a4556a8a588863eccb75dcfa4c734 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 15 Aug 2022 22:53:59 +0200 Subject: [PATCH 121/138] Subnautica: fix 2 logic/locations bugs and add a bit of docs (#917) --- worlds/subnautica/Locations.py | 4 ++-- worlds/subnautica/__init__.py | 2 +- worlds/subnautica/docs/en_Subnautica.md | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index 2ce8cc1190..3effd1eac3 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -555,7 +555,7 @@ location_table: Dict[int, LocationDict] = { 'position': {'x': 348.7, 'y': -1443.5, 'z': -291.9}}, 33128: {'can_slip_through': False, 'name': 'Grassy Plateaus West Wreck - Beam PDA', - 'need_laser_cutter': True, + 'need_laser_cutter': False, 'position': {'x': -641.8, 'y': -111.3, 'z': -19.7}}, 33129: {'can_slip_through': False, 'name': 'Floating Island - Cave Entrance PDA', @@ -564,7 +564,7 @@ location_table: Dict[int, LocationDict] = { 33130: {'can_slip_through': False, 'name': 'Degasi Seabase - Jellyshroom Cave - Outside PDA', 'need_laser_cutter': False, - 'position': {'x': -83.2, 'y': -276.4, 'z': -345.5}}, + 'position': {'x': 83.2, 'y': -276.4, 'z': -345.5}}, } if False: # turn to True to export for Subnautica mod payload = {location_id: location_data["position"] for location_id, location_data in location_table.items()} diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index f36149b5ad..27e75eabad 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -42,7 +42,7 @@ class SubnauticaWorld(World): options = Options.options data_version = 5 - required_client_version = (0, 3, 3) + required_client_version = (0, 3, 4) prefill_items: List[Item] creatures_to_scan: List[str] diff --git a/worlds/subnautica/docs/en_Subnautica.md b/worlds/subnautica/docs/en_Subnautica.md index f71e14b7fe..9a112aa596 100644 --- a/worlds/subnautica/docs/en_Subnautica.md +++ b/worlds/subnautica/docs/en_Subnautica.md @@ -16,8 +16,12 @@ The goal remains unchanged. Cure the plague, build the Neptune Escape Rocket, an ## What items and locations get shuffled? -Most of the technologies the player will need throughout the game will be shuffled. Location checks in Subnautica are -data pads and technology lockers. +Most of the technologies the player will need throughout the game will be shuffled. +Location checks in Subnautica are data pads and technology lockers. + +Optionally up to 50 Creatures to scan can be included as well, for each one added a random duplicate item is created. + +As playing without Seaglide can be daunting, 2 of your Fragments of it can always be found in these locations: Grassy Plateaus South Wreck - Databox, Grassy Plateaus South Wreck - PDA, Grassy Plateaus West Wreck - Locker PDA, Grassy Plateaus West Wreck - Data Terminal, Safe Shallows Wreck - PDA, Kelp Forest Wreck - Databox, Kelp Forest Wreck - PDA, Lifepod 3 - Databox, Lifepod 3 - PDA, Lifepod 17 - PDA, Grassy Plateaus West Wreck - Beam PDA. ## Which items can be in another player's world? From f73b3d71bf34b4e08544460f36f801faaf3e55aa Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 15 Aug 2022 20:16:52 +0200 Subject: [PATCH 122/138] Factorio: fix typo --- worlds/factorio/data/mod_template/data-final-fixes.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 29bfa7276a..cc813b2fff 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -211,8 +211,8 @@ copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0] {%- endif -%} {#- connect Technology #} {%- if original_tech_name in tech_tree_layout_prerequisites %} -{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %} -table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-") +{%- for prerequisite in tech_tree_layout_prerequisites[original_tech_name] %} +table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequisite] }}-") {% endfor %} {% endif -%} {#- add new Technology to game #} From d10fbf82636056b9dca7d2321caa910a3c576499 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 15 Aug 2022 23:30:58 +0200 Subject: [PATCH 123/138] Minecraft: update requests --- worlds/minecraft/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/minecraft/requirements.txt b/worlds/minecraft/requirements.txt index a5590930fd..ddedb7c332 100644 --- a/worlds/minecraft/requirements.txt +++ b/worlds/minecraft/requirements.txt @@ -1 +1 @@ -requests >= 2.27.1 # used by client \ No newline at end of file +requests >= 2.28.1 # used by client \ No newline at end of file From 84841931513ff55db963df0cb33b378cf5712c7c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 15 Aug 2022 20:16:12 +0200 Subject: [PATCH 124/138] Core: crash if non_local pool is too big --- Fill.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Fill.py b/Fill.py index a5cf155ec1..e44c80e720 100644 --- a/Fill.py +++ b/Fill.py @@ -220,8 +220,8 @@ def distribute_items_restrictive(world: MultiWorld) -> None: world.push_item(defaultlocations.pop(i), item_to_place, False) break else: - logging.warning( - f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.") + raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. " + f"Too many non-local items for too few remaining locations.") world.random.shuffle(defaultlocations) From 81cf1508e0c15d9ccafbbbbd352ce1e449c25f8f Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 15 Aug 2022 16:46:59 -0500 Subject: [PATCH 125/138] Core: Refactor Autoworld.options to Autoworld.option_definitions (#906) * refactor `world.options` -> `world.option_definitions` * rename world api reference * missed some self.options --- BaseClasses.py | 6 +++--- Generate.py | 4 ++-- WebHostLib/options.py | 2 +- docs/world api.md | 8 ++++---- test/dungeons/TestDungeon.py | 2 +- test/general/__init__.py | 2 +- test/inverted/TestInverted.py | 2 +- test/inverted/TestInvertedBombRules.py | 2 +- test/inverted_minor_glitches/TestInvertedMinor.py | 2 +- test/inverted_owg/TestInvertedOWG.py | 2 +- test/minor_glitches/TestMinor.py | 2 +- test/owg/TestVanillaOWG.py | 2 +- test/vanilla/TestVanilla.py | 2 +- worlds/AutoWorld.py | 2 +- worlds/alttp/__init__.py | 2 +- worlds/checksfinder/__init__.py | 2 +- worlds/dark_souls_3/__init__.py | 2 +- worlds/dkc3/__init__.py | 2 +- worlds/factorio/__init__.py | 2 +- worlds/ff1/__init__.py | 2 +- worlds/hk/__init__.py | 4 ++-- worlds/meritous/__init__.py | 2 +- worlds/minecraft/__init__.py | 2 +- worlds/oot/__init__.py | 2 +- worlds/oribf/__init__.py | 2 +- worlds/raft/__init__.py | 2 +- worlds/rogue_legacy/__init__.py | 2 +- worlds/ror2/__init__.py | 2 +- worlds/sa2b/__init__.py | 2 +- worlds/sc2wol/__init__.py | 2 +- worlds/sm/__init__.py | 4 ++-- worlds/sm64ex/__init__.py | 2 +- worlds/smz3/__init__.py | 2 +- worlds/soe/__init__.py | 6 +++--- worlds/spire/__init__.py | 2 +- worlds/subnautica/__init__.py | 2 +- worlds/timespinner/__init__.py | 2 +- worlds/v6/__init__.py | 2 +- worlds/witness/__init__.py | 2 +- 39 files changed, 49 insertions(+), 49 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index aa37a097a6..cea1d48e6f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -166,7 +166,7 @@ class MultiWorld(): self.player_types[new_id] = NetUtils.SlotType.group self._region_cache[new_id] = {} world_type = AutoWorld.AutoWorldRegister.world_types[game] - for option_key, option in world_type.options.items(): + for option_key, option in world_type.option_definitions.items(): getattr(self, option_key)[new_id] = option(option.default) for option_key, option in Options.common_options.items(): getattr(self, option_key)[new_id] = option(option.default) @@ -204,7 +204,7 @@ class MultiWorld(): for player in self.player_ids: self.custom_data[player] = {} world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] - for option_key in world_type.options: + for option_key in world_type.option_definitions: setattr(self, option_key, getattr(args, option_key, {})) self.worlds[player] = world_type(self, player) @@ -1388,7 +1388,7 @@ class Spoiler(): outfile.write('Game: %s\n' % self.world.game[player]) for f_option, option in Options.per_game_common_options.items(): write_option(f_option, option) - options = self.world.worlds[player].options + options = self.world.worlds[player].option_definitions if options: for f_option, option in options.items(): write_option(f_option, option) diff --git a/Generate.py b/Generate.py index 70a8eaf667..1cad836345 100644 --- a/Generate.py +++ b/Generate.py @@ -396,7 +396,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: return get_choice(option_key, category_dict) if game in AutoWorldRegister.world_types: game_world = AutoWorldRegister.world_types[game] - options = ChainMap(game_world.options, Options.per_game_common_options) + options = ChainMap(game_world.option_definitions, Options.per_game_common_options) if option_key in options: if options[option_key].supports_weighting: return get_choice(option_key, category_dict) @@ -557,7 +557,7 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default))) if ret.game in AutoWorldRegister.world_types: - for option_key, option in world_type.options.items(): + for option_key, option in world_type.option_definitions.items(): handle_option(ret, game_weights, option_key, option) for option_key, option in Options.per_game_common_options.items(): # skip setting this option if already set from common_options, defaulting to root option diff --git a/WebHostLib/options.py b/WebHostLib/options.py index ccd1b27b3c..3c481be62b 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -60,7 +60,7 @@ def create(): for game_name, world in AutoWorldRegister.world_types.items(): - all_options = {**Options.per_game_common_options, **world.options} + all_options = {**Options.per_game_common_options, **world.option_definitions} res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( options=all_options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, diff --git a/docs/world api.md b/docs/world api.md index 4fa81f4aab..ffc0749e8c 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -86,7 +86,7 @@ inside a World object. Players provide customized settings for their World in the form of yamls. Those are accessible through `self.world.[self.player]`. A dict -of valid options has to be provided in `self.options`. Options are automatically +of valid options has to be provided in `self.option_definitions`. Options are automatically added to the `World` object for easy access. ### World Options @@ -252,7 +252,7 @@ to describe it and a `display_name` property for display on the website and in spoiler logs. The actual name as used in the yaml is defined in a `dict[str, Option]`, that is -assigned to the world under `self.options`. +assigned to the world under `self.option_definitions`. Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`. For more see `Options.py` in AP's base directory. @@ -328,7 +328,7 @@ from .Options import mygame_options # import the options dict class MyGameWorld(World): #... - options = mygame_options # assign the options dict to the world + option_definitions = mygame_options # assign the options dict to the world #... ``` @@ -365,7 +365,7 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation class MyGameWorld(World): """Insert description of the world/game here.""" game: str = "My Game" # name of the game/world - options = mygame_options # options the player can set + option_definitions = mygame_options # options the player can set topology_present: bool = True # show path to required location checks in spoiler remote_items: bool = False # True if all items come from the server remote_start_inventory: bool = False # True if start inventory comes from the server diff --git a/test/dungeons/TestDungeon.py b/test/dungeons/TestDungeon.py index c44c090f6f..0568e799f2 100644 --- a/test/dungeons/TestDungeon.py +++ b/test/dungeons/TestDungeon.py @@ -16,7 +16,7 @@ class TestDungeon(unittest.TestCase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/general/__init__.py b/test/general/__init__.py index 8b966c0e34..479f4af520 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -12,7 +12,7 @@ def setup_default_world(world_type) -> MultiWorld: world.player_name = {1: "Tester"} world.set_seed() args = Namespace() - for name, option in world_type.options.items(): + for name, option in world_type.option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) world.set_options(args) world.set_default_common_options() diff --git a/test/inverted/TestInverted.py b/test/inverted/TestInverted.py index 586eb57907..0c96f0b26d 100644 --- a/test/inverted/TestInverted.py +++ b/test/inverted/TestInverted.py @@ -16,7 +16,7 @@ class TestInverted(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/inverted/TestInvertedBombRules.py b/test/inverted/TestInvertedBombRules.py index cca252e3e1..f6afa9d0dc 100644 --- a/test/inverted/TestInvertedBombRules.py +++ b/test/inverted/TestInvertedBombRules.py @@ -17,7 +17,7 @@ class TestInvertedBombRules(unittest.TestCase): self.world = MultiWorld(1) self.world.mode[1] = "inverted" args = Namespace - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/inverted_minor_glitches/TestInvertedMinor.py b/test/inverted_minor_glitches/TestInvertedMinor.py index d737f21a07..42e7c942d6 100644 --- a/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/test/inverted_minor_glitches/TestInvertedMinor.py @@ -17,7 +17,7 @@ class TestInvertedMinor(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/inverted_owg/TestInvertedOWG.py b/test/inverted_owg/TestInvertedOWG.py index 7192fcb08b..064dd9e083 100644 --- a/test/inverted_owg/TestInvertedOWG.py +++ b/test/inverted_owg/TestInvertedOWG.py @@ -18,7 +18,7 @@ class TestInvertedOWG(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/minor_glitches/TestMinor.py b/test/minor_glitches/TestMinor.py index db77ee919c..81c09cfb27 100644 --- a/test/minor_glitches/TestMinor.py +++ b/test/minor_glitches/TestMinor.py @@ -17,7 +17,7 @@ class TestMinor(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index 68b10732bb..e5489117a7 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -18,7 +18,7 @@ class TestVanillaOWG(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index 4ffddc0747..e5ee73406a 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -16,7 +16,7 @@ class TestVanilla(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5cc7d62590..462108bb8f 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -111,7 +111,7 @@ class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. A Game should have its own subclass of World in which it defines the required data structures.""" - options: Dict[str, Option[Any]] = {} # link your Options mapping + option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping game: str # name the game topology_present: bool = False # indicate if world type has any meaningful layout/pathing diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 8f39b606e4..b43cfc29f4 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -110,7 +110,7 @@ class ALTTPWorld(World): Ganon! """ game: str = "A Link to the Past" - options = alttp_options + option_definitions = alttp_options topology_present = True item_name_groups = item_name_groups hint_blacklist = {"Triforce"} diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index d4a6f2aef3..ec9091c3d2 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -27,7 +27,7 @@ class ChecksFinderWorld(World): with the mines! You win when you get all your items and beat the board! """ game: str = "ChecksFinder" - options = checksfinder_options + option_definitions = checksfinder_options topology_present = True web = ChecksFinderWeb() diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 7245499e28..1ded4203c5 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -46,7 +46,7 @@ class DarkSouls3World(World): """ game: str = "Dark Souls III" - options = dark_souls_options + option_definitions = dark_souls_options topology_present: bool = True remote_items: bool = False remote_start_inventory: bool = False diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index 423693470d..f5b01ff723 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -38,7 +38,7 @@ class DKC3World(World): mystery of why Donkey Kong and Diddy disappeared while on vacation. """ game: str = "Donkey Kong Country 3" - options = dkc3_options + option_definitions = dkc3_options topology_present = False data_version = 1 #hint_blacklist = {LocationName.rocket_rush_flag} diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 9dc1febcba..26e761d4d3 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -193,7 +193,7 @@ class Factorio(World): return super(Factorio, self).collect_item(state, item, remove) - options = factorio_options + option_definitions = factorio_options @classmethod def stage_write_spoiler(cls, world, spoiler_handle): diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index d5a8dd30aa..0d731ace4b 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -27,7 +27,7 @@ class FF1World(World): Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made. """ - options = ff1_options + option_definitions = ff1_options game = "Final Fantasy" topology_present = False remote_items = True diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 6869e14b67..1667ab81f7 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -142,7 +142,7 @@ class HKWorld(World): As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils. """ # from https://www.hollowknight.com game: str = "Hollow Knight" - options = hollow_knight_options + option_definitions = hollow_knight_options web = HKWeb() @@ -435,7 +435,7 @@ class HKWorld(World): slot_data = {} options = slot_data["options"] = {} - for option_name in self.options: + for option_name in self.option_definitions: option = getattr(self.world, option_name)[self.player] try: optionvalue = int(option.value) diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 3a98bfe562..d0d076da40 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -49,7 +49,7 @@ class MeritousWorld(World): # NOTE: Remember to change this before this game goes live required_client_version = (0, 2, 4) - options = meritous_options + option_definitions = meritous_options def __init__(self, world: MultiWorld, player: int): super(MeritousWorld, self).__init__(world, player) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index e5dbe0b0cd..6e7addb2d0 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -58,7 +58,7 @@ class MinecraftWorld(World): victory! """ game: str = "Minecraft" - options = minecraft_options + option_definitions = minecraft_options topology_present = True web = MinecraftWebWorld() diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b640578c16..fb90b04e77 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -95,7 +95,7 @@ class OOTWorld(World): to rescue the Seven Sages, and then confront Ganondorf to save Hyrule! """ game: str = "Ocarina of Time" - options: dict = oot_options + option_definitions: dict = oot_options topology_present: bool = True item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if data[2] is not None} diff --git a/worlds/oribf/__init__.py b/worlds/oribf/__init__.py index 33f8d4b07e..05d237659c 100644 --- a/worlds/oribf/__init__.py +++ b/worlds/oribf/__init__.py @@ -17,7 +17,7 @@ class OriBlindForest(World): item_name_to_id = item_table location_name_to_id = lookup_name_to_id - options = options + option_definitions = options hidden = True diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index cf4b7975e5..da4b58f24f 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -37,7 +37,7 @@ class RaftWorld(World): lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values())) location_name_to_id = locations_lookup_name_to_id - options = raft_options + option_definitions = raft_options data_version = 2 required_client_version = (0, 3, 4) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index af8fa9a791..ba58e133c1 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -30,7 +30,7 @@ class LegacyWorld(World): But that's OK, because no one is perfect, and you don't have to be to succeed. """ game: str = "Rogue Legacy" - options = legacy_options + option_definitions = legacy_options topology_present = False data_version = 3 required_client_version = (0, 2, 3) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 1a7060786f..b1f3aa9307 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -28,7 +28,7 @@ class RiskOfRainWorld(World): first crash landing. """ game: str = "Risk of Rain 2" - options = ror2_options + option_definitions = ror2_options topology_present = False item_name_to_id = item_table diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index ffff2a93ea..84a38f221c 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -49,7 +49,7 @@ class SA2BWorld(World): Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rogue, and Eggman across 31 stages and prevent the destruction of the earth. """ game: str = "Sonic Adventure 2 Battle" - options = sa2b_options + option_definitions = sa2b_options topology_present = False data_version = 2 diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 5d48c9c0f4..33522569d5 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -37,7 +37,7 @@ class SC2WoLWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {location.name: location.code for location in get_locations(None, None)} - options = sc2wol_options + option_definitions = sc2wol_options item_name_groups = item_name_groups locked_locations: typing.List[str] diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 8954e2b5f7..5da1c40f75 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -79,7 +79,7 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True data_version = 1 - options = sm_options + option_definitions = sm_options item_names: Set[str] = frozenset(items_lookup_name_to_id) location_names: Set[str] = frozenset(locations_lookup_name_to_id) item_name_to_id = items_lookup_name_to_id @@ -567,7 +567,7 @@ class SMWorld(World): def fill_slot_data(self): slot_data = {} if not self.world.is_race: - for option_name in self.options: + for option_name in self.option_definitions: option = getattr(self.world, option_name)[self.player] slot_data[option_name] = option.value diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index d40c60a163..e0f911fbd9 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -39,7 +39,7 @@ class SM64World(World): area_connections: typing.Dict[int, int] - options = sm64_options + option_definitions = sm64_options def generate_early(self): self.topology_present = self.world.AreaRandomizer[self.player].value diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 2849567d33..732a8b5548 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -62,7 +62,7 @@ class SMZ3World(World): game: str = "SMZ3" topology_present = False data_version = 2 - options = smz3_options + option_definitions = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] item_name_to_id = TotalSMZ3Item.lookup_name_to_id diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index d708d6d7d3..f86fc48e93 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -151,7 +151,7 @@ class SoEWorld(World): space station where the final boss must be defeated. """ game: str = "Secret of Evermore" - options = soe_options + option_definitions = soe_options topology_present = False remote_items = False data_version = 3 @@ -162,7 +162,7 @@ class SoEWorld(World): location_name_to_id, location_id_to_raw = _get_location_mapping() item_name_groups = _get_item_grouping() - trap_types = [name[12:] for name in options if name.startswith('trap_chance_')] + trap_types = [name[12:] for name in option_definitions if name.startswith('trap_chance_')] evermizer_seed: int connect_name: str @@ -339,7 +339,7 @@ class SoEWorld(World): placement_file = out_base + '.txt' patch_file = out_base + '.apsoe' flags = 'l' # spoiler log - for option_name in self.options: + for option_name in self.option_definitions: option = getattr(self.world, option_name)[self.player] if hasattr(option, 'to_flag'): flags += option.to_flag() diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 4d2917aab9..476afad8d9 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -27,7 +27,7 @@ class SpireWorld(World): immense power, and Slay the Spire! """ - options = spire_options + option_definitions = spire_options game = "Slay the Spire" topology_present = False data_version = 1 diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 27e75eabad..6fa064d53a 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -39,7 +39,7 @@ class SubnauticaWorld(World): item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} location_name_to_id = all_locations - options = Options.options + option_definitions = Options.options data_version = 5 required_client_version = (0, 3, 4) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index d789e9ddef..c8b94a2763 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -40,7 +40,7 @@ class TimespinnerWorld(World): Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family. """ - options = timespinner_options + option_definitions = timespinner_options game = "Timespinner" topology_present = True remote_items = False diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 4959ddca1b..38690e5a00 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -41,7 +41,7 @@ class V6World(World): music_map: typing.Dict[int,int] - options = v6_options + option_definitions = v6_options def create_regions(self): create_regions(self.world,self.player) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index da6683b51c..19c9b97240 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -41,7 +41,7 @@ class WitnessWorld(World): static_locat = StaticWitnessLocations() static_items = StaticWitnessItems() web = WitnessWebWorld() - options = the_witness_options + option_definitions = the_witness_options item_name_to_id = { name: data.code for name, data in static_items.ALL_ITEM_TABLE.items() From 086295adbb67c2d229cbd8c18aeb767fe79a1164 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 15 Aug 2022 23:47:32 +0200 Subject: [PATCH 126/138] AutoWorld: add preliminary .apworld specification (#903) * AutoWorld: add preliminary .apworld specification * Doc: apworld specification: fix typo --- docs/apworld specification.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/apworld specification.md diff --git a/docs/apworld specification.md b/docs/apworld specification.md new file mode 100644 index 0000000000..2dcc3f0bef --- /dev/null +++ b/docs/apworld specification.md @@ -0,0 +1,25 @@ +# apworld Specification + +Archipelago depends on worlds to provide game-specific details like items, locations and output generation. +Those are located in the `worlds/` folder (source) or `/lib/worlds/` (when installed). +See [world api.md](world api.md) for details. + +apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld` +file into the worlds folder. + + +## File Format + +apworld files are zip archives with the case-sensitive file ending `.apworld`. +The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in +the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`. + + +## Metadata + +No metadata is specified yet. + + +## Extra Data + +The zip can contain arbitrary files in addition what was specified above. From ca83905d9f10a50c326b4736f6995f041f2905d7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 15 Aug 2022 23:52:03 +0200 Subject: [PATCH 127/138] Core: allow loading worlds from zip modules (#747) * Core: allow loading worlds from zip modules RoR2: make it zipimport compatible (remove relative imports beyond local top-level) * WebHost: add support for .apworld --- FactorioClient.py | 3 +-- WebHost.py | 34 +++++++++++++++++++----- worlds/AutoWorld.py | 52 ++++++++++++++++++++++--------------- worlds/__init__.py | 57 +++++++++++++++++++++++++++++++---------- worlds/ror2/Rules.py | 2 +- worlds/ror2/__init__.py | 2 +- 6 files changed, 104 insertions(+), 46 deletions(-) diff --git a/FactorioClient.py b/FactorioClient.py index 2fa8ba9c15..6797578a3a 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -20,8 +20,7 @@ import Utils if __name__ == "__main__": Utils.init_logging("FactorioClient", exception_logger="Client") -from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \ - get_base_parser +from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser from MultiServer import mark_raw from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart diff --git a/WebHost.py b/WebHost.py index 09f8d8235a..3d3c8678e2 100644 --- a/WebHost.py +++ b/WebHost.py @@ -42,20 +42,40 @@ def get_app(): def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]: import json import shutil - from worlds.AutoWorld import AutoWorldRegister + import pathlib + import zipfile + + zfile: zipfile.ZipInfo + + from worlds.AutoWorld import AutoWorldRegister, __file__ worlds = {} data = [] for game, world in AutoWorldRegister.world_types.items(): if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'): worlds[game] = world + + base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs") for game, world in worlds.items(): # copy files from world's docs folder to the generated folder - source_path = Utils.local_path(os.path.dirname(sys.modules[world.__module__].__file__), 'docs') - target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game) - files = os.listdir(source_path) - for file in files: - os.makedirs(os.path.dirname(Utils.local_path(target_path, file)), exist_ok=True) - shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file)) + target_path = os.path.join(base_target_path, game) + os.makedirs(target_path, exist_ok=True) + + if world.is_zip: + zipfile_path = pathlib.Path(world.__file__).parents[1] + + assert os.path.isfile(zipfile_path), f"{zipfile_path} is not a valid file(path)." + assert zipfile.is_zipfile(zipfile_path), f"{zipfile_path} is not a valid zipfile." + + with zipfile.ZipFile(zipfile_path) as zf: + for zfile in zf.infolist(): + if not zfile.is_dir() and "/docs/" in zfile.filename: + zf.extract(zfile, target_path) + else: + source_path = Utils.local_path(os.path.dirname(world.__file__), "docs") + files = os.listdir(source_path) + for file in files: + shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file)) + # build a json tutorial dict per game game_data = {'gameTitle': game, 'tutorials': []} for tutorial in world.web.tutorials: diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 462108bb8f..2abb9f3a71 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -2,10 +2,13 @@ from __future__ import annotations import logging import sys -from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, NamedTuple +from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING -from BaseClasses import MultiWorld, Item, CollectionState, Location, Tutorial from Options import Option +from BaseClasses import CollectionState + +if TYPE_CHECKING: + from BaseClasses import MultiWorld, Item, Location, Tutorial class AutoWorldRegister(type): @@ -41,8 +44,11 @@ class AutoWorldRegister(type): # construct class new_class = super().__new__(mcs, name, bases, dct) if "game" in dct: + if dct["game"] in AutoWorldRegister.world_types: + raise RuntimeError(f"""Game {dct["game"]} already registered.""") AutoWorldRegister.world_types[dct["game"]] = new_class new_class.__file__ = sys.modules[new_class.__module__].__file__ + new_class.is_zip = ".apworld" in new_class.__file__ return new_class @@ -62,12 +68,12 @@ class AutoLogicRegister(type): return new_class -def call_single(world: MultiWorld, method_name: str, player: int, *args: Any) -> Any: +def call_single(world: "MultiWorld", method_name: str, player: int, *args: Any) -> Any: method = getattr(world.worlds[player], method_name) return method(*args) -def call_all(world: MultiWorld, method_name: str, *args: Any) -> None: +def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None: world_types: Set[AutoWorldRegister] = set() for player in world.player_ids: world_types.add(world.worlds[player].__class__) @@ -79,7 +85,7 @@ def call_all(world: MultiWorld, method_name: str, *args: Any) -> None: stage_callable(world, *args) -def call_stage(world: MultiWorld, method_name: str, *args: Any) -> None: +def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None: world_types = {world.worlds[player].__class__ for player in world.player_ids} for world_type in world_types: stage_callable = getattr(world_type, f"stage_{method_name}", None) @@ -97,7 +103,7 @@ class WebWorld: # docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial # class is to be used for one guide. - tutorials: List[Tutorial] + tutorials: List["Tutorial"] # Choose a theme for your /game/* pages # Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone @@ -159,8 +165,11 @@ class World(metaclass=AutoWorldRegister): # Hide World Type from various views. Does not remove functionality. hidden: bool = False + # see WebWorld for options + web: WebWorld = WebWorld() + # autoset on creation: - world: MultiWorld + world: "MultiWorld" player: int # automatically generated @@ -170,9 +179,10 @@ class World(metaclass=AutoWorldRegister): item_names: Set[str] # set of all potential item names location_names: Set[str] # set of all potential location names - web: WebWorld = WebWorld() + is_zip: bool # was loaded from a .apworld ? + __file__: str # path it was loaded from - def __init__(self, world: MultiWorld, player: int): + def __init__(self, world: "MultiWorld", player: int): self.world = world self.player = player @@ -207,12 +217,12 @@ class World(metaclass=AutoWorldRegister): @classmethod def fill_hook(cls, - progitempool: List[Item], - nonexcludeditempool: List[Item], - localrestitempool: Dict[int, List[Item]], - nonlocalrestitempool: Dict[int, List[Item]], - restitempool: List[Item], - fill_locations: List[Location]) -> None: + progitempool: List["Item"], + nonexcludeditempool: List["Item"], + localrestitempool: Dict[int, List["Item"]], + nonlocalrestitempool: Dict[int, List["Item"]], + restitempool: List["Item"], + fill_locations: List["Location"]) -> None: """Special method that gets called as part of distribute_items_restrictive (main fill). This gets called once per present world type.""" pass @@ -250,7 +260,7 @@ class World(metaclass=AutoWorldRegister): # end of ordered Main.py calls - def create_item(self, name: str) -> Item: + def create_item(self, name: str) -> "Item": """Create an item for this world type and player. Warning: this may be called with self.world = None, for example by MultiServer""" raise NotImplementedError @@ -261,7 +271,7 @@ class World(metaclass=AutoWorldRegister): return self.world.random.choice(tuple(self.item_name_to_id.keys())) # decent place to implement progressive items, in most cases can stay as-is - def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]: + def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]: """Collect an item name into state. For speed reasons items that aren't logically useful get skipped. Collect None to skip item. :param state: CollectionState to collect into @@ -272,18 +282,18 @@ class World(metaclass=AutoWorldRegister): return None # called to create all_state, return Items that are created during pre_fill - def get_pre_fill_items(self) -> List[Item]: + def get_pre_fill_items(self) -> List["Item"]: return [] # following methods should not need to be overridden. - def collect(self, state: CollectionState, item: Item) -> bool: + def collect(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item) if name: state.prog_items[name, self.player] += 1 return True return False - def remove(self, state: CollectionState, item: Item) -> bool: + def remove(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item, True) if name: state.prog_items[name, self.player] -= 1 @@ -292,7 +302,7 @@ class World(metaclass=AutoWorldRegister): return True return False - def create_filler(self) -> Item: + def create_filler(self) -> "Item": return self.create_item(self.get_filler_item_name()) diff --git a/worlds/__init__.py b/worlds/__init__.py index b927083679..caa170d5c6 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -1,29 +1,57 @@ import importlib +import zipimport import os +import typing -__all__ = {"lookup_any_item_id_to_name", - "lookup_any_location_id_to_name", - "network_data_package", - "AutoWorldRegister"} +folder = os.path.dirname(__file__) + +__all__ = { + "lookup_any_item_id_to_name", + "lookup_any_location_id_to_name", + "network_data_package", + "AutoWorldRegister", + "world_sources", + "folder", +} + +if typing.TYPE_CHECKING: + from .AutoWorld import World + + +class WorldSource(typing.NamedTuple): + path: str # typically relative path from this module + is_zip: bool = False + + +# find potential world containers, currently folders and zip-importable .apworld's +world_sources: typing.List[WorldSource] = [] +file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly +for file in os.scandir(folder): + if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders + if file.is_dir(): + world_sources.append(WorldSource(file.name)) + elif file.is_file() and file.name.endswith(".apworld"): + world_sources.append(WorldSource(file.name, is_zip=True)) # import all submodules to trigger AutoWorldRegister -world_folders = [] -for file in os.scandir(os.path.dirname(__file__)): - if file.is_dir(): - world_folders.append(file.name) -world_folders.sort() -for world in world_folders: - if not world.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders - importlib.import_module(f".{world}", "worlds") +world_sources.sort() +for world_source in world_sources: + if world_source.is_zip: + + importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) + importer.load_module(world_source.path.split(".", 1)[0]) + else: + importlib.import_module(f".{world_source.path}", "worlds") -from .AutoWorld import AutoWorldRegister lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {} games = {} +from .AutoWorld import AutoWorldRegister + for world_name, world in AutoWorldRegister.world_types.items(): games[world_name] = { - "item_name_to_id" : world.item_name_to_id, + "item_name_to_id": world.item_name_to_id, "location_name_to_id": world.location_name_to_id, "version": world.data_version, # seems clients don't actually want this. Keeping it here in case someone changes their mind. @@ -41,5 +69,6 @@ network_data_package = { if any(not world.data_version for world in AutoWorldRegister.world_types.values()): network_data_package["version"] = 0 import logging + logging.warning(f"Datapackage is in custom mode. Custom Worlds: " f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}") diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py index 05c08c8803..64d741f99f 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/Rules.py @@ -1,5 +1,5 @@ from BaseClasses import MultiWorld -from ..generic.Rules import set_rule, add_rule +from worlds.generic.Rules import set_rule, add_rule def set_rules(world: MultiWorld, player: int): diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index b1f3aa9307..9d0d693b61 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -5,7 +5,7 @@ from .Rules import set_rules from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial from .Options import ror2_options -from ..AutoWorld import World, WebWorld +from worlds.AutoWorld import World, WebWorld client_version = 1 From 09afdc25539b5763c49aeeb28df475c84879eac7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 16 Aug 2022 23:57:26 +0200 Subject: [PATCH 128/138] Webhost: prevent tracker crashes with LttP key itemlinks (#922) --- WebHostLib/tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 4179478985..834642849f 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -987,8 +987,8 @@ def getTracker(tracker: UUID): if game_state == 30: inventory[team][player][106] = 1 # Triforce - player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups} - player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups} + player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} + player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} for loc_data in locations.values(): for values in loc_data.values(): item_id, item_player, flags = values From d426226bce6a38947f4eff8c1f09bf102fef6858 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 16 Aug 2022 02:40:05 +0200 Subject: [PATCH 129/138] LttP: run optimize imports on __init__ --- worlds/alttp/__init__.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index b43cfc29f4..e7f111c3b7 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -1,26 +1,23 @@ -import random import logging import os +import random import threading import typing from BaseClasses import Item, CollectionState, Tutorial -from .SubClasses import ALttPItem -from ..AutoWorld import World, WebWorld, LogicMixin -from .Options import alttp_options, smallkey_shuffle -from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem -from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions -from .Rules import set_rules -from .ItemPool import generate_itempool, difficulties -from .Shops import create_shops, ShopSlotFill from .Dungeons import create_dungeons +from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect +from .InvertedRegions import create_inverted_regions, mark_dark_world_regions +from .ItemPool import generate_itempool, difficulties +from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem +from .Options import alttp_options, smallkey_shuffle +from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch -import Patch -from itertools import chain - -from .InvertedRegions import create_inverted_regions, mark_dark_world_regions -from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect +from .Rules import set_rules +from .Shops import create_shops, ShopSlotFill +from .SubClasses import ALttPItem +from ..AutoWorld import World, WebWorld, LogicMixin lttp_logger = logging.getLogger("A Link to the Past") From 431a9b70233a42a541a695e3d9625d368cc1e68a Mon Sep 17 00:00:00 2001 From: Joethepic <60947591+Joethepic@users.noreply.github.com> Date: Wed, 17 Aug 2022 02:59:22 -0500 Subject: [PATCH 130/138] Docs: Mc: fix version in setup guide (#873) Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> --- worlds/minecraft/docs/minecraft_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/minecraft/docs/minecraft_en.md b/worlds/minecraft/docs/minecraft_en.md index b33710f73c..e8b1a3642e 100644 --- a/worlds/minecraft/docs/minecraft_en.md +++ b/worlds/minecraft/docs/minecraft_en.md @@ -33,12 +33,12 @@ leave this window open as this is your server console. ### Connect to the MultiServer -Using Minecraft 1.18.2 connect to the server `localhost`. +Open Minecraft, go to `Multiplayer > Direct Connection`, and join the `localhost` server address. If you are using the website to host the game then it should auto-connect to the AP server without the need to `/connect` otherwise once you are in game type `/connect (Port) (Password)` where `` is the address of the -Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. +Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. Note that there is no colon between `` and `(Port)`. `(Password)` is only required if the Archipelago server you are using has a password set. ### Play the game From 6602c580f4ba7b26f61748552cb593694cdd3d0f Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 17 Aug 2022 07:16:14 -0700 Subject: [PATCH 131/138] Fix another item.type crash bug. (#927) * Fix another item.type crash bug. * Another location that can crash, in the instance of plando fixed. --- worlds/oot/HintList.py | 3 ++- worlds/oot/Patches.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/oot/HintList.py b/worlds/oot/HintList.py index 06af1a9be3..7fc298b0d7 100644 --- a/worlds/oot/HintList.py +++ b/worlds/oot/HintList.py @@ -1,6 +1,7 @@ import random from BaseClasses import LocationProgressType +from .Items import OOTItem # Abbreviations # DMC Death Mountain Crater @@ -1260,7 +1261,7 @@ def hintExclusions(world, clear_cache=False): world.hint_exclusions = [] for location in world.get_locations(): - if (location.locked and (location.item.type != 'Song' or world.shuffle_song_items != 'song')) or location.progress_type == LocationProgressType.EXCLUDED: + if (location.locked and ((isinstance(location.item, OOTItem) and location.item.type != 'Song') or world.shuffle_song_items != 'song')) or location.progress_type == LocationProgressType.EXCLUDED: world.hint_exclusions.append(location.name) world_location_names = [ diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 91f656b4e9..7bf31c4f7a 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -2104,7 +2104,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F shop_objs = { 0x0148 } # "Sold Out" object for location in locations: - if location.item.type == 'Shop': + if isinstance(location.item, OOTItem) and location.item.type == 'Shop': shop_objs.add(location.item.special['object']) rom.write_int16(location.address1, location.item.index) else: From 22c8153ba852cf25fa87bd6186219dfd61948362 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 17 Aug 2022 21:33:13 +0200 Subject: [PATCH 132/138] WebHost: fix indentation in tracker.py --- WebHostLib/tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 834642849f..fb5df81c9a 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -990,7 +990,7 @@ def getTracker(tracker: UUID): player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} for loc_data in locations.values(): - for values in loc_data.values(): + for values in loc_data.values(): item_id, item_player, flags = values if item_id in ids_big_key: From d0faa36eef53dab474b60897937274d0bbbbe617 Mon Sep 17 00:00:00 2001 From: Henrique Gemignani Passos Lima Date: Thu, 18 Aug 2022 01:10:33 +0200 Subject: [PATCH 133/138] Fix CommonClient.server_loop with nogui When running client without a gui, ctx.ui is None --- CommonClient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CommonClient.py b/CommonClient.py index 0b2c22cfd8..f830035425 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -493,7 +493,8 @@ async def server_loop(ctx: CommonContext, address=None): logger.info(f'Connecting to Archipelago server at {address}') try: socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None) - ctx.ui.update_address_bar(server_url.netloc) + if ctx.ui is not None: + ctx.ui.update_address_bar(server_url.netloc) ctx.server = Endpoint(socket) logger.info('Connected') ctx.server_address = address From a1aa9c17ffcb82d8fcd2e6547ef95843902628d8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 18 Aug 2022 00:27:37 +0200 Subject: [PATCH 134/138] Core: convert is_zip to zip_path --- Utils.py | 4 ++++ WebHost.py | 7 +++---- worlds/AutoWorld.py | 10 ++++++---- worlds/__init__.py | 1 - 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Utils.py b/Utils.py index bb29166f75..c621e31c9a 100644 --- a/Utils.py +++ b/Utils.py @@ -422,6 +422,10 @@ def get_text_between(text: str, start: str, end: str) -> str: return text[text.index(start) + len(start): text.rindex(end)] +def get_text_after(text: str, start: str) -> str: + return text[text.index(start) + len(start):] + + loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG} diff --git a/WebHost.py b/WebHost.py index 3d3c8678e2..db802193a6 100644 --- a/WebHost.py +++ b/WebHost.py @@ -42,12 +42,11 @@ def get_app(): def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]: import json import shutil - import pathlib import zipfile zfile: zipfile.ZipInfo - from worlds.AutoWorld import AutoWorldRegister, __file__ + from worlds.AutoWorld import AutoWorldRegister worlds = {} data = [] for game, world in AutoWorldRegister.world_types.items(): @@ -60,8 +59,8 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] target_path = os.path.join(base_target_path, game) os.makedirs(target_path, exist_ok=True) - if world.is_zip: - zipfile_path = pathlib.Path(world.__file__).parents[1] + if world.zip_path: + zipfile_path = world.zip_path assert os.path.isfile(zipfile_path), f"{zipfile_path} is not a valid file(path)." assert zipfile.is_zipfile(zipfile_path), f"{zipfile_path} is not a valid zipfile." diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 2abb9f3a71..1ca5b53422 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging import sys +import pathlib from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING from Options import Option @@ -48,13 +49,14 @@ class AutoWorldRegister(type): raise RuntimeError(f"""Game {dct["game"]} already registered.""") AutoWorldRegister.world_types[dct["game"]] = new_class new_class.__file__ = sys.modules[new_class.__module__].__file__ - new_class.is_zip = ".apworld" in new_class.__file__ + if ".apworld" in new_class.__file__: + new_class.zip_path = pathlib.Path(new_class.__file__).parents[1] return new_class class AutoLogicRegister(type): - def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister: - new_class = super().__new__(cls, name, bases, dct) + def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister: + new_class = super().__new__(mcs, name, bases, dct) function: Callable[..., Any] for item_name, function in dct.items(): if item_name == "copy_mixin": @@ -179,7 +181,7 @@ class World(metaclass=AutoWorldRegister): item_names: Set[str] # set of all potential item names location_names: Set[str] # set of all potential location names - is_zip: bool # was loaded from a .apworld ? + zip_path: Optional[pathlib.Path] = None # If loaded from a .apworld, this is the Path to it. __file__: str # path it was loaded from def __init__(self, world: "MultiWorld", player: int): diff --git a/worlds/__init__.py b/worlds/__init__.py index caa170d5c6..46b383b303 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -37,7 +37,6 @@ for file in os.scandir(folder): world_sources.sort() for world_source in world_sources: if world_source.is_zip: - importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) importer.load_module(world_source.path.split(".", 1)[0]) else: From 0d61192c675f70f6dec78e970e1c1f01dd254945 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 18 Aug 2022 01:33:40 +0200 Subject: [PATCH 135/138] Factorio: make apworld compatible(#935) --- worlds/factorio/Mod.py | 25 ++++++++++++++++++++++--- worlds/factorio/Shapes.py | 2 +- worlds/factorio/Technologies.py | 4 ++-- worlds/factorio/__init__.py | 2 +- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 8c7dcf669c..37c503b047 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -78,9 +78,14 @@ def generate_mod(world, output_directory: str): global data_final_template, locale_template, control_template, data_template, settings_template with template_load_lock: if not data_final_template: - mod_template_folder = os.path.join(os.path.dirname(__file__), "data", "mod_template") + def load_template(name: str): + import pkgutil + data = pkgutil.get_data(__name__, "data/mod_template/" + name).decode() + return data, name, lambda: False + template_env: Optional[jinja2.Environment] = \ - jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder])) + jinja2.Environment(loader=jinja2.FunctionLoader(load_template)) + data_template = template_env.get_template("data.lua") data_final_template = template_env.get_template("data-final-fixes.lua") locale_template = template_env.get_template(r"locale/en/locale.cfg") @@ -158,7 +163,21 @@ def generate_mod(world, output_directory: str): mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) en_locale_dir = os.path.join(mod_dir, "locale", "en") os.makedirs(en_locale_dir, exist_ok=True) - shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) + + if world.zip_path: + # Maybe investigate read from zip, write to zip, without temp file? + with zipfile.ZipFile(world.zip_path) as zf: + for file in zf.infolist(): + if not file.is_dir() and "/data/mod/" in file.filename: + path_part = Utils.get_text_after(file.filename, "/data/mod/") + target = os.path.join(mod_dir, path_part) + os.makedirs(os.path.split(target)[0], exist_ok=True) + + with open(target, "wb") as f: + f.write(zf.read(file)) + else: + shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) + with open(os.path.join(mod_dir, "data.lua"), "wt") as f: f.write(data_template_code) with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f: diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index eaf44ac1ba..f42da4d20c 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -1,7 +1,7 @@ from typing import Dict, List, Set from collections import deque -from worlds.factorio.Options import TechTreeLayout +from .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 f89dc53ee3..b88cc9b1ad 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -19,8 +19,8 @@ pool = ThreadPoolExecutor(1) def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: - with open(os.path.join(source_folder, f"{data_name}.json")) as f: - return json.load(f) + import pkgutil + return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode()) techs_future = pool.submit(load_json_data, "techs") diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 26e761d4d3..a01abac748 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,7 +1,7 @@ import collections import typing -from ..AutoWorld import World, WebWorld +from worlds.AutoWorld import World, WebWorld from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ From 0ac67bfe7617ab0aabf2540d43a5a6c12dc5c5e5 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Fri, 19 Aug 2022 09:02:39 -0400 Subject: [PATCH 136/138] Smz3 early sword fix (#939) --- worlds/smz3/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 732a8b5548..15ac85c7c3 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -590,8 +590,6 @@ class SMZ3World(World): # /* Check Swords option and place as needed */ if self.smz3World.Config.SwordLocation == SwordLocation.Uncle: self.FillItemAtLocation(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword, self.smz3World.GetLocation("Link's Uncle")) - elif self.smz3World.Config.SwordLocation == SwordLocation.Early: - self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword) # /* Check Morph option and place as needed */ if self.smz3World.Config.MorphLocation == MorphLocation.Original: @@ -599,6 +597,10 @@ class SMZ3World(World): elif self.smz3World.Config.MorphLocation == MorphLocation.Early: self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.Morph) + # We do early Sword placement after Morph in case its Original location + if self.smz3World.Config.SwordLocation == SwordLocation.Early: + self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword) + # /* We place a PB and Super in Sphere 1 to make sure the filler # * doesn't start locking items behind this when there are a # * high chance of the trash fill actually making them available */ From 89ab4aff9cd4aef8e23be76a4bbab99475fe67ee Mon Sep 17 00:00:00 2001 From: TheCondor07 Date: Fri, 19 Aug 2022 13:50:44 -0700 Subject: [PATCH 137/138] SC2: Logic changes and fixes, 6 new locations, 2 removed locations (#933) --- worlds/sc2wol/Items.py | 28 ++++---- worlds/sc2wol/Locations.py | 133 ++++++++++++++++++++++-------------- worlds/sc2wol/LogicMixin.py | 21 ++++-- worlds/sc2wol/Regions.py | 5 +- worlds/sc2wol/__init__.py | 2 +- 5 files changed, 116 insertions(+), 73 deletions(-) diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 4ecff7e15f..59b59bc137 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -49,27 +49,27 @@ item_table = { "Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0), "Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1), - "Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2), + "Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler), "Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3), "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4), "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5), - "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6), + "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler), "Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7), "Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8), - "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9), - "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10), - "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11), + "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression), + "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression), + "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression), "Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler), "Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13), "Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14), "Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15), "U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16), - "G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.filler), + "G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression), "Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler), "Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1), - "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2), - "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3), + "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler), + "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler), "Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4), "Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5), "Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler), @@ -77,9 +77,9 @@ item_table = { "Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8), "Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9), "Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler), - "Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11), + "Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler), "Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler), - "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13), + "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler), "Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14), "Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15), "Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler), @@ -88,7 +88,7 @@ item_table = { "Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler), "Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20), "Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21), - "Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22), + "Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression), "Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23), "330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler), "Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler), @@ -97,12 +97,12 @@ item_table = { "Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression), "Sensor Tower": ItemData(402 + SC2WOL_ITEM_ID_OFFSET, "Building", 2), - "War Pigs": ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0), + "War Pigs": ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0, classification=ItemClassification.progression), "Devil Dogs": ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1, classification=ItemClassification.filler), "Hammer Securities": ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2), - "Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3), + "Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3, classification=ItemClassification.progression), "Siege Breakers": ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4), - "Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5), + "Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5, classification=ItemClassification.progression), "Dusk Wings": ItemData(506 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 6), "Jackson's Revenge": ItemData(507 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 7), diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index 92dfb033c0..3425dc7199 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -44,7 +44,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Zero Hour", "Beat Zero Hour", None, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + lambda state: state._sc2wol_has_anti_air(world, player)), LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401), LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402, lambda state: state._sc2wol_has_common_unit(world, player)), @@ -52,7 +52,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Evacuation", "Beat Evacuation", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, @@ -63,19 +63,37 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Safe Haven", "Beat Safe Haven", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Haven's Fall", "Beat Haven's Fall", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801), LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802), LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, @@ -101,11 +119,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L state._sc2wol_has_anti_air(world, player) and state._sc2wol_has_heavy_defense(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, - lambda state: state._sc2wol_has_air(world, player)), - LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001, - lambda state: True), - LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, - lambda state: state._sc2wol_has_air(world, player)), + lambda state: state._sc2wol_has_air(world, player) and state._sc2wol_has_anti_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, @@ -121,33 +135,46 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("The Moebius Factor", "Beat The Moebius Factor", None, lambda state: state._sc2wol_has_air(world, player)), LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), - LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, - lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102), LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Beat Supernova", None, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201), - LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202), + LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Beat Maw of the Void", None, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301), @@ -157,17 +184,17 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401), LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Welcome to the Jungle", "Beat Welcome to the Jungle", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500), LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501), LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502), @@ -180,7 +207,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605), LocationData("Ghost of a Chance", "Beat Ghost of a Chance", None), LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, - lambda state: state._sc2wol_has_train_killers(world, player)), + lambda state: state._sc2wol_has_train_killers(world, player) and + state._sc2wol_has_anti_air(world, player)), LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701), LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702), LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703), @@ -198,20 +226,20 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Cutthroat", "Beat Cutthroat", None, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + lambda state: state._sc2wol_has_competent_anti_air(world, player)), LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901), LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903), LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Beat Engine of Destruction", None, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, lambda state: state._sc2wol_has_competent_comp(world, player)), @@ -224,13 +252,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004), LocationData("Media Blitz", "Beat Media Blitz", None, lambda state: state._sc2wol_has_competent_comp(world, player)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100), + LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102), - LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103), - LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105), - LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None), + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200), LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201), LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202), @@ -251,13 +285,12 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_protoss_common_units(world, player)), LocationData("Echoes of the Future", "Beat Echoes of the Future", None, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), - LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500), LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), - LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502), - LocationData("In Utter Darkness", "Beat In Utter Darkness", None, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, + lambda state: state._sc2wol_has_protoss_common_units(world, player)), + LocationData("In Utter Darkness", "Beat In Utter Darkness", None), LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index baf77dc677..7a08142672 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -10,16 +10,17 @@ class SC2WoLLogic(LogicMixin): return self.has_any({'Marine', 'Marauder'}, player) def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Viking', 'Wraith', 'Medivac', 'Banshee', 'Hercules'}, player) + return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \ + self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player) def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Viking', 'Wraith'}, player) - def _sc2wol_has_mobile_anti_air(self, world: MultiWorld, player: int) -> bool: + def _sc2wol_has_competent_anti_air(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player) def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool: - return self.has('Missile Turret', player) or self._sc2wol_has_mobile_anti_air(world, player) + return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, player) or self._sc2wol_has_competent_anti_air(world, player) def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool: return (self.has_any({'Siege Tank', 'Vulture'}, player) or @@ -28,13 +29,15 @@ class SC2WoLLogic(LogicMixin): def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool: return (self.has('Marine', player) or self.has('Marauder', player) and - self._sc2wol_has_mobile_anti_air(world, player)) and self.has_any({'Medivac', 'Medic'}, player) or \ - self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_mobile_anti_air(world, player) or \ - self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(world, player) + self._sc2wol_has_competent_anti_air(world, player)) and self.has_any({'Medivac', 'Medic'}, player) or \ + self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_competent_anti_air(world, player) or \ + self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(world, player) or \ + self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(world, player) def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool: return (self.has_any({'Siege Tank', 'Diamondback'}, player) or - self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) + self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player) + or self.has('Marauders', player)) def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) @@ -46,6 +49,10 @@ class SC2WoLLogic(LogicMixin): return self._sc2wol_has_protoss_common_units(world, player) and \ self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) + def _sc2wol_beats_protoss_deathball(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air or \ + self._sc2wol_has_competent_comp(world, player) and self._sc2wol_has_air_anti_air(world, player) + def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool: return self.has_group("Missions", player, mission_count) diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index 3a00b60401..4e20752982 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -200,7 +200,10 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, "Menu", missions[i]) else: connect(world, player, names, missions[connection], missions[i], - (lambda name: (lambda state: state.has(f"Beat {name}", player)))(missions[connection])) + (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and + state._sc2wol_cleared_missions(world, player, + missions_req))) + (missions[connection], vanilla_shuffle_order[i].number)) connections.append(connection + 1) mission_req_table.update({missions[i]: MissionInfo( diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 33522569d5..cf3175bd6e 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -33,7 +33,7 @@ class SC2WoLWorld(World): game = "Starcraft 2 Wings of Liberty" web = Starcraft2WoLWebWorld() - data_version = 2 + data_version = 3 item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {location.name: location.code for location in get_locations(None, None)} From a074d16297f906763bdbc9019f1bb0f8671907d3 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sat, 20 Aug 2022 10:46:44 -0400 Subject: [PATCH 138/138] DKC3 v1.1.0 (#938) Features: * KONGsanity option (Collect all KONG letters in each level for a check) * Autosave option * Difficulty option * MERRY option * Handle collected/co-op locations Bugfixes: * Fixed Mekanos softlock * Prevent Brothers Bear giving extra Banana Birds * Fixed Banana Bird Mother check sending prematurely * Fix Logic bug with Krematoa level costs --- worlds/dkc3/Client.py | 52 ++++++--- worlds/dkc3/Locations.py | 53 +++++++++ worlds/dkc3/Names/LocationName.py | 47 +++++++- worlds/dkc3/Options.py | 48 +++++++- worlds/dkc3/Regions.py | 121 +++++++++++++++----- worlds/dkc3/Rom.py | 181 +++++++++++++++++++++++++++++- worlds/dkc3/__init__.py | 15 ++- 7 files changed, 463 insertions(+), 54 deletions(-) diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index 8ac20131f3..7ab82187b0 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -66,7 +66,7 @@ async def dkc3_game_watcher(ctx: Context): return new_checks = [] - from worlds.dkc3.Rom import location_rom_data, item_rom_data + from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map for loc_id, loc_data in location_rom_data.items(): if loc_id not in ctx.locations_checked: data = await snes_read(ctx, WRAM_START + loc_data[0], 1) @@ -186,22 +186,40 @@ async def dkc3_game_watcher(ctx: Context): # DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged # Handle Collected Locations - #for loc_id in ctx.checked_locations: - # if loc_id not in ctx.locations_checked: - # loc_data = location_rom_data[loc_id] - # data = await snes_read(ctx, WRAM_START + loc_data[0], 1) - # invert_bit = ((len(loc_data) >= 3) and loc_data[2]) - # if not invert_bit: - # masked_data = data[0] | (1 << loc_data[1]) - # print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1]) - # snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) - # await snes_flush_writes(ctx) - # else: - # masked_data = data[0] & ~(1 << loc_data[1]) - # print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1]) - # snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) - # await snes_flush_writes(ctx) - # ctx.locations_checked.add(loc_id) + for loc_id in ctx.checked_locations: + if loc_id not in ctx.locations_checked and loc_id not in boss_location_ids: + loc_data = location_rom_data[loc_id] + data = await snes_read(ctx, WRAM_START + loc_data[0], 1) + invert_bit = ((len(loc_data) >= 3) and loc_data[2]) + if not invert_bit: + masked_data = data[0] | (1 << loc_data[1]) + #print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1]) + snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) + + if (loc_data[1] == 1): + # Make the next levels accessible + level_id = loc_data[0] - 0x632 + levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60) + tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60) + tile_id = levels_to_tiles[level_id] if levels_to_tiles[level_id] != 0xFF else level_id + tile_id = tile_id + 0x632 + #print("Tile ID: ", hex(tile_id)) + if tile_id in level_unlock_map: + for next_level_address in level_unlock_map[tile_id]: + next_level_id = next_level_address - 0x632 + next_tile_id = tiles_to_levels[next_level_id] if tiles_to_levels[next_level_id] != 0xFF else next_level_id + next_tile_id = next_tile_id + 0x632 + #print("Next Level ID: ", hex(next_tile_id)) + next_data = await snes_read(ctx, WRAM_START + next_tile_id, 1) + snes_buffered_write(ctx, WRAM_START + next_tile_id, bytes([next_data[0] | 0x01])) + + await snes_flush_writes(ctx) + else: + masked_data = data[0] & ~(1 << loc_data[1]) + print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1]) + snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) + await snes_flush_writes(ctx) + ctx.locations_checked.add(loc_id) # Calculate Boomer Cost Text boomer_cost_text = await snes_read(ctx, WRAM_START + 0xAAFD, 2) diff --git a/worlds/dkc3/Locations.py b/worlds/dkc3/Locations.py index aa8acf729a..e8d5409b15 100644 --- a/worlds/dkc3/Locations.py +++ b/worlds/dkc3/Locations.py @@ -221,6 +221,55 @@ level_location_table = { LocationName.rocket_rush_dk: 0xDC30A0, } +kong_location_table = { + LocationName.lakeside_limbo_kong: 0xDC3100, + LocationName.doorstop_dash_kong: 0xDC3104, + LocationName.tidal_trouble_kong: 0xDC3108, + LocationName.skiddas_row_kong: 0xDC310C, + LocationName.murky_mill_kong: 0xDC3110, + + LocationName.barrel_shield_bust_up_kong: 0xDC3114, + LocationName.riverside_race_kong: 0xDC3118, + LocationName.squeals_on_wheels_kong: 0xDC311C, + LocationName.springin_spiders_kong: 0xDC3120, + LocationName.bobbing_barrel_brawl_kong: 0xDC3124, + + LocationName.bazzas_blockade_kong: 0xDC3128, + LocationName.rocket_barrel_ride_kong: 0xDC312C, + LocationName.kreeping_klasps_kong: 0xDC3130, + LocationName.tracker_barrel_trek_kong: 0xDC3134, + LocationName.fish_food_frenzy_kong: 0xDC3138, + + LocationName.fire_ball_frenzy_kong: 0xDC313C, + LocationName.demolition_drain_pipe_kong: 0xDC3140, + LocationName.ripsaw_rage_kong: 0xDC3144, + LocationName.blazing_bazookas_kong: 0xDC3148, + LocationName.low_g_labyrinth_kong: 0xDC314C, + + LocationName.krevice_kreepers_kong: 0xDC3150, + LocationName.tearaway_toboggan_kong: 0xDC3154, + LocationName.barrel_drop_bounce_kong: 0xDC3158, + LocationName.krack_shot_kroc_kong: 0xDC315C, + LocationName.lemguin_lunge_kong: 0xDC3160, + + LocationName.buzzer_barrage_kong: 0xDC3164, + LocationName.kong_fused_cliffs_kong: 0xDC3168, + LocationName.floodlit_fish_kong: 0xDC316C, + LocationName.pothole_panic_kong: 0xDC3170, + LocationName.ropey_rumpus_kong: 0xDC3174, + + LocationName.konveyor_rope_clash_kong: 0xDC3178, + LocationName.creepy_caverns_kong: 0xDC317C, + LocationName.lightning_lookout_kong: 0xDC3180, + LocationName.koindozer_klamber_kong: 0xDC3184, + LocationName.poisonous_pipeline_kong: 0xDC3188, + + LocationName.stampede_sprint_kong: 0xDC318C, + LocationName.criss_cross_cliffs_kong: 0xDC3191, + LocationName.tyrant_twin_tussle_kong: 0xDC3195, + LocationName.swoopy_salvo_kong: 0xDC319A, +} + boss_location_table = { LocationName.belchas_barn: 0xDC30A1, @@ -266,6 +315,7 @@ all_locations = { **boss_location_table, **secret_cave_location_table, **brothers_bear_location_table, + **kong_location_table, } location_table = {} @@ -277,6 +327,9 @@ def setup_locations(world, player: int): if False:#world.include_trade_sequence[player].value: location_table.update({**brothers_bear_location_table}) + if world.kongsanity[player].value: + location_table.update({**kong_location_table}) + return location_table diff --git a/worlds/dkc3/Names/LocationName.py b/worlds/dkc3/Names/LocationName.py index b3aca3b0f1..f79a25f143 100644 --- a/worlds/dkc3/Names/LocationName.py +++ b/worlds/dkc3/Names/LocationName.py @@ -1,197 +1,236 @@ # Level Definitions lakeside_limbo_flag = "Lakeside Limbo - Flag" +lakeside_limbo_kong = "Lakeside Limbo - KONG" lakeside_limbo_bonus_1 = "Lakeside Limbo - Bonus 1" lakeside_limbo_bonus_2 = "Lakeside Limbo - Bonus 2" lakeside_limbo_dk = "Lakeside Limbo - DK Coin" doorstop_dash_flag = "Doorstop Dash - Flag" +doorstop_dash_kong = "Doorstop Dash - KONG" doorstop_dash_bonus_1 = "Doorstop Dash - Bonus 1" doorstop_dash_bonus_2 = "Doorstop Dash - Bonus 2" doorstop_dash_dk = "Doorstop Dash - DK Coin" tidal_trouble_flag = "Tidal Trouble - Flag" +tidal_trouble_kong = "Tidal Trouble - KONG" tidal_trouble_bonus_1 = "Tidal Trouble - Bonus 1" tidal_trouble_bonus_2 = "Tidal Trouble - Bonus 2" tidal_trouble_dk = "Tidal Trouble - DK Coin" skiddas_row_flag = "Skidda's Row - Flag" +skiddas_row_kong = "Skidda's Row - KONG" skiddas_row_bonus_1 = "Skidda's Row - Bonus 1" skiddas_row_bonus_2 = "Skidda's Row - Bonus 2" skiddas_row_dk = "Skidda's Row - DK Coin" murky_mill_flag = "Murky Mill - Flag" +murky_mill_kong = "Murky Mill - KONG" murky_mill_bonus_1 = "Murky Mill - Bonus 1" murky_mill_bonus_2 = "Murky Mill - Bonus 2" murky_mill_dk = "Murky Mill - DK Coin" barrel_shield_bust_up_flag = "Barrel Shield Bust-Up - Flag" +barrel_shield_bust_up_kong = "Barrel Shield Bust-Up - KONG" barrel_shield_bust_up_bonus_1 = "Barrel Shield Bust-Up - Bonus 1" barrel_shield_bust_up_bonus_2 = "Barrel Shield Bust-Up - Bonus 2" barrel_shield_bust_up_dk = "Barrel Shield Bust-Up - DK Coin" riverside_race_flag = "Riverside Race - Flag" +riverside_race_kong = "Riverside Race - KONG" riverside_race_bonus_1 = "Riverside Race - Bonus 1" riverside_race_bonus_2 = "Riverside Race - Bonus 2" riverside_race_dk = "Riverside Race - DK Coin" squeals_on_wheels_flag = "Squeals On Wheels - Flag" +squeals_on_wheels_kong = "Squeals On Wheels - KONG" squeals_on_wheels_bonus_1 = "Squeals On Wheels - Bonus 1" squeals_on_wheels_bonus_2 = "Squeals On Wheels - Bonus 2" squeals_on_wheels_dk = "Squeals On Wheels - DK Coin" springin_spiders_flag = "Springin' Spiders - Flag" +springin_spiders_kong = "Springin' Spiders - KONG" springin_spiders_bonus_1 = "Springin' Spiders - Bonus 1" springin_spiders_bonus_2 = "Springin' Spiders - Bonus 2" springin_spiders_dk = "Springin' Spiders - DK Coin" bobbing_barrel_brawl_flag = "Bobbing Barrel Brawl - Flag" +bobbing_barrel_brawl_kong = "Bobbing Barrel Brawl - KONG" bobbing_barrel_brawl_bonus_1 = "Bobbing Barrel Brawl - Bonus 1" bobbing_barrel_brawl_bonus_2 = "Bobbing Barrel Brawl - Bonus 2" bobbing_barrel_brawl_dk = "Bobbing Barrel Brawl - DK Coin" bazzas_blockade_flag = "Bazza's Blockade - Flag" +bazzas_blockade_kong = "Bazza's Blockade - KONG" bazzas_blockade_bonus_1 = "Bazza's Blockade - Bonus 1" bazzas_blockade_bonus_2 = "Bazza's Blockade - Bonus 2" bazzas_blockade_dk = "Bazza's Blockade - DK Coin" rocket_barrel_ride_flag = "Rocket Barrel Ride - Flag" +rocket_barrel_ride_kong = "Rocket Barrel Ride - KONG" rocket_barrel_ride_bonus_1 = "Rocket Barrel Ride - Bonus 1" rocket_barrel_ride_bonus_2 = "Rocket Barrel Ride - Bonus 2" rocket_barrel_ride_dk = "Rocket Barrel Ride - DK Coin" kreeping_klasps_flag = "Kreeping Klasps - Flag" +kreeping_klasps_kong = "Kreeping Klasps - KONG" kreeping_klasps_bonus_1 = "Kreeping Klasps - Bonus 1" kreeping_klasps_bonus_2 = "Kreeping Klasps - Bonus 2" kreeping_klasps_dk = "Kreeping Klasps - DK Coin" tracker_barrel_trek_flag = "Tracker Barrel Trek - Flag" +tracker_barrel_trek_kong = "Tracker Barrel Trek - KONG" tracker_barrel_trek_bonus_1 = "Tracker Barrel Trek - Bonus 1" tracker_barrel_trek_bonus_2 = "Tracker Barrel Trek - Bonus 2" tracker_barrel_trek_dk = "Tracker Barrel Trek - DK Coin" fish_food_frenzy_flag = "Fish Food Frenzy - Flag" +fish_food_frenzy_kong = "Fish Food Frenzy - KONG" fish_food_frenzy_bonus_1 = "Fish Food Frenzy - Bonus 1" fish_food_frenzy_bonus_2 = "Fish Food Frenzy - Bonus 2" fish_food_frenzy_dk = "Fish Food Frenzy - DK Coin" fire_ball_frenzy_flag = "Fire-Ball Frenzy - Flag" +fire_ball_frenzy_kong = "Fire-Ball Frenzy - KONG" fire_ball_frenzy_bonus_1 = "Fire-Ball Frenzy - Bonus 1" fire_ball_frenzy_bonus_2 = "Fire-Ball Frenzy - Bonus 2" fire_ball_frenzy_dk = "Fire-Ball Frenzy - DK Coin" demolition_drain_pipe_flag = "Demolition Drain-Pipe - Flag" +demolition_drain_pipe_kong = "Demolition Drain-Pipe - KONG" demolition_drain_pipe_bonus_1 = "Demolition Drain-Pipe - Bonus 1" demolition_drain_pipe_bonus_2 = "Demolition Drain-Pipe - Bonus 2" demolition_drain_pipe_dk = "Demolition Drain-Pipe - DK Coin" ripsaw_rage_flag = "Ripsaw Rage - Flag" +ripsaw_rage_kong = "Ripsaw Rage - KONG" ripsaw_rage_bonus_1 = "Ripsaw Rage - Bonus 1" ripsaw_rage_bonus_2 = "Ripsaw Rage - Bonus 2" ripsaw_rage_dk = "Ripsaw Rage - DK Coin" -blazing_bazookas_flag = "Blazing Bazookas - Flag" -blazing_bazookas_bonus_1 = "Blazing Bazookas - Bonus 1" -blazing_bazookas_bonus_2 = "Blazing Bazookas - Bonus 2" -blazing_bazookas_dk = "Blazing Bazookas - DK Coin" +blazing_bazookas_flag = "Blazing Bazukas - Flag" +blazing_bazookas_kong = "Blazing Bazukas - KONG" +blazing_bazookas_bonus_1 = "Blazing Bazukas - Bonus 1" +blazing_bazookas_bonus_2 = "Blazing Bazukas - Bonus 2" +blazing_bazookas_dk = "Blazing Bazukas - DK Coin" low_g_labyrinth_flag = "Low-G Labyrinth - Flag" +low_g_labyrinth_kong = "Low-G Labyrinth - KONG" low_g_labyrinth_bonus_1 = "Low-G Labyrinth - Bonus 1" low_g_labyrinth_bonus_2 = "Low-G Labyrinth - Bonus 2" low_g_labyrinth_dk = "Low-G Labyrinth - DK Coin" krevice_kreepers_flag = "Krevice Kreepers - Flag" +krevice_kreepers_kong = "Krevice Kreepers - KONG" krevice_kreepers_bonus_1 = "Krevice Kreepers - Bonus 1" krevice_kreepers_bonus_2 = "Krevice Kreepers - Bonus 2" krevice_kreepers_dk = "Krevice Kreepers - DK Coin" tearaway_toboggan_flag = "Tearaway Toboggan - Flag" +tearaway_toboggan_kong = "Tearaway Toboggan - KONG" tearaway_toboggan_bonus_1 = "Tearaway Toboggan - Bonus 1" tearaway_toboggan_bonus_2 = "Tearaway Toboggan - Bonus 2" tearaway_toboggan_dk = "Tearaway Toboggan - DK Coin" barrel_drop_bounce_flag = "Barrel Drop Bounce - Flag" +barrel_drop_bounce_kong = "Barrel Drop Bounce - KONG" barrel_drop_bounce_bonus_1 = "Barrel Drop Bounce - Bonus 1" barrel_drop_bounce_bonus_2 = "Barrel Drop Bounce - Bonus 2" barrel_drop_bounce_dk = "Barrel Drop Bounce - DK Coin" krack_shot_kroc_flag = "Krack-Shot Kroc - Flag" +krack_shot_kroc_kong = "Krack-Shot Kroc - KONG" krack_shot_kroc_bonus_1 = "Krack-Shot Kroc - Bonus 1" krack_shot_kroc_bonus_2 = "Krack-Shot Kroc - Bonus 2" krack_shot_kroc_dk = "Krack-Shot Kroc - DK Coin" lemguin_lunge_flag = "Lemguin Lunge - Flag" +lemguin_lunge_kong = "Lemguin Lunge - KONG" lemguin_lunge_bonus_1 = "Lemguin Lunge - Bonus 1" lemguin_lunge_bonus_2 = "Lemguin Lunge - Bonus 2" lemguin_lunge_dk = "Lemguin Lunge - DK Coin" buzzer_barrage_flag = "Buzzer Barrage - Flag" +buzzer_barrage_kong = "Buzzer Barrage - KONG" buzzer_barrage_bonus_1 = "Buzzer Barrage - Bonus 1" buzzer_barrage_bonus_2 = "Buzzer Barrage - Bonus 2" buzzer_barrage_dk = "Buzzer Barrage - DK Coin" kong_fused_cliffs_flag = "Kong-Fused Cliffs - Flag" +kong_fused_cliffs_kong = "Kong-Fused Cliffs - KONG" kong_fused_cliffs_bonus_1 = "Kong-Fused Cliffs - Bonus 1" kong_fused_cliffs_bonus_2 = "Kong-Fused Cliffs - Bonus 2" kong_fused_cliffs_dk = "Kong-Fused Cliffs - DK Coin" floodlit_fish_flag = "Floodlit Fish - Flag" +floodlit_fish_kong = "Floodlit Fish - KONG" floodlit_fish_bonus_1 = "Floodlit Fish - Bonus 1" floodlit_fish_bonus_2 = "Floodlit Fish - Bonus 2" floodlit_fish_dk = "Floodlit Fish - DK Coin" pothole_panic_flag = "Pothole Panic - Flag" +pothole_panic_kong = "Pothole Panic - KONG" pothole_panic_bonus_1 = "Pothole Panic - Bonus 1" pothole_panic_bonus_2 = "Pothole Panic - Bonus 2" pothole_panic_dk = "Pothole Panic - DK Coin" ropey_rumpus_flag = "Ropey Rumpus - Flag" +ropey_rumpus_kong = "Ropey Rumpus - KONG" ropey_rumpus_bonus_1 = "Ropey Rumpus - Bonus 1" ropey_rumpus_bonus_2 = "Ropey Rumpus - Bonus 2" ropey_rumpus_dk = "Ropey Rumpus - DK Coin" konveyor_rope_clash_flag = "Konveyor Rope Klash - Flag" +konveyor_rope_clash_kong = "Konveyor Rope Klash - KONG" konveyor_rope_clash_bonus_1 = "Konveyor Rope Klash - Bonus 1" konveyor_rope_clash_bonus_2 = "Konveyor Rope Klash - Bonus 2" konveyor_rope_clash_dk = "Konveyor Rope Klash - DK Coin" creepy_caverns_flag = "Creepy Caverns - Flag" +creepy_caverns_kong = "Creepy Caverns - KONG" creepy_caverns_bonus_1 = "Creepy Caverns - Bonus 1" creepy_caverns_bonus_2 = "Creepy Caverns - Bonus 2" creepy_caverns_dk = "Creepy Caverns - DK Coin" lightning_lookout_flag = "Lightning Lookout - Flag" +lightning_lookout_kong = "Lightning Lookout - KONG" lightning_lookout_bonus_1 = "Lightning Lookout - Bonus 1" lightning_lookout_bonus_2 = "Lightning Lookout - Bonus 2" lightning_lookout_dk = "Lightning Lookout - DK Coin" koindozer_klamber_flag = "Koindozer Klamber - Flag" +koindozer_klamber_kong = "Koindozer Klamber - KONG" koindozer_klamber_bonus_1 = "Koindozer Klamber - Bonus 1" koindozer_klamber_bonus_2 = "Koindozer Klamber - Bonus 2" koindozer_klamber_dk = "Koindozer Klamber - DK Coin" poisonous_pipeline_flag = "Poisonous Pipeline - Flag" +poisonous_pipeline_kong = "Poisonous Pipeline - KONG" poisonous_pipeline_bonus_1 = "Poisonous Pipeline - Bonus 1" poisonous_pipeline_bonus_2 = "Poisonous Pipeline - Bonus 2" poisonous_pipeline_dk = "Poisonous Pipeline - DK Coin" stampede_sprint_flag = "Stampede Sprint - Flag" +stampede_sprint_kong = "Stampede Sprint - KONG" stampede_sprint_bonus_1 = "Stampede Sprint - Bonus 1" stampede_sprint_bonus_2 = "Stampede Sprint - Bonus 2" stampede_sprint_bonus_3 = "Stampede Sprint - Bonus 3" stampede_sprint_dk = "Stampede Sprint - DK Coin" criss_cross_cliffs_flag = "Criss Kross Cliffs - Flag" +criss_cross_cliffs_kong = "Criss Kross Cliffs - KONG" criss_cross_cliffs_bonus_1 = "Criss Kross Cliffs - Bonus 1" criss_cross_cliffs_bonus_2 = "Criss Kross Cliffs - Bonus 2" criss_cross_cliffs_dk = "Criss Kross Cliffs - DK Coin" tyrant_twin_tussle_flag = "Tyrant Twin Tussle - Flag" +tyrant_twin_tussle_kong = "Tyrant Twin Tussle - KONG" tyrant_twin_tussle_bonus_1 = "Tyrant Twin Tussle - Bonus 1" tyrant_twin_tussle_bonus_2 = "Tyrant Twin Tussle - Bonus 2" tyrant_twin_tussle_bonus_3 = "Tyrant Twin Tussle - Bonus 3" tyrant_twin_tussle_dk = "Tyrant Twin Tussle - DK Coin" swoopy_salvo_flag = "Swoopy Salvo - Flag" +swoopy_salvo_kong = "Swoopy Salvo - KONG" swoopy_salvo_bonus_1 = "Swoopy Salvo - Bonus 1" swoopy_salvo_bonus_2 = "Swoopy Salvo - Bonus 2" swoopy_salvo_bonus_3 = "Swoopy Salvo - Bonus 3" diff --git a/worlds/dkc3/Options.py b/worlds/dkc3/Options.py index 9e00014933..7c0f532cfc 100644 --- a/worlds/dkc3/Options.py +++ b/worlds/dkc3/Options.py @@ -6,7 +6,7 @@ from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, O class Goal(Choice): """ Determines the goal of the seed - Knautilus: Reach the Knautilus and defeat Baron K. Roolenstein + Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother """ display_name = "Goal" @@ -75,6 +75,13 @@ class PercentageOfBananaBirds(Range): default = 100 +class KONGsanity(Toggle): + """ + Whether collecting all four KONG letters in each level grants a check + """ + display_name = "KONGsanity" + + class LevelShuffle(Toggle): """ Whether levels are shuffled @@ -82,6 +89,41 @@ class LevelShuffle(Toggle): display_name = "Level Shuffle" +class Difficulty(Choice): + """ + Which Difficulty Level to use + NORML: The Normal Difficulty + HARDR: Many DK Barrels are removed + TUFST: Most DK Barrels and all Midway Barrels are removed + """ + display_name = "Difficulty" + option_norml = 0 + option_hardr = 1 + option_tufst = 2 + default = 0 + + @classmethod + def get_option_name(cls, value) -> str: + if cls.auto_display_name: + return cls.name_lookup[value].upper() + else: + return cls.name_lookup[value] + + +class Autosave(DefaultOnToggle): + """ + Whether the game should autosave after each level + """ + display_name = "Autosave" + + +class MERRY(Toggle): + """ + Whether the Bonus Barrels will be Christmas-themed + """ + display_name = "MERRY" + + class MusicShuffle(Toggle): """ Whether music is shuffled @@ -125,7 +167,11 @@ dkc3_options: typing.Dict[str, type(Option)] = { "percentage_of_extra_bonus_coins": PercentageOfExtraBonusCoins, "number_of_banana_birds": NumberOfBananaBirds, "percentage_of_banana_birds": PercentageOfBananaBirds, + "kongsanity": KONGsanity, "level_shuffle": LevelShuffle, + "difficulty": Difficulty, + "autosave": Autosave, + "merry": MERRY, "music_shuffle": MusicShuffle, "kong_palette_swap": KongPaletteSwap, "starting_life_count": StartingLifeCount, diff --git a/worlds/dkc3/Regions.py b/worlds/dkc3/Regions.py index 501f1a0ea4..e33ff38c15 100644 --- a/worlds/dkc3/Regions.py +++ b/worlds/dkc3/Regions.py @@ -44,6 +44,8 @@ def create_regions(world, player: int, active_locations): LocationName.lakeside_limbo_bonus_2 : [0x657, 3], LocationName.lakeside_limbo_dk : [0x657, 5], } + if world.kongsanity[player]: + lakeside_limbo_region_locations[LocationName.lakeside_limbo_kong] = [] lakeside_limbo_region = create_region(world, player, active_locations, LocationName.lakeside_limbo_region, lakeside_limbo_region_locations, None) @@ -53,6 +55,8 @@ def create_regions(world, player: int, active_locations): LocationName.doorstop_dash_bonus_2 : [0x65A, 3], LocationName.doorstop_dash_dk : [0x65A, 5], } + if world.kongsanity[player]: + doorstop_dash_region_locations[LocationName.doorstop_dash_kong] = [] doorstop_dash_region = create_region(world, player, active_locations, LocationName.doorstop_dash_region, doorstop_dash_region_locations, None) @@ -62,6 +66,8 @@ def create_regions(world, player: int, active_locations): LocationName.tidal_trouble_bonus_2 : [0x659, 3], LocationName.tidal_trouble_dk : [0x659, 5], } + if world.kongsanity[player]: + tidal_trouble_region_locations[LocationName.tidal_trouble_kong] = [] tidal_trouble_region = create_region(world, player, active_locations, LocationName.tidal_trouble_region, tidal_trouble_region_locations, None) @@ -71,6 +77,8 @@ def create_regions(world, player: int, active_locations): LocationName.skiddas_row_bonus_2 : [0x65D, 3], LocationName.skiddas_row_dk : [0x65D, 5], } + if world.kongsanity[player]: + skiddas_row_region_locations[LocationName.skiddas_row_kong] = [] skiddas_row_region = create_region(world, player, active_locations, LocationName.skiddas_row_region, skiddas_row_region_locations, None) @@ -80,6 +88,8 @@ def create_regions(world, player: int, active_locations): LocationName.murky_mill_bonus_2 : [0x65C, 3], LocationName.murky_mill_dk : [0x65C, 5], } + if world.kongsanity[player]: + murky_mill_region_locations[LocationName.murky_mill_kong] = [] murky_mill_region = create_region(world, player, active_locations, LocationName.murky_mill_region, murky_mill_region_locations, None) @@ -89,6 +99,8 @@ def create_regions(world, player: int, active_locations): LocationName.barrel_shield_bust_up_bonus_2 : [0x662, 3], LocationName.barrel_shield_bust_up_dk : [0x662, 5], } + if world.kongsanity[player]: + barrel_shield_bust_up_region_locations[LocationName.barrel_shield_bust_up_kong] = [] barrel_shield_bust_up_region = create_region(world, player, active_locations, LocationName.barrel_shield_bust_up_region, barrel_shield_bust_up_region_locations, None) @@ -98,6 +110,8 @@ def create_regions(world, player: int, active_locations): LocationName.riverside_race_bonus_2 : [0x664, 3], LocationName.riverside_race_dk : [0x664, 5], } + if world.kongsanity[player]: + riverside_race_region_locations[LocationName.riverside_race_kong] = [] riverside_race_region = create_region(world, player, active_locations, LocationName.riverside_race_region, riverside_race_region_locations, None) @@ -107,6 +121,8 @@ def create_regions(world, player: int, active_locations): LocationName.squeals_on_wheels_bonus_2 : [0x65B, 3], LocationName.squeals_on_wheels_dk : [0x65B, 5], } + if world.kongsanity[player]: + squeals_on_wheels_region_locations[LocationName.squeals_on_wheels_kong] = [] squeals_on_wheels_region = create_region(world, player, active_locations, LocationName.squeals_on_wheels_region, squeals_on_wheels_region_locations, None) @@ -116,6 +132,8 @@ def create_regions(world, player: int, active_locations): LocationName.springin_spiders_bonus_2 : [0x661, 3], LocationName.springin_spiders_dk : [0x661, 5], } + if world.kongsanity[player]: + springin_spiders_region_locations[LocationName.springin_spiders_kong] = [] springin_spiders_region = create_region(world, player, active_locations, LocationName.springin_spiders_region, springin_spiders_region_locations, None) @@ -125,6 +143,8 @@ def create_regions(world, player: int, active_locations): LocationName.bobbing_barrel_brawl_bonus_2 : [0x666, 3], LocationName.bobbing_barrel_brawl_dk : [0x666, 5], } + if world.kongsanity[player]: + bobbing_barrel_brawl_region_locations[LocationName.bobbing_barrel_brawl_kong] = [] bobbing_barrel_brawl_region = create_region(world, player, active_locations, LocationName.bobbing_barrel_brawl_region, bobbing_barrel_brawl_region_locations, None) @@ -134,6 +154,8 @@ def create_regions(world, player: int, active_locations): LocationName.bazzas_blockade_bonus_2 : [0x667, 3], LocationName.bazzas_blockade_dk : [0x667, 5], } + if world.kongsanity[player]: + bazzas_blockade_region_locations[LocationName.bazzas_blockade_kong] = [] bazzas_blockade_region = create_region(world, player, active_locations, LocationName.bazzas_blockade_region, bazzas_blockade_region_locations, None) @@ -143,6 +165,8 @@ def create_regions(world, player: int, active_locations): LocationName.rocket_barrel_ride_bonus_2 : [0x66A, 3], LocationName.rocket_barrel_ride_dk : [0x66A, 5], } + if world.kongsanity[player]: + rocket_barrel_ride_region_locations[LocationName.rocket_barrel_ride_kong] = [] rocket_barrel_ride_region = create_region(world, player, active_locations, LocationName.rocket_barrel_ride_region, rocket_barrel_ride_region_locations, None) @@ -152,6 +176,8 @@ def create_regions(world, player: int, active_locations): LocationName.kreeping_klasps_bonus_2 : [0x658, 3], LocationName.kreeping_klasps_dk : [0x658, 5], } + if world.kongsanity[player]: + kreeping_klasps_region_locations[LocationName.kreeping_klasps_kong] = [] kreeping_klasps_region = create_region(world, player, active_locations, LocationName.kreeping_klasps_region, kreeping_klasps_region_locations, None) @@ -161,6 +187,8 @@ def create_regions(world, player: int, active_locations): LocationName.tracker_barrel_trek_bonus_2 : [0x66B, 3], LocationName.tracker_barrel_trek_dk : [0x66B, 5], } + if world.kongsanity[player]: + tracker_barrel_trek_region_locations[LocationName.tracker_barrel_trek_kong] = [] tracker_barrel_trek_region = create_region(world, player, active_locations, LocationName.tracker_barrel_trek_region, tracker_barrel_trek_region_locations, None) @@ -170,6 +198,8 @@ def create_regions(world, player: int, active_locations): LocationName.fish_food_frenzy_bonus_2 : [0x668, 3], LocationName.fish_food_frenzy_dk : [0x668, 5], } + if world.kongsanity[player]: + fish_food_frenzy_region_locations[LocationName.fish_food_frenzy_kong] = [] fish_food_frenzy_region = create_region(world, player, active_locations, LocationName.fish_food_frenzy_region, fish_food_frenzy_region_locations, None) @@ -179,6 +209,8 @@ def create_regions(world, player: int, active_locations): LocationName.fire_ball_frenzy_bonus_2 : [0x66D, 3], LocationName.fire_ball_frenzy_dk : [0x66D, 5], } + if world.kongsanity[player]: + fire_ball_frenzy_region_locations[LocationName.fire_ball_frenzy_kong] = [] fire_ball_frenzy_region = create_region(world, player, active_locations, LocationName.fire_ball_frenzy_region, fire_ball_frenzy_region_locations, None) @@ -188,6 +220,8 @@ def create_regions(world, player: int, active_locations): LocationName.demolition_drain_pipe_bonus_2 : [0x672, 3], LocationName.demolition_drain_pipe_dk : [0x672, 5], } + if world.kongsanity[player]: + demolition_drain_pipe_region_locations[LocationName.demolition_drain_pipe_kong] = [] demolition_drain_pipe_region = create_region(world, player, active_locations, LocationName.demolition_drain_pipe_region, demolition_drain_pipe_region_locations, None) @@ -197,6 +231,8 @@ def create_regions(world, player: int, active_locations): LocationName.ripsaw_rage_bonus_2 : [0x660, 3], LocationName.ripsaw_rage_dk : [0x660, 5], } + if world.kongsanity[player]: + ripsaw_rage_region_locations[LocationName.ripsaw_rage_kong] = [] ripsaw_rage_region = create_region(world, player, active_locations, LocationName.ripsaw_rage_region, ripsaw_rage_region_locations, None) @@ -206,6 +242,8 @@ def create_regions(world, player: int, active_locations): LocationName.blazing_bazookas_bonus_2 : [0x66E, 3], LocationName.blazing_bazookas_dk : [0x66E, 5], } + if world.kongsanity[player]: + blazing_bazookas_region_locations[LocationName.blazing_bazookas_kong] = [] blazing_bazookas_region = create_region(world, player, active_locations, LocationName.blazing_bazookas_region, blazing_bazookas_region_locations, None) @@ -215,6 +253,8 @@ def create_regions(world, player: int, active_locations): LocationName.low_g_labyrinth_bonus_2 : [0x670, 3], LocationName.low_g_labyrinth_dk : [0x670, 5], } + if world.kongsanity[player]: + low_g_labyrinth_region_locations[LocationName.low_g_labyrinth_kong] = [] low_g_labyrinth_region = create_region(world, player, active_locations, LocationName.low_g_labyrinth_region, low_g_labyrinth_region_locations, None) @@ -224,6 +264,8 @@ def create_regions(world, player: int, active_locations): LocationName.krevice_kreepers_bonus_2 : [0x673, 3], LocationName.krevice_kreepers_dk : [0x673, 5], } + if world.kongsanity[player]: + krevice_kreepers_region_locations[LocationName.krevice_kreepers_kong] = [] krevice_kreepers_region = create_region(world, player, active_locations, LocationName.krevice_kreepers_region, krevice_kreepers_region_locations, None) @@ -233,6 +275,8 @@ def create_regions(world, player: int, active_locations): LocationName.tearaway_toboggan_bonus_2 : [0x65F, 3], LocationName.tearaway_toboggan_dk : [0x65F, 5], } + if world.kongsanity[player]: + tearaway_toboggan_region_locations[LocationName.tearaway_toboggan_kong] = [] tearaway_toboggan_region = create_region(world, player, active_locations, LocationName.tearaway_toboggan_region, tearaway_toboggan_region_locations, None) @@ -242,6 +286,8 @@ def create_regions(world, player: int, active_locations): LocationName.barrel_drop_bounce_bonus_2 : [0x66C, 3], LocationName.barrel_drop_bounce_dk : [0x66C, 5], } + if world.kongsanity[player]: + barrel_drop_bounce_region_locations[LocationName.barrel_drop_bounce_kong] = [] barrel_drop_bounce_region = create_region(world, player, active_locations, LocationName.barrel_drop_bounce_region, barrel_drop_bounce_region_locations, None) @@ -251,6 +297,8 @@ def create_regions(world, player: int, active_locations): LocationName.krack_shot_kroc_bonus_2 : [0x66F, 3], LocationName.krack_shot_kroc_dk : [0x66F, 5], } + if world.kongsanity[player]: + krack_shot_kroc_region_locations[LocationName.krack_shot_kroc_kong] = [] krack_shot_kroc_region = create_region(world, player, active_locations, LocationName.krack_shot_kroc_region, krack_shot_kroc_region_locations, None) @@ -260,6 +308,8 @@ def create_regions(world, player: int, active_locations): LocationName.lemguin_lunge_bonus_2 : [0x65E, 3], LocationName.lemguin_lunge_dk : [0x65E, 5], } + if world.kongsanity[player]: + lemguin_lunge_region_locations[LocationName.lemguin_lunge_kong] = [] lemguin_lunge_region = create_region(world, player, active_locations, LocationName.lemguin_lunge_region, lemguin_lunge_region_locations, None) @@ -269,6 +319,8 @@ def create_regions(world, player: int, active_locations): LocationName.buzzer_barrage_bonus_2 : [0x676, 3], LocationName.buzzer_barrage_dk : [0x676, 5], } + if world.kongsanity[player]: + buzzer_barrage_region_locations[LocationName.buzzer_barrage_kong] = [] buzzer_barrage_region = create_region(world, player, active_locations, LocationName.buzzer_barrage_region, buzzer_barrage_region_locations, None) @@ -278,6 +330,8 @@ def create_regions(world, player: int, active_locations): LocationName.kong_fused_cliffs_bonus_2 : [0x674, 3], LocationName.kong_fused_cliffs_dk : [0x674, 5], } + if world.kongsanity[player]: + kong_fused_cliffs_region_locations[LocationName.kong_fused_cliffs_kong] = [] kong_fused_cliffs_region = create_region(world, player, active_locations, LocationName.kong_fused_cliffs_region, kong_fused_cliffs_region_locations, None) @@ -287,6 +341,8 @@ def create_regions(world, player: int, active_locations): LocationName.floodlit_fish_bonus_2 : [0x669, 3], LocationName.floodlit_fish_dk : [0x669, 5], } + if world.kongsanity[player]: + floodlit_fish_region_locations[LocationName.floodlit_fish_kong] = [] floodlit_fish_region = create_region(world, player, active_locations, LocationName.floodlit_fish_region, floodlit_fish_region_locations, None) @@ -296,6 +352,8 @@ def create_regions(world, player: int, active_locations): LocationName.pothole_panic_bonus_2 : [0x677, 3], LocationName.pothole_panic_dk : [0x677, 5], } + if world.kongsanity[player]: + pothole_panic_region_locations[LocationName.pothole_panic_kong] = [] pothole_panic_region = create_region(world, player, active_locations, LocationName.pothole_panic_region, pothole_panic_region_locations, None) @@ -305,6 +363,8 @@ def create_regions(world, player: int, active_locations): LocationName.ropey_rumpus_bonus_2 : [0x675, 3], LocationName.ropey_rumpus_dk : [0x675, 5], } + if world.kongsanity[player]: + ropey_rumpus_region_locations[LocationName.ropey_rumpus_kong] = [] ropey_rumpus_region = create_region(world, player, active_locations, LocationName.ropey_rumpus_region, ropey_rumpus_region_locations, None) @@ -314,6 +374,8 @@ def create_regions(world, player: int, active_locations): LocationName.konveyor_rope_clash_bonus_2 : [0x657, 3], LocationName.konveyor_rope_clash_dk : [0x657, 5], } + if world.kongsanity[player]: + konveyor_rope_clash_region_locations[LocationName.konveyor_rope_clash_kong] = [] konveyor_rope_clash_region = create_region(world, player, active_locations, LocationName.konveyor_rope_clash_region, konveyor_rope_clash_region_locations, None) @@ -323,6 +385,8 @@ def create_regions(world, player: int, active_locations): LocationName.creepy_caverns_bonus_2 : [0x678, 3], LocationName.creepy_caverns_dk : [0x678, 5], } + if world.kongsanity[player]: + creepy_caverns_region_locations[LocationName.creepy_caverns_kong] = [] creepy_caverns_region = create_region(world, player, active_locations, LocationName.creepy_caverns_region, creepy_caverns_region_locations, None) @@ -332,6 +396,8 @@ def create_regions(world, player: int, active_locations): LocationName.lightning_lookout_bonus_2 : [0x665, 3], LocationName.lightning_lookout_dk : [0x665, 5], } + if world.kongsanity[player]: + lightning_lookout_region_locations[LocationName.lightning_lookout_kong] = [] lightning_lookout_region = create_region(world, player, active_locations, LocationName.lightning_lookout_region, lightning_lookout_region_locations, None) @@ -341,6 +407,8 @@ def create_regions(world, player: int, active_locations): LocationName.koindozer_klamber_bonus_2 : [0x679, 3], LocationName.koindozer_klamber_dk : [0x679, 5], } + if world.kongsanity[player]: + koindozer_klamber_region_locations[LocationName.koindozer_klamber_kong] = [] koindozer_klamber_region = create_region(world, player, active_locations, LocationName.koindozer_klamber_region, koindozer_klamber_region_locations, None) @@ -350,6 +418,8 @@ def create_regions(world, player: int, active_locations): LocationName.poisonous_pipeline_bonus_2 : [0x671, 3], LocationName.poisonous_pipeline_dk : [0x671, 5], } + if world.kongsanity[player]: + poisonous_pipeline_region_locations[LocationName.poisonous_pipeline_kong] = [] poisonous_pipeline_region = create_region(world, player, active_locations, LocationName.poisonous_pipeline_region, poisonous_pipeline_region_locations, None) @@ -360,6 +430,8 @@ def create_regions(world, player: int, active_locations): LocationName.stampede_sprint_bonus_3 : [0x67B, 4], LocationName.stampede_sprint_dk : [0x67B, 5], } + if world.kongsanity[player]: + stampede_sprint_region_locations[LocationName.stampede_sprint_kong] = [] stampede_sprint_region = create_region(world, player, active_locations, LocationName.stampede_sprint_region, stampede_sprint_region_locations, None) @@ -369,6 +441,8 @@ def create_regions(world, player: int, active_locations): LocationName.criss_cross_cliffs_bonus_2 : [0x67C, 3], LocationName.criss_cross_cliffs_dk : [0x67C, 5], } + if world.kongsanity[player]: + criss_cross_cliffs_region_locations[LocationName.criss_cross_cliffs_kong] = [] criss_cross_cliffs_region = create_region(world, player, active_locations, LocationName.criss_cross_cliffs_region, criss_cross_cliffs_region_locations, None) @@ -379,6 +453,8 @@ def create_regions(world, player: int, active_locations): LocationName.tyrant_twin_tussle_bonus_3 : [0x67D, 4], LocationName.tyrant_twin_tussle_dk : [0x67D, 5], } + if world.kongsanity[player]: + tyrant_twin_tussle_region_locations[LocationName.tyrant_twin_tussle_kong] = [] tyrant_twin_tussle_region = create_region(world, player, active_locations, LocationName.tyrant_twin_tussle_region, tyrant_twin_tussle_region_locations, None) @@ -389,6 +465,8 @@ def create_regions(world, player: int, active_locations): LocationName.swoopy_salvo_bonus_3 : [0x663, 4], LocationName.swoopy_salvo_dk : [0x663, 5], } + if world.kongsanity[player]: + swoopy_salvo_region_locations[LocationName.swoopy_salvo_kong] = [] swoopy_salvo_region = create_region(world, player, active_locations, LocationName.swoopy_salvo_region, swoopy_salvo_region_locations, None) @@ -503,9 +581,7 @@ def create_regions(world, player: int, active_locations): sky_high_secret_region_locations = {} if False:#world.include_trade_sequence[player]: - sky_high_secret_region_locations.update({ - LocationName.sky_high_secret: [0x64B, 1], - }) + sky_high_secret_region_locations[LocationName.sky_high_secret] = [0x64B, 1] sky_high_secret_region = create_region(world, player, active_locations, LocationName.sky_high_secret_region, sky_high_secret_region_locations, None) @@ -517,9 +593,7 @@ def create_regions(world, player: int, active_locations): cifftop_cache_region_locations = {} if False:#world.include_trade_sequence[player]: - cifftop_cache_region_locations.update({ - LocationName.cifftop_cache: [0x64D, 1], - }) + cifftop_cache_region_locations[LocationName.cifftop_cache] = [0x64D, 1] cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region, cifftop_cache_region_locations, None) @@ -622,29 +696,19 @@ def create_regions(world, player: int, active_locations): LocationName.bazaars_general_store_2: [0x615, 3, True], }) - bramble_region_locations.update({ - LocationName.brambles_bungalow: [0x619, 2], - }) + bramble_region_locations[LocationName.brambles_bungalow] = [0x619, 2] #flower_spot_region_locations.update({ # LocationName.flower_spot: [0x615, 3, True], #}) - barter_region_locations.update({ - LocationName.barters_swap_shop: [0x61B, 3], - }) + barter_region_locations[LocationName.barters_swap_shop] = [0x61B, 3] - barnacle_region_locations.update({ - LocationName.barnacles_island: [0x61D, 2], - }) + barnacle_region_locations[LocationName.barnacles_island] = [0x61D, 2] - blue_region_locations.update({ - LocationName.blues_beach_hut: [0x621, 4], - }) + blue_region_locations[LocationName.blues_beach_hut] = [0x621, 4] - blizzard_region_locations.update({ - LocationName.blizzards_basecamp: [0x625, 4, True], - }) + blizzard_region_locations[LocationName.blizzards_basecamp] = [0x625, 4, True] bazaar_region = create_region(world, player, active_locations, LocationName.bazaar_region, bazaar_region_locations, None) @@ -817,7 +881,6 @@ def connect_regions(world, player, level_list): level_list[32], level_list[33], level_list[34], - LocationName.kastle_kaos_region, LocationName.sewer_stockpile_region, ] @@ -835,10 +898,16 @@ def connect_regions(world, player, level_list): for i in range(0, len(krematoa_levels)): connect(world, player, names, LocationName.krematoa_region, krematoa_levels[i], - lambda state: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1)))) - - connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region, - lambda state: (state.has(ItemName.krematoa_cog, player, 5))) + lambda state, i=i: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1)))) + + if world.goal[player] == "knautilus": + connect(world, player, names, LocationName.kaos_kore_region, LocationName.knautilus_region) + connect(world, player, names, LocationName.krematoa_region, LocationName.kastle_kaos_region, + lambda state: (state.has(ItemName.krematoa_cog, player, 5))) + else: + connect(world, player, names, LocationName.kaos_kore_region, LocationName.kastle_kaos_region) + connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region, + lambda state: (state.has(ItemName.krematoa_cog, player, 5))) def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None, exits=None): diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 7e83589ffa..90c4507e44 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -11,187 +11,270 @@ import os import math +level_unlock_map = { + 0x657: [0x65A], + 0x65A: [0x680, 0x639, 0x659], + 0x659: [0x65D], + 0x65D: [0x65C], + 0x65C: [0x688, 0x64F], + + 0x662: [0x681, 0x664], + 0x664: [0x65B], + 0x65B: [0x689, 0x661], + 0x661: [0x63A, 0x666], + 0x666: [0x650, 0x649], + + 0x667: [0x66A], + 0x66A: [0x682, 0x658], + 0x658: [0x68A, 0x66B], + 0x66B: [0x668], + 0x668: [0x651], + + 0x66D: [0x63C, 0x672], + 0x672: [0x68B, 0x660], + 0x660: [0x683, 0x66E], + 0x66E: [0x670], + 0x670: [0x652], + + 0x673: [0x684, 0x65F], + 0x65F: [0x66C], + 0x66C: [0x66F], + 0x66F: [0x65E], + 0x65E: [0x63D, 0x653, 0x68C, 0x64C], + + 0x676: [0x63E, 0x674, 0x685], + 0x674: [0x63F, 0x669], + 0x669: [0x677], + 0x677: [0x68D, 0x675], + 0x675: [0x654], + + 0x67A: [0x640, 0x678], + 0x678: [0x665], + 0x665: [0x686, 0x679], + 0x679: [0x68E, 0x671], + + 0x67B: [0x67C], + 0x67C: [0x67D], + 0x67D: [0x663], + 0x663: [0x67E], +} + location_rom_data = { 0xDC3000: [0x657, 1], # Lakeside Limbo 0xDC3001: [0x657, 2], 0xDC3002: [0x657, 3], 0xDC3003: [0x657, 5], + 0xDC3100: [0x657, 7], 0xDC3004: [0x65A, 1], # Doorstop Dash 0xDC3005: [0x65A, 2], 0xDC3006: [0x65A, 3], 0xDC3007: [0x65A, 5], + 0xDC3104: [0x65A, 7], 0xDC3008: [0x659, 1], # Tidal Trouble 0xDC3009: [0x659, 2], 0xDC300A: [0x659, 3], 0xDC300B: [0x659, 5], + 0xDC3108: [0x659, 7], 0xDC300C: [0x65D, 1], # Skidda's Row 0xDC300D: [0x65D, 2], 0xDC300E: [0x65D, 3], 0xDC300F: [0x65D, 5], + 0xDC310C: [0x65D, 7], 0xDC3010: [0x65C, 1], # Murky Mill 0xDC3011: [0x65C, 2], 0xDC3012: [0x65C, 3], 0xDC3013: [0x65C, 5], + 0xDC3110: [0x65C, 7], 0xDC3014: [0x662, 1], # Barrel Shield Bust-Up 0xDC3015: [0x662, 2], 0xDC3016: [0x662, 3], 0xDC3017: [0x662, 5], + 0xDC3114: [0x662, 7], 0xDC3018: [0x664, 1], # Riverside Race 0xDC3019: [0x664, 2], 0xDC301A: [0x664, 3], 0xDC301B: [0x664, 5], + 0xDC3118: [0x664, 7], 0xDC301C: [0x65B, 1], # Squeals on Wheels 0xDC301D: [0x65B, 2], 0xDC301E: [0x65B, 3], 0xDC301F: [0x65B, 5], + 0xDC311C: [0x65B, 7], 0xDC3020: [0x661, 1], # Springin' Spiders 0xDC3021: [0x661, 2], 0xDC3022: [0x661, 3], 0xDC3023: [0x661, 5], + 0xDC3120: [0x661, 7], 0xDC3024: [0x666, 1], # Bobbing Barrel Brawl 0xDC3025: [0x666, 2], 0xDC3026: [0x666, 3], 0xDC3027: [0x666, 5], + 0xDC3124: [0x666, 7], 0xDC3028: [0x667, 1], # Bazza's Blockade 0xDC3029: [0x667, 2], 0xDC302A: [0x667, 3], 0xDC302B: [0x667, 5], + 0xDC3128: [0x667, 7], 0xDC302C: [0x66A, 1], # Rocket Barrel Ride 0xDC302D: [0x66A, 2], 0xDC302E: [0x66A, 3], 0xDC302F: [0x66A, 5], + 0xDC312C: [0x66A, 7], 0xDC3030: [0x658, 1], # Kreeping Klasps 0xDC3031: [0x658, 2], 0xDC3032: [0x658, 3], 0xDC3033: [0x658, 5], + 0xDC3130: [0x658, 7], 0xDC3034: [0x66B, 1], # Tracker Barrel Trek 0xDC3035: [0x66B, 2], 0xDC3036: [0x66B, 3], 0xDC3037: [0x66B, 5], + 0xDC3134: [0x66B, 7], 0xDC3038: [0x668, 1], # Fish Food Frenzy 0xDC3039: [0x668, 2], 0xDC303A: [0x668, 3], 0xDC303B: [0x668, 5], + 0xDC3138: [0x668, 7], 0xDC303C: [0x66D, 1], # Fire-ball Frenzy 0xDC303D: [0x66D, 2], 0xDC303E: [0x66D, 3], 0xDC303F: [0x66D, 5], + 0xDC313C: [0x66D, 7], 0xDC3040: [0x672, 1], # Demolition Drainpipe 0xDC3041: [0x672, 2], 0xDC3042: [0x672, 3], 0xDC3043: [0x672, 5], + 0xDC3140: [0x672, 7], 0xDC3044: [0x660, 1], # Ripsaw Rage 0xDC3045: [0x660, 2], 0xDC3046: [0x660, 3], 0xDC3047: [0x660, 5], + 0xDC3144: [0x660, 7], 0xDC3048: [0x66E, 1], # Blazing Bazukas 0xDC3049: [0x66E, 2], 0xDC304A: [0x66E, 3], 0xDC304B: [0x66E, 5], + 0xDC3148: [0x66E, 7], 0xDC304C: [0x670, 1], # Low-G Labyrinth 0xDC304D: [0x670, 2], 0xDC304E: [0x670, 3], 0xDC304F: [0x670, 5], + 0xDC314C: [0x670, 7], 0xDC3050: [0x673, 1], # Krevice Kreepers 0xDC3051: [0x673, 2], 0xDC3052: [0x673, 3], 0xDC3053: [0x673, 5], + 0xDC3150: [0x673, 7], 0xDC3054: [0x65F, 1], # Tearaway Toboggan 0xDC3055: [0x65F, 2], 0xDC3056: [0x65F, 3], 0xDC3057: [0x65F, 5], + 0xDC3154: [0x65F, 7], 0xDC3058: [0x66C, 1], # Barrel Drop Bounce 0xDC3059: [0x66C, 2], 0xDC305A: [0x66C, 3], 0xDC305B: [0x66C, 5], + 0xDC3158: [0x66C, 7], 0xDC305C: [0x66F, 1], # Krack-Shot Kroc 0xDC305D: [0x66F, 2], 0xDC305E: [0x66F, 3], 0xDC305F: [0x66F, 5], + 0xDC315C: [0x66F, 7], 0xDC3060: [0x65E, 1], # Lemguin Lunge 0xDC3061: [0x65E, 2], 0xDC3062: [0x65E, 3], 0xDC3063: [0x65E, 5], + 0xDC3160: [0x65E, 7], 0xDC3064: [0x676, 1], # Buzzer Barrage 0xDC3065: [0x676, 2], 0xDC3066: [0x676, 3], 0xDC3067: [0x676, 5], + 0xDC3164: [0x676, 7], 0xDC3068: [0x674, 1], # Kong-Fused Cliffs 0xDC3069: [0x674, 2], 0xDC306A: [0x674, 3], 0xDC306B: [0x674, 5], + 0xDC3168: [0x674, 7], 0xDC306C: [0x669, 1], # Floodlit Fish 0xDC306D: [0x669, 2], 0xDC306E: [0x669, 3], 0xDC306F: [0x669, 5], + 0xDC316C: [0x669, 7], 0xDC3070: [0x677, 1], # Pothole Panic 0xDC3071: [0x677, 2], 0xDC3072: [0x677, 3], 0xDC3073: [0x677, 5], + 0xDC3170: [0x677, 7], 0xDC3074: [0x675, 1], # Ropey Rumpus 0xDC3075: [0x675, 2], 0xDC3076: [0x675, 3], 0xDC3077: [0x675, 5], + 0xDC3174: [0x675, 7], 0xDC3078: [0x67A, 1], # Konveyor Rope Klash 0xDC3079: [0x67A, 2], 0xDC307A: [0x67A, 3], 0xDC307B: [0x67A, 5], + 0xDC3178: [0x67A, 7], 0xDC307C: [0x678, 1], # Creepy Caverns 0xDC307D: [0x678, 2], 0xDC307E: [0x678, 3], 0xDC307F: [0x678, 5], + 0xDC317C: [0x678, 7], 0xDC3080: [0x665, 1], # Lightning Lookout 0xDC3081: [0x665, 2], 0xDC3082: [0x665, 3], 0xDC3083: [0x665, 5], + 0xDC3180: [0x665, 7], 0xDC3084: [0x679, 1], # Koindozer Klamber 0xDC3085: [0x679, 2], 0xDC3086: [0x679, 3], 0xDC3087: [0x679, 5], + 0xDC3184: [0x679, 7], 0xDC3088: [0x671, 1], # Poisonous Pipeline 0xDC3089: [0x671, 2], 0xDC308A: [0x671, 3], 0xDC308B: [0x671, 5], + 0xDC3188: [0x671, 7], 0xDC308C: [0x67B, 1], # Stampede Sprint @@ -199,23 +282,27 @@ location_rom_data = { 0xDC308E: [0x67B, 3], 0xDC308F: [0x67B, 4], 0xDC3090: [0x67B, 5], + 0xDC318C: [0x67B, 7], 0xDC3091: [0x67C, 1], # Criss Kross Cliffs 0xDC3092: [0x67C, 2], 0xDC3093: [0x67C, 3], 0xDC3094: [0x67C, 5], + 0xDC3191: [0x67C, 7], 0xDC3095: [0x67D, 1], # Tyrant Twin Tussle 0xDC3096: [0x67D, 2], 0xDC3097: [0x67D, 3], 0xDC3098: [0x67D, 4], 0xDC3099: [0x67D, 5], + 0xDC3195: [0x67D, 7], 0xDC309A: [0x663, 1], # Swoopy Salvo 0xDC309B: [0x663, 2], 0xDC309C: [0x663, 3], 0xDC309D: [0x663, 4], 0xDC309E: [0x663, 5], + 0xDC319A: [0x663, 7], 0xDC309F: [0x67E, 1], # Rocket Rush 0xDC30A0: [0x67E, 5], @@ -243,7 +330,7 @@ location_rom_data = { #0xDC30B4: [0x64D, 1], # Disabled until Trade Sequence 0xDC30B5: [0x64E, 1], - 0xDC30B6: [0x5FD, 4], # Banana Bird Mother + 0xDC30B6: [0x5FE, 4], # Banana Bird Mother # DKC3_TODO: Disabled until Trade Sequence #0xDC30B7: [0x615, 2, True], @@ -256,6 +343,18 @@ location_rom_data = { #0xDC30BE: [0x625, 4, True], } +boss_location_ids = [ + 0xDC30A1, + 0xDC30A2, + 0xDC30A3, + 0xDC30A4, + 0xDC30A5, + 0xDC30A6, + 0xDC30A7, + 0xDC30A8, + 0xDC30B6, +] + item_rom_data = { 0xDC3001: [0x5D5], # 1-Up Balloon @@ -400,7 +499,6 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x3484DE, 0xEA) rom.write_byte(0x348528, 0x80) # Prevent Single-Ski Lock - # Make Swanky free rom.write_byte(0x348C48, 0x00) @@ -462,6 +560,25 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x9130, world.starting_life_count[player].value) rom.write_byte(0x913B, world.starting_life_count[player].value) + # Cheat options + cheat_bytes = [0x00, 0x00] + + if world.merry[player]: + cheat_bytes[0] |= 0x01 + + if world.autosave[player]: + cheat_bytes[0] |= 0x02 + + if world.difficulty[player] == "tufst": + cheat_bytes[0] |= 0x80 + cheat_bytes[1] |= 0x80 + elif world.difficulty[player] == "hardr": + cheat_bytes[0] |= 0x00 + cheat_bytes[1] |= 0x00 + elif world.difficulty[player] == "norml": + cheat_bytes[1] |= 0x40 + + rom.write_bytes(0x8303, bytearray(cheat_bytes)) # Handle Level Shuffle Here if world.level_shuffle[player]: @@ -469,6 +586,9 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(level_dict[level_list[i]].nameIDAddress, level_dict[active_level_list[i]].nameID) rom.write_byte(level_dict[level_list[i]].levelIDAddress, level_dict[active_level_list[i]].levelID) + rom.write_byte(0x3FF800 + level_dict[active_level_list[i]].levelID, level_dict[level_list[i]].levelID) + rom.write_byte(0x3FF860 + level_dict[level_list[i]].levelID, level_dict[active_level_list[i]].levelID) + # First levels of each world rom.write_byte(0x34BC3E, (0x32 + level_dict[active_level_list[0]].levelID)) rom.write_byte(0x34BC47, (0x32 + level_dict[active_level_list[5]].levelID)) @@ -495,6 +615,52 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x32F339, 0x55) + # Handle KONGsanity Here + if world.kongsanity[player]: + # Arich's Hoard KONGsanity fix + rom.write_bytes(0x34BA8C, bytearray([0xEA, 0xEA])) + + # Don't hide the level flag if the 0x80 bit is set + rom.write_bytes(0x34CE92, bytearray([0x80])) + + # Use the `!` next to level name for indicating KONG letters + rom.write_bytes(0x34B8F0, bytearray([0x80])) + rom.write_bytes(0x34B8F3, bytearray([0x80])) + + # Hijack to code to set the 0x80 flag for the level when you complete KONG + rom.write_bytes(0x3BCD4B, bytearray([0x22, 0x80, 0xFA, 0XB8])) # JSL $B8FA80 + + rom.write_bytes(0x38FA80, bytearray([0xDA])) # PHX + rom.write_bytes(0x38FA81, bytearray([0x48])) # PHA + rom.write_bytes(0x38FA82, bytearray([0x08])) # PHP + rom.write_bytes(0x38FA83, bytearray([0xE2, 0x20])) # SEP #20 + rom.write_bytes(0x38FA85, bytearray([0x48])) # PHA + rom.write_bytes(0x38FA86, bytearray([0x18])) # CLC + rom.write_bytes(0x38FA87, bytearray([0x6D, 0xD3, 0x18])) # ADC $18D3 + rom.write_bytes(0x38FA8A, bytearray([0x8D, 0xD3, 0x18])) # STA $18D3 + rom.write_bytes(0x38FA8D, bytearray([0x68])) # PLA + rom.write_bytes(0x38FA8E, bytearray([0xC2, 0x20])) # REP 20 + rom.write_bytes(0x38FA90, bytearray([0X18])) # CLC + rom.write_bytes(0x38FA91, bytearray([0x6D, 0xD5, 0x05])) # ADC $05D5 + rom.write_bytes(0x38FA94, bytearray([0x8D, 0xD5, 0x05])) # STA $05D5 + rom.write_bytes(0x38FA97, bytearray([0xAE, 0xB9, 0x05])) # LDX $05B9 + rom.write_bytes(0x38FA9A, bytearray([0xBD, 0x32, 0x06])) # LDA $0632, X + rom.write_bytes(0x38FA9D, bytearray([0x09, 0x80, 0x00])) # ORA #8000 + rom.write_bytes(0x38FAA0, bytearray([0x9D, 0x32, 0x06])) # STA $0632, X + rom.write_bytes(0x38FAA3, bytearray([0xAD, 0xD5, 0x18])) # LDA $18D5 + rom.write_bytes(0x38FAA6, bytearray([0xD0, 0x03])) # BNE $80EA + rom.write_bytes(0x38FAA8, bytearray([0x9C, 0xD9, 0x18])) # STZ $18D9 + rom.write_bytes(0x38FAAB, bytearray([0xA9, 0x78, 0x00])) # LDA #0078 + rom.write_bytes(0x38FAAE, bytearray([0x8D, 0xD5, 0x18])) # STA $18D5 + rom.write_bytes(0x38FAB1, bytearray([0x28])) # PLP + rom.write_bytes(0x38FAB2, bytearray([0x68])) # PLA + rom.write_bytes(0x38FAB3, bytearray([0xFA])) # PLX + rom.write_bytes(0x38FAB4, bytearray([0x6B])) # RTL + # End Handle KONGsanity + + # Handle Credits + rom.write_bytes(0x32A5DF, bytearray([0x41, 0x52, 0x43, 0x48, 0x49, 0x50, 0x45, 0x4C, 0x41, 0x47, 0x4F, 0x20, 0x4D, 0x4F, 0xC4])) # "ARCHIPELAGO MOD" + rom.write_bytes(0x32A5EE, bytearray([0x00, 0x03, 0x50, 0x4F, 0x52, 0x59, 0x47, 0x4F, 0x4E, 0xC5])) # "PORYGONE" from Main import __version__ rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21] @@ -516,6 +682,17 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x32DD63, 0xEA) rom.write_byte(0x32DD64, 0xEA) + # Don't grant Banana Birds at Bears + rom.write_byte(0x3492DB, 0xEA) + rom.write_byte(0x3492DC, 0xEA) + rom.write_byte(0x3492DD, 0xEA) + rom.write_byte(0x3493F4, 0xEA) + rom.write_byte(0x3493F5, 0xEA) + rom.write_byte(0x3493F6, 0xEA) + + # Don't grant present at Blizzard + rom.write_byte(0x8454, 0x00) + # Don't grant Patch and Skis from their bosses rom.write_byte(0x3F3762, 0x00) rom.write_byte(0x3F377B, 0x00) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index f5b01ff723..5c575b85b5 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -4,7 +4,7 @@ import math import threading from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification -from .Items import DKC3Item, ItemData, item_table, inventory_table +from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table from .Locations import DKC3Location, all_locations, setup_locations from .Options import dkc3_options from .Regions import create_regions, connect_regions @@ -40,7 +40,7 @@ class DKC3World(World): game: str = "Donkey Kong Country 3" option_definitions = dkc3_options topology_present = False - data_version = 1 + data_version = 2 #hint_blacklist = {LocationName.rocket_rush_flag} item_name_to_id = {name: data.code for name, data in item_table.items()} @@ -99,10 +99,13 @@ class DKC3World(World): # Bosses total_required_locations += number_of_bosses - + # Secret Caves total_required_locations += 13 + if self.world.kongsanity[self.player]: + total_required_locations += 39 + ## Brothers Bear if False:#self.world.include_trade_sequence[self.player]: total_required_locations += 10 @@ -118,7 +121,11 @@ class DKC3World(World): total_junk_count = total_required_locations - len(itempool) - itempool += [self.create_item(ItemName.bear_coin)] * total_junk_count + junk_pool = [] + for item_name in self.world.random.choices(list(junk_table.keys()), k=total_junk_count): + junk_pool += [self.create_item(item_name)] + + itempool += junk_pool self.active_level_list = level_list.copy()