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 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/BaseClasses.py b/BaseClasses.py index 53b69d30e2f9..46edeb5ea059 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1264,6 +1264,10 @@ def useful(self) -> bool: def trap(self) -> bool: return ItemClassification.trap in self.classification + @property + def excludable(self) -> bool: + return not (self.advancement or self.useful) + @property def flags(self) -> int: return self.classification.as_flag() diff --git a/Generate.py b/Generate.py index 52babdf18839..bc359a203da7 100644 --- a/Generate.py +++ b/Generate.py @@ -110,7 +110,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: player_files = {} for file in os.scandir(args.player_files_path): fname = file.name - if file.is_file() and not fname.startswith(".") and \ + if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \ os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}: path = os.path.join(args.player_files_path, fname) try: diff --git a/Launcher.py b/Launcher.py index 84db298b4ad2..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(): @@ -104,6 +103,7 @@ def update_settings(): Component("Open host.yaml", func=open_host_yaml), Component("Open Patch", func=open_patch), Component("Generate Template Options", func=generate_yamls), + Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")), Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), Component("Browse Files", func=browse_files), 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/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/SNIClient.py b/SNIClient.py index 222ed54f5cc5..19440e1dc5be 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -633,7 +633,13 @@ async def game_watcher(ctx: SNIContext) -> None: if not ctx.client_handler: continue - rom_validated = await ctx.client_handler.validate_rom(ctx) + try: + rom_validated = await ctx.client_handler.validate_rom(ctx) + except Exception as e: + snes_logger.error(f"An error occurred, see logs for details: {e}") + text_file_logger = logging.getLogger() + text_file_logger.exception(e) + rom_validated = False if not rom_validated or (ctx.auth and ctx.auth != ctx.rom): snes_logger.warning("ROM change detected, please reconnect to the multiworld server") @@ -649,7 +655,13 @@ async def game_watcher(ctx: SNIContext) -> None: perf_counter = time.perf_counter() - await ctx.client_handler.game_watcher(ctx) + try: + await ctx.client_handler.game_watcher(ctx) + except Exception as e: + snes_logger.error(f"An error occurred, see logs for details: {e}") + text_file_logger = logging.getLogger() + text_file_logger.exception(e) + await snes_disconnect(ctx) async def run_game(romfile: str) -> None: 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/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 diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.png new file mode 100644 index 000000000000..537e27979180 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.webp new file mode 100644 index 000000000000..f34cd5ff2ec1 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png index 2435222d24f2..a0b41b0f8cac 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.webp new file mode 100644 index 000000000000..4a5f2d75a0d4 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png index 3dcc6d1534e0..6e1608d82b7f 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.webp new file mode 100644 index 000000000000..30bd2d047a76 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png index 7e58e4b0ce48..3d3e089ef79f 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.webp new file mode 100644 index 000000000000..f575ac5d9d48 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png index 2dabbf298678..08730d98489c 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.webp new file mode 100644 index 000000000000..f9227e8f2286 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png index afbf0dafb59f..0bc82fa70e9b 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.webp new file mode 100644 index 000000000000..3c0a57740263 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png index 2b5865573ad8..05e675d6a97c 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.webp new file mode 100644 index 000000000000..4283cd42b16a Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png index aeccd805f0fc..e0683a74bba5 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.webp new file mode 100644 index 000000000000..3075cec96add Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png index 1eb072ac0858..cded7ad108d3 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.webp new file mode 100644 index 000000000000..781b8e4df0d0 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.webp differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0001.webp b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.webp new file mode 100644 index 000000000000..73e249f6e530 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.webp differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0002.webp b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.webp new file mode 100644 index 000000000000..e4ac19bef687 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.webp differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp new file mode 100644 index 000000000000..36abe6e552a3 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp differ diff --git a/WebHostLib/static/static/backgrounds/dirt.webp b/WebHostLib/static/static/backgrounds/dirt.webp new file mode 100644 index 000000000000..5a8635506f9f Binary files /dev/null and b/WebHostLib/static/static/backgrounds/dirt.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0001.png b/WebHostLib/static/static/backgrounds/footer/footer-0001.png index d15afe5f9ef2..6752ab4e3279 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0001.png and b/WebHostLib/static/static/backgrounds/footer/footer-0001.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0001.webp b/WebHostLib/static/static/backgrounds/footer/footer-0001.webp new file mode 100644 index 000000000000..fb278c3b1643 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0001.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0002.png b/WebHostLib/static/static/backgrounds/footer/footer-0002.png index 88d0659f691e..3bacab4134e2 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0002.png and b/WebHostLib/static/static/backgrounds/footer/footer-0002.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0002.webp b/WebHostLib/static/static/backgrounds/footer/footer-0002.webp new file mode 100644 index 000000000000..9b8e457c52a9 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0002.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0003.png b/WebHostLib/static/static/backgrounds/footer/footer-0003.png index 9c4d9a4632b0..f8223e690171 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0003.png and b/WebHostLib/static/static/backgrounds/footer/footer-0003.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0003.webp b/WebHostLib/static/static/backgrounds/footer/footer-0003.webp new file mode 100644 index 000000000000..c2ded77536d6 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0003.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0004.png b/WebHostLib/static/static/backgrounds/footer/footer-0004.png index 46519036ebd6..d4476e53f759 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0004.png and b/WebHostLib/static/static/backgrounds/footer/footer-0004.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0004.webp b/WebHostLib/static/static/backgrounds/footer/footer-0004.webp new file mode 100644 index 000000000000..a2100817461a Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0004.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0005.png b/WebHostLib/static/static/backgrounds/footer/footer-0005.png index 58093f7efcf2..794615962454 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0005.png and b/WebHostLib/static/static/backgrounds/footer/footer-0005.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0005.webp b/WebHostLib/static/static/backgrounds/footer/footer-0005.webp new file mode 100644 index 000000000000..c0ee5205ca22 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0005.webp differ diff --git a/WebHostLib/static/static/backgrounds/grass-flowers.webp b/WebHostLib/static/static/backgrounds/grass-flowers.webp new file mode 100644 index 000000000000..1b8ebd7706ad Binary files /dev/null and b/WebHostLib/static/static/backgrounds/grass-flowers.webp differ diff --git a/WebHostLib/static/static/backgrounds/grass.webp b/WebHostLib/static/static/backgrounds/grass.webp new file mode 100644 index 000000000000..212ab377a624 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/grass.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/dirt-header.png b/WebHostLib/static/static/backgrounds/header/dirt-header.png index 4d478ec1aa34..8a9c0963e72f 100644 Binary files a/WebHostLib/static/static/backgrounds/header/dirt-header.png and b/WebHostLib/static/static/backgrounds/header/dirt-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/dirt-header.webp b/WebHostLib/static/static/backgrounds/header/dirt-header.webp new file mode 100644 index 000000000000..6c2b0bd8bf9b Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/dirt-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/grass-header.png b/WebHostLib/static/static/backgrounds/header/grass-header.png index 602d52d32191..6d620e5033a2 100644 Binary files a/WebHostLib/static/static/backgrounds/header/grass-header.png and b/WebHostLib/static/static/backgrounds/header/grass-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/grass-header.webp b/WebHostLib/static/static/backgrounds/header/grass-header.webp new file mode 100644 index 000000000000..ca5d1e23bc2b Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/grass-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/ocean-header.png b/WebHostLib/static/static/backgrounds/header/ocean-header.png index 10d8345af1e1..1e1c18e93c65 100644 Binary files a/WebHostLib/static/static/backgrounds/header/ocean-header.png and b/WebHostLib/static/static/backgrounds/header/ocean-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/ocean-header.webp b/WebHostLib/static/static/backgrounds/header/ocean-header.webp new file mode 100644 index 000000000000..fc1803ca0e4b Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/ocean-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/party-time-header.png b/WebHostLib/static/static/backgrounds/header/party-time-header.png index b033486b2aea..601ad829f1fa 100644 Binary files a/WebHostLib/static/static/backgrounds/header/party-time-header.png and b/WebHostLib/static/static/backgrounds/header/party-time-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/party-time-header.webp b/WebHostLib/static/static/backgrounds/header/party-time-header.webp new file mode 100644 index 000000000000..0b3c70871ada Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/party-time-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/stone-header.png b/WebHostLib/static/static/backgrounds/header/stone-header.png index 6e93a54b7885..f0d2f2fee56e 100644 Binary files a/WebHostLib/static/static/backgrounds/header/stone-header.png and b/WebHostLib/static/static/backgrounds/header/stone-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/stone-header.webp b/WebHostLib/static/static/backgrounds/header/stone-header.webp new file mode 100644 index 000000000000..9f26d1a505d4 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/stone-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/ice.webp b/WebHostLib/static/static/backgrounds/ice.webp new file mode 100644 index 000000000000..a129d5f439c6 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/ice.webp differ diff --git a/WebHostLib/static/static/backgrounds/jungle.webp b/WebHostLib/static/static/backgrounds/jungle.webp new file mode 100644 index 000000000000..d21edc8e55f2 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/jungle.webp differ diff --git a/WebHostLib/static/static/backgrounds/ocean.png b/WebHostLib/static/static/backgrounds/ocean.png index e4dbdecb7368..d6c9d285c963 100644 Binary files a/WebHostLib/static/static/backgrounds/ocean.png and b/WebHostLib/static/static/backgrounds/ocean.png differ diff --git a/WebHostLib/static/static/backgrounds/ocean.webp b/WebHostLib/static/static/backgrounds/ocean.webp new file mode 100644 index 000000000000..a50b7b27f743 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/ocean.webp differ diff --git a/WebHostLib/static/static/backgrounds/party-time.webp b/WebHostLib/static/static/backgrounds/party-time.webp new file mode 100644 index 000000000000..7cd547329a40 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/party-time.webp differ diff --git a/WebHostLib/static/static/backgrounds/stone.webp b/WebHostLib/static/static/backgrounds/stone.webp new file mode 100644 index 000000000000..96303c816227 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/stone.webp differ diff --git a/WebHostLib/static/static/branding/header-logo.webp b/WebHostLib/static/static/branding/header-logo.webp new file mode 100644 index 000000000000..c8088e826266 Binary files /dev/null and b/WebHostLib/static/static/branding/header-logo.webp differ diff --git a/WebHostLib/static/static/branding/landing-logo.webp b/WebHostLib/static/static/branding/landing-logo.webp new file mode 100644 index 000000000000..7bd4673e99e0 Binary files /dev/null and b/WebHostLib/static/static/branding/landing-logo.webp differ diff --git a/WebHostLib/static/static/button-images/hamburger-menu-icon.webp b/WebHostLib/static/static/button-images/hamburger-menu-icon.webp new file mode 100644 index 000000000000..970754d7bfc8 Binary files /dev/null and b/WebHostLib/static/static/button-images/hamburger-menu-icon.webp differ diff --git a/WebHostLib/static/static/button-images/island-button-a.png b/WebHostLib/static/static/button-images/island-button-a.png index 1ce420287f36..552e4d8f6d34 100644 Binary files a/WebHostLib/static/static/button-images/island-button-a.png and b/WebHostLib/static/static/button-images/island-button-a.png differ diff --git a/WebHostLib/static/static/button-images/island-button-a.webp b/WebHostLib/static/static/button-images/island-button-a.webp new file mode 100644 index 000000000000..6da0c1720030 Binary files /dev/null and b/WebHostLib/static/static/button-images/island-button-a.webp differ diff --git a/WebHostLib/static/static/button-images/island-button-b.png b/WebHostLib/static/static/button-images/island-button-b.png index 0df9bc1a495b..fd4a256c7c9f 100644 Binary files a/WebHostLib/static/static/button-images/island-button-b.png and b/WebHostLib/static/static/button-images/island-button-b.png differ diff --git a/WebHostLib/static/static/button-images/island-button-b.webp b/WebHostLib/static/static/button-images/island-button-b.webp new file mode 100644 index 000000000000..6b7c3a279ed0 Binary files /dev/null and b/WebHostLib/static/static/button-images/island-button-b.webp differ diff --git a/WebHostLib/static/static/button-images/island-button-c.png b/WebHostLib/static/static/button-images/island-button-c.png index 86182c362738..2f10f45828c4 100644 Binary files a/WebHostLib/static/static/button-images/island-button-c.png and b/WebHostLib/static/static/button-images/island-button-c.png differ diff --git a/WebHostLib/static/static/button-images/island-button-c.webp b/WebHostLib/static/static/button-images/island-button-c.webp new file mode 100644 index 000000000000..83ce413da807 Binary files /dev/null and b/WebHostLib/static/static/button-images/island-button-c.webp differ diff --git a/WebHostLib/static/static/button-images/popover.webp b/WebHostLib/static/static/button-images/popover.webp new file mode 100644 index 000000000000..cd1c006221b0 Binary files /dev/null and b/WebHostLib/static/static/button-images/popover.webp differ diff --git a/WebHostLib/static/static/decorations/island-a.png b/WebHostLib/static/static/decorations/island-a.png index 9972a915f5ff..4f5d7c264198 100644 Binary files a/WebHostLib/static/static/decorations/island-a.png and b/WebHostLib/static/static/decorations/island-a.png differ diff --git a/WebHostLib/static/static/decorations/island-a.webp b/WebHostLib/static/static/decorations/island-a.webp new file mode 100644 index 000000000000..32c9cc8f6bd6 Binary files /dev/null and b/WebHostLib/static/static/decorations/island-a.webp differ diff --git a/WebHostLib/static/static/decorations/island-b.png b/WebHostLib/static/static/decorations/island-b.png index 04eff72f0573..cceb79af33b0 100644 Binary files a/WebHostLib/static/static/decorations/island-b.png and b/WebHostLib/static/static/decorations/island-b.png differ diff --git a/WebHostLib/static/static/decorations/island-b.webp b/WebHostLib/static/static/decorations/island-b.webp new file mode 100644 index 000000000000..3ec6aae438ba Binary files /dev/null and b/WebHostLib/static/static/decorations/island-b.webp differ diff --git a/WebHostLib/static/static/decorations/island-c.png b/WebHostLib/static/static/decorations/island-c.png index e491d7b2c69b..2beedce19d26 100644 Binary files a/WebHostLib/static/static/decorations/island-c.png and b/WebHostLib/static/static/decorations/island-c.png differ diff --git a/WebHostLib/static/static/decorations/island-c.webp b/WebHostLib/static/static/decorations/island-c.webp new file mode 100644 index 000000000000..98e1add91ee3 Binary files /dev/null and b/WebHostLib/static/static/decorations/island-c.webp differ diff --git a/WebHostLib/static/static/decorations/rock-in-water.webp b/WebHostLib/static/static/decorations/rock-in-water.webp new file mode 100644 index 000000000000..2c8af460d5e2 Binary files /dev/null and b/WebHostLib/static/static/decorations/rock-in-water.webp differ diff --git a/WebHostLib/static/static/decorations/rock-single.webp b/WebHostLib/static/static/decorations/rock-single.webp new file mode 100644 index 000000000000..e53a2fb5c480 Binary files /dev/null and b/WebHostLib/static/static/decorations/rock-single.webp differ 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/WebHostLib/tracker.py b/WebHostLib/tracker.py index 75b5fb0202d9..5450ef510373 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -5,7 +5,7 @@ from uuid import UUID from email.utils import parsedate_to_datetime -from flask import render_template, make_response, Response, request +from flask import make_response, render_template, request, Request, Response from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second @@ -298,17 +298,25 @@ def get_spheres(self) -> List[List[int]]: return self._multidata.get("spheres", []) -def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]: +def _process_if_request_valid(incoming_request: Request, room: Optional[Room]) -> Optional[Response]: if not room: abort(404) - if_modified = incoming_request.headers.get("If-Modified-Since", None) - if if_modified: - if_modified = parsedate_to_datetime(if_modified) + if_modified_str: Optional[str] = incoming_request.headers.get("If-Modified-Since", None) + if if_modified_str: + if_modified = parsedate_to_datetime(if_modified_str) + if if_modified.tzinfo is None: + abort(400) # standard requires "GMT" timezone + # database may use datetime.utcnow(), which is timezone-naive. convert to timezone-aware. + last_activity = room.last_activity + if last_activity.tzinfo is None: + last_activity = room.last_activity.replace(tzinfo=datetime.timezone.utc) # if_modified has less precision than last_activity, so we bring them to same precision - if if_modified >= room.last_activity.replace(microsecond=0): + if if_modified >= last_activity.replace(microsecond=0): return make_response("", 304) + return None + @app.route("/tracker///") def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response: 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/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 diff --git a/docs/running from source.md b/docs/running from source.md index a161265fcb74..ef1594da9588 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -85,4 +85,4 @@ PyCharm has a built-in version control integration that supports Git. ## Running tests -Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder. +Information about running tests can be found in [tests.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/tests.md#running-tests) diff --git a/docs/tests.md b/docs/tests.md index 7a3531f0f84f..c8655ccf3f4d 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -84,7 +84,19 @@ testing portions of your code that can be tested without relying on a multiworld ## Running Tests -In PyCharm, running all tests can be done by right-clicking the root `test` directory and selecting `run Python tests`. -If you do not have pytest installed, you may get import failures. To solve this, edit the run configuration, and set the -working directory of the run to the Archipelago directory. If you only want to run your world's defined tests, repeat -the steps for the test directory within your world. +#### Using Pycharm + +In PyCharm, running all tests can be done by right-clicking the root test directory and selecting Run 'Archipelago Unittests'. +Unless you configured PyCharm to use pytest as a test runner, you may get import failures. To solve this, edit the run configuration, +and set the working directory to the Archipelago directory which contains all the project files. + +If you only want to run your world's defined tests, repeat the steps for the test directory within your world. +Your working directory should be the directory of your world in the worlds directory and the script should be the +tests folder within your world. + +You can also find the 'Archipelago Unittests' as an option in the dropdown at the top of the window +next to the run and debug buttons. + +#### Running Tests without Pycharm + +Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder. 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 diff --git a/setup.py b/setup.py index 4d7fc3942017..afbe17726df4 100644 --- a/setup.py +++ b/setup.py @@ -632,7 +632,7 @@ def find_lib(lib: str, arch: str, libc: str) -> Optional[str]: "packages": ["worlds", "kivy", "cymem", "websockets"], "includes": [], "excludes": ["numpy", "Cython", "PySide2", "PIL", - "pandas"], + "pandas", "zstandard"], "zip_include_packages": ["*"], "zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support "include_files": [], # broken in cx 6.14.0, we use more special sauce now 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 diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 2dba147aca84..c8bcec9581ac 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -688,8 +688,8 @@ def test_non_excluded_local_items(self): for item in multiworld.get_items(): item.classification = ItemClassification.useful - multiworld.local_items[player1.id].value = set(names(player1.basic_items)) - multiworld.local_items[player2.id].value = set(names(player2.basic_items)) + multiworld.worlds[player1.id].options.local_items.value = set(names(player1.basic_items)) + multiworld.worlds[player2.id].options.local_items.value = set(names(player2.basic_items)) locality_rules(multiworld) distribute_items_restrictive(multiworld) @@ -795,8 +795,8 @@ def setUp(self) -> None: def test_balances_progression(self) -> None: """Tests that progression balancing moves progression items earlier""" - self.multiworld.progression_balancing[self.player1.id].value = 50 - self.multiworld.progression_balancing[self.player2.id].value = 50 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 50 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 50 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) @@ -808,8 +808,8 @@ def test_balances_progression(self) -> None: def test_balances_progression_light(self) -> None: """Test that progression balancing still moves items earlier on minimum value""" - self.multiworld.progression_balancing[self.player1.id].value = 1 - self.multiworld.progression_balancing[self.player2.id].value = 1 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 1 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 1 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) @@ -822,8 +822,8 @@ def test_balances_progression_light(self) -> None: def test_balances_progression_heavy(self) -> None: """Test that progression balancing moves items earlier on maximum value""" - self.multiworld.progression_balancing[self.player1.id].value = 99 - self.multiworld.progression_balancing[self.player2.id].value = 99 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 99 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 99 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) @@ -836,8 +836,8 @@ def test_balances_progression_heavy(self) -> None: def test_skips_balancing_progression(self) -> None: """Test that progression balancing is skipped when players have it disabled""" - self.multiworld.progression_balancing[self.player1.id].value = 0 - self.multiworld.progression_balancing[self.player2.id].value = 0 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 0 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 0 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) @@ -849,8 +849,8 @@ def test_skips_balancing_progression(self) -> None: def test_ignores_priority_locations(self) -> None: """Test that progression items on priority locations don't get moved by balancing""" - self.multiworld.progression_balancing[self.player1.id].value = 50 - self.multiworld.progression_balancing[self.player2.id].value = 50 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 50 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 50 self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY diff --git a/test/general/test_options.py b/test/general/test_options.py index ee2f22a6dc71..d6d5ce6da06b 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -21,6 +21,17 @@ def test_options_are_not_set_by_world(self): self.assertFalse(hasattr(world_type, "options"), f"Unexpected assignment to {world_type.__name__}.options!") + def test_duplicate_options(self) -> None: + """Tests that a world doesn't reuse the same option class.""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=game_name): + seen_options = set() + for option in world_type.options_dataclass.type_hints.values(): + if not option.visibility: + continue + self.assertFalse(option in seen_options, f"{option} found in assigned options multiple times.") + seen_options.add(option) + def test_item_links_name_groups(self): """Tests that item links successfully unfold item_name_groups""" item_link_groups = [ 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) diff --git a/test/programs/data/weights/weights.yaml b/test/programs/data/weights/weights.yaml new file mode 100644 index 000000000000..1e3c65d8f9c1 --- /dev/null +++ b/test/programs/data/weights/weights.yaml @@ -0,0 +1,10 @@ +name: Player{number} +game: Archipelago # we only need to test options work and this "supports" all the base options +Archipelago: + progression_balancing: + 0: 50 + 50: 50 + 99: 50 + accessibility: + 0: 50 + 2: 50 diff --git a/test/programs/test_generate.py b/test/programs/test_generate.py index 9281c9c753cd..51800a0ec5c2 100644 --- a/test/programs/test_generate.py +++ b/test/programs/test_generate.py @@ -92,3 +92,48 @@ def test_generate_yaml(self): user_path.cached_path = user_path_backup self.assertOutput(self.output_tempdir.name) + + +class TestGenerateWeights(TestGenerateMain): + """Tests Generate.py using a weighted file to generate for multiple players.""" + + # this test will probably break if something in generation is changed that affects the seed before the weights get processed + # can be fixed by changing the expected_results dict + generate_dir = TestGenerateMain.generate_dir + run_dir = TestGenerateMain.run_dir + abs_input_dir = Path(__file__).parent / "data" / "weights" + rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd + yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path + + # don't need to run these tests + test_generate_absolute = None + test_generate_relative = None + + def test_generate_yaml(self): + from settings import get_settings + from Utils import user_path, local_path + settings = get_settings() + settings.generator.player_files_path = settings.generator.PlayerFilesPath(self.yaml_input_dir) + settings.generator.players = 5 # arbitrary number, should be enough + settings._filename = None + user_path_backup = user_path.cached_path + user_path.cached_path = local_path() + try: + sys.argv = [sys.argv[0], "--seed", "1"] + namespace, seed = Generate.main() + finally: + user_path.cached_path = user_path_backup + + # there's likely a better way to do this, but hardcode the results from seed 1 to ensure they're always this + expected_results = { + "accessibility": [0, 2, 0, 2, 2], + "progression_balancing": [0, 50, 99, 0, 50], + } + + self.assertEqual(seed, 1) + for option_name, results in expected_results.items(): + for player, result in enumerate(results, 1): + self.assertEqual( + result, getattr(namespace, option_name)[player].value, + "Generated results from weights file did not match expected value." + ) diff --git a/test/webhost/data/One_Archipelago.archipelago b/test/webhost/data/One_Archipelago.archipelago new file mode 100644 index 000000000000..8b7a8ce0a8a1 Binary files /dev/null and b/test/webhost/data/One_Archipelago.archipelago differ 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.' ) diff --git a/test/webhost/test_tracker.py b/test/webhost/test_tracker.py new file mode 100644 index 000000000000..58145d77f3bc --- /dev/null +++ b/test/webhost/test_tracker.py @@ -0,0 +1,95 @@ +import os +import pickle +from pathlib import Path +from typing import ClassVar +from uuid import UUID, uuid4 + +from flask import url_for + +from . import TestBase + + +class TestTracker(TestBase): + room_id: UUID + tracker_uuid: UUID + log_filename: str + data: ClassVar[bytes] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + with (Path(__file__).parent / "data" / "One_Archipelago.archipelago").open("rb") as f: + cls.data = f.read() + + def setUp(self) -> None: + from pony.orm import db_session + from MultiServer import Context as MultiServerContext + from Utils import user_path + from WebHostLib.models import GameDataPackage, Room, Seed + + super().setUp() + + multidata = MultiServerContext.decompress(self.data) + + with self.client.session_transaction() as session: + session["_id"] = uuid4() + self.tracker_uuid = uuid4() + with db_session: + # store game datapackage(s) + for game, game_data in multidata["datapackage"].items(): + if not GameDataPackage.get(checksum=game_data["checksum"]): + GameDataPackage(checksum=game_data["checksum"], + data=pickle.dumps(game_data)) + # create an empty seed and a room from it + seed = Seed(multidata=self.data, owner=session["_id"]) + room = Room(seed=seed, owner=session["_id"], tracker=self.tracker_uuid) + self.room_id = room.id + self.log_filename = user_path("logs", f"{self.room_id}.txt") + + def tearDown(self) -> None: + from pony.orm import db_session, select + from WebHostLib.models import Command, Room + + with db_session: + for command in select(command for command in Command if command.room.id == self.room_id): # type: ignore + command.delete() + room: Room = Room.get(id=self.room_id) + room.seed.delete() + room.delete() + + try: + os.unlink(self.log_filename) + except FileNotFoundError: + pass + + def test_valid_if_modified_since(self) -> None: + """ + Verify that we get a 200 response for valid If-Modified-Since + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get( + url_for( + "get_player_tracker", + tracker=self.tracker_uuid, + tracked_team=0, + tracked_player=1, + ), + headers={"If-Modified-Since": "Wed, 21 Oct 2015 07:28:00 GMT"}, + ) + self.assertEqual(response.status_code, 200) + + def test_invalid_if_modified_since(self) -> None: + """ + Verify that we get a 400 response for invalid If-Modified-Since + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get( + url_for( + "get_player_tracker", + tracker=self.tracker_uuid, + tracked_team=1, + tracked_player=0, + ), + headers={"If-Modified-Since": "Wed, 21 Oct 2015 07:28:00"}, # missing timezone + ) + self.assertEqual(response.status_code, 400) diff --git a/worlds/AutoSNIClient.py b/worlds/AutoSNIClient.py index 2b984d9c8846..b3f40be2958d 100644 --- a/worlds/AutoSNIClient.py +++ b/worlds/AutoSNIClient.py @@ -1,6 +1,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 @@ -60,8 +61,12 @@ def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut @staticmethod async def get_handler(ctx: SNIContext) -> Optional[SNIClient]: for _game, handler in AutoSNIClientRegister.game_handlers.items(): - if await handler.validate_rom(ctx): - return handler + try: + if await handler.validate_rom(ctx): + return handler + except Exception as e: + text_file_logger = logging.getLogger() + text_file_logger.exception(e) return None diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index f7dae2b92750..3c4edc1b0c3b 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -10,7 +10,7 @@ from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) -from Options import item_and_loc_options, OptionGroup, PerGameCommonOptions +from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -480,6 +480,7 @@ def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set group = cls(multiworld, new_player_id) group.options = cls.options_dataclass(**{option_key: option.from_any(option.default) for option_key, option in cls.options_dataclass.type_hints.items()}) + group.options.accessibility = ItemsAccessibility(ItemsAccessibility.option_items) return group 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) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index f759b6309a0e..d0487494aa64 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -3338,25 +3338,6 @@ def plando_connect(world, player: int): ('Turtle Rock Exit (Front)', 'Dark Death Mountain'), ('Ice Palace Exit', 'Dark Lake Hylia')] -# Regions that can be required to access entrances through rules, not paths -indirect_connections = { - "Turtle Rock (Top)": "Turtle Rock", - "East Dark World": "Pyramid Fairy", - "Dark Desert": "Pyramid Fairy", - "West Dark World": "Pyramid Fairy", - "South Dark World": "Pyramid Fairy", - "Light World": "Pyramid Fairy", - "Old Man Cave": "Old Man S&Q" -} - -indirect_connections_inverted = { - "Inverted Big Bomb Shop": "Pyramid Fairy", -} - -indirect_connections_not_inverted = { - "Big Bomb Shop": "Pyramid Fairy", -} - # format: # Key=Name # addr = (door_index, exitdata) # multiexit diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3cdbb1cb458a..f897d3762929 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -8,8 +8,7 @@ import Utils from BaseClasses import Item, CollectionState, Tutorial, MultiWorld from .Dungeons import create_dungeons, Dungeon -from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \ - indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted +from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect from .InvertedRegions import create_inverted_regions, mark_dark_world_regions from .ItemPool import generate_itempool, difficulties from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem @@ -137,6 +136,7 @@ class ALTTPWorld(World): settings_key = "lttp_options" settings: typing.ClassVar[ALTTPSettings] topology_present = True + explicit_indirect_conditions = False item_name_groups = item_name_groups location_name_groups = { "Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right", @@ -394,23 +394,13 @@ def create_regions(self): if multiworld.mode[player] != 'inverted': link_entrances(multiworld, player) mark_light_world_regions(multiworld, player) - for region_name, entrance_name in indirect_connections_not_inverted.items(): - multiworld.register_indirect_condition(multiworld.get_region(region_name, player), - multiworld.get_entrance(entrance_name, player)) else: link_inverted_entrances(multiworld, player) mark_dark_world_regions(multiworld, player) - for region_name, entrance_name in indirect_connections_inverted.items(): - multiworld.register_indirect_condition(multiworld.get_region(region_name, player), - multiworld.get_entrance(entrance_name, player)) multiworld.random = old_random plando_connect(multiworld, player) - for region_name, entrance_name in indirect_connections.items(): - multiworld.register_indirect_condition(multiworld.get_region(region_name, player), - multiworld.get_entrance(entrance_name, player)) - def collect_item(self, state: CollectionState, item: Item, remove=False): item_name = item.name if item_name.startswith('Progressive '): diff --git a/worlds/alttp/test/items/TestDifficulty.py b/worlds/alttp/test/items/TestDifficulty.py index 8fee56f393c7..69dd8a4dc6ba 100644 --- a/worlds/alttp/test/items/TestDifficulty.py +++ b/worlds/alttp/test/items/TestDifficulty.py @@ -1,5 +1,5 @@ from worlds.alttp.ItemPool import difficulties -from test.TestBase import TestBase +from test.bases import TestBase base_items = 41 extra_counts = (15, 15, 10, 5, 25) diff --git a/worlds/alttp/test/items/TestPrizes.py b/worlds/alttp/test/items/TestPrizes.py index 5e729093f9b3..5a9f6aa9c9ae 100644 --- a/worlds/alttp/test/items/TestPrizes.py +++ b/worlds/alttp/test/items/TestPrizes.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import Item, Location -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class TestPrizes(WorldTestBase): diff --git a/worlds/alttp/test/minor_glitches/TestMinor.py b/worlds/alttp/test/minor_glitches/TestMinor.py index 8432028bf007..7663c20a2943 100644 --- a/worlds/alttp/test/minor_glitches/TestMinor.py +++ b/worlds/alttp/test/minor_glitches/TestMinor.py @@ -2,7 +2,7 @@ from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import item_factory -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.Options import GlitchesRequired from worlds.alttp.test import LTTPTestBase diff --git a/worlds/alttp/test/owg/TestVanillaOWG.py b/worlds/alttp/test/owg/TestVanillaOWG.py index 67156eb97275..e51970bc50b6 100644 --- a/worlds/alttp/test/owg/TestVanillaOWG.py +++ b/worlds/alttp/test/owg/TestVanillaOWG.py @@ -2,7 +2,7 @@ from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import item_factory -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.Options import GlitchesRequired from worlds.alttp.test import LTTPTestBase diff --git a/worlds/alttp/test/shops/TestSram.py b/worlds/alttp/test/shops/TestSram.py index f5feedfb373e..74a41a628988 100644 --- a/worlds/alttp/test/shops/TestSram.py +++ b/worlds/alttp/test/shops/TestSram.py @@ -1,5 +1,5 @@ from worlds.alttp.Shops import shop_table -from test.TestBase import TestBase +from test.bases import TestBase class TestSram(TestBase): diff --git a/worlds/alttp/test/vanilla/TestVanilla.py b/worlds/alttp/test/vanilla/TestVanilla.py index 7eebc349d43f..9b5db7b12291 100644 --- a/worlds/alttp/test/vanilla/TestVanilla.py +++ b/worlds/alttp/test/vanilla/TestVanilla.py @@ -2,7 +2,7 @@ from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import item_factory -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.Options import GlitchesRequired from worlds.alttp.test import LTTPTestBase diff --git a/worlds/bumpstik/test/__init__.py b/worlds/bumpstik/test/__init__.py index 1199d7b8e506..5f40426e5b10 100644 --- a/worlds/bumpstik/test/__init__.py +++ b/worlds/bumpstik/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class BumpStikTestBase(WorldTestBase): diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index 9ba57b059185..0b9b7105bfc4 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -11,19 +11,18 @@ class ChecksFinderWeb(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", - "A guide to setting up the Archipelago ChecksFinder software on your computer. This guide covers " - "single-player, multiworld, and related software.", + "A guide to playing Archipelago ChecksFinder.", "English", "setup_en.md", "setup/en", - ["Mewlif"] + ["SunCat"] )] class ChecksFinderWorld(World): """ - ChecksFinder is a game where you avoid mines and find checks inside the board - with the mines! You win when you get all your items and beat the board! + ChecksFinder is a game where you avoid mines and collect checks by beating boards! + You win when you get all your items and beat the last board! """ game = "ChecksFinder" options_dataclass = PerGameCommonOptions diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py index 0d384acc8f3d..1bd069a2cea8 100644 --- a/worlds/cv64/__init__.py +++ b/worlds/cv64/__init__.py @@ -89,7 +89,7 @@ class CV64World(World): def generate_early(self) -> None: # Generate the player's unique authentication - self.auth = bytearray(self.multiworld.random.getrandbits(8) for _ in range(16)) + self.auth = bytearray(self.random.getrandbits(8) for _ in range(16)) self.total_s1s = self.options.total_special1s.value self.s1s_per_warp = self.options.special1s_per_warp.value 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/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 ab4371b0ac12..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: @@ -944,13 +948,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])) 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 diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index 9755cce1c6a8..484afdce3fcb 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -18,7 +18,8 @@ installation folder. Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This is the latest version, so you don't need to do any downpatching! However, if you've already downpatched your game to use an older version of the randomizer, you'll need to reinstall the latest -version before using this version. +version before using this version. You should also delete the `dinput8.dll` file if you still have +one from an older randomizer version. ### One-Time Setup 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 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/__init__.py b/worlds/dlcquest/test/__init__.py index 8a39b43a2cfd..0432ae8b60ba 100644 --- a/worlds/dlcquest/test/__init__.py +++ b/worlds/dlcquest/test/__init__.py @@ -4,7 +4,7 @@ from argparse import Namespace from BaseClasses import MultiWorld -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from .. import DLCqworld from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from worlds.AutoWorld import call_all 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)) 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..4383357546d9 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,19 @@ 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.get_item_count("satellite") > 0 or GOAL == 0 then - global.forcedata[event.rocket.force.name]['victory'] = 1 + if event.rocket and event.rocket.valid and storage.forcedata[event.rocket.force.name]['victory'] == 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 { @@ -318,7 +340,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 +349,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 +397,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 +445,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 +502,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 +562,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 +585,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 +613,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 +628,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 +644,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 +683,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 +708,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 +724,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 +732,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 +744,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 +754,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 +770,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 diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 9ec77e6bf0cd..486aa164cd5d 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -509,9 +509,13 @@ def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]): per_player_grubs_per_player[player][player] += 1 if grub.location and grub.location.player in group_lookup.keys(): - for real_player in group_lookup[grub.location.player]: + # will count the item linked grub instead + pass + elif player in group_lookup: + for real_player in group_lookup[player]: grub_count_per_player[real_player] += 1 else: + # for non-linked grubs grub_count_per_player[player] += 1 for player, count in grub_count_per_player.items(): diff --git a/worlds/hk/test/__init__.py b/worlds/hk/test/__init__.py new file mode 100644 index 000000000000..c41d20127fcc --- /dev/null +++ b/worlds/hk/test/__init__.py @@ -0,0 +1,62 @@ +import typing +from argparse import Namespace +from BaseClasses import CollectionState, MultiWorld +from Options import ItemLinks +from test.bases import WorldTestBase +from worlds.AutoWorld import AutoWorldRegister, call_all +from .. import HKWorld + + +class linkedTestHK(): + run_default_tests = False + game = "Hollow Knight" + world: HKWorld + expected_grubs: int + item_link_group: typing.List[typing.Dict[str, typing.Any]] + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any(self.item_link_group), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": False, + "replacement_item": "One_Geo", + }]) + }) + return args + + def world_setup(self) -> None: + """ + Create a multiworld with two players that share an itemlink + """ + self.multiworld = MultiWorld(2) + self.multiworld.game = {1: self.game, 2: self.game} + self.multiworld.player_name = {1: "Linker 1", 2: "Linker 2"} + self.multiworld.set_seed() + args = Namespace() + options_dataclass = AutoWorldRegister.world_types[self.game].options_dataclass + for name, option in options_dataclass.type_hints.items(): + setattr(args, name, { + 1: option.from_any(self.options.get(name, option.default)), + 2: option.from_any(self.options.get(name, option.default)) + }) + args = self.setup_item_links(args) + self.multiworld.set_options(args) + self.multiworld.set_item_links() + # groups get added to state during its constructor so this has to be after item links are set + self.multiworld.state = CollectionState(self.multiworld) + gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic") + for step in gen_steps: + call_all(self.multiworld, step) + # link the items together and stop at prefill + self.multiworld.link_items() + self.multiworld._all_state = None + call_all(self.multiworld, "pre_fill") + + self.world = self.multiworld.worlds[self.player] + + def test_grub_count(self) -> None: + assert self.world.grub_count == self.expected_grubs, \ + f"Expected {self.expected_grubs} but found {self.world.grub_count}" diff --git a/worlds/hk/test/test_grub_count.py b/worlds/hk/test/test_grub_count.py new file mode 100644 index 000000000000..dba15b614dd9 --- /dev/null +++ b/worlds/hk/test/test_grub_count.py @@ -0,0 +1,165 @@ +from . import linkedTestHK, WorldTestBase +from Options import ItemLinks + + +class test_grubcount_limited(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": 20, + "Goal": "any", + } + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": True, + "replacement_item": "Grub", + }] + expected_grubs = 20 + + +class test_grubcount_default(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "Goal": "any", + } + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": True, + "replacement_item": "Grub", + }] + expected_grubs = 46 + + +class test_grubcount_all_unlinked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + item_link_group = [] + expected_grubs = 46 + + +class test_grubcount_all_linked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": True, + "replacement_item": "Grub", + }] + expected_grubs = 46 + 23 + + +class test_replacement_only(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + expected_grubs = 46 + 18 # the count of grubs + skills removed from item links + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "Grub", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "Grub", + }]) + }) + return args + + +class test_replacement_only_unlinked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + expected_grubs = 46 + 9 # Player1s replacement Grubs + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "Grub", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "Grub", + }]) + }) + return args + + +class test_ignore_others(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + # player2 has more than 46 grubs but they are unlinked so player1s grubs are vanilla + expected_grubs = 46 + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "One_Geo", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "Grub", + }]) + }) + return args + + +class test_replacement_only_linked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + expected_grubs = 46 + 9 # Player2s linkreplacement grubs + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "One_Geo", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "Grub", + }]) + }) + return args diff --git a/worlds/ladx/test/__init__.py b/worlds/ladx/test/__init__.py index 0e616ac557d0..059a09b0728d 100644 --- a/worlds/ladx/test/__init__.py +++ b/worlds/ladx/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from ..Common import LINKS_AWAKENING class LADXTestBase(WorldTestBase): game = LINKS_AWAKENING 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 diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index bbed1464530b..3783b68af98c 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -1966,7 +1966,10 @@ entrances: The Observant: warp: True - Eight Room: True + Eight Room: + # It is possible to get to the second floor warpless, but there are no warpless exits from the second floor, + # meaning that this connection is essentially always a warp for the purposes of Pilgrimage. + warp: True Eight Alcove: door: Eight Door Orange Tower Sixth Floor: diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 789fc0856d62..9abb0276c8b5 100644 Binary files a/worlds/lingo/data/generated.dat and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 2fd57ff5ede3..2d6e9967dfc4 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -80,10 +80,15 @@ class ShuffleColors(DefaultOnToggle): class ShufflePanels(Choice): - """If on, the puzzles on each panel are randomized. + """Determines how panel puzzles are randomized. - On "rearrange", the puzzles are the same as the ones in the base game, but - are placed in different areas. + - **None:** Most panels remain the same as in the base game. Note that there are + some panels (in particular, in Starting Room and Second Room) that are changed + by the randomizer even when panel shuffle is disabled. + - **Rearrange:** The puzzles are the same as the ones in the base game, but are + placed in different areas. + + More options for puzzle randomization are planned in the future. """ display_name = "Shuffle Panels" option_none = 0 diff --git a/worlds/lufia2ac/test/__init__.py b/worlds/lufia2ac/test/__init__.py index 24925675e36b..306ffa771660 100644 --- a/worlds/lufia2ac/test/__init__.py +++ b/worlds/lufia2ac/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class L2ACTestBase(WorldTestBase): 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" 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" diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 9ed55f218d9f..58f23a2bc1d9 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -190,12 +190,83 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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('*', '') @@ -224,6 +295,20 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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: @@ -236,6 +321,7 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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( @@ -249,12 +335,15 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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: @@ -267,6 +356,7 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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( @@ -280,12 +370,15 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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 = \ @@ -297,6 +390,7 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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( @@ -309,13 +403,16 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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 = \ @@ -327,6 +424,7 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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( @@ -339,9 +437,11 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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: @@ -355,6 +455,7 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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( @@ -367,12 +468,15 @@ def generate_special_rules_for(self, entrance, region_row, outbound_region_name) 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 roll_locations(self): diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index 20111aa01d66..cf268509493c 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -35,17 +35,13 @@ def has_requirements_for_level_star( state: CollectionState, level: Overcooked2GenericLevel, stars: int, player: int) -> bool: assert 0 <= stars <= 3 - # First ensure that previous stars are obtainable - if stars > 1: - if not has_requirements_for_level_star(state, level, stars-1, player): - return False - - # Second, ensure that global requirements are met + # First, ensure that global requirements for this many stars are met. + # Lower numbers of stars are implied meetable if this level is meetable. if not meets_requirements(state, "*", stars, player): return False - # Finally, return success only if this level's requirements are met - return meets_requirements(state, level.shortname, stars, player) + # Then return success only if this level's requirements are met at all stars up through this one + return all(meets_requirements(state, level.shortname, s, player) for s in range(1, stars + 1)) def meets_requirements(state: CollectionState, name: str, stars: int, player: int): @@ -421,6 +417,7 @@ def can_reach_kevin_eight_island(state: CollectionState, player: int, allow_tric }, ), ( # 3-star + # Necessarily implies 2-star [ # Exclusive "Progressive Dash", "Spare Plate", diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 2d7db0dad4d5..0dd874b25029 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -11,6 +11,8 @@ - Fixed a rare issue where receiving a wonder trade could partially corrupt the save data, preventing the player from receiving new items. - Fixed the client spamming the "goal complete" status update to the server instead of sending it once. +- Fixed the `trainer_party_blacklist` option checking for the existence of the "_Legendaries" shortcut in the +`starter_blacklist` option instead of itself. - Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if the player randomized NPC gifts. - The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower. @@ -18,6 +20,8 @@ the player randomized NPC gifts. with another NPC was moved to an unoccupied space. - Fixed a problem where the client would crash on certain operating systems while using certain python versions if the player tried to wonder trade. +- Prevent the poke flute sound from replacing the evolution fanfare, which would cause the game to wait in silence for +a long time during the evolution scene. # 2.2.0 diff --git a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md index 9a3991e97f75..732b2092a28c 100644 --- a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md +++ b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md @@ -30,7 +30,7 @@ randomizer. Here are some of the more important ones: - The Wally catching tutorial is skipped - All text is instant and, with an option, can be automatically progressed by holding A - When a Repel runs out, you will be prompted to use another -- Many more minor improvements… +- [Many more minor improvements…](/tutorial/Pokemon%20Emerald/rom_changes/en) ## Where is my starting inventory? diff --git a/worlds/pokemon_emerald/docs/rom changes.md b/worlds/pokemon_emerald/docs/rom_changes_en.md similarity index 100% rename from worlds/pokemon_emerald/docs/rom changes.md rename to worlds/pokemon_emerald/docs/rom_changes_en.md diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index e05b5d96ac74..8fcc74d1c34a 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -123,6 +123,8 @@ class Dexsanity(Toggle): Defeating gym leaders provides dex info, allowing you to see where on the map you can catch species you need. Each pokedex entry adds a Poke Ball, Great Ball, or Ultra Ball to the pool. + + Warning: This adds a lot of locations and will slow you down significantly. """ display_name = "Dexsanity" @@ -132,6 +134,8 @@ class Trainersanity(Toggle): Defeating a trainer gives you an item. Trainers are no longer missable. Trainers no longer give you money for winning. Each trainer adds a valuable item (Nugget, Stardust, etc.) to the pool. + + Warning: This adds a lot of locations and will slow you down significantly. """ display_name = "Trainersanity" @@ -265,6 +269,8 @@ class RandomizeWildPokemon(Choice): """ Randomizes wild pokemon encounters (grass, caves, water, fishing). + Warning: Matching both base stats and type may severely limit the variety for certain pokemon. + - Vanilla: Wild encounters are unchanged - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst - Match Type: Wild pokemon are replaced with species that share a type with the original @@ -327,6 +333,8 @@ class RandomizeTrainerParties(Choice): """ Randomizes the parties of all trainers. + Warning: Matching both base stats and type may severely limit the variety for certain pokemon. + - Vanilla: Parties are unchanged - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst - Match Type: Trainer pokemon are replaced with species that share a type with the original @@ -357,6 +365,10 @@ class TrainerPartyBlacklist(OptionSet): class ForceFullyEvolved(Range): """ When an opponent uses a pokemon of the specified level or higher, restricts the species to only fully evolved pokemon. + + Only applies when trainer parties are randomized. + + Warning: Combining a low value with matched base stats may severely limit the variety for certain pokemon. """ display_name = "Force Fully Evolved" range_start = 1 diff --git a/worlds/pokemon_emerald/rom.py b/worlds/pokemon_emerald/rom.py index 2c0b5021d099..e2a7a4800bfb 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -73,6 +73,7 @@ "MUS_OBTAIN_SYMBOL": 318, "MUS_REGISTER_MATCH_CALL": 135, } +_EVOLUTION_FANFARE_INDEX = list(_FANFARES.keys()).index("MUS_EVOLVED") CAVE_EVENT_NAME_TO_ID = { "TERRA_CAVE_ROUTE_114_1": 1, @@ -661,6 +662,15 @@ def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePat # Shuffle the lists, pair new tracks with original tracks, set the new track ids, and set new fanfare durations randomized_fanfares = [fanfare_name for fanfare_name in _FANFARES] world.random.shuffle(randomized_fanfares) + + # Prevent the evolution fanfare from receiving the poke flute by swapping it with something else. + # The poke flute sound causes the evolution scene to get stuck for like 40 seconds + if randomized_fanfares[_EVOLUTION_FANFARE_INDEX] == "MUS_RG_POKE_FLUTE": + swap_index = (_EVOLUTION_FANFARE_INDEX + 1) % len(_FANFARES) + temp = randomized_fanfares[_EVOLUTION_FANFARE_INDEX] + randomized_fanfares[_EVOLUTION_FANFARE_INDEX] = randomized_fanfares[swap_index] + randomized_fanfares[swap_index] = temp + for i, fanfare_pair in enumerate(zip(_FANFARES.keys(), randomized_fanfares)): patch.write_token( APTokenTypes.WRITE, diff --git a/worlds/pokemon_emerald/test/__init__.py b/worlds/pokemon_emerald/test/__init__.py index 84ce64003d57..bf2a8da5b0c5 100644 --- a/worlds/pokemon_emerald/test/__init__.py +++ b/worlds/pokemon_emerald/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class PokemonEmeraldTestBase(WorldTestBase): diff --git a/worlds/pokemon_emerald/test/test_warps.py b/worlds/pokemon_emerald/test/test_warps.py index 75a2417dfbe6..d1b5b01dcf7f 100644 --- a/worlds/pokemon_emerald/test/test_warps.py +++ b/worlds/pokemon_emerald/test/test_warps.py @@ -1,4 +1,4 @@ -from test.TestBase import TestBase +from test.bases import TestBase from ..data import Warp diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 2065507e0d59..809179cbef74 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -526,15 +526,24 @@ def stage_post_fill(cls, multiworld): # This cuts down on time spent calculating the spoiler playthrough. found_mons = set() 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: location.item.classification = ItemClassification.useful else: - found_mons.add(key) + mon_locations_in_sphere.setdefault(key, []).append(location) + for key, mon_locations in mon_locations_in_sphere.items(): + found_mons.add(key) + if len(mon_locations) > 1: + # Sort for deterministic results. + mon_locations.sort() + # Convert all but the first to useful classification. + for location in mon_locations[1:]: + location.item.classification = ItemClassification.useful def create_regions(self): if (self.options.old_man == "vanilla" or @@ -703,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(): 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), diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 290f4a60ac21..7ffdd459db48 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -46,30 +46,6 @@ def fill_slot_data(self) -> dict: return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()]) def generate_early(self): - location_ids_used_per_game = { - world.game: set(world.location_id_to_name) for world in self.multiworld.worlds.values() - } - item_ids_used_per_game = { - world.game: set(world.item_id_to_name) for world in self.multiworld.worlds.values() - } - overlapping_games = set() - - for id_lookup in (location_ids_used_per_game, item_ids_used_per_game): - for game_1, ids_1 in id_lookup.items(): - for game_2, ids_2 in id_lookup.items(): - if game_1 == game_2: - continue - - if ids_1 & ids_2: - overlapping_games.add(tuple(sorted([game_1, game_2]))) - - if overlapping_games: - raise RuntimeError( - "In this multiworld, there are games with overlapping item/location IDs.\n" - "The current Rogue Legacy does not support these and a fix is not currently planned.\n" - f"The overlapping games are: {overlapping_games}" - ) - # Check validation of names. additional_lady_names = len(self.options.additional_lady_names.value) additional_sir_names = len(self.options.additional_sir_names.value) diff --git a/worlds/sc2/Locations.py b/worlds/sc2/Locations.py index bf9c06fa3f78..53f41f4e4c3d 100644 --- a/worlds/sc2/Locations.py +++ b/worlds/sc2/Locations.py @@ -1445,11 +1445,11 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]: LocationData("The Escape", "The Escape: Agent Stone", SC2NCO_LOC_ID_OFFSET + 105, LocationType.VANILLA, lambda state: logic.the_escape_requirement(state)), LocationData("Sudden Strike", "Sudden Strike: Victory", SC2NCO_LOC_ID_OFFSET + 200, LocationType.VICTORY, - lambda state: logic.sudden_strike_can_reach_objectives(state)), + lambda state: logic.sudden_strike_requirement(state)), LocationData("Sudden Strike", "Sudden Strike: Research Center", SC2NCO_LOC_ID_OFFSET + 201, LocationType.VANILLA, lambda state: logic.sudden_strike_can_reach_objectives(state)), LocationData("Sudden Strike", "Sudden Strike: Weaponry Labs", SC2NCO_LOC_ID_OFFSET + 202, LocationType.VANILLA, - lambda state: logic.sudden_strike_requirement(state)), + lambda state: logic.sudden_strike_can_reach_objectives(state)), LocationData("Sudden Strike", "Sudden Strike: Brutalisk", SC2NCO_LOC_ID_OFFSET + 203, LocationType.EXTRA, lambda state: logic.sudden_strike_requirement(state)), LocationData("Enemy Intelligence", "Enemy Intelligence: Victory", SC2NCO_LOC_ID_OFFSET + 300, LocationType.VICTORY, diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 60ec4bbe13c2..8269d3a262cd 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -91,12 +91,11 @@ class BuddyChecks(Toggle): display_name = "Bob-omb Buddy Checks" -class ExclamationBoxes(Choice): +class ExclamationBoxes(Toggle): """Include 1Up Exclamation Boxes during randomization. Adds 29 locations to the pool.""" display_name = "Randomize 1Up !-Blocks" - option_Off = 0 - option_1Ups_Only = 1 + alias_1Ups_Only = 1 class CompletionType(Choice): diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 833ae56ca302..d4bafbafcc57 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -55,7 +55,7 @@ def generate_early(self): for action in self.options.move_rando_actions.value: max_stars -= 1 self.move_rando_bitvec |= (1 << (action_item_table[action] - action_item_table['Double Jump'])) - if (self.options.exclamation_boxes > 0): + if self.options.exclamation_boxes: max_stars += 29 self.number_of_stars = min(self.options.amount_of_stars, max_stars) self.filler_count = max_stars - self.number_of_stars @@ -133,7 +133,7 @@ def generate_basic(self): self.multiworld.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI")) self.multiworld.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR")) - if (self.options.exclamation_boxes == 0): + if not self.options.exclamation_boxes: self.multiworld.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom")) 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 = { diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index b2e1a71897c0..ab751d8e669d 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -56,6 +56,7 @@ In general: - Bushes are not considered in logic. It is assumed that the player will find a way past them, whether it is with a sword, a bomb, fire, luring an enemy, etc. There is also an option in the in-game randomizer settings menu to clear some of the early bushes. - The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance. - The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside. +- For the Ice Grappling, Ladder Storage, and Laurels Zips options, there is [this document](https://docs.google.com/document/d/1SFZBfsqZWH1_EAV9zyZobvrBcvCd3_54JP3iVnJ8rUg/edit?usp=sharing) that shows the individual applications of these tricks in logic. For the Entrance Randomizer: - Activating a fuse to turn on a yellow teleporter pad also activates its counterpart in the Far Shore. diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index bd2498a56a35..6f5eec020be6 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -344,9 +344,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) - regions["Overworld"].connect( + cube_entrance = regions["Overworld"].connect( connecting_region=regions["Cube Cave Entrance Region"], rule=lambda state: state.has(gun, player) or can_shop(state, world)) + world.multiworld.register_indirect_condition(regions["Shop"], cube_entrance) regions["Cube Cave Entrance Region"].connect( connecting_region=regions["Overworld"]) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 1683b3ca5aee..cdd37a889461 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -183,7 +183,7 @@ class IceGrappling(Choice): Easy includes ice grappling enemies that are in range without luring them. May include clips through terrain. Medium includes using ice grapples to push enemies through doors or off ledges without luring them. Also includes bringing an enemy over to the Temple Door to grapple through it. Hard includes luring or grappling enemies to get to where you want to go. - The Medium and Hard options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Enabling any of these difficulty options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. Note: You will still be expected to ice grapple to the slime in East Forest from below with this option off. """ internal_name = "ice_grappling" @@ -201,7 +201,7 @@ class LadderStorage(Choice): Easy includes uses of Ladder Storage to get to open doors over a long distance without too much difficulty. May include convenient elevation changes (going up Mountain stairs, stairs in front of Special Shop, etc.). Medium includes the above as well as changing your elevation using the environment and getting knocked down by melee enemies mid-LS. Hard includes the above as well as going behind the map to enter closed doors from behind, shooting a fuse with the magic wand to knock yourself down at close range, and getting into the Cathedral Secret Legend room mid-LS. - Enabling any of these difficulty options will give the player the Torch item to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Enabling any of these difficulty options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. Opening individual chests while doing ladder storage is excluded due to tedium. Knocking yourself out of LS with a bomb is excluded due to the problematic nature of consumables in logic. """ 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"