From 930529e211225517220bc473f818cffa210aa64d Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Sat, 9 Nov 2024 13:29:51 -0500 Subject: [PATCH 01/60] DS3: Update tests to use bases.py (#4138) --- worlds/dark_souls_3/test/TestDarkSouls3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/dark_souls_3/test/TestDarkSouls3.py b/worlds/dark_souls_3/test/TestDarkSouls3.py index e590cd732b41..7acdad465da7 100644 --- a/worlds/dark_souls_3/test/TestDarkSouls3.py +++ b/worlds/dark_souls_3/test/TestDarkSouls3.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from worlds.dark_souls_3.Items import item_dictionary from worlds.dark_souls_3.Locations import location_tables From 6b4f6ebc1eaac7e1a8e738a73f3ac9005ad6273f Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Sun, 10 Nov 2024 05:14:34 +1000 Subject: [PATCH 02/60] Update to Muse Dash v4.9.0 [Ensemble Arcanum] (#4095) * Update to Muse Dash v4.9.0 * Add trailing newline * I've forgotten to adjust this for a while. Lets increase it by a decent chunk. --- worlds/musedash/MuseDashData.txt | 25 ++++++++++++++++++++----- worlds/musedash/Options.py | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 1f1a2a011cff..d913449ed540 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -31,7 +31,7 @@ Blackest Luxury Car|0-18|Default Music|True|3|6|8| Medicine of Sing|0-19|Default Music|False|3|6|8| irregulyze|0-20|Default Music|True|3|6|8| I don't care about Christmas though|0-47|Default Music|False|4|6|8| -Imaginary World|0-21|Default Music|True|4|6|8| +Imaginary World|0-21|Default Music|True|4|6|8|10 Dysthymia|0-22|Default Music|True|4|7|9| From the New World|0-42|Default Music|False|2|5|7| NISEGAO|0-33|Default Music|True|4|7|9| @@ -266,7 +266,7 @@ Medusa|31-1|Happy Otaku Pack Vol.11|False|4|6|8|10 Final Step!|31-2|Happy Otaku Pack Vol.11|False|5|7|10| MAGENTA POTION|31-3|Happy Otaku Pack Vol.11|False|4|7|9| Cross Ray|31-4|Happy Otaku Pack Vol.11|False|3|6|9| -Square Lake|31-5|Happy Otaku Pack Vol.11|True|6|8|9|11 +Square Lake|31-5|Happy Otaku Pack Vol.11|False|6|8|9|11 Girly Cupid|30-0|Cute Is Everything Vol.6|False|3|6|8| sheep in the light|30-1|Cute Is Everything Vol.6|False|2|5|8| Breaker city|30-2|Cute Is Everything Vol.6|False|4|6|9| @@ -353,7 +353,7 @@ Re End of a Dream|16-1|Give Up TREATMENT Vol.6|False|5|8|11| Etude -Storm-|16-2|Give Up TREATMENT Vol.6|True|6|8|10| Unlimited Katharsis|16-3|Give Up TREATMENT Vol.6|False|4|6|10| Magic Knight Girl|16-4|Give Up TREATMENT Vol.6|False|4|7|9| -Eeliaas|16-5|Give Up TREATMENT Vol.6|True|6|9|11| +Eeliaas|16-5|Give Up TREATMENT Vol.6|False|6|9|11| Magic Spell|15-0|Cute Is Everything Vol.3|True|2|5|7| Colorful Star, Colored Drawing, Travel Poem|15-1|Cute Is Everything Vol.3|False|3|4|6| Satell Knight|15-2|Cute Is Everything Vol.3|False|3|6|8| @@ -396,7 +396,7 @@ Chronomia|9-2|Happy Otaku Pack Vol.4|False|5|7|10| Dandelion's Daydream|9-3|Happy Otaku Pack Vol.4|True|5|7|8| Lorikeet Flat design|9-4|Happy Otaku Pack Vol.4|True|5|7|10| GOODRAGE|9-5|Happy Otaku Pack Vol.4|False|6|9|11| -Altale|8-0|Give Up TREATMENT Vol.3|False|3|5|7| +Altale|8-0|Give Up TREATMENT Vol.3|False|3|5|7|10 Brain Power|8-1|Give Up TREATMENT Vol.3|False|4|7|10| Berry Go!!|8-2|Give Up TREATMENT Vol.3|False|3|6|9| Sweet* Witch* Girl*|8-3|Give Up TREATMENT Vol.3|False|6|8|10|? @@ -579,4 +579,19 @@ The Whole Rest|77-1|Let's Rhythm Jam!|False|5|8|10|11 Hydra|77-2|Let's Rhythm Jam!|False|4|7|11| Pastel Lines|77-3|Let's Rhythm Jam!|False|3|6|9| LINK x LIN#S|77-4|Let's Rhythm Jam!|False|3|6|9| -Arcade ViruZ|77-5|Let's Rhythm Jam!|False|6|8|10| +Arcade ViruZ|77-5|Let's Rhythm Jam!|False|6|8|11| +Eve Avenir|78-0|Endless Pirouette|True|6|8|10| +Silverstring|78-1|Endless Pirouette|True|5|7|10| +Melusia|78-2|Endless Pirouette|False|5|7|10|11 +Devil's Castle|78-3|Endless Pirouette|True|4|7|10| +Abatement|78-4|Endless Pirouette|True|6|8|10|11 +Azalea|78-5|Endless Pirouette|False|4|8|10| +Brightly World|78-6|Endless Pirouette|True|6|8|10| +We'll meet in every world ***|78-7|Endless Pirouette|True|7|9|11| +Collapsar|78-8|Endless Pirouette|True|7|9|10|11 +Parousia|78-9|Endless Pirouette|False|6|8|10| +Gunners in the Rain|79-0|Ensemble Arcanum|False|5|8|10| +Halzion|79-1|Ensemble Arcanum|False|2|5|8| +SHOWTIME!!|79-2|Ensemble Arcanum|False|6|8|10| +Achromic Riddle|79-3|Ensemble Arcanum|False|6|8|10|11 +karanosu|79-4|Ensemble Arcanum|False|3|6|8| diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index 7164aa3e1362..e647c18d7096 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -39,7 +39,7 @@ class AdditionalSongs(Range): - The final song count may be lower due to other settings. """ range_start = 15 - range_end = 534 # Note will probably not reach this high if any other settings are done. + range_end = 600 # Note will probably not reach this high if any other settings are done. default = 40 display_name = "Additional Song Count" From fa93bc5d1eb9a3e694f335ca24683ea044f2f563 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:10:54 +0100 Subject: [PATCH 03/60] =?UTF-8?q?The=20Witness:=20Get=20rid=20of=20Menu=20?= =?UTF-8?q?region,=20prepare=20for=20other=20worlds=20to=20change=20theirs?= =?UTF-8?q?=20(hints)=C2=A0#3888?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/witness/__init__.py | 2 ++ worlds/witness/data/WitnessLogic.txt | 2 -- worlds/witness/data/WitnessLogicExpert.txt | 2 -- worlds/witness/data/WitnessLogicVanilla.txt | 2 -- worlds/witness/data/WitnessLogicVariety.txt | 2 -- worlds/witness/hints.py | 7 +++++-- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index c9848f2ffe47..a21a5bb3ca7e 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -50,6 +50,8 @@ class WitnessWorld(World): topology_present = False web = WitnessWebWorld() + origin_region_name = "Entry" + options_dataclass = TheWitnessOptions options: TheWitnessOptions diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index fabd1428810b..8fadf68c3131 100644 --- a/worlds/witness/data/WitnessLogic.txt +++ b/worlds/witness/data/WitnessLogic.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: diff --git a/worlds/witness/data/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt index 200138dee1f7..c6d6efa96485 100644 --- a/worlds/witness/data/WitnessLogicExpert.txt +++ b/worlds/witness/data/WitnessLogicExpert.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: diff --git a/worlds/witness/data/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt index 67a42ba7e4d4..1186c470233e 100644 --- a/worlds/witness/data/WitnessLogicVanilla.txt +++ b/worlds/witness/data/WitnessLogicVanilla.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: diff --git a/worlds/witness/data/WitnessLogicVariety.txt b/worlds/witness/data/WitnessLogicVariety.txt index a3c388dfb1e4..31263aa33790 100644 --- a/worlds/witness/data/WitnessLogicVariety.txt +++ b/worlds/witness/data/WitnessLogicVariety.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 99e8eea2eb89..dac7e3fb4d05 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -250,8 +250,11 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes elif group_type == "Group": location_name = f"a \"{chosen_group}\" location in {player_name}'s world" elif group_type == "Region": - if chosen_group == "Menu": - location_name = f"a location near the start of {player_name}'s game (\"Menu\" region)" + origin_region_name = world.multiworld.worlds[hint.location.player].origin_region_name + if chosen_group == origin_region_name: + location_name = ( + f"a location in the origin region of {player_name}'s world (\"{origin_region_name}\" region)" + ) else: location_name = f"a location in {player_name}'s \"{chosen_group}\" region" From 9be996ba0ea9d01ffd7fb003428cd2a3ece3a22f Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 10 Nov 2024 01:23:29 +0100 Subject: [PATCH 04/60] Core: downgrade websockets and minor fixes (#4166) * Core: downgrade websockets to 13.x 14.x currently doesn't work for MultiServer. 14.x is not supported with py3.8, so updating to 14.x should be scheduled for AP 0.6.0. * CI: 5min timeout for hosting test * MultiServer: properly shutdown even if ctx is invalid * CI: increase hosting test timeout to 10min this is 4x expected time, just to be safe. --- .github/workflows/unittests.yml | 2 +- MultiServer.py | 6 ++++-- requirements.txt | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 9a3a6d11217f..a38fef8fda08 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -89,4 +89,4 @@ jobs: run: | source venv/bin/activate export PYTHONPATH=$(pwd) - python test/hosting/__main__.py + timeout 600 python test/hosting/__main__.py diff --git a/MultiServer.py b/MultiServer.py index bac35648cf5a..764b56362ecc 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1960,8 +1960,10 @@ def _cmd_status(self, tag: str = "") -> bool: def _cmd_exit(self) -> bool: """Shutdown the server""" - self.ctx.server.ws_server.close() - self.ctx.exit_event.set() + try: + self.ctx.server.ws_server.close() + finally: + self.ctx.exit_event.set() return True @mark_raw diff --git a/requirements.txt b/requirements.txt index 6fe14c9f32ce..946546cb6961 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ colorama>=0.4.6 -websockets>=13.0.1 +websockets>=13.0.1,<14 PyYAML>=6.0.2 jellyfish>=1.1.0 jinja2>=3.1.4 From b3e5ef876a25f49bd864eaf898d126fc74f5a7f1 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 10 Nov 2024 01:40:29 +0100 Subject: [PATCH 05/60] WebHost: update werkzeug (#4167) --- WebHostLib/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 2020387053f9..5c79415312d4 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,5 +1,5 @@ flask>=3.0.3 -werkzeug>=3.0.4 +werkzeug>=3.0.6 pony>=0.7.19 waitress>=3.0.0 Flask-Caching>=2.3.0 From f3413e9cef1f3b719d4e039ef170544b52476482 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 11 Nov 2024 11:43:16 +0100 Subject: [PATCH 06/60] Factorio: support 2.0 update (#4110) - removed tutorialization (Craft/Do X to unlock tech) - start with everything needed for power, electric mining drills, science lab and automation science already unlocked - updated world gen - updated mod api use - updated fluid boxes (CaitSith2) - new option: free sample quality (needs quality mod) - removed old gruft, faster gen speed, faster load time - lists space age as explicitly not supported, so it prevents the game from trying to load both - fixes Y offset of traps being wrong (way higher than intended) - client now has a 5 second timeout to communicate with the bound factorio server, so it aborts actions if the server died - savegames are now stored in write_data_directory -> saves -> Archipelago - add cargo-landing-pad handling - starting rocket silo and cargo landing pad respect free sample quality - supports Factorio 2.0 --------- Co-authored-by: CaitSith2 --- worlds/factorio/Client.py | 10 +- worlds/factorio/Mod.py | 20 +-- worlds/factorio/Options.py | 30 +++- worlds/factorio/Technologies.py | 135 +++++++++-------- worlds/factorio/__init__.py | 63 ++++---- worlds/factorio/data/fluids.json | 2 +- worlds/factorio/data/items.json | 2 +- worlds/factorio/data/machines.json | 2 +- worlds/factorio/data/mod/lib.lua | 8 +- worlds/factorio/data/mod_template/control.lua | 137 ++++++++++-------- .../data/mod_template/data-final-fixes.lua | 31 ++-- worlds/factorio/data/mod_template/data.lua | 13 +- worlds/factorio/data/mod_template/macros.lua | 2 +- .../factorio/data/mod_template/settings.lua | 2 +- worlds/factorio/data/recipes.json | 2 +- worlds/factorio/data/resources.json | 2 +- worlds/factorio/data/techs.json | 2 +- 17 files changed, 263 insertions(+), 200 deletions(-) diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index 23dfa0633eb4..3c35c4cb0986 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -304,13 +304,13 @@ def queuer(): async def factorio_server_watcher(ctx: FactorioContext): - savegame_name = os.path.abspath(ctx.savegame_name) + savegame_name = os.path.abspath(os.path.join(ctx.write_data_path, "saves", "Archipelago", ctx.savegame_name)) if not os.path.exists(savegame_name): logger.info(f"Creating savegame {savegame_name}") subprocess.run(( executable, "--create", savegame_name, "--preset", "archipelago" )) - factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name, + factorio_process = subprocess.Popen((executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)), stderr=subprocess.PIPE, stdout=subprocess.PIPE, @@ -331,7 +331,8 @@ async def factorio_server_watcher(ctx: FactorioContext): factorio_queue.task_done() if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg: - ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) + ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password, + timeout=5) if not ctx.server: logger.info("Established bridge to Factorio Server. " "Ready to connect to Archipelago via /connect") @@ -405,8 +406,7 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient): info = json.loads(rcon_client.send_command("/ap-rcon-info")) ctx.auth = info["slot_name"] ctx.seed_name = info["seed_name"] - # 0.2.0 addition, not present earlier - death_link = bool(info.get("death_link", False)) + death_link = info["death_link"] ctx.energy_link_increment = info.get("energy_link", 0) logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}") if ctx.energy_link_increment and ctx.ui: diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 7eec71875829..7dee04afbee3 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -35,9 +35,11 @@ "author": "Berserker", "homepage": "https://archipelago.gg", "description": "Integration client for the Archipelago Randomizer", - "factorio_version": "1.1", + "factorio_version": "2.0", "dependencies": [ - "base >= 1.1.0", + "base >= 2.0.15", + "? quality >= 2.0.15", + "! space-age", "? science-not-invited", "? factory-levels" ] @@ -133,21 +135,21 @@ def flop_random(low, high, base=None): "allowed_science_packs": world.options.max_science_pack.get_allowed_packs(), "custom_technologies": world.custom_technologies, "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites, - "slot_name": world.player_name, "seed_name": multiworld.seed_name, + "slot_name": world.player_name, + "seed_name": multiworld.seed_name, "slot_player": player, - "starting_items": world.options.starting_items, "recipes": recipes, - "random": random, "flop_random": flop_random, + "recipes": recipes, + "random": random, + "flop_random": flop_random, "recipe_time_scale": recipe_time_scales.get(world.options.recipe_time.value, None), "recipe_time_range": recipe_time_ranges.get(world.options.recipe_time.value, None), "free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, + "free_sample_quality_name": world.options.free_samples_quality.current_key, "progressive_technology_table": {tech.name: tech.progressive for tech in progressive_technology_table.values()}, "custom_recipes": world.custom_recipes, - "max_science_pack": world.options.max_science_pack.value, "liquids": fluids, - "goal": world.options.goal.value, - "energy_link": world.options.energy_link.value, - "useless_technologies": useless_technologies, + "removed_technologies": world.removed_technologies, "chunk_shuffle": 0, } diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 788d1f9e1d92..5a41250fa760 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,12 +1,11 @@ from __future__ import annotations from dataclasses import dataclass -import datetime import typing from schema import Schema, Optional, And, Or -from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ +from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \ StartInventoryPool, PerGameCommonOptions # schema helpers @@ -122,6 +121,18 @@ class FreeSamples(Choice): default = 3 +class FreeSamplesQuality(Choice): + """If free samples are on, determine the quality of the granted items. + Requires the quality mod, which is part of the Space Age DLC. Without it, normal quality is given.""" + display_name = "Free Samples Quality" + option_normal = 0 + option_uncommon = 1 + option_rare = 2 + option_epic = 3 + option_legendary = 4 + default = 0 + + class TechTreeLayout(Choice): """Selects how the tech tree nodes are interwoven. Single: No dependencies @@ -284,17 +295,21 @@ class FactorioWorldGen(OptionDict): # FIXME: do we want default be a rando-optimized default or in-game DS? value: typing.Dict[str, typing.Dict[str, typing.Any]] default = { - "terrain_segmentation": 0.5, - "water": 1.5, "autoplace_controls": { + # terrain + "water": {"frequency": 1, "size": 1, "richness": 1}, + "nauvis_cliff": {"frequency": 1, "size": 1, "richness": 1}, + "starting_area_moisture": {"frequency": 1, "size": 1, "richness": 1}, + # resources "coal": {"frequency": 1, "size": 3, "richness": 6}, "copper-ore": {"frequency": 1, "size": 3, "richness": 6}, "crude-oil": {"frequency": 1, "size": 3, "richness": 6}, - "enemy-base": {"frequency": 1, "size": 1, "richness": 1}, "iron-ore": {"frequency": 1, "size": 3, "richness": 6}, "stone": {"frequency": 1, "size": 3, "richness": 6}, + "uranium-ore": {"frequency": 1, "size": 3, "richness": 6}, + # misc "trees": {"frequency": 1, "size": 1, "richness": 1}, - "uranium-ore": {"frequency": 1, "size": 3, "richness": 6} + "enemy-base": {"frequency": 1, "size": 1, "richness": 1}, }, "seed": None, "starting_area": 1, @@ -336,8 +351,6 @@ class FactorioWorldGen(OptionDict): } schema = Schema({ "basic": { - Optional("terrain_segmentation"): FloatRange(0.166, 6), - Optional("water"): FloatRange(0.166, 6), Optional("autoplace_controls"): { str: { "frequency": FloatRange(0, 6), @@ -438,6 +451,7 @@ class FactorioOptions(PerGameCommonOptions): silo: Silo satellite: Satellite free_samples: FreeSamples + free_samples_quality: FreeSamplesQuality tech_tree_information: TechTreeInformation starting_items: FactorioStartItems free_sample_blacklist: FactorioFreeSampleBlacklist diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 112cc49f0920..6111462e8ca9 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -1,13 +1,13 @@ from __future__ import annotations -import orjson -import logging -import os -import string +import functools import pkgutil +import string from collections import Counter from concurrent.futures import ThreadPoolExecutor -from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any +from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any, Optional + +import orjson import Utils from . import Options @@ -32,8 +32,23 @@ def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: tech_table: Dict[str, int] = {} technology_table: Dict[str, Technology] = {} +start_unlocked_recipes = { + "offshore-pump", + "boiler", + "steam-engine", + "automation-science-pack", + "inserter", + "small-electric-pole", + "copper-cable", + "lab", + "electronic-circuit", + "electric-mining-drill", + "pipe", + "pipe-to-ground", +} + -def always(state): +def always(state) -> bool: return True @@ -50,15 +65,13 @@ def __hash__(self): class Technology(FactorioElement): # maybe make subclass of Location? has_modifier: bool factorio_id: int - ingredients: Set[str] progressive: Tuple[str] unlocks: Union[Set[str], bool] # bool case is for progressive technologies - def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = (), + def __init__(self, technology_name: str, factorio_id: int, progressive: Tuple[str] = (), has_modifier: bool = False, unlocks: Union[Set[str], bool] = None): - self.name = name + self.name = technology_name self.factorio_id = factorio_id - self.ingredients = ingredients self.progressive = progressive self.has_modifier = has_modifier if unlocks: @@ -66,19 +79,6 @@ def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressi else: self.unlocks = set() - def build_rule(self, player: int): - logging.debug(f"Building rules for {self.name}") - - return lambda state: all(state.has(f"Automated {ingredient}", player) - for ingredient in self.ingredients) - - def get_prior_technologies(self) -> Set[Technology]: - """Get Technologies that have to precede this one to resolve tree connections.""" - technologies = set() - for ingredient in self.ingredients: - technologies |= required_technologies[ingredient] # technologies that unlock the recipes - return technologies - def __hash__(self): return self.factorio_id @@ -91,22 +91,22 @@ def useful(self) -> bool: class CustomTechnology(Technology): """A particularly configured Technology for a world.""" + ingredients: Set[str] def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int): - ingredients = origin.ingredients & allowed_packs - military_allowed = "military-science-pack" in allowed_packs \ - and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) - or origin.name == "rocket-silo") + ingredients = allowed_packs self.player = player if origin.name not in world.special_nodes: - if military_allowed: - ingredients.add("military-science-pack") - ingredients = list(ingredients) - ingredients.sort() # deterministic sample - ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients))) - elif origin.name == "rocket-silo" and military_allowed: - ingredients.add("military-science-pack") - super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id) + ingredients = set(world.random.sample(list(ingredients), world.random.randint(1, len(ingredients)))) + self.ingredients = ingredients + super(CustomTechnology, self).__init__(origin.name, origin.factorio_id) + + def get_prior_technologies(self) -> Set[Technology]: + """Get Technologies that have to precede this one to resolve tree connections.""" + technologies = set() + for ingredient in self.ingredients: + technologies |= required_technologies[ingredient] # technologies that unlock the recipes + return technologies class Recipe(FactorioElement): @@ -149,19 +149,22 @@ def rel_cost(self) -> float: ingredients = sum(self.ingredients.values()) return min(ingredients / amount for product, amount in self.products.items()) - @property + @functools.cached_property def base_cost(self) -> Dict[str, int]: ingredients = Counter() - for ingredient, cost in self.ingredients.items(): - if ingredient in all_product_sources: - for recipe in all_product_sources[ingredient]: - if recipe.ingredients: - ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in - recipe.base_cost.items()}) - else: - ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient] - else: - ingredients[ingredient] += cost + try: + for ingredient, cost in self.ingredients.items(): + if ingredient in all_product_sources: + for recipe in all_product_sources[ingredient]: + if recipe.ingredients: + ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in + recipe.base_cost.items()}) + else: + ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient] + else: + ingredients[ingredient] += cost + except RecursionError as e: + raise Exception(f"Infinite recursion in ingredients of {self}.") from e return ingredients @property @@ -191,9 +194,12 @@ def __init__(self, name, categories): # recipes and technologies can share names in Factorio for technology_name, data in sorted(techs_future.result().items()): - current_ingredients = set(data["ingredients"]) - technology = Technology(technology_name, current_ingredients, factorio_tech_id, - has_modifier=data["has_modifier"], unlocks=set(data["unlocks"])) + technology = Technology( + technology_name, + factorio_tech_id, + has_modifier=data["has_modifier"], + unlocks=set(data["unlocks"]) - start_unlocked_recipes, + ) factorio_tech_id += 1 tech_table[technology_name] = technology.factorio_id technology_table[technology_name] = technology @@ -226,11 +232,12 @@ def __init__(self, name, categories): recipes[recipe_name] = recipe if set(recipe.products).isdisjoint( # prevents loop recipes like uranium centrifuging - set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \ + set(recipe.ingredients)) and ("barrel" not in recipe.products or recipe.name == "barrel") and \ not recipe_name.endswith("-reprocessing"): for product_name in recipe.products: all_product_sources.setdefault(product_name, set()).add(recipe) +assert all(recipe_name in raw_recipes for recipe_name in start_unlocked_recipes), "Unknown Recipe defined." machines: Dict[str, Machine] = {} @@ -248,9 +255,7 @@ def __init__(self, name, categories): # build requirements graph for all technology ingredients -all_ingredient_names: Set[str] = set() -for technology in technology_table.values(): - all_ingredient_names |= technology.ingredients +all_ingredient_names: Set[str] = set(Options.MaxSciencePack.get_ordered_science_packs()) def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]: @@ -319,13 +324,17 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_f recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))) -def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_recipe: Recipe) -> Set[str]: +def get_rocket_requirements(silo_recipe: Optional[Recipe], part_recipe: Recipe, + satellite_recipe: Optional[Recipe], cargo_landing_pad_recipe: Optional[Recipe]) -> Set[str]: techs = set() if silo_recipe: for ingredient in silo_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) for ingredient in part_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) + if cargo_landing_pad_recipe: + for ingredient in cargo_landing_pad_recipe.ingredients: + techs |= recursively_get_unlocking_technologies(ingredient) if satellite_recipe: techs |= satellite_recipe.unlocking_technologies for ingredient in satellite_recipe.ingredients: @@ -382,15 +391,15 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ "uranium-processing", "kovarex-enrichment-process", "nuclear-fuel-reprocessing") progressive_rows["progressive-rocketry"] = ("rocketry", "explosive-rocketry", "atomic-bomb") progressive_rows["progressive-vehicle"] = ("automobilism", "tank", "spidertron") -progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon", - "automated-rail-transportation", "rail-signals") +progressive_rows["progressive-fluid-handling"] = ("fluid-handling", "fluid-wagon") +progressive_rows["progressive-train-network"] = ("railway", "automated-rail-transportation") progressive_rows["progressive-engine"] = ("engine", "electric-engine") progressive_rows["progressive-armor"] = ("heavy-armor", "modular-armor", "power-armor", "power-armor-mk2") progressive_rows["progressive-personal-battery"] = ("battery-equipment", "battery-mk2-equipment") progressive_rows["progressive-energy-shield"] = ("energy-shield-equipment", "energy-shield-mk2-equipment") progressive_rows["progressive-wall"] = ("stone-wall", "gate") progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer") -progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter") +progressive_rows["progressive-inserter"] = ("fast-inserter", "bulk-inserter") progressive_rows["progressive-turret"] = ("gun-turret", "laser-turret") progressive_rows["progressive-flamethrower"] = ("flamethrower",) # leaving out flammables, as they do nothing progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment", @@ -402,7 +411,7 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ source_target_mapping: Dict[str, str] = { "progressive-braking-force": "progressive-train-network", "progressive-inserter-capacity-bonus": "progressive-inserter", - "progressive-refined-flammables": "progressive-flamethrower" + "progressive-refined-flammables": "progressive-flamethrower", } for source, target in source_target_mapping.items(): @@ -416,12 +425,14 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ for root in sorted_rows: progressive = progressive_rows[root] - assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology" + assert all(tech in tech_table for tech in progressive), \ + (f"Declared a progressive technology ({root}) without base technology. " + f"Missing: f{tuple(tech for tech in progressive if tech not in tech_table)}") factorio_tech_id += 1 - progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_tech_id, - progressive, + progressive_technology = Technology(root, factorio_tech_id, + tuple(progressive), has_modifier=any(technology_table[tech].has_modifier for tech in progressive), - unlocks=any(technology_table[tech].unlocks for tech in progressive)) + unlocks=any(technology_table[tech].unlocks for tech in progressive),) progressive_tech_table[root] = progressive_technology.factorio_id progressive_technology_table[root] = progressive_technology diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 925327655a24..9f1f3cb573f9 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -2,10 +2,11 @@ import collections import logging -import settings import typing -from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification +import Utils +import settings +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld from worlds.LauncherComponents import Component, components, Type, launch_subprocess from worlds.generic import Rules @@ -14,7 +15,7 @@ from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution from .Shapes import get_shapes from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ - all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ + 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, valid_ingredients, progressive_rows @@ -97,19 +98,21 @@ class Factorio(World): item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - required_client_version = (0, 5, 0) - + required_client_version = (0, 5, 1) + if Utils.version_tuple < required_client_version: + raise Exception(f"Update Archipelago to use this world ({game}).") ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]] tech_mix: int = 0 skip_silo: bool = False origin_region_name = "Nauvis" science_locations: typing.List[FactorioScienceLocation] - + removed_technologies: typing.Set[str] settings: typing.ClassVar[FactorioSettings] def __init__(self, world, player: int): super(Factorio, self).__init__(world, player) + self.removed_technologies = useless_technologies.copy() self.advancement_technologies = set() self.custom_recipes = {} self.science_locations = [] @@ -208,11 +211,9 @@ def create_items(self) -> None: for loc in self.science_locations: loc.revealed = True if self.skip_silo: - removed = useless_technologies | {"rocket-silo"} - else: - removed = useless_technologies + self.removed_technologies |= {"rocket-silo"} for tech_name in base_tech_table: - if tech_name not in removed: + if tech_name not in self.removed_technologies: progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name) want_progressive = want_progressives[progressive_item_name] item_name = progressive_item_name if want_progressive else tech_name @@ -240,40 +241,49 @@ def set_rules(self): custom_recipe = self.custom_recipes[ingredient] location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ - (ingredient not in technology_table or state.has(ingredient, player)) and \ + (not technology_table[ingredient].unlocks or state.has(ingredient, player)) and \ all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients for technology in required_technologies[sub_ingredient]) and \ all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine]) + else: location.access_rule = lambda state, ingredient=ingredient: \ all(state.has(technology.name, player) for technology in required_technologies[ingredient]) for location in self.science_locations: - Rules.set_rule(location, lambda state, ingredients=location.ingredients: + Rules.set_rule(location, lambda state, ingredients=frozenset(location.ingredients): all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) prerequisites = shapes.get(location) if prerequisites: - Rules.add_rule(location, lambda state, locations= - prerequisites: all(state.can_reach(loc) for loc in locations)) + Rules.add_rule(location, lambda state, locations=frozenset(prerequisites): + all(state.can_reach(loc) for loc in locations)) silo_recipe = None + cargo_pad_recipe = None if self.options.silo == Silo.option_spawn: - silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ - else next(iter(all_product_sources.get("rocket-silo"))) + silo_recipe = self.get_recipe("rocket-silo") + cargo_pad_recipe = self.get_recipe("cargo-landing-pad") part_recipe = self.custom_recipes["rocket-part"] satellite_recipe = None if self.options.goal == Goal.option_satellite: - satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ - else next(iter(all_product_sources.get("satellite"))) - victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) - if self.options.silo != Silo.option_spawn: - victory_tech_names.add("rocket-silo") + satellite_recipe = self.get_recipe("satellite") + victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe, cargo_pad_recipe) + if self.options.silo == Silo.option_spawn: + victory_tech_names -= {"rocket-silo"} + else: + victory_tech_names |= {"rocket-silo"} self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player) for technology in victory_tech_names) - + for tech_name in victory_tech_names: + if not self.multiworld.get_all_state(True).has(tech_name, player): + print(tech_name) self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) + def get_recipe(self, name: str) -> Recipe: + return self.custom_recipes[name] if name in self.custom_recipes \ + else next(iter(all_product_sources.get(name))) + def generate_basic(self): map_basic_settings = self.options.world_gen.value["basic"] if map_basic_settings.get("seed", None) is None: # allow seed 0 @@ -321,9 +331,11 @@ def get_category(category: str, liquids: int) -> str: def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2, ingredients_offset: int = 0) -> Recipe: + count: int = len(original.ingredients) + ingredients_offset + assert len(pool) >= count, f"Can't pick {count} many items from pool {pool}." new_ingredients = {} liquids_used = 0 - for _ in range(len(original.ingredients) + ingredients_offset): + for _ in range(count): new_ingredient = pool.pop() if new_ingredient in fluids: while liquids_used == allow_liquids and new_ingredient in fluids: @@ -440,7 +452,8 @@ def set_custom_recipes(self): ingredients_offset = self.options.recipe_ingredients_offset original_rocket_part = recipes["rocket-part"] science_pack_pools = get_science_pack_pools() - valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients) + valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] + & valid_ingredients) self.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 + ingredients_offset)}, @@ -489,7 +502,7 @@ def set_custom_recipes(self): needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"} if self.options.silo != Silo.option_spawn: - needed_recipes |= {"rocket-silo"} + needed_recipes |= {"rocket-silo", "cargo-landing-pad"} if self.options.goal.value == Goal.option_satellite: needed_recipes |= {"satellite"} diff --git a/worlds/factorio/data/fluids.json b/worlds/factorio/data/fluids.json index 448ccf4e4921..6972690f5355 100644 --- a/worlds/factorio/data/fluids.json +++ b/worlds/factorio/data/fluids.json @@ -1 +1 @@ -["fluid-unknown","water","crude-oil","steam","heavy-oil","light-oil","petroleum-gas","sulfuric-acid","lubricant"] \ No newline at end of file +["water","steam","crude-oil","petroleum-gas","light-oil","heavy-oil","lubricant","sulfuric-acid","parameter-0","parameter-1","parameter-2","parameter-3","parameter-4","parameter-5","parameter-6","parameter-7","parameter-8","parameter-9","fluid-unknown"] \ No newline at end of file diff --git a/worlds/factorio/data/items.json b/worlds/factorio/data/items.json index fa34430f40c4..d9ec7befba90 100644 --- a/worlds/factorio/data/items.json +++ b/worlds/factorio/data/items.json @@ -1 +1 @@ -{"wooden-chest":50,"iron-chest":50,"steel-chest":50,"storage-tank":50,"transport-belt":100,"fast-transport-belt":100,"express-transport-belt":100,"underground-belt":50,"fast-underground-belt":50,"express-underground-belt":50,"splitter":50,"fast-splitter":50,"express-splitter":50,"loader":50,"fast-loader":50,"express-loader":50,"burner-inserter":50,"inserter":50,"long-handed-inserter":50,"fast-inserter":50,"filter-inserter":50,"stack-inserter":50,"stack-filter-inserter":50,"small-electric-pole":50,"medium-electric-pole":50,"big-electric-pole":50,"substation":50,"pipe":100,"pipe-to-ground":50,"pump":50,"rail":100,"train-stop":10,"rail-signal":50,"rail-chain-signal":50,"locomotive":5,"cargo-wagon":5,"fluid-wagon":5,"artillery-wagon":5,"car":1,"tank":1,"spidertron":1,"spidertron-remote":1,"logistic-robot":50,"construction-robot":50,"logistic-chest-active-provider":50,"logistic-chest-passive-provider":50,"logistic-chest-storage":50,"logistic-chest-buffer":50,"logistic-chest-requester":50,"roboport":10,"small-lamp":50,"red-wire":200,"green-wire":200,"arithmetic-combinator":50,"decider-combinator":50,"constant-combinator":50,"power-switch":50,"programmable-speaker":50,"stone-brick":100,"concrete":100,"hazard-concrete":100,"refined-concrete":100,"refined-hazard-concrete":100,"landfill":100,"cliff-explosives":20,"dummy-steel-axe":1,"repair-pack":100,"blueprint":1,"deconstruction-planner":1,"upgrade-planner":1,"blueprint-book":1,"copy-paste-tool":1,"cut-paste-tool":1,"boiler":50,"steam-engine":10,"solar-panel":50,"accumulator":50,"nuclear-reactor":10,"heat-pipe":50,"heat-exchanger":50,"steam-turbine":10,"burner-mining-drill":50,"electric-mining-drill":50,"offshore-pump":20,"pumpjack":20,"stone-furnace":50,"steel-furnace":50,"electric-furnace":50,"assembling-machine-1":50,"assembling-machine-2":50,"assembling-machine-3":50,"oil-refinery":10,"chemical-plant":10,"centrifuge":50,"lab":10,"beacon":10,"speed-module":50,"speed-module-2":50,"speed-module-3":50,"effectivity-module":50,"effectivity-module-2":50,"effectivity-module-3":50,"productivity-module":50,"productivity-module-2":50,"productivity-module-3":50,"rocket-silo":1,"satellite":1,"wood":100,"coal":50,"stone":50,"iron-ore":50,"copper-ore":50,"uranium-ore":50,"raw-fish":100,"iron-plate":100,"copper-plate":100,"solid-fuel":50,"steel-plate":100,"plastic-bar":100,"sulfur":50,"battery":200,"explosives":50,"crude-oil-barrel":10,"heavy-oil-barrel":10,"light-oil-barrel":10,"lubricant-barrel":10,"petroleum-gas-barrel":10,"sulfuric-acid-barrel":10,"water-barrel":10,"copper-cable":200,"iron-stick":100,"iron-gear-wheel":100,"empty-barrel":10,"electronic-circuit":200,"advanced-circuit":200,"processing-unit":100,"engine-unit":50,"electric-engine-unit":50,"flying-robot-frame":50,"rocket-control-unit":10,"low-density-structure":10,"rocket-fuel":10,"rocket-part":5,"nuclear-fuel":1,"uranium-235":100,"uranium-238":100,"uranium-fuel-cell":50,"used-up-uranium-fuel-cell":50,"automation-science-pack":200,"logistic-science-pack":200,"military-science-pack":200,"chemical-science-pack":200,"production-science-pack":200,"utility-science-pack":200,"space-science-pack":2000,"coin":100000,"pistol":5,"submachine-gun":5,"tank-machine-gun":1,"vehicle-machine-gun":1,"tank-flamethrower":1,"shotgun":5,"combat-shotgun":5,"rocket-launcher":5,"flamethrower":5,"land-mine":100,"artillery-wagon-cannon":1,"spidertron-rocket-launcher-1":1,"spidertron-rocket-launcher-2":1,"spidertron-rocket-launcher-3":1,"spidertron-rocket-launcher-4":1,"tank-cannon":1,"firearm-magazine":200,"piercing-rounds-magazine":200,"uranium-rounds-magazine":200,"shotgun-shell":200,"piercing-shotgun-shell":200,"cannon-shell":200,"explosive-cannon-shell":200,"uranium-cannon-shell":200,"explosive-uranium-cannon-shell":200,"artillery-shell":1,"rocket":200,"explosive-rocket":200,"atomic-bomb":10,"flamethrower-ammo":100,"grenade":100,"cluster-grenade":100,"poison-capsule":100,"slowdown-capsule":100,"defender-capsule":100,"distractor-capsule":100,"destroyer-capsule":100,"light-armor":1,"heavy-armor":1,"modular-armor":1,"power-armor":1,"power-armor-mk2":1,"solar-panel-equipment":20,"fusion-reactor-equipment":20,"battery-equipment":20,"battery-mk2-equipment":20,"belt-immunity-equipment":20,"exoskeleton-equipment":20,"personal-roboport-equipment":20,"personal-roboport-mk2-equipment":20,"night-vision-equipment":20,"energy-shield-equipment":20,"energy-shield-mk2-equipment":20,"personal-laser-defense-equipment":20,"discharge-defense-equipment":20,"discharge-defense-remote":1,"stone-wall":100,"gate":50,"gun-turret":50,"laser-turret":50,"flamethrower-turret":50,"artillery-turret":10,"artillery-targeting-remote":1,"radar":50,"player-port":50,"item-unknown":1,"electric-energy-interface":50,"linked-chest":10,"heat-interface":20,"linked-belt":10,"infinity-chest":10,"infinity-pipe":10,"selection-tool":1,"item-with-inventory":1,"item-with-label":1,"item-with-tags":1,"simple-entity-with-force":50,"simple-entity-with-owner":50,"burner-generator":10} \ No newline at end of file +{"wooden-chest":50,"iron-chest":50,"steel-chest":50,"storage-tank":50,"transport-belt":100,"fast-transport-belt":100,"express-transport-belt":100,"underground-belt":50,"fast-underground-belt":50,"express-underground-belt":50,"splitter":50,"fast-splitter":50,"express-splitter":50,"loader":50,"fast-loader":50,"express-loader":50,"burner-inserter":50,"inserter":50,"long-handed-inserter":50,"fast-inserter":50,"bulk-inserter":50,"small-electric-pole":50,"medium-electric-pole":50,"big-electric-pole":50,"substation":50,"pipe":100,"pipe-to-ground":50,"pump":50,"rail":100,"train-stop":10,"rail-signal":50,"rail-chain-signal":50,"locomotive":5,"cargo-wagon":5,"fluid-wagon":5,"artillery-wagon":5,"car":1,"tank":1,"spidertron":1,"logistic-robot":50,"construction-robot":50,"active-provider-chest":50,"passive-provider-chest":50,"storage-chest":50,"buffer-chest":50,"requester-chest":50,"roboport":10,"small-lamp":50,"arithmetic-combinator":50,"decider-combinator":50,"selector-combinator":50,"constant-combinator":50,"power-switch":10,"programmable-speaker":10,"display-panel":10,"stone-brick":100,"concrete":100,"hazard-concrete":100,"refined-concrete":100,"refined-hazard-concrete":100,"landfill":100,"cliff-explosives":20,"repair-pack":100,"blueprint":1,"deconstruction-planner":1,"upgrade-planner":1,"blueprint-book":1,"copy-paste-tool":1,"cut-paste-tool":1,"boiler":50,"steam-engine":10,"solar-panel":50,"accumulator":50,"nuclear-reactor":10,"heat-pipe":50,"heat-exchanger":50,"steam-turbine":10,"burner-mining-drill":50,"electric-mining-drill":50,"offshore-pump":20,"pumpjack":20,"stone-furnace":50,"steel-furnace":50,"electric-furnace":50,"assembling-machine-1":50,"assembling-machine-2":50,"assembling-machine-3":50,"oil-refinery":10,"chemical-plant":10,"centrifuge":50,"lab":10,"beacon":20,"speed-module":50,"speed-module-2":50,"speed-module-3":50,"efficiency-module":50,"efficiency-module-2":50,"efficiency-module-3":50,"productivity-module":50,"productivity-module-2":50,"productivity-module-3":50,"empty-module-slot":1,"rocket-silo":1,"cargo-landing-pad":1,"satellite":1,"wood":100,"coal":50,"stone":50,"iron-ore":50,"copper-ore":50,"uranium-ore":50,"raw-fish":100,"iron-plate":100,"copper-plate":100,"steel-plate":100,"solid-fuel":50,"plastic-bar":100,"sulfur":50,"battery":200,"explosives":50,"water-barrel":10,"crude-oil-barrel":10,"petroleum-gas-barrel":10,"light-oil-barrel":10,"heavy-oil-barrel":10,"lubricant-barrel":10,"sulfuric-acid-barrel":10,"iron-gear-wheel":100,"iron-stick":100,"copper-cable":200,"barrel":10,"electronic-circuit":200,"advanced-circuit":200,"processing-unit":100,"engine-unit":50,"electric-engine-unit":50,"flying-robot-frame":50,"low-density-structure":50,"rocket-fuel":20,"rocket-part":5,"uranium-235":100,"uranium-238":100,"uranium-fuel-cell":50,"depleted-uranium-fuel-cell":50,"nuclear-fuel":1,"automation-science-pack":200,"logistic-science-pack":200,"military-science-pack":200,"chemical-science-pack":200,"production-science-pack":200,"utility-science-pack":200,"space-science-pack":2000,"coin":100000,"science":1,"pistol":5,"submachine-gun":5,"tank-machine-gun":1,"vehicle-machine-gun":1,"tank-flamethrower":1,"shotgun":5,"combat-shotgun":5,"rocket-launcher":5,"flamethrower":5,"artillery-wagon-cannon":1,"spidertron-rocket-launcher-1":1,"spidertron-rocket-launcher-2":1,"spidertron-rocket-launcher-3":1,"spidertron-rocket-launcher-4":1,"tank-cannon":1,"firearm-magazine":100,"piercing-rounds-magazine":100,"uranium-rounds-magazine":100,"shotgun-shell":100,"piercing-shotgun-shell":100,"cannon-shell":100,"explosive-cannon-shell":100,"uranium-cannon-shell":100,"explosive-uranium-cannon-shell":100,"artillery-shell":1,"rocket":100,"explosive-rocket":100,"atomic-bomb":10,"flamethrower-ammo":100,"grenade":100,"cluster-grenade":100,"poison-capsule":100,"slowdown-capsule":100,"defender-capsule":100,"distractor-capsule":100,"destroyer-capsule":100,"light-armor":1,"heavy-armor":1,"modular-armor":1,"power-armor":1,"power-armor-mk2":1,"solar-panel-equipment":20,"fission-reactor-equipment":20,"battery-equipment":20,"battery-mk2-equipment":20,"belt-immunity-equipment":20,"exoskeleton-equipment":20,"personal-roboport-equipment":20,"personal-roboport-mk2-equipment":20,"night-vision-equipment":20,"energy-shield-equipment":20,"energy-shield-mk2-equipment":20,"personal-laser-defense-equipment":20,"discharge-defense-equipment":20,"stone-wall":100,"gate":50,"radar":50,"land-mine":100,"gun-turret":50,"laser-turret":50,"flamethrower-turret":50,"artillery-turret":10,"parameter-0":1,"parameter-1":1,"parameter-2":1,"parameter-3":1,"parameter-4":1,"parameter-5":1,"parameter-6":1,"parameter-7":1,"parameter-8":1,"parameter-9":1,"copper-wire":1,"green-wire":1,"red-wire":1,"spidertron-remote":1,"discharge-defense-remote":1,"artillery-targeting-remote":1,"item-unknown":1,"electric-energy-interface":50,"linked-chest":10,"heat-interface":20,"lane-splitter":50,"linked-belt":10,"infinity-chest":10,"infinity-pipe":10,"selection-tool":1,"simple-entity-with-force":50,"simple-entity-with-owner":50,"burner-generator":10} \ No newline at end of file diff --git a/worlds/factorio/data/machines.json b/worlds/factorio/data/machines.json index 15a79580d060..c8629ab8bef0 100644 --- a/worlds/factorio/data/machines.json +++ b/worlds/factorio/data/machines.json @@ -1 +1 @@ -{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}} \ No newline at end of file +{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true,"parameters":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true,"parameters":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true,"parameters":true},"oil-refinery":{"oil-processing":true,"parameters":true},"chemical-plant":{"chemistry":true,"parameters":true},"centrifuge":{"centrifuging":true,"parameters":true},"rocket-silo":{"rocket-building":true,"parameters":true},"character":{"crafting":true}} \ No newline at end of file diff --git a/worlds/factorio/data/mod/lib.lua b/worlds/factorio/data/mod/lib.lua index 2b18f119a427..7be7403e48f1 100644 --- a/worlds/factorio/data/mod/lib.lua +++ b/worlds/factorio/data/mod/lib.lua @@ -1,9 +1,9 @@ function get_any_stack_size(name) - local item = game.item_prototypes[name] + local item = prototypes.item[name] if item ~= nil then return item.stack_size end - item = game.equipment_prototypes[name] + item = prototypes.equipment[name] if item ~= nil then return item.stack_size end @@ -24,7 +24,7 @@ function split(s, sep) end function random_offset_position(position, offset) - return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-1024, 1024)} + return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-offset, offset)} end function fire_entity_at_players(entity_name, speed) @@ -36,4 +36,4 @@ function fire_entity_at_players(entity_name, speed) target=current_character, speed=speed} end end -end \ No newline at end of file +end diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index ace231e12b4b..51a588a94cb9 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -143,24 +143,24 @@ function on_check_energy_link(event) local force = "player" local bridges = surface.find_entities_filtered({name="ap-energy-bridge", force=force}) local bridgecount = table_size(bridges) - global.forcedata[force].energy_bridges = bridgecount - if global.forcedata[force].energy == nil then - global.forcedata[force].energy = 0 + storage.forcedata[force].energy_bridges = bridgecount + if storage.forcedata[force].energy == nil then + storage.forcedata[force].energy = 0 end - if global.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then + if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then for i, bridge in ipairs(bridges) do if bridge.energy > ENERGY_INCREMENT*3 then - global.forcedata[force].energy = global.forcedata[force].energy + (ENERGY_INCREMENT * ENERGY_LINK_EFFICIENCY) + storage.forcedata[force].energy = storage.forcedata[force].energy + (ENERGY_INCREMENT * ENERGY_LINK_EFFICIENCY) bridge.energy = bridge.energy - ENERGY_INCREMENT end end end for i, bridge in ipairs(bridges) do - if global.forcedata[force].energy < ENERGY_INCREMENT then + if storage.forcedata[force].energy < ENERGY_INCREMENT then break end - if bridge.energy < ENERGY_INCREMENT*2 and global.forcedata[force].energy > ENERGY_INCREMENT then - global.forcedata[force].energy = global.forcedata[force].energy - ENERGY_INCREMENT + if bridge.energy < ENERGY_INCREMENT*2 and storage.forcedata[force].energy > ENERGY_INCREMENT then + storage.forcedata[force].energy = storage.forcedata[force].energy - ENERGY_INCREMENT bridge.energy = bridge.energy + ENERGY_INCREMENT end end @@ -186,23 +186,41 @@ function check_spawn_silo(force) local surface = game.get_surface(1) local spawn_position = force.get_spawn_position(surface) spawn_entity(surface, force, "rocket-silo", spawn_position.x, spawn_position.y, 80, true, true) + spawn_entity(surface, force, "cargo-landing-pad", spawn_position.x, spawn_position.y, 80, true, true) end end function check_despawn_silo(force) - if not force.players or #force.players < 1 and force.get_entity_count("rocket-silo") > 0 then - local surface = game.get_surface(1) - local spawn_position = force.get_spawn_position(surface) - local x1 = spawn_position.x - 41 - local x2 = spawn_position.x + 41 - local y1 = spawn_position.y - 41 - local y2 = spawn_position.y + 41 - local silos = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} }, - name = "rocket-silo", - force = force} - for i,silo in ipairs(silos) do - silo.destructible = true - silo.destroy() + if not force.players or #force.players < 1 then + if force.get_entity_count("rocket-silo") > 0 then + local surface = game.get_surface(1) + local spawn_position = force.get_spawn_position(surface) + local x1 = spawn_position.x - 41 + local x2 = spawn_position.x + 41 + local y1 = spawn_position.y - 41 + local y2 = spawn_position.y + 41 + local silos = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} }, + name = "rocket-silo", + force = force} + for i, silo in ipairs(silos) do + silo.destructible = true + silo.destroy() + end + end + if force.get_entity_count("cargo-landing-pad") > 0 then + local surface = game.get_surface(1) + local spawn_position = force.get_spawn_position(surface) + local x1 = spawn_position.x - 41 + local x2 = spawn_position.x + 41 + local y1 = spawn_position.y - 41 + local y2 = spawn_position.y + 41 + local pads = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} }, + name = "cargo-landing-pad", + force = force} + for i, pad in ipairs(pads) do + pad.destructible = true + pad.destroy() + end end end end @@ -214,19 +232,18 @@ function on_force_created(event) if type(event.force) == "string" then -- should be of type LuaForce force = game.forces[force] end - force.research_queue_enabled = true local data = {} data['earned_samples'] = {{ dict_to_lua(starting_items) }} data["victory"] = 0 data["death_link_tick"] = 0 data["energy"] = 0 data["energy_bridges"] = 0 - global.forcedata[event.force] = data + storage.forcedata[event.force] = data {%- if silo == 2 %} check_spawn_silo(force) {%- endif %} -{%- for tech_name in useless_technologies %} - force.technologies.{{ tech_name }}.researched = true +{%- for tech_name in removed_technologies %} + force.technologies["{{ tech_name }}"].researched = true {%- endfor %} end script.on_event(defines.events.on_force_created, on_force_created) @@ -236,7 +253,7 @@ function on_force_destroyed(event) {%- if silo == 2 %} check_despawn_silo(event.force) {%- endif %} - global.forcedata[event.force.name] = nil + storage.forcedata[event.force.name] = nil end function on_runtime_mod_setting_changed(event) @@ -267,8 +284,8 @@ function on_player_created(event) -- FIXME: This (probably) fires before any other mod has a chance to change the player's force -- For now, they will (probably) always be on the 'player' force when this event fires. local data = {} - data['pending_samples'] = table.deepcopy(global.forcedata[player.force.name]['earned_samples']) - global.playerdata[player.index] = data + data['pending_samples'] = table.deepcopy(storage.forcedata[player.force.name]['earned_samples']) + storage.playerdata[player.index] = data update_player(player.index) -- Attempt to send pending free samples, if relevant. {%- if silo == 2 %} check_spawn_silo(game.players[event.player_index].force) @@ -287,14 +304,14 @@ end script.on_event(defines.events.on_player_changed_force, on_player_changed_force) function on_player_removed(event) - global.playerdata[event.player_index] = nil + storage.playerdata[event.player_index] = nil end script.on_event(defines.events.on_player_removed, on_player_removed) function on_rocket_launched(event) - if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then + if event.rocket and event.rocket.valid and storage.forcedata[event.rocket.force.name]['victory'] == 0 then if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then - global.forcedata[event.rocket.force.name]['victory'] = 1 + storage.forcedata[event.rocket.force.name]['victory'] = 1 dumpInfo(event.rocket.force) game.set_game_state { @@ -318,7 +335,7 @@ function update_player(index) if not character or not character.valid then return end - local data = global.playerdata[index] + local data = storage.playerdata[index] local samples = data['pending_samples'] local sent --player.print(serpent.block(data['pending_samples'])) @@ -327,14 +344,17 @@ function update_player(index) for name, count in pairs(samples) do stack.name = name stack.count = count - if game.item_prototypes[name] then + if script.active_mods["quality"] then + stack.quality = "{{ free_sample_quality_name }}" + end + if prototypes.item[name] then if character.can_insert(stack) then sent = character.insert(stack) else sent = 0 end if sent > 0 then - player.print("Received " .. sent .. "x [item=" .. name .. "]") + player.print("Received " .. sent .. "x [item=" .. name .. ",quality={{ free_sample_quality_name }}]") data.suppress_full_inventory_message = false end if sent ~= count then -- Couldn't full send. @@ -372,19 +392,19 @@ function add_samples(force, name, count) end t[name] = (t[name] or 0) + count end - -- Add to global table of earned samples for future new players - add_to_table(global.forcedata[force.name]['earned_samples']) + -- Add to storage table of earned samples for future new players + add_to_table(storage.forcedata[force.name]['earned_samples']) -- Add to existing players for _, player in pairs(force.players) do - add_to_table(global.playerdata[player.index]['pending_samples']) + add_to_table(storage.playerdata[player.index]['pending_samples']) update_player(player.index) end end script.on_init(function() {% if not imported_blueprints %}set_permissions(){% endif %} - global.forcedata = {} - global.playerdata = {} + storage.forcedata = {} + storage.playerdata = {} -- Fire dummy events for all currently existing forces. local e = {} for name, _ in pairs(game.forces) do @@ -420,12 +440,12 @@ script.on_event(defines.events.on_research_finished, function(event) if FREE_SAMPLES == 0 then return -- Nothing else to do end - if not technology.effects then + if not technology.prototype.effects then return -- No technology effects, so nothing to do. end - for _, effect in pairs(technology.effects) do + for _, effect in pairs(technology.prototype.effects) do if effect.type == "unlock-recipe" then - local recipe = game.recipe_prototypes[effect.recipe] + local recipe = prototypes.recipe[effect.recipe] for _, result in pairs(recipe.products) do if result.type == "item" and result.amount then local name = result.name @@ -477,7 +497,7 @@ function kill_players(force) end function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) - local prototype = game.entity_prototypes[name] + local prototype = prototypes.entity[name] local args = { -- For can_place_entity and place_entity name = prototype.name, position = {x = x, y = y}, @@ -537,7 +557,7 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) } local entities = surface.find_entities_filtered { area = collision_area, - collision_mask = prototype.collision_mask + collision_mask = prototype.collision_mask.layers } local can_place = true for _, entity in pairs(entities) do @@ -560,6 +580,9 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) end args.build_check_type = defines.build_check_type.script args.create_build_effect_smoke = false + if script.active_mods["quality"] then + args.quality = "{{ free_sample_quality_name }}" + end new_entity = surface.create_entity(args) if new_entity then new_entity.destructible = false @@ -585,7 +608,7 @@ script.on_event(defines.events.on_entity_died, function(event) end local force = event.entity.force - global.forcedata[force.name].death_link_tick = game.tick + storage.forcedata[force.name].death_link_tick = game.tick dumpInfo(force) kill_players(force) end, {LuaEntityDiedEventFilter = {["filter"] = "name", ["name"] = "character"}}) @@ -600,7 +623,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress force = game.players[call.player_index].force end local research_done = {} - local forcedata = chain_lookup(global, "forcedata", force.name) + local forcedata = chain_lookup(storage, "forcedata", force.name) local data_collection = { ["research_done"] = research_done, ["victory"] = chain_lookup(forcedata, "victory"), @@ -616,7 +639,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress research_done[tech_name] = tech.researched end end - rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection})) + rcon.print(helpers.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection})) end) commands.add_command("ap-print", "Used by the Archipelago client to print messages", function (call) @@ -655,8 +678,8 @@ end, } commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call) - if global.index_sync == nil then - global.index_sync = {} + if storage.index_sync == nil then + storage.index_sync = {} end local tech local force = game.forces["player"] @@ -680,8 +703,8 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end return elseif progressive_technologies[item_name] ~= nil then - if global.index_sync[index] ~= item_name then -- not yet received prog item - global.index_sync[index] = item_name + if storage.index_sync[index] ~= item_name then -- not yet received prog item + storage.index_sync[index] = item_name local tech_stack = progressive_technologies[item_name] for _, item_name in ipairs(tech_stack) do tech = force.technologies[item_name] @@ -696,7 +719,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi elseif force.technologies[item_name] ~= nil then tech = force.technologies[item_name] if tech ~= nil then - global.index_sync[index] = tech + storage.index_sync[index] = tech if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] from ", source}) game.play_sound({path="utility/research_completed"}) @@ -704,8 +727,8 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end end elseif TRAP_TABLE[item_name] ~= nil then - if global.index_sync[index] ~= item_name then -- not yet received trap - global.index_sync[index] = item_name + if storage.index_sync[index] ~= item_name then -- not yet received trap + storage.index_sync[index] = item_name game.print({"", "Received ", item_name, " from ", source}) TRAP_TABLE[item_name]() end @@ -716,7 +739,7 @@ end) commands.add_command("ap-rcon-info", "Used by the Archipelago client to get information", function(call) - rcon.print(game.table_to_json({ + rcon.print(helpers.table_to_json({ ["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["death_link"] = DEATH_LINK, @@ -726,7 +749,7 @@ end) {% if allow_cheats -%} -commands.add_command("ap-spawn-silo", "Attempts to spawn a silo around 0,0", function(call) +commands.add_command("ap-spawn-silo", "Attempts to spawn a silo and cargo landing pad around 0,0", function(call) spawn_entity(game.player.surface, game.player.force, "rocket-silo", 0, 0, 80, true, true) end) {% endif -%} @@ -742,7 +765,7 @@ end) commands.add_command("ap-energylink", "Used by the Archipelago client to manage Energy Link", function(call) local change = tonumber(call.parameter or "0") local force = "player" - global.forcedata[force].energy = global.forcedata[force].energy + change + storage.forcedata[force].energy = storage.forcedata[force].energy + change end) commands.add_command("energy-link", "Print the status of the Archipelago energy link.", function(call) diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 3021fd5dadca..dc068c4f62aa 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -6,43 +6,46 @@ data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = { production_type = "input", pipe_picture = assembler2pipepictures(), pipe_covers = pipecoverspictures(), + volume = 1000, 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 } } + { flow_direction = "input", direction = defines.direction.south, position = { 0, 4.2 } }, + { flow_direction = "input", direction = defines.direction.north, position = { 0, -4.2 } }, + { flow_direction = "input", direction = defines.direction.east, position = { 4.2, 0 } }, + { flow_direction = "input", direction = defines.direction.west, position = { -4.2, 0 } } } }, { production_type = "input", pipe_picture = assembler2pipepictures(), pipe_covers = pipecoverspictures(), + volume = 1000, 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 } } + { flow_direction = "input", direction = defines.direction.south, position = { -3, 4.2 } }, + { flow_direction = "input", direction = defines.direction.north, position = { -3, -4.2 } }, + { flow_direction = "input", direction = defines.direction.east, position = { 4.2, -3 } }, + { flow_direction = "input", direction = defines.direction.west, position = { -4.2, -3 } } } }, { production_type = "input", pipe_picture = assembler2pipepictures(), pipe_covers = pipecoverspictures(), + volume = 1000, 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 } } + { flow_direction = "input", direction = defines.direction.south, position = { 3, 4.2 } }, + { flow_direction = "input", direction = defines.direction.north, position = { 3, -4.2 } }, + { flow_direction = "input", direction = defines.direction.east, position = { 4.2, 3 } }, + { flow_direction = "input", direction = defines.direction.west, position = { -4.2, 3 } } } - }, - off_when_no_fluid_recipe = true + } } +data.raw["rocket-silo"]["rocket-silo"].fluid_boxes_off_when_no_fluid_recipe = true {%- for recipe_name, recipe in custom_recipes.items() %} data.raw["recipe"]["{{recipe_name}}"].category = "{{recipe.category}}" diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua index 82053453eadf..43151ff00840 100644 --- a/worlds/factorio/data/mod_template/data.lua +++ b/worlds/factorio/data/mod_template/data.lua @@ -18,12 +18,9 @@ energy_bridge.energy_source.buffer_capacity = "50MJ" energy_bridge.energy_source.input_flow_limit = "10MW" energy_bridge.energy_source.output_flow_limit = "10MW" tint_icon(energy_bridge, energy_bridge_tint()) -energy_bridge.picture.layers[1].tint = energy_bridge_tint() -energy_bridge.picture.layers[1].hr_version.tint = energy_bridge_tint() -energy_bridge.charge_animation.layers[1].layers[1].tint = energy_bridge_tint() -energy_bridge.charge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() -energy_bridge.discharge_animation.layers[1].layers[1].tint = energy_bridge_tint() -energy_bridge.discharge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() +energy_bridge.chargable_graphics.picture.layers[1].tint = energy_bridge_tint() +energy_bridge.chargable_graphics.charge_animation.layers[1].layers[1].tint = energy_bridge_tint() +energy_bridge.chargable_graphics.discharge_animation.layers[1].layers[1].tint = energy_bridge_tint() data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"]) @@ -35,9 +32,9 @@ data.raw["item"]["ap-energy-bridge"] = energy_bridge_item local energy_bridge_recipe = table.deepcopy(data.raw["recipe"]["accumulator"]) energy_bridge_recipe.name = "ap-energy-bridge" -energy_bridge_recipe.result = energy_bridge_item.name +energy_bridge_recipe.results = { {type = "item", name = energy_bridge_item.name, amount = 1} } energy_bridge_recipe.energy_required = 1 -energy_bridge_recipe.enabled = {{ energy_link }} +energy_bridge_recipe.enabled = {% if energy_link %}true{% else %}false{% endif %} energy_bridge_recipe.localised_name = "Archipelago EnergyLink Bridge" data.raw["recipe"]["ap-energy-bridge"] = energy_bridge_recipe diff --git a/worlds/factorio/data/mod_template/macros.lua b/worlds/factorio/data/mod_template/macros.lua index 1b271031a393..f1530359c823 100644 --- a/worlds/factorio/data/mod_template/macros.lua +++ b/worlds/factorio/data/mod_template/macros.lua @@ -26,4 +26,4 @@ {type = {% if key in liquids %}"fluid"{% else %}"item"{% endif %}, name = "{{ key }}", amount = {{ value | safe }}}{% if not loop.last %},{% endif %} {% endfor -%} } -{%- endmacro %} \ No newline at end of file +{%- endmacro %} diff --git a/worlds/factorio/data/mod_template/settings.lua b/worlds/factorio/data/mod_template/settings.lua index 73e131a60e7c..41d30e58d552 100644 --- a/worlds/factorio/data/mod_template/settings.lua +++ b/worlds/factorio/data/mod_template/settings.lua @@ -27,4 +27,4 @@ data:extend({ default_value = false {% endif %} } -}) \ No newline at end of file +}) diff --git a/worlds/factorio/data/recipes.json b/worlds/factorio/data/recipes.json index 4c4ab81526af..b0633b493d79 100644 --- a/worlds/factorio/data/recipes.json +++ b/worlds/factorio/data/recipes.json @@ -1 +1 @@ -{"accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting","energy":10},"advanced-circuit":{"ingredients":{"plastic-bar":2,"copper-cable":4,"electronic-circuit":2},"products":{"advanced-circuit":1},"category":"crafting","energy":6},"arithmetic-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"arithmetic-combinator":1},"category":"crafting","energy":0.5},"artillery-shell":{"ingredients":{"explosives":8,"explosive-cannon-shell":4,"radar":1},"products":{"artillery-shell":1},"category":"crafting","energy":15},"artillery-targeting-remote":{"ingredients":{"processing-unit":1,"radar":1},"products":{"artillery-targeting-remote":1},"category":"crafting","energy":0.5},"artillery-turret":{"ingredients":{"steel-plate":60,"iron-gear-wheel":40,"advanced-circuit":20,"concrete":60},"products":{"artillery-turret":1},"category":"crafting","energy":40},"artillery-wagon":{"ingredients":{"steel-plate":40,"iron-gear-wheel":10,"advanced-circuit":20,"engine-unit":64,"pipe":16},"products":{"artillery-wagon":1},"category":"crafting","energy":4},"assembling-machine-1":{"ingredients":{"iron-plate":9,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"assembling-machine-1":1},"category":"crafting","energy":0.5},"assembling-machine-2":{"ingredients":{"steel-plate":2,"iron-gear-wheel":5,"electronic-circuit":3,"assembling-machine-1":1},"products":{"assembling-machine-2":1},"category":"crafting","energy":0.5},"assembling-machine-3":{"ingredients":{"assembling-machine-2":2,"speed-module":4},"products":{"assembling-machine-3":1},"category":"crafting","energy":0.5},"atomic-bomb":{"ingredients":{"explosives":10,"rocket-control-unit":10,"uranium-235":30},"products":{"atomic-bomb":1},"category":"crafting","energy":50},"automation-science-pack":{"ingredients":{"copper-plate":1,"iron-gear-wheel":1},"products":{"automation-science-pack":1},"category":"crafting","energy":5},"battery":{"ingredients":{"iron-plate":1,"copper-plate":1,"sulfuric-acid":20},"products":{"battery":1},"category":"chemistry","energy":4},"battery-equipment":{"ingredients":{"steel-plate":10,"battery":5},"products":{"battery-equipment":1},"category":"crafting","energy":10},"battery-mk2-equipment":{"ingredients":{"processing-unit":15,"low-density-structure":5,"battery-equipment":10},"products":{"battery-mk2-equipment":1},"category":"crafting","energy":10},"beacon":{"ingredients":{"steel-plate":10,"copper-cable":10,"electronic-circuit":20,"advanced-circuit":20},"products":{"beacon":1},"category":"crafting","energy":15},"belt-immunity-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"belt-immunity-equipment":1},"category":"crafting","energy":10},"big-electric-pole":{"ingredients":{"copper-plate":5,"steel-plate":5,"iron-stick":8},"products":{"big-electric-pole":1},"category":"crafting","energy":0.5},"boiler":{"ingredients":{"pipe":4,"stone-furnace":1},"products":{"boiler":1},"category":"crafting","energy":0.5},"burner-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"burner-inserter":1},"category":"crafting","energy":0.5},"burner-mining-drill":{"ingredients":{"iron-plate":3,"iron-gear-wheel":3,"stone-furnace":1},"products":{"burner-mining-drill":1},"category":"crafting","energy":2},"cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":1},"products":{"cannon-shell":1},"category":"crafting","energy":8},"car":{"ingredients":{"iron-plate":20,"steel-plate":5,"engine-unit":8},"products":{"car":1},"category":"crafting","energy":2},"cargo-wagon":{"ingredients":{"iron-plate":20,"steel-plate":20,"iron-gear-wheel":10},"products":{"cargo-wagon":1},"category":"crafting","energy":1},"centrifuge":{"ingredients":{"steel-plate":50,"iron-gear-wheel":100,"advanced-circuit":100,"concrete":100},"products":{"centrifuge":1},"category":"crafting","energy":4},"chemical-plant":{"ingredients":{"steel-plate":5,"iron-gear-wheel":5,"electronic-circuit":5,"pipe":5},"products":{"chemical-plant":1},"category":"crafting","energy":5},"chemical-science-pack":{"ingredients":{"sulfur":1,"advanced-circuit":3,"engine-unit":2},"products":{"chemical-science-pack":2},"category":"crafting","energy":24},"cliff-explosives":{"ingredients":{"explosives":10,"empty-barrel":1,"grenade":1},"products":{"cliff-explosives":1},"category":"crafting","energy":8},"cluster-grenade":{"ingredients":{"steel-plate":5,"explosives":5,"grenade":7},"products":{"cluster-grenade":1},"category":"crafting","energy":8},"combat-shotgun":{"ingredients":{"wood":10,"copper-plate":10,"steel-plate":15,"iron-gear-wheel":5},"products":{"combat-shotgun":1},"category":"crafting","energy":10},"concrete":{"ingredients":{"iron-ore":1,"stone-brick":5,"water":100},"products":{"concrete":10},"category":"crafting-with-fluid","energy":10},"constant-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":2},"products":{"constant-combinator":1},"category":"crafting","energy":0.5},"construction-robot":{"ingredients":{"electronic-circuit":2,"flying-robot-frame":1},"products":{"construction-robot":1},"category":"crafting","energy":0.5},"copper-cable":{"ingredients":{"copper-plate":1},"products":{"copper-cable":2},"category":"crafting","energy":0.5},"copper-plate":{"ingredients":{"copper-ore":1},"products":{"copper-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"decider-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"decider-combinator":1},"category":"crafting","energy":0.5},"defender-capsule":{"ingredients":{"iron-gear-wheel":3,"electronic-circuit":3,"piercing-rounds-magazine":3},"products":{"defender-capsule":1},"category":"crafting","energy":8},"destroyer-capsule":{"ingredients":{"speed-module":1,"distractor-capsule":4},"products":{"destroyer-capsule":1},"category":"crafting","energy":15},"discharge-defense-equipment":{"ingredients":{"steel-plate":20,"processing-unit":5,"laser-turret":10},"products":{"discharge-defense-equipment":1},"category":"crafting","energy":10},"discharge-defense-remote":{"ingredients":{"electronic-circuit":1},"products":{"discharge-defense-remote":1},"category":"crafting","energy":0.5},"distractor-capsule":{"ingredients":{"advanced-circuit":3,"defender-capsule":4},"products":{"distractor-capsule":1},"category":"crafting","energy":15},"effectivity-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"effectivity-module":1},"category":"crafting","energy":15},"effectivity-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"effectivity-module":4},"products":{"effectivity-module-2":1},"category":"crafting","energy":30},"effectivity-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"effectivity-module-2":5},"products":{"effectivity-module-3":1},"category":"crafting","energy":60},"electric-engine-unit":{"ingredients":{"electronic-circuit":2,"engine-unit":1,"lubricant":15},"products":{"electric-engine-unit":1},"category":"crafting-with-fluid","energy":10},"electric-furnace":{"ingredients":{"steel-plate":10,"advanced-circuit":5,"stone-brick":10},"products":{"electric-furnace":1},"category":"crafting","energy":5},"electric-mining-drill":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"electric-mining-drill":1},"category":"crafting","energy":2},"electronic-circuit":{"ingredients":{"iron-plate":1,"copper-cable":3},"products":{"electronic-circuit":1},"category":"crafting","energy":0.5},"empty-barrel":{"ingredients":{"steel-plate":1},"products":{"empty-barrel":1},"category":"crafting","energy":1},"energy-shield-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"energy-shield-equipment":1},"category":"crafting","energy":10},"energy-shield-mk2-equipment":{"ingredients":{"processing-unit":5,"low-density-structure":5,"energy-shield-equipment":10},"products":{"energy-shield-mk2-equipment":1},"category":"crafting","energy":10},"engine-unit":{"ingredients":{"steel-plate":1,"iron-gear-wheel":1,"pipe":2},"products":{"engine-unit":1},"category":"advanced-crafting","energy":10},"exoskeleton-equipment":{"ingredients":{"steel-plate":20,"processing-unit":10,"electric-engine-unit":30},"products":{"exoskeleton-equipment":1},"category":"crafting","energy":10},"explosive-cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":2},"products":{"explosive-cannon-shell":1},"category":"crafting","energy":8},"explosive-rocket":{"ingredients":{"explosives":2,"rocket":1},"products":{"explosive-rocket":1},"category":"crafting","energy":8},"explosive-uranium-cannon-shell":{"ingredients":{"uranium-238":1,"explosive-cannon-shell":1},"products":{"explosive-uranium-cannon-shell":1},"category":"crafting","energy":12},"explosives":{"ingredients":{"coal":1,"sulfur":1,"water":10},"products":{"explosives":2},"category":"chemistry","energy":4},"express-splitter":{"ingredients":{"iron-gear-wheel":10,"advanced-circuit":10,"fast-splitter":1,"lubricant":80},"products":{"express-splitter":1},"category":"crafting-with-fluid","energy":2},"express-transport-belt":{"ingredients":{"iron-gear-wheel":10,"fast-transport-belt":1,"lubricant":20},"products":{"express-transport-belt":1},"category":"crafting-with-fluid","energy":0.5},"express-underground-belt":{"ingredients":{"iron-gear-wheel":80,"fast-underground-belt":2,"lubricant":40},"products":{"express-underground-belt":2},"category":"crafting-with-fluid","energy":2},"fast-inserter":{"ingredients":{"iron-plate":2,"electronic-circuit":2,"inserter":1},"products":{"fast-inserter":1},"category":"crafting","energy":0.5},"fast-splitter":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"splitter":1},"products":{"fast-splitter":1},"category":"crafting","energy":2},"fast-transport-belt":{"ingredients":{"iron-gear-wheel":5,"transport-belt":1},"products":{"fast-transport-belt":1},"category":"crafting","energy":0.5},"fast-underground-belt":{"ingredients":{"iron-gear-wheel":40,"underground-belt":2},"products":{"fast-underground-belt":2},"category":"crafting","energy":2},"filter-inserter":{"ingredients":{"electronic-circuit":4,"fast-inserter":1},"products":{"filter-inserter":1},"category":"crafting","energy":0.5},"firearm-magazine":{"ingredients":{"iron-plate":4},"products":{"firearm-magazine":1},"category":"crafting","energy":1},"flamethrower":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10},"products":{"flamethrower":1},"category":"crafting","energy":10},"flamethrower-ammo":{"ingredients":{"steel-plate":5,"crude-oil":100},"products":{"flamethrower-ammo":1},"category":"chemistry","energy":6},"flamethrower-turret":{"ingredients":{"steel-plate":30,"iron-gear-wheel":15,"engine-unit":5,"pipe":10},"products":{"flamethrower-turret":1},"category":"crafting","energy":20},"fluid-wagon":{"ingredients":{"steel-plate":16,"iron-gear-wheel":10,"storage-tank":1,"pipe":8},"products":{"fluid-wagon":1},"category":"crafting","energy":1.5},"flying-robot-frame":{"ingredients":{"steel-plate":1,"battery":2,"electronic-circuit":3,"electric-engine-unit":1},"products":{"flying-robot-frame":1},"category":"crafting","energy":20},"fusion-reactor-equipment":{"ingredients":{"processing-unit":200,"low-density-structure":50},"products":{"fusion-reactor-equipment":1},"category":"crafting","energy":10},"gate":{"ingredients":{"steel-plate":2,"electronic-circuit":2,"stone-wall":1},"products":{"gate":1},"category":"crafting","energy":0.5},"green-wire":{"ingredients":{"copper-cable":1,"electronic-circuit":1},"products":{"green-wire":1},"category":"crafting","energy":0.5},"grenade":{"ingredients":{"coal":10,"iron-plate":5},"products":{"grenade":1},"category":"crafting","energy":8},"gun-turret":{"ingredients":{"iron-plate":20,"copper-plate":10,"iron-gear-wheel":10},"products":{"gun-turret":1},"category":"crafting","energy":8},"hazard-concrete":{"ingredients":{"concrete":10},"products":{"hazard-concrete":10},"category":"crafting","energy":0.25},"heat-exchanger":{"ingredients":{"copper-plate":100,"steel-plate":10,"pipe":10},"products":{"heat-exchanger":1},"category":"crafting","energy":3},"heat-pipe":{"ingredients":{"copper-plate":20,"steel-plate":10},"products":{"heat-pipe":1},"category":"crafting","energy":1},"heavy-armor":{"ingredients":{"copper-plate":100,"steel-plate":50},"products":{"heavy-armor":1},"category":"crafting","energy":8},"inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"electronic-circuit":1},"products":{"inserter":1},"category":"crafting","energy":0.5},"iron-chest":{"ingredients":{"iron-plate":8},"products":{"iron-chest":1},"category":"crafting","energy":0.5},"iron-gear-wheel":{"ingredients":{"iron-plate":2},"products":{"iron-gear-wheel":1},"category":"crafting","energy":0.5},"iron-plate":{"ingredients":{"iron-ore":1},"products":{"iron-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"iron-stick":{"ingredients":{"iron-plate":1},"products":{"iron-stick":2},"category":"crafting","energy":0.5},"lab":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"transport-belt":4},"products":{"lab":1},"category":"crafting","energy":2},"land-mine":{"ingredients":{"steel-plate":1,"explosives":2},"products":{"land-mine":4},"category":"crafting","energy":5},"landfill":{"ingredients":{"stone":20},"products":{"landfill":1},"category":"crafting","energy":0.5},"laser-turret":{"ingredients":{"steel-plate":20,"battery":12,"electronic-circuit":20},"products":{"laser-turret":1},"category":"crafting","energy":20},"light-armor":{"ingredients":{"iron-plate":40},"products":{"light-armor":1},"category":"crafting","energy":3},"locomotive":{"ingredients":{"steel-plate":30,"electronic-circuit":10,"engine-unit":20},"products":{"locomotive":1},"category":"crafting","energy":4},"logistic-chest-active-provider":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-active-provider":1},"category":"crafting","energy":0.5},"logistic-chest-buffer":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-buffer":1},"category":"crafting","energy":0.5},"logistic-chest-passive-provider":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-passive-provider":1},"category":"crafting","energy":0.5},"logistic-chest-requester":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-requester":1},"category":"crafting","energy":0.5},"logistic-chest-storage":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-storage":1},"category":"crafting","energy":0.5},"logistic-robot":{"ingredients":{"advanced-circuit":2,"flying-robot-frame":1},"products":{"logistic-robot":1},"category":"crafting","energy":0.5},"logistic-science-pack":{"ingredients":{"transport-belt":1,"inserter":1},"products":{"logistic-science-pack":1},"category":"crafting","energy":6},"long-handed-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"inserter":1},"products":{"long-handed-inserter":1},"category":"crafting","energy":0.5},"low-density-structure":{"ingredients":{"copper-plate":20,"steel-plate":2,"plastic-bar":5},"products":{"low-density-structure":1},"category":"crafting","energy":20},"lubricant":{"ingredients":{"heavy-oil":10},"products":{"lubricant":10},"category":"chemistry","energy":1},"medium-electric-pole":{"ingredients":{"copper-plate":2,"steel-plate":2,"iron-stick":4},"products":{"medium-electric-pole":1},"category":"crafting","energy":0.5},"military-science-pack":{"ingredients":{"piercing-rounds-magazine":1,"grenade":1,"stone-wall":2},"products":{"military-science-pack":2},"category":"crafting","energy":10},"modular-armor":{"ingredients":{"steel-plate":50,"advanced-circuit":30},"products":{"modular-armor":1},"category":"crafting","energy":15},"night-vision-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"night-vision-equipment":1},"category":"crafting","energy":10},"nuclear-fuel":{"ingredients":{"rocket-fuel":1,"uranium-235":1},"products":{"nuclear-fuel":1},"category":"centrifuging","energy":90},"nuclear-reactor":{"ingredients":{"copper-plate":500,"steel-plate":500,"advanced-circuit":500,"concrete":500},"products":{"nuclear-reactor":1},"category":"crafting","energy":8},"offshore-pump":{"ingredients":{"iron-gear-wheel":1,"electronic-circuit":2,"pipe":1},"products":{"offshore-pump":1},"category":"crafting","energy":0.5},"oil-refinery":{"ingredients":{"steel-plate":15,"iron-gear-wheel":10,"electronic-circuit":10,"pipe":10,"stone-brick":10},"products":{"oil-refinery":1},"category":"crafting","energy":8},"personal-laser-defense-equipment":{"ingredients":{"processing-unit":20,"low-density-structure":5,"laser-turret":5},"products":{"personal-laser-defense-equipment":1},"category":"crafting","energy":10},"personal-roboport-equipment":{"ingredients":{"steel-plate":20,"battery":45,"iron-gear-wheel":40,"advanced-circuit":10},"products":{"personal-roboport-equipment":1},"category":"crafting","energy":10},"personal-roboport-mk2-equipment":{"ingredients":{"processing-unit":100,"low-density-structure":20,"personal-roboport-equipment":5},"products":{"personal-roboport-mk2-equipment":1},"category":"crafting","energy":20},"piercing-rounds-magazine":{"ingredients":{"copper-plate":5,"steel-plate":1,"firearm-magazine":1},"products":{"piercing-rounds-magazine":1},"category":"crafting","energy":3},"piercing-shotgun-shell":{"ingredients":{"copper-plate":5,"steel-plate":2,"shotgun-shell":2},"products":{"piercing-shotgun-shell":1},"category":"crafting","energy":8},"pipe":{"ingredients":{"iron-plate":1},"products":{"pipe":1},"category":"crafting","energy":0.5},"pipe-to-ground":{"ingredients":{"iron-plate":5,"pipe":10},"products":{"pipe-to-ground":2},"category":"crafting","energy":0.5},"pistol":{"ingredients":{"iron-plate":5,"copper-plate":5},"products":{"pistol":1},"category":"crafting","energy":5},"plastic-bar":{"ingredients":{"coal":1,"petroleum-gas":20},"products":{"plastic-bar":2},"category":"chemistry","energy":1},"poison-capsule":{"ingredients":{"coal":10,"steel-plate":3,"electronic-circuit":3},"products":{"poison-capsule":1},"category":"crafting","energy":8},"power-armor":{"ingredients":{"steel-plate":40,"processing-unit":40,"electric-engine-unit":20},"products":{"power-armor":1},"category":"crafting","energy":20},"power-armor-mk2":{"ingredients":{"processing-unit":60,"electric-engine-unit":40,"low-density-structure":30,"speed-module-2":25,"effectivity-module-2":25},"products":{"power-armor-mk2":1},"category":"crafting","energy":25},"power-switch":{"ingredients":{"iron-plate":5,"copper-cable":5,"electronic-circuit":2},"products":{"power-switch":1},"category":"crafting","energy":2},"processing-unit":{"ingredients":{"electronic-circuit":20,"advanced-circuit":2,"sulfuric-acid":5},"products":{"processing-unit":1},"category":"crafting-with-fluid","energy":10},"production-science-pack":{"ingredients":{"rail":30,"electric-furnace":1,"productivity-module":1},"products":{"production-science-pack":3},"category":"crafting","energy":21},"productivity-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"productivity-module":1},"category":"crafting","energy":15},"productivity-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module":4},"products":{"productivity-module-2":1},"category":"crafting","energy":30},"productivity-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module-2":5},"products":{"productivity-module-3":1},"category":"crafting","energy":60},"programmable-speaker":{"ingredients":{"iron-plate":3,"copper-cable":5,"iron-stick":4,"electronic-circuit":4},"products":{"programmable-speaker":1},"category":"crafting","energy":2},"pump":{"ingredients":{"steel-plate":1,"engine-unit":1,"pipe":1},"products":{"pump":1},"category":"crafting","energy":2},"pumpjack":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10,"electronic-circuit":5,"pipe":10},"products":{"pumpjack":1},"category":"crafting","energy":5},"radar":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"radar":1},"category":"crafting","energy":0.5},"rail":{"ingredients":{"stone":1,"steel-plate":1,"iron-stick":1},"products":{"rail":2},"category":"crafting","energy":0.5},"rail-chain-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-chain-signal":1},"category":"crafting","energy":0.5},"rail-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-signal":1},"category":"crafting","energy":0.5},"red-wire":{"ingredients":{"copper-cable":1,"electronic-circuit":1},"products":{"red-wire":1},"category":"crafting","energy":0.5},"refined-concrete":{"ingredients":{"steel-plate":1,"iron-stick":8,"concrete":20,"water":100},"products":{"refined-concrete":10},"category":"crafting-with-fluid","energy":15},"refined-hazard-concrete":{"ingredients":{"refined-concrete":10},"products":{"refined-hazard-concrete":10},"category":"crafting","energy":0.25},"repair-pack":{"ingredients":{"iron-gear-wheel":2,"electronic-circuit":2},"products":{"repair-pack":1},"category":"crafting","energy":0.5},"roboport":{"ingredients":{"steel-plate":45,"iron-gear-wheel":45,"advanced-circuit":45},"products":{"roboport":1},"category":"crafting","energy":5},"rocket":{"ingredients":{"iron-plate":2,"explosives":1,"electronic-circuit":1},"products":{"rocket":1},"category":"crafting","energy":8},"rocket-control-unit":{"ingredients":{"processing-unit":1,"speed-module":1},"products":{"rocket-control-unit":1},"category":"crafting","energy":30},"rocket-fuel":{"ingredients":{"solid-fuel":10,"light-oil":10},"products":{"rocket-fuel":1},"category":"crafting-with-fluid","energy":30},"rocket-launcher":{"ingredients":{"iron-plate":5,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"rocket-launcher":1},"category":"crafting","energy":10},"rocket-part":{"ingredients":{"rocket-control-unit":10,"low-density-structure":10,"rocket-fuel":10},"products":{"rocket-part":1},"category":"rocket-building","energy":3},"rocket-silo":{"ingredients":{"steel-plate":1000,"processing-unit":200,"electric-engine-unit":200,"pipe":100,"concrete":1000},"products":{"rocket-silo":1},"category":"crafting","energy":30},"satellite":{"ingredients":{"processing-unit":100,"low-density-structure":100,"rocket-fuel":50,"solar-panel":100,"accumulator":100,"radar":5},"products":{"satellite":1},"category":"crafting","energy":5},"shotgun":{"ingredients":{"wood":5,"iron-plate":15,"copper-plate":10,"iron-gear-wheel":5},"products":{"shotgun":1},"category":"crafting","energy":10},"shotgun-shell":{"ingredients":{"iron-plate":2,"copper-plate":2},"products":{"shotgun-shell":1},"category":"crafting","energy":3},"slowdown-capsule":{"ingredients":{"coal":5,"steel-plate":2,"electronic-circuit":2},"products":{"slowdown-capsule":1},"category":"crafting","energy":8},"small-electric-pole":{"ingredients":{"wood":1,"copper-cable":2},"products":{"small-electric-pole":2},"category":"crafting","energy":0.5},"small-lamp":{"ingredients":{"iron-plate":1,"copper-cable":3,"electronic-circuit":1},"products":{"small-lamp":1},"category":"crafting","energy":0.5},"solar-panel":{"ingredients":{"copper-plate":5,"steel-plate":5,"electronic-circuit":15},"products":{"solar-panel":1},"category":"crafting","energy":10},"solar-panel-equipment":{"ingredients":{"steel-plate":5,"advanced-circuit":2,"solar-panel":1},"products":{"solar-panel-equipment":1},"category":"crafting","energy":10},"speed-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"speed-module":1},"category":"crafting","energy":15},"speed-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module":4},"products":{"speed-module-2":1},"category":"crafting","energy":30},"speed-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module-2":5},"products":{"speed-module-3":1},"category":"crafting","energy":60},"spidertron":{"ingredients":{"raw-fish":1,"rocket-control-unit":16,"low-density-structure":150,"effectivity-module-3":2,"rocket-launcher":4,"fusion-reactor-equipment":2,"exoskeleton-equipment":4,"radar":2},"products":{"spidertron":1},"category":"crafting","energy":10},"spidertron-remote":{"ingredients":{"rocket-control-unit":1,"radar":1},"products":{"spidertron-remote":1},"category":"crafting","energy":0.5},"splitter":{"ingredients":{"iron-plate":5,"electronic-circuit":5,"transport-belt":4},"products":{"splitter":1},"category":"crafting","energy":1},"stack-filter-inserter":{"ingredients":{"electronic-circuit":5,"stack-inserter":1},"products":{"stack-filter-inserter":1},"category":"crafting","energy":0.5},"stack-inserter":{"ingredients":{"iron-gear-wheel":15,"electronic-circuit":15,"advanced-circuit":1,"fast-inserter":1},"products":{"stack-inserter":1},"category":"crafting","energy":0.5},"steam-engine":{"ingredients":{"iron-plate":10,"iron-gear-wheel":8,"pipe":5},"products":{"steam-engine":1},"category":"crafting","energy":0.5},"steam-turbine":{"ingredients":{"copper-plate":50,"iron-gear-wheel":50,"pipe":20},"products":{"steam-turbine":1},"category":"crafting","energy":3},"steel-chest":{"ingredients":{"steel-plate":8},"products":{"steel-chest":1},"category":"crafting","energy":0.5},"steel-furnace":{"ingredients":{"steel-plate":6,"stone-brick":10},"products":{"steel-furnace":1},"category":"crafting","energy":3},"steel-plate":{"ingredients":{"iron-plate":5},"products":{"steel-plate":1},"category":"smelting","energy":16},"stone-brick":{"ingredients":{"stone":2},"products":{"stone-brick":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"stone-furnace":{"ingredients":{"stone":5},"products":{"stone-furnace":1},"category":"crafting","energy":0.5},"stone-wall":{"ingredients":{"stone-brick":5},"products":{"stone-wall":1},"category":"crafting","energy":0.5},"storage-tank":{"ingredients":{"iron-plate":20,"steel-plate":5},"products":{"storage-tank":1},"category":"crafting","energy":3},"submachine-gun":{"ingredients":{"iron-plate":10,"copper-plate":5,"iron-gear-wheel":10},"products":{"submachine-gun":1},"category":"crafting","energy":10},"substation":{"ingredients":{"copper-plate":5,"steel-plate":10,"advanced-circuit":5},"products":{"substation":1},"category":"crafting","energy":0.5},"sulfur":{"ingredients":{"water":30,"petroleum-gas":30},"products":{"sulfur":2},"category":"chemistry","energy":1},"sulfuric-acid":{"ingredients":{"iron-plate":1,"sulfur":5,"water":100},"products":{"sulfuric-acid":50},"category":"chemistry","energy":1},"tank":{"ingredients":{"steel-plate":50,"iron-gear-wheel":15,"advanced-circuit":10,"engine-unit":32},"products":{"tank":1},"category":"crafting","energy":5},"train-stop":{"ingredients":{"iron-plate":6,"steel-plate":3,"iron-stick":6,"electronic-circuit":5},"products":{"train-stop":1},"category":"crafting","energy":0.5},"transport-belt":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"transport-belt":2},"category":"crafting","energy":0.5},"underground-belt":{"ingredients":{"iron-plate":10,"transport-belt":5},"products":{"underground-belt":2},"category":"crafting","energy":1},"uranium-cannon-shell":{"ingredients":{"uranium-238":1,"cannon-shell":1},"products":{"uranium-cannon-shell":1},"category":"crafting","energy":12},"uranium-fuel-cell":{"ingredients":{"iron-plate":10,"uranium-235":1,"uranium-238":19},"products":{"uranium-fuel-cell":10},"category":"crafting","energy":10},"uranium-rounds-magazine":{"ingredients":{"uranium-238":1,"piercing-rounds-magazine":1},"products":{"uranium-rounds-magazine":1},"category":"crafting","energy":10},"utility-science-pack":{"ingredients":{"processing-unit":2,"flying-robot-frame":1,"low-density-structure":3},"products":{"utility-science-pack":3},"category":"crafting","energy":21},"wooden-chest":{"ingredients":{"wood":2},"products":{"wooden-chest":1},"category":"crafting","energy":0.5},"basic-oil-processing":{"ingredients":{"crude-oil":100},"products":{"petroleum-gas":45},"category":"oil-processing","energy":5},"advanced-oil-processing":{"ingredients":{"water":50,"crude-oil":100},"products":{"heavy-oil":25,"light-oil":45,"petroleum-gas":55},"category":"oil-processing","energy":5},"coal-liquefaction":{"ingredients":{"coal":10,"heavy-oil":25,"steam":50},"products":{"heavy-oil":90,"light-oil":20,"petroleum-gas":10},"category":"oil-processing","energy":5},"fill-crude-oil-barrel":{"ingredients":{"empty-barrel":1,"crude-oil":50},"products":{"crude-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-heavy-oil-barrel":{"ingredients":{"empty-barrel":1,"heavy-oil":50},"products":{"heavy-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-light-oil-barrel":{"ingredients":{"empty-barrel":1,"light-oil":50},"products":{"light-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-lubricant-barrel":{"ingredients":{"empty-barrel":1,"lubricant":50},"products":{"lubricant-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-petroleum-gas-barrel":{"ingredients":{"empty-barrel":1,"petroleum-gas":50},"products":{"petroleum-gas-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-sulfuric-acid-barrel":{"ingredients":{"empty-barrel":1,"sulfuric-acid":50},"products":{"sulfuric-acid-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-water-barrel":{"ingredients":{"empty-barrel":1,"water":50},"products":{"water-barrel":1},"category":"crafting-with-fluid","energy":0.2},"heavy-oil-cracking":{"ingredients":{"water":30,"heavy-oil":40},"products":{"light-oil":30},"category":"chemistry","energy":2},"light-oil-cracking":{"ingredients":{"water":30,"light-oil":30},"products":{"petroleum-gas":20},"category":"chemistry","energy":2},"solid-fuel-from-light-oil":{"ingredients":{"light-oil":10},"products":{"solid-fuel":1},"category":"chemistry","energy":2},"solid-fuel-from-petroleum-gas":{"ingredients":{"petroleum-gas":20},"products":{"solid-fuel":1},"category":"chemistry","energy":2},"solid-fuel-from-heavy-oil":{"ingredients":{"heavy-oil":20},"products":{"solid-fuel":1},"category":"chemistry","energy":2},"empty-crude-oil-barrel":{"ingredients":{"crude-oil-barrel":1},"products":{"empty-barrel":1,"crude-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-heavy-oil-barrel":{"ingredients":{"heavy-oil-barrel":1},"products":{"empty-barrel":1,"heavy-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-light-oil-barrel":{"ingredients":{"light-oil-barrel":1},"products":{"empty-barrel":1,"light-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-lubricant-barrel":{"ingredients":{"lubricant-barrel":1},"products":{"empty-barrel":1,"lubricant":50},"category":"crafting-with-fluid","energy":0.2},"empty-petroleum-gas-barrel":{"ingredients":{"petroleum-gas-barrel":1},"products":{"empty-barrel":1,"petroleum-gas":50},"category":"crafting-with-fluid","energy":0.2},"empty-sulfuric-acid-barrel":{"ingredients":{"sulfuric-acid-barrel":1},"products":{"empty-barrel":1,"sulfuric-acid":50},"category":"crafting-with-fluid","energy":0.2},"empty-water-barrel":{"ingredients":{"water-barrel":1},"products":{"empty-barrel":1,"water":50},"category":"crafting-with-fluid","energy":0.2},"uranium-processing":{"ingredients":{"uranium-ore":10},"products":{"uranium-235":1,"uranium-238":1},"category":"centrifuging","energy":12},"nuclear-fuel-reprocessing":{"ingredients":{"used-up-uranium-fuel-cell":5},"products":{"uranium-238":3},"category":"centrifuging","energy":60},"kovarex-enrichment-process":{"ingredients":{"uranium-235":40,"uranium-238":5},"products":{"uranium-235":41,"uranium-238":2},"category":"centrifuging","energy":60}} \ No newline at end of file +{"wooden-chest":{"ingredients":{"wood":2},"products":{"wooden-chest":1},"category":"crafting","energy":0.5},"iron-chest":{"ingredients":{"iron-plate":8},"products":{"iron-chest":1},"category":"crafting","energy":0.5},"steel-chest":{"ingredients":{"steel-plate":8},"products":{"steel-chest":1},"category":"crafting","energy":0.5},"storage-tank":{"ingredients":{"iron-plate":20,"steel-plate":5},"products":{"storage-tank":1},"category":"crafting","energy":3},"transport-belt":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"transport-belt":2},"category":"crafting","energy":0.5},"fast-transport-belt":{"ingredients":{"iron-gear-wheel":5,"transport-belt":1},"products":{"fast-transport-belt":1},"category":"crafting","energy":0.5},"express-transport-belt":{"ingredients":{"iron-gear-wheel":10,"fast-transport-belt":1,"lubricant":20},"products":{"express-transport-belt":1},"category":"crafting-with-fluid","energy":0.5},"underground-belt":{"ingredients":{"iron-plate":10,"transport-belt":5},"products":{"underground-belt":2},"category":"crafting","energy":1},"fast-underground-belt":{"ingredients":{"iron-gear-wheel":40,"underground-belt":2},"products":{"fast-underground-belt":2},"category":"crafting","energy":2},"express-underground-belt":{"ingredients":{"iron-gear-wheel":80,"fast-underground-belt":2,"lubricant":40},"products":{"express-underground-belt":2},"category":"crafting-with-fluid","energy":2},"splitter":{"ingredients":{"iron-plate":5,"electronic-circuit":5,"transport-belt":4},"products":{"splitter":1},"category":"crafting","energy":1},"fast-splitter":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"splitter":1},"products":{"fast-splitter":1},"category":"crafting","energy":2},"express-splitter":{"ingredients":{"iron-gear-wheel":10,"advanced-circuit":10,"fast-splitter":1,"lubricant":80},"products":{"express-splitter":1},"category":"crafting-with-fluid","energy":2},"burner-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"burner-inserter":1},"category":"crafting","energy":0.5},"inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"electronic-circuit":1},"products":{"inserter":1},"category":"crafting","energy":0.5},"long-handed-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"inserter":1},"products":{"long-handed-inserter":1},"category":"crafting","energy":0.5},"fast-inserter":{"ingredients":{"iron-plate":2,"electronic-circuit":2,"inserter":1},"products":{"fast-inserter":1},"category":"crafting","energy":0.5},"bulk-inserter":{"ingredients":{"iron-gear-wheel":15,"electronic-circuit":15,"advanced-circuit":1,"fast-inserter":1},"products":{"bulk-inserter":1},"category":"crafting","energy":0.5},"small-electric-pole":{"ingredients":{"wood":1,"copper-cable":2},"products":{"small-electric-pole":2},"category":"crafting","energy":0.5},"medium-electric-pole":{"ingredients":{"steel-plate":2,"iron-stick":4,"copper-cable":2},"products":{"medium-electric-pole":1},"category":"crafting","energy":0.5},"big-electric-pole":{"ingredients":{"steel-plate":5,"iron-stick":8,"copper-cable":4},"products":{"big-electric-pole":1},"category":"crafting","energy":0.5},"substation":{"ingredients":{"steel-plate":10,"copper-cable":6,"advanced-circuit":5},"products":{"substation":1},"category":"crafting","energy":0.5},"pipe":{"ingredients":{"iron-plate":1},"products":{"pipe":1},"category":"crafting","energy":0.5},"pipe-to-ground":{"ingredients":{"iron-plate":5,"pipe":10},"products":{"pipe-to-ground":2},"category":"crafting","energy":0.5},"pump":{"ingredients":{"steel-plate":1,"engine-unit":1,"pipe":1},"products":{"pump":1},"category":"crafting","energy":2},"rail":{"ingredients":{"stone":1,"steel-plate":1,"iron-stick":1},"products":{"rail":2},"category":"crafting","energy":0.5},"train-stop":{"ingredients":{"iron-plate":6,"steel-plate":3,"iron-stick":6,"electronic-circuit":5},"products":{"train-stop":1},"category":"crafting","energy":0.5},"rail-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-signal":1},"category":"crafting","energy":0.5},"rail-chain-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-chain-signal":1},"category":"crafting","energy":0.5},"locomotive":{"ingredients":{"steel-plate":30,"electronic-circuit":10,"engine-unit":20},"products":{"locomotive":1},"category":"crafting","energy":4},"cargo-wagon":{"ingredients":{"iron-plate":20,"steel-plate":20,"iron-gear-wheel":10},"products":{"cargo-wagon":1},"category":"crafting","energy":1},"fluid-wagon":{"ingredients":{"steel-plate":16,"iron-gear-wheel":10,"storage-tank":1,"pipe":8},"products":{"fluid-wagon":1},"category":"crafting","energy":1.5},"artillery-wagon":{"ingredients":{"steel-plate":40,"iron-gear-wheel":10,"advanced-circuit":20,"engine-unit":64,"pipe":16},"products":{"artillery-wagon":1},"category":"crafting","energy":4},"car":{"ingredients":{"iron-plate":20,"steel-plate":5,"engine-unit":8},"products":{"car":1},"category":"crafting","energy":2},"tank":{"ingredients":{"steel-plate":50,"iron-gear-wheel":15,"advanced-circuit":10,"engine-unit":32},"products":{"tank":1},"category":"crafting","energy":5},"spidertron":{"ingredients":{"raw-fish":1,"processing-unit":16,"low-density-structure":150,"efficiency-module-3":2,"rocket-launcher":4,"fission-reactor-equipment":2,"exoskeleton-equipment":4,"radar":2},"products":{"spidertron":1},"category":"crafting","energy":10},"logistic-robot":{"ingredients":{"advanced-circuit":2,"flying-robot-frame":1},"products":{"logistic-robot":1},"category":"crafting","energy":0.5},"construction-robot":{"ingredients":{"electronic-circuit":2,"flying-robot-frame":1},"products":{"construction-robot":1},"category":"crafting","energy":0.5},"active-provider-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"active-provider-chest":1},"category":"crafting","energy":0.5},"passive-provider-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"passive-provider-chest":1},"category":"crafting","energy":0.5},"storage-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"storage-chest":1},"category":"crafting","energy":0.5},"buffer-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"buffer-chest":1},"category":"crafting","energy":0.5},"requester-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"requester-chest":1},"category":"crafting","energy":0.5},"roboport":{"ingredients":{"steel-plate":45,"iron-gear-wheel":45,"advanced-circuit":45},"products":{"roboport":1},"category":"crafting","energy":5},"small-lamp":{"ingredients":{"iron-plate":1,"copper-cable":3,"electronic-circuit":1},"products":{"small-lamp":1},"category":"crafting","energy":0.5},"arithmetic-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"arithmetic-combinator":1},"category":"crafting","energy":0.5},"decider-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"decider-combinator":1},"category":"crafting","energy":0.5},"selector-combinator":{"ingredients":{"advanced-circuit":2,"decider-combinator":5},"products":{"selector-combinator":1},"category":"crafting","energy":0.5},"constant-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":2},"products":{"constant-combinator":1},"category":"crafting","energy":0.5},"power-switch":{"ingredients":{"iron-plate":5,"copper-cable":5,"electronic-circuit":2},"products":{"power-switch":1},"category":"crafting","energy":2},"programmable-speaker":{"ingredients":{"iron-plate":3,"iron-stick":4,"copper-cable":5,"electronic-circuit":4},"products":{"programmable-speaker":1},"category":"crafting","energy":2},"display-panel":{"ingredients":{"iron-plate":1,"electronic-circuit":1},"products":{"display-panel":1},"category":"crafting","energy":0.5},"stone-brick":{"ingredients":{"stone":2},"products":{"stone-brick":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"concrete":{"ingredients":{"iron-ore":1,"stone-brick":5,"water":100},"products":{"concrete":10},"category":"crafting-with-fluid","energy":10},"hazard-concrete":{"ingredients":{"concrete":10},"products":{"hazard-concrete":10},"category":"crafting","energy":0.25},"refined-concrete":{"ingredients":{"steel-plate":1,"iron-stick":8,"concrete":20,"water":100},"products":{"refined-concrete":10},"category":"crafting-with-fluid","energy":15},"refined-hazard-concrete":{"ingredients":{"refined-concrete":10},"products":{"refined-hazard-concrete":10},"category":"crafting","energy":0.25},"landfill":{"ingredients":{"stone":50},"products":{"landfill":1},"category":"crafting","energy":0.5},"cliff-explosives":{"ingredients":{"explosives":10,"barrel":1,"grenade":1},"products":{"cliff-explosives":1},"category":"crafting","energy":8},"repair-pack":{"ingredients":{"iron-gear-wheel":2,"electronic-circuit":2},"products":{"repair-pack":1},"category":"crafting","energy":0.5},"boiler":{"ingredients":{"pipe":4,"stone-furnace":1},"products":{"boiler":1},"category":"crafting","energy":0.5},"steam-engine":{"ingredients":{"iron-plate":10,"iron-gear-wheel":8,"pipe":5},"products":{"steam-engine":1},"category":"crafting","energy":0.5},"solar-panel":{"ingredients":{"copper-plate":5,"steel-plate":5,"electronic-circuit":15},"products":{"solar-panel":1},"category":"crafting","energy":10},"accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting","energy":10},"nuclear-reactor":{"ingredients":{"copper-plate":500,"steel-plate":500,"advanced-circuit":500,"concrete":500},"products":{"nuclear-reactor":1},"category":"crafting","energy":8},"heat-pipe":{"ingredients":{"copper-plate":20,"steel-plate":10},"products":{"heat-pipe":1},"category":"crafting","energy":1},"heat-exchanger":{"ingredients":{"copper-plate":100,"steel-plate":10,"pipe":10},"products":{"heat-exchanger":1},"category":"crafting","energy":3},"steam-turbine":{"ingredients":{"copper-plate":50,"iron-gear-wheel":50,"pipe":20},"products":{"steam-turbine":1},"category":"crafting","energy":3},"burner-mining-drill":{"ingredients":{"iron-plate":3,"iron-gear-wheel":3,"stone-furnace":1},"products":{"burner-mining-drill":1},"category":"crafting","energy":2},"electric-mining-drill":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"electric-mining-drill":1},"category":"crafting","energy":2},"offshore-pump":{"ingredients":{"iron-gear-wheel":2,"pipe":3},"products":{"offshore-pump":1},"category":"crafting","energy":0.5},"pumpjack":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10,"electronic-circuit":5,"pipe":10},"products":{"pumpjack":1},"category":"crafting","energy":5},"stone-furnace":{"ingredients":{"stone":5},"products":{"stone-furnace":1},"category":"crafting","energy":0.5},"steel-furnace":{"ingredients":{"steel-plate":6,"stone-brick":10},"products":{"steel-furnace":1},"category":"crafting","energy":3},"electric-furnace":{"ingredients":{"steel-plate":10,"advanced-circuit":5,"stone-brick":10},"products":{"electric-furnace":1},"category":"crafting","energy":5},"assembling-machine-1":{"ingredients":{"iron-plate":9,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"assembling-machine-1":1},"category":"crafting","energy":0.5},"assembling-machine-2":{"ingredients":{"steel-plate":2,"iron-gear-wheel":5,"electronic-circuit":3,"assembling-machine-1":1},"products":{"assembling-machine-2":1},"category":"crafting","energy":0.5},"assembling-machine-3":{"ingredients":{"assembling-machine-2":2,"speed-module":4},"products":{"assembling-machine-3":1},"category":"crafting","energy":0.5},"oil-refinery":{"ingredients":{"steel-plate":15,"iron-gear-wheel":10,"electronic-circuit":10,"pipe":10,"stone-brick":10},"products":{"oil-refinery":1},"category":"crafting","energy":8},"chemical-plant":{"ingredients":{"steel-plate":5,"iron-gear-wheel":5,"electronic-circuit":5,"pipe":5},"products":{"chemical-plant":1},"category":"crafting","energy":5},"centrifuge":{"ingredients":{"steel-plate":50,"iron-gear-wheel":100,"advanced-circuit":100,"concrete":100},"products":{"centrifuge":1},"category":"crafting","energy":4},"lab":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"transport-belt":4},"products":{"lab":1},"category":"crafting","energy":2},"beacon":{"ingredients":{"steel-plate":10,"copper-cable":10,"electronic-circuit":20,"advanced-circuit":20},"products":{"beacon":1},"category":"crafting","energy":15},"speed-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"speed-module":1},"category":"crafting","energy":15},"speed-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module":4},"products":{"speed-module-2":1},"category":"crafting","energy":30},"speed-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module-2":4},"products":{"speed-module-3":1},"category":"crafting","energy":60},"efficiency-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"efficiency-module":1},"category":"crafting","energy":15},"efficiency-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"efficiency-module":4},"products":{"efficiency-module-2":1},"category":"crafting","energy":30},"efficiency-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"efficiency-module-2":4},"products":{"efficiency-module-3":1},"category":"crafting","energy":60},"productivity-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"productivity-module":1},"category":"crafting","energy":15},"productivity-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module":4},"products":{"productivity-module-2":1},"category":"crafting","energy":30},"productivity-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module-2":4},"products":{"productivity-module-3":1},"category":"crafting","energy":60},"rocket-silo":{"ingredients":{"steel-plate":1000,"processing-unit":200,"electric-engine-unit":200,"pipe":100,"concrete":1000},"products":{"rocket-silo":1},"category":"crafting","energy":30},"cargo-landing-pad":{"ingredients":{"steel-plate":25,"processing-unit":10,"concrete":200},"products":{"cargo-landing-pad":1},"category":"crafting","energy":30},"satellite":{"ingredients":{"processing-unit":100,"low-density-structure":100,"rocket-fuel":50,"solar-panel":100,"accumulator":100,"radar":5},"products":{"satellite":1},"category":"crafting","energy":5},"basic-oil-processing":{"ingredients":{"crude-oil":100},"products":{"petroleum-gas":45},"category":"oil-processing","energy":5},"advanced-oil-processing":{"ingredients":{"water":50,"crude-oil":100},"products":{"heavy-oil":25,"light-oil":45,"petroleum-gas":55},"category":"oil-processing","energy":5},"coal-liquefaction":{"ingredients":{"coal":10,"heavy-oil":25,"steam":50},"products":{"heavy-oil":90,"light-oil":20,"petroleum-gas":10},"category":"oil-processing","energy":5},"heavy-oil-cracking":{"ingredients":{"water":30,"heavy-oil":40},"products":{"light-oil":30},"category":"chemistry","energy":2},"light-oil-cracking":{"ingredients":{"water":30,"light-oil":30},"products":{"petroleum-gas":20},"category":"chemistry","energy":2},"solid-fuel-from-petroleum-gas":{"ingredients":{"petroleum-gas":20},"products":{"solid-fuel":1},"category":"chemistry","energy":1},"solid-fuel-from-light-oil":{"ingredients":{"light-oil":10},"products":{"solid-fuel":1},"category":"chemistry","energy":1},"solid-fuel-from-heavy-oil":{"ingredients":{"heavy-oil":20},"products":{"solid-fuel":1},"category":"chemistry","energy":1},"lubricant":{"ingredients":{"heavy-oil":10},"products":{"lubricant":10},"category":"chemistry","energy":1},"sulfuric-acid":{"ingredients":{"iron-plate":1,"sulfur":5,"water":100},"products":{"sulfuric-acid":50},"category":"chemistry","energy":1},"iron-plate":{"ingredients":{"iron-ore":1},"products":{"iron-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"copper-plate":{"ingredients":{"copper-ore":1},"products":{"copper-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"steel-plate":{"ingredients":{"iron-plate":5},"products":{"steel-plate":1},"category":"smelting","energy":16},"plastic-bar":{"ingredients":{"coal":1,"petroleum-gas":20},"products":{"plastic-bar":2},"category":"chemistry","energy":1},"sulfur":{"ingredients":{"water":30,"petroleum-gas":30},"products":{"sulfur":2},"category":"chemistry","energy":1},"battery":{"ingredients":{"iron-plate":1,"copper-plate":1,"sulfuric-acid":20},"products":{"battery":1},"category":"chemistry","energy":4},"explosives":{"ingredients":{"coal":1,"sulfur":1,"water":10},"products":{"explosives":2},"category":"chemistry","energy":4},"water-barrel":{"ingredients":{"barrel":1,"water":50},"products":{"water-barrel":1},"category":"crafting-with-fluid","energy":0.2},"crude-oil-barrel":{"ingredients":{"barrel":1,"crude-oil":50},"products":{"crude-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"petroleum-gas-barrel":{"ingredients":{"barrel":1,"petroleum-gas":50},"products":{"petroleum-gas-barrel":1},"category":"crafting-with-fluid","energy":0.2},"light-oil-barrel":{"ingredients":{"barrel":1,"light-oil":50},"products":{"light-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"heavy-oil-barrel":{"ingredients":{"barrel":1,"heavy-oil":50},"products":{"heavy-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"lubricant-barrel":{"ingredients":{"barrel":1,"lubricant":50},"products":{"lubricant-barrel":1},"category":"crafting-with-fluid","energy":0.2},"sulfuric-acid-barrel":{"ingredients":{"barrel":1,"sulfuric-acid":50},"products":{"sulfuric-acid-barrel":1},"category":"crafting-with-fluid","energy":0.2},"empty-water-barrel":{"ingredients":{"water-barrel":1},"products":{"barrel":1,"water":50},"category":"crafting-with-fluid","energy":0.2},"empty-crude-oil-barrel":{"ingredients":{"crude-oil-barrel":1},"products":{"barrel":1,"crude-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-petroleum-gas-barrel":{"ingredients":{"petroleum-gas-barrel":1},"products":{"barrel":1,"petroleum-gas":50},"category":"crafting-with-fluid","energy":0.2},"empty-light-oil-barrel":{"ingredients":{"light-oil-barrel":1},"products":{"barrel":1,"light-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-heavy-oil-barrel":{"ingredients":{"heavy-oil-barrel":1},"products":{"barrel":1,"heavy-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-lubricant-barrel":{"ingredients":{"lubricant-barrel":1},"products":{"barrel":1,"lubricant":50},"category":"crafting-with-fluid","energy":0.2},"empty-sulfuric-acid-barrel":{"ingredients":{"sulfuric-acid-barrel":1},"products":{"barrel":1,"sulfuric-acid":50},"category":"crafting-with-fluid","energy":0.2},"iron-gear-wheel":{"ingredients":{"iron-plate":2},"products":{"iron-gear-wheel":1},"category":"crafting","energy":0.5},"iron-stick":{"ingredients":{"iron-plate":1},"products":{"iron-stick":2},"category":"crafting","energy":0.5},"copper-cable":{"ingredients":{"copper-plate":1},"products":{"copper-cable":2},"category":"crafting","energy":0.5},"barrel":{"ingredients":{"steel-plate":1},"products":{"barrel":1},"category":"crafting","energy":1},"electronic-circuit":{"ingredients":{"iron-plate":1,"copper-cable":3},"products":{"electronic-circuit":1},"category":"crafting","energy":0.5},"advanced-circuit":{"ingredients":{"plastic-bar":2,"copper-cable":4,"electronic-circuit":2},"products":{"advanced-circuit":1},"category":"crafting","energy":6},"processing-unit":{"ingredients":{"electronic-circuit":20,"advanced-circuit":2,"sulfuric-acid":5},"products":{"processing-unit":1},"category":"crafting-with-fluid","energy":10},"engine-unit":{"ingredients":{"steel-plate":1,"iron-gear-wheel":1,"pipe":2},"products":{"engine-unit":1},"category":"advanced-crafting","energy":10},"electric-engine-unit":{"ingredients":{"electronic-circuit":2,"engine-unit":1,"lubricant":15},"products":{"electric-engine-unit":1},"category":"crafting-with-fluid","energy":10},"flying-robot-frame":{"ingredients":{"steel-plate":1,"battery":2,"electronic-circuit":3,"electric-engine-unit":1},"products":{"flying-robot-frame":1},"category":"crafting","energy":20},"low-density-structure":{"ingredients":{"copper-plate":20,"steel-plate":2,"plastic-bar":5},"products":{"low-density-structure":1},"category":"crafting","energy":15},"rocket-fuel":{"ingredients":{"solid-fuel":10,"light-oil":10},"products":{"rocket-fuel":1},"category":"crafting-with-fluid","energy":15},"rocket-part":{"ingredients":{"processing-unit":10,"low-density-structure":10,"rocket-fuel":10},"products":{"rocket-part":1},"category":"rocket-building","energy":3},"uranium-processing":{"ingredients":{"uranium-ore":10},"products":{"uranium-235":1,"uranium-238":1},"category":"centrifuging","energy":12},"uranium-fuel-cell":{"ingredients":{"iron-plate":10,"uranium-235":1,"uranium-238":19},"products":{"uranium-fuel-cell":10},"category":"crafting","energy":10},"nuclear-fuel-reprocessing":{"ingredients":{"depleted-uranium-fuel-cell":5},"products":{"uranium-238":3},"category":"centrifuging","energy":60},"kovarex-enrichment-process":{"ingredients":{"uranium-235":40,"uranium-238":5},"products":{"uranium-235":41,"uranium-238":2},"category":"centrifuging","energy":60},"nuclear-fuel":{"ingredients":{"rocket-fuel":1,"uranium-235":1},"products":{"nuclear-fuel":1},"category":"centrifuging","energy":90},"automation-science-pack":{"ingredients":{"copper-plate":1,"iron-gear-wheel":1},"products":{"automation-science-pack":1},"category":"crafting","energy":5},"logistic-science-pack":{"ingredients":{"transport-belt":1,"inserter":1},"products":{"logistic-science-pack":1},"category":"crafting","energy":6},"military-science-pack":{"ingredients":{"piercing-rounds-magazine":1,"grenade":1,"stone-wall":2},"products":{"military-science-pack":2},"category":"crafting","energy":10},"chemical-science-pack":{"ingredients":{"sulfur":1,"advanced-circuit":3,"engine-unit":2},"products":{"chemical-science-pack":2},"category":"crafting","energy":24},"production-science-pack":{"ingredients":{"rail":30,"electric-furnace":1,"productivity-module":1},"products":{"production-science-pack":3},"category":"crafting","energy":21},"utility-science-pack":{"ingredients":{"processing-unit":2,"flying-robot-frame":1,"low-density-structure":3},"products":{"utility-science-pack":3},"category":"crafting","energy":21},"submachine-gun":{"ingredients":{"iron-plate":10,"copper-plate":5,"iron-gear-wheel":10},"products":{"submachine-gun":1},"category":"crafting","energy":10},"shotgun":{"ingredients":{"wood":5,"iron-plate":15,"copper-plate":10,"iron-gear-wheel":5},"products":{"shotgun":1},"category":"crafting","energy":10},"combat-shotgun":{"ingredients":{"wood":10,"copper-plate":10,"steel-plate":15,"iron-gear-wheel":5},"products":{"combat-shotgun":1},"category":"crafting","energy":10},"rocket-launcher":{"ingredients":{"iron-plate":5,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"rocket-launcher":1},"category":"crafting","energy":10},"flamethrower":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10},"products":{"flamethrower":1},"category":"crafting","energy":10},"firearm-magazine":{"ingredients":{"iron-plate":4},"products":{"firearm-magazine":1},"category":"crafting","energy":1},"piercing-rounds-magazine":{"ingredients":{"copper-plate":5,"steel-plate":1,"firearm-magazine":1},"products":{"piercing-rounds-magazine":1},"category":"crafting","energy":3},"uranium-rounds-magazine":{"ingredients":{"uranium-238":1,"piercing-rounds-magazine":1},"products":{"uranium-rounds-magazine":1},"category":"crafting","energy":10},"shotgun-shell":{"ingredients":{"iron-plate":2,"copper-plate":2},"products":{"shotgun-shell":1},"category":"crafting","energy":3},"piercing-shotgun-shell":{"ingredients":{"copper-plate":5,"steel-plate":2,"shotgun-shell":2},"products":{"piercing-shotgun-shell":1},"category":"crafting","energy":8},"cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":1},"products":{"cannon-shell":1},"category":"crafting","energy":8},"explosive-cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":2},"products":{"explosive-cannon-shell":1},"category":"crafting","energy":8},"uranium-cannon-shell":{"ingredients":{"uranium-238":1,"cannon-shell":1},"products":{"uranium-cannon-shell":1},"category":"crafting","energy":12},"explosive-uranium-cannon-shell":{"ingredients":{"uranium-238":1,"explosive-cannon-shell":1},"products":{"explosive-uranium-cannon-shell":1},"category":"crafting","energy":12},"artillery-shell":{"ingredients":{"explosives":8,"explosive-cannon-shell":4,"radar":1},"products":{"artillery-shell":1},"category":"crafting","energy":15},"rocket":{"ingredients":{"iron-plate":2,"explosives":1},"products":{"rocket":1},"category":"crafting","energy":4},"explosive-rocket":{"ingredients":{"explosives":2,"rocket":1},"products":{"explosive-rocket":1},"category":"crafting","energy":8},"atomic-bomb":{"ingredients":{"explosives":10,"processing-unit":10,"uranium-235":30},"products":{"atomic-bomb":1},"category":"crafting","energy":50},"flamethrower-ammo":{"ingredients":{"steel-plate":5,"crude-oil":100},"products":{"flamethrower-ammo":1},"category":"chemistry","energy":6},"grenade":{"ingredients":{"coal":10,"iron-plate":5},"products":{"grenade":1},"category":"crafting","energy":8},"cluster-grenade":{"ingredients":{"steel-plate":5,"explosives":5,"grenade":7},"products":{"cluster-grenade":1},"category":"crafting","energy":8},"poison-capsule":{"ingredients":{"coal":10,"steel-plate":3,"electronic-circuit":3},"products":{"poison-capsule":1},"category":"crafting","energy":8},"slowdown-capsule":{"ingredients":{"coal":5,"steel-plate":2,"electronic-circuit":2},"products":{"slowdown-capsule":1},"category":"crafting","energy":8},"defender-capsule":{"ingredients":{"iron-gear-wheel":3,"electronic-circuit":3,"piercing-rounds-magazine":3},"products":{"defender-capsule":1},"category":"crafting","energy":8},"distractor-capsule":{"ingredients":{"advanced-circuit":3,"defender-capsule":4},"products":{"distractor-capsule":1},"category":"crafting","energy":15},"destroyer-capsule":{"ingredients":{"speed-module":1,"distractor-capsule":4},"products":{"destroyer-capsule":1},"category":"crafting","energy":15},"light-armor":{"ingredients":{"iron-plate":40},"products":{"light-armor":1},"category":"crafting","energy":3},"heavy-armor":{"ingredients":{"copper-plate":100,"steel-plate":50},"products":{"heavy-armor":1},"category":"crafting","energy":8},"modular-armor":{"ingredients":{"steel-plate":50,"advanced-circuit":30},"products":{"modular-armor":1},"category":"crafting","energy":15},"power-armor":{"ingredients":{"steel-plate":40,"processing-unit":40,"electric-engine-unit":20},"products":{"power-armor":1},"category":"crafting","energy":20},"power-armor-mk2":{"ingredients":{"processing-unit":60,"electric-engine-unit":40,"low-density-structure":30,"speed-module-2":25,"efficiency-module-2":25},"products":{"power-armor-mk2":1},"category":"crafting","energy":25},"solar-panel-equipment":{"ingredients":{"steel-plate":5,"advanced-circuit":2,"solar-panel":1},"products":{"solar-panel-equipment":1},"category":"crafting","energy":10},"fission-reactor-equipment":{"ingredients":{"processing-unit":200,"low-density-structure":50,"uranium-fuel-cell":4},"products":{"fission-reactor-equipment":1},"category":"crafting","energy":10},"battery-equipment":{"ingredients":{"steel-plate":10,"battery":5},"products":{"battery-equipment":1},"category":"crafting","energy":10},"battery-mk2-equipment":{"ingredients":{"processing-unit":15,"low-density-structure":5,"battery-equipment":10},"products":{"battery-mk2-equipment":1},"category":"crafting","energy":10},"belt-immunity-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"belt-immunity-equipment":1},"category":"crafting","energy":10},"exoskeleton-equipment":{"ingredients":{"steel-plate":20,"processing-unit":10,"electric-engine-unit":30},"products":{"exoskeleton-equipment":1},"category":"crafting","energy":10},"personal-roboport-equipment":{"ingredients":{"steel-plate":20,"battery":45,"iron-gear-wheel":40,"advanced-circuit":10},"products":{"personal-roboport-equipment":1},"category":"crafting","energy":10},"personal-roboport-mk2-equipment":{"ingredients":{"processing-unit":100,"low-density-structure":20,"personal-roboport-equipment":5},"products":{"personal-roboport-mk2-equipment":1},"category":"crafting","energy":20},"night-vision-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"night-vision-equipment":1},"category":"crafting","energy":10},"energy-shield-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"energy-shield-equipment":1},"category":"crafting","energy":10},"energy-shield-mk2-equipment":{"ingredients":{"processing-unit":5,"low-density-structure":5,"energy-shield-equipment":10},"products":{"energy-shield-mk2-equipment":1},"category":"crafting","energy":10},"personal-laser-defense-equipment":{"ingredients":{"processing-unit":20,"low-density-structure":5,"laser-turret":5},"products":{"personal-laser-defense-equipment":1},"category":"crafting","energy":10},"discharge-defense-equipment":{"ingredients":{"steel-plate":20,"processing-unit":5,"laser-turret":10},"products":{"discharge-defense-equipment":1},"category":"crafting","energy":10},"stone-wall":{"ingredients":{"stone-brick":5},"products":{"stone-wall":1},"category":"crafting","energy":0.5},"gate":{"ingredients":{"steel-plate":2,"electronic-circuit":2,"stone-wall":1},"products":{"gate":1},"category":"crafting","energy":0.5},"radar":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"radar":1},"category":"crafting","energy":0.5},"land-mine":{"ingredients":{"steel-plate":1,"explosives":2},"products":{"land-mine":4},"category":"crafting","energy":5},"gun-turret":{"ingredients":{"iron-plate":20,"copper-plate":10,"iron-gear-wheel":10},"products":{"gun-turret":1},"category":"crafting","energy":8},"laser-turret":{"ingredients":{"steel-plate":20,"battery":12,"electronic-circuit":20},"products":{"laser-turret":1},"category":"crafting","energy":20},"flamethrower-turret":{"ingredients":{"steel-plate":30,"iron-gear-wheel":15,"engine-unit":5,"pipe":10},"products":{"flamethrower-turret":1},"category":"crafting","energy":20},"artillery-turret":{"ingredients":{"steel-plate":60,"iron-gear-wheel":40,"advanced-circuit":20,"concrete":60},"products":{"artillery-turret":1},"category":"crafting","energy":40},"parameter-0":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-1":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-2":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-3":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-4":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-5":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-6":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-7":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-8":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-9":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"recipe-unknown":{"ingredients":{},"products":{},"category":"crafting","energy":0.5}} \ No newline at end of file diff --git a/worlds/factorio/data/resources.json b/worlds/factorio/data/resources.json index 10279db37955..80c00fe3df42 100644 --- a/worlds/factorio/data/resources.json +++ b/worlds/factorio/data/resources.json @@ -1 +1 @@ -{"iron-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"iron-ore":{"name":"iron-ore","amount":1}}},"copper-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"copper-ore":{"name":"copper-ore","amount":1}}},"stone":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"stone":{"name":"stone","amount":1}}},"coal":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"coal":{"name":"coal","amount":1}}},"uranium-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":2,"required_fluid":"sulfuric-acid","fluid_amount":10,"products":{"uranium-ore":{"name":"uranium-ore","amount":1}}},"crude-oil":{"minable":true,"infinite":true,"infinite_depletion":10,"category":"basic-fluid","mining_time":1,"products":{"crude-oil":{"name":"crude-oil","amount":10}}}} \ No newline at end of file +{"iron-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"iron-ore":{"name":"iron-ore","amount":1}}},"copper-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"copper-ore":{"name":"copper-ore","amount":1}}},"stone":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"stone":{"name":"stone","amount":1}}},"coal":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"coal":{"name":"coal","amount":1}}},"crude-oil":{"minable":true,"infinite":true,"infinite_depletion":10,"category":"basic-fluid","mining_time":1,"products":{"crude-oil":{"name":"crude-oil","amount":10}}},"uranium-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":2,"required_fluid":"sulfuric-acid","fluid_amount":10,"products":{"uranium-ore":{"name":"uranium-ore","amount":1}}}} \ No newline at end of file diff --git a/worlds/factorio/data/techs.json b/worlds/factorio/data/techs.json index d9977f2986d6..ecb31126e1dc 100644 --- a/worlds/factorio/data/techs.json +++ b/worlds/factorio/data/techs.json @@ -1 +1 @@ -{"automation":{"unlocks":["assembling-machine-1","long-handed-inserter"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"automation-2":{"unlocks":["assembling-machine-2"],"requires":["electronics","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"automation-3":{"unlocks":["assembling-machine-3"],"requires":["speed-module","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"electronics":{"unlocks":{},"requires":["automation"],"ingredients":["automation-science-pack"],"has_modifier":false},"fast-inserter":{"unlocks":["fast-inserter","filter-inserter"],"requires":["electronics"],"ingredients":["automation-science-pack"],"has_modifier":false},"advanced-electronics":{"unlocks":["advanced-circuit"],"requires":["plastics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"advanced-electronics-2":{"unlocks":["processing-unit"],"requires":["chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"circuit-network":{"unlocks":["red-wire","green-wire","arithmetic-combinator","decider-combinator","constant-combinator","power-switch","programmable-speaker"],"requires":["electronics","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"explosives":{"unlocks":["explosives"],"requires":["sulfur-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"logistics":{"unlocks":["underground-belt","splitter"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"logistics-2":{"unlocks":["fast-transport-belt","fast-underground-belt","fast-splitter"],"requires":["logistics","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"logistics-3":{"unlocks":["express-transport-belt","express-underground-belt","express-splitter"],"requires":["production-science-pack","lubricant"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"optics":{"unlocks":["small-lamp"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"laser":{"unlocks":{},"requires":["optics","battery","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"solar-energy":{"unlocks":["solar-panel"],"requires":["optics","electronics","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"gun-turret":{"unlocks":["gun-turret"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"laser-turret":{"unlocks":["laser-turret"],"requires":["laser","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"stone-wall":{"unlocks":["stone-wall"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"gate":{"unlocks":["gate"],"requires":["stone-wall","military-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"engine":{"unlocks":["engine-unit"],"requires":["steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-engine":{"unlocks":["electric-engine-unit"],"requires":["lubricant"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"lubricant":{"unlocks":["lubricant"],"requires":["advanced-oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"battery":{"unlocks":["battery"],"requires":["sulfur-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"landfill":{"unlocks":["landfill"],"requires":["logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"braking-force-1":{"unlocks":{},"requires":["railway","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"braking-force-2":{"unlocks":{},"requires":["braking-force-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"braking-force-3":{"unlocks":{},"requires":["braking-force-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"braking-force-4":{"unlocks":{},"requires":["braking-force-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"braking-force-5":{"unlocks":{},"requires":["braking-force-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"braking-force-6":{"unlocks":{},"requires":["braking-force-5"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"braking-force-7":{"unlocks":{},"requires":["braking-force-6"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"chemical-science-pack":{"unlocks":["chemical-science-pack"],"requires":["advanced-electronics","sulfur-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"logistic-science-pack":{"unlocks":["logistic-science-pack"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"military-science-pack":{"unlocks":["military-science-pack"],"requires":["military-2","stone-wall"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"production-science-pack":{"unlocks":["production-science-pack"],"requires":["productivity-module","advanced-material-processing-2","railway"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"space-science-pack":{"unlocks":["satellite"],"requires":["rocket-silo","electric-energy-accumulators","solar-energy"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"steel-processing":{"unlocks":["steel-plate","steel-chest"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"utility-science-pack":{"unlocks":["utility-science-pack"],"requires":["robotics","advanced-electronics-2","low-density-structure"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"advanced-material-processing":{"unlocks":["steel-furnace"],"requires":["steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"steel-axe":{"unlocks":{},"requires":["steel-processing"],"ingredients":["automation-science-pack"],"has_modifier":true},"advanced-material-processing-2":{"unlocks":["electric-furnace"],"requires":["advanced-material-processing","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"concrete":{"unlocks":["concrete","hazard-concrete","refined-concrete","refined-hazard-concrete"],"requires":["advanced-material-processing","automation-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-energy-accumulators":{"unlocks":["accumulator"],"requires":["electric-energy-distribution-1","battery"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-energy-distribution-1":{"unlocks":["medium-electric-pole","big-electric-pole"],"requires":["electronics","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-energy-distribution-2":{"unlocks":["substation"],"requires":["electric-energy-distribution-1","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"railway":{"unlocks":["rail","locomotive","cargo-wagon"],"requires":["logistics-2","engine"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"fluid-wagon":{"unlocks":["fluid-wagon"],"requires":["railway","fluid-handling"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"automated-rail-transportation":{"unlocks":["train-stop"],"requires":["railway"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"rail-signals":{"unlocks":["rail-signal","rail-chain-signal"],"requires":["automated-rail-transportation"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"robotics":{"unlocks":["flying-robot-frame"],"requires":["electric-engine","battery"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"construction-robotics":{"unlocks":["roboport","logistic-chest-passive-provider","logistic-chest-storage","construction-robot"],"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"logistic-robotics":{"unlocks":["roboport","logistic-chest-passive-provider","logistic-chest-storage","logistic-robot"],"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"logistic-system":{"unlocks":["logistic-chest-active-provider","logistic-chest-requester","logistic-chest-buffer"],"requires":["utility-science-pack","logistic-robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"personal-roboport-equipment":{"unlocks":["personal-roboport-equipment"],"requires":["construction-robotics","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"personal-roboport-mk2-equipment":{"unlocks":["personal-roboport-mk2-equipment"],"requires":["personal-roboport-equipment","utility-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"worker-robots-speed-1":{"unlocks":{},"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"worker-robots-speed-2":{"unlocks":{},"requires":["worker-robots-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"worker-robots-speed-3":{"unlocks":{},"requires":["worker-robots-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"worker-robots-speed-4":{"unlocks":{},"requires":["worker-robots-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"mining-productivity-1":{"unlocks":{},"requires":["advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"mining-productivity-2":{"unlocks":{},"requires":["mining-productivity-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"mining-productivity-3":{"unlocks":{},"requires":["mining-productivity-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"worker-robots-speed-5":{"unlocks":{},"requires":["worker-robots-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"worker-robots-storage-1":{"unlocks":{},"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"worker-robots-storage-2":{"unlocks":{},"requires":["worker-robots-storage-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"worker-robots-storage-3":{"unlocks":{},"requires":["worker-robots-storage-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"toolbelt":{"unlocks":{},"requires":["logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"research-speed-1":{"unlocks":{},"requires":["automation-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"research-speed-2":{"unlocks":{},"requires":["research-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"research-speed-3":{"unlocks":{},"requires":["research-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"research-speed-4":{"unlocks":{},"requires":["research-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"research-speed-5":{"unlocks":{},"requires":["research-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"research-speed-6":{"unlocks":{},"requires":["research-speed-5"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"stack-inserter":{"unlocks":["stack-inserter","stack-filter-inserter"],"requires":["fast-inserter","logistics-2","advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"inserter-capacity-bonus-1":{"unlocks":{},"requires":["stack-inserter"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"inserter-capacity-bonus-2":{"unlocks":{},"requires":["inserter-capacity-bonus-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"inserter-capacity-bonus-3":{"unlocks":{},"requires":["inserter-capacity-bonus-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"inserter-capacity-bonus-4":{"unlocks":{},"requires":["inserter-capacity-bonus-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-5":{"unlocks":{},"requires":["inserter-capacity-bonus-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-6":{"unlocks":{},"requires":["inserter-capacity-bonus-5"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-7":{"unlocks":{},"requires":["inserter-capacity-bonus-6"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"oil-processing":{"unlocks":["pumpjack","oil-refinery","chemical-plant","basic-oil-processing","solid-fuel-from-petroleum-gas"],"requires":["fluid-handling"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"fluid-handling":{"unlocks":["storage-tank","pump","empty-barrel","fill-water-barrel","empty-water-barrel","fill-sulfuric-acid-barrel","empty-sulfuric-acid-barrel","fill-crude-oil-barrel","empty-crude-oil-barrel","fill-heavy-oil-barrel","empty-heavy-oil-barrel","fill-light-oil-barrel","empty-light-oil-barrel","fill-petroleum-gas-barrel","empty-petroleum-gas-barrel","fill-lubricant-barrel","empty-lubricant-barrel"],"requires":["automation-2","engine"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"advanced-oil-processing":{"unlocks":["advanced-oil-processing","heavy-oil-cracking","light-oil-cracking","solid-fuel-from-heavy-oil","solid-fuel-from-light-oil"],"requires":["chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"coal-liquefaction":{"unlocks":["coal-liquefaction"],"requires":["advanced-oil-processing","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"sulfur-processing":{"unlocks":["sulfuric-acid","sulfur"],"requires":["oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"plastics":{"unlocks":["plastic-bar"],"requires":["oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"artillery":{"unlocks":["artillery-wagon","artillery-turret","artillery-shell","artillery-targeting-remote"],"requires":["military-4","tank"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"spidertron":{"unlocks":["spidertron","spidertron-remote"],"requires":["military-4","exoskeleton-equipment","fusion-reactor-equipment","rocketry","rocket-control-unit","effectivity-module-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"military":{"unlocks":["submachine-gun","shotgun","shotgun-shell"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"atomic-bomb":{"unlocks":["atomic-bomb"],"requires":["military-4","kovarex-enrichment-process","rocket-control-unit","rocketry"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"military-2":{"unlocks":["piercing-rounds-magazine","grenade"],"requires":["military","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"uranium-ammo":{"unlocks":["uranium-rounds-magazine","uranium-cannon-shell","explosive-uranium-cannon-shell"],"requires":["uranium-processing","military-4","tank"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"military-3":{"unlocks":["poison-capsule","slowdown-capsule","combat-shotgun"],"requires":["chemical-science-pack","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"military-4":{"unlocks":["piercing-shotgun-shell","cluster-grenade"],"requires":["military-3","utility-science-pack","explosives"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"automobilism":{"unlocks":["car"],"requires":["logistics-2","engine"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"flammables":{"unlocks":{},"requires":["oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"flamethrower":{"unlocks":["flamethrower","flamethrower-ammo","flamethrower-turret"],"requires":["flammables","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"tank":{"unlocks":["tank","cannon-shell","explosive-cannon-shell"],"requires":["automobilism","military-3","explosives"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"land-mine":{"unlocks":["land-mine"],"requires":["explosives","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"rocketry":{"unlocks":["rocket-launcher","rocket"],"requires":["explosives","flammables","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"explosive-rocketry":{"unlocks":["explosive-rocket"],"requires":["rocketry","military-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"energy-weapons-damage-1":{"unlocks":{},"requires":["laser","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"refined-flammables-1":{"unlocks":{},"requires":["flamethrower"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"stronger-explosives-1":{"unlocks":{},"requires":["military-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"weapon-shooting-speed-1":{"unlocks":{},"requires":["military"],"ingredients":["automation-science-pack"],"has_modifier":true},"physical-projectile-damage-1":{"unlocks":{},"requires":["military"],"ingredients":["automation-science-pack"],"has_modifier":true},"energy-weapons-damage-2":{"unlocks":{},"requires":["energy-weapons-damage-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-2":{"unlocks":{},"requires":["physical-projectile-damage-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"refined-flammables-2":{"unlocks":{},"requires":["refined-flammables-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"stronger-explosives-2":{"unlocks":{},"requires":["stronger-explosives-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"weapon-shooting-speed-2":{"unlocks":{},"requires":["weapon-shooting-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"energy-weapons-damage-3":{"unlocks":{},"requires":["energy-weapons-damage-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-3":{"unlocks":{},"requires":["physical-projectile-damage-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"refined-flammables-3":{"unlocks":{},"requires":["refined-flammables-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"stronger-explosives-3":{"unlocks":{},"requires":["stronger-explosives-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"weapon-shooting-speed-3":{"unlocks":{},"requires":["weapon-shooting-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"energy-weapons-damage-4":{"unlocks":{},"requires":["energy-weapons-damage-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-4":{"unlocks":{},"requires":["physical-projectile-damage-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"refined-flammables-4":{"unlocks":{},"requires":["refined-flammables-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"stronger-explosives-4":{"unlocks":{},"requires":["stronger-explosives-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"weapon-shooting-speed-4":{"unlocks":{},"requires":["weapon-shooting-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"energy-weapons-damage-5":{"unlocks":{},"requires":["energy-weapons-damage-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"physical-projectile-damage-5":{"unlocks":{},"requires":["physical-projectile-damage-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"refined-flammables-5":{"unlocks":{},"requires":["refined-flammables-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"stronger-explosives-5":{"unlocks":{},"requires":["stronger-explosives-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"weapon-shooting-speed-5":{"unlocks":{},"requires":["weapon-shooting-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"energy-weapons-damage-6":{"unlocks":{},"requires":["energy-weapons-damage-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"physical-projectile-damage-6":{"unlocks":{},"requires":["physical-projectile-damage-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"refined-flammables-6":{"unlocks":{},"requires":["refined-flammables-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"stronger-explosives-6":{"unlocks":{},"requires":["stronger-explosives-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"weapon-shooting-speed-6":{"unlocks":{},"requires":["weapon-shooting-speed-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-1":{"unlocks":{},"requires":["laser","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-2":{"unlocks":{},"requires":["laser-shooting-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-3":{"unlocks":{},"requires":["laser-shooting-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-4":{"unlocks":{},"requires":["laser-shooting-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-5":{"unlocks":{},"requires":["laser-shooting-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-6":{"unlocks":{},"requires":["laser-shooting-speed-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-7":{"unlocks":{},"requires":["laser-shooting-speed-6"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"defender":{"unlocks":["defender-capsule"],"requires":["military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"distractor":{"unlocks":["distractor-capsule"],"requires":["defender","military-3","laser"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"destroyer":{"unlocks":["destroyer-capsule"],"requires":["military-4","distractor","speed-module"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"follower-robot-count-1":{"unlocks":{},"requires":["defender"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"follower-robot-count-2":{"unlocks":{},"requires":["follower-robot-count-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"follower-robot-count-3":{"unlocks":{},"requires":["follower-robot-count-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"follower-robot-count-4":{"unlocks":{},"requires":["follower-robot-count-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"follower-robot-count-5":{"unlocks":{},"requires":["follower-robot-count-4","destroyer"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"follower-robot-count-6":{"unlocks":{},"requires":["follower-robot-count-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"kovarex-enrichment-process":{"unlocks":["kovarex-enrichment-process","nuclear-fuel"],"requires":["production-science-pack","uranium-processing","rocket-fuel"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"nuclear-fuel-reprocessing":{"unlocks":["nuclear-fuel-reprocessing"],"requires":["nuclear-power","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"nuclear-power":{"unlocks":["nuclear-reactor","heat-exchanger","heat-pipe","steam-turbine"],"requires":["uranium-processing"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"uranium-processing":{"unlocks":["centrifuge","uranium-processing","uranium-fuel-cell"],"requires":["chemical-science-pack","concrete"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"heavy-armor":{"unlocks":["heavy-armor"],"requires":["military","steel-processing"],"ingredients":["automation-science-pack"],"has_modifier":false},"modular-armor":{"unlocks":["modular-armor"],"requires":["heavy-armor","advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"power-armor":{"unlocks":["power-armor"],"requires":["modular-armor","electric-engine","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"power-armor-mk2":{"unlocks":["power-armor-mk2"],"requires":["power-armor","military-4","speed-module-2","effectivity-module-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"energy-shield-equipment":{"unlocks":["energy-shield-equipment"],"requires":["solar-panel-equipment","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"energy-shield-mk2-equipment":{"unlocks":["energy-shield-mk2-equipment"],"requires":["energy-shield-equipment","military-3","low-density-structure","power-armor"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"night-vision-equipment":{"unlocks":["night-vision-equipment"],"requires":["solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"belt-immunity-equipment":{"unlocks":["belt-immunity-equipment"],"requires":["solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"exoskeleton-equipment":{"unlocks":["exoskeleton-equipment"],"requires":["advanced-electronics-2","electric-engine","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"battery-equipment":{"unlocks":["battery-equipment"],"requires":["battery","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"battery-mk2-equipment":{"unlocks":["battery-mk2-equipment"],"requires":["battery-equipment","low-density-structure","power-armor"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"solar-panel-equipment":{"unlocks":["solar-panel-equipment"],"requires":["modular-armor","solar-energy"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"fusion-reactor-equipment":{"unlocks":["fusion-reactor-equipment"],"requires":["utility-science-pack","power-armor","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"personal-laser-defense-equipment":{"unlocks":["personal-laser-defense-equipment"],"requires":["laser-turret","military-3","low-density-structure","power-armor","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"discharge-defense-equipment":{"unlocks":["discharge-defense-equipment","discharge-defense-remote"],"requires":["laser-turret","military-3","power-armor","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"modules":{"unlocks":{},"requires":["advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"speed-module":{"unlocks":["speed-module"],"requires":["modules"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"speed-module-2":{"unlocks":["speed-module-2"],"requires":["speed-module","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"speed-module-3":{"unlocks":["speed-module-3"],"requires":["speed-module-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"productivity-module":{"unlocks":["productivity-module"],"requires":["modules"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"productivity-module-2":{"unlocks":["productivity-module-2"],"requires":["productivity-module","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"productivity-module-3":{"unlocks":["productivity-module-3"],"requires":["productivity-module-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"effectivity-module":{"unlocks":["effectivity-module"],"requires":["modules"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"effectivity-module-2":{"unlocks":["effectivity-module-2"],"requires":["effectivity-module","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"effectivity-module-3":{"unlocks":["effectivity-module-3"],"requires":["effectivity-module-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"effect-transmission":{"unlocks":["beacon"],"requires":["advanced-electronics-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"low-density-structure":{"unlocks":["low-density-structure"],"requires":["advanced-material-processing","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"rocket-control-unit":{"unlocks":["rocket-control-unit"],"requires":["utility-science-pack","speed-module"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"rocket-fuel":{"unlocks":["rocket-fuel"],"requires":["flammables","advanced-oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"rocket-silo":{"unlocks":["rocket-silo","rocket-part"],"requires":["concrete","speed-module-3","productivity-module-3","rocket-fuel","rocket-control-unit"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"cliff-explosives":{"unlocks":["cliff-explosives"],"requires":["explosives","military-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false}} \ No newline at end of file +{"advanced-circuit":{"unlocks":["advanced-circuit"],"requires":["plastics"],"has_modifier":false},"advanced-combinators":{"unlocks":["selector-combinator"],"requires":["circuit-network","chemical-science-pack"],"has_modifier":false},"advanced-material-processing":{"unlocks":["steel-furnace"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"advanced-material-processing-2":{"unlocks":["electric-furnace"],"requires":["advanced-material-processing","chemical-science-pack"],"has_modifier":false},"advanced-oil-processing":{"unlocks":["advanced-oil-processing","heavy-oil-cracking","light-oil-cracking","solid-fuel-from-heavy-oil","solid-fuel-from-light-oil"],"requires":["chemical-science-pack"],"has_modifier":false},"artillery":{"unlocks":["artillery-wagon","artillery-turret","artillery-shell"],"requires":["military-4","tank","concrete","radar"],"has_modifier":false},"atomic-bomb":{"unlocks":["atomic-bomb"],"requires":["military-4","kovarex-enrichment-process","rocketry"],"has_modifier":false},"automated-rail-transportation":{"unlocks":["train-stop","rail-signal","rail-chain-signal"],"requires":["railway"],"has_modifier":false},"automation":{"unlocks":["assembling-machine-1","long-handed-inserter"],"requires":["automation-science-pack"],"has_modifier":false},"automation-2":{"unlocks":["assembling-machine-2"],"requires":["automation","steel-processing","logistic-science-pack"],"has_modifier":false},"automation-3":{"unlocks":["assembling-machine-3"],"requires":["speed-module","production-science-pack","electric-engine"],"has_modifier":false},"automation-science-pack":{"unlocks":["automation-science-pack"],"requires":["steam-power","electronics"],"has_modifier":false},"automobilism":{"unlocks":["car"],"requires":["logistics-2","engine"],"has_modifier":false},"battery":{"unlocks":["battery"],"requires":["sulfur-processing"],"has_modifier":false},"battery-equipment":{"unlocks":["battery-equipment"],"requires":["battery","solar-panel-equipment"],"has_modifier":false},"battery-mk2-equipment":{"unlocks":["battery-mk2-equipment"],"requires":["battery-equipment","low-density-structure","power-armor"],"has_modifier":false},"belt-immunity-equipment":{"unlocks":["belt-immunity-equipment"],"requires":["solar-panel-equipment"],"has_modifier":false},"braking-force-1":{"unlocks":{},"requires":["railway","chemical-science-pack"],"has_modifier":true},"braking-force-2":{"unlocks":{},"requires":["braking-force-1"],"has_modifier":true},"braking-force-3":{"unlocks":{},"requires":["braking-force-2","production-science-pack"],"has_modifier":true},"braking-force-4":{"unlocks":{},"requires":["braking-force-3"],"has_modifier":true},"braking-force-5":{"unlocks":{},"requires":["braking-force-4"],"has_modifier":true},"braking-force-6":{"unlocks":{},"requires":["braking-force-5","utility-science-pack"],"has_modifier":true},"braking-force-7":{"unlocks":{},"requires":["braking-force-6"],"has_modifier":true},"bulk-inserter":{"unlocks":["bulk-inserter"],"requires":["fast-inserter","logistics-2","advanced-circuit"],"has_modifier":true},"chemical-science-pack":{"unlocks":["chemical-science-pack"],"requires":["advanced-circuit","sulfur-processing"],"has_modifier":false},"circuit-network":{"unlocks":["arithmetic-combinator","decider-combinator","constant-combinator","power-switch","programmable-speaker","display-panel","iron-stick"],"requires":["logistic-science-pack"],"has_modifier":true},"cliff-explosives":{"unlocks":["cliff-explosives"],"requires":["explosives","military-2"],"has_modifier":true},"coal-liquefaction":{"unlocks":["coal-liquefaction"],"requires":["advanced-oil-processing","production-science-pack"],"has_modifier":false},"concrete":{"unlocks":["concrete","hazard-concrete","refined-concrete","refined-hazard-concrete","iron-stick"],"requires":["advanced-material-processing","automation-2"],"has_modifier":false},"construction-robotics":{"unlocks":["roboport","passive-provider-chest","storage-chest","construction-robot"],"requires":["robotics"],"has_modifier":true},"defender":{"unlocks":["defender-capsule"],"requires":["military-science-pack"],"has_modifier":true},"destroyer":{"unlocks":["destroyer-capsule"],"requires":["military-4","distractor","speed-module"],"has_modifier":false},"discharge-defense-equipment":{"unlocks":["discharge-defense-equipment"],"requires":["laser-turret","military-3","power-armor","solar-panel-equipment"],"has_modifier":false},"distractor":{"unlocks":["distractor-capsule"],"requires":["defender","military-3","laser"],"has_modifier":false},"effect-transmission":{"unlocks":["beacon"],"requires":["processing-unit","production-science-pack"],"has_modifier":false},"efficiency-module":{"unlocks":["efficiency-module"],"requires":["modules"],"has_modifier":false},"efficiency-module-2":{"unlocks":["efficiency-module-2"],"requires":["efficiency-module","processing-unit"],"has_modifier":false},"efficiency-module-3":{"unlocks":["efficiency-module-3"],"requires":["efficiency-module-2","production-science-pack"],"has_modifier":false},"electric-energy-accumulators":{"unlocks":["accumulator"],"requires":["electric-energy-distribution-1","battery"],"has_modifier":false},"electric-energy-distribution-1":{"unlocks":["medium-electric-pole","big-electric-pole","iron-stick"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"electric-energy-distribution-2":{"unlocks":["substation"],"requires":["electric-energy-distribution-1","chemical-science-pack"],"has_modifier":false},"electric-engine":{"unlocks":["electric-engine-unit"],"requires":["lubricant"],"has_modifier":false},"electric-mining-drill":{"unlocks":["electric-mining-drill"],"requires":["automation-science-pack"],"has_modifier":false},"electronics":{"unlocks":["copper-cable","electronic-circuit","lab","inserter","small-electric-pole"],"requires":{},"has_modifier":false},"energy-shield-equipment":{"unlocks":["energy-shield-equipment"],"requires":["solar-panel-equipment","military-science-pack"],"has_modifier":false},"energy-shield-mk2-equipment":{"unlocks":["energy-shield-mk2-equipment"],"requires":["energy-shield-equipment","military-3","low-density-structure","power-armor"],"has_modifier":false},"engine":{"unlocks":["engine-unit"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"exoskeleton-equipment":{"unlocks":["exoskeleton-equipment"],"requires":["processing-unit","electric-engine","solar-panel-equipment"],"has_modifier":false},"explosive-rocketry":{"unlocks":["explosive-rocket"],"requires":["rocketry","military-3"],"has_modifier":false},"explosives":{"unlocks":["explosives"],"requires":["sulfur-processing"],"has_modifier":false},"fast-inserter":{"unlocks":["fast-inserter"],"requires":["automation-science-pack"],"has_modifier":false},"fission-reactor-equipment":{"unlocks":["fission-reactor-equipment"],"requires":["utility-science-pack","power-armor","military-science-pack","nuclear-power"],"has_modifier":false},"flamethrower":{"unlocks":["flamethrower","flamethrower-ammo","flamethrower-turret"],"requires":["flammables","military-science-pack"],"has_modifier":false},"flammables":{"unlocks":{},"requires":["oil-processing"],"has_modifier":false},"fluid-handling":{"unlocks":["storage-tank","pump","barrel","water-barrel","empty-water-barrel","sulfuric-acid-barrel","empty-sulfuric-acid-barrel","crude-oil-barrel","empty-crude-oil-barrel","heavy-oil-barrel","empty-heavy-oil-barrel","light-oil-barrel","empty-light-oil-barrel","petroleum-gas-barrel","empty-petroleum-gas-barrel","lubricant-barrel","empty-lubricant-barrel"],"requires":["automation-2","engine"],"has_modifier":false},"fluid-wagon":{"unlocks":["fluid-wagon"],"requires":["railway","fluid-handling"],"has_modifier":false},"follower-robot-count-1":{"unlocks":{},"requires":["defender"],"has_modifier":true},"follower-robot-count-2":{"unlocks":{},"requires":["follower-robot-count-1"],"has_modifier":true},"follower-robot-count-3":{"unlocks":{},"requires":["follower-robot-count-2","chemical-science-pack"],"has_modifier":true},"follower-robot-count-4":{"unlocks":{},"requires":["follower-robot-count-3","destroyer"],"has_modifier":true},"gate":{"unlocks":["gate"],"requires":["stone-wall","military-2"],"has_modifier":false},"gun-turret":{"unlocks":["gun-turret"],"requires":["automation-science-pack"],"has_modifier":false},"heavy-armor":{"unlocks":["heavy-armor"],"requires":["military","steel-processing"],"has_modifier":false},"inserter-capacity-bonus-1":{"unlocks":{},"requires":["bulk-inserter"],"has_modifier":true},"inserter-capacity-bonus-2":{"unlocks":{},"requires":["inserter-capacity-bonus-1"],"has_modifier":true},"inserter-capacity-bonus-3":{"unlocks":{},"requires":["inserter-capacity-bonus-2","chemical-science-pack"],"has_modifier":true},"inserter-capacity-bonus-4":{"unlocks":{},"requires":["inserter-capacity-bonus-3","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-5":{"unlocks":{},"requires":["inserter-capacity-bonus-4"],"has_modifier":true},"inserter-capacity-bonus-6":{"unlocks":{},"requires":["inserter-capacity-bonus-5"],"has_modifier":true},"inserter-capacity-bonus-7":{"unlocks":{},"requires":["inserter-capacity-bonus-6","utility-science-pack"],"has_modifier":true},"kovarex-enrichment-process":{"unlocks":["kovarex-enrichment-process","nuclear-fuel"],"requires":["production-science-pack","uranium-processing","rocket-fuel"],"has_modifier":false},"lamp":{"unlocks":["small-lamp"],"requires":["automation-science-pack"],"has_modifier":false},"land-mine":{"unlocks":["land-mine"],"requires":["explosives","military-science-pack"],"has_modifier":false},"landfill":{"unlocks":["landfill"],"requires":["logistic-science-pack"],"has_modifier":false},"laser":{"unlocks":{},"requires":["battery","chemical-science-pack"],"has_modifier":false},"laser-shooting-speed-1":{"unlocks":{},"requires":["laser","military-science-pack"],"has_modifier":true},"laser-shooting-speed-2":{"unlocks":{},"requires":["laser-shooting-speed-1"],"has_modifier":true},"laser-shooting-speed-3":{"unlocks":{},"requires":["laser-shooting-speed-2"],"has_modifier":true},"laser-shooting-speed-4":{"unlocks":{},"requires":["laser-shooting-speed-3"],"has_modifier":true},"laser-shooting-speed-5":{"unlocks":{},"requires":["laser-shooting-speed-4","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-6":{"unlocks":{},"requires":["laser-shooting-speed-5"],"has_modifier":true},"laser-shooting-speed-7":{"unlocks":{},"requires":["laser-shooting-speed-6"],"has_modifier":true},"laser-turret":{"unlocks":["laser-turret"],"requires":["laser","military-science-pack"],"has_modifier":false},"laser-weapons-damage-1":{"unlocks":{},"requires":["laser","military-science-pack"],"has_modifier":true},"laser-weapons-damage-2":{"unlocks":{},"requires":["laser-weapons-damage-1"],"has_modifier":true},"laser-weapons-damage-3":{"unlocks":{},"requires":["laser-weapons-damage-2"],"has_modifier":true},"laser-weapons-damage-4":{"unlocks":{},"requires":["laser-weapons-damage-3"],"has_modifier":true},"laser-weapons-damage-5":{"unlocks":{},"requires":["laser-weapons-damage-4","utility-science-pack"],"has_modifier":true},"laser-weapons-damage-6":{"unlocks":{},"requires":["laser-weapons-damage-5"],"has_modifier":true},"logistic-robotics":{"unlocks":["roboport","passive-provider-chest","storage-chest","logistic-robot"],"requires":["robotics"],"has_modifier":true},"logistic-science-pack":{"unlocks":["logistic-science-pack"],"requires":["automation-science-pack"],"has_modifier":false},"logistic-system":{"unlocks":["active-provider-chest","requester-chest","buffer-chest"],"requires":["utility-science-pack","logistic-robotics"],"has_modifier":true},"logistics":{"unlocks":["underground-belt","splitter"],"requires":["automation-science-pack"],"has_modifier":false},"logistics-2":{"unlocks":["fast-transport-belt","fast-underground-belt","fast-splitter"],"requires":["logistics","logistic-science-pack"],"has_modifier":false},"logistics-3":{"unlocks":["express-transport-belt","express-underground-belt","express-splitter"],"requires":["production-science-pack","lubricant"],"has_modifier":false},"low-density-structure":{"unlocks":["low-density-structure"],"requires":["advanced-material-processing","chemical-science-pack"],"has_modifier":false},"lubricant":{"unlocks":["lubricant"],"requires":["advanced-oil-processing"],"has_modifier":false},"military":{"unlocks":["submachine-gun","shotgun","shotgun-shell"],"requires":["automation-science-pack"],"has_modifier":false},"military-2":{"unlocks":["piercing-rounds-magazine","grenade"],"requires":["military","steel-processing","logistic-science-pack"],"has_modifier":false},"military-3":{"unlocks":["poison-capsule","slowdown-capsule","combat-shotgun"],"requires":["chemical-science-pack","military-science-pack"],"has_modifier":false},"military-4":{"unlocks":["piercing-shotgun-shell","cluster-grenade"],"requires":["military-3","utility-science-pack","explosives"],"has_modifier":false},"military-science-pack":{"unlocks":["military-science-pack"],"requires":["military-2","stone-wall"],"has_modifier":false},"mining-productivity-1":{"unlocks":{},"requires":["advanced-circuit"],"has_modifier":true},"mining-productivity-2":{"unlocks":{},"requires":["mining-productivity-1","chemical-science-pack"],"has_modifier":true},"mining-productivity-3":{"unlocks":{},"requires":["mining-productivity-2","production-science-pack","utility-science-pack"],"has_modifier":true},"modular-armor":{"unlocks":["modular-armor"],"requires":["heavy-armor","advanced-circuit"],"has_modifier":false},"modules":{"unlocks":{},"requires":["advanced-circuit"],"has_modifier":false},"night-vision-equipment":{"unlocks":["night-vision-equipment"],"requires":["solar-panel-equipment"],"has_modifier":false},"nuclear-fuel-reprocessing":{"unlocks":["nuclear-fuel-reprocessing"],"requires":["nuclear-power","production-science-pack"],"has_modifier":false},"nuclear-power":{"unlocks":["nuclear-reactor","heat-exchanger","heat-pipe","steam-turbine","uranium-fuel-cell"],"requires":["uranium-processing"],"has_modifier":false},"oil-gathering":{"unlocks":["pumpjack"],"requires":["fluid-handling"],"has_modifier":false},"oil-processing":{"unlocks":["oil-refinery","chemical-plant","basic-oil-processing","solid-fuel-from-petroleum-gas"],"requires":["oil-gathering"],"has_modifier":false},"personal-laser-defense-equipment":{"unlocks":["personal-laser-defense-equipment"],"requires":["laser-turret","military-3","low-density-structure","power-armor","solar-panel-equipment"],"has_modifier":false},"personal-roboport-equipment":{"unlocks":["personal-roboport-equipment"],"requires":["construction-robotics","solar-panel-equipment"],"has_modifier":false},"personal-roboport-mk2-equipment":{"unlocks":["personal-roboport-mk2-equipment"],"requires":["personal-roboport-equipment","utility-science-pack"],"has_modifier":false},"physical-projectile-damage-1":{"unlocks":{},"requires":["military"],"has_modifier":true},"physical-projectile-damage-2":{"unlocks":{},"requires":["physical-projectile-damage-1","logistic-science-pack"],"has_modifier":true},"physical-projectile-damage-3":{"unlocks":{},"requires":["physical-projectile-damage-2","military-science-pack"],"has_modifier":true},"physical-projectile-damage-4":{"unlocks":{},"requires":["physical-projectile-damage-3"],"has_modifier":true},"physical-projectile-damage-5":{"unlocks":{},"requires":["physical-projectile-damage-4","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-6":{"unlocks":{},"requires":["physical-projectile-damage-5","utility-science-pack"],"has_modifier":true},"plastics":{"unlocks":["plastic-bar"],"requires":["oil-processing"],"has_modifier":false},"power-armor":{"unlocks":["power-armor"],"requires":["modular-armor","electric-engine","processing-unit"],"has_modifier":false},"power-armor-mk2":{"unlocks":["power-armor-mk2"],"requires":["power-armor","military-4","speed-module-2","efficiency-module-2"],"has_modifier":false},"processing-unit":{"unlocks":["processing-unit"],"requires":["chemical-science-pack"],"has_modifier":false},"production-science-pack":{"unlocks":["production-science-pack"],"requires":["productivity-module","advanced-material-processing-2","railway"],"has_modifier":false},"productivity-module":{"unlocks":["productivity-module"],"requires":["modules"],"has_modifier":false},"productivity-module-2":{"unlocks":["productivity-module-2"],"requires":["productivity-module","processing-unit"],"has_modifier":false},"productivity-module-3":{"unlocks":["productivity-module-3"],"requires":["productivity-module-2","production-science-pack"],"has_modifier":false},"radar":{"unlocks":["radar"],"requires":["automation-science-pack"],"has_modifier":false},"railway":{"unlocks":["rail","locomotive","cargo-wagon","iron-stick"],"requires":["logistics-2","engine"],"has_modifier":false},"refined-flammables-1":{"unlocks":{},"requires":["flamethrower"],"has_modifier":true},"refined-flammables-2":{"unlocks":{},"requires":["refined-flammables-1"],"has_modifier":true},"refined-flammables-3":{"unlocks":{},"requires":["refined-flammables-2","chemical-science-pack"],"has_modifier":true},"refined-flammables-4":{"unlocks":{},"requires":["refined-flammables-3","utility-science-pack"],"has_modifier":true},"refined-flammables-5":{"unlocks":{},"requires":["refined-flammables-4"],"has_modifier":true},"refined-flammables-6":{"unlocks":{},"requires":["refined-flammables-5"],"has_modifier":true},"repair-pack":{"unlocks":["repair-pack"],"requires":["automation-science-pack"],"has_modifier":false},"research-speed-1":{"unlocks":{},"requires":["automation-2"],"has_modifier":true},"research-speed-2":{"unlocks":{},"requires":["research-speed-1"],"has_modifier":true},"research-speed-3":{"unlocks":{},"requires":["research-speed-2","chemical-science-pack"],"has_modifier":true},"research-speed-4":{"unlocks":{},"requires":["research-speed-3"],"has_modifier":true},"research-speed-5":{"unlocks":{},"requires":["research-speed-4","production-science-pack"],"has_modifier":true},"research-speed-6":{"unlocks":{},"requires":["research-speed-5","utility-science-pack"],"has_modifier":true},"robotics":{"unlocks":["flying-robot-frame"],"requires":["electric-engine","battery"],"has_modifier":false},"rocket-fuel":{"unlocks":["rocket-fuel"],"requires":["flammables","advanced-oil-processing"],"has_modifier":false},"rocket-silo":{"unlocks":["rocket-silo","rocket-part","cargo-landing-pad","satellite"],"requires":["concrete","rocket-fuel","electric-energy-accumulators","solar-energy","utility-science-pack","speed-module-3","productivity-module-3","radar"],"has_modifier":false},"rocketry":{"unlocks":["rocket-launcher","rocket"],"requires":["explosives","flammables","military-science-pack"],"has_modifier":false},"solar-energy":{"unlocks":["solar-panel"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"solar-panel-equipment":{"unlocks":["solar-panel-equipment"],"requires":["modular-armor","solar-energy"],"has_modifier":false},"space-science-pack":{"unlocks":{},"requires":["rocket-silo"],"has_modifier":false},"speed-module":{"unlocks":["speed-module"],"requires":["modules"],"has_modifier":false},"speed-module-2":{"unlocks":["speed-module-2"],"requires":["speed-module","processing-unit"],"has_modifier":false},"speed-module-3":{"unlocks":["speed-module-3"],"requires":["speed-module-2","production-science-pack"],"has_modifier":false},"spidertron":{"unlocks":["spidertron"],"requires":["military-4","exoskeleton-equipment","fission-reactor-equipment","rocketry","efficiency-module-3","radar"],"has_modifier":false},"steam-power":{"unlocks":["pipe","pipe-to-ground","offshore-pump","boiler","steam-engine"],"requires":{},"has_modifier":false},"steel-axe":{"unlocks":{},"requires":["steel-processing"],"has_modifier":true},"steel-processing":{"unlocks":["steel-plate","steel-chest"],"requires":["automation-science-pack"],"has_modifier":false},"stone-wall":{"unlocks":["stone-wall"],"requires":["automation-science-pack"],"has_modifier":false},"stronger-explosives-1":{"unlocks":{},"requires":["military-2"],"has_modifier":true},"stronger-explosives-2":{"unlocks":{},"requires":["stronger-explosives-1","military-science-pack"],"has_modifier":true},"stronger-explosives-3":{"unlocks":{},"requires":["stronger-explosives-2","chemical-science-pack"],"has_modifier":true},"stronger-explosives-4":{"unlocks":{},"requires":["stronger-explosives-3","utility-science-pack"],"has_modifier":true},"stronger-explosives-5":{"unlocks":{},"requires":["stronger-explosives-4"],"has_modifier":true},"stronger-explosives-6":{"unlocks":{},"requires":["stronger-explosives-5"],"has_modifier":true},"sulfur-processing":{"unlocks":["sulfuric-acid","sulfur"],"requires":["oil-processing"],"has_modifier":false},"tank":{"unlocks":["tank","cannon-shell","explosive-cannon-shell"],"requires":["automobilism","military-3","explosives"],"has_modifier":false},"toolbelt":{"unlocks":{},"requires":["logistic-science-pack"],"has_modifier":true},"uranium-ammo":{"unlocks":["uranium-rounds-magazine","uranium-cannon-shell","explosive-uranium-cannon-shell"],"requires":["uranium-processing","military-4","tank"],"has_modifier":false},"uranium-mining":{"unlocks":{},"requires":["chemical-science-pack","concrete"],"has_modifier":true},"uranium-processing":{"unlocks":["centrifuge","uranium-processing"],"requires":["uranium-mining"],"has_modifier":false},"utility-science-pack":{"unlocks":["utility-science-pack"],"requires":["robotics","processing-unit","low-density-structure"],"has_modifier":false},"weapon-shooting-speed-1":{"unlocks":{},"requires":["military"],"has_modifier":true},"weapon-shooting-speed-2":{"unlocks":{},"requires":["weapon-shooting-speed-1","logistic-science-pack"],"has_modifier":true},"weapon-shooting-speed-3":{"unlocks":{},"requires":["weapon-shooting-speed-2","military-science-pack"],"has_modifier":true},"weapon-shooting-speed-4":{"unlocks":{},"requires":["weapon-shooting-speed-3"],"has_modifier":true},"weapon-shooting-speed-5":{"unlocks":{},"requires":["weapon-shooting-speed-4","chemical-science-pack"],"has_modifier":true},"weapon-shooting-speed-6":{"unlocks":{},"requires":["weapon-shooting-speed-5","utility-science-pack"],"has_modifier":true},"worker-robots-speed-1":{"unlocks":{},"requires":["robotics"],"has_modifier":true},"worker-robots-speed-2":{"unlocks":{},"requires":["worker-robots-speed-1"],"has_modifier":true},"worker-robots-speed-3":{"unlocks":{},"requires":["worker-robots-speed-2","utility-science-pack"],"has_modifier":true},"worker-robots-speed-4":{"unlocks":{},"requires":["worker-robots-speed-3"],"has_modifier":true},"worker-robots-speed-5":{"unlocks":{},"requires":["worker-robots-speed-4","production-science-pack"],"has_modifier":true},"worker-robots-storage-1":{"unlocks":{},"requires":["robotics"],"has_modifier":true},"worker-robots-storage-2":{"unlocks":{},"requires":["worker-robots-storage-1","production-science-pack"],"has_modifier":true},"worker-robots-storage-3":{"unlocks":{},"requires":["worker-robots-storage-2","utility-science-pack"],"has_modifier":true}} \ No newline at end of file From 432d8fa1c243862f0d420bca37e77309ba46dbd6 Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:02:37 +0100 Subject: [PATCH 07/60] [PKMN RB] Adds slot data that tells the tracker V5 logic should be considered (#3995) * Adds v5_update-info to slot data * Adds a comma Co-authored-by: Scipio Wright --------- Co-authored-by: Scipio Wright --- worlds/pokemon_rb/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 6db0c9e0f8bf..98b1a0c614b8 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -712,6 +712,7 @@ def fill_slot_data(self) -> dict: "require_pokedex": self.options.require_pokedex.value, "area_1_to_1_mapping": self.options.area_1_to_1_mapping.value, "blind_trainers": self.options.blind_trainers.value, + "v5_update": True, } if self.options.type_chart_seed == "random" or self.options.type_chart_seed.value.isdigit(): From 10bc05a172d6c82c91493a7a71487907f87cabf7 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:50:12 -0500 Subject: [PATCH 08/60] Update codeQL to v3 (#4143) --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b0cfe35d2bc5..3abbb5f6449f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +58,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -72,4 +72,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 74f922ea379d78a5e5787626b85e9f7cff49616e Mon Sep 17 00:00:00 2001 From: Hexa <57190472+Hexadigital@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:03:48 -0500 Subject: [PATCH 09/60] MMBN3: Typo for SloGauge (#3457) --- worlds/mmbn3/Items.py | 2 +- worlds/mmbn3/Names/ItemName.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/mmbn3/Items.py b/worlds/mmbn3/Items.py index 2e249ce79e8c..30ec311ecbe2 100644 --- a/worlds/mmbn3/Items.py +++ b/worlds/mmbn3/Items.py @@ -171,7 +171,7 @@ class MMBN3Item(Item): ItemData(0xB31063, ItemName.SandStage_C, ItemClassification.filler, ItemType.Chip, 182, chip_code('C')), ItemData(0xB31064, ItemName.SideGun_S, ItemClassification.filler, ItemType.Chip, 12, chip_code('S')), ItemData(0xB31065, ItemName.Slasher_B, ItemClassification.useful, ItemType.Chip, 43, chip_code('B')), - ItemData(0xB31066, ItemName.SloGuage_star, ItemClassification.filler, ItemType.Chip, 157, chip_code('*')), + ItemData(0xB31066, ItemName.SloGauge_star, ItemClassification.filler, ItemType.Chip, 157, chip_code('*')), ItemData(0xB31067, ItemName.Snake_D, ItemClassification.useful, ItemType.Chip, 131, chip_code('D')), ItemData(0xB31068, ItemName.Snctuary_C, ItemClassification.useful, ItemType.Chip, 184, chip_code('C')), ItemData(0xB31069, ItemName.Spreader_star, ItemClassification.useful, ItemType.Chip, 13, chip_code('*')), diff --git a/worlds/mmbn3/Names/ItemName.py b/worlds/mmbn3/Names/ItemName.py index 441bdc591c51..677eff22b353 100644 --- a/worlds/mmbn3/Names/ItemName.py +++ b/worlds/mmbn3/Names/ItemName.py @@ -72,7 +72,7 @@ class ItemName(): SandStage_C = "SandStage C" SideGun_S = "SideGun S" Slasher_B = "Slasher B" - SloGuage_star = "SloGuage *" + SloGauge_star = "SloGauge *" Snake_D = "Snake D" Snctuary_C = "Snctuary C" Spreader_star = "Spreader *" @@ -235,4 +235,4 @@ class ItemName(): RegUP3 = "RegUP3" SubMem = "SubMem" - Victory = "Victory" \ No newline at end of file + Victory = "Victory" From 17f03bb5f8a286e96dac496063840ab3cbdb24a2 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 11 Nov 2024 23:27:43 -0500 Subject: [PATCH 10/60] Stardew valley: Fixed furnace logic bug (#4163) --- worlds/stardew_valley/data/craftable_data.py | 11 ++++-- worlds/stardew_valley/data/recipe_source.py | 5 +++ worlds/stardew_valley/logic/crafting_logic.py | 7 ++-- .../test/assertion/rule_assert.py | 9 +++++ .../stardew_valley/test/rules/TestBundles.py | 1 + .../test/rules/TestCraftingRecipes.py | 34 +++++++++++++++++++ 6 files changed, 62 insertions(+), 5 deletions(-) diff --git a/worlds/stardew_valley/data/craftable_data.py b/worlds/stardew_valley/data/craftable_data.py index d83478a62051..713db4732075 100644 --- a/worlds/stardew_valley/data/craftable_data.py +++ b/worlds/stardew_valley/data/craftable_data.py @@ -1,7 +1,7 @@ from typing import Dict, List, Optional from .recipe_source import RecipeSource, StarterSource, QueenOfSauceSource, ShopSource, SkillSource, FriendshipSource, ShopTradeSource, CutsceneSource, \ - ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource + ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource, SkillCraftsanitySource from ..mods.mod_data import ModNames from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood @@ -64,6 +64,11 @@ def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], return create_recipe(name, ingredients, source, mod_name) +def skill_craftsanity_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: + source = SkillCraftsanitySource(skill, level) + return create_recipe(name, ingredients, source, mod_name) + + def mastery_recipe(name: str, skill: str, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: source = MasterySource(skill) return create_recipe(name, ingredients, source, mod_name) @@ -249,7 +254,9 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 2, {Material.wood: 20, MetalBar.copper: 2}) crystalarium = skill_recipe(Machine.crystalarium, Skill.mining, 9, {Material.stone: 99, MetalBar.gold: 5, MetalBar.iridium: 2, ArtisanGood.battery_pack: 1}) -furnace = skill_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) +# In-Game, the Furnace recipe is completely unique. It is the only recipe that is obtained in a cutscene after doing a skill-related action. +# So it has a custom source that needs both the craftsanity item from AP and the skill, if craftsanity is enabled. +furnace = skill_craftsanity_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) geode_crusher = special_order_recipe(Machine.geode_crusher, SpecialOrder.cave_patrol, {MetalBar.gold: 2, Material.stone: 50, Mineral.diamond: 1}) mushroom_log = skill_recipe(Machine.mushroom_log, Skill.foraging, 4, {Material.hardwood: 10, Material.moss: 10}) heavy_tapper = ap_recipe(Machine.heavy_tapper, {Material.hardwood: 30, MetalBar.radioactive: 1}) diff --git a/worlds/stardew_valley/data/recipe_source.py b/worlds/stardew_valley/data/recipe_source.py index 24b03bf77bd4..ead4d62f1650 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -94,6 +94,11 @@ def __repr__(self): return f"SkillSource at level {self.level} {self.skill}" +class SkillCraftsanitySource(SkillSource): + def __repr__(self): + return f"SkillCraftsanitySource at level {self.level} {self.skill}" + + class MasterySource(RecipeSource): skill: str diff --git a/worlds/stardew_valley/logic/crafting_logic.py b/worlds/stardew_valley/logic/crafting_logic.py index e346e4ba238b..0403230eee34 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -14,7 +14,7 @@ from .. import options from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \ - FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource + FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource from ..locations import locations_by_tag, LocationTags from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland, SkillProgression from ..stardew_rule import StardewRule, True_, False_ @@ -54,8 +54,7 @@ def knows_recipe(self, recipe: CraftingRecipe) -> StardewRule: return self.logic.crafting.received_recipe(recipe.item) if self.options.craftsanity == Craftsanity.option_none: return self.logic.crafting.can_learn_recipe(recipe) - if isinstance(recipe.source, StarterSource) or isinstance(recipe.source, ShopTradeSource) or isinstance( - recipe.source, ShopSource): + if isinstance(recipe.source, (StarterSource, ShopTradeSource, ShopSource, SkillCraftsanitySource)): return self.logic.crafting.received_recipe(recipe.item) if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations & SpecialOrderLocations.option_board: return self.logic.crafting.received_recipe(recipe.item) @@ -71,6 +70,8 @@ def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule: return self.logic.money.can_trade_at(recipe.source.region, recipe.source.currency, recipe.source.price) if isinstance(recipe.source, ShopSource): return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price) + if isinstance(recipe.source, SkillCraftsanitySource): + return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) & self.logic.skill.can_earn_level(recipe.source.skill, recipe.source.level) if isinstance(recipe.source, SkillSource): return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) if isinstance(recipe.source, MasterySource): diff --git a/worlds/stardew_valley/test/assertion/rule_assert.py b/worlds/stardew_valley/test/assertion/rule_assert.py index 5a1dad2925cf..1031a18e115c 100644 --- a/worlds/stardew_valley/test/assertion/rule_assert.py +++ b/worlds/stardew_valley/test/assertion/rule_assert.py @@ -1,3 +1,4 @@ +from typing import List from unittest import TestCase from BaseClasses import CollectionState, Location @@ -14,6 +15,10 @@ def assert_rule_true(self, rule: StardewRule, state: CollectionState): raise AssertionError(f"Error while checking rule {rule}: {e}" f"\nExplanation: {expl}") + def assert_rules_true(self, rules: List[StardewRule], state: CollectionState): + for rule in rules: + self.assert_rule_true(rule, state) + def assert_rule_false(self, rule: StardewRule, state: CollectionState): expl = explain(rule, state, expected=False) try: @@ -22,6 +27,10 @@ def assert_rule_false(self, rule: StardewRule, state: CollectionState): raise AssertionError(f"Error while checking rule {rule}: {e}" f"\nExplanation: {expl}") + def assert_rules_false(self, rules: List[StardewRule], state: CollectionState): + for rule in rules: + self.assert_rule_false(rule, state) + def assert_rule_can_be_resolved(self, rule: StardewRule, complete_state: CollectionState): expl = explain(rule, complete_state) try: diff --git a/worlds/stardew_valley/test/rules/TestBundles.py b/worlds/stardew_valley/test/rules/TestBundles.py index ab376c90d4ea..0bc7f9bfdfd4 100644 --- a/worlds/stardew_valley/test/rules/TestBundles.py +++ b/worlds/stardew_valley/test/rules/TestBundles.py @@ -56,6 +56,7 @@ def test_raccoon_bundles_rely_on_previous_ones(self): self.collect("Mushroom Boxes") self.collect("Progressive Fishing Rod", 4) self.collect("Fishing Level", 10) + self.collect("Furnace Recipe") self.assertFalse(raccoon_rule_1(self.multiworld.state)) self.assertFalse(raccoon_rule_3(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py index 93c325ae5c5c..4719edea1d59 100644 --- a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -50,6 +50,23 @@ def test_can_craft_festival_recipe(self): self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) + def test_require_furnace_recipe_for_smelting_checks(self): + locations = ["Craft Furnace", "Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] + rules = [self.world.logic.region.can_reach_location(location) for location in locations] + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Progressive Trash Can")] * 2) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rules_false(rules, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Furnace Recipe"), prevent_sweep=False) + self.assert_rules_true(rules, self.multiworld.state) + class TestCraftsanityWithFestivalsLogic(SVTestBase): options = { @@ -101,6 +118,23 @@ def test_can_craft_festival_recipe(self): self.collect([self.create_item("Progressive Season")] * 2) self.assert_rule_true(rule, self.multiworld.state) + def test_requires_mining_levels_for_smelting_checks(self): + locations = ["Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] + rules = [self.world.logic.region.can_reach_location(location) for location in locations] + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Progressive Trash Can")] * 2) + self.multiworld.state.collect(self.create_item("Furnace Recipe"), prevent_sweep=False) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rules_false(rules, self.multiworld.state) + + self.collect([self.create_item("Mining Level")] * 10) + self.assert_rules_true(rules, self.multiworld.state) + class TestNoCraftsanityWithFestivalsLogic(SVTestBase): options = { From 8b87e20a966077c8aa84c56764a53aeefda2c896 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:13:49 -0500 Subject: [PATCH 11/60] DLCQuest: Use options API for campaign and remove unused imports in tests #4181 --- worlds/dlcquest/test/TestOptionsLong.py | 1 - worlds/dlcquest/test/checks/world_checks.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py index 3e9acac7e791..c6c594b6a004 100644 --- a/worlds/dlcquest/test/TestOptionsLong.py +++ b/worlds/dlcquest/test/TestOptionsLong.py @@ -5,7 +5,6 @@ from .option_names import options_to_include from .checks.world_checks import assert_can_win, assert_same_number_items_locations from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld -from ... import AutoWorldRegister def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld): diff --git a/worlds/dlcquest/test/checks/world_checks.py b/worlds/dlcquest/test/checks/world_checks.py index cc2fa7f51ad2..48c919c0f62b 100644 --- a/worlds/dlcquest/test/checks/world_checks.py +++ b/worlds/dlcquest/test/checks/world_checks.py @@ -1,6 +1,6 @@ from typing import List -from BaseClasses import MultiWorld, ItemClassification +from BaseClasses import MultiWorld from .. import DLCQuestTestBase from ... import Options @@ -14,7 +14,7 @@ def get_all_location_names(multiworld: MultiWorld) -> List[str]: def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld): - campaign = multiworld.campaign[1] + campaign = multiworld.worlds[1].options.campaign all_items = [item.name for item in multiworld.get_items()] if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both: tester.assertIn("Victory Basic", all_items) @@ -25,7 +25,7 @@ def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld): def collect_all_then_assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld): for item in multiworld.get_items(): multiworld.state.collect(item) - campaign = multiworld.campaign[1] + campaign = multiworld.worlds[1].options.campaign if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both: tester.assertTrue(multiworld.find_item("Victory Basic", 1).can_reach(multiworld.state)) if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both: @@ -39,4 +39,4 @@ def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld): def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld): non_event_locations = [location for location in multiworld.get_locations() if not location.advancement] - tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) \ No newline at end of file + tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) From 85159a4f1f12ed1c84d5f2be6aa3f90611334b6b Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Tue, 12 Nov 2024 17:03:27 -0800 Subject: [PATCH 12/60] Factorio: Fix satellite goal (#4183) --- worlds/factorio/data/mod_template/control.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 51a588a94cb9..4383357546d9 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -310,7 +310,12 @@ script.on_event(defines.events.on_player_removed, on_player_removed) function on_rocket_launched(event) if event.rocket and event.rocket.valid and storage.forcedata[event.rocket.force.name]['victory'] == 0 then - if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then + satellite_count = 0 + cargo_pod = event.rocket.cargo_pod + if cargo_pod then + satellite_count = cargo_pod.get_item_count("satellite") + end + if satellite_count > 0 or GOAL == 0 then storage.forcedata[event.rocket.force.name]['victory'] = 1 dumpInfo(event.rocket.force) game.set_game_state From c295926ce1ff3d6f909236d215f9e2517ad6b314 Mon Sep 17 00:00:00 2001 From: Benjamin S Wolf Date: Wed, 13 Nov 2024 04:05:34 -0800 Subject: [PATCH 13/60] Landstalker: remove global ref to multiworld (#4175) * Landstalker: remove global ref to multiworld `cached_spheres` holds a reference to the multiworld, which leaks the multiworld if multidata is skipped. Instead of making it a class variable, give a reference to each matching world. * Switch to using `get_game_worlds` --- worlds/landstalker/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index 2b3dc41239c3..8463e56e54c1 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -38,7 +38,7 @@ class LandstalkerWorld(World): item_name_to_id = build_item_name_to_id_table() location_name_to_id = build_location_name_to_id_table() - cached_spheres: ClassVar[List[Set[Location]]] + cached_spheres: List[Set[Location]] def __init__(self, multiworld, player): super().__init__(multiworld, player) @@ -47,6 +47,7 @@ def __init__(self, multiworld, player): self.dark_region_ids = [] self.teleport_tree_pairs = [] self.jewel_items = [] + self.cached_spheres = [] def fill_slot_data(self) -> dict: # Generate hints. @@ -220,14 +221,17 @@ def get_starting_health(self): return 4 @classmethod - def stage_post_fill(cls, multiworld): + def stage_post_fill(cls, multiworld: MultiWorld): # Cache spheres for hint calculation after fill completes. - cls.cached_spheres = list(multiworld.get_spheres()) + cached_spheres = list(multiworld.get_spheres()) + for world in multiworld.get_game_worlds(cls.game): + world.cached_spheres = cached_spheres @classmethod - def stage_modify_multidata(cls, *_): + def stage_modify_multidata(cls, multiworld: MultiWorld, *_): # Clean up all references in cached spheres after generation completes. - del cls.cached_spheres + for world in multiworld.get_game_worlds(cls.game): + world.cached_spheres = [] def adjust_shop_prices(self): # Calculate prices for items in shops once all items have their final position From eac3e3c29e472ad4e9c222b7d115640cbad550f1 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:04:31 -0500 Subject: [PATCH 14/60] Tests: Add ignore filter to s2clientprotocol warnings (#4180) --- test/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/__init__.py b/test/__init__.py index 37ebe3f62743..ab9383b3cd90 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -4,6 +4,7 @@ import settings warnings.simplefilter("always") +warnings.filterwarnings(action="ignore", category=DeprecationWarning, module="s2clientprotocol") settings.no_gui = True settings.skip_autosave = True From 8f60a4a259b5530f3be2424783669d18046ac8b0 Mon Sep 17 00:00:00 2001 From: Katelyn Gigante Date: Fri, 15 Nov 2024 07:51:05 +1100 Subject: [PATCH 15/60] Core: Detect and account for apworlds being downloaded with a (1) in their name (#4144) * Core: Detect and account for apworlds being downloaded with a (1) in their name * Reword comment * Always use internal module name * Requested changes from black-silver --- worlds/LauncherComponents.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index fe6e44bb308e..3c4c4477ef09 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -100,10 +100,16 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path apworld_path = pathlib.Path(apworld_src) - module_name = pathlib.Path(apworld_path.name).stem try: import zipfile - zipfile.ZipFile(apworld_path).open(module_name + "/__init__.py") + zip = zipfile.ZipFile(apworld_path) + directories = [f.filename.strip('/') for f in zip.filelist if f.CRC == 0 and f.file_size == 0 and f.filename.count('/') == 1] + if len(directories) == 1 and directories[0] in apworld_path.stem: + module_name = directories[0] + apworld_name = module_name + ".apworld" + else: + raise Exception("APWorld appears to be invalid or damaged. (expected a single directory)") + zip.open(module_name + "/__init__.py") except ValueError as e: raise Exception("Archive appears invalid or damaged.") from e except KeyError as e: @@ -122,7 +128,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path # TODO: run generic test suite over the apworld. # TODO: have some kind of version system to tell from metadata if the apworld should be compatible. - target = pathlib.Path(worlds.user_folder) / apworld_path.name + target = pathlib.Path(worlds.user_folder) / apworld_name import shutil shutil.copyfile(apworld_path, target) From c9e63a836a6bdc33f0c0bc2968a6f9dc70159f20 Mon Sep 17 00:00:00 2001 From: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:40:39 -0500 Subject: [PATCH 16/60] CV64: Fix some textbox message truncation issues #4157 --- worlds/cv64/client.py | 17 ++++++++++++++--- worlds/cv64/rom.py | 14 ++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/worlds/cv64/client.py b/worlds/cv64/client.py index 2430cc5ffc67..cec5f551b9e5 100644 --- a/worlds/cv64/client.py +++ b/worlds/cv64/client.py @@ -66,8 +66,9 @@ def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None: self.received_deathlinks += 1 if "cause" in args["data"]: cause = args["data"]["cause"] - if len(cause) > 88: - cause = cause[0x00:0x89] + # Truncate the death cause message at 120 characters. + if len(cause) > 120: + cause = cause[0:120] else: cause = f"{args['data']['source']} killed you!" self.death_causes.append(cause) @@ -146,8 +147,18 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: text_color = bytearray([0xA2, 0x0B]) else: text_color = bytearray([0xA2, 0x02]) + + # Get the item's player's name. If it's longer than 40 characters, truncate it at 40. + # 35 should be the max number of characters in a server player name right now (16 for the original + # name + 16 for the alias + 3 for the added parenthesis and space), but if it ever goes higher it + # should be future-proofed now. No need to truncate CV64 items names because its longest item name + # gets nowhere near the limit. + player_name = ctx.player_names[next_item.player] + if len(player_name) > 40: + player_name = player_name[0:40] + received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_game(next_item.item)}\n" - f"from {ctx.player_names[next_item.player]}", 96) + f"from {player_name}", 96) await bizhawk.guarded_write(ctx.bizhawk_ctx, [(0x389BE1, [next_item.item & 0xFF], "RDRAM"), (0x18C0A8, text_color + cv64_string_to_bytearray(received_text, False), diff --git a/worlds/cv64/rom.py b/worlds/cv64/rom.py index ab4371b0ac12..8f069555343a 100644 --- a/worlds/cv64/rom.py +++ b/worlds/cv64/rom.py @@ -944,13 +944,19 @@ def write_patch(world: "CV64World", patch: CV64ProcedurePatch, offset_data: Dict for loc in active_locations: if loc.address is None or get_location_info(loc.name, "type") == "shop" or loc.item.player == world.player: continue - if len(loc.item.name) > 67: - item_name = loc.item.name[0x00:0x68] + # If the Item's name is longer than 104 characters, truncate the name to inject at 104. + if len(loc.item.name) > 104: + item_name = loc.item.name[0:104] else: item_name = loc.item.name + # Get the item's player's name. If it's longer than 16 characters (which can happen if it's an ItemLinked item), + # truncate it at 16. + player_name = world.multiworld.get_player_name(loc.item.player) + if len(player_name) > 16: + player_name = player_name[0:16] + inject_address = 0xBB7164 + (256 * (loc.address & 0xFFF)) - wrapped_name, num_lines = cv64_text_wrap(item_name + "\nfor " + - world.multiworld.get_player_name(loc.item.player), 96) + wrapped_name, num_lines = cv64_text_wrap(item_name + "\nfor " + player_name, 96) patch.write_token(APTokenTypes.WRITE, inject_address, bytes(get_item_text_color(loc) + cv64_string_to_bytearray(wrapped_name))) patch.write_token(APTokenTypes.WRITE, inject_address + 255, bytes([num_lines])) From 7916d1e67c76537d1aa7da03c99906ef65803b73 Mon Sep 17 00:00:00 2001 From: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:41:57 -0500 Subject: [PATCH 17/60] CV64: Fix DeathLink Nitro explosions hitting you at times they shouldn't #4158 --- worlds/cv64/data/patches.py | 17 +++++++++++++++++ worlds/cv64/rom.py | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/worlds/cv64/data/patches.py b/worlds/cv64/data/patches.py index 938b615b3213..6ef4eafb67d3 100644 --- a/worlds/cv64/data/patches.py +++ b/worlds/cv64/data/patches.py @@ -197,6 +197,23 @@ 0xA168FFFD, # SB T0, 0xFFFD (T3) ] +deathlink_nitro_state_checker = [ + # Checks to see if the player is in an alright state before exploding them. If not, then the Nitro explosion spawn + # code will be aborted, and they should eventually explode after getting out of that state. + # + # Invalid states so far include: interacting/going through a door, being grabbed by a vampire. + 0x90880009, # LBU T0, 0x0009 (A0) + 0x24090005, # ADDIU T1, R0, 0x0005 + 0x11090005, # BEQ T0, T1, [forward 0x05] + 0x24090002, # ADDIU T1, R0, 0x0002 + 0x11090003, # BEQ T0, T1, [forward 0x03] + 0x00000000, # NOP + 0x08000660, # J 0x80001980 + 0x00000000, # NOP + 0x03E00008, # JR RA + 0xAC400048 # SW R0, 0x0048 (V0) +] + launch_fall_killer = [ # Custom code to force the instant fall death if at a high enough falling speed after getting killed by something # that launches you (whether it be the Nitro explosion or a Big Toss hit). The game doesn't normally run the check diff --git a/worlds/cv64/rom.py b/worlds/cv64/rom.py index 8f069555343a..db621c7101d6 100644 --- a/worlds/cv64/rom.py +++ b/worlds/cv64/rom.py @@ -357,8 +357,12 @@ def apply_patches(caller: APProcedurePatch, rom: bytes, options_file: str) -> by # Make received DeathLinks blow you to smithereens instead of kill you normally. if options["death_link"] == DeathLink.option_explosive: - rom_data.write_int32(0x27A70, 0x10000008) # B [forward 0x08] rom_data.write_int32s(0xBFC0D0, patches.deathlink_nitro_edition) + rom_data.write_int32(0x27A70, 0x10000008) # B [forward 0x08] + rom_data.write_int32(0x27AA0, 0x0C0FFA78) # JAL 0x803FE9E0 + rom_data.write_int32s(0xBFE9E0, patches.deathlink_nitro_state_checker) + # NOP the function call to subtract Nitro from the inventory after exploding, just in case. + rom_data.write_int32(0x32DBC, 0x00000000) # Set the DeathLink ROM flag if it's on at all. if options["death_link"] != DeathLink.option_off: From dd659de07942e822b93b1b909181c1cbaea746b3 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 14 Nov 2024 22:43:34 +0000 Subject: [PATCH 18/60] DS3: List compatible static randomizer versions in slot data (#4178) This will make it easier for players to understand when they have a mismatch between their DS3 apworld and their local randomizer version, mitigating a common source of confusion and support requests. --- worlds/dark_souls_3/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 1aec6945eb8b..765ffb1fc544 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1568,6 +1568,16 @@ def fill_slot_data(self) -> Dict[str, object]: "apIdsToItemIds": ap_ids_to_ds3_ids, "itemCounts": item_counts, "locationIdsToKeys": location_ids_to_keys, + # The range of versions of the static randomizer that are compatible + # with this slot data. Incompatible versions should have at least a + # minor version bump. Pre-release versions should generally only be + # compatible with a single version, except very close to a stable + # release when no changes are expected. + # + # This is checked by the static randomizer, which will surface an + # error to the user if its version doesn't fall into the allowed + # range. + "versions": ">=3.0.0-beta.24 <3.1.0", } return slot_data From 6c9b7eca10686ad64cefd0307090b605e73d4844 Mon Sep 17 00:00:00 2001 From: gurglemurgle5 <95941332+gurglemurgle5@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:43:42 -0600 Subject: [PATCH 19/60] Core: Fix Template Yamls for games with colon in name (#4106) * add quotes around strings containing {{ game }} * do the actually correct thing instead of a hack thanks berserker66 for pointing out to me that I was doing this the completly wrong way, so I fixed it up * Clean up filenames to prevent illegal chars * Use %s substitution instead of concatenation * whoops somehow i removed a space from the comment for the regex, so this adds it back * Use pre-existing function in Utils.py * Test: add test for option yaml with colon --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Options.py | 4 +- data/options.yaml | 6 +-- test/options/test_generate_templates.py | 55 +++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 test/options/test_generate_templates.py diff --git a/Options.py b/Options.py index aa6f175fa58d..992348cb546d 100644 --- a/Options.py +++ b/Options.py @@ -15,7 +15,7 @@ from schema import And, Optional, Or, Schema from typing_extensions import Self -from Utils import get_fuzzy_results, is_iterable_except_str, output_path +from Utils import get_file_safe_name, get_fuzzy_results, is_iterable_except_str, output_path if typing.TYPE_CHECKING: from BaseClasses import MultiWorld, PlandoOptions @@ -1531,7 +1531,7 @@ def yaml_dump_scalar(scalar) -> str: del file_data - with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: + with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f: f.write(res) diff --git a/data/options.yaml b/data/options.yaml index ee8866627d52..09bfcdcec1f6 100644 --- a/data/options.yaml +++ b/data/options.yaml @@ -28,9 +28,9 @@ name: Player{number} # Used to describe your yaml. Useful if you have multiple files. -description: Default {{ game }} Template +description: {{ yaml_dump("Default %s Template" % game) }} -game: {{ game }} +game: {{ yaml_dump(game) }} requires: version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected. @@ -44,7 +44,7 @@ requires: {%- endfor -%} {% endmacro %} -{{ game }}: +{{ yaml_dump(game) }}: {%- for group_name, group_options in option_groups.items() %} # {{ group_name }} diff --git a/test/options/test_generate_templates.py b/test/options/test_generate_templates.py new file mode 100644 index 000000000000..cab97c54b129 --- /dev/null +++ b/test/options/test_generate_templates.py @@ -0,0 +1,55 @@ +import unittest + +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING, Dict, Type +from Utils import parse_yaml + +if TYPE_CHECKING: + from worlds.AutoWorld import World + + +class TestGenerateYamlTemplates(unittest.TestCase): + old_world_types: Dict[str, Type["World"]] + + def setUp(self) -> None: + import worlds.AutoWorld + + self.old_world_types = worlds.AutoWorld.AutoWorldRegister.world_types + + def tearDown(self) -> None: + import worlds.AutoWorld + + worlds.AutoWorld.AutoWorldRegister.world_types = self.old_world_types + + if "World: with colon" in worlds.AutoWorld.AutoWorldRegister.world_types: + del worlds.AutoWorld.AutoWorldRegister.world_types["World: with colon"] + + def test_name_with_colon(self) -> None: + from Options import generate_yaml_templates + from worlds.AutoWorld import AutoWorldRegister + from worlds.AutoWorld import World + + class WorldWithColon(World): + game = "World: with colon" + item_name_to_id = {} + location_name_to_id = {} + + AutoWorldRegister.world_types = {WorldWithColon.game: WorldWithColon} + with TemporaryDirectory(f"archipelago_{__name__}") as temp_dir: + generate_yaml_templates(temp_dir) + path: Path + for path in Path(temp_dir).iterdir(): + self.assertTrue(path.is_file()) + self.assertTrue(path.suffix == ".yaml") + with path.open(encoding="utf-8") as f: + try: + data = parse_yaml(f) + except: + f.seek(0) + print(f"Error in {path.name}:\n{f.read()}") + raise + self.assertIn("game", data) + self.assertIn(":", data["game"]) + self.assertIn(data["game"], data) + self.assertIsInstance(data[data["game"]], dict) From 70b9b978413b5161074f24bda9ea4b44f7813e85 Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Thu, 14 Nov 2024 23:50:36 +0100 Subject: [PATCH 20/60] [PKMN RB] Fixes faulty logic in Victory Road 1 #4191 --- worlds/pokemon_rb/rules.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index ba4bfd471c52..3c1cdc57e99b 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -94,6 +94,9 @@ def prize_rule(i): "Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player), + "Victory Road 1F - Top Item": lambda state: logic.can_strength(state, world, player), + "Victory Road 1F - Left Item": lambda state: logic.can_strength(state, world, player), + # # Rock Tunnel "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, world, player), "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player), From 2a850261b82fc2e5713faa5a6f133ef43f5858a8 Mon Sep 17 00:00:00 2001 From: Branden Wood <44546325+BrandenEK@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:57:49 -0500 Subject: [PATCH 21/60] docs: Add @BrandenEK as codeowner (#4177) --- docs/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index ee7fd7ed863b..a51cac37026b 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -143,7 +143,7 @@ /worlds/shivers/ @GodlFire # A Short Hike -/worlds/shorthike/ @chandler05 +/worlds/shorthike/ @chandler05 @BrandenEK # Sonic Adventure 2 Battle /worlds/sa2b/ @PoryGone @RaspberrySpace From a734d25f66cfba6648a3865181a4d8488135aedc Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:57:08 -0500 Subject: [PATCH 22/60] PKMN R/B: Don't change classification of items from other worlds #4192 --- worlds/pokemon_rb/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 98b1a0c614b8..809179cbef74 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -528,8 +528,8 @@ def stage_post_fill(cls, multiworld): for sphere in multiworld.get_spheres(): mon_locations_in_sphere = {} for location in sphere: - if (location.game == "Pokemon Red and Blue" and (location.item.name in poke_data.pokemon_data.keys() - or "Static " in location.item.name) + if (location.game == location.item.game == "Pokemon Red and Blue" + and (location.item.name in poke_data.pokemon_data.keys() or "Static " in location.item.name) and location.item.advancement): key = (location.player, location.item.name) if key in found_mons: From f9c6ecc8b21f6a374f3d8441b6a95fa2f3d22e24 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:31:03 +0100 Subject: [PATCH 23/60] Webhost: fix doc and yaml filenames / install paths (#4193) * WebHost: use new safe yaml template filename this mirrors the change in ArchipelagoMW/#4106 in WebHost * WebHost: install docs into safe filename and require docs to be named safe * Test: update doc test for safe name * WebHost: fix import order to not break ModuleUpdate --- WebHost.py | 3 ++- WebHostLib/__init__.py | 3 ++- WebHostLib/templates/gameInfo.html | 2 +- WebHostLib/templates/playerOptions/playerOptions.html | 2 +- WebHostLib/templates/tutorial.html | 2 +- test/webhost/test_docs.py | 10 ++++++++-- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/WebHost.py b/WebHost.py index e597de24763d..3bf75eb35ae0 100644 --- a/WebHost.py +++ b/WebHost.py @@ -12,6 +12,7 @@ # in case app gets imported by something like gunicorn import Utils import settings +from Utils import get_file_safe_name if typing.TYPE_CHECKING: from flask import Flask @@ -71,7 +72,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] shutil.rmtree(base_target_path, ignore_errors=True) for game, world in worlds.items(): # copy files from world's docs folder to the generated folder - target_path = os.path.join(base_target_path, game) + target_path = os.path.join(base_target_path, get_file_safe_name(game)) os.makedirs(target_path, exist_ok=True) if world.zip_path: diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index fdf3037fe015..dbe2182b0747 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -9,7 +9,7 @@ from pony.flask import Pony from werkzeug.routing import BaseConverter -from Utils import title_sorted +from Utils import title_sorted, get_file_safe_name UPLOAD_FOLDER = os.path.relpath('uploads') LOGS_FOLDER = os.path.relpath('logs') @@ -20,6 +20,7 @@ app.jinja_env.filters['any'] = any app.jinja_env.filters['all'] = all +app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name app.config["SELFHOST"] = True # application process is in charge of running the websites app.config["GENERATORS"] = 8 # maximum concurrent world gens diff --git a/WebHostLib/templates/gameInfo.html b/WebHostLib/templates/gameInfo.html index c5ebba82848d..3b908004b1be 100644 --- a/WebHostLib/templates/gameInfo.html +++ b/WebHostLib/templates/gameInfo.html @@ -11,7 +11,7 @@ {% block body %} {% include 'header/'+theme+'Header.html' %} -
+
{% endblock %} diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html index 73de5d56eb20..7e2f0ee11cb4 100644 --- a/WebHostLib/templates/playerOptions/playerOptions.html +++ b/WebHostLib/templates/playerOptions/playerOptions.html @@ -42,7 +42,7 @@

Player Options

A list of all games you have generated can be found on the User Content Page.
You may also download the - template file for this game. + template file for this game.

diff --git a/WebHostLib/templates/tutorial.html b/WebHostLib/templates/tutorial.html index d3a7e0a05ecc..4b6622c31336 100644 --- a/WebHostLib/templates/tutorial.html +++ b/WebHostLib/templates/tutorial.html @@ -11,7 +11,7 @@ {% endblock %} {% block body %} -
+
{% endblock %} diff --git a/test/webhost/test_docs.py b/test/webhost/test_docs.py index 68aba05f9dcc..1e6c1b88f42c 100644 --- a/test/webhost/test_docs.py +++ b/test/webhost/test_docs.py @@ -30,10 +30,16 @@ def test_has_tutorial(self): def test_has_game_info(self): for game_name, world_type in AutoWorldRegister.world_types.items(): if not world_type.hidden: - target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game_name) + safe_name = Utils.get_file_safe_name(game_name) + target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", safe_name) for game_info_lang in world_type.web.game_info_languages: with self.subTest(game_name): self.assertTrue( - os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{game_name}.md')), + safe_name == game_name or + not os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{game_name}.md')), + f'Info docs have be named _{safe_name}.md for {game_name}.' + ) + self.assertTrue( + os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{safe_name}.md')), f'{game_name} missing game info file for "{game_info_lang}" language.' ) From 9ba613277e8b0be650295f9d7a3225561631ab9f Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 16 Nov 2024 03:00:34 +0100 Subject: [PATCH 24/60] Launcher: change import order to fix ModuleUpdate (#4194) --- Launcher.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Launcher.py b/Launcher.py index 2620f786a54b..ea59e8beb500 100644 --- a/Launcher.py +++ b/Launcher.py @@ -22,16 +22,15 @@ from shutil import which from typing import Callable, Optional, Sequence, Tuple, Union -import Utils -import settings -from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths - if __name__ == "__main__": import ModuleUpdate ModuleUpdate.update() -from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ - is_windows, is_macos, is_linux +import settings +import Utils +from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename, + user_path) +from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type def open_host_yaml(): From 5141f36e9578a918a65a59ee1147eb8c6248cf71 Mon Sep 17 00:00:00 2001 From: "Zach \"Phar\" Parks" Date: Sat, 16 Nov 2024 16:16:09 +0000 Subject: [PATCH 25/60] WebHost: Fix 500 server errors for hints involving ItemLink slots on tracker pages (#4198) * Also makes adjustments to the style for these slots by italicizing its names (including multi-tracker). * Player-specific trackers do not link to ItemLink player trackers (they do not exist). * Fixes a bug on Factorio multi-tracker when item links exist. --- WebHostLib/templates/genericTracker.html | 4 ++++ WebHostLib/templates/multitrackerHintTable.html | 16 ++++++++++++++-- WebHostLib/tracker.py | 6 +++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 947cf2837278..b92097ceea08 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -98,6 +98,8 @@ {% if hint.finding_player == player %} {{ player_names_with_alias[(team, hint.finding_player)] }} + {% elif get_slot_info(team, hint.finding_player).type == 2 %} + {{ player_names_with_alias[(team, hint.finding_player)] }} {% else %} {{ player_names_with_alias[(team, hint.finding_player)] }} @@ -107,6 +109,8 @@ {% if hint.receiving_player == player %} {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% elif get_slot_info(team, hint.receiving_player).type == 2 %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} {% else %} {{ player_names_with_alias[(team, hint.receiving_player)] }} diff --git a/WebHostLib/templates/multitrackerHintTable.html b/WebHostLib/templates/multitrackerHintTable.html index a931e9b04845..fcc15fb37a9f 100644 --- a/WebHostLib/templates/multitrackerHintTable.html +++ b/WebHostLib/templates/multitrackerHintTable.html @@ -21,8 +21,20 @@ ) -%} - {{ player_names_with_alias[(team, hint.finding_player)] }} - {{ player_names_with_alias[(team, hint.receiving_player)] }} + + {% if get_slot_info(team, hint.finding_player).type == 2 %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} + + + {% if get_slot_info(team, hint.receiving_player).type == 2 %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} + {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }} {{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }} {{ games[(team, hint.finding_player)] }} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 5450ef510373..043764a53b08 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -423,6 +423,7 @@ def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> template_name_or_list="genericTracker.html", game_specific_tracker=game in _player_trackers, room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, team=team, player=player, player_name=tracker_data.get_room_long_player_names()[team, player], @@ -446,6 +447,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker enabled_trackers=enabled_trackers, current_tracker="Generic", room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, all_slots=tracker_data.get_all_slots(), room_players=tracker_data.get_all_players(), locations=tracker_data.get_room_locations(), @@ -497,7 +499,7 @@ def render_Factorio_multiworld_tracker(tracker_data: TrackerData, enabled_tracke (team, player): collections.Counter({ tracker_data.item_id_to_name["Factorio"][item_id]: count for item_id, count in tracker_data.get_player_inventory_counts(team, player).items() - }) for team, players in tracker_data.get_all_slots().items() for player in players + }) for team, players in tracker_data.get_all_players().items() for player in players if tracker_data.get_player_game(team, player) == "Factorio" } @@ -506,6 +508,7 @@ def render_Factorio_multiworld_tracker(tracker_data: TrackerData, enabled_tracke enabled_trackers=enabled_trackers, current_tracker="Factorio", room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, all_slots=tracker_data.get_all_slots(), room_players=tracker_data.get_all_players(), locations=tracker_data.get_room_locations(), @@ -638,6 +641,7 @@ def render_ALinkToThePast_multiworld_tracker(tracker_data: TrackerData, enabled_ enabled_trackers=enabled_trackers, current_tracker="A Link to the Past", room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, all_slots=tracker_data.get_all_slots(), room_players=tracker_data.get_all_players(), locations=tracker_data.get_room_locations(), From 66314de965f7fd5f8df566c84a3cf0286ae43ab4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 17 Nov 2024 00:55:18 +0100 Subject: [PATCH 26/60] Subnautica: compose DeathLink custom text instead of overwriting (#4172) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/subnautica/options.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/subnautica/options.py b/worlds/subnautica/options.py index 4bdd9aafa53f..6cdcb33d8954 100644 --- a/worlds/subnautica/options.py +++ b/worlds/subnautica/options.py @@ -112,8 +112,7 @@ def get_pool(self) -> typing.List[str]: 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".""" + __doc__ = DeathLink.__doc__ + "\n\n Note: can be toggled via in-game console command \"deathlink\"." class FillerItemsDistribution(ItemDict): From 73146ef30ca7082b4351236fa5f76a6f092d6e1e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 17 Nov 2024 01:52:49 +0100 Subject: [PATCH 27/60] Tests: Use Option.from_any instead of Option() in test_pickle_dumps, which is currently preventing Range options from using default: "random" #4197 --- test/general/test_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/general/test_options.py b/test/general/test_options.py index d6d5ce6da06b..7a3743e5a4e7 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -78,4 +78,4 @@ def test_pickle_dumps(self): if not world_type.hidden: for option_key, option in world_type.options_dataclass.type_hints.items(): with self.subTest(game=gamename, option=option_key): - pickle.dumps(option(option.default)) + pickle.dumps(option.from_any(option.default)) From 7eb12174b74ba566e7934b53686bc68a44861935 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Sun, 17 Nov 2024 15:55:42 +0000 Subject: [PATCH 28/60] Core: Fix empty rule comparisons with subclasses (#4201) If a world uses a `Location` or `Entrance` subclass that overrides the `item_rule`/`access_rule` class attribute, then `spot.__class__.item_rule`/`spot.__class__.access_rule` will get the overridden rule, which may not be an empty rule. Uses of `spot.__class__` have been replaced with getting the class attribute rule belonging to the `Location` or `Entrance` class. --- worlds/generic/Rules.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index e930c4b8d6e9..31d725bff722 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -69,7 +69,7 @@ def forbid(sender: int, receiver: int, items: typing.Set[str]): if (location.player, location.item_rule) in func_cache: location.item_rule = func_cache[location.player, location.item_rule] # empty rule that just returns True, overwrite - elif location.item_rule is location.__class__.item_rule: + elif location.item_rule is Location.item_rule: func_cache[location.player, location.item_rule] = location.item_rule = \ lambda i, sending_blockers = forbid_data[location.player], \ old_rule = location.item_rule: \ @@ -103,7 +103,7 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"): old_rule = spot.access_rule # empty rule, replace instead of add - if old_rule is spot.__class__.access_rule: + if old_rule is Location.access_rule or old_rule is Entrance.access_rule: spot.access_rule = rule if combine == "and" else old_rule else: if combine == "and": @@ -115,7 +115,7 @@ def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], def forbid_item(location: "BaseClasses.Location", item: str, player: int): old_rule = location.item_rule # empty rule - if old_rule is location.__class__.item_rule: + if old_rule is Location.item_rule: location.item_rule = lambda i: i.name != item or i.player != player else: location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) @@ -135,7 +135,7 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"): old_rule = location.item_rule # empty rule, replace instead of add - if old_rule is location.__class__.item_rule: + if old_rule is Location.item_rule: location.item_rule = rule if combine == "and" else old_rule else: if combine == "and": From a3d6036939bf0aa606808b5a51f2df8f303f2039 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 17 Nov 2024 07:58:14 -0800 Subject: [PATCH 29/60] Factorio: energy link bridge improvements (#4182) * improve energy link performance on large surfaces * Add Energy link bridge storage table to initialization. * Fix event based energy link for Factorio 2.0 * Adjust energy link bridge for quality. --- worlds/factorio/data/mod_template/control.lua | 91 +++++++++++++++---- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 4383357546d9..b08608a60ae9 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -105,8 +105,8 @@ function on_player_changed_position(event) end local target_direction = exit_table[outbound_direction] - local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, - (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} + local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, + (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} target_position = character.surface.find_non_colliding_position(character.prototype.name, target_position, 32, 0.5) if target_position ~= nil then @@ -134,40 +134,96 @@ end script.on_event(defines.events.on_player_changed_position, on_player_changed_position) {% endif %} - +function count_energy_bridges() + local count = 0 + for i, bridge in pairs(storage.energy_link_bridges) do + if validate_energy_link_bridge(i, bridge) then + count = count + 1 + (bridge.quality.level * 0.3) + end + end + return count +end +function get_energy_increment(bridge) + return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level) +end function on_check_energy_link(event) --- assuming 1 MJ increment and 5MJ battery: --- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing if event.tick % 60 == 30 then - local surface = game.get_surface(1) local force = "player" - local bridges = surface.find_entities_filtered({name="ap-energy-bridge", force=force}) - local bridgecount = table_size(bridges) + local bridges = storage.energy_link_bridges + local bridgecount = count_energy_bridges() storage.forcedata[force].energy_bridges = bridgecount if storage.forcedata[force].energy == nil then storage.forcedata[force].energy = 0 end if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then - for i, bridge in ipairs(bridges) do - if bridge.energy > ENERGY_INCREMENT*3 then - storage.forcedata[force].energy = storage.forcedata[force].energy + (ENERGY_INCREMENT * ENERGY_LINK_EFFICIENCY) - bridge.energy = bridge.energy - ENERGY_INCREMENT + for i, bridge in pairs(bridges) do + if validate_energy_link_bridge(i, bridge) then + energy_increment = get_energy_increment(bridge) + if bridge.energy > energy_increment*3 then + storage.forcedata[force].energy = storage.forcedata[force].energy + (energy_increment * ENERGY_LINK_EFFICIENCY) + bridge.energy = bridge.energy - energy_increment + end end end end - for i, bridge in ipairs(bridges) do - if storage.forcedata[force].energy < ENERGY_INCREMENT then - break - end - if bridge.energy < ENERGY_INCREMENT*2 and storage.forcedata[force].energy > ENERGY_INCREMENT then - storage.forcedata[force].energy = storage.forcedata[force].energy - ENERGY_INCREMENT - bridge.energy = bridge.energy + ENERGY_INCREMENT + for i, bridge in pairs(bridges) do + if validate_energy_link_bridge(i, bridge) then + energy_increment = get_energy_increment(bridge) + if storage.forcedata[force].energy < energy_increment and bridge.quality.level == 0 then + break + end + if bridge.energy < energy_increment*2 and storage.forcedata[force].energy > energy_increment then + storage.forcedata[force].energy = storage.forcedata[force].energy - energy_increment + bridge.energy = bridge.energy + energy_increment + end end end end end +function string_starts_with(str, start) + return str:sub(1, #start) == start +end +function validate_energy_link_bridge(unit_number, entity) + if not entity then + if storage.energy_link_bridges[unit_number] == nil then return false end + storage.energy_link_bridges[unit_number] = nil + return false + end + if not entity.valid then + if storage.energy_link_bridges[unit_number] == nil then return false end + storage.energy_link_bridges[unit_number] = nil + return false + end + return true +end +function on_energy_bridge_constructed(entity) + if entity and entity.valid then + if string_starts_with(entity.prototype.name, "ap-energy-bridge") then + storage.energy_link_bridges[entity.unit_number] = entity + end + end +end +function on_energy_bridge_removed(entity) + if string_starts_with(entity.prototype.name, "ap-energy-bridge") then + if storage.energy_link_bridges[entity.unit_number] == nil then return end + storage.energy_link_bridges[entity.unit_number] = nil + end +end if (ENERGY_INCREMENT) then script.on_event(defines.events.on_tick, on_check_energy_link) + + script.on_event({defines.events.on_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.on_robot_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.on_entity_cloned}, function(event) on_energy_bridge_constructed(event.destination) end) + + script.on_event({defines.events.script_raised_revive}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.script_raised_built}, function(event) on_energy_bridge_constructed(event.entity) end) + + script.on_event({defines.events.on_entity_died}, function(event) on_energy_bridge_removed(event.entity) end) + script.on_event({defines.events.on_player_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end) + script.on_event({defines.events.on_robot_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end) end {% if not imported_blueprints -%} @@ -410,6 +466,7 @@ script.on_init(function() {% if not imported_blueprints %}set_permissions(){% endif %} storage.forcedata = {} storage.playerdata = {} + storage.energy_link_bridges = {} -- Fire dummy events for all currently existing forces. local e = {} for name, _ in pairs(game.forces) do From 75e18e3cc98f1d95eb92568000a427d56c3eaf7b Mon Sep 17 00:00:00 2001 From: Louis M Date: Sun, 17 Nov 2024 10:59:50 -0500 Subject: [PATCH 30/60] Aquaria: Fixing no progression bug (#4199) --- worlds/aquaria/Regions.py | 50 +++++++++---------- ...st_no_progression_hard_hidden_locations.py | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 792d7b73dfdb..7a41e0d0c864 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -1152,79 +1152,79 @@ def __adjusting_manual_rules(self) -> None: def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Mithalas boss area, beating Mithalan God", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Temple boss area, beating Sun God", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sunken City, bulb on top of the boss area", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Home Water, Nautilus Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Mithalas City Castle, beating the Priests", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Mermog cave, Piranha Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Octopus Cave, Dumbo Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Final Boss area, bulb in the boss third form room", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Bubble Cave, Verse Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Temple, Sun Key", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("The Body bottom area, Mutant Costume", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement def adjusting_rules(self, options: AquariaOptions) -> None: """ diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index f015b26de10b..517af3028dd2 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -49,7 +49,7 @@ def test_unconfine_home_water_both_location_fillable(self) -> None: for location in self.unfillable_locations: for item_name in self.world.item_names: item = self.get_item_by_name(item_name) - if item.classification == ItemClassification.progression: + if item.advancement: self.assertFalse( self.world.get_location(location).can_fill(self.multiworld.state, item, False), "The location \"" + location + "\" can be filled with \"" + item_name + "\"") From 9c102da901c33615b085a901bbf3102770befb87 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 18 Nov 2024 02:16:14 +0100 Subject: [PATCH 31/60] The Witness: Allow setting the puzzle randomization seed yourself (#4196) * Allow setting the puzzle randomization seed yourself * longer tooltip * Oh * Also actually have the correct values that the client will accept (lol, thanks Medic) * Update worlds/witness/options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/witness/__init__.py | 2 +- worlds/witness/options.py | 12 ++++++++++++ worlds/witness/player_items.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index a21a5bb3ca7e..ac9197bd92bb 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -80,7 +80,7 @@ class WitnessWorld(World): def _get_slot_data(self) -> Dict[str, Any]: return { - "seed": self.random.randrange(0, 1000000), + "seed": self.options.puzzle_randomization_seed.value, "victory_location": int(self.player_logic.VICTORY_LOCATION, 16), "panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID, "item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(), diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 4de966abe96d..e1462cc37508 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -401,6 +401,16 @@ class DeathLinkAmnesty(Range): default = 1 +class PuzzleRandomizationSeed(Range): + """ + Sigma Rando, which is the basis for all puzzle randomization in this randomizer, uses a seed from 1 to 9999999 for the puzzle randomization. + This option lets you set this seed yourself. + """ + range_start = 1 + range_end = 9999999 + default = "random" + + @dataclass class TheWitnessOptions(PerGameCommonOptions): puzzle_randomization: PuzzleRandomization @@ -435,6 +445,7 @@ class TheWitnessOptions(PerGameCommonOptions): laser_hints: LaserHints death_link: DeathLink death_link_amnesty: DeathLinkAmnesty + puzzle_randomization_seed: PuzzleRandomizationSeed shuffle_dog: ShuffleDog @@ -483,6 +494,7 @@ class TheWitnessOptions(PerGameCommonOptions): ElevatorsComeToYou, DeathLink, DeathLinkAmnesty, + PuzzleRandomizationSeed, ]), OptionGroup("Silly Options", [ ShuffleDog, diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 4c98cb78495e..3be298ebccae 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -2,7 +2,7 @@ Defines progression, junk and event items for The Witness """ import copy -from typing import TYPE_CHECKING, Dict, List, Set, cast +from typing import TYPE_CHECKING, Dict, List, Set from BaseClasses import Item, ItemClassification, MultiWorld From baf291d7a277bef4fd06ee5812fd7952b98bc5eb Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Sun, 17 Nov 2024 20:19:26 -0500 Subject: [PATCH 32/60] TLOZ: Assorted Logic Fixes (#4203) * TLOZ: Assorded Logic Fixes - Add needing arrows for Pols Voice rule. Not super necessary at the moment since wooden arrows are always accessible in one of the opening shops, but future proofing for future plans - Create Gohma Locations and make sure all Gohma blocked locations have the required rule (was missing at least one location before) - Remove the rule requiring Bow for all locations of level 8 (not sure why that was there, it's theoretically redundant now that Gohma and Pols Voice are properly marked) - Make sure Digdogger locations properly require Recorder, and clean up redundant Level 7 rules as level 7 currently requires Recorder to access the entrance * Update worlds/tloz/Rules.py forgor that has_any exists Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Remove world = multiworld --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/tloz/Locations.py | 6 ++- worlds/tloz/Rules.py | 102 ++++++++++++++++++--------------------- 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py index 9715cc684291..f95e5d80443e 100644 --- a/worlds/tloz/Locations.py +++ b/worlds/tloz/Locations.py @@ -108,11 +108,15 @@ ] food_locations = [ - "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", + "Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", "Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)" ] +gohma_locations = [ + "Level 6 Boss", "Level 6 Triforce", "Level 8 Item (Magical Key)", "Level 8 Bomb Drop (Darknuts North)" +] + gleeok_locations = [ "Level 4 Boss", "Level 4 Triforce", "Level 8 Boss", "Level 8 Triforce" ] diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index 39c3b954f0d4..de627a533bd3 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from worlds.generic.Rules import add_rule -from .Locations import food_locations, shop_locations, gleeok_locations +from .Locations import food_locations, shop_locations, gleeok_locations, gohma_locations from .ItemPool import dangerous_weapon_locations from .Options import StartingPosition @@ -10,13 +10,12 @@ def set_rules(tloz_world: "TLoZWorld"): player = tloz_world.player - world = tloz_world.multiworld options = tloz_world.options # Boss events for a nicer spoiler log play through for level in range(1, 9): - boss = world.get_location(f"Level {level} Boss", player) - boss_event = world.get_location(f"Level {level} Boss Status", player) + boss = tloz_world.get_location(f"Level {level} Boss") + boss_event = tloz_world.get_location(f"Level {level} Boss Status") status = tloz_world.create_event(f"Boss {level} Defeated") boss_event.place_locked_item(status) add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player)) @@ -26,136 +25,131 @@ def set_rules(tloz_world: "TLoZWorld"): for location in level.locations: if options.StartingPosition < StartingPosition.option_dangerous \ or location.name not in dangerous_weapon_locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("weapons", player)) # This part of the loop sets up an expected amount of defense needed for each dungeon if i > 0: # Don't need an extra heart for Level 1 - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state, hearts=i: state.has("Heart Container", player, hearts) or (state.has("Blue Ring", player) and state.has("Heart Container", player, int(hearts / 2))) or (state.has("Red Ring", player) and state.has("Heart Container", player, int(hearts / 4)))) if "Pols Voice" in location.name: # This enemy needs specific weapons - add_rule(world.get_location(location.name, player), - lambda state: state.has_group("swords", player) or state.has("Bow", player)) + add_rule(tloz_world.get_location(location.name), + lambda state: state.has_group("swords", player) or + (state.has("Bow", player) and state.has_group("arrows", player))) # No requiring anything in a shop until we can farm for money for location in shop_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has_group("weapons", player)) # Everything from 4 on up has dark rooms for level in tloz_world.levels[4:]: for location in level.locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("candles", player) or (state.has("Magical Rod", player) and state.has("Book of Magic", player))) # Everything from 5 on up has gaps for level in tloz_world.levels[5:]: for location in level.locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Stepladder", player)) - add_rule(world.get_location("Level 5 Boss", player), - lambda state: state.has("Recorder", player)) - - add_rule(world.get_location("Level 6 Boss", player), - lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + # Level 4 Access + for location in tloz_world.levels[4].locations: + add_rule(tloz_world.get_location(location.name), + lambda state: state.has_any(("Raft", "Recorder"), player)) - add_rule(world.get_location("Level 7 Item (Red Candle)", player), + # Digdogger boss. Rework this once ER happens + add_rule(tloz_world.get_location("Level 5 Boss"), lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Boss", player), + add_rule(tloz_world.get_location("Level 5 Triforce"), lambda state: state.has("Recorder", player)) - if options.ExpandedPool: - add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player), - lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player), - lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player), + + for location in gohma_locations: + if options.ExpandedPool or "Drop" not in location: + add_rule(tloz_world.get_location(location), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + + # Recorder Access for Level 7 + for location in tloz_world.levels[7].locations: + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Recorder", player)) for location in food_locations: if options.ExpandedPool or "Drop" not in location: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Food", player)) for location in gleeok_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has_group("swords", player) or state.has("Magical Rod", player)) # Candle access for Level 8 for location in tloz_world.levels[8].locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("candles", player)) - add_rule(world.get_location("Level 8 Item (Magical Key)", player), + add_rule(tloz_world.get_location("Level 8 Item (Magical Key)"), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) if options.ExpandedPool: - add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), + add_rule(tloz_world.get_location("Level 8 Bomb Drop (Darknuts North)"), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) for location in tloz_world.levels[9].locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Triforce Fragment", player, 8) and state.has_group("swords", player)) # Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop for level in range(1, 9): - add_rule(world.get_location(f"Level {level} Triforce", player), + add_rule(tloz_world.get_location(f"Level {level} Triforce"), lambda state, l=level: state.has(f"Boss {l} Defeated", player)) # Sword, raft, and ladder spots - add_rule(world.get_location("White Sword Pond", player), + add_rule(tloz_world.get_location("White Sword Pond"), lambda state: state.has("Heart Container", player, 2)) - add_rule(world.get_location("Magical Sword Grave", player), + add_rule(tloz_world.get_location("Magical Sword Grave"), lambda state: state.has("Heart Container", player, 9)) stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"] stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"] for location in stepladder_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Stepladder", player)) if options.ExpandedPool: for location in stepladder_locations_expanded: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Stepladder", player)) # Don't allow Take Any Items until we can actually get in one if options.ExpandedPool: - add_rule(world.get_location("Take Any Item Left", player), + add_rule(tloz_world.get_location("Take Any Item Left"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Middle", player), + add_rule(tloz_world.get_location("Take Any Item Middle"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Right", player), + add_rule(tloz_world.get_location("Take Any Item Right"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - for location in tloz_world.levels[4].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Raft", player) or state.has("Recorder", player)) - for location in tloz_world.levels[7].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Recorder", player)) - for location in tloz_world.levels[8].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Bow", player)) - add_rule(world.get_location("Potion Shop Item Left", player), + add_rule(tloz_world.get_location("Potion Shop Item Left"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Potion Shop Item Middle", player), + add_rule(tloz_world.get_location("Potion Shop Item Middle"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Potion Shop Item Right", player), + add_rule(tloz_world.get_location("Potion Shop Item Right"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Shield Shop Item Left", player), + add_rule(tloz_world.get_location("Shield Shop Item Left"), lambda state: state.has_group("candles", player) or state.has("Bomb", player)) - add_rule(world.get_location("Shield Shop Item Middle", player), + add_rule(tloz_world.get_location("Shield Shop Item Middle"), lambda state: state.has_group("candles", player) or state.has("Bomb", player)) - add_rule(world.get_location("Shield Shop Item Right", player), + add_rule(tloz_world.get_location("Shield Shop Item Right"), lambda state: state.has_group("candles", player) or - state.has("Bomb", player)) \ No newline at end of file + state.has("Bomb", player)) From bd5c8ec172b51397c8aac4f7f701d47e7ff8bbb2 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 17 Nov 2024 19:22:25 -0600 Subject: [PATCH 33/60] MM2: minor bugfixes (#4190) * move special cases to be outside strict * Update text.py * fix wily machine edge case, incorrect weapons, and time stopper failsafe * bump world version * weakness checking is inclusive * Update __init__.py * add air shooter to edge case validation --- worlds/mm2/__init__.py | 6 ++--- worlds/mm2/rules.py | 53 ++++++++++++++++++++++-------------------- worlds/mm2/text.py | 2 +- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/worlds/mm2/__init__.py b/worlds/mm2/__init__.py index 07e1823f9387..4a43ee8df0f0 100644 --- a/worlds/mm2/__init__.py +++ b/worlds/mm2/__init__.py @@ -96,13 +96,13 @@ class MM2World(World): location_name_groups = location_groups web = MM2WebWorld() rom_name: bytearray - world_version: Tuple[int, int, int] = (0, 3, 1) + world_version: Tuple[int, int, int] = (0, 3, 2) wily_5_weapons: Dict[int, List[int]] - def __init__(self, world: MultiWorld, player: int): + def __init__(self, multiworld: MultiWorld, player: int): self.rom_name = bytearray() self.rom_name_available_event = threading.Event() - super().__init__(world, player) + super().__init__(multiworld, player) self.weapon_damage = deepcopy(weapon_damage) self.wily_5_weapons = {} diff --git a/worlds/mm2/rules.py b/worlds/mm2/rules.py index eddd09927445..7e2ce1f3c752 100644 --- a/worlds/mm2/rules.py +++ b/worlds/mm2/rules.py @@ -133,28 +133,6 @@ def set_rules(world: "MM2World") -> None: # Wily Machine needs all three weaknesses present, so allow elif 4 > world.weapon_damage[weapon][i] > 0: world.weapon_damage[weapon][i] = 0 - # handle special cases - for boss in range(14): - for weapon in (1, 3, 6, 8): - if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and - not any(world.weapon_damage[i][boss] > 0 for i in range(1, 8) if i != weapon)): - # Weapon does not have enough possible ammo to kill the boss, raise the damage - if boss == 9: - if weapon != 3: - # Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness - world.weapon_damage[weapon][boss] = 0 - weakness = world.random.choice((2, 3, 4, 5, 7, 8)) - world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] - elif boss == 11: - if weapon == 1: - # Atomic Fire cannot be Boobeam Trap's only weakness - world.weapon_damage[weapon][boss] = 0 - weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8)) - world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] - else: - world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] - starting = world.options.starting_robot_master.value - world.weapon_damage[0][starting] = 1 for p_boss in world.options.plando_weakness: for p_weapon in world.options.plando_weakness[p_boss]: @@ -168,6 +146,28 @@ def set_rules(world: "MM2World") -> None: world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \ = world.options.plando_weakness[p_boss][p_weapon] + # handle special cases + for boss in range(14): + for weapon in (1, 2, 3, 6, 8): + if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and + not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon] + for i in range(9) if i != weapon)): + # Weapon does not have enough possible ammo to kill the boss, raise the damage + if boss == 9: + if weapon in (1, 6): + # Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness + world.weapon_damage[weapon][boss] = 0 + weakness = world.random.choice((2, 3, 4, 5, 7, 8)) + world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] + elif boss == 11: + if weapon == 1: + # Atomic Fire cannot be Boobeam Trap's only weakness + world.weapon_damage[weapon][boss] = 0 + weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8)) + world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] + else: + world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] + if world.weapon_damage[0][world.options.starting_robot_master.value] < 1: world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value] @@ -209,11 +209,11 @@ def set_rules(world: "MM2World") -> None: continue highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys())) uses = weapon_energy[wp] // weapon_costs[wp] - used_weapons[boss].add(wp) if int(uses * boss_damage[wp]) > boss_health[boss]: used = ceil(boss_health[boss] / boss_damage[wp]) weapon_energy[wp] -= weapon_costs[wp] * used boss_health[boss] = 0 + used_weapons[boss].add(wp) elif highest <= 0: # we are out of weapons that can actually damage the boss # so find the weapon that has the most uses, and apply that as an additional weakness @@ -221,18 +221,21 @@ def set_rules(world: "MM2World") -> None: # Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should # be able to cover wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight - if weapon != 0) + if weapon != 0 and (weapon != 8 or boss != 12)) + # Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp] used = min(int(weapon_energy[wp] // weapon_costs[wp]), - ceil(boss_health[boss] // minimum_weakness_requirement[wp])) + ceil(boss_health[boss] / minimum_weakness_requirement[wp])) weapon_energy[wp] -= weapon_costs[wp] * used boss_health[boss] -= int(used * minimum_weakness_requirement[wp]) weapon_weight.pop(wp) + used_weapons[boss].add(wp) else: # drain the weapon and continue boss_health[boss] -= int(uses * boss_damage[wp]) weapon_energy[wp] -= weapon_costs[wp] * uses weapon_weight.pop(wp) + used_weapons[boss].add(wp) world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons} diff --git a/worlds/mm2/text.py b/worlds/mm2/text.py index 32d665bf6c7f..7dda12ac0346 100644 --- a/worlds/mm2/text.py +++ b/worlds/mm2/text.py @@ -1,7 +1,7 @@ from typing import DefaultDict from collections import defaultdict -MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda x: 0x6F, { +MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x6F, { ' ': 0x40, 'A': 0x41, 'B': 0x42, From 4b80b786e23b118999a6746fa0e22f7c53e89b4f Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 18 Nov 2024 02:45:04 -0500 Subject: [PATCH 34/60] Stardew Valley: Removed Walnutsanity and Filler buffs from the all random preset (#4206) --- worlds/stardew_valley/presets.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index 1861a914235c..62672f29e424 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -41,9 +41,7 @@ Friendsanity.internal_name: "random", FriendsanityHeartSize.internal_name: "random", Booksanity.internal_name: "random", - Walnutsanity.internal_name: "random", NumberOfMovementBuffs.internal_name: "random", - EnabledFillerBuffs.internal_name: "random", ExcludeGingerIsland.internal_name: "random", TrapItems.internal_name: "random", MultipleDaySleepEnabled.internal_name: "random", From 01c603756254d4c16383698bc00ac9227d1c480d Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 18 Nov 2024 08:39:58 -0500 Subject: [PATCH 35/60] TUNIC: Fix a few missing tricks in logic (#4132) * Add missing connection to the furnace entry by west garden * Add missing connection to the furnace entry by west garden * Add missing hard ls for ruined passage door * Allow shield for LS * Split PR into two * Split PR into two * Split PR into two * Add dark tomb ice grapple through the wall --- worlds/tunic/er_data.py | 2 +- worlds/tunic/er_rules.py | 11 +++++++---- worlds/tunic/ladder_storage_data.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 343bf3055378..1269f3b85e45 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -807,7 +807,7 @@ class DeadEnd(IntEnum): [], # drop a rudeling, icebolt or ice bomb "Overworld to West Garden from Furnace": - [["IG3"]], + [["IG3"], ["LS1"]], }, "East Overworld": { "Above Ruined Passage": diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 6f5eec020be6..3b111ad83488 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -501,9 +501,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Entry Point"]) + # ice grapple through the wall, get the little secret sound to trigger regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Main"], - rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Upper"], rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) @@ -779,12 +781,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Fortress East Shortcut Upper"].connect( connecting_region=regions["Fortress East Shortcut Lower"]) - # nmg: can ice grapple upwards regions["Fortress East Shortcut Lower"].connect( connecting_region=regions["Fortress East Shortcut Upper"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - # nmg: ice grapple through the big gold door, can do it both ways regions["Eastern Vault Fortress"].connect( connecting_region=regions["Eastern Vault Fortress Gold Door"], rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", @@ -807,7 +807,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Fortress Hero's Grave Region"].connect( connecting_region=regions["Fortress Grave Path"]) - # nmg: ice grapple from upper grave path to lower regions["Fortress Grave Path Upper"].connect( connecting_region=regions["Fortress Grave Path"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) @@ -1139,6 +1138,9 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None: for portal_dest in region_info.portals: ls_connect(ladder_region, "Overworld Redux, " + portal_dest) + # convenient staircase means this one is easy difficulty, even though there's an elevation change + ls_connect("LS Elev 0", "Overworld Redux, Furnace_gyro_west") + # connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail if options.ladder_storage >= LadderStorage.option_medium: for ladder_region, region_info in ow_ladder_groups.items(): @@ -1154,6 +1156,7 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None: if options.ladder_storage >= LadderStorage.option_hard: ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_") ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_") + ls_connect("LS Elev 2", "Overworld Redux, Ruins Passage_west") ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house") ls_connect("LS Elev 5", "Overworld Redux, Temple_main") diff --git a/worlds/tunic/ladder_storage_data.py b/worlds/tunic/ladder_storage_data.py index a29d50b4f455..c6dda42bca79 100644 --- a/worlds/tunic/ladder_storage_data.py +++ b/worlds/tunic/ladder_storage_data.py @@ -17,7 +17,7 @@ class OWLadderInfo(NamedTuple): ["Overworld Beach"]), # also the east filigree room "LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"}, - ["Furnace_gyro_lower", "Swamp Redux 2_wall"], + ["Furnace_gyro_lower", "Furnace_gyro_west", "Swamp Redux 2_wall"], ["Overworld Tunnel Turret"]), # also the fountain filigree room and ruined passage door "LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"}, From 3ae8992fb61d24104faa502cff275ba6a771cdbe Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:59:17 +0100 Subject: [PATCH 36/60] Clients: fix high CPU usage when launched via MultiProcessing (#4209) * Core: make Utils.stream_input not consume all CPU for non-blocking streams * Clients: ignore MultiProcessing pipe as input console --- CommonClient.py | 5 +++++ Utils.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index 77ed85b5c652..47100a7383ab 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -710,6 +710,11 @@ def run_gui(self): def run_cli(self): if sys.stdin: + if sys.stdin.fileno() != 0: + from multiprocessing import parent_process + if parent_process(): + return # ignore MultiProcessing pipe + # steam overlay breaks when starting console_loop if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''): logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.") diff --git a/Utils.py b/Utils.py index 412011200f8a..2dfcd9d3e19a 100644 --- a/Utils.py +++ b/Utils.py @@ -18,6 +18,7 @@ from argparse import Namespace from settings import Settings, get_settings +from time import sleep from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union from typing_extensions import TypeGuard from yaml import load, load_all, dump @@ -568,6 +569,8 @@ def queuer(): else: if text: queue.put_nowait(text) + else: + sleep(0.01) # non-blocking stream from threading import Thread thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True) From 85a713771b98aca84c900f809b0ccf41d1002d6d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Mon, 18 Nov 2024 11:09:27 -0600 Subject: [PATCH 37/60] Tests: have option preset validation test do full validation (#4208) * Tests: have option preset validation test do full validation * sum on an IntFlag is a thing apparently --- test/webhost/test_option_presets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py index b0af8a871183..7105c7f80593 100644 --- a/test/webhost/test_option_presets.py +++ b/test/webhost/test_option_presets.py @@ -1,5 +1,6 @@ import unittest +from BaseClasses import PlandoOptions from worlds import AutoWorldRegister from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet @@ -14,6 +15,10 @@ def test_option_presets_have_valid_options(self): with self.subTest(game=game_name, preset=preset_name, option=option_name): try: option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) + # some options may need verification to ensure the provided option is actually valid + # pass in all plando options in case a preset wants to require certain plando options + # for some reason + option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions))) supported_types = [NumericOption, OptionSet, OptionList, ItemDict] if not any([issubclass(option.__class__, t) for t in supported_types]): self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " From 48ea274655223ea50bffaa21d395a8b5fef543cd Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 19 Nov 2024 15:16:10 -0500 Subject: [PATCH 38/60] MultiServer: persist hints even if previously found (#4214) * change to persist all hints to ctx.hints regardless of found status * remove if not found entirely as it seems like it was added to not double charge hint points https://github.com/ArchipelagoMW/Archipelago/commit/9842399d8be082b757a140137706c12d84cda8c1 --- MultiServer.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 764b56362ecc..847a0b281c40 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -727,15 +727,15 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b if not hint.local and data not in concerns[hint.finding_player]: concerns[hint.finding_player].append(data) # remember hints in all cases - if not hint.found: - # since hints are bidirectional, finding player and receiving player, - # we can check once if hint already exists - if hint not in self.hints[team, hint.finding_player]: - self.hints[team, hint.finding_player].add(hint) - new_hint_events.add(hint.finding_player) - for player in self.slot_set(hint.receiving_player): - self.hints[team, player].add(hint) - new_hint_events.add(player) + + # since hints are bidirectional, finding player and receiving player, + # we can check once if hint already exists + if hint not in self.hints[team, hint.finding_player]: + self.hints[team, hint.finding_player].add(hint) + new_hint_events.add(hint.finding_player) + for player in self.slot_set(hint.receiving_player): + self.hints[team, player].add(hint) + new_hint_events.add(player) self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint))) for slot in new_hint_events: From 124ce13da75747be31da09358208d851ad8da3fb Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Wed, 20 Nov 2024 00:45:41 -0800 Subject: [PATCH 39/60] Core: improve error message for missing "game" entry in yaml (#4185) --- Generate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Generate.py b/Generate.py index bc359a203da7..8aba72abafe9 100644 --- a/Generate.py +++ b/Generate.py @@ -453,6 +453,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") ret.game = get_choice("game", weights) + if not isinstance(ret.game, str): + if ret.game is None: + raise Exception('"game" not specified') + raise Exception(f"Invalid game: {ret.game}") if ret.game not in AutoWorldRegister.world_types: from worlds import failed_world_loads picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] From 859ae87ec911c26c439767a65be0efe098f08357 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 21 Nov 2024 11:43:01 -0500 Subject: [PATCH 40/60] Launcher: ports the _stop fix in the Launcher kivy App to handle_url Popup App (#4213) * fixes url launched popup so it can close cleanly after spawning another kivy app like text client * whoops --- Launcher.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Launcher.py b/Launcher.py index ea59e8beb500..f04d67a5aa0d 100644 --- a/Launcher.py +++ b/Launcher.py @@ -181,6 +181,11 @@ def update_label(self, dt): App.get_running_app().stop() Window.close() + def _stop(self, *largs): + # see run_gui Launcher _stop comment for details + self.root_window.close() + super()._stop(*largs) + Popup().run() From d4b1351c99f44f4d0444e5d8055572317f52218b Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Thu, 21 Nov 2024 19:43:37 +0000 Subject: [PATCH 41/60] Aquaria: Remove BaseException handling from create_item (#4218) * Aquaria: Remove BaseException handling from create_item Catching `BaseException` without re-raising the exception should almost never be done because `BaseException` includes exit exceptions, such as `SystemExit` and `KeyboardInterrupt`. Ideally, the caught exception types should be as narrow as possible to not mask bugs from catching unexpected exceptions. Having narrow exception types can also help indicate to other developers what exceptions are expected to be raisable by the code within the `try` clause. Similarly, the `try` clause should ideally contain the minimum code necessary, to avoid masking bugs in the case that code within the `try` clause that is not expected to raise an exception does so. In this case, the only expected exception that can occur appears to be `item_table[name]` that can raise a `KeyError` when `create_item()` is passed an unexpected `name` argument. So this patch moves the other code out of the `try` clause and changes the caught exception types to only `KeyError`. * Remove try-except The KeyError that would be raised will be propagated as-is rather than raising a new exception in its place. * Remove extra newline The original code did not have this newline, so it has been removed. --- worlds/aquaria/__init__.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index f79978f25fc4..f620bf6d7306 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -117,16 +117,13 @@ def create_item(self, name: str) -> AquariaItem: Create an AquariaItem using 'name' as item name. """ result: AquariaItem - try: - data = item_table[name] - classification: ItemClassification = ItemClassification.useful - if data.type == ItemType.JUNK: - classification = ItemClassification.filler - elif data.type == ItemType.PROGRESSION: - classification = ItemClassification.progression - result = AquariaItem(name, classification, data.id, self.player) - except BaseException: - raise Exception('The item ' + name + ' is not valid.') + data = item_table[name] + classification: ItemClassification = ItemClassification.useful + if data.type == ItemType.JUNK: + classification = ItemClassification.filler + elif data.type == ItemType.PROGRESSION: + classification = ItemClassification.progression + result = AquariaItem(name, classification, data.id, self.player) return result From 2424b79626ee4e227ce3579c6c2d4a8cee7dca86 Mon Sep 17 00:00:00 2001 From: digiholic Date: Fri, 22 Nov 2024 08:33:27 -0700 Subject: [PATCH 42/60] OSRS: Fixes to Logic errors related to Max Skill Level determining when Regions are accessible (#4188) * Removes explicit indirect conditions * Changes special rules function add rule instead of setting, and call it unconditionally * Fixes issues in rule generation that have been around but unused the whole time * Finally moves rules out into a separate file. Fixes level-related logic * Removes redundant max skill level checks on canoes, since they're in the skill training rules now * For some reason, canoe logic assumed you could always walk from lumbridge to south varrock without farms. This has been fixed * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Quests now respect skill limits and can be excluded. Tasks that take multiple skills how actually check all skills * Adds alternative route for cooking that doesn't require fishing * Remove debug code --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/osrs/LogicCSV/locations_generated.py | 6 +- worlds/osrs/Names.py | 2 +- worlds/osrs/Rules.py | 337 +++++++++++++ worlds/osrs/__init__.py | 503 +++----------------- 4 files changed, 401 insertions(+), 447 deletions(-) create mode 100644 worlds/osrs/Rules.py diff --git a/worlds/osrs/LogicCSV/locations_generated.py b/worlds/osrs/LogicCSV/locations_generated.py index 073e505ad8f4..2d617a7038fe 100644 --- a/worlds/osrs/LogicCSV/locations_generated.py +++ b/worlds/osrs/LogicCSV/locations_generated.py @@ -57,11 +57,11 @@ LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12), LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0), LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0), - LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2), + LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2), LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6), LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8), - LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0), - LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0), + LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0), + LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0), LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0), LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0), LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0), diff --git a/worlds/osrs/Names.py b/worlds/osrs/Names.py index cc92439ef859..1a44aa389c6a 100644 --- a/worlds/osrs/Names.py +++ b/worlds/osrs/Names.py @@ -31,7 +31,7 @@ class RegionNames(str, Enum): Mudskipper_Point = "Mudskipper Point" Karamja = "Karamja" Corsair_Cove = "Corsair Cove" - Wilderness = "The Wilderness" + Wilderness = "Wilderness" Crandor = "Crandor" # Resource Regions Egg = "Egg" diff --git a/worlds/osrs/Rules.py b/worlds/osrs/Rules.py new file mode 100644 index 000000000000..22a19934c8e1 --- /dev/null +++ b/worlds/osrs/Rules.py @@ -0,0 +1,337 @@ +""" + Ensures a target level can be reached with available resources + """ +from worlds.generic.Rules import CollectionRule, add_rule +from .Names import RegionNames, ItemNames + + +def get_fishing_skill_rule(level, player, options) -> CollectionRule: + if options.max_fishing_level < level: + return lambda state: False + + if options.brutal_grinds or level < 5: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) + if level < 20: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) + else: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) and \ + state.can_reach_region(RegionNames.Fly_Fish, player) + + +def get_mining_skill_rule(level, player, options) -> CollectionRule: + if options.max_mining_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) or \ + state.can_reach_region(RegionNames.Clay_Rock, player) + else: + # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for + return lambda state: (state.can_reach_region(RegionNames.Bronze_Ores, player) or + state.can_reach_region(RegionNames.Clay_Rock, player)) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) + + +def get_woodcutting_skill_rule(level, player, options) -> CollectionRule: + if options.max_woodcutting_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. + # Even the desert. + return lambda state: True + if level < 30: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) + else: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and \ + state.can_reach_region(RegionNames.Willow_Tree, player) + + +def get_smithing_skill_rule(level, player, options) -> CollectionRule: + if options.max_smithing_level < level: + return lambda state: False + + if options.brutal_grinds: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) + if level < 15: + # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included + # in the "Anvil" resource region. We still need to check for it though. + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + (state.can_reach_region(RegionNames.Anvil, player) or + state.can_reach_region(RegionNames.Lumbridge, player)) + if level < 30: + # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + else: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Coal_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + + +def get_crafting_skill_rule(level, player, options): + if options.max_crafting_level < level: + return lambda state: False + + # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable + def can_spin(state): + return state.can_reach_region(RegionNames.Sheep, player) and \ + state.can_reach_region(RegionNames.Spinning_Wheel, player) + + def can_pot(state): + return state.can_reach_region(RegionNames.Clay_Rock, player) and \ + state.can_reach_region(RegionNames.Barbarian_Village, player) + + def can_tan(state): + return state.can_reach_region(RegionNames.Milk, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + def mould_access(state): + return state.can_reach_region(RegionNames.Al_Kharid, player) or \ + state.can_reach_region(RegionNames.Rimmington, player) + + def can_silver(state): + return state.can_reach_region(RegionNames.Silver_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + def can_gold(state): + return state.can_reach_region(RegionNames.Gold_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + if options.brutal_grinds or level < 5: + return lambda state: can_spin(state) or can_pot(state) or can_tan(state) + + can_smelt_gold = get_smithing_skill_rule(40, player, options) + can_smelt_silver = get_smithing_skill_rule(20, player, options) + if level < 16: + return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state)) + else: + return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ + (can_gold(state) and can_smelt_gold(state)) + + +def get_cooking_skill_rule(level, player, options) -> CollectionRule: + if options.max_cooking_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Milk, player) or \ + state.can_reach_region(RegionNames.Egg, player) or \ + state.can_reach_region(RegionNames.Shrimp, player) or \ + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + else: + can_catch_fly_fish = get_fishing_skill_rule(20, player, options) + + return lambda state: ( + (state.can_reach_region(RegionNames.Fly_Fish, player) and can_catch_fly_fish(state)) or + (state.can_reach_region(RegionNames.Port_Sarim, player)) + ) and ( + state.can_reach_region(RegionNames.Milk, player) or + state.can_reach_region(RegionNames.Egg, player) or + state.can_reach_region(RegionNames.Shrimp, player) or + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + ) + + +def get_runecraft_skill_rule(level, player, options) -> CollectionRule: + if options.max_runecraft_level < level: + return lambda state: False + if not options.brutal_grinds: + # Ensure access to the relevant altars + if level >= 5: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) + if level >= 9: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) + if level >= 14: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) + + +def get_magic_skill_rule(level, player, options) -> CollectionRule: + if options.max_magic_level < level: + return lambda state: False + + return lambda state: state.can_reach_region(RegionNames.Mind_Runes, player) + + +def get_firemaking_skill_rule(level, player, options) -> CollectionRule: + if options.max_firemaking_level < level: + return lambda state: False + if not options.brutal_grinds: + if level >= 30: + can_chop_willows = get_woodcutting_skill_rule(30, player, options) + return lambda state: state.can_reach_region(RegionNames.Willow_Tree, player) and can_chop_willows(state) + if level >= 15: + can_chop_oaks = get_woodcutting_skill_rule(15, player, options) + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and can_chop_oaks(state) + # If brutal grinds are on, or if the level is less than 15, you can train it. + return lambda state: True + + +def get_skill_rule(skill, level, player, options) -> CollectionRule: + if skill.lower() == "fishing": + return get_fishing_skill_rule(level, player, options) + if skill.lower() == "mining": + return get_mining_skill_rule(level, player, options) + if skill.lower() == "woodcutting": + return get_woodcutting_skill_rule(level, player, options) + if skill.lower() == "smithing": + return get_smithing_skill_rule(level, player, options) + if skill.lower() == "crafting": + return get_crafting_skill_rule(level, player, options) + if skill.lower() == "cooking": + return get_cooking_skill_rule(level, player, options) + if skill.lower() == "runecraft": + return get_runecraft_skill_rule(level, player, options) + if skill.lower() == "magic": + return get_magic_skill_rule(level, player, options) + if skill.lower() == "firemaking": + return get_firemaking_skill_rule(level, player, options) + + return lambda state: True + + +def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options): + if outbound_region_name == RegionNames.Cooks_Guild: + add_rule(entrance, get_cooking_skill_rule(32, player, options)) + elif outbound_region_name == RegionNames.Crafting_Guild: + add_rule(entrance, get_crafting_skill_rule(40, player, options)) + elif outbound_region_name == RegionNames.Corsair_Cove: + # Need to be able to start Corsair Curse in addition to having the item + add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player)) + elif outbound_region_name == "Camdozaal*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player)) + elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player)) + + # Special logic for canoes + canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, + RegionNames.Edgeville, RegionNames.Wilderness] + if region_row.name in canoe_regions: + # Skill rules for greater distances + woodcutting_rule_d1 = get_woodcutting_skill_rule(12, player, options) + woodcutting_rule_d2 = get_woodcutting_skill_rule(27, player, options) + woodcutting_rule_d3 = get_woodcutting_skill_rule(42, player, options) + woodcutting_rule_all = get_woodcutting_skill_rule(57, player, options) + + if region_row.name == RegionNames.Lumbridge: + # Canoe Tree access for the Location + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_all(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + + elif region_row.name == RegionNames.South_Of_Varrock: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d3(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + elif region_row.name == RegionNames.Barbarian_Village: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d2(state)) or (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d2(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + # Edgeville does not need to be checked, because it's already adjacent + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_d3) + elif region_row.name == RegionNames.Edgeville: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d2) + # Barbarian Village does not need to be checked, because it's already adjacent + # Wilderness does not need to be checked, because it's already adjacent + elif region_row.name == RegionNames.Wilderness: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_all(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_all) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + # Edgeville does not need to be checked, because it's already adjacent diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 58f23a2bc1d9..d6ddd63875f4 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -1,12 +1,12 @@ import typing -from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld +from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState +from Fill import fill_restrictive, FillError from worlds.AutoWorld import WebWorld, World -from worlds.generic.Rules import add_rule, CollectionRule from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ chunksanity_special_region_names from .Locations import OSRSLocation, LocationRow - +from .Rules import * from .Options import OSRSOptions, StartingArea from .Names import LocationNames, ItemNames, RegionNames @@ -46,6 +46,7 @@ class OSRSWorld(World): web = OSRSWeb() base_id = 0x070000 data_version = 1 + explicit_indirect_conditions = False item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))} @@ -61,6 +62,7 @@ class OSRSWorld(World): starting_area_item: str locations_by_category: typing.Dict[str, typing.List[LocationRow]] + available_QP_locations: typing.List[str] def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) @@ -75,6 +77,7 @@ def __init__(self, multiworld: MultiWorld, player: int): self.starting_area_item = "" self.locations_by_category = {} + self.available_QP_locations = [] def generate_early(self) -> None: location_categories = [location_row.category for location_row in location_rows] @@ -90,9 +93,9 @@ def generate_early(self) -> None: rnd = self.random starting_area = self.options.starting_area - + #UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT - if not hasattr(self.multiworld, "generation_is_fake"): + if not hasattr(self.multiworld, "generation_is_fake"): if starting_area.value == StartingArea.option_any_bank: self.starting_area_item = rnd.choice(starting_area_dict) elif starting_area.value < StartingArea.option_chunksanity: @@ -127,7 +130,6 @@ def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None: starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) starting_entrance.connect(self.region_name_to_data[starting_area_region]) - def create_regions(self) -> None: """ called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done @@ -145,7 +147,8 @@ def create_regions(self) -> None: # Removes the word "Area: " from the item name to get the region it applies to. # I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse - if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it + # if area hasn't been set, then we shouldn't connect it + if self.starting_area_item != "": if self.starting_area_item in chunksanity_special_region_names: starting_area_region = chunksanity_special_region_names[self.starting_area_item] else: @@ -164,11 +167,8 @@ def create_regions(self) -> None: entrance.connect(self.region_name_to_data[parsed_outbound]) item_name = self.region_rows_by_name[parsed_outbound].itemReq - if "*" not in outbound_region_name and "*" not in item_name: - entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player) - continue - - self.generate_special_rules_for(entrance, region_row, outbound_region_name) + entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player) + generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options) for resource_region in region_row.resources: if not resource_region: @@ -178,321 +178,34 @@ def create_regions(self) -> None: if "*" not in resource_region: entrance.connect(self.region_name_to_data[resource_region]) else: - self.generate_special_rules_for(entrance, region_row, resource_region) entrance.connect(self.region_name_to_data[resource_region.replace('*', '')]) + generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options) self.roll_locations() - def generate_special_rules_for(self, entrance, region_row, outbound_region_name): - # print(f"Special rules required to access region {outbound_region_name} from {region_row.name}") - if outbound_region_name == RegionNames.Cooks_Guild: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - cooking_level_rule = self.get_skill_rule("cooking", 32) - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - cooking_level_rule(state) - if self.options.brutal_grinds: - cooking_level_32_regions = { - RegionNames.Milk, - RegionNames.Egg, - RegionNames.Shrimp, - RegionNames.Wheat, - RegionNames.Windmill, - } - else: - # Level 15 cooking and higher requires level 20 fishing. - fishing_level_20_regions = { - RegionNames.Shrimp, - RegionNames.Port_Sarim, - } - cooking_level_32_regions = { - RegionNames.Milk, - RegionNames.Egg, - RegionNames.Shrimp, - RegionNames.Wheat, - RegionNames.Windmill, - RegionNames.Fly_Fish, - *fishing_level_20_regions, - } - for region_name in cooking_level_32_regions: - self.multiworld.register_indirect_condition(self.get_region(region_name), entrance) - return - if outbound_region_name == RegionNames.Crafting_Guild: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - crafting_level_rule = self.get_skill_rule("crafting", 40) - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - crafting_level_rule(state) - if self.options.brutal_grinds: - crafting_level_40_regions = { - # can_spin - RegionNames.Sheep, - RegionNames.Spinning_Wheel, - # can_pot - RegionNames.Clay_Rock, - RegionNames.Barbarian_Village, - # can_tan - RegionNames.Milk, - RegionNames.Al_Kharid, - } - else: - mould_access_regions = { - RegionNames.Al_Kharid, - RegionNames.Rimmington, - } - smithing_level_20_regions = { - RegionNames.Bronze_Ores, - RegionNames.Iron_Rock, - RegionNames.Furnace, - RegionNames.Anvil, - } - smithing_level_40_regions = { - *smithing_level_20_regions, - RegionNames.Coal_Rock, - } - crafting_level_40_regions = { - # can_tan - RegionNames.Milk, - RegionNames.Al_Kharid, - # can_silver - RegionNames.Silver_Rock, - RegionNames.Furnace, - *mould_access_regions, - # can_smelt_silver - *smithing_level_20_regions, - # can_gold - RegionNames.Gold_Rock, - RegionNames.Furnace, - *mould_access_regions, - # can_smelt_gold - *smithing_level_40_regions, - } - for region_name in crafting_level_40_regions: - self.multiworld.register_indirect_condition(self.get_region(region_name), entrance) - return - if outbound_region_name == RegionNames.Corsair_Cove: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - # Need to be able to start Corsair Curse in addition to having the item - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - state.can_reach(RegionNames.Falador_Farm, "Region", self.player) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance) - - return - if outbound_region_name == "Camdozaal*": - item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - state.has(ItemNames.QP_Below_Ice_Mountain, self.player) - return - if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": - entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player) - return - # Special logic for canoes - canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, - RegionNames.Edgeville, RegionNames.Wilderness] - if region_row.name in canoe_regions: - # Skill rules for greater distances - woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12) - woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27) - woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42) - woodcutting_rule_all = self.get_skill_rule("woodcutting", 57) - - def add_indirect_conditions_for_woodcutting_levels(entrance, *levels: int): - if self.options.brutal_grinds: - # No access to specific regions required. - return - # Currently, each level requirement requires everything from the previous level requirements, so the - # maximum level requirement can be taken. - max_level = max(levels, default=0) - max_level = min(max_level, self.options.max_woodcutting_level.value) - if 15 <= max_level < 30: - self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance) - elif 30 <= max_level: - self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance) - self.multiworld.register_indirect_condition(self.get_region(RegionNames.Willow_Tree), entrance) - - if region_row.name == RegionNames.Lumbridge: - # Canoe Tree access for the Location - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - # South of Varrock does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - if outbound_region_name == RegionNames.Edgeville: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 57 - add_indirect_conditions_for_woodcutting_levels(entrance, 57) - - if region_row.name == RegionNames.South_Of_Varrock: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - # Lumbridge does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ - and self.options.max_woodcutting_level >= 12 - add_indirect_conditions_for_woodcutting_levels(entrance, 12) - if outbound_region_name == RegionNames.Edgeville: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if region_row.name == RegionNames.Barbarian_Village: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ - and self.options.max_woodcutting_level >= 12 - add_indirect_conditions_for_woodcutting_levels(entrance, 12) - # Edgeville does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if region_row.name == RegionNames.Edgeville: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - # Barbarian Village does not need to be checked, because it's already adjacent - # Wilderness does not need to be checked, because it's already adjacent - if region_row.name == RegionNames.Wilderness: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 57 - add_indirect_conditions_for_woodcutting_levels(entrance, 57) - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - # Edgeville does not need to be checked, because it's already adjacent + def task_within_skill_levels(self, skills_required): + # Loop through each required skill. If any of its requirements are out of the defined limit, return false + for skill in skills_required: + max_level_for_skill = getattr(self.options, f"max_{skill.skill.lower()}_level") + if skill.level > max_level_for_skill: + return False + return True def roll_locations(self): - locations_required = 0 generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override + locations_required = 0 for item_row in item_rows: locations_required += item_row.amount locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 - # Quests are always added + # Quests are always added first, before anything else is rolled for i, location_row in enumerate(location_rows): if location_row.category in {"quest", "points", "goal"}: - self.create_and_add_location(i) - if location_row.category == "quest": - locations_added += 1 + if self.task_within_skill_levels(location_row.skills): + self.create_and_add_location(i) + if location_row.category == "quest": + locations_added += 1 # Build up the weighted Task Pool rnd = self.random @@ -516,10 +229,9 @@ def roll_locations(self): task_types = ["prayer", "magic", "runecraft", "mining", "crafting", "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] for task_type in task_types: - max_level_for_task_type = getattr(self.options, f"max_{task_type}_level") max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks") tasks_for_this_type = [task for task in self.locations_by_category[task_type] - if task.skills[0].level <= max_level_for_task_type] + if self.task_within_skill_levels(task.skills)] if not self.options.progressive_tasks: rnd.shuffle(tasks_for_this_type) else: @@ -568,6 +280,7 @@ def roll_locations(self): self.add_location(task) locations_added += 1 + def add_location(self, location): index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0] self.create_and_add_location(index) @@ -586,11 +299,15 @@ def get_filler_item_name(self) -> str: def create_and_add_location(self, row_index) -> None: location_row = location_rows[row_index] - # print(f"Adding task {location_row.name}") + + # Quest Points are handled differently now, but in case this gets fed an older version of the data sheet, + # the points might still be listed in a different row + if location_row.category == "points": + return # Create Location location_id = self.base_id + row_index - if location_row.category == "points" or location_row.category == "goal": + if location_row.category == "goal": location_id = None location = OSRSLocation(self.player, location_row.name, location_id) self.location_name_to_data[location_row.name] = location @@ -602,6 +319,14 @@ def create_and_add_location(self, row_index) -> None: location.parent_region = region region.locations.append(location) + # If it's a quest, generate a "Points" location we'll add an event to + if location_row.category == "quest": + points_name = location_row.name.replace("Quest:", "Points:") + points_location = OSRSLocation(self.player, points_name) + self.location_name_to_data[points_name] = points_location + points_location.parent_region = region + region.locations.append(points_location) + def set_rules(self) -> None: """ called to set access and item rules on locations and entrances. @@ -612,18 +337,26 @@ def set_rules(self) -> None: "Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure", "Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot", "Below_Ice_Mountain"] - for qp_attr_name in quest_attr_names: - loc_name = getattr(LocationNames, f"QP_{qp_attr_name}") - item_name = getattr(ItemNames, f"QP_{qp_attr_name}") - self.multiworld.get_location(loc_name, self.player) \ - .place_locked_item(self.create_event(item_name)) for quest_attr_name in quest_attr_names: qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}") + qp_loc = self.location_name_to_data.get(qp_loc_name) + q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}") - add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: ( - self.multiworld.get_location(q_loc_name, self.player).can_reach(state) - )) + q_loc = self.location_name_to_data.get(q_loc_name) + + # Checks to make sure the task is actually in the list before trying to create its rules + if qp_loc and q_loc: + # Create the QP Event Item + item_name = getattr(ItemNames, f"QP_{quest_attr_name}") + qp_loc.place_locked_item(self.create_event(item_name)) + + # If a quest is excluded, don't actually consider it for quest point progression + if q_loc_name not in self.options.exclude_locations: + self.available_QP_locations.append(item_name) + + # Set the access rule for the QP Location + add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state))) # place "Victory" at "Dragon Slayer" and set collection as win condition self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ @@ -639,7 +372,7 @@ def set_rules(self) -> None: lambda state, region_required=region_required: state.can_reach(region_required, "Region", self.player)) for skill_req in location_row.skills: - add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level)) + add_rule(location, get_skill_rule(skill_req.skill, skill_req.level, self.player, self.options)) for item_req in location_row.items: add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player)) if location_row.qp: @@ -664,124 +397,8 @@ def create_event(self, event: str): def quest_points(self, state): qp = 0 - for qp_event in QP_Items: + for qp_event in self.available_QP_locations: if state.has(qp_event, self.player): qp += int(qp_event[0]) return qp - """ - Ensures a target level can be reached with available resources - """ - - def get_skill_rule(self, skill, level) -> CollectionRule: - if skill.lower() == "fishing": - if self.options.brutal_grinds or level < 5: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) - if level < 20: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ - state.can_reach(RegionNames.Port_Sarim, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ - state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \ - state.can_reach(RegionNames.Fly_Fish, "Region", self.player) - if skill.lower() == "mining": - if self.options.brutal_grinds or level < 15: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \ - state.can_reach(RegionNames.Clay_Rock, "Region", self.player) - else: - # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for - return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or - state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) - if skill.lower() == "woodcutting": - if self.options.brutal_grinds or level < 15: - # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. - # Even the desert. - return lambda state: True - if level < 30: - return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \ - state.can_reach(RegionNames.Willow_Tree, "Region", self.player) - if skill.lower() == "smithing": - if self.options.brutal_grinds: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) - if level < 15: - # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included - # in the "Anvil" resource region. We still need to check for it though. - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - (state.can_reach(RegionNames.Anvil, "Region", self.player) or - state.can_reach(RegionNames.Lumbridge, "Region", self.player)) - if level < 30: - # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - state.can_reach(RegionNames.Anvil, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - state.can_reach(RegionNames.Anvil, "Region", self.player) - if skill.lower() == "crafting": - # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable - def can_spin(state): - return state.can_reach(RegionNames.Sheep, "Region", self.player) and \ - state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player) - - def can_pot(state): - return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Barbarian_Village, "Region", self.player) - - def can_tan(state): - return state.can_reach(RegionNames.Milk, "Region", self.player) and \ - state.can_reach(RegionNames.Al_Kharid, "Region", self.player) - - def mould_access(state): - return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \ - state.can_reach(RegionNames.Rimmington, "Region", self.player) - - def can_silver(state): - - return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) - - def can_gold(state): - return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) - - if self.options.brutal_grinds or level < 5: - return lambda state: can_spin(state) or can_pot(state) or can_tan(state) - - can_smelt_gold = self.get_skill_rule("smithing", 40) - can_smelt_silver = self.get_skill_rule("smithing", 20) - if level < 16: - return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state)) - else: - return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ - (can_gold(state) and can_smelt_gold(state)) - if skill.lower() == "cooking": - if self.options.brutal_grinds or level < 15: - return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \ - state.can_reach(RegionNames.Egg, "Region", self.player) or \ - state.can_reach(RegionNames.Shrimp, "Region", self.player) or \ - (state.can_reach(RegionNames.Wheat, "Region", self.player) and - state.can_reach(RegionNames.Windmill, "Region", self.player)) - else: - can_catch_fly_fish = self.get_skill_rule("fishing", 20) - return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \ - can_catch_fly_fish(state) and \ - (state.can_reach(RegionNames.Milk, "Region", self.player) or - state.can_reach(RegionNames.Egg, "Region", self.player) or - state.can_reach(RegionNames.Shrimp, "Region", self.player) or - (state.can_reach(RegionNames.Wheat, "Region", self.player) and - state.can_reach(RegionNames.Windmill, "Region", self.player))) - if skill.lower() == "runecraft": - return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player) - if skill.lower() == "magic": - return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player) - - return lambda state: True From ba50c947bade7ec04a597a1b73b232a7af89da65 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 22 Nov 2024 23:13:57 +0000 Subject: [PATCH 43/60] AHiT: Fix reconnecting rift access regions for starting and plando acts (#4200) Reconnecting an act in a telescope to a time rift removes the entrances to the time rift from its access regions because it will be accessible from the telescope instead. By doing so early on, as a starting act with insanity act randomizer or as a plando-ed act, this can happen before the time rift itself has been reconnected to an act or other time rift. In which case, when later attempting to connect that time rift to an act or other time rift, the entrances from the rift access regions will no longer exist, so must be re-created. The original code was mistakenly re-creating the entrances from the time rift being reconnected, instead of from the rift access regions. --- worlds/ahit/Regions.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index c70f08b475eb..31edf1d0b057 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -740,17 +740,20 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): - i = 1 - while i <= len(rift_access_regions[time_rift.name]): + for i, access_region in enumerate(rift_access_regions[time_rift.name], start=1): + # Matches the naming convention and iteration order in `create_rift_connections()`. name = f"{time_rift.name} Portal - Entrance {i}" entrance: Entrance try: - entrance = world.multiworld.get_entrance(name, world.player) + entrance = world.get_entrance(name) + # Reconnect the rift access region to the new exit region. reconnect_regions(entrance, entrance.parent_region, exit_region) except KeyError: - time_rift.connect(exit_region, name) - - i += 1 + # The original entrance to the time rift has been deleted by already reconnecting a telescope act to the + # time rift, so create a new entrance from the original rift access region to the new exit region. + # Normally, acts and time rifts are sorted such that time rifts are reconnected to acts/rifts first, but + # starting acts/rifts and act-plando can reconnect acts to time rifts before this happens. + world.get_region(access_region).connect(exit_region, name) def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]: From 5729b785040e3b6f3d12444c934d988dfbbfcc71 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 22 Nov 2024 19:42:44 -0500 Subject: [PATCH 44/60] TUNIC: Fix it so item linked locations are correct in slot data (#4105) * Fix it so item linked locations are correct in slot data * List -> Set * Cache the locations instead * Just loop the multiworld once * Move it all to fill slot data and pretend we're doing a stage * Move groups up so it doesn't loop over the multiworld locations if no item links are present * Update worlds/tunic/__init__.py Co-authored-by: Mysteryem --------- Co-authored-by: Mysteryem --- worlds/tunic/__init__.py | 43 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 6360ba493853..5a2847278bd5 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -83,6 +83,11 @@ class TunicWorld(World): shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work + # so we only loop the multiworld locations once + # if these are locations instead of their info, it gives a memory leak error + item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {} + player_item_link_locations: Dict[str, List[Location]] + def generate_early(self) -> None: if self.options.logic_rules >= LogicRules.option_no_major_glitches: self.options.laurels_zips.value = LaurelsZips.option_true @@ -387,6 +392,18 @@ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if hint_text: hint_data[self.player][location.address] = hint_text + def get_real_location(self, location: Location) -> Tuple[str, int]: + # if it's not in a group, it's not in an item link + if location.player not in self.multiworld.groups or not location.item: + return location.name, location.player + try: + loc = self.player_item_link_locations[location.item.name].pop() + return loc.name, loc.player + except IndexError: + warning(f"TUNIC: Failed to parse item location for in-game hints for {self.player_name}. " + f"Using a potentially incorrect location name instead.") + return location.name, location.player + def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = { "seed": self.random.randint(0, 2147483647), @@ -412,12 +429,34 @@ def fill_slot_data(self) -> Dict[str, Any]: "disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race), } + # this would be in a stage if there was an appropriate stage for it + self.player_item_link_locations = {} + groups = self.multiworld.get_player_groups(self.player) + if groups: + if not self.item_link_locations: + tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC") + # figure out our groups and the items in them + for tunic in tunic_worlds: + for group in self.multiworld.get_player_groups(tunic.player): + self.item_link_locations.setdefault(group, {}) + for location in self.multiworld.get_locations(): + if location.item and location.item.player in self.item_link_locations.keys(): + (self.item_link_locations[location.item.player].setdefault(location.item.name, []) + .append((location.player, location.name))) + + # if item links are on, set up the player's personal item link locations, so we can pop them as needed + for group, item_links in self.item_link_locations.items(): + if group in groups: + for item_name, locs in item_links.items(): + self.player_item_link_locations[item_name] = \ + [self.multiworld.get_location(location_name, player) for player, location_name in locs] + for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): if tunic_item.name not in slot_data: slot_data[tunic_item.name] = [] if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6: continue - slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player]) + slot_data[tunic_item.name].extend(self.get_real_location(tunic_item.location)) for start_item in self.options.start_inventory_from_pool: if start_item in slot_data_item_names: @@ -436,7 +475,7 @@ def fill_slot_data(self) -> Dict[str, Any]: if item in slot_data_item_names: slot_data[item] = [] for item_location in self.multiworld.find_item_locations(item, self.player): - slot_data[item].extend([item_location.name, item_location.player]) + slot_data[item].extend(self.get_real_location(item_location)) return slot_data From 03b90cf39ba1d10952545c44fee91fd874837124 Mon Sep 17 00:00:00 2001 From: Jarno Date: Sun, 24 Nov 2024 15:57:39 +0100 Subject: [PATCH 45/60] =?UTF-8?q?Timespinner:=20Re-added=20missing=20enmem?= =?UTF-8?q?y=20rando=20option=C2=A0#4235?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/timespinner/Options.py | 6 ++++++ worlds/timespinner/__init__.py | 1 + 2 files changed, 7 insertions(+) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index f6a3dba3e311..c06dd36797fd 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -379,6 +379,7 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): cantoran: Cantoran lore_checks: LoreChecks boss_rando: BossRando + enemy_rando: EnemyRando damage_rando: DamageRando damage_rando_overrides: DamageRandoOverrides hp_cap: HpCap @@ -445,6 +446,7 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): Cantoran: hidden(Cantoran) # type: ignore LoreChecks: hidden(LoreChecks) # type: ignore BossRando: hidden(BossRando) # type: ignore + EnemyRando: hidden(EnemyRando) # type: ignore DamageRando: hidden(DamageRando) # type: ignore DamageRandoOverrides: HiddenDamageRandoOverrides HpCap: hidden(HpCap) # type: ignore @@ -516,6 +518,10 @@ def handle_backward_compatibility(self) -> None: self.boss_rando == BossRando.default: self.boss_rando.value = self.BossRando.value self.has_replaced_options.value = Toggle.option_true + if self.EnemyRando != EnemyRando.default and \ + self.enemy_rando == EnemyRando.default: + self.enemy_rando.value = self.EnemyRando.value + self.has_replaced_options.value = Toggle.option_true if self.DamageRando != DamageRando.default and \ self.damage_rando == DamageRando.default: self.damage_rando.value = self.DamageRando.value diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index f241d4468162..72903bd5ffea 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -98,6 +98,7 @@ def fill_slot_data(self) -> Dict[str, object]: "Cantoran": self.options.cantoran.value, "LoreChecks": self.options.lore_checks.value, "BossRando": self.options.boss_rando.value, + "EnemyRando": self.options.enemy_rando.value, "DamageRando": self.options.damage_rando.value, "DamageRandoOverrides": self.options.damage_rando_overrides.value, "HpCap": self.options.hp_cap.value, From 36f17111bf5a9cd1027065422098e7b677640860 Mon Sep 17 00:00:00 2001 From: gaithern <36639398+gaithern@users.noreply.github.com> Date: Sun, 24 Nov 2024 11:42:21 -0600 Subject: [PATCH 46/60] Kingdom Hearts: Minor Logic Fixes (#4236) * Update Rules.py * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright --------- Co-authored-by: Scipio Wright --- worlds/kh1/Rules.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/worlds/kh1/Rules.py b/worlds/kh1/Rules.py index e1f72f5b3e54..130238e5048e 100644 --- a/worlds/kh1/Rules.py +++ b/worlds/kh1/Rules.py @@ -235,6 +235,11 @@ def set_rules(kh1world): lambda state: ( state.has("Progressive Glide", player) or + ( + state.has("High Jump", player, 2) + and state.has("Footprints", player) + ) + or ( options.advanced_logic and state.has_all({ @@ -246,6 +251,11 @@ def set_rules(kh1world): lambda state: ( state.has("Progressive Glide", player) or + ( + state.has("High Jump", player, 2) + and state.has("Footprints", player) + ) + or ( options.advanced_logic and state.has_all({ @@ -258,7 +268,6 @@ def set_rules(kh1world): state.has("Footprints", player) or (options.advanced_logic and state.has("Progressive Glide", player)) - or state.has("High Jump", player, 2) )) add_rule(kh1world.get_location("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest"), lambda state: ( @@ -376,7 +385,7 @@ def set_rules(kh1world): lambda state: state.has("White Trinity", player)) add_rule(kh1world.get_location("Monstro Chamber 6 Other Platform Chest"), lambda state: ( - state.has("High Jump", player) + state.has_all(("High Jump", "Progressive Glide"), player) or (options.advanced_logic and state.has("Combo Master", player)) )) add_rule(kh1world.get_location("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest"), @@ -386,7 +395,7 @@ def set_rules(kh1world): )) add_rule(kh1world.get_location("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest"), lambda state: ( - state.has("High Jump", player) + state.has_all(("High Jump", "Progressive Glide"), player) or (options.advanced_logic and state.has("Combo Master", player)) )) add_rule(kh1world.get_location("Halloween Town Moonlight Hill White Trinity Chest"), @@ -595,6 +604,7 @@ def set_rules(kh1world): lambda state: ( state.has("Green Trinity", player) and has_all_magic_lvx(state, player, 2) + and has_defensive_tools(state, player) )) add_rule(kh1world.get_location("Neverland Hold Flight 2nd Chest"), lambda state: ( @@ -710,8 +720,7 @@ def set_rules(kh1world): lambda state: state.has("White Trinity", player)) add_rule(kh1world.get_location("End of the World Giant Crevasse 5th Chest"), lambda state: ( - state.has("High Jump", player) - or state.has("Progressive Glide", player) + state.has("Progressive Glide", player) )) add_rule(kh1world.get_location("End of the World Giant Crevasse 1st Chest"), lambda state: ( @@ -1441,10 +1450,11 @@ def set_rules(kh1world): has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) + and state.has("Progressive Blizzard", player, 3) )) add_rule(kh1world.get_location("Agrabah Defeat Kurt Zisa Zantetsuken Event"), lambda state: ( - has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) + has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) and state.has("Progressive Blizzard", player, 3) )) if options.super_bosses or options.goal.current_key == "sephiroth": add_rule(kh1world.get_location("Olympus Coliseum Defeat Sephiroth Ansem's Report 12"), From a650e90b576bb5ad791e9b6fb7c036cfe747ec97 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 24 Nov 2024 12:43:28 -0500 Subject: [PATCH 47/60] =?UTF-8?q?TUNIC:=20Add=20clarifying=20comment=20to?= =?UTF-8?q?=20item=20links=20handling=C2=A0#4233?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/tunic/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 5a2847278bd5..d1430aac1895 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -432,6 +432,7 @@ def fill_slot_data(self) -> Dict[str, Any]: # this would be in a stage if there was an appropriate stage for it self.player_item_link_locations = {} groups = self.multiworld.get_player_groups(self.player) + # checking if groups so that this doesn't run if the player isn't in a group if groups: if not self.item_link_locations: tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC") From e6e31a27e688ecbb45fa9569e72d2a4db111e37f Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:25:00 -0500 Subject: [PATCH 48/60] SC2: Fix Nondeterministic Behavior (#4246) * Add < for sorting * Sorting for determinism * id instead of value --- worlds/sc2/MissionTables.py | 3 +++ worlds/sc2/Regions.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/sc2/MissionTables.py b/worlds/sc2/MissionTables.py index 4dece46411bf..08e1f133deda 100644 --- a/worlds/sc2/MissionTables.py +++ b/worlds/sc2/MissionTables.py @@ -43,6 +43,9 @@ def __init__(self, campaign_id: int, name: str, goal_priority: SC2CampaignGoalPr self.goal_priority = goal_priority self.race = race + def __lt__(self, other: "SC2Campaign"): + return self.id < other.id + GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS diff --git a/worlds/sc2/Regions.py b/worlds/sc2/Regions.py index 84830a9a32bd..273bc4a5e87c 100644 --- a/worlds/sc2/Regions.py +++ b/worlds/sc2/Regions.py @@ -50,7 +50,7 @@ def create_vanilla_regions( names: Dict[str, int] = {} # Generating all regions and locations for each enabled campaign - for campaign in enabled_campaigns: + for campaign in sorted(enabled_campaigns): for region_name in vanilla_mission_req_table[campaign].keys(): regions.append(create_region(world, locations_per_region, location_cache, region_name)) world.multiworld.regions += regions From 0ad5b0ade891e7547d2af772e5b09e926daad2f8 Mon Sep 17 00:00:00 2001 From: wildham <64616385+wildham0@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:25:29 -0500 Subject: [PATCH 49/60] [FFMQ] Fix all checks sending on hard reset + stronger read validation check (#4242) * Fix all checks sending on hard reset * stronger validation * Fix typo * remove extraneous else Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * fix style Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/ffmq/Client.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index 93688a6116f6..401c240a46ba 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -47,6 +47,17 @@ def get_flag(data, flag): bit = int(0x80 / (2 ** (flag % 8))) return (data[byte] & bit) > 0 +def validate_read_state(data1, data2): + validation_array = bytes([0x01, 0x46, 0x46, 0x4D, 0x51, 0x52]) + + if data1 is None or data2 is None: + return False + for i in range(6): + if data1[i] != validation_array[i] or data2[i] != validation_array[i]: + return False; + return True + + class FFMQClient(SNIClient): game = "Final Fantasy Mystic Quest" @@ -67,11 +78,11 @@ async def validate_rom(self, ctx): async def game_watcher(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - check_1 = await snes_read(ctx, 0xF53749, 1) + check_1 = await snes_read(ctx, 0xF53749, 6) received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) - check_2 = await snes_read(ctx, 0xF53749, 1) - if check_1 != b'\x01' or check_2 != b'\x01': + check_2 = await snes_read(ctx, 0xF53749, 6) + if not validate_read_state(check_1, check_2): return def get_range(data_range): From d3a3c29bc9811970c423003d6550909bae0b7fb4 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:25:55 -0500 Subject: [PATCH 50/60] Landstalker: Fix Nondeterministic Behavior #4245 --- worlds/landstalker/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py index b0148269eab3..0fe63526c63b 100644 --- a/worlds/landstalker/Locations.py +++ b/worlds/landstalker/Locations.py @@ -34,7 +34,7 @@ def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], n for data in WORLD_PATHS_JSON: if "requiredNodes" in data: regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]]) - regions_with_entrance_checks = list(set(regions_with_entrance_checks)) + regions_with_entrance_checks = sorted(set(regions_with_entrance_checks)) for region_id in regions_with_entrance_checks: region = regions_table[region_id] location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event") From 41a7d7eeee67daef201a829a8ac7fed6224bfe6d Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:26:21 -0500 Subject: [PATCH 51/60] HK: Fix Nondeterministic Behavior #4244 --- worlds/hk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 486aa164cd5d..aede8e59cca5 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -231,7 +231,7 @@ def create_regions(self): all_event_names.update(set(godhome_event_names)) # Link regions - for event_name in all_event_names: + for event_name in sorted(all_event_names): #if event_name in wp_exclusions: # continue loc = HKLocation(self.player, event_name, None, menu_region) From 6073d5e37e722b6c1c0f3ce266158b9b83a6f52a Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:26:44 -0500 Subject: [PATCH 52/60] Lufia2: Fix Nondeterministic Behavior #4243 --- worlds/lufia2ac/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 6433452cefea..96de24a4b6a0 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -118,7 +118,7 @@ def create_regions(self) -> None: L2ACItem("Progressive chest access", ItemClassification.progression, None, self.player)) chest_access.show_in_spoiler = False ancient_dungeon.locations.append(chest_access) - for iris in self.item_name_groups["Iris treasures"]: + for iris in sorted(self.item_name_groups["Iris treasures"]): treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}" iris_treasure: Location = \ L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon) From fcaba14b6273c487c42ab26f7ea3f188f57f3823 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:27:31 -0500 Subject: [PATCH 53/60] Zillion: Add display_name to ZillionSkill #4241 --- worlds/zillion/options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 5de0b65c82f0..8df612654db4 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -233,6 +233,7 @@ class ZillionSkill(Range): range_start = 0 range_end = 5 default = 2 + display_name = "skill" class ZillionStartingCards(NamedRange): From 0dade05133932deb9603167cb1d9a7fef67a4340 Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Tue, 26 Nov 2024 00:35:24 +0100 Subject: [PATCH 54/60] SC2: Fix wrongly classified location type (#4249) --- worlds/sc2/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sc2/Locations.py b/worlds/sc2/Locations.py index 53f41f4e4c3d..b9c30bb70106 100644 --- a/worlds/sc2/Locations.py +++ b/worlds/sc2/Locations.py @@ -1387,7 +1387,7 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]: lambda state: logic.templars_return_requirement(state)), LocationData("The Host", "The Host: Victory", SC2LOTV_LOC_ID_OFFSET + 2100, LocationType.VICTORY, lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.VICTORY, + LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.EXTRA, lambda state: logic.the_host_requirement(state)), LocationData("The Host", "The Host: South Void Shard", SC2LOTV_LOC_ID_OFFSET + 2102, LocationType.EXTRA, lambda state: logic.the_host_requirement(state)), From 75624042f7a56b7c394b5a5e36936635cb45e0f8 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Tue, 26 Nov 2024 18:44:33 -0500 Subject: [PATCH 55/60] Stardew Valley: Make progressive movie theater a progression trap (#3985) --- worlds/stardew_valley/__init__.py | 2 +- worlds/stardew_valley/data/items.csv | 2 +- worlds/stardew_valley/items.py | 3 ++- worlds/stardew_valley/test/TestGeneration.py | 4 ++-- worlds/stardew_valley/test/__init__.py | 2 +- worlds/stardew_valley/test/mods/TestMods.py | 4 ++-- worlds/stardew_valley/test/rules/TestStateRules.py | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index f9df8c292e37..44306011361c 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -319,7 +319,7 @@ def create_item(self, item: Union[str, ItemData], override_classification: ItemC if override_classification is None: override_classification = item.classification - if override_classification == ItemClassification.progression: + if override_classification & ItemClassification.progression: self.total_progression_items += 1 return StardewItem(item.name, override_classification, item.code, self.player) diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index ffcae223e251..05af275ba472 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -7,7 +7,7 @@ id,name,classification,groups,mod_name 19,Glittering Boulder Removed,progression,COMMUNITY_REWARD, 20,Minecarts Repair,useful,COMMUNITY_REWARD, 21,Bus Repair,progression,COMMUNITY_REWARD, -22,Progressive Movie Theater,progression,COMMUNITY_REWARD, +22,Progressive Movie Theater,"progression,trap",COMMUNITY_REWARD, 23,Stardrop,progression,, 24,Progressive Backpack,progression,, 25,Rusty Sword,filler,"WEAPON,DEPRECATED", diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 31c7da5e3ade..730cb24ce6e3 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -2,6 +2,7 @@ import enum import logging from dataclasses import dataclass, field +from functools import reduce from pathlib import Path from random import Random from typing import Dict, List, Protocol, Union, Set, Optional @@ -134,7 +135,7 @@ def load_item_csv(): item_reader = csv.DictReader(file) for item in item_reader: id = int(item["id"]) if item["id"] else None - classification = ItemClassification[item["classification"]] + classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")}) groups = {Group[group] for group in item["groups"].split(",") if group} mod_name = str(item["mod_name"]) if item["mod_name"] else None items.append(ItemData(id, item["name"], classification, mod_name, groups)) diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 8431e6857eaf..56f338fe8e11 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -35,7 +35,7 @@ def test_all_progression_items_are_added_to_the_pool(self): items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore] + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): self.assertIn(progression_item.name, all_created_items) @@ -86,7 +86,7 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self): items_to_ignore.extend(season.name for season in items.items_by_group[Group.WEAPON]) items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore] + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): if Group.GINGER_ISLAND in progression_item.groups: diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 3fe05d205ce0..8f4e5af28f84 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -306,7 +306,7 @@ def collect(self, item: Union[str, Item, Iterable[Item]], count: int = 1) -> Uni def create_item(self, item: str) -> StardewItem: created_item = self.world.create_item(item) - if created_item.classification == ItemClassification.progression: + if created_item.classification & ItemClassification.progression: self.multiworld.worlds[self.player].total_progression_items -= 1 return created_item diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 97184b1338b8..07a75f21b1de 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -75,7 +75,7 @@ def test_all_progression_items_are_added_to_the_pool(self): items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): @@ -105,7 +105,7 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self): items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): diff --git a/worlds/stardew_valley/test/rules/TestStateRules.py b/worlds/stardew_valley/test/rules/TestStateRules.py index 4f53b9a7f536..7d10f4ceb1d3 100644 --- a/worlds/stardew_valley/test/rules/TestStateRules.py +++ b/worlds/stardew_valley/test/rules/TestStateRules.py @@ -8,5 +8,5 @@ class TestHasProgressionPercent(unittest.TestCase): def test_max_item_amount_is_full_collection(self): # Not caching because it fails too often for some reason with solo_multiworld(world_caching=False) as (multiworld, world): - progression_item_count = sum(1 for i in multiworld.get_items() if ItemClassification.progression in i.classification) + progression_item_count = sum(1 for i in multiworld.get_items() if i.classification & ItemClassification.progression) self.assertEqual(world.total_progression_items, progression_item_count - 1) # -1 to skip Victory From dbf284d4b27f8ac31460087863e0b5c341da1101 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 27 Nov 2024 02:09:13 +0100 Subject: [PATCH 56/60] The Witness: Give an actual name to the new option (lol) #4238 --- worlds/witness/options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index e1462cc37508..fe039f9d47e3 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -406,6 +406,7 @@ class PuzzleRandomizationSeed(Range): Sigma Rando, which is the basis for all puzzle randomization in this randomizer, uses a seed from 1 to 9999999 for the puzzle randomization. This option lets you set this seed yourself. """ + display_name = "Puzzle Randomization Seed" range_start = 1 range_end = 9999999 default = "random" From e882c68277f9274848110807b2b177c03cfbf3d6 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Tue, 26 Nov 2024 20:09:53 -0500 Subject: [PATCH 57/60] Stardew Valley - Update documentation 5.x.x links into 6.x.x links #4255 --- worlds/stardew_valley/docs/en_Stardew Valley.md | 2 +- worlds/stardew_valley/docs/setup_en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index 0ed693031b82..62755dad798d 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -138,7 +138,7 @@ This means that, for these specific mods, if you decide to include them in your with the assumption that you will install and play with these mods. The multiworld will contain related items and locations for these mods, the specifics will vary from mod to mod -[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) +[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) List of supported mods: diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index c672152543cf..801bf345e916 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -12,7 +12,7 @@ - Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) * (Only for the TextClient) - Other Stardew Valley Mods [Nexus Mods](https://www.nexusmods.com/stardewvalley) - * There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) + * There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) that you can add to your yaml to include them with the Archipelago randomization * It is **not** recommended to further mod Stardew Valley with unsupported mods, although it is possible to do so. From 6c939d2d594c2b282dbdd365cf9f6f5b170f22b4 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 27 Nov 2024 02:49:18 +0100 Subject: [PATCH 58/60] The Witness: Rename "Panel Hunt Settings" to "Panel Hunt Options" (#4251) Who let me get away with this lmao --- worlds/witness/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index fe039f9d47e3..d1713e73c541 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -457,7 +457,7 @@ class TheWitnessOptions(PerGameCommonOptions): MountainLasers, ChallengeLasers, ]), - OptionGroup("Panel Hunt Settings", [ + OptionGroup("Panel Hunt Options", [ PanelHuntRequiredPercentage, PanelHuntTotal, PanelHuntPostgame, From 334781e976e0b701841634cc155fccfc86ca7348 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Nov 2024 03:28:00 +0100 Subject: [PATCH 59/60] Core: purge py3.8 and py3.9 (#3973) Co-authored-by: Remy Jette Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com> Co-authored-by: Aaron Wagener --- .github/pyright-config.json | 2 +- .github/workflows/analyze-modified-files.yml | 2 +- .github/workflows/build.yml | 4 +-- .github/workflows/unittests.yml | 4 +-- BaseClasses.py | 12 ++++---- ModuleUpdate.py | 4 +-- Utils.py | 5 ++-- WebHost.py | 2 +- WebHostLib/requirements.txt | 4 +-- docs/contributing.md | 2 +- docs/running from source.md | 2 +- kvui.py | 5 +--- setup.py | 2 +- worlds/AutoSNIClient.py | 4 +-- worlds/__init__.py | 19 ++++-------- worlds/hk/Extractor.py | 6 +--- worlds/hk/requirements.txt | 1 - worlds/messenger/__init__.py | 30 +++++++++---------- worlds/messenger/client_setup.py | 5 ++-- worlds/messenger/connections.py | 8 ++--- worlds/messenger/constants.py | 18 +++++------ worlds/messenger/options.py | 3 +- worlds/messenger/portals.py | 12 ++++---- worlds/messenger/regions.py | 13 ++++---- worlds/messenger/rules.py | 8 ++--- worlds/messenger/shop.py | 18 +++++------ worlds/messenger/subclasses.py | 10 +++---- worlds/messenger/test/test_shop.py | 2 +- worlds/stardew_valley/content/unpacking.py | 6 +--- worlds/stardew_valley/data/artisan.py | 4 +-- worlds/stardew_valley/data/game_item.py | 18 +++-------- worlds/stardew_valley/data/harvest.py | 17 +++++------ worlds/stardew_valley/data/shop.py | 13 ++++---- worlds/stardew_valley/data/skill.py | 4 +-- worlds/stardew_valley/items.py | 5 +--- worlds/stardew_valley/locations.py | 5 +--- worlds/stardew_valley/requirements.txt | 2 -- .../test/stability/TestStability.py | 2 -- worlds/zillion/options.py | 3 +- 39 files changed, 113 insertions(+), 173 deletions(-) delete mode 100644 worlds/hk/requirements.txt delete mode 100644 worlds/stardew_valley/requirements.txt diff --git a/.github/pyright-config.json b/.github/pyright-config.json index 6ad7fa5f19b5..7d981778905f 100644 --- a/.github/pyright-config.json +++ b/.github/pyright-config.json @@ -16,7 +16,7 @@ "reportMissingImports": true, "reportMissingTypeStubs": true, - "pythonVersion": "3.8", + "pythonVersion": "3.10", "pythonPlatform": "Windows", "executionEnvironments": [ diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml index c9995fa2d043..b59336fafe9b 100644 --- a/.github/workflows/analyze-modified-files.yml +++ b/.github/workflows/analyze-modified-files.yml @@ -53,7 +53,7 @@ jobs: - uses: actions/setup-python@v5 if: env.diff != '' with: - python-version: 3.8 + python-version: '3.10' - name: "Install dependencies" if: env.diff != '' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23c463fb947a..c013172ea034 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,14 +24,14 @@ env: jobs: # build-release-macos: # LF volunteer - build-win-py38: # RCs will still be built and signed by hand + build-win-py310: # RCs will still be built and signed by hand runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Install python uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.10' - name: Download run-time dependencies run: | Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index a38fef8fda08..88b5d12987ad 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -33,13 +33,11 @@ jobs: matrix: os: [ubuntu-latest] python: - - {version: '3.8'} - - {version: '3.9'} - {version: '3.10'} - {version: '3.11'} - {version: '3.12'} include: - - python: {version: '3.8'} # win7 compat + - python: {version: '3.10'} # old compat os: windows-latest - python: {version: '3.12'} # current os: windows-latest diff --git a/BaseClasses.py b/BaseClasses.py index 46edeb5ea059..2e4efd606df9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,18 +1,16 @@ from __future__ import annotations import collections -import itertools import functools import logging import random import secrets -import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace from collections import Counter, deque from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, - Optional, Protocol, Set, Tuple, Union, Type) + Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING) from typing_extensions import NotRequired, TypedDict @@ -20,7 +18,7 @@ import Options import Utils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from worlds import AutoWorld @@ -231,7 +229,7 @@ def set_options(self, args: Namespace) -> None: for player in self.player_ids: world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) - options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass + options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] for option_key in options_dataclass.type_hints}) @@ -975,7 +973,7 @@ class Region: entrances: List[Entrance] exits: List[Entrance] locations: List[Location] - entrance_type: ClassVar[Type[Entrance]] = Entrance + entrance_type: ClassVar[type[Entrance]] = Entrance class Register(MutableSequence): region_manager: MultiWorld.RegionManager @@ -1075,7 +1073,7 @@ def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool]) return entrance.parent_region.get_connecting_entrance(is_main_entrance) def add_locations(self, locations: Dict[str, Optional[int]], - location_type: Optional[Type[Location]] = None) -> None: + location_type: Optional[type[Location]] = None) -> None: """ Adds locations to the Region object, where location_type is your Location class and locations is a dict of location names to address. diff --git a/ModuleUpdate.py b/ModuleUpdate.py index f49182bb7863..dada16cefcaf 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -5,8 +5,8 @@ import warnings -if sys.version_info < (3, 8, 6): - raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") +if sys.version_info < (3, 10, 11): + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.11+ is supported.") # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) _skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) diff --git a/Utils.py b/Utils.py index 2dfcd9d3e19a..535933d815b1 100644 --- a/Utils.py +++ b/Utils.py @@ -19,8 +19,7 @@ from argparse import Namespace from settings import Settings, get_settings from time import sleep -from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union -from typing_extensions import TypeGuard +from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard from yaml import load, load_all, dump try: @@ -48,7 +47,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.5.1" +__version__ = "0.6.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") diff --git a/WebHost.py b/WebHost.py index 3bf75eb35ae0..3790a5f6f4d2 100644 --- a/WebHost.py +++ b/WebHost.py @@ -17,7 +17,7 @@ if typing.TYPE_CHECKING: from flask import Flask -Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 +Utils.local_path.cached_path = os.path.dirname(__file__) settings.no_gui = True configpath = os.path.abspath("config.yaml") if not os.path.exists(configpath): # fall back to config.yaml in home diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 5c79415312d4..b7b14dea1e6f 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -5,9 +5,7 @@ waitress>=3.0.0 Flask-Caching>=2.3.0 Flask-Compress>=1.15 Flask-Limiter>=3.8.0 -bokeh>=3.1.1; python_version <= '3.8' -bokeh>=3.4.3; python_version == '3.9' -bokeh>=3.5.2; python_version >= '3.10' +bokeh>=3.5.2 markupsafe>=2.1.5 Markdown>=3.7 mdx-breakless-lists>=1.0.1 diff --git a/docs/contributing.md b/docs/contributing.md index 9fd21408eb7b..96fc316be82c 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -16,7 +16,7 @@ game contributions: * **Do not introduce unit test failures/regressions.** Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test your changes. Currently, the oldest supported version - is [Python 3.8](https://www.python.org/downloads/release/python-380/). + is [Python 3.10](https://www.python.org/downloads/release/python-31015/). It is recommended that automated github actions are turned on in your fork to have github run unit tests after pushing. You can turn them on here: diff --git a/docs/running from source.md b/docs/running from source.md index ef1594da9588..66dd1925c897 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -7,7 +7,7 @@ use that version. These steps are for developers or platforms without compiled r ## General What you'll need: - * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version + * [Python 3.10.15 or newer](https://www.python.org/downloads/), not the Windows Store version * Python 3.12.x is currently the newest supported version * pip: included in downloads from python.org, separate in many Linux distributions * Matching C compiler diff --git a/kvui.py b/kvui.py index 74d8ad06734a..2723654214c1 100644 --- a/kvui.py +++ b/kvui.py @@ -12,10 +12,7 @@ # kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout # by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's - try: - ctypes.windll.shcore.SetProcessDpiAwareness(0) - except FileNotFoundError: # shcore may not be found on <= Windows 7 - pass # TODO: remove silent except when Python 3.8 is phased out. + ctypes.windll.shcore.SetProcessDpiAwareness(0) os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1" diff --git a/setup.py b/setup.py index afbe17726df4..f075551d58b0 100644 --- a/setup.py +++ b/setup.py @@ -634,7 +634,7 @@ def find_lib(lib: str, arch: str, libc: str) -> Optional[str]: "excludes": ["numpy", "Cython", "PySide2", "PIL", "pandas", "zstandard"], "zip_include_packages": ["*"], - "zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support + "zip_exclude_packages": ["worlds", "sc2"], "include_files": [], # broken in cx 6.14.0, we use more special sauce now "include_msvcr": False, "replace_paths": ["*."], diff --git a/worlds/AutoSNIClient.py b/worlds/AutoSNIClient.py index b3f40be2958d..f9444eee73c6 100644 --- a/worlds/AutoSNIClient.py +++ b/worlds/AutoSNIClient.py @@ -2,9 +2,7 @@ from __future__ import annotations import abc import logging -from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union - -from typing_extensions import TypeGuard +from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components diff --git a/worlds/__init__.py b/worlds/__init__.py index c277ac9ca1de..7db651bdd9e3 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -66,19 +66,12 @@ def load(self) -> bool: start = time.perf_counter() if self.is_zip: importer = zipimport.zipimporter(self.resolved_path) - if hasattr(importer, "find_spec"): # new in Python 3.10 - spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0]) - assert spec, f"{self.path} is not a loadable module" - mod = importlib.util.module_from_spec(spec) - else: # TODO: remove with 3.8 support - mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0]) - - if mod.__package__ is not None: - mod.__package__ = f"worlds.{mod.__package__}" - else: - # load_module does not populate package, we'll have to assume mod.__name__ is correct here - # probably safe to remove with 3.8 support - mod.__package__ = f"worlds.{mod.__name__}" + spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0]) + assert spec, f"{self.path} is not a loadable module" + mod = importlib.util.module_from_spec(spec) + + mod.__package__ = f"worlds.{mod.__package__}" + mod.__name__ = f"worlds.{mod.__name__}" sys.modules[mod.__name__] = mod with warnings.catch_warnings(): diff --git a/worlds/hk/Extractor.py b/worlds/hk/Extractor.py index 61fabc4da0d9..866608489ec2 100644 --- a/worlds/hk/Extractor.py +++ b/worlds/hk/Extractor.py @@ -9,11 +9,7 @@ import jinja2 -try: - from ast import unparse -except ImportError: - # Py 3.8 and earlier compatibility module - from astunparse import unparse +from ast import unparse from Utils import get_text_between diff --git a/worlds/hk/requirements.txt b/worlds/hk/requirements.txt deleted file mode 100644 index 1b410ffb2aed..000000000000 --- a/worlds/hk/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -astunparse>=1.6.3; python_version <= '3.8' \ No newline at end of file diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 9a38953ffbdf..59e724d3fb7f 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,5 +1,5 @@ import logging -from typing import Any, ClassVar, Dict, List, Optional, Set, TextIO +from typing import Any, ClassVar, TextIO from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial from Options import Accessibility @@ -120,16 +120,16 @@ class MessengerWorld(World): required_seals: int = 0 created_seals: int = 0 total_shards: int = 0 - shop_prices: Dict[str, int] - figurine_prices: Dict[str, int] - _filler_items: List[str] - starting_portals: List[str] - plando_portals: List[str] - spoiler_portal_mapping: Dict[str, str] - portal_mapping: List[int] - transitions: List[Entrance] + shop_prices: dict[str, int] + figurine_prices: dict[str, int] + _filler_items: list[str] + starting_portals: list[str] + plando_portals: list[str] + spoiler_portal_mapping: dict[str, str] + portal_mapping: list[int] + transitions: list[Entrance] reachable_locs: int = 0 - filler: Dict[str, int] + filler: dict[str, int] def generate_early(self) -> None: if self.options.goal == Goal.option_power_seal_hunt: @@ -178,7 +178,7 @@ def create_regions(self) -> None: for reg_name in sub_region] for region in complex_regions: - region_name = region.name.replace(f"{region.parent} - ", "") + region_name = region.name.removeprefix(f"{region.parent} - ") connection_data = CONNECTIONS[region.parent][region_name] for exit_region in connection_data: region.connect(self.multiworld.get_region(exit_region, self.player)) @@ -191,7 +191,7 @@ def create_items(self) -> None: # create items that are always in the item pool main_movement_items = ["Rope Dart", "Wingsuit"] precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]] - itempool: List[MessengerItem] = [ + itempool: list[MessengerItem] = [ self.create_item(item) for item in self.item_name_to_id if item not in { @@ -290,7 +290,7 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: for portal, output in portal_info: spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player) - def fill_slot_data(self) -> Dict[str, Any]: + def fill_slot_data(self) -> dict[str, Any]: slot_data = { "shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}, "figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}, @@ -316,7 +316,7 @@ def get_filler_item_name(self) -> str: return self._filler_items.pop(0) def create_item(self, name: str) -> MessengerItem: - item_id: Optional[int] = self.item_name_to_id.get(name, None) + item_id: int | None = self.item_name_to_id.get(name, None) return MessengerItem( name, ItemClassification.progression if item_id is None else self.get_item_classification(name), @@ -351,7 +351,7 @@ def get_item_classification(self, name: str) -> ItemClassification: return ItemClassification.filler @classmethod - def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World: + def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World: group = super().create_group(multiworld, new_player_id, players) assert isinstance(group, MessengerWorld) diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 77a0f634326c..6b98a1b44013 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -5,7 +5,7 @@ import subprocess import urllib.request from shutil import which -from typing import Any, Optional +from typing import Any from zipfile import ZipFile from Utils import open_file @@ -17,7 +17,7 @@ MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" -def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: +def ask_yes_no_cancel(title: str, text: str) -> bool | None: """ Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons. @@ -33,7 +33,6 @@ def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: return ret - def launch_game(*args) -> None: """Check the game installation, then launch it""" def courier_installed() -> bool: diff --git a/worlds/messenger/connections.py b/worlds/messenger/connections.py index 69dd7aa7f286..79912a5688c2 100644 --- a/worlds/messenger/connections.py +++ b/worlds/messenger/connections.py @@ -1,6 +1,4 @@ -from typing import Dict, List - -CONNECTIONS: Dict[str, Dict[str, List[str]]] = { +CONNECTIONS: dict[str, dict[str, list[str]]] = { "Ninja Village": { "Right": [ "Autumn Hills - Left", @@ -640,7 +638,7 @@ }, } -RANDOMIZED_CONNECTIONS: Dict[str, str] = { +RANDOMIZED_CONNECTIONS: dict[str, str] = { "Ninja Village - Right": "Autumn Hills - Left", "Autumn Hills - Left": "Ninja Village - Right", "Autumn Hills - Right": "Forlorn Temple - Left", @@ -680,7 +678,7 @@ "Sunken Shrine - Left": "Howling Grotto - Bottom", } -TRANSITIONS: List[str] = [ +TRANSITIONS: list[str] = [ "Ninja Village - Right", "Autumn Hills - Left", "Autumn Hills - Right", diff --git a/worlds/messenger/constants.py b/worlds/messenger/constants.py index ea15c71068db..47b5a1a85cff 100644 --- a/worlds/messenger/constants.py +++ b/worlds/messenger/constants.py @@ -2,7 +2,7 @@ # items # listing individual groups first for easy lookup -NOTES = [ +NOTES: list[str] = [ "Key of Hope", "Key of Chaos", "Key of Courage", @@ -11,7 +11,7 @@ "Key of Symbiosis", ] -PROG_ITEMS = [ +PROG_ITEMS: list[str] = [ "Wingsuit", "Rope Dart", "Lightfoot Tabi", @@ -28,18 +28,18 @@ "Seashell", ] -PHOBEKINS = [ +PHOBEKINS: list[str] = [ "Necro", "Pyro", "Claustro", "Acro", ] -USEFUL_ITEMS = [ +USEFUL_ITEMS: list[str] = [ "Windmill Shuriken", ] -FILLER = { +FILLER: dict[str, int] = { "Time Shard": 5, "Time Shard (10)": 10, "Time Shard (50)": 20, @@ -48,13 +48,13 @@ "Time Shard (500)": 5, } -TRAPS = { +TRAPS: dict[str, int] = { "Teleport Trap": 5, "Prophecy Trap": 10, } # item_name_to_id needs to be deterministic and match upstream -ALL_ITEMS = [ +ALL_ITEMS: list[str] = [ *NOTES, "Windmill Shuriken", "Wingsuit", @@ -83,7 +83,7 @@ # locations # the names of these don't actually matter, but using the upstream's names for now # order must be exactly the same as upstream -ALWAYS_LOCATIONS = [ +ALWAYS_LOCATIONS: list[str] = [ # notes "Sunken Shrine - Key of Love", "Corrupted Future - Key of Courage", @@ -160,7 +160,7 @@ "Elemental Skylands Seal - Fire", ] -BOSS_LOCATIONS = [ +BOSS_LOCATIONS: list[str] = [ "Autumn Hills - Leaf Golem", "Catacombs - Ruxxtin", "Howling Grotto - Emerald Golem", diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 59e694cd3963..8b61a9435422 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Dict from schema import And, Optional, Or, Schema @@ -167,7 +166,7 @@ class ShopPrices(Range): default = 100 -def planned_price(location: str) -> Dict[Optional, Or]: +def planned_price(location: str) -> dict[Optional, Or]: return { Optional(location): Or( And(int, lambda n: n >= 0), diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 17152a1a1538..896fefa686f1 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -1,5 +1,5 @@ from copy import deepcopy -from typing import List, TYPE_CHECKING +from typing import TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions from Options import PlandoConnection @@ -8,7 +8,7 @@ from . import MessengerWorld -PORTALS = [ +PORTALS: list[str] = [ "Autumn Hills", "Riviere Turquoise", "Howling Grotto", @@ -18,7 +18,7 @@ ] -SHOP_POINTS = { +SHOP_POINTS: dict[str, list[str]] = { "Autumn Hills": [ "Climbing Claws", "Hope Path", @@ -113,7 +113,7 @@ } -CHECKPOINTS = { +CHECKPOINTS: dict[str, list[str]] = { "Autumn Hills": [ "Hope Latch", "Key of Hope", @@ -186,7 +186,7 @@ } -REGION_ORDER = [ +REGION_ORDER: list[str] = [ "Autumn Hills", "Forlorn Temple", "Catacombs", @@ -228,7 +228,7 @@ def create_mapping(in_portal: str, warp: str) -> str: return parent - def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: + def handle_planned_portals(plando_connections: list[PlandoConnection]) -> None: """checks the provided plando connections for portals and connects them""" nonlocal available_portals diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index 153f8510f1bd..d53b84fe3401 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -1,7 +1,4 @@ -from typing import Dict, List - - -LOCATIONS: Dict[str, List[str]] = { +LOCATIONS: dict[str, list[str]] = { "Ninja Village - Nest": [ "Ninja Village - Candle", "Ninja Village - Astral Seed", @@ -201,7 +198,7 @@ } -SUB_REGIONS: Dict[str, List[str]] = { +SUB_REGIONS: dict[str, list[str]] = { "Ninja Village": [ "Right", ], @@ -385,7 +382,7 @@ # order is slightly funky here for back compat -MEGA_SHARDS: Dict[str, List[str]] = { +MEGA_SHARDS: dict[str, list[str]] = { "Autumn Hills - Lakeside Checkpoint": ["Autumn Hills Mega Shard"], "Forlorn Temple - Outside Shop": ["Hidden Entrance Mega Shard"], "Catacombs - Top Left": ["Catacombs Mega Shard"], @@ -414,7 +411,7 @@ } -REGION_CONNECTIONS: Dict[str, Dict[str, str]] = { +REGION_CONNECTIONS: dict[str, dict[str, str]] = { "Menu": {"Tower HQ": "Start Game"}, "Tower HQ": { "Autumn Hills - Portal": "ToTHQ Autumn Hills Portal", @@ -436,7 +433,7 @@ # regions that don't have sub-regions -LEVELS: List[str] = [ +LEVELS: list[str] = [ "Menu", "Tower HQ", "The Shop", diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index c354ad70aba6..f09025c7edce 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,4 +1,4 @@ -from typing import Dict, TYPE_CHECKING +from typing import TYPE_CHECKING from BaseClasses import CollectionState from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items @@ -12,9 +12,9 @@ class MessengerRules: player: int world: "MessengerWorld" - connection_rules: Dict[str, CollectionRule] - region_rules: Dict[str, CollectionRule] - location_rules: Dict[str, CollectionRule] + connection_rules: dict[str, CollectionRule] + region_rules: dict[str, CollectionRule] + location_rules: dict[str, CollectionRule] maximum_price: int required_seals: int diff --git a/worlds/messenger/shop.py b/worlds/messenger/shop.py index 3c8c7bf6f21e..6ab72f9765f3 100644 --- a/worlds/messenger/shop.py +++ b/worlds/messenger/shop.py @@ -1,11 +1,11 @@ -from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union +from typing import NamedTuple, TYPE_CHECKING if TYPE_CHECKING: from . import MessengerWorld else: MessengerWorld = object -PROG_SHOP_ITEMS: List[str] = [ +PROG_SHOP_ITEMS: list[str] = [ "Path of Resilience", "Meditation", "Strike of the Ninja", @@ -14,7 +14,7 @@ "Aerobatics Warrior", ] -USEFUL_SHOP_ITEMS: List[str] = [ +USEFUL_SHOP_ITEMS: list[str] = [ "Karuta Plates", "Serendipitous Bodies", "Kusari Jacket", @@ -29,10 +29,10 @@ class ShopData(NamedTuple): internal_name: str min_price: int max_price: int - prerequisite: Optional[Union[str, Set[str]]] = None + prerequisite: str | set[str] | None = None -SHOP_ITEMS: Dict[str, ShopData] = { +SHOP_ITEMS: dict[str, ShopData] = { "Karuta Plates": ShopData("HP_UPGRADE_1", 20, 200), "Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"), "Path of Resilience": ShopData("DAMAGE_REDUCTION", 100, 500, "The Shop - Serendipitous Bodies"), @@ -56,7 +56,7 @@ class ShopData(NamedTuple): "Focused Power Sense": ShopData("POWER_SEAL_WORLD_MAP", 300, 600, "The Shop - Power Sense"), } -FIGURINES: Dict[str, ShopData] = { +FIGURINES: dict[str, ShopData] = { "Green Kappa Figurine": ShopData("GREEN_KAPPA", 100, 500), "Blue Kappa Figurine": ShopData("BLUE_KAPPA", 100, 500), "Ountarde Figurine": ShopData("OUNTARDE", 100, 500), @@ -73,12 +73,12 @@ class ShopData(NamedTuple): } -def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]: +def shuffle_shop_prices(world: MessengerWorld) -> tuple[dict[str, int], dict[str, int]]: shop_price_mod = world.options.shop_price.value shop_price_planned = world.options.shop_price_plan - shop_prices: Dict[str, int] = {} - figurine_prices: Dict[str, int] = {} + shop_prices: dict[str, int] = {} + figurine_prices: dict[str, int] = {} for item, price in shop_price_planned.value.items(): if not isinstance(price, int): price = world.random.choices(list(price.keys()), weights=list(price.values()))[0] diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index b60aeb179feb..29e3ea8953ec 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -1,5 +1,5 @@ from functools import cached_property -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region from .regions import LOCATIONS, MEGA_SHARDS @@ -10,14 +10,14 @@ class MessengerEntrance(Entrance): - world: Optional["MessengerWorld"] = None + world: "MessengerWorld | None" = None class MessengerRegion(Region): parent: str entrance_type = MessengerEntrance - def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = None) -> None: + def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None: super().__init__(name, world.player, world.multiworld) self.parent = parent locations = [] @@ -48,7 +48,7 @@ def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = N class MessengerLocation(Location): game = "The Messenger" - def __init__(self, player: int, name: str, loc_id: Optional[int], parent: MessengerRegion) -> None: + def __init__(self, player: int, name: str, loc_id: int | None, parent: MessengerRegion) -> None: super().__init__(player, name, loc_id, parent) if loc_id is None: if name == "Rescue Phantom": @@ -59,7 +59,7 @@ def __init__(self, player: int, name: str, loc_id: Optional[int], parent: Messen class MessengerShopLocation(MessengerLocation): @cached_property def cost(self) -> int: - name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped + name = self.name.removeprefix("The Shop - ") world = self.parent_region.multiworld.worlds[self.player] shop_data = SHOP_ITEMS[name] if shop_data.prerequisite: diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index ce6fd19e33c8..21a0c352bff4 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -77,7 +77,7 @@ def test_costs(self) -> None: loc = f"The Shop - {loc}" self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost) - self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS) + self.assertTrue(loc.removeprefix("The Shop - ") in SHOP_ITEMS) self.assertEqual(len(prices), len(SHOP_ITEMS)) figures = self.world.figurine_prices diff --git a/worlds/stardew_valley/content/unpacking.py b/worlds/stardew_valley/content/unpacking.py index f069866d56cd..3c57f91afe3a 100644 --- a/worlds/stardew_valley/content/unpacking.py +++ b/worlds/stardew_valley/content/unpacking.py @@ -1,16 +1,12 @@ from __future__ import annotations +from graphlib import TopologicalSorter from typing import Iterable, Mapping, Callable from .game_content import StardewContent, ContentPack, StardewFeatures from .vanilla.base import base_game as base_game_content_pack from ..data.game_item import GameItem, ItemSource -try: - from graphlib import TopologicalSorter -except ImportError: - from graphlib_backport import TopologicalSorter # noqa - def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent: # Base game is always registered first. diff --git a/worlds/stardew_valley/data/artisan.py b/worlds/stardew_valley/data/artisan.py index 593ab6a3ddf0..90be5b1684f0 100644 --- a/worlds/stardew_valley/data/artisan.py +++ b/worlds/stardew_valley/data/artisan.py @@ -1,9 +1,9 @@ from dataclasses import dataclass -from .game_item import kw_only, ItemSource +from .game_item import ItemSource -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class MachineSource(ItemSource): item: str # this should be optional (worm bin) machine: str diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py index 6c8d30ed8e6f..c6e4717cd1e0 100644 --- a/worlds/stardew_valley/data/game_item.py +++ b/worlds/stardew_valley/data/game_item.py @@ -1,5 +1,4 @@ import enum -import sys from abc import ABC from dataclasses import dataclass, field from types import MappingProxyType @@ -7,11 +6,6 @@ from ..stardew_rule.protocol import StardewRule -if sys.version_info >= (3, 10): - kw_only = {"kw_only": True} -else: - kw_only = {} - DEFAULT_REQUIREMENT_TAGS = MappingProxyType({}) @@ -36,21 +30,17 @@ class ItemTag(enum.Enum): class ItemSource(ABC): add_tags: ClassVar[Tuple[ItemTag]] = () + other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple) + @property def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: return DEFAULT_REQUIREMENT_TAGS - # FIXME this should just be an optional field, but kw_only requires python 3.10... - @property - def other_requirements(self) -> Iterable[Requirement]: - return () - -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class GenericSource(ItemSource): regions: Tuple[str, ...] = () """No region means it's available everywhere.""" - other_requirements: Tuple[Requirement, ...] = () @dataclass(frozen=True) @@ -59,7 +49,7 @@ class CustomRuleSource(ItemSource): create_rule: Callable[[Any], StardewRule] -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class CompoundSource(ItemSource): sources: Tuple[ItemSource, ...] = () diff --git a/worlds/stardew_valley/data/harvest.py b/worlds/stardew_valley/data/harvest.py index 087d7c3fa86b..0fdae9549587 100644 --- a/worlds/stardew_valley/data/harvest.py +++ b/worlds/stardew_valley/data/harvest.py @@ -1,18 +1,17 @@ from dataclasses import dataclass from typing import Tuple, Sequence, Mapping -from .game_item import ItemSource, kw_only, ItemTag, Requirement +from .game_item import ItemSource, ItemTag from ..strings.season_names import Season -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class ForagingSource(ItemSource): regions: Tuple[str, ...] seasons: Tuple[str, ...] = Season.all - other_requirements: Tuple[Requirement, ...] = () -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class SeasonalForagingSource(ItemSource): season: str days: Sequence[int] @@ -22,17 +21,17 @@ def as_foraging_source(self) -> ForagingSource: return ForagingSource(seasons=(self.season,), regions=self.regions) -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class FruitBatsSource(ItemSource): ... -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class MushroomCaveSource(ItemSource): ... -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class HarvestFruitTreeSource(ItemSource): add_tags = (ItemTag.CROPSANITY,) @@ -46,7 +45,7 @@ def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: } -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class HarvestCropSource(ItemSource): add_tags = (ItemTag.CROPSANITY,) @@ -61,6 +60,6 @@ def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: } -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class ArtifactSpotSource(ItemSource): amount: int diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py index f14dbac82131..cc9506023f19 100644 --- a/worlds/stardew_valley/data/shop.py +++ b/worlds/stardew_valley/data/shop.py @@ -1,40 +1,39 @@ from dataclasses import dataclass from typing import Tuple, Optional -from .game_item import ItemSource, kw_only, Requirement +from .game_item import ItemSource from ..strings.season_names import Season ItemPrice = Tuple[int, str] -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class ShopSource(ItemSource): shop_region: str money_price: Optional[int] = None items_price: Optional[Tuple[ItemPrice, ...]] = None seasons: Tuple[str, ...] = Season.all - other_requirements: Tuple[Requirement, ...] = () def __post_init__(self): assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined." assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple." -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class MysteryBoxSource(ItemSource): amount: int -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class ArtifactTroveSource(ItemSource): amount: int -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class PrizeMachineSource(ItemSource): amount: int -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class FishingTreasureChestSource(ItemSource): amount: int diff --git a/worlds/stardew_valley/data/skill.py b/worlds/stardew_valley/data/skill.py index d0674f34c0e1..4c754ddd8716 100644 --- a/worlds/stardew_valley/data/skill.py +++ b/worlds/stardew_valley/data/skill.py @@ -1,9 +1,7 @@ from dataclasses import dataclass, field -from ..data.game_item import kw_only - @dataclass(frozen=True) class Skill: name: str - has_mastery: bool = field(**kw_only) + has_mastery: bool = field(kw_only=True) diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 730cb24ce6e3..993863bf5bf5 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -125,10 +125,7 @@ def __call__(self, item: Item): def load_item_csv(): - try: - from importlib.resources import files - except ImportError: - from importlib_resources import files # noqa + from importlib.resources import files items = [] with files(data).joinpath("items.csv").open() as file: diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 43246a94a356..1d67d535ccee 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -130,10 +130,7 @@ def __call__(self, name: str, code: Optional[int], region: str) -> None: def load_location_csv() -> List[LocationData]: - try: - from importlib.resources import files - except ImportError: - from importlib_resources import files + from importlib.resources import files with files(data).joinpath("locations.csv").open() as file: reader = csv.DictReader(file) diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt deleted file mode 100644 index 65e922a64483..000000000000 --- a/worlds/stardew_valley/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -importlib_resources; python_version <= '3.8' -graphlib_backport; python_version <= '3.8' diff --git a/worlds/stardew_valley/test/stability/TestStability.py b/worlds/stardew_valley/test/stability/TestStability.py index 8bb904a56ea2..137a7172aff4 100644 --- a/worlds/stardew_valley/test/stability/TestStability.py +++ b/worlds/stardew_valley/test/stability/TestStability.py @@ -12,8 +12,6 @@ # at 0x102ca98a0> lambda_regex = re.compile(r"^ at (.*)>$") -# Python 3.10.2\r\n -python_version_regex = re.compile(r"^Python (\d+)\.(\d+)\.(\d+)\s*$") class TestGenerationIsStable(SVTestCase): diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 8df612654db4..ec0fdb0b22e1 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,7 +1,6 @@ from collections import Counter from dataclasses import dataclass -from typing import ClassVar, Dict, Literal, Tuple -from typing_extensions import TypeGuard # remove when Python >= 3.10 +from typing import ClassVar, Dict, Literal, Tuple, TypeGuard from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle From e1f16c672183122dcbd40e60ae7b4c4f79096c4f Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:19:52 +0100 Subject: [PATCH 60/60] WebHost: Fix crash on advanced options when a Range option used "random" as its default (#4263) --- WebHostLib/templates/weightedOptions/macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index 68d3968a178a..d18d0f0b8957 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -53,7 +53,7 @@ {{ RangeRow(option_name, option, option.range_start, option.range_start, True) }} - {% if option.range_start < option.default < option.range_end %} + {% if option.default is number and option.range_start < option.default < option.range_end %} {{ RangeRow(option_name, option, option.default, option.default, True) }} {% endif %} {{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}