diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b0cfe35d2b..3abbb5f644 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 9a3a6d1121..a38fef8fda 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 0d4f34e514..46edeb5ea0 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -341,7 +341,7 @@ class MultiWorld(): new_item.classification |= classifications[item_name] new_itempool.append(new_item) - region = Region("Menu", group_id, self, "ItemLink") + region = Region(group["world"].origin_region_name, group_id, self, "ItemLink") self.regions.append(region) locations = region.locations # ensure that progression items are linked first, then non-progression @@ -1264,6 +1264,10 @@ class Item: 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 52babdf188..bc359a203d 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 85e49da7e5..ea59e8beb5 100644 --- a/Launcher.py +++ b/Launcher.py @@ -22,16 +22,15 @@ from os.path import isfile 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 @@ components.extend([ 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), @@ -254,7 +254,7 @@ def run_gui(): _client_layout: Optional[ScrollBox] = None def __init__(self, ctx=None): - self.title = self.base_title + self.title = self.base_title + " " + Utils.__version__ self.ctx = ctx self.icon = r"data/icon.png" super().__init__() diff --git a/MultiServer.py b/MultiServer.py index bac35648cf..764b56362e 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1960,8 +1960,10 @@ class ServerCommandProcessor(CommonCommandProcessor): 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 aa6f175fa5..992348cb54 100644 --- a/Options.py +++ b/Options.py @@ -15,7 +15,7 @@ from dataclasses import dataclass 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 generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge 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 222ed54f5c..19440e1dc5 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/Utils.py b/Utils.py index 232fd4c5a0..412011200f 100644 --- a/Utils.py +++ b/Utils.py @@ -31,6 +31,7 @@ if typing.TYPE_CHECKING: import tkinter import pathlib from BaseClasses import Region + import multiprocessing def tuplize_version(version: str) -> Version: @@ -664,6 +665,19 @@ def get_input_text_from_response(text: str, command: str) -> typing.Optional[str return None +def is_kivy_running() -> bool: + if "kivy" in sys.modules: + from kivy.app import App + return App.get_running_app() is not None + return False + + +def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None: + if is_kivy_running(): + raise RuntimeError("kivy should not be running in multiprocess") + res.put(open_filename(*args)) + + def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \ -> typing.Optional[str]: logging.info(f"Opening file input dialog for {title}.") @@ -693,6 +707,13 @@ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin f'This attempt was made because open_filename was used for "{title}".') raise e else: + if is_macos and is_kivy_running(): + # on macOS, mixing kivy and tk does not work, so spawn a new process + # FIXME: performance of this is pretty bad, and we should (also) look into alternatives + from multiprocessing import Process, Queue + res: "Queue[typing.Optional[str]]" = Queue() + Process(target=_mp_open_filename, args=(res, title, filetypes, suggest)).start() + return res.get() try: root = tkinter.Tk() except tkinter.TclError: @@ -702,6 +723,12 @@ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin initialfile=suggest or None) +def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None: + if is_kivy_running(): + raise RuntimeError("kivy should not be running in multiprocess") + res.put(open_directory(*args)) + + def open_directory(title: str, suggest: str = "") -> typing.Optional[str]: def run(*args: str): return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None @@ -725,9 +752,16 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]: import tkinter.filedialog except Exception as e: logging.error('Could not load tkinter, which is likely not installed. ' - f'This attempt was made because open_filename was used for "{title}".') + f'This attempt was made because open_directory was used for "{title}".') raise e else: + if is_macos and is_kivy_running(): + # on macOS, mixing kivy and tk does not work, so spawn a new process + # FIXME: performance of this is pretty bad, and we should (also) look into alternatives + from multiprocessing import Process, Queue + res: "Queue[typing.Optional[str]]" = Queue() + Process(target=_mp_open_directory, args=(res, title, suggest)).start() + return res.get() try: root = tkinter.Tk() except tkinter.TclError: @@ -740,12 +774,6 @@ def messagebox(title: str, text: str, error: bool = False) -> None: def run(*args: str): return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None - def is_kivy_running(): - if "kivy" in sys.modules: - from kivy.app import App - return App.get_running_app() is not None - return False - if is_kivy_running(): from kvui import MessageBox MessageBox(title, text, error).open() diff --git a/WebHost.py b/WebHost.py index e597de2476..3bf75eb35a 100644 --- a/WebHost.py +++ b/WebHost.py @@ -12,6 +12,7 @@ ModuleUpdate.update() # 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 fdf3037fe0..dbe2182b07 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -9,7 +9,7 @@ from flask_compress import Compress 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 @@ Pony(app) 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/misc.py b/WebHostLib/misc.py index 1f86e21066..c49b1ae178 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -77,7 +77,13 @@ def faq(lang: str): return render_template( "markdown_document.html", title="Frequently Asked Questions", - html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]), + html_from_markdown=markdown.markdown( + document, + extensions=["toc", "mdx_breakless_lists"], + extension_configs={ + "toc": {"anchorlink": True} + } + ), ) @@ -90,7 +96,13 @@ def glossary(lang: str): return render_template( "markdown_document.html", title="Glossary", - html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]), + html_from_markdown=markdown.markdown( + document, + extensions=["toc", "mdx_breakless_lists"], + extension_configs={ + "toc": {"anchorlink": True} + } + ), ) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 2020387053..5c79415312 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 0000000000..537e279791 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 0000000000..f34cd5ff2e 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 2435222d24..a0b41b0f8c 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 0000000000..4a5f2d75a0 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 3dcc6d1534..6e1608d82b 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 0000000000..30bd2d047a 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 7e58e4b0ce..3d3e089ef7 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 0000000000..f575ac5d9d 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 2dabbf2986..08730d9848 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 0000000000..f9227e8f22 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 afbf0dafb5..0bc82fa70e 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 0000000000..3c0a577402 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 2b5865573a..05e675d6a9 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 0000000000..4283cd42b1 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 aeccd805f0..e0683a74bb 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 0000000000..3075cec96a 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 1eb072ac08..cded7ad108 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 0000000000..781b8e4df0 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 0000000000..73e249f6e5 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 0000000000..e4ac19bef6 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 0000000000..36abe6e552 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 0000000000..5a8635506f 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 d15afe5f9e..6752ab4e32 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 0000000000..fb278c3b16 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 88d0659f69..3bacab4134 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 0000000000..9b8e457c52 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 9c4d9a4632..f8223e6901 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 0000000000..c2ded77536 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 46519036eb..d4476e53f7 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 0000000000..a210081746 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 58093f7efc..7946159624 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 0000000000..c0ee5205ca 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 0000000000..1b8ebd7706 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 0000000000..212ab377a6 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 4d478ec1aa..8a9c0963e7 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 0000000000..6c2b0bd8bf 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 602d52d321..6d620e5033 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 0000000000..ca5d1e23bc 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 10d8345af1..1e1c18e93c 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 0000000000..fc1803ca0e 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 b033486b2a..601ad829f1 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 0000000000..0b3c70871a 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 6e93a54b78..f0d2f2fee5 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 0000000000..9f26d1a505 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 0000000000..a129d5f439 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 0000000000..d21edc8e55 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 e4dbdecb73..d6c9d285c9 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 0000000000..a50b7b27f7 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 0000000000..7cd547329a 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 0000000000..96303c8162 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 0000000000..c8088e8262 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 0000000000..7bd4673e99 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 0000000000..970754d7bf 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 1ce420287f..552e4d8f6d 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 0000000000..6da0c17200 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 0df9bc1a49..fd4a256c7c 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 0000000000..6b7c3a279e 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 86182c3627..2f10f45828 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 0000000000..83ce413da8 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 0000000000..cd1c006221 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 9972a915f5..4f5d7c2641 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 0000000000..32c9cc8f6b 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 04eff72f05..cceb79af33 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 0000000000..3ec6aae438 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 e491d7b2c6..2beedce19d 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 0000000000..98e1add91e 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 0000000000..2c8af460d5 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 0000000000..e53a2fb5c4 Binary files /dev/null and b/WebHostLib/static/static/decorations/rock-single.webp differ diff --git a/WebHostLib/static/styles/markdown.css b/WebHostLib/static/styles/markdown.css index e0165b7489..5ead2c60f7 100644 --- a/WebHostLib/static/styles/markdown.css +++ b/WebHostLib/static/styles/markdown.css @@ -28,7 +28,7 @@ font-weight: normal; font-family: LondrinaSolid-Regular, sans-serif; text-transform: uppercase; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ width: 100%; text-shadow: 1px 1px 4px #000000; } @@ -37,7 +37,7 @@ font-size: 38px; font-weight: normal; font-family: LondrinaSolid-Light, sans-serif; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ width: 100%; margin-top: 20px; margin-bottom: 0.5rem; @@ -50,7 +50,7 @@ font-family: LexendDeca-Regular, sans-serif; text-transform: none; text-align: left; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ width: 100%; margin-bottom: 0.5rem; } @@ -59,7 +59,7 @@ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 24px; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ margin-bottom: 24px; } @@ -67,20 +67,29 @@ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 22px; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ } .markdown h6, .markdown details summary.h6{ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 20px; - cursor: pointer;; + cursor: pointer; /* TODO: remove once we drop showdown.js */ } .markdown h4, .markdown h5, .markdown h6{ margin-bottom: 0.5rem; } +.markdown h1 > a, +.markdown h2 > a, +.markdown h3 > a, +.markdown h4 > a, +.markdown h5 > a, +.markdown h6 > a { + color: inherit; +} + .markdown ul{ margin-top: 0.5rem; margin-bottom: 0.5rem; diff --git a/WebHostLib/templates/gameInfo.html b/WebHostLib/templates/gameInfo.html index c5ebba8284..3b908004b1 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/genericTracker.html b/WebHostLib/templates/genericTracker.html index 947cf28372..b92097ceea 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -98,6 +98,8 @@ {% if hint.finding_player == player %} {{ player_names_with_alias[(team, hint.finding_player)] }} + {% elif get_slot_info(team, hint.finding_player).type == 2 %} + {{ player_names_with_alias[(team, hint.finding_player)] }} {% else %} {{ player_names_with_alias[(team, hint.finding_player)] }} @@ -107,6 +109,8 @@ {% if hint.receiving_player == player %} {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% elif get_slot_info(team, hint.receiving_player).type == 2 %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} {% else %} {{ player_names_with_alias[(team, hint.receiving_player)] }} diff --git a/WebHostLib/templates/multitrackerHintTable.html b/WebHostLib/templates/multitrackerHintTable.html index a931e9b048..fcc15fb37a 100644 --- a/WebHostLib/templates/multitrackerHintTable.html +++ b/WebHostLib/templates/multitrackerHintTable.html @@ -21,8 +21,20 @@ ) -%} - {{ player_names_with_alias[(team, hint.finding_player)] }} - {{ player_names_with_alias[(team, hint.receiving_player)] }} + + {% if get_slot_info(team, hint.finding_player).type == 2 %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} + + + {% if get_slot_info(team, hint.receiving_player).type == 2 %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} + {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }} {{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }} {{ games[(team, hint.finding_player)] }} diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index 30a4fc78df..64f0f140de 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -196,13 +196,14 @@ {% macro OptionTitle(option_name, option) %} 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 d3a7e0a05e..4b6622c313 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 75b5fb0202..043764a53b 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -5,7 +5,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple, 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 @@ class TrackerData: 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: @@ -415,6 +423,7 @@ def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> template_name_or_list="genericTracker.html", game_specific_tracker=game in _player_trackers, room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, team=team, player=player, player_name=tracker_data.get_room_long_player_names()[team, player], @@ -438,6 +447,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker enabled_trackers=enabled_trackers, current_tracker="Generic", room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, all_slots=tracker_data.get_all_slots(), room_players=tracker_data.get_all_players(), locations=tracker_data.get_room_locations(), @@ -489,7 +499,7 @@ if "Factorio" in network_data_package["games"]: (team, player): collections.Counter({ tracker_data.item_id_to_name["Factorio"][item_id]: count for item_id, count in tracker_data.get_player_inventory_counts(team, player).items() - }) for team, players in tracker_data.get_all_slots().items() for player in players + }) for team, players in tracker_data.get_all_players().items() for player in players if tracker_data.get_player_game(team, player) == "Factorio" } @@ -498,6 +508,7 @@ if "Factorio" in network_data_package["games"]: enabled_trackers=enabled_trackers, current_tracker="Factorio", room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, all_slots=tracker_data.get_all_slots(), room_players=tracker_data.get_all_players(), locations=tracker_data.get_room_locations(), @@ -630,6 +641,7 @@ if "A Link to the Past" in network_data_package["games"]: enabled_trackers=enabled_trackers, current_tracker="A Link to the Past", room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, all_slots=tracker_data.get_all_slots(), room_players=tracker_data.get_all_players(), locations=tracker_data.get_room_locations(), diff --git a/data/options.yaml b/data/options.yaml index ee8866627d..09bfcdcec1 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 ee7fd7ed86..a51cac3702 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 a161265fcb..ef1594da95 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 7a3531f0f8..c8655ccf3f 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 6fe14c9f32..946546cb69 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 0c9ee2c293..afbe17726d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ import platform import shutil import sys import sysconfig -import typing import warnings import zipfile import urllib.request @@ -14,14 +13,14 @@ import json import threading import subprocess -from collections.abc import Iterable from hashlib import sha3_512 from pathlib import Path +from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it +requirement = 'cx-Freeze==7.2.0' try: - requirement = 'cx-Freeze==7.2.0' import pkg_resources try: pkg_resources.require(requirement) @@ -30,7 +29,7 @@ try: install_cx_freeze = True except ImportError: install_cx_freeze = True - pkg_resources = None # type: ignore [assignment] + pkg_resources = None # type: ignore[assignment] if install_cx_freeze: # check if pip is available @@ -61,7 +60,7 @@ from Cython.Build import cythonize # On Python < 3.10 LogicMixin is not currently supported. -non_apworlds: set = { +non_apworlds: Set[str] = { "A Link to the Past", "Adventure", "ArchipIDLE", @@ -84,7 +83,7 @@ non_apworlds: set = { if sys.version_info < (3,10): non_apworlds.add("Hollow Knight") -def download_SNI(): +def download_SNI() -> None: print("Updating SNI") machine_to_go = { "x86_64": "amd64", @@ -94,7 +93,7 @@ def download_SNI(): platform_name = platform.system().lower() machine_name = platform.machine().lower() # force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH - machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) + machine_name = "universal" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request: data = json.load(request) files = data["assets"] @@ -105,17 +104,19 @@ def download_SNI(): download_url: str = file["browser_download_url"] machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name if platform_name in download_url and machine_match: + source_url = download_url # prefer "many" builds if "many" in download_url: - source_url = download_url break - source_url = download_url + # prefer the correct windows or windows7 build + if platform_name == "windows" and ("windows7" in download_url) == (sys.version_info < (3, 9)): + break if source_url and source_url.endswith(".zip"): with urllib.request.urlopen(source_url) as download: with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: - for member in zf.infolist(): - zf.extract(member, path="SNI") + for zf_member in zf.infolist(): + zf.extract(zf_member, path="SNI") print(f"Downloaded SNI from {source_url}") elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): @@ -129,11 +130,13 @@ def download_SNI(): raise ValueError(f"Unexpected file '{member.name}' in {source_url}") elif member.isdir() and not sni_dir: sni_dir = member.name - elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): + elif member.isfile() and not sni_dir or sni_dir and not member.name.startswith(sni_dir): raise ValueError(f"Expected folder before '{member.name}' in {source_url}") elif member.isfile() and sni_dir: tf.extract(member) # sadly SNI is in its own folder on non-windows, so we need to rename + if not sni_dir: + raise ValueError("Did not find SNI in archive") shutil.rmtree("SNI", True) os.rename(sni_dir, "SNI") print(f"Downloaded SNI from {source_url}") @@ -145,7 +148,7 @@ def download_SNI(): print(f"No SNI found for system spec {platform_name} {machine_name}") -signtool: typing.Optional[str] +signtool: Optional[str] if os.path.exists("X:/pw.txt"): print("Using signtool") with open("X:/pw.txt", encoding="utf-8-sig") as f: @@ -197,13 +200,13 @@ extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] -def remove_sprites_from_folder(folder): +def remove_sprites_from_folder(folder: Path) -> None: for file in os.listdir(folder): if file != ".gitignore": os.remove(folder / file) -def _threaded_hash(filepath): +def _threaded_hash(filepath: Union[str, Path]) -> str: hasher = sha3_512() hasher.update(open(filepath, "rb").read()) return base64.b85encode(hasher.digest()).decode() @@ -217,11 +220,11 @@ class BuildCommand(setuptools.command.build.build): yes: bool last_yes: bool = False # used by sub commands of build - def initialize_options(self): + def initialize_options(self) -> None: super().initialize_options() type(self).last_yes = self.yes = False - def finalize_options(self): + def finalize_options(self) -> None: super().finalize_options() type(self).last_yes = self.yes @@ -233,27 +236,27 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): ('extra-data=', None, 'Additional files to add.'), ] yes: bool - extra_data: Iterable # [any] not available in 3.8 - extra_libs: Iterable # work around broken include_files + extra_data: Iterable[str] + extra_libs: Iterable[str] # work around broken include_files buildfolder: Path libfolder: Path library: Path buildtime: datetime.datetime - def initialize_options(self): + def initialize_options(self) -> None: super().initialize_options() self.yes = BuildCommand.last_yes self.extra_data = [] self.extra_libs = [] - def finalize_options(self): + def finalize_options(self) -> None: super().finalize_options() self.buildfolder = self.build_exe self.libfolder = Path(self.buildfolder, "lib") self.library = Path(self.libfolder, "library.zip") - def installfile(self, path, subpath=None, keep_content: bool = False): + def installfile(self, path: Path, subpath: Optional[Union[str, Path]] = None, keep_content: bool = False) -> None: folder = self.buildfolder if subpath: folder /= subpath @@ -268,7 +271,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): else: print('Warning,', path, 'not found') - def create_manifest(self, create_hashes=False): + def create_manifest(self, create_hashes: bool = False) -> None: # Since the setup is now split into components and the manifest is not, # it makes most sense to just remove the hashes for now. Not aware of anyone using them. hashes = {} @@ -290,7 +293,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): json.dump(manifest, open(manifestpath, "wt"), indent=4) print("Created Manifest") - def run(self): + def run(self) -> None: # start downloading sni asap sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") sni_thread.start() @@ -341,7 +344,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): # post build steps if is_windows: # kivy_deps is win32 only, linux picks them up automatically - from kivy_deps import sdl2, glew + from kivy_deps import sdl2, glew # type: ignore for folder in sdl2.dep_bins + glew.dep_bins: shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) print(f"copying {folder} -> {self.libfolder}") @@ -362,7 +365,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): self.installfile(Path(data)) # kivi data files - import kivy + import kivy # type: ignore[import-untyped] shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"), self.buildfolder / "data", dirs_exist_ok=True) @@ -372,7 +375,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): from worlds.AutoWorld import AutoWorldRegister assert not non_apworlds - set(AutoWorldRegister.world_types), \ f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" - folders_to_remove: typing.List[str] = [] + folders_to_remove: List[str] = [] disabled_worlds_folder = "worlds_disabled" for entry in os.listdir(disabled_worlds_folder): if os.path.isdir(os.path.join(disabled_worlds_folder, entry)): @@ -393,7 +396,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): shutil.rmtree(world_directory) shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") try: - from maseya import z3pr + from maseya import z3pr # type: ignore[import-untyped] except ImportError: print("Maseya Palette Shuffle not found, skipping data files.") else: @@ -444,16 +447,16 @@ class AppImageCommand(setuptools.Command): ("app-exec=", None, "The application to run inside the image."), ("yes", "y", 'Answer "yes" to all questions.'), ] - build_folder: typing.Optional[Path] - dist_file: typing.Optional[Path] - app_dir: typing.Optional[Path] + build_folder: Optional[Path] + dist_file: Optional[Path] + app_dir: Optional[Path] app_name: str - app_exec: typing.Optional[Path] - app_icon: typing.Optional[Path] # source file + app_exec: Optional[Path] + app_icon: Optional[Path] # source file app_id: str # lower case name, used for icon and .desktop yes: bool - def write_desktop(self): + def write_desktop(self) -> None: assert self.app_dir, "Invalid app_dir" desktop_filename = self.app_dir / f"{self.app_id}.desktop" with open(desktop_filename, 'w', encoding="utf-8") as f: @@ -468,7 +471,7 @@ class AppImageCommand(setuptools.Command): ))) desktop_filename.chmod(0o755) - def write_launcher(self, default_exe: Path): + def write_launcher(self, default_exe: Path) -> None: assert self.app_dir, "Invalid app_dir" launcher_filename = self.app_dir / "AppRun" with open(launcher_filename, 'w', encoding="utf-8") as f: @@ -491,7 +494,7 @@ $APPDIR/$exe "$@" """) launcher_filename.chmod(0o755) - def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): + def install_icon(self, src: Path, name: Optional[str] = None, symlink: Optional[Path] = None) -> None: assert self.app_dir, "Invalid app_dir" try: from PIL import Image @@ -513,7 +516,8 @@ $APPDIR/$exe "$@" if symlink: symlink.symlink_to(dest_file.relative_to(symlink.parent)) - def initialize_options(self): + def initialize_options(self) -> None: + assert self.distribution.metadata.name self.build_folder = None self.app_dir = None self.app_name = self.distribution.metadata.name @@ -527,17 +531,22 @@ $APPDIR/$exe "$@" )) self.yes = False - def finalize_options(self): + def finalize_options(self) -> None: + assert self.build_folder if not self.app_dir: self.app_dir = self.build_folder.parent / "AppDir" self.app_id = self.app_name.lower() - def run(self): + def run(self) -> None: + assert self.build_folder and self.dist_file, "Command not properly set up" + assert ( + self.app_icon and self.app_id and self.app_dir and self.app_exec and self.app_name + ), "AppImageCommand not properly set up" self.dist_file.parent.mkdir(parents=True, exist_ok=True) if self.app_dir.is_dir(): shutil.rmtree(self.app_dir) self.app_dir.mkdir(parents=True) - opt_dir = self.app_dir / "opt" / self.distribution.metadata.name + opt_dir = self.app_dir / "opt" / self.app_name shutil.copytree(self.build_folder, opt_dir) root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}' self.install_icon(self.app_icon, self.app_id, symlink=root_icon) @@ -548,7 +557,7 @@ $APPDIR/$exe "$@" subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) -def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: +def find_libs(*args: str) -> Sequence[Tuple[str, str]]: """Try to find system libraries to be included.""" if not args: return [] @@ -556,7 +565,7 @@ def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: arch = build_arch.replace('_', '-') libc = 'libc6' # we currently don't support musl - def parse(line): + def parse(line: str) -> Tuple[Tuple[str, str, str], str]: lib, path = line.strip().split(' => ') lib, typ = lib.split(' ', 1) for test_arch in ('x86-64', 'i386', 'aarch64'): @@ -577,26 +586,29 @@ def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: ldconfig = shutil.which("ldconfig") assert ldconfig, "Make sure ldconfig is in PATH" data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] - find_libs.cache = { # type: ignore [attr-defined] + find_libs.cache = { # type: ignore[attr-defined] k: v for k, v in (parse(line) for line in data if "=>" in line) } - def find_lib(lib, arch, libc): - for k, v in find_libs.cache.items(): + def find_lib(lib: str, arch: str, libc: str) -> Optional[str]: + cache: Dict[Tuple[str, str, str], str] = getattr(find_libs, "cache") + for k, v in cache.items(): if k == (lib, arch, libc): return v - for k, v, in find_libs.cache.items(): + for k, v, in cache.items(): if k[0].startswith(lib) and k[1] == arch and k[2] == libc: return v return None - res = [] + res: List[Tuple[str, str]] = [] for arg in args: # try exact match, empty libc, empty arch, empty arch and libc file = find_lib(arg, arch, libc) file = file or find_lib(arg, arch, '') file = file or find_lib(arg, '', libc) file = file or find_lib(arg, '', '') + if not file: + raise ValueError(f"Could not find lib {arg}") # resolve symlinks for n in range(0, 5): res.append((file, os.path.join('lib', os.path.basename(file)))) @@ -620,7 +632,7 @@ cx_Freeze.setup( "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 37ebe3f627..ab9383b3cd 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -4,6 +4,7 @@ import warnings 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 2dba147aca..c8bcec9581 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -688,8 +688,8 @@ class TestDistributeItemsRestrictive(unittest.TestCase): 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 @@ class TestBalanceMultiworldProgression(unittest.TestCase): 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 @@ class TestBalanceMultiworldProgression(unittest.TestCase): 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 @@ class TestBalanceMultiworldProgression(unittest.TestCase): 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 @@ class TestBalanceMultiworldProgression(unittest.TestCase): 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 @@ class TestBalanceMultiworldProgression(unittest.TestCase): 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 ee2f22a6dc..7a3743e5a4 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -21,6 +21,17 @@ class TestOptions(unittest.TestCase): 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 = [ @@ -67,4 +78,4 @@ class TestOptions(unittest.TestCase): if not world_type.hidden: for option_key, option in world_type.options_dataclass.type_hints.items(): with self.subTest(game=gamename, option=option_key): - pickle.dumps(option(option.default)) + pickle.dumps(option.from_any(option.default)) diff --git a/test/options/test_generate_templates.py b/test/options/test_generate_templates.py new file mode 100644 index 0000000000..cab97c54b1 --- /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 0000000000..1e3c65d8f9 --- /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 9281c9c753..51800a0ec5 100644 --- a/test/programs/test_generate.py +++ b/test/programs/test_generate.py @@ -92,3 +92,48 @@ class TestGenerateMain(unittest.TestCase): 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 0000000000..8b7a8ce0a8 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 68aba05f9d..1e6c1b88f4 100644 --- a/test/webhost/test_docs.py +++ b/test/webhost/test_docs.py @@ -30,10 +30,16 @@ class TestDocs(unittest.TestCase): 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 0000000000..58145d77f3 --- /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 2b984d9c88..b3f40be295 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 @@ class AutoSNIClientRegister(abc.ABCMeta): @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 f7dae2b927..3c4edc1b0c 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -10,7 +10,7 @@ from dataclasses import make_dataclass 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 @@ class World(metaclass=AutoWorldRegister): 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 fe6e44bb30..3c4c4477ef 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 f759b6309a..d0487494aa 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -3338,25 +3338,6 @@ inverted_default_dungeon_connections = [('Desert Palace Entrance (South)', 'Dese ('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 3cdbb1cb45..f897d37629 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -8,8 +8,7 @@ import typing 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 @@ class ALTTPWorld(World): 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 8fee56f393..69dd8a4dc6 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 5e729093f9..5a9f6aa9c9 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 8432028bf0..7663c20a29 100644 --- a/worlds/alttp/test/minor_glitches/TestMinor.py +++ b/worlds/alttp/test/minor_glitches/TestMinor.py @@ -2,7 +2,7 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool 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 67156eb972..e51970bc50 100644 --- a/worlds/alttp/test/owg/TestVanillaOWG.py +++ b/worlds/alttp/test/owg/TestVanillaOWG.py @@ -2,7 +2,7 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool 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 f5feedfb37..74a41a6289 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 7eebc349d4..9b5db7b122 100644 --- a/worlds/alttp/test/vanilla/TestVanilla.py +++ b/worlds/alttp/test/vanilla/TestVanilla.py @@ -2,7 +2,7 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool 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/aquaria/Regions.py b/worlds/aquaria/Regions.py index 792d7b73df..7a41e0d0c8 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -1152,79 +1152,79 @@ class AquariaRegions: def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Mithalas boss area, beating Mithalan God", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Temple boss area, beating Sun God", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sunken City, bulb on top of the boss area", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Home Water, Nautilus Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Mithalas City Castle, beating the Priests", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Mermog cave, Piranha Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Octopus Cave, Dumbo Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Final Boss area, bulb in the boss third form room", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Bubble Cave, Verse Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Temple, Sun Key", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("The Body bottom area, Mutant Costume", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement def adjusting_rules(self, options: AquariaOptions) -> None: """ diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index f015b26de1..517af3028d 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -49,7 +49,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): for location in self.unfillable_locations: for item_name in self.world.item_names: item = self.get_item_by_name(item_name) - if item.classification == ItemClassification.progression: + if item.advancement: self.assertFalse( self.world.get_location(location).can_fill(self.multiworld.state, item, False), "The location \"" + location + "\" can be filled with \"" + item_name + "\"") diff --git a/worlds/bumpstik/test/__init__.py b/worlds/bumpstik/test/__init__.py index 1199d7b8e5..5f40426e5b 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 9ba57b0591..0b9b7105bf 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -11,19 +11,18 @@ client_version = 7 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 0d384acc8f..1bd069a2ce 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 2430cc5ffc..cec5f551b9 100644 --- a/worlds/cv64/client.py +++ b/worlds/cv64/client.py @@ -66,8 +66,9 @@ class Castlevania64Client(BizHawkClient): 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 @@ class Castlevania64Client(BizHawkClient): 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 938b615b32..6ef4eafb67 100644 --- a/worlds/cv64/data/patches.py +++ b/worlds/cv64/data/patches.py @@ -197,6 +197,23 @@ deathlink_nitro_edition = [ 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 ab4371b0ac..db621c7101 100644 --- a/worlds/cv64/rom.py +++ b/worlds/cv64/rom.py @@ -357,8 +357,12 @@ class CV64PatchExtensions(APPatchExtension): # 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 1aec6945eb..765ffb1fc5 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1568,6 +1568,16 @@ class DarkSouls3World(World): "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 9755cce1c6..484afdce3f 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 e590cd732b..7acdad465d 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 3e9acac7e7..c6c594b6a0 100644 --- a/worlds/dlcquest/test/TestOptionsLong.py +++ b/worlds/dlcquest/test/TestOptionsLong.py @@ -5,7 +5,6 @@ from Options import NamedRange 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 8a39b43a2c..0432ae8b60 100644 --- a/worlds/dlcquest/test/__init__.py +++ b/worlds/dlcquest/test/__init__.py @@ -4,7 +4,7 @@ from typing import Dict, FrozenSet, Tuple, Any 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 cc2fa7f51a..48c919c0f6 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 23dfa0633e..3c35c4cb09 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -304,13 +304,13 @@ def stream_factorio_output(pipe, queue, process): 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 7eec718758..7dee04afbe 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -35,9 +35,11 @@ base_info = { "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 generate_mod(world: "Factorio", output_directory: str): "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 788d1f9e1d..5a41250fa7 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 112cc49f09..6111462e8c 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 @@ items_future = pool.submit(load_json_data, "items") 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 @@ class FactorioElement: 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 @@ class Technology(FactorioElement): # maybe make subclass of Location? 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 @@ class Technology(FactorioElement): # maybe make subclass of Location? 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 @@ class Recipe(FactorioElement): 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 @@ recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source # 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 @@ for recipe_name, recipe_data in raw_recipes.items(): 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 @@ del machines_future # 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 @@ required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict 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 @@ progressive_rows["progressive-processing"] = ( "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 @@ sorted_rows = sorted(progressive_rows) 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 @@ progressive_technology_table: Dict[str, Technology] = {} 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 925327655a..9f1f3cb573 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -2,10 +2,11 @@ from __future__ import annotations 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 .Mod import generate_mod 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 @@ class Factorio(World): 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 @@ class Factorio(World): 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 @@ class Factorio(World): 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 @@ class Factorio(World): 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 @@ class Factorio(World): 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 448ccf4e49..6972690f53 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 fa34430f40..d9ec7befba 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 15a79580d0..c8629ab8be 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 2b18f119a4..7be7403e48 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 ace231e12b..b08608a60a 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -105,8 +105,8 @@ function on_player_changed_position(event) end local target_direction = exit_table[outbound_direction] - local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, - (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} + local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, + (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} target_position = character.surface.find_non_colliding_position(character.prototype.name, target_position, 32, 0.5) if target_position ~= nil then @@ -134,40 +134,96 @@ end script.on_event(defines.events.on_player_changed_position, on_player_changed_position) {% endif %} - +function count_energy_bridges() + local count = 0 + for i, bridge in pairs(storage.energy_link_bridges) do + if validate_energy_link_bridge(i, bridge) then + count = count + 1 + (bridge.quality.level * 0.3) + end + end + return count +end +function get_energy_increment(bridge) + return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level) +end function on_check_energy_link(event) --- assuming 1 MJ increment and 5MJ battery: --- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing if event.tick % 60 == 30 then - local surface = game.get_surface(1) local force = "player" - local bridges = surface.find_entities_filtered({name="ap-energy-bridge", force=force}) - local bridgecount = table_size(bridges) - global.forcedata[force].energy_bridges = bridgecount - if global.forcedata[force].energy == nil then - global.forcedata[force].energy = 0 + local bridges = storage.energy_link_bridges + local bridgecount = count_energy_bridges() + storage.forcedata[force].energy_bridges = bridgecount + if storage.forcedata[force].energy == nil then + storage.forcedata[force].energy = 0 end - if global.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) - bridge.energy = bridge.energy - ENERGY_INCREMENT + if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then + for i, bridge in pairs(bridges) do + if validate_energy_link_bridge(i, bridge) then + energy_increment = get_energy_increment(bridge) + if bridge.energy > energy_increment*3 then + storage.forcedata[force].energy = storage.forcedata[force].energy + (energy_increment * ENERGY_LINK_EFFICIENCY) + bridge.energy = bridge.energy - energy_increment + end end end end - for i, bridge in ipairs(bridges) do - if global.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 - bridge.energy = bridge.energy + ENERGY_INCREMENT + for i, bridge in pairs(bridges) do + if validate_energy_link_bridge(i, bridge) then + energy_increment = get_energy_increment(bridge) + if storage.forcedata[force].energy < energy_increment and bridge.quality.level == 0 then + break + end + if bridge.energy < energy_increment*2 and storage.forcedata[force].energy > energy_increment then + storage.forcedata[force].energy = storage.forcedata[force].energy - energy_increment + bridge.energy = bridge.energy + energy_increment + end end end end end +function string_starts_with(str, start) + return str:sub(1, #start) == start +end +function validate_energy_link_bridge(unit_number, entity) + if not entity then + if storage.energy_link_bridges[unit_number] == nil then return false end + storage.energy_link_bridges[unit_number] = nil + return false + end + if not entity.valid then + if storage.energy_link_bridges[unit_number] == nil then return false end + storage.energy_link_bridges[unit_number] = nil + return false + end + return true +end +function on_energy_bridge_constructed(entity) + if entity and entity.valid then + if string_starts_with(entity.prototype.name, "ap-energy-bridge") then + storage.energy_link_bridges[entity.unit_number] = entity + end + end +end +function on_energy_bridge_removed(entity) + if string_starts_with(entity.prototype.name, "ap-energy-bridge") then + if storage.energy_link_bridges[entity.unit_number] == nil then return end + storage.energy_link_bridges[entity.unit_number] = nil + end +end if (ENERGY_INCREMENT) then script.on_event(defines.events.on_tick, on_check_energy_link) + + script.on_event({defines.events.on_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.on_robot_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.on_entity_cloned}, function(event) on_energy_bridge_constructed(event.destination) end) + + script.on_event({defines.events.script_raised_revive}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.script_raised_built}, function(event) on_energy_bridge_constructed(event.entity) end) + + script.on_event({defines.events.on_entity_died}, function(event) on_energy_bridge_removed(event.entity) end) + script.on_event({defines.events.on_player_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end) + script.on_event({defines.events.on_robot_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end) end {% if not imported_blueprints -%} @@ -186,23 +242,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 +288,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 +309,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 +340,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 +360,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 +396,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 +405,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 +453,20 @@ 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 = {} + storage.energy_link_bridges = {} -- Fire dummy events for all currently existing forces. local e = {} for name, _ in pairs(game.forces) do @@ -420,12 +502,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 +559,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 +619,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 +642,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 +670,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 +685,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 +701,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 +740,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 +765,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 +781,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 +789,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 +801,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 +811,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 +827,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 3021fd5dad..dc068c4f62 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 82053453ea..43151ff008 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 1b271031a3..f1530359c8 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 73e131a60e..41d30e58d5 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 4c4ab81526..b0633b493d 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 10279db379..80c00fe3df 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 d9977f2986..ecb31126e1 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/generic/Rules.py b/worlds/generic/Rules.py index e930c4b8d6..31d725bff7 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -69,7 +69,7 @@ def locality_rules(multiworld: MultiWorld): if (location.player, location.item_rule) in func_cache: location.item_rule = func_cache[location.player, location.item_rule] # empty rule that just returns True, overwrite - elif location.item_rule is location.__class__.item_rule: + elif location.item_rule is Location.item_rule: func_cache[location.player, location.item_rule] = location.item_rule = \ lambda i, sending_blockers = forbid_data[location.player], \ old_rule = location.item_rule: \ @@ -103,7 +103,7 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"): old_rule = spot.access_rule # empty rule, replace instead of add - if old_rule is spot.__class__.access_rule: + if old_rule is Location.access_rule or old_rule is Entrance.access_rule: spot.access_rule = rule if combine == "and" else old_rule else: if combine == "and": @@ -115,7 +115,7 @@ def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], def forbid_item(location: "BaseClasses.Location", item: str, player: int): old_rule = location.item_rule # empty rule - if old_rule is location.__class__.item_rule: + if old_rule is Location.item_rule: location.item_rule = lambda i: i.name != item or i.player != player else: location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) @@ -135,7 +135,7 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"): old_rule = location.item_rule # empty rule, replace instead of add - if old_rule is location.__class__.item_rule: + if old_rule is Location.item_rule: location.item_rule = rule if combine == "and" else old_rule else: if combine == "and": diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 9ec77e6bf0..486aa164cd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -509,9 +509,13 @@ class HKWorld(World): 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 0000000000..c41d20127f --- /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 0000000000..dba15b614d --- /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/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index ed4d90bb54..9fe9b23a13 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -36,10 +36,16 @@ When you generate a game you will see a download link for a KH2 .zip seed on the Make sure the seed is on the top of the list (Highest Priority)
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms. +

Optional Software:

+ +- [Kingdom Hearts 2 AP Tracker](https://github.com/palex00/kh2-ap-tracker/releases/latest/), for use with +[PopTracker](https://github.com/black-sliver/PopTracker/releases) +

What the Mod Manager Should Look Like.

![image](https://i.imgur.com/Si4oZ8w.png) +

Using the KH2 Client

Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
@@ -73,10 +79,24 @@ Enter `The room's port number` into the top box where the x's are and pr - Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out. - Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed -

Logic Sheet

+

Logic Sheet & PopTracker Autotracking

Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing) +Alternatively you can use the Kingdom Hearts 2 PopTracker Pack that is based off of the logic sheet above and does all the work for you. + +

PopTracker Pack

+ +1. Download [Kingdom Hearts 2 AP Tracker](https://github.com/palex00/kh2-ap-tracker/releases/latest/) and +[PopTracker](https://github.com/black-sliver/PopTracker/releases). +2. Put the tracker pack into packs/ in your PopTracker install. +3. Open PopTracker, and load the Kingdom Hearts 2 pack. +4. For autotracking, click on the "AP" symbol at the top. +5. Enter the Archipelago server address (the one you connected your client to), slot name, and password. + +This pack will handle logic, received items, checked locations and autotabbing for you! + +

F.A.Q.

- Why is my Client giving me a "Cannot Open Process: " error? diff --git a/worlds/ladx/test/__init__.py b/worlds/ladx/test/__init__.py index 0e616ac557..059a09b072 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 2b3dc41239..8463e56e54 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 @@ class LandstalkerWorld(World): 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 @@ class LandstalkerWorld(World): 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 bbed146453..3783b68af9 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 789fc0856d..9abb0276c8 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 2fd57ff5ed..2d6e9967df 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 24925675e3..306ffa7716 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/mm2/__init__.py b/worlds/mm2/__init__.py index 07e1823f93..4a43ee8df0 100644 --- a/worlds/mm2/__init__.py +++ b/worlds/mm2/__init__.py @@ -96,13 +96,13 @@ class MM2World(World): location_name_groups = location_groups web = MM2WebWorld() rom_name: bytearray - world_version: Tuple[int, int, int] = (0, 3, 1) + world_version: Tuple[int, int, int] = (0, 3, 2) wily_5_weapons: Dict[int, List[int]] - def __init__(self, world: MultiWorld, player: int): + def __init__(self, multiworld: MultiWorld, player: int): self.rom_name = bytearray() self.rom_name_available_event = threading.Event() - super().__init__(world, player) + super().__init__(multiworld, player) self.weapon_damage = deepcopy(weapon_damage) self.wily_5_weapons = {} diff --git a/worlds/mm2/rules.py b/worlds/mm2/rules.py index eddd099274..7e2ce1f3c7 100644 --- a/worlds/mm2/rules.py +++ b/worlds/mm2/rules.py @@ -133,28 +133,6 @@ def set_rules(world: "MM2World") -> None: # Wily Machine needs all three weaknesses present, so allow elif 4 > world.weapon_damage[weapon][i] > 0: world.weapon_damage[weapon][i] = 0 - # handle special cases - for boss in range(14): - for weapon in (1, 3, 6, 8): - if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and - not any(world.weapon_damage[i][boss] > 0 for i in range(1, 8) if i != weapon)): - # Weapon does not have enough possible ammo to kill the boss, raise the damage - if boss == 9: - if weapon != 3: - # Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness - world.weapon_damage[weapon][boss] = 0 - weakness = world.random.choice((2, 3, 4, 5, 7, 8)) - world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] - elif boss == 11: - if weapon == 1: - # Atomic Fire cannot be Boobeam Trap's only weakness - world.weapon_damage[weapon][boss] = 0 - weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8)) - world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] - else: - world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] - starting = world.options.starting_robot_master.value - world.weapon_damage[0][starting] = 1 for p_boss in world.options.plando_weakness: for p_weapon in world.options.plando_weakness[p_boss]: @@ -168,6 +146,28 @@ def set_rules(world: "MM2World") -> None: world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \ = world.options.plando_weakness[p_boss][p_weapon] + # handle special cases + for boss in range(14): + for weapon in (1, 2, 3, 6, 8): + if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and + not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon] + for i in range(9) if i != weapon)): + # Weapon does not have enough possible ammo to kill the boss, raise the damage + if boss == 9: + if weapon in (1, 6): + # Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness + world.weapon_damage[weapon][boss] = 0 + weakness = world.random.choice((2, 3, 4, 5, 7, 8)) + world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] + elif boss == 11: + if weapon == 1: + # Atomic Fire cannot be Boobeam Trap's only weakness + world.weapon_damage[weapon][boss] = 0 + weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8)) + world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] + else: + world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] + if world.weapon_damage[0][world.options.starting_robot_master.value] < 1: world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value] @@ -209,11 +209,11 @@ def set_rules(world: "MM2World") -> None: continue highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys())) uses = weapon_energy[wp] // weapon_costs[wp] - used_weapons[boss].add(wp) if int(uses * boss_damage[wp]) > boss_health[boss]: used = ceil(boss_health[boss] / boss_damage[wp]) weapon_energy[wp] -= weapon_costs[wp] * used boss_health[boss] = 0 + used_weapons[boss].add(wp) elif highest <= 0: # we are out of weapons that can actually damage the boss # so find the weapon that has the most uses, and apply that as an additional weakness @@ -221,18 +221,21 @@ def set_rules(world: "MM2World") -> None: # Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should # be able to cover wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight - if weapon != 0) + if weapon != 0 and (weapon != 8 or boss != 12)) + # Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp] used = min(int(weapon_energy[wp] // weapon_costs[wp]), - ceil(boss_health[boss] // minimum_weakness_requirement[wp])) + ceil(boss_health[boss] / minimum_weakness_requirement[wp])) weapon_energy[wp] -= weapon_costs[wp] * used boss_health[boss] -= int(used * minimum_weakness_requirement[wp]) weapon_weight.pop(wp) + used_weapons[boss].add(wp) else: # drain the weapon and continue boss_health[boss] -= int(uses * boss_damage[wp]) weapon_energy[wp] -= weapon_costs[wp] * uses weapon_weight.pop(wp) + used_weapons[boss].add(wp) world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons} diff --git a/worlds/mm2/text.py b/worlds/mm2/text.py index 32d665bf6c..7dda12ac03 100644 --- a/worlds/mm2/text.py +++ b/worlds/mm2/text.py @@ -1,7 +1,7 @@ from typing import DefaultDict from collections import defaultdict -MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda x: 0x6F, { +MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x6F, { ' ': 0x40, 'A': 0x41, 'B': 0x42, diff --git a/worlds/mmbn3/Items.py b/worlds/mmbn3/Items.py index 2e249ce79e..30ec311ecb 100644 --- a/worlds/mmbn3/Items.py +++ b/worlds/mmbn3/Items.py @@ -171,7 +171,7 @@ chipList: typing.List[ItemData] = [ 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 441bdc591c..677eff22b3 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 1f1a2a011c..d913449ed5 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 7164aa3e13..e647c18d70 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/oot/__init__.py b/worlds/oot/__init__.py index c3925bf2a8..975902ae6e 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1348,23 +1348,9 @@ class OOTWorld(World): def get_locations(self): return self.multiworld.get_locations(self.player) - def get_location(self, location): - return self.multiworld.get_location(location, self.player) - - def get_region(self, region_name): - try: - return self._regions_cache[region_name] - except KeyError: - ret = self.multiworld.get_region(region_name, self.player) - self._regions_cache[region_name] = ret - return ret - def get_entrances(self): return self.multiworld.get_entrances(self.player) - def get_entrance(self, entrance): - return self.multiworld.get_entrance(entrance, self.player) - def is_major_item(self, item: OOTItem): if item.type == 'Token': return self.bridge == 'tokens' or self.lacs_condition == 'tokens' diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 9ed55f218d..58f23a2bc1 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -190,12 +190,83 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 @@ class OSRSWorld(World): 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 20111aa01d..cf26850949 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 @@ level_logic = { }, ), ( # 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 2d7db0dad4..0dd874b250 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 9a3991e97f..732b2092a2 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 e05b5d96ac..8fcc74d1c3 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 2c0b5021d0..e2a7a4800b 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -73,6 +73,7 @@ _FANFARES: Dict[str, int] = { "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 84ce64003d..bf2a8da5b0 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 75a2417dfb..d1b5b01dcf 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 2065507e0d..809179cbef 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -526,15 +526,24 @@ class PokemonRedBlueWorld(World): # 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 @@ class PokemonRedBlueWorld(World): "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 ba4bfd471c..3c1cdc57e9 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -94,6 +94,9 @@ def set_rules(multiworld, world, player): "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 290f4a60ac..7ffdd459db 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -46,30 +46,6 @@ class RLWorld(World): 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 bf9c06fa3f..53f41f4e4c 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 60ec4bbe13..8269d3a262 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 833ae56ca3..d4bafbafcc 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -55,7 +55,7 @@ class SM64World(World): 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 @@ class SM64World(World): 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/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 7456bcb70b..afb5bad50f 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -36,6 +36,8 @@ Then continue to `Using the Launcher` - Windows: If you did not use the default install directory for MSYS, close this window, check `Show advanced options` and reopen using `Re-check Requirements`. You can then set the path manually. 5. When finished, use `Compile default SM64AP build` to continue - Advanced user can use `Show advanced options` to build with custom makeflags (`BETTERCAMERA`, `NODRAWINGDISTANCE`, ...), different repos and branches, and game patches such as 60FPS, Enhanced Moveset and others. + - [Available Makeflags](https://github.com/sm64pc/sm64ex/wiki/Build-options) + - [Included Game Patches](https://github.com/N00byKing/sm64ex/blob/archipelago/enhancements/README.md) 6. Press `Download Files` to prepare the build, afterwards `Create Build`. 7. SM64EX will now be compiled. This can take a while. diff --git a/worlds/stardew_valley/data/craftable_data.py b/worlds/stardew_valley/data/craftable_data.py index d83478a620..713db47320 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 @@ bait_maker = skill_recipe(Machine.bait_maker, Skill.fishing, 6, {MetalBar.iron: 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 24b03bf77b..ead4d62f16 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -94,6 +94,11 @@ class SkillSource(RecipeSource): 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 e346e4ba23..0403230eee 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -14,7 +14,7 @@ from .special_order_logic import SpecialOrderLogicMixin 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 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]): 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 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]): 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/presets.py b/worlds/stardew_valley/presets.py index 1861a91423..62672f29e4 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -41,9 +41,7 @@ all_random_settings = { Friendsanity.internal_name: "random", FriendsanityHeartSize.internal_name: "random", Booksanity.internal_name: "random", - Walnutsanity.internal_name: "random", NumberOfMovementBuffs.internal_name: "random", - EnabledFillerBuffs.internal_name: "random", ExcludeGingerIsland.internal_name: "random", TrapItems.internal_name: "random", MultipleDaySleepEnabled.internal_name: "random", diff --git a/worlds/stardew_valley/test/assertion/rule_assert.py b/worlds/stardew_valley/test/assertion/rule_assert.py index 5a1dad2925..1031a18e11 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 @@ class RuleAssertMixin(TestCase): 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 @@ class RuleAssertMixin(TestCase): 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 ab376c90d4..0bc7f9bfdf 100644 --- a/worlds/stardew_valley/test/rules/TestBundles.py +++ b/worlds/stardew_valley/test/rules/TestBundles.py @@ -56,6 +56,7 @@ class TestRaccoonBundlesLogic(SVTestBase): 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 93c325ae5c..4719edea1d 100644 --- a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -50,6 +50,23 @@ class TestCraftsanityLogic(SVTestBase): 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 @@ class TestNoCraftsanityLogic(SVTestBase): 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/subnautica/options.py b/worlds/subnautica/options.py index 4bdd9aafa5..6cdcb33d89 100644 --- a/worlds/subnautica/options.py +++ b/worlds/subnautica/options.py @@ -112,8 +112,7 @@ class AggressiveScanLogic(Choice): class SubnauticaDeathLink(DeathLink): - """When you die, everyone dies. Of course the reverse is true too. - Note: can be toggled via in-game console command "deathlink".""" + __doc__ = DeathLink.__doc__ + "\n\n Note: can be toggled via in-game console command \"deathlink\"." class FillerItemsDistribution(ItemDict): diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py index 9715cc6842..f95e5d8044 100644 --- a/worlds/tloz/Locations.py +++ b/worlds/tloz/Locations.py @@ -108,11 +108,15 @@ sword_cave_locations = [ ] food_locations = [ - "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", + "Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", "Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)" ] +gohma_locations = [ + "Level 6 Boss", "Level 6 Triforce", "Level 8 Item (Magical Key)", "Level 8 Bomb Drop (Darknuts North)" +] + gleeok_locations = [ "Level 4 Boss", "Level 4 Triforce", "Level 8 Boss", "Level 8 Triforce" ] diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index 39c3b954f0..de627a533b 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from worlds.generic.Rules import add_rule -from .Locations import food_locations, shop_locations, gleeok_locations +from .Locations import food_locations, shop_locations, gleeok_locations, gohma_locations from .ItemPool import dangerous_weapon_locations from .Options import StartingPosition @@ -10,13 +10,12 @@ if TYPE_CHECKING: def set_rules(tloz_world: "TLoZWorld"): player = tloz_world.player - world = tloz_world.multiworld options = tloz_world.options # Boss events for a nicer spoiler log play through for level in range(1, 9): - boss = world.get_location(f"Level {level} Boss", player) - boss_event = world.get_location(f"Level {level} Boss Status", player) + boss = tloz_world.get_location(f"Level {level} Boss") + boss_event = tloz_world.get_location(f"Level {level} Boss Status") status = tloz_world.create_event(f"Boss {level} Defeated") boss_event.place_locked_item(status) add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player)) @@ -26,136 +25,131 @@ def set_rules(tloz_world: "TLoZWorld"): for location in level.locations: if options.StartingPosition < StartingPosition.option_dangerous \ or location.name not in dangerous_weapon_locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("weapons", player)) # This part of the loop sets up an expected amount of defense needed for each dungeon if i > 0: # Don't need an extra heart for Level 1 - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state, hearts=i: state.has("Heart Container", player, hearts) or (state.has("Blue Ring", player) and state.has("Heart Container", player, int(hearts / 2))) or (state.has("Red Ring", player) and state.has("Heart Container", player, int(hearts / 4)))) if "Pols Voice" in location.name: # This enemy needs specific weapons - add_rule(world.get_location(location.name, player), - lambda state: state.has_group("swords", player) or state.has("Bow", player)) + add_rule(tloz_world.get_location(location.name), + lambda state: state.has_group("swords", player) or + (state.has("Bow", player) and state.has_group("arrows", player))) # No requiring anything in a shop until we can farm for money for location in shop_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has_group("weapons", player)) # Everything from 4 on up has dark rooms for level in tloz_world.levels[4:]: for location in level.locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("candles", player) or (state.has("Magical Rod", player) and state.has("Book of Magic", player))) # Everything from 5 on up has gaps for level in tloz_world.levels[5:]: for location in level.locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Stepladder", player)) - add_rule(world.get_location("Level 5 Boss", player), + # Level 4 Access + for location in tloz_world.levels[4].locations: + add_rule(tloz_world.get_location(location.name), + lambda state: state.has_any(("Raft", "Recorder"), player)) + + # Digdogger boss. Rework this once ER happens + add_rule(tloz_world.get_location("Level 5 Boss"), + lambda state: state.has("Recorder", player)) + add_rule(tloz_world.get_location("Level 5 Triforce"), lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 6 Boss", player), - lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + for location in gohma_locations: + if options.ExpandedPool or "Drop" not in location: + add_rule(tloz_world.get_location(location), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) - add_rule(world.get_location("Level 7 Item (Red Candle)", player), - lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Boss", player), - lambda state: state.has("Recorder", player)) - if options.ExpandedPool: - add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player), - lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player), - lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player), + # Recorder Access for Level 7 + for location in tloz_world.levels[7].locations: + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Recorder", player)) for location in food_locations: if options.ExpandedPool or "Drop" not in location: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Food", player)) for location in gleeok_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has_group("swords", player) or state.has("Magical Rod", player)) # Candle access for Level 8 for location in tloz_world.levels[8].locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("candles", player)) - add_rule(world.get_location("Level 8 Item (Magical Key)", player), + add_rule(tloz_world.get_location("Level 8 Item (Magical Key)"), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) if options.ExpandedPool: - add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), + add_rule(tloz_world.get_location("Level 8 Bomb Drop (Darknuts North)"), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) for location in tloz_world.levels[9].locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Triforce Fragment", player, 8) and state.has_group("swords", player)) # Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop for level in range(1, 9): - add_rule(world.get_location(f"Level {level} Triforce", player), + add_rule(tloz_world.get_location(f"Level {level} Triforce"), lambda state, l=level: state.has(f"Boss {l} Defeated", player)) # Sword, raft, and ladder spots - add_rule(world.get_location("White Sword Pond", player), + add_rule(tloz_world.get_location("White Sword Pond"), lambda state: state.has("Heart Container", player, 2)) - add_rule(world.get_location("Magical Sword Grave", player), + add_rule(tloz_world.get_location("Magical Sword Grave"), lambda state: state.has("Heart Container", player, 9)) stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"] stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"] for location in stepladder_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Stepladder", player)) if options.ExpandedPool: for location in stepladder_locations_expanded: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Stepladder", player)) # Don't allow Take Any Items until we can actually get in one if options.ExpandedPool: - add_rule(world.get_location("Take Any Item Left", player), + add_rule(tloz_world.get_location("Take Any Item Left"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Middle", player), + add_rule(tloz_world.get_location("Take Any Item Middle"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Right", player), + add_rule(tloz_world.get_location("Take Any Item Right"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - for location in tloz_world.levels[4].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Raft", player) or state.has("Recorder", player)) - for location in tloz_world.levels[7].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Recorder", player)) - for location in tloz_world.levels[8].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Bow", player)) - add_rule(world.get_location("Potion Shop Item Left", player), + add_rule(tloz_world.get_location("Potion Shop Item Left"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Potion Shop Item Middle", player), + add_rule(tloz_world.get_location("Potion Shop Item Middle"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Potion Shop Item Right", player), + add_rule(tloz_world.get_location("Potion Shop Item Right"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Shield Shop Item Left", player), + add_rule(tloz_world.get_location("Shield Shop Item Left"), lambda state: state.has_group("candles", player) or state.has("Bomb", player)) - add_rule(world.get_location("Shield Shop Item Middle", player), + add_rule(tloz_world.get_location("Shield Shop Item Middle"), lambda state: state.has_group("candles", player) or state.has("Bomb", player)) - add_rule(world.get_location("Shield Shop Item Right", player), + add_rule(tloz_world.get_location("Shield Shop Item Right"), lambda state: state.has_group("candles", player) or - state.has("Bomb", player)) \ No newline at end of file + state.has("Bomb", player)) diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index c05bb98f91..d5fdc55c3a 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_data.py b/worlds/tunic/er_data.py index 529488307d..605c702ff1 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -835,7 +835,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { [], # drop a rudeling, icebolt or ice bomb "Overworld to West Garden from Furnace": - [["IG3"]], + [["IG3"], ["LS1"]], }, "East Overworld": { "Above Ruined Passage": diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 5f92528d92..6c2df1a692 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -379,9 +379,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"]) @@ -534,9 +535,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Entry Point"]) + # ice grapple through the wall, get the little secret sound to trigger regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Main"], - rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Upper"], rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) @@ -838,12 +841,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ fort_east_upper_lower = regions["Fortress East Shortcut Upper"].connect( connecting_region=regions["Fortress East Shortcut Lower"]) - # nmg: can ice grapple upwards regions["Fortress East Shortcut Lower"].connect( connecting_region=regions["Fortress East Shortcut Upper"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - # nmg: ice grapple through the big gold door, can do it both ways regions["Eastern Vault Fortress"].connect( connecting_region=regions["Eastern Vault Fortress Gold Door"], rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", @@ -876,7 +877,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: state.has(laurels, player)) # reverse connection is conditionally made later, depending on whether combat logic is on, and the details of ER - # nmg: ice grapple from upper grave path to lower regions["Fortress Grave Path Upper"].connect( connecting_region=regions["Fortress Grave Path Entry"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) @@ -1224,6 +1224,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ for portal_dest in region_info.portals: ls_connect(ladder_region, "Overworld Redux, " + portal_dest) + # convenient staircase means this one is easy difficulty, even though there's an elevation change + ls_connect("LS Elev 0", "Overworld Redux, Furnace_gyro_west") + # connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail if options.ladder_storage >= LadderStorage.option_medium: for ladder_region, region_info in ow_ladder_groups.items(): @@ -1239,6 +1242,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ if options.ladder_storage >= LadderStorage.option_hard: ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_") ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_") + ls_connect("LS Elev 2", "Overworld Redux, Ruins Passage_west") ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house") ls_connect("LS Elev 5", "Overworld Redux, Temple_main") diff --git a/worlds/tunic/ladder_storage_data.py b/worlds/tunic/ladder_storage_data.py index 37d2432e3e..f2d4b94406 100644 --- a/worlds/tunic/ladder_storage_data.py +++ b/worlds/tunic/ladder_storage_data.py @@ -17,7 +17,7 @@ ow_ladder_groups: Dict[str, OWLadderInfo] = { ["Overworld Beach"]), # also the east filigree room "LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"}, - ["Furnace_gyro_lower", "Swamp Redux 2_wall"], + ["Furnace_gyro_lower", "Furnace_gyro_west", "Swamp Redux 2_wall"], ["Overworld Tunnel Turret"]), # also the fountain filigree room and ruined passage door "LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"}, diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 5d849d1633..c5629c551b 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -235,7 +235,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" @@ -253,7 +253,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 c9848f2ffe..ac9197bd92 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 @@ -78,7 +80,7 @@ class WitnessWorld(World): def _get_slot_data(self) -> Dict[str, Any]: return { - "seed": self.random.randrange(0, 1000000), + "seed": self.options.puzzle_randomization_seed.value, "victory_location": int(self.player_logic.VICTORY_LOCATION, 16), "panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID, "item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(), diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index fabd142881..8fadf68c31 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 200138dee1..c6d6efa964 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 67a42ba7e4..1186c47023 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 a3c388dfb1..31263aa337 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 99e8eea2eb..dac7e3fb4d 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" diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 4de966abe9..e1462cc375 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -401,6 +401,16 @@ class DeathLinkAmnesty(Range): default = 1 +class PuzzleRandomizationSeed(Range): + """ + Sigma Rando, which is the basis for all puzzle randomization in this randomizer, uses a seed from 1 to 9999999 for the puzzle randomization. + This option lets you set this seed yourself. + """ + range_start = 1 + range_end = 9999999 + default = "random" + + @dataclass class TheWitnessOptions(PerGameCommonOptions): puzzle_randomization: PuzzleRandomization @@ -435,6 +445,7 @@ class TheWitnessOptions(PerGameCommonOptions): laser_hints: LaserHints death_link: DeathLink death_link_amnesty: DeathLinkAmnesty + puzzle_randomization_seed: PuzzleRandomizationSeed shuffle_dog: ShuffleDog @@ -483,6 +494,7 @@ witness_option_groups = [ ElevatorsComeToYou, DeathLink, DeathLinkAmnesty, + PuzzleRandomizationSeed, ]), OptionGroup("Silly Options", [ ShuffleDog, diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 4c98cb7849..3be298ebcc 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -2,7 +2,7 @@ Defines progression, junk and event items for The Witness """ import copy -from typing import TYPE_CHECKING, Dict, List, Set, cast +from typing import TYPE_CHECKING, Dict, List, Set from BaseClasses import Item, ItemClassification, MultiWorld diff --git a/worlds/yachtdice/Options.py b/worlds/yachtdice/Options.py index e687936224..f311caa5a9 100644 --- a/worlds/yachtdice/Options.py +++ b/worlds/yachtdice/Options.py @@ -80,7 +80,7 @@ class NumberRollFragmentsPerRoll(Range): """ Rolls can be split into fragments, gathering enough will give you an extra roll. You start with one roll, and there will always be one full roll in the pool. - The other three rolls are split into fragments, according to this option. + The other rolls are split into fragments, according to this option. Setting this to 1 fragment per roll just puts "Roll" objects in the pool. """ diff --git a/worlds/yachtdice/docs/en_Yacht Dice.md b/worlds/yachtdice/docs/en_Yacht Dice.md index 53eefe9e9c..c671dcee50 100644 --- a/worlds/yachtdice/docs/en_Yacht Dice.md +++ b/worlds/yachtdice/docs/en_Yacht Dice.md @@ -3,7 +3,7 @@ Welcome to Yacht Dice, the ultimate dice-rolling adventure in Archipelago! Cast your dice, chase high scores, and unlock valuable treasures. Discover new dice, extra rolls, multipliers, and special scoring categories to enhance your game. Roll your way to victory by reaching the target score! ## Understanding Location Checks -In Yacht Dice, location checks happen when you hit certain scores for the first time. The target score for your next location check is always displayed on the website. +In Yacht Dice, location checks happen when you hit certain scores for the first time. The target score for your next location check is always displayed in the game. ## Items and Their Effects When you receive an item, it could be extra dice, extra rolls, score multipliers, or new scoring categories. These boosts help you sail towards higher scores and more loot. Other items include extra points, lore, and fun facts to enrich your journey. diff --git a/worlds/yachtdice/docs/setup_en.md b/worlds/yachtdice/docs/setup_en.md index c76cd398ce..f6c15af2b6 100644 --- a/worlds/yachtdice/docs/setup_en.md +++ b/worlds/yachtdice/docs/setup_en.md @@ -3,19 +3,13 @@ ## Required Software - A browser (you are probably using one right now!). -- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases). ## Playing the game Open the Yacht Dice website. There are two options: -- Download the latest release from [Yacht Dice Release](https://github.com/spinerak/ArchipelagoYachtDice/releases/latest) and unzip the Website.zip. Then open player.html in your browser. -- Cruise over to the [Yacht Dice website](https://yacht-dice-ap.netlify.app/). This also works on mobile. If the website is not available, use the first option. +- Cruise over to the [Yacht Dice Website](https://yacht-dice-ap.netlify.app/). This is the easiest option. If the website is unavailable, use the next option. +- Download the latest release from [Yacht Dice Release](https://github.com/spinerak/ArchipelagoYachtDice/releases/latest) and unzip the Website.zip. Then open index.html in your browser. -Both options have an "offline" play option to try out the game without having to generate a game first. +Press Archipelago, and after logging in, you are good to go. The website has a built-in client, where you can chat and send commands. +Both options also have a "Solo play" mode to try out the game without having to generate a game first. -## Play with Archipelago - -- Create your yaml file via the [Yacht Dice Player Options Page](../player-options). -- After generating, open the Yacht Dice website. After the tutoroll, fill in the room information. -- After logging in, you are good to go. The website has a built-in client, where you can chat and send commands. - -For more information on yaml files, generating Archipelago games, and connecting to servers, please see the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en). +For more information on generating Archipelago games and connecting to servers, please see the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en). \ No newline at end of file