diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..5ab53793 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +worlds/blasphemous/region_data.py linguist-generated=true +worlds/yachtdice/YachtWeights.py linguist-generated=true diff --git a/.github/pyright-config.json b/.github/pyright-config.json index 6ad7fa5f..de7758a7 100644 --- a/.github/pyright-config.json +++ b/.github/pyright-config.json @@ -1,8 +1,20 @@ { "include": [ - "type_check.py", + "../BizHawkClient.py", + "../Patch.py", + "../test/general/test_groups.py", + "../test/general/test_helpers.py", + "../test/general/test_memory.py", + "../test/general/test_names.py", + "../test/multiworld/__init__.py", + "../test/multiworld/test_multiworlds.py", + "../test/netutils/__init__.py", + "../test/programs/__init__.py", + "../test/programs/test_multi_server.py", + "../test/utils/__init__.py", + "../test/webhost/test_descriptions.py", "../worlds/AutoSNIClient.py", - "../Patch.py" + "type_check.py" ], "exclude": [ @@ -16,7 +28,7 @@ "reportMissingImports": true, "reportMissingTypeStubs": true, - "pythonVersion": "3.8", + "pythonVersion": "3.10", "pythonPlatform": "Windows", "executionEnvironments": [ diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml index c9995fa2..b59336fa 100644 --- a/.github/workflows/analyze-modified-files.yml +++ b/.github/workflows/analyze-modified-files.yml @@ -53,7 +53,7 @@ jobs: - uses: actions/setup-python@v5 if: env.diff != '' with: - python-version: 3.8 + python-version: '3.10' - name: "Install dependencies" if: env.diff != '' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80aaf70c..27ca76e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,22 +24,28 @@ env: jobs: # build-release-macos: # LF volunteer - build-win-py38: # RCs will still be built and signed by hand + build-win: # RCs will still be built and signed by hand runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Install python uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '~3.12.7' + check-latest: true - name: Download run-time dependencies run: | Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force + choco install innosetup --version=6.2.2 --allow-downgrade - name: Build run: | python -m pip install --upgrade pip python setup.py build_exe --yes + if ( $? -eq $false ) { + Write-Error "setup.py failed!" + exit 1 + } $NAME="$(ls build | Select-String -Pattern 'exe')".Split('.',2)[1] $ZIP_NAME="Archipelago_$NAME.7z" echo "$NAME -> $ZIP_NAME" @@ -49,12 +55,6 @@ jobs: Rename-Item "exe.$NAME" Archipelago 7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name - - name: Store 7z - uses: actions/upload-artifact@v4 - with: - name: ${{ env.ZIP_NAME }} - path: dist/${{ env.ZIP_NAME }} - retention-days: 7 # keep for 7 days, should be enough - name: Build Setup run: | & "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL @@ -65,11 +65,38 @@ jobs: $contents = Get-ChildItem -Path setups/*.exe -Force -Recurse $SETUP_NAME=$contents[0].Name echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV + - name: Check build loads expected worlds + shell: bash + run: | + cd build/exe* + mv Players/Templates/meta.yaml . + ls -1 Players/Templates | sort > setup-player-templates.txt + rm -R Players/Templates + timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true + ls -1 Players/Templates | sort > generated-player-templates.txt + cmp setup-player-templates.txt generated-player-templates.txt \ + || diff setup-player-templates.txt generated-player-templates.txt + mv meta.yaml Players/Templates/ + - name: Test Generate + shell: bash + run: | + cd build/exe* + cp Players/Templates/Clique.yaml Players/ + timeout 30 ./ArchipelagoGenerate + - name: Store 7z + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ZIP_NAME }} + path: dist/${{ env.ZIP_NAME }} + compression-level: 0 # .7z is incompressible by zip + if-no-files-found: error + retention-days: 7 # keep for 7 days, should be enough - name: Store Setup uses: actions/upload-artifact@v4 with: name: ${{ env.SETUP_NAME }} path: setups/${{ env.SETUP_NAME }} + if-no-files-found: error retention-days: 7 # keep for 7 days, should be enough build-ubuntu2004: @@ -85,10 +112,11 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '~3.12.7' + check-latest: true - name: Install build-time dependencies run: | - echo "PYTHON=python3.11" >> $GITHUB_ENV + echo "PYTHON=python3.12" >> $GITHUB_ENV wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract @@ -110,7 +138,7 @@ jobs: echo -e "setup.py dist output:\n `ls dist`" cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd .. export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz" - (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME") + (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME") echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV # - copy code above to release.yml - @@ -118,15 +146,36 @@ jobs: run: | source venv/bin/activate python setup.py build_exe --yes + - name: Check build loads expected worlds + shell: bash + run: | + cd build/exe* + mv Players/Templates/meta.yaml . + ls -1 Players/Templates | sort > setup-player-templates.txt + rm -R Players/Templates + timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true + ls -1 Players/Templates | sort > generated-player-templates.txt + cmp setup-player-templates.txt generated-player-templates.txt \ + || diff setup-player-templates.txt generated-player-templates.txt + mv meta.yaml Players/Templates/ + - name: Test Generate + shell: bash + run: | + cd build/exe* + cp Players/Templates/Clique.yaml Players/ + timeout 30 ./ArchipelagoGenerate - name: Store AppImage uses: actions/upload-artifact@v4 with: name: ${{ env.APPIMAGE_NAME }} path: dist/${{ env.APPIMAGE_NAME }} + if-no-files-found: error retention-days: 7 - name: Store .tar.gz uses: actions/upload-artifact@v4 with: name: ${{ env.TAR_NAME }} path: dist/${{ env.TAR_NAME }} + compression-level: 0 # .gz is incompressible by zip + if-no-files-found: error retention-days: 7 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b0cfe35d..3abbb5f6 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/ctest.yml b/.github/workflows/ctest.yml new file mode 100644 index 00000000..9492c83c --- /dev/null +++ b/.github/workflows/ctest.yml @@ -0,0 +1,54 @@ +# Run CMake / CTest C++ unit tests + +name: ctest + +on: + push: + paths: + - '**.cc?' + - '**.cpp' + - '**.cxx' + - '**.hh?' + - '**.hpp' + - '**.hxx' + - '**.CMakeLists' + - '.github/workflows/ctest.yml' + pull_request: + paths: + - '**.cc?' + - '**.cpp' + - '**.cxx' + - '**.hh?' + - '**.hpp' + - '**.hxx' + - '**.CMakeLists' + - '.github/workflows/ctest.yml' + +jobs: + ctest: + runs-on: ${{ matrix.os }} + name: Test C++ ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + - uses: ilammy/msvc-dev-cmd@v1 + if: startsWith(matrix.os,'windows') + - uses: Bacondish2023/setup-googletest@v1 + with: + build-type: 'Release' + - name: Build tests + run: | + cd test/cpp + mkdir build + cmake -S . -B build/ -DCMAKE_BUILD_TYPE=Release + cmake --build build/ --config Release + ls + - name: Run tests + run: | + cd test/cpp + ctest --test-dir build/ -C Release --output-on-failure diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d7f1253..aec4f909 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,10 +44,11 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '~3.12.7' + check-latest: true - name: Install build-time dependencies run: | - echo "PYTHON=python3.11" >> $GITHUB_ENV + echo "PYTHON=python3.12" >> $GITHUB_ENV wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract @@ -69,7 +70,7 @@ jobs: echo -e "setup.py dist output:\n `ls dist`" cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd .. export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz" - (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME") + (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME") echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV # - code above copied from build.yml - diff --git a/.github/workflows/scan-build.yml b/.github/workflows/scan-build.yml index 5234d862..ac842070 100644 --- a/.github/workflows/scan-build.yml +++ b/.github/workflows/scan-build.yml @@ -40,10 +40,10 @@ jobs: run: | wget https://apt.llvm.org/llvm.sh chmod +x ./llvm.sh - sudo ./llvm.sh 17 + sudo ./llvm.sh 19 - name: Install scan-build command run: | - sudo apt install clang-tools-17 + sudo apt install clang-tools-19 - name: Get a recent python uses: actions/setup-python@v5 with: @@ -56,7 +56,7 @@ jobs: - name: scan-build run: | source venv/bin/activate - scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y + scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y - name: Store report if: failure() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/strict-type-check.yml b/.github/workflows/strict-type-check.yml index bafd572a..2ccdad8d 100644 --- a/.github/workflows/strict-type-check.yml +++ b/.github/workflows/strict-type-check.yml @@ -26,7 +26,7 @@ jobs: - name: "Install dependencies" run: | - python -m pip install --upgrade pip pyright==1.1.358 + python -m pip install --upgrade pip pyright==1.1.392.post0 python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes - name: "pyright: strict check on specific files" diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 3ad29b00..88b5d129 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -33,16 +33,15 @@ jobs: matrix: os: [ubuntu-latest] python: - - {version: '3.8'} - - {version: '3.9'} - {version: '3.10'} - {version: '3.11'} + - {version: '3.12'} include: - - python: {version: '3.8'} # win7 compat + - python: {version: '3.10'} # old compat os: windows-latest - - python: {version: '3.11'} # current + - python: {version: '3.12'} # current os: windows-latest - - python: {version: '3.11'} # current + - python: {version: '3.12'} # current os: macos-latest steps: @@ -70,7 +69,7 @@ jobs: os: - ubuntu-latest python: - - {version: '3.11'} # current + - {version: '3.12'} # current steps: - uses: actions/checkout@v4 @@ -88,4 +87,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/.gitignore b/.gitignore index 0bba6f17..791f7b1b 100644 --- a/.gitignore +++ b/.gitignore @@ -150,7 +150,7 @@ venv/ ENV/ env.bak/ venv.bak/ -.code-workspace +*.code-workspace shell.nix # Spyder project settings @@ -178,6 +178,7 @@ dmypy.json cython_debug/ # Cython intermediates +_speedups.c _speedups.cpp _speedups.html diff --git a/AdventureClient.py b/AdventureClient.py index 206c55df..24c6a4c4 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -112,7 +112,7 @@ class AdventureContext(CommonContext): if ': !' not in msg: self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "ReceivedItems": - msg = f"Received {', '.join([self.item_names.lookup_in_slot(item.item) for item in args['items']])}" + msg = f"Received {', '.join([self.item_names.lookup_in_game(item.item) for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: diff --git a/BaseClasses.py b/BaseClasses.py index 88857f80..e19ba5f7 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,37 +1,38 @@ from __future__ import annotations -import copy -import itertools +import collections import functools import logging import random import secrets -import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace from collections import Counter, deque from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag -from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \ - TypedDict, Union, Type, ClassVar +from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, + Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING) + +from typing_extensions import NotRequired, TypedDict import NetUtils import Options import Utils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: + from entrance_rando import ERPlacementState from worlds import AutoWorld -class Group(TypedDict, total=False): +class Group(TypedDict): name: str game: str world: "AutoWorld.World" - players: Set[int] - item_pool: Set[str] - replacement_items: Dict[int, Optional[str]] - local_items: Set[str] - non_local_items: Set[str] - link_replacement: bool + players: AbstractSet[int] + item_pool: NotRequired[Set[str]] + replacement_items: NotRequired[Dict[int, Optional[str]]] + local_items: NotRequired[Set[str]] + non_local_items: NotRequired[Set[str]] + link_replacement: NotRequired[bool] class ThreadBarrierProxy: @@ -48,6 +49,11 @@ class ThreadBarrierProxy: "Please use multiworld.per_slot_randoms[player] or randomize ahead of output.") +class HasNameAndPlayer(Protocol): + name: str + player: int + + class MultiWorld(): debug_types = False player_name: Dict[int, str] @@ -63,7 +69,6 @@ class MultiWorld(): state: CollectionState plando_options: PlandoOptions - accessibility: Dict[int, Options.Accessibility] early_items: Dict[int, Dict[str, int]] local_early_items: Dict[int, Dict[str, int]] local_items: Dict[int, Options.LocalItems] @@ -157,7 +162,7 @@ class MultiWorld(): self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {} for player in range(1, players + 1): - def set_player_attr(attr, val): + def set_player_attr(attr: str, val) -> None: self.__dict__.setdefault(attr, {})[player] = val set_player_attr('plando_items', []) set_player_attr('plando_texts', {}) @@ -166,13 +171,13 @@ class MultiWorld(): set_player_attr('completion_condition', lambda state: True) self.worlds = {} self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the " - "world's random object instead (usually self.random)") + "world's random object instead (usually self.random)") self.plando_options = PlandoOptions.none def get_all_ids(self) -> Tuple[int, ...]: return self.player_ids + tuple(self.groups) - def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]: + def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]: """Create a group with name and return the assigned player ID and group. If a group of this name already exists, the set of players is extended instead of creating a new one.""" from worlds import AutoWorld @@ -188,7 +193,9 @@ class MultiWorld(): self.player_types[new_id] = NetUtils.SlotType.group world_type = AutoWorld.AutoWorldRegister.world_types[game] self.worlds[new_id] = world_type.create_group(self, new_id, players) - self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) + self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id]) + self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id]) + self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id]) self.player_name[new_id] = name new_group = self.groups[new_id] = Group(name=name, game=game, players=players, @@ -196,7 +203,7 @@ class MultiWorld(): return new_id, new_group - def get_player_groups(self, player) -> Set[int]: + def get_player_groups(self, player: int) -> Set[int]: return {group_id for group_id, group in self.groups.items() if player in group["players"]} def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None): @@ -223,7 +230,7 @@ class MultiWorld(): for player in self.player_ids: world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) - options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass + options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] for option_key in options_dataclass.type_hints}) @@ -259,7 +266,7 @@ class MultiWorld(): "link_replacement": replacement_prio.index(item_link["link_replacement"]), } - for name, item_link in item_links.items(): + for _name, item_link in item_links.items(): current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups pool = set() local_items = set() @@ -288,6 +295,88 @@ class MultiWorld(): group["non_local_items"] = item_link["non_local_items"] group["link_replacement"] = replacement_prio[item_link["link_replacement"]] + def link_items(self) -> None: + """Called to link together items in the itempool related to the registered item link groups.""" + from worlds import AutoWorld + + for group_id, group in self.groups.items(): + def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ + Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] + ]: + classifications: Dict[str, int] = collections.defaultdict(int) + counters = {player: {name: 0 for name in shared_pool} for player in players} + for item in self.itempool: + if item.player in counters and item.name in shared_pool: + counters[item.player][item.name] += 1 + classifications[item.name] |= item.classification + + for player in players.copy(): + if all([counters[player][item] == 0 for item in shared_pool]): + players.remove(player) + del (counters[player]) + + if not players: + return None, None + + for item in shared_pool: + count = min(counters[player][item] for player in players) + if count: + for player in players: + counters[player][item] = count + else: + for player in players: + del (counters[player][item]) + return counters, classifications + + common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) + if not common_item_count: + continue + + new_itempool: List[Item] = [] + for item_name, item_count in next(iter(common_item_count.values())).items(): + for _ in range(item_count): + new_item = group["world"].create_item(item_name) + # mangle together all original classification bits + new_item.classification |= classifications[item_name] + new_itempool.append(new_item) + + 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 + self.itempool.sort(key=lambda item: item.advancement) + for item in self.itempool: + count = common_item_count.get(item.player, {}).get(item.name, 0) + if count: + loc = Location(group_id, f"Item Link: {item.name} -> {self.player_name[item.player]} {count}", + None, region) + loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ + state.has(item_name, group_id_, count_) + + locations.append(loc) + loc.place_locked_item(item) + common_item_count[item.player][item.name] -= 1 + else: + new_itempool.append(item) + + itemcount = len(self.itempool) + self.itempool = new_itempool + + while itemcount > len(self.itempool): + items_to_add = [] + for player in group["players"]: + if group["link_replacement"]: + item_player = group_id + else: + item_player = player + if group["replacement_items"][player]: + items_to_add.append(AutoWorld.call_single(self, "create_item", item_player, + group["replacement_items"][player])) + else: + items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player)) + self.random.shuffle(items_to_add) + self.itempool.extend(items_to_add[:itemcount - len(self.itempool)]) + def secure(self): self.random = ThreadBarrierProxy(secrets.SystemRandom()) self.is_race = True @@ -309,7 +398,7 @@ class MultiWorld(): return tuple(world for player, world in self.worlds.items() if player not in self.groups and self.game[player] == game_name) - def get_name_string_for_object(self, obj) -> str: + def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str: return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})' def get_player_name(self, player: int) -> str: @@ -338,12 +427,12 @@ class MultiWorld(): def get_location(self, location_name: str, player: int) -> Location: return self.regions.location_cache[player][location_name] - def get_all_state(self, use_cache: bool) -> CollectionState: + def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False) -> CollectionState: cached = getattr(self, "_all_state", None) if use_cache and cached: return cached.copy() - ret = CollectionState(self) + ret = CollectionState(self, allow_partial_entrances) for item in self.itempool: self.worlds[item.player].collect(ret, item) @@ -351,7 +440,7 @@ class MultiWorld(): subworld = self.worlds[player] for item in subworld.get_pre_fill_items(): subworld.collect(ret, item) - ret.sweep_for_events() + ret.sweep_for_advancements() if use_cache: self._all_state = ret @@ -360,7 +449,7 @@ class MultiWorld(): def get_items(self) -> List[Item]: return [loc.item for loc in self.get_filled_locations()] + self.itempool - def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]: + def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]: if resolve_group_locations: player_groups = self.get_player_groups(player) return [location for location in self.get_locations() if @@ -369,7 +458,7 @@ class MultiWorld(): return [location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player] - def find_item(self, item, player: int) -> Location: + def find_item(self, item: str, player: int) -> Location: return next(location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player) @@ -462,9 +551,9 @@ class MultiWorld(): return True state = starting_state.copy() else: - if self.has_beaten_game(self.state): - return True state = CollectionState(self) + if self.has_beaten_game(state): + return True prog_locations = {location for location in self.get_locations() if location.item and location.item.advancement and location not in state.locations_checked} @@ -516,6 +605,49 @@ class MultiWorld(): state.collect(location.item, True, location) locations -= sphere + def get_sendable_spheres(self) -> Iterator[Set[Location]]: + """ + yields a set of multiserver sendable locations (location.item.code: int) for each logical sphere + + If there are unreachable locations, the last sphere of reachable locations is followed by an empty set, + and then a set of all of the unreachable locations. + """ + state = CollectionState(self) + locations: Set[Location] = set() + events: Set[Location] = set() + for location in self.get_filled_locations(): + if type(location.item.code) is int: + locations.add(location) + else: + events.add(location) + + while locations: + sphere: Set[Location] = set() + + # cull events out + done_events: Set[Union[Location, None]] = {None} + while done_events: + done_events = set() + for event in events: + if event.can_reach(state): + state.collect(event.item, True, event) + done_events.add(event) + events -= done_events + + for location in locations: + if location.can_reach(state): + sphere.add(location) + + yield sphere + if not sphere: + if locations: + yield locations # unreachable locations + break + + for location in sphere: + state.collect(location.item, True, location) + locations -= sphere + def fulfills_accessibility(self, state: Optional[CollectionState] = None): """Check if accessibility rules are fulfilled with current or supplied state.""" if not state: @@ -523,26 +655,21 @@ class MultiWorld(): players: Dict[str, Set[int]] = { "minimal": set(), "items": set(), - "locations": set() + "full": set() } - for player, access in self.accessibility.items(): - players[access.current_key].add(player) + for player, world in self.worlds.items(): + players[world.options.accessibility.current_key].add(player) beatable_fulfilled = False - def location_condition(location: Location): + def location_condition(location: Location) -> bool: """Determine if this location has to be accessible, location is already filtered by location_relevant""" - if location.player in players["locations"] or (location.item and location.item.player not in - players["minimal"]): - return True - return False + return location.player in players["full"] or \ + (location.item and location.item.player not in players["minimal"]) - def location_relevant(location: Location): + def location_relevant(location: Location) -> bool: """Determine if this location is relevant to sweep.""" - if location.progress_type != LocationProgressType.EXCLUDED \ - and (location.player in players["locations"] or location.advancement): - return True - return False + return location.player in players["full"] or location.advancement def all_done() -> bool: """Check if all access rules are fulfilled""" @@ -587,22 +714,24 @@ class CollectionState(): multiworld: MultiWorld reachable_regions: Dict[int, Set[Region]] blocked_connections: Dict[int, Set[Entrance]] - events: Set[Location] + advancements: Set[Location] path: Dict[Union[Region, Entrance], PathValue] locations_checked: Set[Location] stale: Dict[int, bool] + allow_partial_entrances: bool additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = [] additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = [] - def __init__(self, parent: MultiWorld): + def __init__(self, parent: MultiWorld, allow_partial_entrances: bool = False): self.prog_items = {player: Counter() for player in parent.get_all_ids()} self.multiworld = parent self.reachable_regions = {player: set() for player in parent.get_all_ids()} self.blocked_connections = {player: set() for player in parent.get_all_ids()} - self.events = set() + self.advancements = set() self.path = {} self.locations_checked = set() self.stale = {player: True for player in parent.get_all_ids()} + self.allow_partial_entrances = allow_partial_entrances for function in self.additional_init_functions: function(self, parent) for items in parent.precollected_items.values(): @@ -611,17 +740,25 @@ class CollectionState(): def update_reachable_regions(self, player: int): self.stale[player] = False + world: AutoWorld.World = self.multiworld.worlds[player] reachable_regions = self.reachable_regions[player] - blocked_connections = self.blocked_connections[player] queue = deque(self.blocked_connections[player]) - start = self.multiworld.get_region("Menu", player) + start: Region = world.get_region(world.origin_region_name) # init on first call - this can't be done on construction since the regions don't exist yet if start not in reachable_regions: reachable_regions.add(start) - blocked_connections.update(start.exits) + self.blocked_connections[player].update(start.exits) queue.extend(start.exits) + if world.explicit_indirect_conditions: + self._update_reachable_regions_explicit_indirect_conditions(player, queue) + else: + self._update_reachable_regions_auto_indirect_conditions(player, queue) + + def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque): + reachable_regions = self.reachable_regions[player] + blocked_connections = self.blocked_connections[player] # run BFS on all connections, and keep track of those blocked by missing items while queue: connection = queue.popleft() @@ -629,7 +766,9 @@ class CollectionState(): if new_region in reachable_regions: blocked_connections.remove(connection) elif connection.can_reach(self): - assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region" + if self.allow_partial_entrances and not new_region: + continue + assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region" reachable_regions.add(new_region) blocked_connections.remove(connection) blocked_connections.update(new_region.exits) @@ -641,16 +780,42 @@ class CollectionState(): if new_entrance in blocked_connections and new_entrance not in queue: queue.append(new_entrance) + def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque): + reachable_regions = self.reachable_regions[player] + blocked_connections = self.blocked_connections[player] + new_connection: bool = True + # run BFS on all connections, and keep track of those blocked by missing items + while new_connection: + new_connection = False + while queue: + connection = queue.popleft() + new_region = connection.connected_region + if new_region in reachable_regions: + blocked_connections.remove(connection) + elif connection.can_reach(self): + if self.allow_partial_entrances and not new_region: + continue + assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region" + reachable_regions.add(new_region) + blocked_connections.remove(connection) + blocked_connections.update(new_region.exits) + queue.extend(new_region.exits) + self.path[new_region] = (new_region.name, self.path.get(connection, None)) + new_connection = True + # sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region) + queue.extend(blocked_connections) + def copy(self) -> CollectionState: ret = CollectionState(self.multiworld) - ret.prog_items = copy.deepcopy(self.prog_items) - ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in - self.reachable_regions} - ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in - self.blocked_connections} - ret.events = copy.copy(self.events) - ret.path = copy.copy(self.path) - ret.locations_checked = copy.copy(self.locations_checked) + ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()} + ret.reachable_regions = {player: region_set.copy() for player, region_set in + self.reachable_regions.items()} + ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in + self.blocked_connections.items()} + ret.advancements = self.advancements.copy() + ret.path = self.path.copy() + ret.locations_checked = self.locations_checked.copy() + ret.allow_partial_entrances = self.allow_partial_entrances for function in self.additional_copy_functions: ret = function(self, ret) return ret @@ -680,20 +845,25 @@ class CollectionState(): def can_reach_region(self, spot: str, player: int) -> bool: return self.multiworld.get_region(spot, player).can_reach(self) - def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None: + def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None: + Utils.deprecate("sweep_for_events has been renamed to sweep_for_advancements. The functionality is the same. " + "Please switch over to sweep_for_advancements.") + return self.sweep_for_advancements(locations) + + def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None: if locations is None: locations = self.multiworld.get_filled_locations() - reachable_events = True - # since the loop has a good chance to run more than once, only filter the events once - locations = {location for location in locations if location.advancement and location not in self.events and - not key_only or getattr(location.item, "locked_dungeon_item", False)} - while reachable_events: - reachable_events = {location for location in locations if location.can_reach(self)} - locations -= reachable_events - for event in reachable_events: - self.events.add(event) - assert isinstance(event.item, Item), "tried to collect Event with no Item" - self.collect(event.item, True, event) + reachable_advancements = True + # since the loop has a good chance to run more than once, only filter the advancements once + locations = {location for location in locations if location.advancement and location not in self.advancements} + + while reachable_advancements: + reachable_advancements = {location for location in locations if location.can_reach(self)} + locations -= reachable_advancements + for advancement in reachable_advancements: + self.advancements.add(advancement) + assert isinstance(advancement.item, Item), "tried to collect Event with no Item" + self.collect(advancement.item, True, advancement) # item name related def has(self, item: str, player: int, count: int = 1) -> bool: @@ -727,7 +897,7 @@ class CollectionState(): if found >= count: return True return False - + def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool: """Returns True if the state contains at least `count` items matching any of the item names from a list. Ignores duplicates of the same item.""" @@ -742,7 +912,7 @@ class CollectionState(): def count_from_list(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state.""" return sum(self.prog_items[player][item_name] for item_name in items) - + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item.""" return sum(self.prog_items[player][item_name] > 0 for item_name in items) @@ -788,20 +958,16 @@ class CollectionState(): ) # Item related - def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: + def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool: if location: self.locations_checked.add(location) changed = self.multiworld.worlds[item.player].collect(self, item) - if not changed and event: - self.prog_items[item.player][item.name] += 1 - changed = True - self.stale[item.player] = True - if changed and not event: - self.sweep_for_events() + if changed and not prevent_sweep: + self.sweep_for_advancements() return changed @@ -814,6 +980,11 @@ class CollectionState(): self.stale[item.player] = True +class EntranceType(IntEnum): + ONE_WAY = 1 + TWO_WAY = 2 + + class Entrance: access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) hide_path: bool = False @@ -821,18 +992,24 @@ class Entrance: name: str parent_region: Optional[Region] connected_region: Optional[Region] = None + randomization_group: int + randomization_type: EntranceType # LttP specific, TODO: should make a LttPEntrance addresses = None target = None - def __init__(self, player: int, name: str = '', parent: Region = None): + def __init__(self, player: int, name: str = "", parent: Optional[Region] = None, + randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None: self.name = name self.parent_region = parent self.player = player + self.randomization_group = randomization_group + self.randomization_type = randomization_type def can_reach(self, state: CollectionState) -> bool: + assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region" if self.parent_region.can_reach(state) and self.access_rule(state): - if not self.hide_path and not self in state.path: + if not self.hide_path and self not in state.path: state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None))) return True @@ -844,10 +1021,33 @@ class Entrance: self.addresses = addresses region.entrances.append(self) - def __repr__(self): - return self.__str__() + def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool: + """ + Determines whether this is a valid source transition, that is, whether the entrance + randomizer is allowed to pair it to place any other regions. By default, this is the + same as a reachability check, but can be modified by Entrance implementations to add + other restrictions based on the placement state. - def __str__(self): + :param er_state: The current (partial) state of the ongoing entrance randomization + """ + return self.can_reach(er_state.collection_state) + + def can_connect_to(self, other: Entrance, dead_end: bool, er_state: "ERPlacementState") -> bool: + """ + Determines whether a given Entrance is a valid target transition, that is, whether + the entrance randomizer is allowed to pair this Entrance to that Entrance. By default, + only allows connection between entrances of the same type (one ways only go to one ways, + two ways always go to two ways) and prevents connecting an exit to itself in coupled mode. + + :param other: The proposed Entrance to connect to + :param dead_end: Whether the other entrance considered a dead end by Entrance randomization + :param er_state: The current (partial) state of the ongoing entrance randomization + """ + # the implementation of coupled causes issues for self-loops since the reverse entrance will be the + # same as the forward entrance. In uncoupled they are ok. + return self.randomization_type == other.randomization_type and (not er_state.coupled or self.name != other.name) + + def __repr__(self): multiworld = self.parent_region.multiworld if self.parent_region else None return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' @@ -860,7 +1060,7 @@ class Region: entrances: List[Entrance] exits: List[Entrance] locations: List[Location] - entrance_type: ClassVar[Type[Entrance]] = Entrance + entrance_type: ClassVar[type[Entrance]] = Entrance class Register(MutableSequence): region_manager: MultiWorld.RegionManager @@ -960,7 +1160,7 @@ class Region: return entrance.parent_region.get_connecting_entrance(is_main_entrance) def add_locations(self, locations: Dict[str, Optional[int]], - location_type: Optional[Type[Location]] = None) -> None: + location_type: Optional[type[Location]] = None) -> None: """ Adds locations to the Region object, where location_type is your Location class and locations is a dict of location names to address. @@ -973,7 +1173,7 @@ class Region: self.locations.append(location_type(self.player, location, address, self)) def connect(self, connecting_region: Region, name: Optional[str] = None, - rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type: + rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance: """ Connects this Region to another Region, placing the provided rule on the connection. @@ -996,8 +1196,18 @@ class Region: self.exits.append(exit_) return exit_ + def create_er_target(self, name: str) -> Entrance: + """ + Creates and returns an Entrance object as an entrance to this region + + :param name: name of the Entrance being created + """ + entrance = self.entrance_type(self.player, name) + entrance.connect(self) + return entrance + def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]], - rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None: + rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]: """ Connects current region to regions in exit dictionary. Passed region names must exist first. @@ -1007,15 +1217,16 @@ class Region: """ if not isinstance(exits, Dict): exits = dict.fromkeys(exits) - for connecting_region, name in exits.items(): - self.connect(self.multiworld.get_region(connecting_region, self.player), - name, - rules[connecting_region] if rules and connecting_region in rules else None) + return [ + self.connect( + self.multiworld.get_region(connecting_region, self.player), + name, + rules[connecting_region] if rules and connecting_region in rules else None, + ) + for connecting_region, name in exits.items() + ] def __repr__(self): - return self.__str__() - - def __str__(self): return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})' @@ -1034,9 +1245,9 @@ class Location: locked: bool = False show_in_spoiler: bool = True progress_type: LocationProgressType = LocationProgressType.DEFAULT - always_allow = staticmethod(lambda state, item: False) + always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False) access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) - item_rule = staticmethod(lambda item: True) + item_rule: Callable[[Item], bool] = staticmethod(lambda item: True) item: Optional[Item] = None def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None): @@ -1045,16 +1256,20 @@ class Location: self.address = address self.parent_region = parent - def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: - return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items) - or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) - and self.item_rule(item) - and (not check_access or self.can_reach(state)))) + def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool: + return (( + self.always_allow(state, item) + and item.name not in state.multiworld.worlds[item.player].options.non_local_items + ) or ( + (self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) + and self.item_rule(item) + and (not check_access or self.can_reach(state)) + )) def can_reach(self, state: CollectionState) -> bool: - # self.access_rule computes faster on average, so placing it first for faster abort - assert self.parent_region, "Can't reach location without region" - return self.access_rule(state) and self.parent_region.can_reach(state) + # Region.can_reach is just a cache lookup, so placing it first for faster abort on average + assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region" + return self.parent_region.can_reach(state) and self.access_rule(state) def place_locked_item(self, item: Item): if self.item: @@ -1064,9 +1279,6 @@ class Location: self.locked = True def __repr__(self): - return self.__str__() - - def __str__(self): multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' @@ -1088,7 +1300,7 @@ class Location: @property def native_item(self) -> bool: """Returns True if the item in this location matches game.""" - return self.item and self.item.game == self.game + return self.item is not None and self.item.game == self.game @property def hint_text(self) -> str: @@ -1096,13 +1308,26 @@ class Location: class ItemClassification(IntFlag): - filler = 0b0000 # aka trash, as in filler items like ammo, currency etc, - progression = 0b0001 # Item that is logically relevant - useful = 0b0010 # Item that is generally quite useful, but not required for anything logical - trap = 0b0100 # detrimental or entirely useless (nothing) item - skip_balancing = 0b1000 # should technically never occur on its own - # Item that is logically relevant, but progression balancing should not touch. - # Typically currency or other counted items. + filler = 0b0000 + """ aka trash, as in filler items like ammo, currency etc """ + + progression = 0b0001 + """ Item that is logically relevant. + Protects this item from being placed on excluded or unreachable locations. """ + + useful = 0b0010 + """ Item that is especially useful. + Protects this item from being placed on excluded or unreachable locations. + When combined with another flag like "progression", it means "an especially useful progression item". """ + + trap = 0b0100 + """ Item that is detrimental in some way. """ + + skip_balancing = 0b1000 + """ should technically never occur on its own + Item that is logically relevant, but progression balancing should not touch. + Typically currency or other counted items. """ + progression_skip_balancing = 0b1001 # only progression gets balanced def as_flag(self) -> int: @@ -1151,6 +1376,14 @@ class Item: def trap(self) -> bool: return ItemClassification.trap in self.classification + @property + def filler(self) -> bool: + return not (self.advancement or self.useful or self.trap) + + @property + def excludable(self) -> bool: + return not (self.advancement or self.useful) + @property def flags(self) -> int: return self.classification.as_flag() @@ -1171,9 +1404,6 @@ class Item: return hash((self.name, self.player)) def __repr__(self) -> str: - return self.__str__() - - def __str__(self) -> str: if self.location and self.location.parent_region and self.location.parent_region.multiworld: return self.location.parent_region.multiworld.get_name_string_for_object(self) return f"{self.name} (Player {self.player})" @@ -1251,9 +1481,9 @@ class Spoiler: # in the second phase, we cull each sphere such that the game is still beatable, # reducing each range of influence to the bare minimum required inside it - restore_later = {} + restore_later: Dict[Location, Item] = {} for num, sphere in reversed(tuple(enumerate(collection_spheres))): - to_delete = set() + to_delete: Set[Location] = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, @@ -1271,15 +1501,22 @@ class Spoiler: sphere -= to_delete # second phase, sphere 0 - removed_precollected = [] - for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement): - logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) - multiworld.precollected_items[item.player].remove(item) - multiworld.state.remove(item) - if not multiworld.can_beat_game(): - multiworld.push_precollected(item) - else: - removed_precollected.append(item) + removed_precollected: List[Item] = [] + + for precollected_items in multiworld.precollected_items.values(): + # The list of items is mutated by removing one item at a time to determine if each item is required to beat + # the game, and re-adding that item if it was required, so a copy needs to be made before iterating. + for item in precollected_items.copy(): + if not item.advancement: + continue + logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) + precollected_items.remove(item) + multiworld.state.remove(item) + if not multiworld.can_beat_game(): + # Add the item back into `precollected_items` and collect it into `multiworld.state`. + multiworld.push_precollected(item) + else: + removed_precollected.append(item) # we are now down to just the required progress items in collection_spheres. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others @@ -1291,8 +1528,6 @@ class Spoiler: state = CollectionState(multiworld) collection_spheres = [] while required_locations: - state.sweep_for_events(key_only=True) - sphere = set(filter(state.can_reach, required_locations)) for location in sphere: @@ -1354,7 +1589,7 @@ class Spoiler: # Maybe move the big bomb over to the Event system instead? if any(exit_path == 'Pyramid Fairy' for path in self.paths.values() for (_, exit_path) in path): - if multiworld.mode[player] != 'inverted': + if multiworld.worlds[player].options.mode != 'inverted': self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \ get_path(state, multiworld.get_region('Big Bomb Shop', player)) else: @@ -1420,15 +1655,15 @@ class Spoiler: [f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else [f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()])) if self.unreachables: - outfile.write('\n\nUnreachable Items:\n\n') + outfile.write('\n\nUnreachable Progression Items:\n\n') outfile.write( '\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables])) if self.paths: outfile.write('\n\nPaths:\n\n') - path_listings = [] + path_listings: List[str] = [] for location, path in sorted(self.paths.items()): - path_lines = [] + path_lines: List[str] = [] for region, exit in path: if exit is not None: path_lines.append("{} -> {}".format(region, exit)) diff --git a/BizHawkClient.py b/BizHawkClient.py index 86c8e519..743785b2 100644 --- a/BizHawkClient.py +++ b/BizHawkClient.py @@ -1,9 +1,10 @@ from __future__ import annotations +import sys import ModuleUpdate ModuleUpdate.update() from worlds._bizhawk.context import launch if __name__ == "__main__": - launch() + launch(*sys.argv[1:]) diff --git a/CommonClient.py b/CommonClient.py index 8f1e64c0..996ba330 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -23,7 +23,7 @@ if __name__ == "__main__": from MultiServer import CommandProcessor from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot, - RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes) + RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType) from Utils import Version, stream_input, async_start from worlds import network_data_package, AutoWorldRegister import os @@ -31,6 +31,7 @@ import ssl if typing.TYPE_CHECKING: import kvui + import argparse logger = logging.getLogger("Client") @@ -45,10 +46,21 @@ def get_ssl_context(): class ClientCommandProcessor(CommandProcessor): + """ + The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called + when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit". + + The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first + space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw + and method("one", "two", "three") without. + + In addition all docstrings for command methods will be displayed to the user on launch and when using "/help" + """ def __init__(self, ctx: CommonContext): self.ctx = ctx def output(self, text: str): + """Helper function to abstract logging to the CommonClient UI""" logger.info(text) def _cmd_exit(self) -> bool: @@ -61,6 +73,7 @@ class ClientCommandProcessor(CommandProcessor): if address: self.ctx.server_address = None self.ctx.username = None + self.ctx.password = None elif not self.ctx.server_address: self.output("Please specify an address.") return False @@ -163,13 +176,14 @@ class ClientCommandProcessor(CommandProcessor): async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate") def default(self, raw: str): + """The default message parser to be used when parsing any messages that do not match a command""" raw = self.ctx.on_user_say(raw) if raw: async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say") class CommonContext: - # Should be adjusted as needed in subclasses + # The following attributes are used to Connect and should be adjusted as needed in subclasses tags: typing.Set[str] = {"AP"} game: typing.Optional[str] = None items_handling: typing.Optional[int] = None @@ -225,6 +239,9 @@ class CommonContext: def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str: """Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is omitted. + + Use of `lookup_in_slot` should not be used when not connected to a server. If looking in own game, set + `ctx.game` and use `lookup_in_game` method instead. """ if slot is None: slot = self.ctx.slot @@ -248,7 +265,7 @@ class CommonContext: starting_reconnect_delay: int = 5 current_reconnect_delay: int = starting_reconnect_delay command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor - ui = None + ui: typing.Optional["kvui.GameManager"] = None ui_task: typing.Optional["asyncio.Task[None]"] = None input_task: typing.Optional["asyncio.Task[None]"] = None keep_alive_task: typing.Optional["asyncio.Task[None]"] = None @@ -339,6 +356,8 @@ class CommonContext: self.item_names = self.NameLookupDict(self, "item") self.location_names = self.NameLookupDict(self, "location") + self.versions = {} + self.checksums = {} self.jsontotextparser = JSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self) @@ -394,6 +413,7 @@ class CommonContext: await self.server.socket.close() if self.server_task is not None: await self.server_task + self.ui.update_hints() async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: """ `msgs` JSON serializable """ @@ -425,7 +445,10 @@ class CommonContext: self.auth = await self.console_input() async def send_connect(self, **kwargs: typing.Any) -> None: - """ send `Connect` packet to log in to server """ + """ + Send a `Connect` packet to log in to the server, + additional keyword args can override any value in the connection packet + """ payload = { 'cmd': 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, @@ -435,6 +458,14 @@ class CommonContext: if kwargs: payload.update(kwargs) await self.send_msgs([payload]) + await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}]) + + async def check_locations(self, locations: typing.Collection[int]) -> set[int]: + """Send new location checks to the server. Returns the set of actually new locations that were sent.""" + locations = set(locations) & self.missing_locations + if locations: + await self.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(locations)}]) + return locations async def console_input(self) -> str: if self.ui: @@ -455,6 +486,7 @@ class CommonContext: return False def slot_concerns_self(self, slot) -> bool: + """Helper function to abstract player groups, should be used instead of checking slot == self.slot directly.""" if slot == self.slot: return True if slot in self.slot_info: @@ -462,6 +494,7 @@ class CommonContext: return False def is_echoed_chat(self, print_json_packet: dict) -> bool: + """Helper function for filtering out messages sent by self.""" return print_json_packet.get("type", "") == "Chat" \ and print_json_packet.get("team", None) == self.team \ and print_json_packet.get("slot", None) == self.slot @@ -493,13 +526,14 @@ class CommonContext: """Gets called before sending a Say to the server from the user. Returned text is sent, or sending is aborted if None is returned.""" return text - + def on_ui_command(self, text: str) -> None: """Gets called by kivy when the user executes a command starting with `/` or `!`. The command processor is still called; this is just intended for command echoing.""" self.ui.print_json([{"text": text, "type": "color", "color": "orange"}]) def update_permissions(self, permissions: typing.Dict[str, int]): + """Internal method to parse and save server permissions from RoomInfo""" for permission_name, permission_flag in permissions.items(): try: flag = Permission(permission_flag) @@ -511,6 +545,7 @@ class CommonContext: async def shutdown(self): self.server_address = "" self.username = None + self.password = None self.cancel_autoreconnect() if self.server and not self.server.socket.closed: await self.server.socket.close() @@ -525,7 +560,14 @@ class CommonContext: await self.ui_task if self.input_task: self.input_task.cancel() - + + # Hints + def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None: + msg = {"cmd": "UpdateHint", "location": location, "player": finding_player} + if status is not None: + msg["status"] = status + async_start(self.send_msgs([msg]), name="update_hint") + # DataPackage async def prepare_data_package(self, relevant_games: typing.Set[str], remote_date_package_versions: typing.Dict[str, int], @@ -547,26 +589,34 @@ class CommonContext: needed_updates.add(game) continue - local_version: int = network_data_package["games"].get(game, {}).get("version", 0) - local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") - # no action required if local version is new enough - if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \ - or remote_checksum != local_checksum: - cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) - cache_version: int = cached_game.get("version", 0) - cache_checksum: typing.Optional[str] = cached_game.get("checksum") - # download remote version if cache is not new enough - if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ - or remote_checksum != cache_checksum: - needed_updates.add(game) + cached_version: int = self.versions.get(game, 0) + cached_checksum: typing.Optional[str] = self.checksums.get(game) + # no action required if cached version is new enough + if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \ + or remote_checksum != cached_checksum: + local_version: int = network_data_package["games"].get(game, {}).get("version", 0) + local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") + if ((remote_checksum or remote_version <= local_version and remote_version != 0) + and remote_checksum == local_checksum): + self.update_game(network_data_package["games"][game], game) else: - self.update_game(cached_game, game) + cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) + cache_version: int = cached_game.get("version", 0) + cache_checksum: typing.Optional[str] = cached_game.get("checksum") + # download remote version if cache is not new enough + if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ + or remote_checksum != cache_checksum: + needed_updates.add(game) + else: + self.update_game(cached_game, game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) def update_game(self, game_package: dict, game: str): self.item_names.update_game(game, game_package["item_name_to_id"]) self.location_names.update_game(game, game_package["location_name_to_id"]) + self.versions[game] = game_package.get("version", 0) + self.checksums[game] = game_package.get("checksum") def update_data_package(self, data_package: dict): for game, game_data in data_package["games"].items(): @@ -608,6 +658,7 @@ class CommonContext: logger.info(f"DeathLink: Received from {data['source']}") async def send_death(self, death_text: str = ""): + """Helper function to send a deathlink using death_text as the unique death cause string.""" if self.server and self.server.socket: logger.info("DeathLink: Sending death to your friends...") self.last_death_link = time.time() @@ -621,6 +672,7 @@ class CommonContext: }]) async def update_death_link(self, death_link: bool): + """Helper function to set Death Link connection tag on/off and update the connection if already connected.""" old_tags = self.tags.copy() if death_link: self.tags.add("DeathLink") @@ -630,7 +682,7 @@ class CommonContext: await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]) def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]: - """Displays an error messagebox""" + """Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework""" if not self.ui: return None title = title or "Error" @@ -657,21 +709,36 @@ class CommonContext: logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) - def run_gui(self): - """Import kivy UI system and start running it as self.ui_task.""" + def make_gui(self) -> "type[kvui.GameManager]": + """ + To return the Kivy `App` class needed for `run_gui` so it can be overridden before being built + + Common changes are changing `base_title` to update the window title of the client and + updating `logging_pairs` to automatically make new tabs that can be filled with their respective logger. + + ex. `logging_pairs.append(("Foo", "Bar"))` + will add a "Bar" tab which follows the logger returned from `logging.getLogger("Foo")` + """ from kvui import GameManager class TextManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] base_title = "Archipelago Text Client" - self.ui = TextManager(self) + return TextManager + + def run_gui(self): + """Import kivy UI system from make_gui() and start running it as self.ui_task.""" + ui_class = self.make_gui() + self.ui = ui_class(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") def run_cli(self): if sys.stdin: + if sys.stdin.fileno() != 0: + from multiprocessing import parent_process + if parent_process(): + return # ignore MultiProcessing pipe + # steam overlay breaks when starting console_loop if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''): logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.") @@ -859,7 +926,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict): ctx.team = args["team"] ctx.slot = args["slot"] # int keys get lost in JSON transfer - ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()} + ctx.slot_info = {0: NetworkSlot("Archipelago", "Archipelago", SlotType.player)} + ctx.slot_info.update({int(pid): data for pid, data in args["slot_info"].items()}) ctx.hint_points = args.get("hint_points", 0) ctx.consume_players_package(args["players"]) ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}") @@ -979,6 +1047,7 @@ async def console_loop(ctx: CommonContext): def get_base_parser(description: typing.Optional[str] = None): + """Base argument parser to be reused for components subclassing off of CommonClient""" import argparse parser = argparse.ArgumentParser(description=description) parser.add_argument('--connect', default=None, help='Address of the multiworld host.') @@ -988,7 +1057,33 @@ def get_base_parser(description: typing.Optional[str] = None): return parser -def run_as_textclient(): +def handle_url_arg(args: "argparse.Namespace", + parser: "typing.Optional[argparse.ArgumentParser]" = None) -> "argparse.Namespace": + """ + Parse the url arg "archipelago://name:pass@host:port" from launcher into correct launch args for CommonClient + If alternate data is required the urlparse response is saved back to args.url if valid + """ + if not args.url: + return args + + url = urllib.parse.urlparse(args.url) + if url.scheme != "archipelago": + if not parser: + parser = get_base_parser() + parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + return args + + args.url = url + args.connect = url.netloc + if url.username: + args.name = urllib.parse.unquote(url.username) + if url.password: + args.password = urllib.parse.unquote(url.password) + + return args + + +def run_as_textclient(*args): class TextContext(CommonContext): # Text Mode to use !hint and such with games that have no text entry tags = CommonContext.tags | {"TextOnly"} @@ -1027,16 +1122,11 @@ def run_as_textclient(): parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.") parser.add_argument('--name', default=None, help="Slot Name to connect as.") parser.add_argument("url", nargs="?", help="Archipelago connection url") - args = parser.parse_args() + args = parser.parse_args(args) - if args.url: - url = urllib.parse.urlparse(args.url) - args.connect = url.netloc - if url.username: - args.name = urllib.parse.unquote(url.username) - if url.password: - args.password = urllib.parse.unquote(url.password) + args = handle_url_arg(args, parser=parser) + # use colorama to display colored text highlighting on windows colorama.init() asyncio.run(main(args)) @@ -1045,4 +1135,4 @@ def run_as_textclient(): if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING - run_as_textclient() + run_as_textclient(*sys.argv[1:]) # default value for parse_args diff --git a/Fill.py b/Fill.py index d8147b2e..d1773c82 100644 --- a/Fill.py +++ b/Fill.py @@ -12,7 +12,12 @@ from worlds.generic.Rules import add_item_rule class FillError(RuntimeError): - pass + def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None: + if "multiworld" in kwargs and isinstance(args[0], str): + placements = (args[0] + f"\nAll Placements:\n" + + f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}") + args = (placements, *args[1:]) + super().__init__(*args) def _log_fill_progress(name: str, placed: int, total_items: int) -> None: @@ -24,14 +29,15 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] new_state = base_state.copy() for item in itempool: new_state.collect(item, True) - new_state.sweep_for_events(locations=locations) + new_state.sweep_for_advancements(locations=locations) return new_state def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location], item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None, - allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None: + allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True, + name: str = "Unknown") -> None: """ :param multiworld: Multiworld to be filled. :param base_state: State assumed before fill. @@ -58,14 +64,22 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati placed = 0 while any(reachable_items.values()) and locations: - # grab one item per player - items_to_place = [items.pop() - for items in reachable_items.values() if items] + if one_item_per_player: + # grab one item per player + items_to_place = [items.pop() + for items in reachable_items.values() if items] + else: + next_player = multiworld.random.choice([player for player, items in reachable_items.items() if items]) + items_to_place = [] + if item_pool: + items_to_place.append(reachable_items[next_player].pop()) + for item in items_to_place: for p, pool_item in enumerate(item_pool): if pool_item is item: item_pool.pop(p) break + maximum_exploration_state = sweep_from_pool( base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player) if single_player_placement else None) @@ -212,7 +226,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati f"Unfilled locations:\n" f"{', '.join(str(location) for location in locations)}\n" f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) item_pool.extend(unplaced_items) @@ -221,18 +235,30 @@ def remaining_fill(multiworld: MultiWorld, locations: typing.List[Location], itempool: typing.List[Item], name: str = "Remaining", - move_unplaceable_to_start_inventory: bool = False) -> None: + move_unplaceable_to_start_inventory: bool = False, + check_location_can_fill: bool = False) -> None: unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() total = min(len(itempool), len(locations)) placed = 0 + + # Optimisation: Decide whether to do full location.can_fill check (respect excluded), or only check the item rule + if check_location_can_fill: + state = CollectionState(multiworld) + + def location_can_fill_item(location_to_fill: Location, item_to_fill: Item): + return location_to_fill.can_fill(state, item_to_fill, check_access=False) + else: + def location_can_fill_item(location_to_fill: Location, item_to_fill: Item): + return location_to_fill.item_rule(item_to_fill) + while locations and itempool: item_to_place = itempool.pop() spot_to_fill: typing.Optional[Location] = None for i, location in enumerate(locations): - if location.item_rule(item_to_place): + if location_can_fill_item(location, item_to_place): # popping by index is faster than removing by content, spot_to_fill = locations.pop(i) # skipping a scan for the element @@ -253,7 +279,7 @@ def remaining_fill(multiworld: MultiWorld, location.item = None placed_item.location = None - if location.item_rule(item_to_place): + if location_can_fill_item(location, item_to_place): # Add this item to the existing placement, and # add the old item to the back of the queue spot_to_fill = placements.pop(i) @@ -299,7 +325,7 @@ def remaining_fill(multiworld: MultiWorld, f"Unfilled locations:\n" f"{', '.join(str(location) for location in locations)}\n" f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) itempool.extend(unplaced_items) @@ -324,8 +350,8 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo pool.append(location.item) state.remove(location.item) location.item = None - if location in state.events: - state.events.remove(location) + if location in state.advancements: + state.advancements.remove(location) locations.append(location) if pool and locations: locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) @@ -358,7 +384,7 @@ def distribute_early_items(multiworld: MultiWorld, early_priority_locations: typing.List[Location] = [] loc_indexes_to_remove: typing.Set[int] = set() base_state = multiworld.state.copy() - base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None)) + base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None)) for i, loc in enumerate(fill_locations): if loc.can_reach(base_state): if loc.progress_type == LocationProgressType.PRIORITY: @@ -470,28 +496,33 @@ def distribute_items_restrictive(multiworld: MultiWorld, nonlocal lock_later lock_later.append(location) + single_player = multiworld.players == 1 and not multiworld.groups + if prioritylocations: # "priority fill" fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, - single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking, - name="Priority") + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority", one_item_per_player=True, allow_partial=True) + + if prioritylocations: + # retry with one_item_per_player off because some priority fills can fail to fill with that optimization + fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority Retry", one_item_per_player=False) accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: # "advancement/progression fill" if panic_method == "swap": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=True, - on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True, + name="Progression", single_player_placement=single_player) elif panic_method == "raise": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=False, - on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, + name="Progression", single_player_placement=single_player) elif panic_method == "start_inventory": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=False, allow_partial=True, - on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, + allow_partial=True, name="Progression", single_player_placement=single_player) if progitempool: for item in progitempool: logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") @@ -506,7 +537,9 @@ def distribute_items_restrictive(multiworld: MultiWorld, if progitempool: raise FillError( f"Not enough locations for progression items. " - f"There are {len(progitempool)} more progression items than there are available locations." + f"There are {len(progitempool)} more progression items than there are available locations.\n" + f"Unfilled locations:\n{multiworld.get_unfilled_locations()}.", + multiworld=multiworld, ) accessibility_corrections(multiworld, multiworld.state, defaultlocations) @@ -523,7 +556,8 @@ def distribute_items_restrictive(multiworld: MultiWorld, if excludedlocations: raise FillError( f"Not enough filler items for excluded locations. " - f"There are {len(excludedlocations)} more excluded locations than filler or trap items." + f"There are {len(excludedlocations)} more excluded locations than excludable items.", + multiworld=multiworld, ) restitempool = filleritempool + usefulitempool @@ -543,6 +577,26 @@ def distribute_items_restrictive(multiworld: MultiWorld, print_data = {"items": items_counter, "locations": locations_counter} logging.info(f"Per-Player counts: {print_data})") + more_locations = locations_counter - items_counter + more_items = items_counter - locations_counter + for player in multiworld.player_ids: + if more_locations[player]: + logging.error( + f"Player {multiworld.get_player_name(player)} had {more_locations[player]} more locations than items.") + elif more_items[player]: + logging.warning( + f"Player {multiworld.get_player_name(player)} had {more_items[player]} more items than locations.") + if unfilled: + raise FillError( + f"Unable to fill all locations.\n" + + f"Unfilled locations({len(unfilled)}): {unfilled}" + ) + else: + logging.warning( + f"Unable to place all items.\n" + + f"Unplaced items({len(unplaced)}): {unplaced}" + ) + def flood_items(multiworld: MultiWorld) -> None: # get items to distribute @@ -551,7 +605,7 @@ def flood_items(multiworld: MultiWorld) -> None: progress_done = False # sweep once to pick up preplaced items - multiworld.state.sweep_for_events() + multiworld.state.sweep_for_advancements() # fill multiworld from top of itempool while we can while not progress_done: @@ -589,7 +643,7 @@ def flood_items(multiworld: MultiWorld) -> None: if candidate_item_to_place is not None: item_to_place = candidate_item_to_place else: - raise FillError('No more progress items left to place.') + raise FillError('No more progress items left to place.', multiworld=multiworld) # find item to replace with progress item location_list = multiworld.get_reachable_locations() @@ -646,7 +700,6 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: def get_sphere_locations(sphere_state: CollectionState, locations: typing.Set[Location]) -> typing.Set[Location]: - sphere_state.sweep_for_events(key_only=True, locations=locations) return {loc for loc in locations if sphere_state.can_reach(loc)} def item_percentage(player: int, num: int) -> float: @@ -740,7 +793,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: ), items_to_test): reducing_state.collect(location.item, True, location) - reducing_state.sweep_for_events(locations=locations_to_test) + reducing_state.sweep_for_advancements(locations=locations_to_test) if multiworld.has_beaten_game(balancing_state): if not multiworld.has_beaten_game(reducing_state): @@ -823,7 +876,7 @@ def distribute_planned(multiworld: MultiWorld) -> None: warn(warning, force) swept_state = multiworld.state.copy() - swept_state.sweep_for_events() + swept_state.sweep_for_advancements() reachable = frozenset(multiworld.get_reachable_locations(swept_state)) early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) @@ -974,15 +1027,32 @@ def distribute_planned(multiworld: MultiWorld) -> None: multiworld.random.shuffle(items) count = 0 err: typing.List[str] = [] - successful_pairs: typing.List[typing.Tuple[Item, Location]] = [] + successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = [] + claimed_indices: typing.Set[typing.Optional[int]] = set() for item_name in items: - item = multiworld.worlds[player].create_item(item_name) + index_to_delete: typing.Optional[int] = None + if from_pool: + try: + # If from_pool, try to find an existing item with this name & player in the itempool and use it + index_to_delete, item = next( + (i, item) for i, item in enumerate(multiworld.itempool) + if item.player == player and item.name == item_name and i not in claimed_indices + ) + except StopIteration: + warn( + f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.", + placement['force']) + item = multiworld.worlds[player].create_item(item_name) + else: + item = multiworld.worlds[player].create_item(item_name) + for location in reversed(candidates): if (location.address is None) == (item.code is None): # either both None or both not None if not location.item: if location.item_rule(item): if location.can_fill(multiworld.state, item, False): - successful_pairs.append((item, location)) + successful_pairs.append((index_to_delete, item, location)) + claimed_indices.add(index_to_delete) candidates.remove(location) count = count + 1 break @@ -994,6 +1064,7 @@ def distribute_planned(multiworld: MultiWorld) -> None: err.append(f"Cannot place {item_name} into already filled location {location}.") else: err.append(f"Mismatch between {item_name} and {location}, only one is an event.") + if count == maxcount: break if count < placement['count']['min']: @@ -1001,17 +1072,16 @@ def distribute_planned(multiworld: MultiWorld) -> None: failed( f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}", placement['force']) - for (item, location) in successful_pairs: + + # Sort indices in reverse so we can remove them one by one + successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True) + + for (index, item, location) in successful_pairs: multiworld.push_item(location, item, collect=False) location.locked = True logging.debug(f"Plando placed {item} at {location}") - if from_pool: - try: - multiworld.itempool.remove(item) - except ValueError: - warn( - f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.", - placement['force']) + if index is not None: # If this item is from_pool and was found in the pool, remove it. + multiworld.itempool.pop(index) except Exception as e: raise Exception( diff --git a/Generate.py b/Generate.py index 0cef0811..b057db25 100644 --- a/Generate.py +++ b/Generate.py @@ -42,11 +42,13 @@ def mystery_argparse(): help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--meta_file_path', default=defaults.meta_file_path) - parser.add_argument('--log_level', default='info', help='Sets log level') - parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0), - help='Output rolled mystery results to yaml up to specified number (made for async multiworld)') - parser.add_argument('--plando', default=defaults.plando_options, - help='List of options that can be set manually. Can be combined, for example "bosses, items"') + parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level') + parser.add_argument('--log_time', help="Add timestamps to STDOUT", + default=defaults.logtime, action='store_true') + parser.add_argument("--csv_output", action="store_true", + help="Output rolled player options to csv (made for async multiworld).") + parser.add_argument("--plando", default=defaults.plando_options, + help="List of options that can be set manually. Can be combined, for example \"bosses, items\"") parser.add_argument("--skip_prog_balancing", action="store_true", help="Skip progression balancing step during generation.") parser.add_argument("--skip_output", action="store_true", @@ -65,15 +67,17 @@ def get_seed_name(random_source) -> str: return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits) -def main(args=None): +def main(args=None) -> Tuple[argparse.Namespace, int]: + # __name__ == "__main__" check so unittests that already imported worlds don't trip this. + if __name__ == "__main__" and "worlds" in sys.modules: + raise Exception("Worlds system should not be loaded before logging init.") + if not args: args = mystery_argparse() seed = get_seed(args.seed) - # __name__ == "__main__" check so unittests that already imported worlds don't trip this. - if __name__ == "__main__" and "worlds" in sys.modules: - raise Exception("Worlds system should not be loaded before logging init.") - Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) + + Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time) random.seed(seed) seed_name = get_seed_name(random) @@ -108,11 +112,18 @@ def main(args=None): 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: - weights_cache[fname] = read_weights_yamls(path) + weights_for_file = [] + for doc_idx, yaml in enumerate(read_weights_yamls(path)): + if yaml is None: + logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}") + else: + weights_for_file.append(yaml) + weights_cache[fname] = tuple(weights_for_file) + except Exception as e: raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e @@ -153,6 +164,8 @@ def main(args=None): erargs.outputpath = args.outputpath erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_output = args.skip_output + erargs.name = {} + erargs.csv_output = args.csv_output settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) @@ -200,7 +213,7 @@ def main(args=None): if path == args.weights_file_path: # if name came from the weights file, just use base player name erargs.name[player] = f"Player{player}" - elif not erargs.name[player]: # if name was not specified, generate it from filename + elif player not in erargs.name: # if name was not specified, generate it from filename erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] erargs.name[player] = handle_name(erargs.name[player], player, name_counter) @@ -213,30 +226,7 @@ def main(args=None): if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name): raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}") - if args.yaml_output: - import yaml - important = {} - for option, player_settings in vars(erargs).items(): - if type(player_settings) == dict: - if all(type(value) != list for value in player_settings.values()): - if len(player_settings.values()) > 1: - important[option] = {player: value for player, value in player_settings.items() if - player <= args.yaml_output} - else: - logging.debug(f"No player settings defined for option '{option}'") - - else: - if player_settings != "": # is not empty name - important[option] = player_settings - else: - logging.debug(f"No player settings defined for option '{option}'") - if args.outputpath: - os.makedirs(args.outputpath, exist_ok=True) - with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f: - yaml.dump(important, f) - - from Main import main as ERmain - return ERmain(erargs, seed) + return erargs, seed def read_weights_yamls(path) -> Tuple[Any, ...]: @@ -450,7 +440,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b if "linked_options" in weights: weights = roll_linked_options(weights) - valid_keys = set() + valid_keys = {"triggers"} if "triggers" in weights: weights = roll_triggers(weights, weights["triggers"], valid_keys) @@ -472,6 +462,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") ret.game = get_choice("game", weights) + if not isinstance(ret.game, str): + if ret.game is None: + raise Exception('"game" not specified') + raise Exception(f"Invalid game: {ret.game}") if ret.game not in AutoWorldRegister.world_types: from worlds import failed_world_loads picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] @@ -505,15 +499,23 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b for option_key, option in world_type.options_dataclass.type_hints.items(): handle_option(ret, game_weights, option_key, option, plando_options) valid_keys.add(option_key) - for option_key in game_weights: - if option_key in {"triggers", *valid_keys}: - continue - logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.") + + # TODO remove plando_items after moving it to the options system + valid_keys.add("plando_items") if PlandoOptions.items in plando_options: - ret.plando_items = game_weights.get("plando_items", []) + ret.plando_items = copy.deepcopy(game_weights.get("plando_items", [])) if ret.game == "A Link to the Past": + # TODO there are still more LTTP options not on the options system + valid_keys |= {"sprite_pool", "sprite", "random_sprite_on_event"} roll_alttp_settings(ret, game_weights) + # log a warning for options within a game section that aren't determined as valid + for option_key in game_weights: + if option_key in valid_keys: + continue + logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers " + f"for player {ret.name}.") + return ret @@ -545,7 +547,9 @@ def roll_alttp_settings(ret: argparse.Namespace, weights): if __name__ == '__main__': import atexit confirmation = atexit.register(input, "Press enter to close.") - multiworld = main() + erargs, seed = main() + from Main import main as ERmain + multiworld = ERmain(erargs, seed) if __debug__: import gc import sys diff --git a/KH1Client.py b/KH1Client.py new file mode 100644 index 00000000..4c3ed501 --- /dev/null +++ b/KH1Client.py @@ -0,0 +1,9 @@ +if __name__ == '__main__': + import ModuleUpdate + ModuleUpdate.update() + + import Utils + Utils.init_logging("KH1Client", exception_logger="Client") + + from worlds.kh1.Client import launch + launch() diff --git a/LICENSE b/LICENSE index 40716cff..60d31b7b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Copyright (c) 2017 LLCoolDave -Copyright (c) 2022 Berserker66 +Copyright (c) 2025 Berserker66 Copyright (c) 2022 CaitSith2 Copyright (c) 2021 LegendaryLinux diff --git a/Launcher.py b/Launcher.py index e4b65be9..22c0944a 100644 --- a/Launcher.py +++ b/Launcher.py @@ -16,25 +16,27 @@ import multiprocessing import shlex import subprocess import sys +import urllib.parse import webbrowser from os.path import isfile from shutil import which -from typing import Callable, Sequence, Union, Optional - -import Utils -import settings -from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths +from typing import Callable, Optional, Sequence, Tuple, Union 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(): - file = settings.get_settings().filename + s = settings.get_settings() + file = s.filename + s.save() assert file, "host.yaml missing" if is_linux: exe = which('sensible-editor') or which('gedit') or \ @@ -101,13 +103,71 @@ 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), ]) -def identify(path: Union[None, str]): +def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None: + url = urllib.parse.urlparse(path) + queries = urllib.parse.parse_qs(url.query) + launch_args = (path, *launch_args) + client_component = None + text_client_component = None + if "game" in queries: + game = queries["game"][0] + else: # TODO around 0.6.0 - this is for pre this change webhost uri's + game = "Archipelago" + for component in components: + if component.supports_uri and component.game_name == game: + client_component = component + elif component.display_name == "Text Client": + text_client_component = component + + if client_component is None: + run_component(text_client_component, *launch_args) + return + + from kvui import App, Button, BoxLayout, Label, Window + + class Popup(App): + def __init__(self): + self.title = "Connect to Multiworld" + self.icon = r"data/icon.png" + super().__init__() + + def build(self): + layout = BoxLayout(orientation="vertical") + layout.add_widget(Label(text="Select client to open and connect with.")) + button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4)) + + text_client_button = Button( + text=text_client_component.display_name, + on_release=lambda *args: run_component(text_client_component, *launch_args) + ) + button_row.add_widget(text_client_button) + + game_client_button = Button( + text=client_component.display_name, + on_release=lambda *args: run_component(client_component, *launch_args) + ) + button_row.add_widget(game_client_button) + + layout.add_widget(button_row) + + return layout + + def _stop(self, *largs): + # see run_gui Launcher _stop comment for details + self.root_window.close() + super()._stop(*largs) + + Popup().run() + + +def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]: if path is None: return None, None for component in components: @@ -164,9 +224,8 @@ refresh_components: Optional[Callable[[], None]] = None def run_gui(): - from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget + from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage from kivy.core.window import Window - from kivy.uix.image import AsyncImage from kivy.uix.relativelayout import RelativeLayout class Launcher(App): @@ -177,7 +236,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__() @@ -199,8 +258,8 @@ def run_gui(): button.component = component button.bind(on_release=self.component_action) if component.icon != "icon": - image = AsyncImage(source=icon_paths[component.icon], - size=(38, 38), size_hint=(None, 1), pos=(5, 0)) + image = ApAsyncImage(source=icon_paths[component.icon], + size=(38, 38), size_hint=(None, 1), pos=(5, 0)) box_layout = RelativeLayout(size_hint_y=None, height=40) box_layout.add_widget(button) box_layout.add_widget(image) @@ -266,7 +325,7 @@ def run_gui(): if file and component: run_component(component, file) else: - logging.warning(f"unable to identify component for {filename}") + logging.warning(f"unable to identify component for {file}") def _stop(self, *largs): # ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm. @@ -299,20 +358,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None): elif not args: args = {} - if args.get("Patch|Game|Component", None) is not None: - file, component = identify(args["Patch|Game|Component"]) + path = args.get("Patch|Game|Component|url", None) + if path is not None: + if path.startswith("archipelago://"): + handle_uri(path, args.get("args", ())) + return + file, component = identify(path) if file: args['file'] = file if component: args['component'] = component if not component: - logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}") + logging.warning(f"Could not identify Component responsible for {path}") if args["update_settings"]: update_settings() - if 'file' in args: + if "file" in args: run_component(args["component"], args["file"], *args["args"]) - elif 'component' in args: + elif "component" in args: run_component(args["component"], *args["args"]) elif not args["update_settings"]: run_gui() @@ -322,12 +385,16 @@ if __name__ == '__main__': init_logging('Launcher') Utils.freeze_support() multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work - parser = argparse.ArgumentParser(description='Archipelago Launcher') + parser = argparse.ArgumentParser( + description='Archipelago Launcher', + usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]" + ) run_group = parser.add_argument_group("Run") run_group.add_argument("--update_settings", action="store_true", help="Update host.yaml and exit.") - run_group.add_argument("Patch|Game|Component", type=str, nargs="?", - help="Pass either a patch file, a generated game or the name of a component to run.") + run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?", + help="Pass either a patch file, a generated game, the component name to run, or a url to " + "connect with.") run_group.add_argument("args", nargs="*", help="Arguments to pass to component.") main(parser.parse_args()) diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py index a51645fe..e2e16922 100644 --- a/LinksAwakeningClient.py +++ b/LinksAwakeningClient.py @@ -235,7 +235,7 @@ class RAGameboy(): def check_command_response(self, command: str, response: bytes): if command == "VERSION": - ok = re.match("\d+\.\d+\.\d+", response.decode('ascii')) is not None + ok = re.match(r"\d+\.\d+\.\d+", response.decode('ascii')) is not None else: ok = response.startswith(command.encode()) if not ok: @@ -467,6 +467,8 @@ class LinksAwakeningContext(CommonContext): def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None: self.client = LinksAwakeningClient() + self.slot_data = {} + if magpie: self.magpie_enabled = True self.magpie = MagpieBridge() @@ -558,12 +560,18 @@ class LinksAwakeningContext(CommonContext): while self.client.auth == None: await asyncio.sleep(0.1) + + # Just return if we're closing + if self.exit_event.is_set(): + return self.auth = self.client.auth await self.send_connect() def on_package(self, cmd: str, args: dict): if cmd == "Connected": self.game = self.slot_info[self.slot].game + self.slot_data = args.get("slot_data", {}) + # TODO - use watcher_event if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): @@ -628,6 +636,7 @@ class LinksAwakeningContext(CommonContext): self.magpie.set_checks(self.client.tracker.all_checks) await self.magpie.set_item_tracker(self.client.item_tracker) await self.magpie.send_gps(self.client.gps_tracker) + self.magpie.slot_data = self.slot_data except Exception: # Don't let magpie errors take out the client pass diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 9c5bd102..7e33a3d5 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -14,7 +14,7 @@ import tkinter as tk from argparse import Namespace from concurrent.futures import as_completed, ThreadPoolExecutor from glob import glob -from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \ +from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, BOTH, TOP, LabelFrame, \ IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage from tkinter.constants import DISABLED, NORMAL from urllib.parse import urlparse @@ -29,7 +29,8 @@ from Utils import output_path, local_path, user_path, open_file, get_cert_none_s GAME_ALTTP = "A Link to the Past" - +WINDOW_MIN_HEIGHT = 525 +WINDOW_MIN_WIDTH = 425 class AdjusterWorld(object): def __init__(self, sprite_pool): @@ -242,16 +243,17 @@ def adjustGUI(): from argparse import Namespace from Utils import __version__ as MWVersion adjustWindow = Tk() + adjustWindow.minsize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT) adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion) set_icon(adjustWindow) rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow) - bottomFrame2 = Frame(adjustWindow) + bottomFrame2 = Frame(adjustWindow, padx=8, pady=2) romFrame, romVar = get_rom_frame(adjustWindow) - romDialogFrame = Frame(adjustWindow) + romDialogFrame = Frame(adjustWindow, padx=8, pady=2) baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust') romVar2 = StringVar() romEntry2 = Entry(romDialogFrame, textvariable=romVar2) @@ -261,9 +263,9 @@ def adjustGUI(): romVar2.set(rom) romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2) - romDialogFrame.pack(side=TOP, expand=True, fill=X) - baseRomLabel2.pack(side=LEFT) - romEntry2.pack(side=LEFT, expand=True, fill=X) + romDialogFrame.pack(side=TOP, expand=False, fill=X) + baseRomLabel2.pack(side=LEFT, expand=False, fill=X, padx=(0, 8)) + romEntry2.pack(side=LEFT, expand=True, fill=BOTH, pady=1) romSelectButton2.pack(side=LEFT) def adjustRom(): @@ -331,12 +333,11 @@ def adjustGUI(): messagebox.showinfo(title="Success", message="Settings saved to persistent storage") adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom) - rom_options_frame.pack(side=TOP) + rom_options_frame.pack(side=TOP, padx=8, pady=8, fill=BOTH, expand=True) adjustButton.pack(side=LEFT, padx=(5,5)) saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings) saveButton.pack(side=LEFT, padx=(5,5)) - bottomFrame2.pack(side=TOP, pady=(5,5)) tkinter_center_window(adjustWindow) @@ -576,7 +577,7 @@ class AttachTooltip(object): def get_rom_frame(parent=None): adjuster_settings = get_adjuster_settings(GAME_ALTTP) - romFrame = Frame(parent) + romFrame = Frame(parent, padx=8, pady=8) baseRomLabel = Label(romFrame, text='LttP Base Rom: ') romVar = StringVar(value=adjuster_settings.baserom) romEntry = Entry(romFrame, textvariable=romVar) @@ -596,20 +597,19 @@ def get_rom_frame(parent=None): romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect) baseRomLabel.pack(side=LEFT) - romEntry.pack(side=LEFT, expand=True, fill=X) + romEntry.pack(side=LEFT, expand=True, fill=BOTH, pady=1) romSelectButton.pack(side=LEFT) - romFrame.pack(side=TOP, expand=True, fill=X) + romFrame.pack(side=TOP, fill=X) return romFrame, romVar def get_rom_options_frame(parent=None): adjuster_settings = get_adjuster_settings(GAME_ALTTP) - romOptionsFrame = LabelFrame(parent, text="Rom options") - romOptionsFrame.columnconfigure(0, weight=1) - romOptionsFrame.columnconfigure(1, weight=1) + romOptionsFrame = LabelFrame(parent, text="Rom options", padx=8, pady=8) + for i in range(5): - romOptionsFrame.rowconfigure(i, weight=1) + romOptionsFrame.rowconfigure(i, weight=0, pad=4) vars = Namespace() vars.MusicVar = IntVar() @@ -660,7 +660,7 @@ def get_rom_options_frame(parent=None): spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect) baseSpriteLabel.pack(side=LEFT) - spriteEntry.pack(side=LEFT) + spriteEntry.pack(side=LEFT, expand=True, fill=X) spriteSelectButton.pack(side=LEFT) oofDialogFrame = Frame(romOptionsFrame) diff --git a/Main.py b/Main.py index de6b467f..d0e7a7f8 100644 --- a/Main.py +++ b/Main.py @@ -11,7 +11,8 @@ from typing import Dict, List, Optional, Set, Tuple, Union import worlds from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region -from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items +from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \ + flood_items from Options import StartInventoryPool from Utils import __version__, output_path, version_tuple, get_settings from settings import get_settings @@ -45,6 +46,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld.sprite_pool = args.sprite_pool.copy() multiworld.set_options(args) + if args.csv_output: + from Options import dump_player_options + dump_player_options(multiworld) multiworld.set_item_links() multiworld.state = CollectionState(multiworld) logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed) @@ -100,7 +104,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld.early_items[player][item_name] = max(0, early-count) remaining_count = count-early if remaining_count > 0: - local_early = multiworld.early_local_items[player].get(item_name, 0) + local_early = multiworld.local_early_items[player].get(item_name, 0) if local_early: multiworld.early_items[player][item_name] = max(0, local_early - remaining_count) del local_early @@ -124,14 +128,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player in multiworld.player_ids: exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value) multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value + world_excluded_locations = set() for location_name in multiworld.worlds[player].options.priority_locations.value: try: location = multiworld.get_location(location_name, player) - except KeyError as e: # failed to find the given location. Check if it's a legitimate location - if location_name not in multiworld.worlds[player].location_name_to_id: - raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e - else: + except KeyError: + continue + + if location.progress_type != LocationProgressType.EXCLUDED: location.progress_type = LocationProgressType.PRIORITY + else: + logger.warning(f"Unable to prioritize location \"{location_name}\" in player {player}'s world because the world excluded it.") + world_excluded_locations.add(location_name) + multiworld.worlds[player].options.priority_locations.value -= world_excluded_locations # Set local and non-local item rules. if multiworld.players > 1: @@ -139,122 +148,46 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No else: multiworld.worlds[1].options.non_local_items.value = set() multiworld.worlds[1].options.local_items.value = set() - + + AutoWorld.call_all(multiworld, "connect_entrances") AutoWorld.call_all(multiworld, "generate_basic") # remove starting inventory from pool items. # Because some worlds don't actually create items during create_items this has to be as late as possible. - if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids): - new_items: List[Item] = [] - depletion_pool: Dict[int, Dict[str, int]] = { - player: getattr(multiworld.worlds[player].options, - "start_inventory_from_pool", - StartInventoryPool({})).value.copy() - for player in multiworld.player_ids - } - for player, items in depletion_pool.items(): - player_world: AutoWorld.World = multiworld.worlds[player] - for count in items.values(): - for _ in range(count): - new_items.append(player_world.create_filler()) - target: int = sum(sum(items.values()) for items in depletion_pool.values()) - for i, item in enumerate(multiworld.itempool): - if depletion_pool[item.player].get(item.name, 0): - target -= 1 - depletion_pool[item.player][item.name] -= 1 - # quick abort if we have found all items - if not target: - new_items.extend(multiworld.itempool[i+1:]) - break - else: - new_items.append(item) - - # leftovers? - if target: - for player, remaining_items in depletion_pool.items(): - remaining_items = {name: count for name, count in remaining_items.items() if count} - if remaining_items: - raise Exception(f"{multiworld.get_player_name(player)}" - f" is trying to remove items from their pool that don't exist: {remaining_items}") - assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change." - multiworld.itempool[:] = new_items - - # temporary home for item links, should be moved out of Main - for group_id, group in multiworld.groups.items(): - def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ - Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] - ]: - classifications: Dict[str, int] = collections.defaultdict(int) - counters = {player: {name: 0 for name in shared_pool} for player in players} - for item in multiworld.itempool: - if item.player in counters and item.name in shared_pool: - counters[item.player][item.name] += 1 - classifications[item.name] |= item.classification - - for player in players.copy(): - if all([counters[player][item] == 0 for item in shared_pool]): - players.remove(player) - del (counters[player]) - - if not players: - return None, None - - for item in shared_pool: - count = min(counters[player][item] for player in players) - if count: - for player in players: - counters[player][item] = count - else: - for player in players: - del (counters[player][item]) - return counters, classifications - - common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) - if not common_item_count: - continue + fallback_inventory = StartInventoryPool({}) + depletion_pool: Dict[int, Dict[str, int]] = { + player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy() + for player in multiworld.player_ids + } + target_per_player = { + player: sum(target_items.values()) for player, target_items in depletion_pool.items() if target_items + } + if target_per_player: new_itempool: List[Item] = [] - for item_name, item_count in next(iter(common_item_count.values())).items(): - for _ in range(item_count): - new_item = group["world"].create_item(item_name) - # mangle together all original classification bits - new_item.classification |= classifications[item_name] - new_itempool.append(new_item) - region = Region("Menu", group_id, multiworld, "ItemLink") - multiworld.regions.append(region) - locations = region.locations + # Make new itempool with start_inventory_from_pool items removed for item in multiworld.itempool: - count = common_item_count.get(item.player, {}).get(item.name, 0) - if count: - loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}", - None, region) - loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ - state.has(item_name, group_id_, count_) - - locations.append(loc) - loc.place_locked_item(item) - common_item_count[item.player][item.name] -= 1 + if depletion_pool[item.player].get(item.name, 0): + depletion_pool[item.player][item.name] -= 1 else: new_itempool.append(item) - itemcount = len(multiworld.itempool) - multiworld.itempool = new_itempool + # Create filler in place of the removed items, warn if any items couldn't be found in the multiworld itempool + for player, target in target_per_player.items(): + unfound_items = {item: count for item, count in depletion_pool[player].items() if count} - while itemcount > len(multiworld.itempool): - items_to_add = [] - for player in group["players"]: - if group["link_replacement"]: - item_player = group_id - else: - item_player = player - if group["replacement_items"][player]: - items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player, - group["replacement_items"][player])) - else: - items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player)) - multiworld.random.shuffle(items_to_add) - multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)]) + if unfound_items: + player_name = multiworld.get_player_name(player) + logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}") + + needed_items = target_per_player[player] - sum(unfound_items.values()) + new_itempool += [multiworld.worlds[player].create_filler() for _ in range(needed_items)] + + assert len(multiworld.itempool) == len(new_itempool), "Item Pool amounts should not change." + multiworld.itempool[:] = new_itempool + + multiworld.link_items() if any(multiworld.item_links.values()): multiworld._all_state = None @@ -310,6 +243,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No def write_multidata(): import NetUtils + from NetUtils import HintStatus slot_data = {} client_versions = {} games = {} @@ -334,10 +268,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for slot in multiworld.player_ids: slot_data[slot] = multiworld.worlds[slot].fill_slot_data() - def precollect_hint(location): + def precollect_hint(location: Location, auto_status: HintStatus): entrance = er_hint_data.get(location.player, {}).get(location.address, "") hint = NetUtils.Hint(location.item.player, location.player, location.address, - location.item.code, False, entrance, location.item.flags) + location.item.code, False, entrance, location.item.flags, auto_status) precollected_hints[location.player].add(hint) if location.item.player not in multiworld.groups: precollected_hints[location.item.player].add(hint) @@ -350,19 +284,22 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No if type(location.address) == int: assert location.item.code is not None, "item code None should be event, " \ "location.address should then also be None. Location: " \ - f" {location}" + f" {location}, Item: {location.item}" assert location.address not in locations_data[location.player], ( f"Locations with duplicate address. {location} and " f"{locations_data[location.player][location.address]}") locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags + auto_status = HintStatus.HINT_AVOID if location.item.trap else HintStatus.HINT_PRIORITY if location.name in multiworld.worlds[location.player].options.start_location_hints: - precollect_hint(location) + if not location.item.trap: # Unspecified status for location hints, except traps + auto_status = HintStatus.HINT_UNSPECIFIED + precollect_hint(location, auto_status) elif location.item.name in multiworld.worlds[location.item.player].options.start_hints: - precollect_hint(location) + precollect_hint(location, auto_status) elif any([location.item.name in multiworld.worlds[player].options.start_hints for player in multiworld.groups.get(location.item.player, {}).get("players", [])]): - precollect_hint(location) + precollect_hint(location, auto_status) # embedded data package data_package = { @@ -374,11 +311,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # get spheres -> filter address==None -> skip empty spheres: List[Dict[int, Set[int]]] = [] - for sphere in multiworld.get_spheres(): + for sphere in multiworld.get_sendable_spheres(): current_sphere: Dict[int, Set[int]] = collections.defaultdict(set) for sphere_location in sphere: - if type(sphere_location.address) is int: - current_sphere[sphere_location.player].add(sphere_location.address) + current_sphere[sphere_location.player].add(sphere_location.address) if current_sphere: spheres.append(dict(current_sphere)) @@ -399,6 +335,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No "seed_name": multiworld.seed_name, "spheres": spheres, "datapackage": data_package, + "race_mode": int(multiworld.is_race), } AutoWorld.call_all(multiworld, "modify_multidata", multidata) @@ -411,7 +348,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No output_file_futures.append(pool.submit(write_multidata)) if not check_accessibility_task.result(): if not multiworld.can_beat_game(): - raise Exception("Game appears as unbeatable. Aborting.") + raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld) else: logger.warning("Location Accessibility requirements not fulfilled.") diff --git a/ModuleUpdate.py b/ModuleUpdate.py index ed041bef..04cf25ea 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -5,8 +5,15 @@ import multiprocessing import warnings -if sys.version_info < (3, 8, 6): - raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") +if sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 11): + # Official micro version updates. This should match the number in docs/running from source.md. + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.10.15+ is supported.") +elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 15): + # There are known security issues, but no easy way to install fixed versions on Windows for testing. + warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.") +elif sys.version_info < (3, 10, 1): + # Other platforms may get security backports instead of micro updates, so the number is unreliable. + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.1+ is supported.") # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) _skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) @@ -75,13 +82,13 @@ def update(yes: bool = False, force: bool = False) -> None: if not update_ran: update_ran = True + install_pkg_resources(yes=yes) + import pkg_resources + if force: update_command() return - install_pkg_resources(yes=yes) - import pkg_resources - prev = "" # if a line ends in \ we store here and merge later for req_file in requirements_files: path = os.path.join(os.path.dirname(sys.argv[0]), req_file) diff --git a/MultiServer.py b/MultiServer.py index dc5e3d21..51b72c93 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -15,6 +15,7 @@ import math import operator import pickle import random +import shlex import threading import time import typing @@ -27,9 +28,11 @@ ModuleUpdate.update() if typing.TYPE_CHECKING: import ssl + from NetUtils import ServerConnection -import websockets import colorama +import websockets +from websockets.extensions.permessage_deflate import PerMessageDeflate try: # ponyorm is a requirement for webhost, not default server, so may not be importable from pony.orm.dbapiprovider import OperationalError @@ -40,7 +43,8 @@ import NetUtils import Utils from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ - SlotType, LocationStore + SlotType, LocationStore, Hint, HintStatus +from BaseClasses import ItemClassification min_client_version = Version(0, 1, 6) colorama.init() @@ -67,6 +71,21 @@ def update_dict(dictionary, entries): return dictionary +def queue_gc(): + import gc + from threading import Thread + + gc_thread: typing.Optional[Thread] = getattr(queue_gc, "_thread", None) + def async_collect(): + time.sleep(2) + setattr(queue_gc, "_thread", None) + gc.collect() + if not gc_thread: + gc_thread = Thread(target=async_collect) + setattr(queue_gc, "_thread", gc_thread) + gc_thread.start() + + # functions callable on storable data on the server by clients modify_functions = { # generic: @@ -102,13 +121,14 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int: class Client(Endpoint): version = Version(0, 0, 0) - tags: typing.List[str] = [] + tags: typing.List[str] remote_items: bool remote_start_inventory: bool no_items: bool no_locations: bool + no_text: bool - def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context): + def __init__(self, socket: "ServerConnection", ctx: Context) -> None: super().__init__(socket) self.auth = False self.team = None @@ -158,6 +178,7 @@ class Context: "compatibility": int} # team -> slot id -> list of clients authenticated to slot. clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]] + endpoints: list[Client] locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]] location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]] hints_used: typing.Dict[typing.Tuple[int, int], int] @@ -169,11 +190,9 @@ class Context: slot_info: typing.Dict[int, NetworkSlot] generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] - item_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) + item_names: typing.Dict[str, typing.Dict[int, str]] item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] - location_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) + location_names: typing.Dict[str, typing.Dict[int, str]] location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] @@ -182,7 +201,6 @@ class Context: """ each sphere is { player: { location_id, ... } } """ logger: logging.Logger - def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -215,7 +233,7 @@ class Context: self.hint_cost = hint_cost self.location_check_points = location_check_points self.hints_used = collections.defaultdict(int) - self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set) + self.hints: typing.Dict[team_slot, typing.Set[Hint]] = collections.defaultdict(set) self.release_mode: str = release_mode self.remaining_mode: str = remaining_mode self.collect_mode: str = collect_mode @@ -253,6 +271,10 @@ class Context: self.location_name_groups = {} self.all_item_and_group_names = {} self.all_location_and_group_names = {} + self.item_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')) + self.location_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')) self.non_hintable_names = collections.defaultdict(frozenset) self._load_game_data() @@ -346,18 +368,28 @@ class Context: return True def broadcast_all(self, msgs: typing.List[dict]): - msgs = self.dumper(msgs) - endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth) - async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) + msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs) + data = self.dumper(msgs) + endpoints = ( + endpoint + for endpoint in self.endpoints + if endpoint.auth and not (msg_is_text and endpoint.no_text) + ) + async_start(self.broadcast_send_encoded_msgs(endpoints, data)) def broadcast_text_all(self, text: str, additional_arguments: dict = {}): self.logger.info("Notice (all): %s" % text) self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}]) def broadcast_team(self, team: int, msgs: typing.List[dict]): - msgs = self.dumper(msgs) - endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values())) - async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) + msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs) + data = self.dumper(msgs) + endpoints = ( + endpoint + for endpoint in itertools.chain.from_iterable(self.clients[team].values()) + if not (msg_is_text and endpoint.no_text) + ) + async_start(self.broadcast_send_encoded_msgs(endpoints, data)) def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]): msgs = self.dumper(msgs) @@ -371,13 +403,13 @@ class Context: await on_client_disconnected(self, endpoint) def notify_client(self, client: Client, text: str, additional_arguments: dict = {}): - if not client.auth: + if not client.auth or client.no_text: return self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}])) def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}): - if not client.auth: + if not client.auth or client.no_text: return async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments} @@ -412,6 +444,8 @@ class Context: use_embedded_server_options: bool): self.read_data = {} + # there might be a better place to put this. + self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0) mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," @@ -424,7 +458,7 @@ class Context: self.slot_info = decoded_obj["slot_info"] self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()} - self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items() + self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items() if slot_info.type == SlotType.group} self.clients = {0: {}} @@ -551,6 +585,9 @@ class Context: self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.") else: self.save_dirty = False + if not atexit_save: # if atexit is used, that keeps a reference anyway + queue_gc() + self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True) self.auto_saver_thread.start() @@ -634,13 +671,29 @@ class Context: return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot]))) return 0 - def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None): + def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None, + changed: typing.Optional[typing.Set[team_slot]] = None) -> None: + """Refreshes the hints for the specified team/slot. Providing 'None' for either team or slot + will refresh all teams or all slots respectively. If a set is passed for 'changed', each (team,slot) + pair that has at least one hint modified will be added to the set. + """ for hint_team, hint_slot in self.hints: - if (team is None or team == hint_team) and (slot is None or slot == hint_slot): - self.hints[hint_team, hint_slot] = { - hint.re_check(self, hint_team) for hint in - self.hints[hint_team, hint_slot] - } + if team != hint_team and team is not None: + continue # Check specified team only, all if team is None + if slot != hint_slot and slot is not None: + continue # Check specified slot only, all if slot is None + new_hints: typing.Set[Hint] = set() + for hint in self.hints[hint_team, hint_slot]: + new_hint = hint.re_check(self, hint_team) + new_hints.add(new_hint) + if hint == new_hint: + continue + for player in self.slot_set(hint.receiving_player) | {hint.finding_player}: + if changed is not None: + changed.add((hint_team,player)) + if slot is not None and slot != player: + self.replace_hint(hint_team, player, hint, new_hint) + self.hints[hint_team, hint_slot] = new_hints def get_rechecked_hints(self, team: int, slot: int): self.recheck_hints(team, slot) @@ -689,7 +742,7 @@ class Context: else: return self.player_names[team, slot] - def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False, + def notify_hints(self, team: int, hints: typing.List[Hint], only_new: bool = False, recipients: typing.Sequence[int] = None): """Send and remember hints.""" if only_new: @@ -704,7 +757,8 @@ class Context: concerns[player].append(data) if not hint.local and data not in concerns[hint.finding_player]: concerns[hint.finding_player].append(data) - # remember hints in all cases + + # only remember hints that were not already found at the time of creation if not hint.found: # since hints are bidirectional, finding player and receiving player, # we can check once if hint already exists @@ -720,13 +774,24 @@ class Context: self.on_new_hint(team, slot) for slot, hint_data in concerns.items(): if recipients is None or slot in recipients: - clients = self.clients[team].get(slot) + clients = filter(lambda c: not c.no_text, self.clients[team].get(slot, [])) if not clients: continue client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)] for client in clients: async_start(self.send_msgs(client, client_hints)) + def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]: + for hint in self.hints[team, finding_player]: + if hint.location == seeked_location: + return hint + return None + + def replace_hint(self, team: int, slot: int, old_hint: Hint, new_hint: Hint) -> None: + if old_hint in self.hints[team, slot]: + self.hints[team, slot].remove(old_hint) + self.hints[team, slot].add(new_hint) + # "events" def on_goal_achieved(self, client: Client): @@ -768,7 +833,7 @@ def update_aliases(ctx: Context, team: int): async_start(ctx.send_encoded_msgs(client, cmd)) -async def server(websocket, path: str = "/", ctx: Context = None): +async def server(websocket: "ServerConnection", path: str = "/", ctx: Context = None) -> None: client = Client(websocket, ctx) ctx.endpoints.append(client) @@ -859,6 +924,10 @@ async def on_client_joined(ctx: Context, client: Client): "If your client supports it, " "you may have additional local commands you can list with /help.", {"type": "Tutorial"}) + if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions): + ctx.notify_client(client, "Warning: your client does not support compressed websocket connections! " + "It may stop working in the future. If you are a player, please report this to the " + "client's developer.") ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) @@ -925,9 +994,13 @@ def get_status_string(ctx: Context, team: int, tag: str): tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags]) completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})" tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else "" - goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "." + status_text = ( + " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else + " and is ready." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_READY else + "." + ) text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \ - f"{tag_text}{goal_text} {completion_text}" + f"{tag_text}{status_text} {completion_text}" return text @@ -991,7 +1064,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False): collect_player(ctx, team, group, True) -def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]: +def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[typing.Tuple[int, int]]: return ctx.locations.get_remaining(ctx.location_checks, team, slot) @@ -1005,21 +1078,37 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int], count_activity: bool = True): + slot_locations = ctx.locations[slot] new_locations = set(locations) - ctx.location_checks[team, slot] - new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata + new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata if new_locations: if count_activity: ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) + + sortable: list[tuple[int, int, int, int]] = [] for location in new_locations: - item_id, target_player, flags = ctx.locations[slot][location] + # extract all fields to avoid runtime overhead in LocationStore + item_id, target_player, flags = slot_locations[location] + # sort/group by receiver and item + sortable.append((target_player, item_id, location, flags)) + + info_texts: list[dict[str, typing.Any]] = [] + for target_player, item_id, location, flags in sorted(sortable): new_item = NetworkItem(item_id, location, slot, flags) send_items_to(ctx, team, target_player, new_item) ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) - info_text = json_format_send_event(new_item, target_player) - ctx.broadcast_team(team, [info_text]) + if len(info_texts) >= 140: + # split into chunks that are close to compression window of 64K but not too big on the wire + # (roughly 1300-2600 bytes after compression depending on repetitiveness) + ctx.broadcast_team(team, info_texts) + info_texts.clear() + info_texts.append(json_format_send_event(new_item, target_player)) + ctx.broadcast_team(team, info_texts) + del info_texts + del sortable ctx.location_checks[team, slot] |= new_locations send_new_items(ctx) @@ -1028,14 +1117,15 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi "hint_points": get_slot_points(ctx, team, slot), "checked_locations": new_locations, # send back new checks only }]) - old_hints = ctx.hints[team, slot].copy() - ctx.recheck_hints(team, slot) - if old_hints != ctx.hints[team, slot]: - ctx.on_changed_hints(team, slot) + updated_slots: typing.Set[tuple[int, int]] = set() + ctx.recheck_hints(team, slot, updated_slots) + for hint_team, hint_slot in updated_slots: + ctx.on_changed_hints(hint_team, hint_slot) ctx.save() -def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]: +def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str], auto_status: HintStatus) \ + -> typing.List[Hint]: hints = [] slots: typing.Set[int] = {slot} for group_id, group in ctx.groups.items(): @@ -1045,31 +1135,58 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item] for finding_player, location_id, item_id, receiving_player, item_flags \ in ctx.locations.find_item(slots, seeked_item_id): - found = location_id in ctx.location_checks[team, finding_player] - entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") - hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance, - item_flags)) + prev_hint = ctx.get_hint(team, slot, location_id) + if prev_hint: + hints.append(prev_hint) + else: + found = location_id in ctx.location_checks[team, finding_player] + entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") + new_status = auto_status + if found: + new_status = HintStatus.HINT_FOUND + elif item_flags & ItemClassification.trap: + new_status = HintStatus.HINT_AVOID + hints.append(Hint(receiving_player, finding_player, location_id, item_id, found, entrance, + item_flags, new_status)) return hints -def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: +def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str, auto_status: HintStatus) \ + -> typing.List[Hint]: seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location] - return collect_hint_location_id(ctx, team, slot, seeked_location) + return collect_hint_location_id(ctx, team, slot, seeked_location, auto_status) -def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]: +def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int, auto_status: HintStatus) \ + -> typing.List[Hint]: + prev_hint = ctx.get_hint(team, slot, seeked_location) + if prev_hint: + return [prev_hint] result = ctx.locations[slot].get(seeked_location, (None, None, None)) if any(result): item_id, receiving_player, item_flags = result found = seeked_location in ctx.location_checks[team, slot] entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "") - return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)] + new_status = auto_status + if found: + new_status = HintStatus.HINT_FOUND + elif item_flags & ItemClassification.trap: + new_status = HintStatus.HINT_AVOID + return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags, + new_status)] return [] -def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: +status_names: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "(found)", + HintStatus.HINT_UNSPECIFIED: "(unspecified)", + HintStatus.HINT_NO_PRIORITY: "(no priority)", + HintStatus.HINT_AVOID: "(avoid)", + HintStatus.HINT_PRIORITY: "(priority)", +} +def format_hint(ctx: Context, team: int, hint: Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \ f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \ @@ -1077,7 +1194,8 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: if hint.entrance: text += f" at {hint.entrance}" - return text + (". (found)" if hint.found else ".") + + return text + ". " + status_names.get(hint.status, "(unknown)") def json_format_send_event(net_item: NetworkItem, receiving_player: int): @@ -1132,7 +1250,10 @@ class CommandProcessor(metaclass=CommandMeta): if not raw: return try: - command = raw.split() + try: + command = shlex.split(raw, comments=False) + except ValueError: # most likely: "ValueError: No closing quotation" + command = raw.split() basecommand = command[0] if basecommand[0] == self.marker: method = self.commands.get(basecommand[1:].lower(), None) @@ -1203,6 +1324,10 @@ class CommonCommandProcessor(CommandProcessor): timer = int(seconds, 10) except ValueError: timer = 10 + else: + if timer > 60 * 60: + raise ValueError(f"{timer} is invalid. Maximum is 1 hour.") + async_start(countdown(self.ctx, timer)) return True @@ -1350,10 +1475,10 @@ class ClientMessageProcessor(CommonCommandProcessor): def _cmd_remaining(self) -> bool: """List remaining items in your game, but not their location or recipient""" if self.ctx.remaining_mode == "enabled": - remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) - if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] - for item_id in remaining_item_ids)) + rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot) + if rest_locations: + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id] + for slot, item_id in rest_locations)) else: self.output("No remaining items found.") return True @@ -1363,10 +1488,10 @@ class ClientMessageProcessor(CommonCommandProcessor): return False else: # is goal if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: - remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) - if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] - for item_id in remaining_item_ids)) + rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot) + if rest_locations: + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id] + for slot, item_id in rest_locations)) else: self.output("No remaining items found.") return True @@ -1474,7 +1599,7 @@ class ClientMessageProcessor(CommonCommandProcessor): def get_hints(self, input_text: str, for_location: bool = False) -> bool: points_available = get_client_points(self.ctx, self.client) cost = self.ctx.get_hint_cost(self.client.slot) - + auto_status = HintStatus.HINT_UNSPECIFIED if for_location else HintStatus.HINT_PRIORITY if not input_text: hints = {hint.re_check(self.ctx, self.client.team) for hint in self.ctx.hints[self.client.team, self.client.slot]} @@ -1500,9 +1625,9 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") hints = [] elif not for_location: - hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id) + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id, auto_status) else: - hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id) + hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id, auto_status) else: game = self.ctx.games[self.client.slot] @@ -1522,16 +1647,16 @@ class ClientMessageProcessor(CommonCommandProcessor): hints = [] for item_name in self.ctx.item_name_groups[game][hint_name]: if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID - hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) + hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name, auto_status)) elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name - hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name, auto_status) elif hint_name in self.ctx.location_name_groups[game]: # location group name hints = [] for loc_name in self.ctx.location_name_groups[game][hint_name]: if loc_name in self.ctx.location_names_for_game(game): - hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name)) + hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name, auto_status)) else: # location name - hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) + hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name, auto_status) else: self.output(response) @@ -1696,7 +1821,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): ctx.clients[team][slot].append(client) client.version = args['version'] client.tags = args['tags'] - client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags + client.no_locations = "TextOnly" in client.tags or "Tracker" in client.tags + # set NoText for old PopTracker clients that predate the tag to save traffic + client.no_text = "NoText" in client.tags or ("PopTracker" in client.tags and client.version < (0, 5, 1)) connected_packet = { "cmd": "Connected", "team": client.team, "slot": client.slot, @@ -1769,6 +1896,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): client.tags = args["tags"] if set(old_tags) != set(client.tags): client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags + client.no_text = "NoText" in client.tags or ( + "PopTracker" in client.tags and client.version < (0, 5, 1) + ) ctx.broadcast_text_all( f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags " f"from {old_tags} to {client.tags}.", @@ -1797,19 +1927,63 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): for location in args["locations"]: if type(location) is not int: await ctx.send_msgs(client, - [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts', + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'Locations has to be a list of integers', "original_cmd": cmd}]) return target_item, target_player, flags = ctx.locations[client.slot][location] if create_as_hint: - hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location)) + hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location, + HintStatus.HINT_UNSPECIFIED)) locs.append(NetworkItem(target_item, location, target_player, flags)) ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2) if locs and create_as_hint: ctx.save() await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) - + + elif cmd == 'UpdateHint': + location = args["location"] + player = args["player"] + status = args["status"] + if not isinstance(player, int) or not isinstance(location, int) \ + or (status is not None and not isinstance(status, int)): + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint', + "original_cmd": cmd}]) + return + hint = ctx.get_hint(client.team, player, location) + if not hint: + return # Ignored safely + if client.slot not in ctx.slot_set(hint.receiving_player): + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission', + "original_cmd": cmd}]) + return + new_hint = hint + if status is None: + return + try: + status = HintStatus(status) + except ValueError: + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'UpdateHint: Invalid Status', "original_cmd": cmd}]) + return + if status == HintStatus.HINT_FOUND: + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'UpdateHint: Cannot manually update status to "HINT_FOUND"', "original_cmd": cmd}]) + return + new_hint = new_hint.re_prioritize(ctx, status) + if hint == new_hint: + return + ctx.replace_hint(client.team, hint.finding_player, hint, new_hint) + ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint) + ctx.save() + ctx.on_changed_hints(client.team, hint.finding_player) + ctx.on_changed_hints(client.team, hint.receiving_player) + elif cmd == 'StatusUpdate': update_client_status(ctx, client, args["status"]) @@ -1857,6 +2031,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): args["cmd"] = "SetReply" value = ctx.stored_data.get(args["key"], args.get("default", 0)) args["original_value"] = copy.copy(value) + args["slot"] = client.slot for operation in args["operations"]: func = modify_functions[operation["operation"]] value = func(value, operation["value"]) @@ -1931,8 +2106,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 @@ -2039,6 +2216,8 @@ class ServerCommandProcessor(CommonCommandProcessor): item_name, usable, response = get_intended_text(item_name, names) if usable: amount: int = int(amount) + if amount > 100: + raise ValueError(f"{amount} is invalid. Maximum is 100.") new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))] send_items_to(self.ctx, team, slot, *new_items) @@ -2110,9 +2289,9 @@ class ServerCommandProcessor(CommonCommandProcessor): hints = [] for item_name_from_group in self.ctx.item_name_groups[game][item]: if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID - hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) + hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group, HintStatus.HINT_PRIORITY)) else: # item name or id - hints = collect_hints(self.ctx, team, slot, item) + hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY) if hints: self.ctx.notify_hints(team, hints) @@ -2146,14 +2325,17 @@ class ServerCommandProcessor(CommonCommandProcessor): if usable: if isinstance(location, int): - hints = collect_hint_location_id(self.ctx, team, slot, location) + hints = collect_hint_location_id(self.ctx, team, slot, location, + HintStatus.HINT_UNSPECIFIED) elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]: hints = [] for loc_name_from_group in self.ctx.location_name_groups[game][location]: if loc_name_from_group in self.ctx.location_names_for_game(game): - hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group)) + hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group, + HintStatus.HINT_UNSPECIFIED)) else: - hints = collect_hint_location_name(self.ctx, team, slot, location) + hints = collect_hint_location_name(self.ctx, team, slot, location, + HintStatus.HINT_UNSPECIFIED) if hints: self.ctx.notify_hints(team, hints) else: @@ -2243,6 +2425,8 @@ def parse_args() -> argparse.Namespace: parser.add_argument('--cert_key', help="Path to SSL Certificate Key file") parser.add_argument('--loglevel', default=defaults["loglevel"], choices=['debug', 'info', 'warning', 'error', 'critical']) + parser.add_argument('--logtime', help="Add timestamps to STDOUT", + default=defaults["logtime"], action='store_true') parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int) parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int) parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true') @@ -2323,7 +2507,9 @@ def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLConte async def main(args: argparse.Namespace): - Utils.init_logging("Server", loglevel=args.loglevel.lower()) + Utils.init_logging(name="Server", + loglevel=args.loglevel.lower(), + add_timestamp=args.logtime) ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode, diff --git a/NetUtils.py b/NetUtils.py index f8d698c7..5bcc583c 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -5,11 +5,20 @@ import enum import warnings from json import JSONEncoder, JSONDecoder -import websockets +if typing.TYPE_CHECKING: + from websockets import WebSocketServerProtocol as ServerConnection from Utils import ByValue, Version +class HintStatus(ByValue, enum.IntEnum): + HINT_FOUND = 0 + HINT_UNSPECIFIED = 1 + HINT_NO_PRIORITY = 10 + HINT_AVOID = 20 + HINT_PRIORITY = 30 + + class JSONMessagePart(typing.TypedDict, total=False): text: str # optional @@ -19,6 +28,8 @@ class JSONMessagePart(typing.TypedDict, total=False): player: int # if type == item indicates item flags flags: int + # if type == hint_status + hint_status: HintStatus class ClientStatus(ByValue, enum.IntEnum): @@ -79,6 +90,7 @@ class NetworkItem(typing.NamedTuple): item: int location: int player: int + """ Sending player, except in LocationInfo (from LocationScouts), where it is the receiving player. """ flags: int = 0 @@ -140,7 +152,7 @@ decode = JSONDecoder(object_hook=_object_hook).decode class Endpoint: - socket: websockets.WebSocketServerProtocol + socket: "ServerConnection" def __init__(self, socket): self.socket = socket @@ -183,6 +195,7 @@ class JSONTypes(str, enum.Enum): location_name = "location_name" location_id = "location_id" entrance_name = "entrance_name" + hint_status = "hint_status" class JSONtoTextParser(metaclass=HandlerMeta): @@ -223,7 +236,7 @@ class JSONtoTextParser(metaclass=HandlerMeta): def _handle_player_id(self, node: JSONMessagePart): player = int(node["text"]) - node["color"] = 'magenta' if player == self.ctx.slot else 'yellow' + node["color"] = 'magenta' if self.ctx.slot_concerns_self(player) else 'yellow' node["text"] = self.ctx.player_names[player] return self._handle_color(node) @@ -264,6 +277,10 @@ class JSONtoTextParser(metaclass=HandlerMeta): node["color"] = 'blue' return self._handle_color(node) + def _handle_hint_status(self, node: JSONMessagePart): + node["color"] = status_colors.get(node["hint_status"], "red") + return self._handle_color(node) + class RawJSONtoTextParser(JSONtoTextParser): def _handle_color(self, node: JSONMessagePart): @@ -272,7 +289,8 @@ class RawJSONtoTextParser(JSONtoTextParser): color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, - 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47} + 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47, + 'plum': 35, 'slateblue': 34, 'salmon': 31,} # convert ui colors to terminal colors def color_code(*args): @@ -295,6 +313,27 @@ def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs) parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs}) +status_names: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "(found)", + HintStatus.HINT_UNSPECIFIED: "(unspecified)", + HintStatus.HINT_NO_PRIORITY: "(no priority)", + HintStatus.HINT_AVOID: "(avoid)", + HintStatus.HINT_PRIORITY: "(priority)", +} +status_colors: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "green", + HintStatus.HINT_UNSPECIFIED: "white", + HintStatus.HINT_NO_PRIORITY: "slateblue", + HintStatus.HINT_AVOID: "salmon", + HintStatus.HINT_PRIORITY: "plum", +} + + +def add_json_hint_status(parts: list, hint_status: HintStatus, text: typing.Optional[str] = None, **kwargs): + parts.append({"text": text if text != None else status_names.get(hint_status, "(unknown)"), + "hint_status": hint_status, "type": JSONTypes.hint_status, **kwargs}) + + class Hint(typing.NamedTuple): receiving_player: int finding_player: int @@ -303,14 +342,21 @@ class Hint(typing.NamedTuple): found: bool entrance: str = "" item_flags: int = 0 + status: HintStatus = HintStatus.HINT_UNSPECIFIED def re_check(self, ctx, team) -> Hint: - if self.found: + if self.found and self.status == HintStatus.HINT_FOUND: return self found = self.location in ctx.location_checks[team, self.finding_player] if found: - return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance, - self.item_flags) + return self._replace(found=found, status=HintStatus.HINT_FOUND) + return self + + def re_prioritize(self, ctx, status: HintStatus) -> Hint: + if self.found and status != HintStatus.HINT_FOUND: + status = HintStatus.HINT_FOUND + if status != self.status: + return self._replace(status=status) return self def __hash__(self): @@ -332,10 +378,7 @@ class Hint(typing.NamedTuple): else: add_json_text(parts, "'s World") add_json_text(parts, ". ") - if self.found: - add_json_text(parts, "(found)", type="color", color="green") - else: - add_json_text(parts, "(not found)", type="color", color="red") + add_json_hint_status(parts, self.status) return {"cmd": "PrintJSON", "data": parts, "type": "Hint", "receiving": self.receiving_player, @@ -381,6 +424,8 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu checked = state[team, slot] if not checked: # This optimizes the case where everyone connects to a fresh game at the same time. + if slot not in self: + raise KeyError(slot) return [] return [location_id for location_id in self[slot] if @@ -397,12 +442,12 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu location_id not in checked] def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int - ) -> typing.List[int]: + ) -> typing.List[typing.Tuple[int, int]]: checked = state[team, slot] player_locations = self[slot] - return sorted([player_locations[location_id][0] for - location_id in player_locations if - location_id not in checked]) + return sorted([(player_locations[location_id][1], player_locations[location_id][0]) for + location_id in player_locations if + location_id not in checked]) if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub diff --git a/OoTAdjuster.py b/OoTAdjuster.py index 9519b191..1581d653 100644 --- a/OoTAdjuster.py +++ b/OoTAdjuster.py @@ -1,7 +1,6 @@ import tkinter as tk import argparse import logging -import random import os import zipfile from itertools import chain @@ -197,7 +196,6 @@ def set_icon(window): def adjust(args): # Create a fake multiworld and OOTWorld to use as a base multiworld = MultiWorld(1) - multiworld.per_slot_randoms = {1: random} ootworld = OOTWorld(multiworld, 1) # Set options in the fake OOTWorld for name, option in chain(cosmetic_options.items(), sfx_options.items()): diff --git a/Options.py b/Options.py index 40a6996d..49e82069 100644 --- a/Options.py +++ b/Options.py @@ -8,16 +8,17 @@ import numbers import random import typing import enum +from collections import defaultdict from copy import deepcopy 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 +from Utils import get_file_safe_name, get_fuzzy_results, is_iterable_except_str, output_path if typing.TYPE_CHECKING: - from BaseClasses import PlandoOptions + from BaseClasses import MultiWorld, PlandoOptions from worlds.AutoWorld import World import pathlib @@ -53,8 +54,8 @@ class AssembleOptions(abc.ABCMeta): attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) options.update(new_options) # apply aliases, without name_lookup - aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if - name.startswith("alias_")} + aliases = attrs["aliases"] = {name[6:].lower(): option_id for name, option_id in attrs.items() if + name.startswith("alias_")} assert ( name in {"Option", "VerifyKeys"} or # base abstract classes don't need default @@ -126,10 +127,28 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): # can be weighted between selections supports_weighting = True + rich_text_doc: typing.Optional[bool] = None + """Whether the WebHost should render the Option's docstring as rich text. + + If this is True, the Option's docstring is interpreted as reStructuredText_, + the standard Python markup format. In the WebHost, it's rendered to HTML so + that lists, emphasis, and other rich text features are displayed properly. + + If this is False, the docstring is instead interpreted as plain text, and + displayed as-is on the WebHost with whitespace preserved. + + If this is None, it inherits the value of `WebWorld.rich_text_options_doc`. For + backwards compatibility, this defaults to False, but worlds are encouraged to + set it to True and use reStructuredText for their Option documentation. + + .. _reStructuredText: https://docutils.sourceforge.io/rst.html + """ + # filled by AssembleOptions: name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore # https://github.com/python/typing/discussions/1460 the reason for this type: ignore options: typing.ClassVar[typing.Dict[str, int]] + aliases: typing.ClassVar[typing.Dict[str, int]] def __repr__(self) -> str: return f"{self.__class__.__name__}({self.current_option_name})" @@ -477,7 +496,7 @@ class TextChoice(Choice): def __init__(self, value: typing.Union[str, int]): assert isinstance(value, str) or isinstance(value, int), \ - f"{value} is not a valid option for {self.__class__.__name__}" + f"'{value}' is not a valid option for '{self.__class__.__name__}'" self.value = value @property @@ -598,17 +617,17 @@ class PlandoBosses(TextChoice, metaclass=BossMeta): used_locations.append(location) used_bosses.append(boss) if not cls.valid_boss_name(boss): - raise ValueError(f"{boss.title()} is not a valid boss name.") + raise ValueError(f"'{boss.title()}' is not a valid boss name.") if not cls.valid_location_name(location): - raise ValueError(f"{location.title()} is not a valid boss location name.") + raise ValueError(f"'{location.title()}' is not a valid boss location name.") if not cls.can_place_boss(boss, location): - raise ValueError(f"{location.title()} is not a valid location for {boss.title()} to be placed.") + raise ValueError(f"'{location.title()}' is not a valid location for {boss.title()} to be placed.") else: if cls.duplicate_bosses: if not cls.valid_boss_name(option): - raise ValueError(f"{option} is not a valid boss name.") + raise ValueError(f"'{option}' is not a valid boss name.") else: - raise ValueError(f"{option.title()} is not formatted correctly.") + raise ValueError(f"'{option.title()}' is not formatted correctly.") @classmethod def can_place_boss(cls, boss: str, location: str) -> bool: @@ -670,9 +689,9 @@ class Range(NumericOption): @classmethod def weighted_range(cls, text) -> Range: if text == "random-low": - return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_start)) + return cls(cls.triangular(cls.range_start, cls.range_end, 0.0)) elif text == "random-high": - return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_end)) + return cls(cls.triangular(cls.range_start, cls.range_end, 1.0)) elif text == "random-middle": return cls(cls.triangular(cls.range_start, cls.range_end)) elif text.startswith("random-range-"): @@ -698,11 +717,11 @@ class Range(NumericOption): f"{random_range[0]}-{random_range[1]} is outside allowed range " f"{cls.range_start}-{cls.range_end} for option {cls.__name__}") if text.startswith("random-range-low"): - return cls(cls.triangular(random_range[0], random_range[1], random_range[0])) + return cls(cls.triangular(random_range[0], random_range[1], 0.0)) elif text.startswith("random-range-middle"): return cls(cls.triangular(random_range[0], random_range[1])) elif text.startswith("random-range-high"): - return cls(cls.triangular(random_range[0], random_range[1], random_range[1])) + return cls(cls.triangular(random_range[0], random_range[1], 1.0)) else: return cls(random.randint(random_range[0], random_range[1])) @@ -720,8 +739,16 @@ class Range(NumericOption): return str(self.value) @staticmethod - def triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int: - return int(round(random.triangular(lower, end, tri), 0)) + def triangular(lower: int, end: int, tri: float = 0.5) -> int: + """ + Integer triangular distribution for `lower` inclusive to `end` inclusive. + + Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined. + """ + # Use the continuous range [lower, end + 1) to produce an integer result in [lower, end]. + # random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even + # when a != b, so ensure the result is never more than `end`. + return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower)) class NamedRange(Range): @@ -735,6 +762,12 @@ class NamedRange(Range): elif value > self.range_end and value not in self.special_range_names.values(): raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " + f"and is also not one of the supported named special values: {self.special_range_names}") + + # See docstring + for key in self.special_range_names: + if key != key.lower(): + raise Exception(f"{self.__class__.__name__} has an invalid special_range_names key: {key}. " + f"NamedRange keys must use only lowercase letters, and ideally should be snake_case.") self.value = value @classmethod @@ -762,17 +795,22 @@ class VerifyKeys(metaclass=FreezeValidKeys): verify_location_name: bool = False value: typing.Any - @classmethod - def verify_keys(cls, data: typing.Iterable[str]) -> None: - if cls.valid_keys: - data = set(data) - dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) - extra = dataset - cls._valid_keys + def verify_keys(self) -> None: + if self.valid_keys: + data = set(self.value) + dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data) + extra = dataset - self._valid_keys if extra: - raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " - f"Allowed keys: {cls._valid_keys}.") + raise OptionError( + f"Found unexpected key {', '.join(extra)} in {getattr(self, 'display_name', self)}. " + f"Allowed keys: {self._valid_keys}." + ) def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + try: + self.verify_keys() + except OptionError as validation_error: + raise OptionError(f"Player {player_name} has invalid option keys:\n{validation_error}") if self.convert_name_groups and self.verify_item_name: new_value = type(self.value)() # empty container of whatever value is for item_name in self.value: @@ -787,18 +825,21 @@ class VerifyKeys(metaclass=FreezeValidKeys): for item_name in self.value: if item_name not in world.item_names: picks = get_fuzzy_results(item_name, world.item_names, limit=1) - raise Exception(f"Item {item_name} from option {self} " - f"is not a valid item name from {world.game}. " + raise Exception(f"Item '{item_name}' from option '{self}' " + f"is not a valid item name from '{world.game}'. " f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") elif self.verify_location_name: for location_name in self.value: if location_name not in world.location_names: picks = get_fuzzy_results(location_name, world.location_names, limit=1) - raise Exception(f"Location {location_name} from option {self} " - f"is not a valid location name from {world.game}. " + raise Exception(f"Location '{location_name}' from option '{self}' " + f"is not a valid location name from '{world.game}'. " f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") + def __iter__(self) -> typing.Iterator[typing.Any]: + return self.value.__iter__() + class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]): default = {} supports_weighting = False @@ -809,7 +850,6 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin @classmethod def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict: if type(data) == dict: - cls.verify_keys(data) return cls(data) else: raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") @@ -831,6 +871,8 @@ class ItemDict(OptionDict): verify_item_name = True def __init__(self, value: typing.Dict[str, int]): + if any(item_count is None for item_count in value.values()): + raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .") if any(item_count < 1 for item_count in value.values()): raise Exception("Cannot have non-positive item counts.") super(ItemDict, self).__init__(value) @@ -855,7 +897,6 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys): @classmethod def from_any(cls, data: typing.Any): if is_iterable_except_str(data): - cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) @@ -881,7 +922,6 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys): @classmethod def from_any(cls, data: typing.Any): if is_iterable_except_str(data): - cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) @@ -924,6 +964,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): self.value = [] logging.warning(f"The plando texts module is turned off, " f"so text for {player_name} will be ignored.") + else: + super().verify(world, player_name, plando_options) + + def verify_keys(self) -> None: + if self.valid_keys: + data = set(text.at for text in self) + dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data) + extra = dataset - self._valid_keys + if extra: + raise OptionError( + f"Invalid \"at\" placement {', '.join(extra)} in {getattr(self, 'display_name', self)}. " + f"Allowed placements: {self._valid_keys}." + ) @classmethod def from_any(cls, data: PlandoTextsFromAnyType) -> Self: @@ -934,7 +987,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): if random.random() < float(text.get("percentage", 100)/100): at = text.get("at", None) if at is not None: + if isinstance(at, dict): + if at: + at = random.choices(list(at.keys()), + weights=list(at.values()), k=1)[0] + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") given_text = text.get("text", []) + if isinstance(given_text, dict): + if not given_text: + given_text = [] + else: + given_text = random.choices(list(given_text.keys()), + weights=list(given_text.values()), k=1) if isinstance(given_text, str): given_text = [given_text] texts.append(PlandoText( @@ -942,12 +1007,13 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): given_text, text.get("percentage", 100) )) + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") elif isinstance(text, PlandoText): if random.random() < float(text.percentage/100): texts.append(text) else: raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") - cls.verify_keys([text.at for text in texts]) return cls(texts) else: raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") @@ -1053,11 +1119,11 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect used_entrances.append(entrance) used_exits.append(exit) if not cls.validate_entrance_name(entrance): - raise ValueError(f"{entrance.title()} is not a valid entrance.") + raise ValueError(f"'{entrance.title()}' is not a valid entrance.") if not cls.validate_exit_name(exit): - raise ValueError(f"{exit.title()} is not a valid exit.") + raise ValueError(f"'{exit.title()}' is not a valid exit.") if not cls.can_connect(entrance, exit): - raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.") + raise ValueError(f"Connection between '{entrance.title()}' and '{exit.title()}' is invalid.") @classmethod def from_any(cls, data: PlandoConFromAnyType) -> Self: @@ -1120,27 +1186,48 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect class Accessibility(Choice): - """Set rules for reachability of your items/locations. - Locations: ensure everything can be reached and acquired. - Items: ensure all logically relevant items can be acquired. - Minimal: ensure what is needed to reach your goal can be acquired.""" + """ + Set rules for reachability of your items/locations. + + **Full:** ensure everything can be reached and acquired. + + **Minimal:** ensure what is needed to reach your goal can be acquired. + """ display_name = "Accessibility" - option_locations = 0 - option_items = 1 + rich_text_doc = True + option_full = 0 option_minimal = 2 alias_none = 2 + alias_locations = 0 + alias_items = 0 + default = 0 + + +class ItemsAccessibility(Accessibility): + """ + Set rules for reachability of your items/locations. + + **Full:** ensure everything can be reached and acquired. + + **Minimal:** ensure what is needed to reach your goal can be acquired. + + **Items:** ensure all logically relevant items can be acquired. Some items, such as keys, may be self-locking, and + some locations may be inaccessible. + """ + option_items = 1 default = 1 class ProgressionBalancing(NamedRange): - """ - A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. + """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. + A lower setting means more getting stuck. A higher setting means less getting stuck. """ default = 50 range_start = 0 range_end = 99 display_name = "Progression Balancing" + rich_text_doc = True special_range_names = { "disabled": 0, "normal": 50, @@ -1170,13 +1257,18 @@ class CommonOptions(metaclass=OptionsMetaProperty): progression_balancing: ProgressionBalancing accessibility: Accessibility - def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]: + def as_dict(self, + *option_names: str, + casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake", + toggles_as_bools: bool = False) -> typing.Dict[str, typing.Any]: """ Returns a dictionary of [str, Option.value] :param option_names: names of the options to return :param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab` + :param toggles_as_bools: whether toggle options should be output as bools instead of strings """ + assert option_names, "options.as_dict() was used without any option names." option_results = {} for option_name in option_names: if option_name in type(self).type_hints: @@ -1196,6 +1288,8 @@ class CommonOptions(metaclass=OptionsMetaProperty): value = getattr(self, option_name).value if isinstance(value, set): value = sorted(value) + elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle): + value = bool(value) option_results[display_name] = value else: raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") @@ -1205,29 +1299,36 @@ class CommonOptions(metaclass=OptionsMetaProperty): class LocalItems(ItemSet): """Forces these items to be in their native world.""" display_name = "Local Items" + rich_text_doc = True class NonLocalItems(ItemSet): """Forces these items to be outside their native world.""" display_name = "Non-local Items" + rich_text_doc = True class StartInventory(ItemDict): """Start with these items.""" verify_item_name = True display_name = "Start Inventory" + rich_text_doc = True class StartInventoryPool(StartInventory): """Start with these items and don't place them in the world. - The game decides what the replacement items will be.""" + + The game decides what the replacement items will be. + """ verify_item_name = True display_name = "Start Inventory from Pool" + rich_text_doc = True class StartHints(ItemSet): - """Start with these item's locations prefilled into the !hint command.""" + """Start with these item's locations prefilled into the ``!hint`` command.""" display_name = "Start Hints" + rich_text_doc = True class LocationSet(OptionSet): @@ -1236,28 +1337,33 @@ class LocationSet(OptionSet): class StartLocationHints(LocationSet): - """Start with these locations and their item prefilled into the !hint command""" + """Start with these locations and their item prefilled into the ``!hint`` command.""" display_name = "Start Location Hints" + rich_text_doc = True class ExcludeLocations(LocationSet): - """Prevent these locations from having an important item""" + """Prevent these locations from having an important item.""" display_name = "Excluded Locations" + rich_text_doc = True class PriorityLocations(LocationSet): - """Prevent these locations from having an unimportant item""" + """Prevent these locations from having an unimportant item.""" display_name = "Priority Locations" + rich_text_doc = True class DeathLink(Toggle): - """When you die, everyone dies. Of course the reverse is true too.""" + """When you die, everyone who enabled death link dies. Of course, the reverse is true too.""" display_name = "Death Link" + rich_text_doc = True class ItemLinks(OptionList): """Share part of your item pool with other players.""" display_name = "Item Links" + rich_text_doc = True default = [] schema = Schema([ { @@ -1281,8 +1387,8 @@ class ItemLinks(OptionList): picks_group = get_fuzzy_results(item_name, world.item_name_groups.keys(), limit=1) picks_group = f" or '{picks_group[0][0]}' ({picks_group[0][1]}% sure)" if allow_item_groups else "" - raise Exception(f"Item {item_name} from item link {item_link} " - f"is not a valid item from {world.game} for {pool_name}. " + raise Exception(f"Item '{item_name}' from item link '{item_link}' " + f"is not a valid item from '{world.game}' for '{pool_name}'. " f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure){picks_group}") if allow_item_groups: pool |= world.item_name_groups.get(item_name, {item_name}) @@ -1324,6 +1430,7 @@ class ItemLinks(OptionList): class Removed(FreeText): """This Option has been Removed.""" + rich_text_doc = True default = "" visibility = Visibility.none @@ -1372,22 +1479,26 @@ it. def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[ str, typing.Dict[str, typing.Type[Option[typing.Any]]]]: """Generates and returns a dictionary for the option groups of a specified world.""" - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} + option_to_name = {option: option_name for option_name, option in world.options_dataclass.type_hints.items()} + + ordered_groups = {group.name: group.options for group in world.web.option_groups} + # add a default option group for uncategorized options to get thrown into - ordered_groups = ["Game Options"] - [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] - grouped_options = {group: {} for group in ordered_groups} - for option_name, option in world.options_dataclass.type_hints.items(): - if visibility_level & option.visibility: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option + if "Game Options" not in ordered_groups: + grouped_options = set(option for group in ordered_groups.values() for option in group) + ungrouped_options = [option for option in option_to_name if option not in grouped_options] + # only add the game options group if we have ungrouped options + if ungrouped_options: + ordered_groups = {**{"Game Options": ungrouped_options}, **ordered_groups} - # if the world doesn't have any ungrouped options, this group will be empty so just remove it - if not grouped_options["Game Options"]: - del grouped_options["Game Options"] - - return grouped_options + return { + group: { + option_to_name[option]: option + for option in group_options + if (visibility_level in option.visibility and option in option_to_name) + } + for group, group_options in ordered_groups.items() + } def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None: @@ -1426,46 +1537,61 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge return data, notes + def yaml_dump_scalar(scalar) -> str: + # yaml dump may add end of document marker and newlines. + return yaml.dump(scalar).replace("...\n", "").strip() + for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: - grouped_options = get_option_groups(world) + option_groups = get_option_groups(world) with open(local_path("data", "options.yaml")) as f: file_data = f.read() res = Template(file_data).render( - option_groups=grouped_options, - __version__=__version__, game=game_name, yaml_dump=yaml.dump, + option_groups=option_groups, + __version__=__version__, game=game_name, yaml_dump=yaml_dump_scalar, dictify_range=dictify_range, ) 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) -if __name__ == "__main__": +def dump_player_options(multiworld: MultiWorld) -> None: + from csv import DictWriter - from worlds.alttp.Options import Logic - import argparse + game_players = defaultdict(list) + for player, game in multiworld.game.items(): + game_players[game].append(player) + game_players = dict(sorted(game_players.items())) - map_shuffle = Toggle - compass_shuffle = Toggle - key_shuffle = Toggle - big_key_shuffle = Toggle - hints = Toggle - test = argparse.Namespace() - test.logic = Logic.from_text("no_logic") - test.map_shuffle = map_shuffle.from_text("ON") - test.hints = hints.from_text('OFF') - try: - test.logic = Logic.from_text("overworld_glitches_typo") - except KeyError as e: - print(e) - try: - test.logic_owg = Logic.from_text("owg") - except KeyError as e: - print(e) - if test.map_shuffle: - print("map_shuffle is on") - print(f"Hints are {bool(test.hints)}") - print(test) + output = [] + per_game_option_names = [ + getattr(option, "display_name", option_key) + for option_key, option in PerGameCommonOptions.type_hints.items() + ] + all_option_names = per_game_option_names.copy() + for game, players in game_players.items(): + game_option_names = per_game_option_names.copy() + for player in players: + world = multiworld.worlds[player] + player_output = { + "Game": multiworld.game[player], + "Name": multiworld.get_player_name(player), + } + output.append(player_output) + for option_key, option in world.options_dataclass.type_hints.items(): + if option.visibility == Visibility.none: + continue + display_name = getattr(option, "display_name", option_key) + player_output[display_name] = getattr(world.options, option_key).current_option_name + if display_name not in game_option_names: + all_option_names.append(display_name) + game_option_names.append(display_name) + + with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file: + fields = ["Game", "Name", *all_option_names] + writer = DictWriter(file, fields) + writer.writeheader() + writer.writerows(output) diff --git a/README.md b/README.md index cebd4f7e..d60f1b96 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,14 @@ Currently, the following games are supported: * Aquaria * Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 * A Hat in Time +* Old School Runescape +* Kingdom Hearts 1 +* Mega Man 2 +* Yacht Dice +* Faxanadu +* Saving Princess +* Castlevania: Circle of the Moon +* Inscryption For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/SNIClient.py b/SNIClient.py index 222ed54f..9140c73c 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -243,6 +243,9 @@ class SNIContext(CommonContext): # Once the games handled by SNIClient gets made to be remote items, # this will no longer be needed. async_start(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}])) + + if self.client_handler is not None: + self.client_handler.on_package(self, cmd, args) def run_gui(self) -> None: from kvui import GameManager @@ -633,7 +636,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 +658,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/UndertaleClient.py b/UndertaleClient.py index cdc21c56..dfacee14 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -29,7 +29,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor): def _cmd_patch(self): """Patch the game. Only use this command if /auto_patch fails.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) self.ctx.patch_game() self.output("Patched.") @@ -43,7 +43,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor): def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): """Patch the game automatically.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) tempInstall = steaminstall if not os.path.isfile(os.path.join(tempInstall, "data.win")): tempInstall = None @@ -62,7 +62,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor): for file_name in os.listdir(tempInstall): if file_name != "steam_api.dll": shutil.copy(os.path.join(tempInstall, file_name), - os.path.join(os.getcwd(), "Undertale", file_name)) + Utils.user_path("Undertale", file_name)) self.ctx.patch_game() self.output("Patching successful!") @@ -111,12 +111,12 @@ class UndertaleContext(CommonContext): self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE") def patch_game(self): - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f: + with open(Utils.user_path("Undertale", "data.win"), "rb") as f: patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff")) - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f: + with open(Utils.user_path("Undertale", "data.win"), "wb") as f: f.write(patchedFile) - os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True) - with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites", + os.makedirs(name=Utils.user_path("Undertale", "Custom Sprites"), exist_ok=True) + with open(os.path.expandvars(Utils.user_path("Undertale", "Custom Sprites", "Which Character.txt")), "w") as f: f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only " "line other than this one.\n", "frisk"]) @@ -247,8 +247,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): with open(os.path.join(ctx.save_game_folder, filename), "w") as f: toDraw = "" for i in range(20): - if i < len(str(ctx.item_names.lookup_in_slot(l.item))): - toDraw += str(ctx.item_names.lookup_in_slot(l.item))[i] + if i < len(str(ctx.item_names.lookup_in_game(l.item))): + toDraw += str(ctx.item_names.lookup_in_game(l.item))[i] else: break f.write(toDraw) diff --git a/Utils.py b/Utils.py index f89330cf..0aa81af1 100644 --- a/Utils.py +++ b/Utils.py @@ -18,8 +18,8 @@ import warnings from argparse import Namespace from settings import Settings, get_settings -from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union -from typing_extensions import TypeGuard +from time import sleep +from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard from yaml import load, load_all, dump try: @@ -31,6 +31,7 @@ if typing.TYPE_CHECKING: import tkinter import pathlib from BaseClasses import Region + import multiprocessing def tuplize_version(version: str) -> Version: @@ -46,7 +47,7 @@ class Version(typing.NamedTuple): return ".".join(str(item) for item in self) -__version__ = "0.5.0" +__version__ = "0.6.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -151,8 +152,15 @@ def home_path(*path: str) -> str: if hasattr(home_path, 'cached_path'): pass elif sys.platform.startswith('linux'): - home_path.cached_path = os.path.expanduser('~/Archipelago') - os.makedirs(home_path.cached_path, 0o700, exist_ok=True) + xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share')) + home_path.cached_path = xdg_data_home + '/Archipelago' + if not os.path.isdir(home_path.cached_path): + legacy_home_path = os.path.expanduser('~/Archipelago') + if os.path.isdir(legacy_home_path): + os.renames(legacy_home_path, home_path.cached_path) + os.symlink(home_path.cached_path, legacy_home_path) + else: + os.makedirs(home_path.cached_path, 0o700, exist_ok=True) else: # not implemented home_path.cached_path = local_path() # this will generate the same exceptions we got previously @@ -420,10 +428,11 @@ class RestrictedUnpickler(pickle.Unpickler): if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # used by MultiServer -> savegame/multidata - if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}: + if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", + "SlotType", "NetworkSlot", "HintStatus"}: return getattr(self.net_utils_module, name) # Options and Plando are unpickled by WebHost -> Generate - if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}: + if module == "worlds.generic" and name == "PlandoItem": if not self.generic_properties_module: self.generic_properties_module = importlib.import_module("worlds.generic") return getattr(self.generic_properties_module, name) @@ -434,7 +443,7 @@ class RestrictedUnpickler(pickle.Unpickler): else: mod = importlib.import_module(module) obj = getattr(mod, name) - if issubclass(obj, self.options_module.Option): + if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)): return obj # Forbid everything else. raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") @@ -483,9 +492,9 @@ def get_text_after(text: str, start: str) -> str: loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG} -def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w", - log_format: str = "[%(name)s at %(asctime)s]: %(message)s", - exception_logger: typing.Optional[str] = None): +def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, + write_mode: str = "w", log_format: str = "[%(name)s at %(asctime)s]: %(message)s", + add_timestamp: bool = False, exception_logger: typing.Optional[str] = None): import datetime loglevel: int = loglevel_mapping.get(loglevel, loglevel) log_folder = user_path("logs") @@ -512,11 +521,15 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri def filter(self, record: logging.LogRecord) -> bool: return self.condition(record) - file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False))) + file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False))) + file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.getMessage())) root_logger.addHandler(file_handler) if sys.stdout: + formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') stream_handler = logging.StreamHandler(sys.stdout) stream_handler.addFilter(Filter("NoFile", lambda record: not getattr(record, "NoStream", False))) + if add_timestamp: + stream_handler.setFormatter(formatter) root_logger.addHandler(stream_handler) # Relay unhandled exceptions to logger. @@ -528,7 +541,8 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri sys.__excepthook__(exc_type, exc_value, exc_traceback) return logging.getLogger(exception_logger).exception("Uncaught exception", - exc_info=(exc_type, exc_value, exc_traceback)) + exc_info=(exc_type, exc_value, exc_traceback), + extra={"NoStream": exception_logger is None}) return orig_hook(exc_type, exc_value, exc_traceback) handle_exception._wrapped = True @@ -551,7 +565,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri import platform logging.info( f"Archipelago ({__version__}) logging initialized" - f" on {platform.platform()}" + f" on {platform.platform()} process {os.getpid()}" f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" f"{' (frozen)' if is_frozen() else ''}" ) @@ -567,6 +581,8 @@ def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"): else: if text: queue.put_nowait(text) + else: + sleep(0.01) # non-blocking stream from threading import Thread thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True) @@ -664,6 +680,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 +722,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 +738,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 +767,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 +789,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() @@ -824,11 +867,10 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non task.add_done_callback(_faf_tasks.discard) -def deprecate(message: str): +def deprecate(message: str, add_stacklevels: int = 0): if __debug__: raise Exception(message) - import warnings - warnings.warn(message) + warnings.warn(message, stacklevel=2 + add_stacklevels) class DeprecateDict(dict): @@ -842,10 +884,9 @@ class DeprecateDict(dict): def __getitem__(self, item: Any) -> Any: if self.should_error: - deprecate(self.log_message) + deprecate(self.log_message, add_stacklevels=1) elif __debug__: - import warnings - warnings.warn(self.log_message) + warnings.warn(self.log_message, stacklevel=2) return super().__getitem__(item) @@ -899,7 +940,7 @@ def freeze_support() -> None: def visualize_regions(root_region: Region, file_name: str, *, show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True, - linetype_ortho: bool = True) -> None: + linetype_ortho: bool = True, regions_to_highlight: set[Region] | None = None) -> None: """Visualize the layout of a world as a PlantUML diagram. :param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.) @@ -915,16 +956,22 @@ def visualize_regions(root_region: Region, file_name: str, *, Items without ID will be shown in italics. :param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown. :param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines. + :param regions_to_highlight: Regions that will be highlighted in green if they are reachable. Example usage in World code: from Utils import visualize_regions - visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") + state = self.multiworld.get_all_state(False) + state.update_reachable_regions(self.player) + visualize_regions(self.get_region("Menu"), "my_world.puml", show_entrance_names=True, + regions_to_highlight=state.reachable_regions[self.player]) Example usage in Main code: from Utils import visualize_regions for player in multiworld.player_ids: visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml") """ + if regions_to_highlight is None: + regions_to_highlight = set() assert root_region.multiworld, "The multiworld attribute of root_region has to be filled" from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region from collections import deque @@ -977,7 +1024,7 @@ def visualize_regions(root_region: Region, file_name: str, *, uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}") def visualize_region(region: Region) -> None: - uml.append(f"class \"{fmt(region)}\"") + uml.append(f"class \"{fmt(region)}\" {'#00FF00' if region in regions_to_highlight else ''}") if show_locations: visualize_locations(region) visualize_exits(region) diff --git a/WargrooveClient.py b/WargrooveClient.py index c5fdeb35..f9971f7a 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -176,7 +176,7 @@ class WargrooveContext(CommonContext): if not os.path.isfile(path): open(path, 'w').close() # Announcing commander unlocks - item_name = self.item_names.lookup_in_slot(network_item.item) + item_name = self.item_names.lookup_in_game(network_item.item) if item_name in faction_table.keys(): for commander in faction_table[item_name]: logger.info(f"{commander.name} has been unlocked!") @@ -197,7 +197,7 @@ class WargrooveContext(CommonContext): open(print_path, 'w').close() with open(print_path, 'w') as f: f.write("Received " + - self.item_names.lookup_in_slot(network_item.item) + + self.item_names.lookup_in_game(network_item.item) + " from " + self.player_names[network_item.player]) f.close() @@ -267,9 +267,7 @@ class WargrooveContext(CommonContext): def build(self): container = super().build() - panel = TabbedPanelItem(text="Wargroove") - panel.content = self.build_tracker() - self.tabs.add_widget(panel) + self.add_client_tab("Wargroove", self.build_tracker()) return container def build_tracker(self) -> TrackerLayout: @@ -342,7 +340,7 @@ class WargrooveContext(CommonContext): faction_items = 0 faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()] for network_item in self.items_received: - if self.item_names.lookup_in_slot(network_item.item) in faction_item_names: + if self.item_names.lookup_in_game(network_item.item) in faction_item_names: faction_items += 1 starting_groove = (faction_items - 1) * self.starting_groove_multiplier # Must be an integer larger than 0 diff --git a/WebHost.py b/WebHost.py index 30308297..6c8dde07 100755 --- a/WebHost.py +++ b/WebHost.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import argparse import os import multiprocessing import logging @@ -13,11 +14,12 @@ 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 -Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 +Utils.local_path.cached_path = os.path.dirname(__file__) settings.no_gui = True configpath = os.path.abspath("config.yaml") if not os.path.exists(configpath): # fall back to config.yaml in home @@ -33,6 +35,15 @@ def get_app() -> "Flask": import yaml app.config.from_file(configpath, yaml.safe_load) logging.info(f"Updated config from {configpath}") + # inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it. + parser = argparse.ArgumentParser(allow_abbrev=False) + parser.add_argument('--config_override', default=None, + help="Path to yaml config file that overrules config.yaml.") + args = parser.parse_known_args()[0] + if args.config_override: + import yaml + app.config.from_file(os.path.abspath(args.config_override), yaml.safe_load) + logging.info(f"Updated config from {args.config_override}") if not app.config["HOST_ADDRESS"]: logging.info("Getting public IP, as HOST_ADDRESS is empty.") app.config["HOST_ADDRESS"] = Utils.get_public_ipv4() @@ -60,9 +71,10 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] worlds[game] = world base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs") + 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 fdf3037f..9c713419 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 @@ -38,6 +39,8 @@ app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8") app.config["JOB_THRESHOLD"] = 1 # after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable. app.config["JOB_TIME"] = 600 +# memory limit for generator processes in bytes +app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296 app.config['SESSION_PERMANENT'] = True # waitress uses one thread for I/O, these are for processing of views that then get sent @@ -84,6 +87,6 @@ def register(): from WebHostLib.customserver import run_server_process # to trigger app routing picking up on it - from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options + from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options, session app.register_blueprint(api.api_endpoints) diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 4003243a..d0b9d05c 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -1,51 +1,15 @@ """API endpoints package.""" from typing import List, Tuple -from uuid import UUID -from flask import Blueprint, abort, url_for +from flask import Blueprint -import worlds.Files -from ..models import Room, Seed +from ..models import Seed, Slot api_endpoints = Blueprint('api', __name__, url_prefix="/api") -# unsorted/misc endpoints - def get_players(seed: Seed) -> List[Tuple[str, str]]: - return [(slot.player_name, slot.game) for slot in seed.slots] + return [(slot.player_name, slot.game) for slot in seed.slots.order_by(Slot.player_id)] -@api_endpoints.route('/room_status/') -def room_info(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - - def supports_apdeltapatch(game: str): - return game in worlds.Files.AutoPatchRegister.patch_types - downloads = [] - for slot in sorted(room.seed.slots): - if slot.data and not supports_apdeltapatch(slot.game): - slot_download = { - "slot": slot.player_id, - "download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id) - } - downloads.append(slot_download) - elif slot.data: - slot_download = { - "slot": slot.player_id, - "download": url_for("download_patch", patch_id=slot.id, room_id=room.id) - } - downloads.append(slot_download) - return { - "tracker": room.tracker, - "players": get_players(room.seed), - "last_port": room.last_port, - "last_activity": room.last_activity, - "timeout": room.timeout, - "downloads": downloads, - } - - -from . import generate, user, datapackage # trigger registration +from . import datapackage, generate, room, user # trigger registration diff --git a/WebHostLib/api/room.py b/WebHostLib/api/room.py new file mode 100644 index 00000000..93379756 --- /dev/null +++ b/WebHostLib/api/room.py @@ -0,0 +1,42 @@ +from typing import Any, Dict +from uuid import UUID + +from flask import abort, url_for + +import worlds.Files +from . import api_endpoints, get_players +from ..models import Room + + +@api_endpoints.route('/room_status/') +def room_info(room_id: UUID) -> Dict[str, Any]: + room = Room.get(id=room_id) + if room is None: + return abort(404) + + def supports_apdeltapatch(game: str) -> bool: + return game in worlds.Files.AutoPatchRegister.patch_types + + downloads = [] + for slot in sorted(room.seed.slots): + if slot.data and not supports_apdeltapatch(slot.game): + slot_download = { + "slot": slot.player_id, + "download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id) + } + downloads.append(slot_download) + elif slot.data: + slot_download = { + "slot": slot.player_id, + "download": url_for("download_patch", patch_id=slot.id, room_id=room.id) + } + downloads.append(slot_download) + + return { + "tracker": room.tracker, + "players": get_players(room.seed), + "last_port": room.last_port, + "last_activity": room.last_activity, + "timeout": room.timeout, + "downloads": downloads, + } diff --git a/WebHostLib/api/user.py b/WebHostLib/api/user.py index 116d3afa..0ddb6fe8 100644 --- a/WebHostLib/api/user.py +++ b/WebHostLib/api/user.py @@ -30,4 +30,4 @@ def get_seeds(): "creation_time": seed.creation_time, "players": get_players(seed.slots), }) - return jsonify(response) \ No newline at end of file + return jsonify(response) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 08a1309e..8ba093e0 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -6,6 +6,7 @@ import multiprocessing import typing from datetime import timedelta, datetime from threading import Event, Thread +from typing import Any from uuid import UUID from pony.orm import db_session, select, commit @@ -53,7 +54,21 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): generation.state = STATE_STARTED -def init_db(pony_config: dict): +def init_generator(config: dict[str, Any]) -> None: + try: + import resource + except ModuleNotFoundError: + pass # unix only module + else: + # set soft limit for memory to from config (default 4GiB) + soft_limit = config["GENERATOR_MEMORY_LIMIT"] + old_limit, hard_limit = resource.getrlimit(resource.RLIMIT_AS) + if soft_limit != old_limit: + resource.setrlimit(resource.RLIMIT_AS, (soft_limit, hard_limit)) + logging.debug(f"Changed AS mem limit {old_limit} -> {soft_limit}") + del resource, soft_limit, hard_limit + + pony_config = config["PONY"] db.bind(**pony_config) db.generate_mapping() @@ -105,8 +120,8 @@ def autogen(config: dict): try: with Locker("autogen"): - with multiprocessing.Pool(config["GENERATORS"], initializer=init_db, - initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool: + with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator, + initargs=(config,), maxtasksperchild=10) as generator_pool: with db_session: to_start = select(generation for generation in Generation if generation.state == STATE_STARTED) diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 97cb797f..4e0cf117 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -105,8 +105,9 @@ def roll_options(options: Dict[str, Union[dict, str]], plando_options=plando_options) else: for i, yaml_data in enumerate(yaml_datas): - rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data, - plando_options=plando_options) + if yaml_data is not None: + rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data, + plando_options=plando_options) except Exception as e: if e.__cause__: results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}" diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 9f70165b..a2eef108 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -72,6 +72,14 @@ class WebHostContext(Context): self.video = {} self.tags = ["AP", "WebHost"] + def __del__(self): + try: + import psutil + from Utils import format_SI_prefix + self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB") + except ImportError: + self.logger.debug("Context destroyed") + def _load_game_data(self): for key, value in self.static_server_data.items(): # NOTE: attributes are mutable and shared, so they will have to be copied before being modified @@ -249,6 +257,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, ctx = WebHostContext(static_server_data, logger) ctx.load(room_id) ctx.init_save() + assert ctx.server is None try: ctx.server = websockets.serve( functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) @@ -279,6 +288,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, ctx.auto_shutdown = Room.get(id=room_id).timeout if ctx.saving: setattr(asyncio.current_task(), "save", lambda: ctx._save(True)) + assert ctx.shutdown_task is None ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) await ctx.shutdown_task @@ -325,10 +335,12 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, def run(self): while 1: next_room = rooms_to_run.get(block=True, timeout=None) + gc.collect() task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) self._tasks.append(task) task.add_done_callback(self._done) logging.info(f"Starting room {next_room} on {name}.") + del task # delete reference to task object starter = Starter() starter.daemon = True diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index a12dc0f4..0bd9f7e5 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -31,11 +31,11 @@ def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[s server_options = { "hint_cost": int(options_source.get("hint_cost", ServerOptions.hint_cost)), - "release_mode": options_source.get("release_mode", ServerOptions.release_mode), - "remaining_mode": options_source.get("remaining_mode", ServerOptions.remaining_mode), - "collect_mode": options_source.get("collect_mode", ServerOptions.collect_mode), + "release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)), + "remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)), + "collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)), "item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))), - "server_password": options_source.get("server_password", None), + "server_password": str(options_source.get("server_password", None)), } generator_options = { "spoiler": int(options_source.get("spoiler", GeneratorOptions.spoiler)), @@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]) elif len(gen_options) > app.config["MAX_ROLL"]: flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " f"If you have a larger group, please generate it yourself and upload it.") + return redirect(url_for(request.endpoint, **(request.view_args or {}))) elif len(gen_options) >= app.config["JOB_THRESHOLD"]: gen = Generation( options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), @@ -134,6 +135,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non {"bosses", "items", "connections", "texts"})) erargs.skip_prog_balancing = False erargs.skip_output = False + erargs.csv_output = False name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 5072f113..6be0e470 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -1,10 +1,11 @@ import datetime import os -from typing import List, Dict, Union +from typing import Any, IO, Dict, Iterator, List, Tuple, Union import jinja2.exceptions from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory from pony.orm import count, commit, db_session +from werkzeug.utils import secure_filename from worlds.AutoWorld import AutoWorldRegister from . import app, cache @@ -17,13 +18,6 @@ def get_world_theme(game_name: str): return 'grass' -@app.before_request -def register_session(): - session.permanent = True # technically 31 days after the last visit - if not session.get("_id", None): - session["_id"] = uuid4() # uniquely identify each session without needing a login - - @app.errorhandler(404) @app.errorhandler(jinja2.exceptions.TemplateNotFound) def page_not_found(err): @@ -69,14 +63,40 @@ def tutorial_landing(): @app.route('/faq//') @cache.cached() -def faq(lang): - return render_template("faq.html", lang=lang) +def faq(lang: str): + import markdown + with open(os.path.join(app.static_folder, "assets", "faq", secure_filename(lang)+".md")) as f: + document = f.read() + return render_template( + "markdown_document.html", + title="Frequently Asked Questions", + html_from_markdown=markdown.markdown( + document, + extensions=["toc", "mdx_breakless_lists"], + extension_configs={ + "toc": {"anchorlink": True} + } + ), + ) @app.route('/glossary//') @cache.cached() -def terms(lang): - return render_template("glossary.html", lang=lang) +def glossary(lang: str): + import markdown + with open(os.path.join(app.static_folder, "assets", "glossary", secure_filename(lang)+".md")) as f: + document = f.read() + return render_template( + "markdown_document.html", + title="Glossary", + html_from_markdown=markdown.markdown( + document, + extensions=["toc", "mdx_breakless_lists"], + extension_configs={ + "toc": {"anchorlink": True} + } + ), + ) @app.route('/seed/') @@ -97,49 +117,91 @@ def new_room(seed: UUID): return redirect(url_for("host_room", room=room.id)) -def _read_log(path: str): - if os.path.exists(path): - with open(path, encoding="utf-8-sig") as log: - yield from log - else: - yield f"Logfile {path} does not exist. " \ - f"Likely a crash during spinup of multiworld instance or it is still spinning up." +def _read_log(log: IO[Any], offset: int = 0) -> Iterator[bytes]: + marker = log.read(3) # skip optional BOM + if marker != b'\xEF\xBB\xBF': + log.seek(0, os.SEEK_SET) + log.seek(offset, os.SEEK_CUR) + yield from log + log.close() # free file handle as soon as possible @app.route('/log/') -def display_log(room: UUID): +def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]: room = Room.get(id=room) if room is None: return abort(404) if room.owner == session["_id"]: file_path = os.path.join("logs", str(room.id) + ".txt") - if os.path.exists(file_path): - return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8") - return "Log File does not exist." + try: + log = open(file_path, "rb") + range_header = request.headers.get("Range") + if range_header: + range_type, range_values = range_header.split('=') + start, end = map(str.strip, range_values.split('-', 1)) + if range_type != "bytes" or end != "": + return "Unsupported range", 500 + # NOTE: we skip Content-Range in the response here, which isn't great but works for our JS + return Response(_read_log(log, int(start)), mimetype="text/plain", status=206) + return Response(_read_log(log), mimetype="text/plain") + except FileNotFoundError: + return Response(f"Logfile {file_path} does not exist. " + f"Likely a crash during spinup of multiworld instance or it is still spinning up.", + mimetype="text/plain") return "Access Denied", 403 -@app.route('/room/', methods=['GET', 'POST']) +@app.post("/room/") +def host_room_command(room: UUID): + room: Room = Room.get(id=room) + if room is None: + return abort(404) + + if room.owner == session["_id"]: + cmd = request.form["cmd"] + if cmd: + Command(room=room, commandtext=cmd) + commit() + return redirect(url_for("host_room", room=room.id)) + + +@app.get("/room/") def host_room(room: UUID): room: Room = Room.get(id=room) if room is None: return abort(404) - if request.method == "POST": - if room.owner == session["_id"]: - cmd = request.form["cmd"] - if cmd: - Command(room=room, commandtext=cmd) - commit() - return redirect(url_for("host_room", room=room.id)) now = datetime.datetime.utcnow() # indicate that the page should reload to get the assigned port - should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3) + should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)) + or room.last_activity < now - datetime.timedelta(seconds=room.timeout)) with db_session: room.last_activity = now # will trigger a spinup, if it's not already running - return render_template("hostRoom.html", room=room, should_refresh=should_refresh) + browser_tokens = "Mozilla", "Chrome", "Safari" + automated = ("update" in request.args + or "Discordbot" in request.user_agent.string + or not any(browser_token in request.user_agent.string for browser_token in browser_tokens)) + + def get_log(max_size: int = 0 if automated else 1024000) -> str: + if max_size == 0: + return "â€Ļ" + try: + with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log: + raw_size = 0 + fragments: List[str] = [] + for block in _read_log(log): + if raw_size + len(block) > max_size: + fragments.append("â€Ļ") + break + raw_size += len(block) + fragments.append(block.decode("utf-8")) + return "".join(fragments) + except FileNotFoundError: + return "" + + return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log) @app.route('/favicon.ico') diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 53c3a615..15b7bd61 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -3,6 +3,7 @@ import json import os from textwrap import dedent from typing import Dict, Union +from docutils.core import publish_parts import yaml from flask import redirect, render_template, request, Response @@ -66,6 +67,22 @@ def filter_dedent(text: str) -> str: return dedent(text).strip("\n ") +@app.template_filter("rst_to_html") +def filter_rst_to_html(text: str) -> str: + """Converts reStructuredText (such as a Python docstring) to HTML.""" + if text.startswith(" ") or text.startswith("\t"): + text = dedent(text) + elif "\n" in text: + lines = text.splitlines() + text = lines[0] + "\n" + dedent("\n".join(lines[1:])) + + return publish_parts(text, writer_name='html', settings=None, settings_overrides={ + 'raw_enable': False, + 'file_insertion_enabled': False, + 'output_encoding': 'unicode' + })['body'] + + @app.template_test("ordered") def test_ordered(obj): return isinstance(obj, collections.abc.Sequence) @@ -214,6 +231,13 @@ def generate_yaml(game: str): del options[key] + # Detect keys which end with -range, indicating a NamedRange with a possible custom value + elif key_parts[-1].endswith("-range"): + if options[key_parts[-1][:-6]] == "custom": + options[key_parts[-1][:-6]] = val + + del options[key] + # Detect random-* keys and set their options accordingly for key, val in options.copy().items(): if key.startswith("random-"): diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 3452c9d4..b7b14dea 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,10 +1,11 @@ flask>=3.0.3 -werkzeug>=3.0.3 -pony>=0.7.17 +werkzeug>=3.0.6 +pony>=0.7.19 waitress>=3.0.0 Flask-Caching>=2.3.0 Flask-Compress>=1.15 -Flask-Limiter>=3.7.0 -bokeh>=3.1.1; python_version <= '3.8' -bokeh>=3.4.1; python_version >= '3.9' +Flask-Limiter>=3.8.0 +bokeh>=3.5.2 markupsafe>=2.1.5 +Markdown>=3.7 +mdx-breakless-lists>=1.0.1 diff --git a/WebHostLib/robots.py b/WebHostLib/robots.py index 410a92c8..93c735c7 100644 --- a/WebHostLib/robots.py +++ b/WebHostLib/robots.py @@ -8,7 +8,8 @@ from . import cache def robots(): # If this host is not official, do not allow search engine crawling if not app.config["ASSET_RIGHTS"]: - return app.send_static_file('robots.txt') + # filename changed in case the path is intercepted and served by an outside service + return app.send_static_file('robots_file.txt') # Send 404 if the host has affirmed this to be the official WebHost abort(404) diff --git a/WebHostLib/session.py b/WebHostLib/session.py new file mode 100644 index 00000000..d5dab7d6 --- /dev/null +++ b/WebHostLib/session.py @@ -0,0 +1,31 @@ +from uuid import uuid4, UUID + +from flask import session, render_template + +from WebHostLib import app + + +@app.before_request +def register_session(): + session.permanent = True # technically 31 days after the last visit + if not session.get("_id", None): + session["_id"] = uuid4() # uniquely identify each session without needing a login + + +@app.route('/session') +def show_session(): + return render_template( + "session.html", + ) + + +@app.route('/session/') +def set_session(_id: str): + new_id: UUID = UUID(_id, version=4) + old_id: UUID = session["_id"] + if old_id != new_id: + session["_id"] = new_id + return render_template( + "session.html", + old_id=old_id, + ) diff --git a/WebHostLib/static/assets/faq.js b/WebHostLib/static/assets/faq.js deleted file mode 100644 index 1bf5e5a6..00000000 --- a/WebHostLib/static/assets/faq.js +++ /dev/null @@ -1,51 +0,0 @@ -window.addEventListener('load', () => { - const tutorialWrapper = document.getElementById('faq-wrapper'); - new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } - if (ajax.status === 404) { - reject("Sorry, the tutorial is not available in that language yet."); - return; - } - if (ajax.status !== 200) { - reject("Something went wrong while loading the tutorial."); - return; - } - resolve(ajax.responseText); - }; - ajax.open('GET', `${window.location.origin}/static/assets/faq/` + - `faq_${tutorialWrapper.getAttribute('data-lang')}.md`, true); - ajax.send(); - }).then((results) => { - // Populate page with HTML generated from markdown - showdown.setOption('tables', true); - showdown.setOption('strikethrough', true); - showdown.setOption('literalMidWordUnderscores', true); - tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); - - // Reset the id of all header divs to something nicer - for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { - const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); - header.setAttribute('id', headerId); - header.addEventListener('click', () => { - window.location.hash = `#${headerId}`; - header.scrollIntoView(); - }); - } - - // Manually scroll the user to the appropriate header if anchor navigation is used - document.fonts.ready.finally(() => { - if (window.location.hash) { - const scrollTarget = document.getElementById(window.location.hash.substring(1)); - scrollTarget?.scrollIntoView(); - } - }); - }).catch((error) => { - console.error(error); - tutorialWrapper.innerHTML = - `

This page is out of logic!

-

Click here to return to safety.

`; - }); -}); diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/en.md similarity index 98% rename from WebHostLib/static/assets/faq/faq_en.md rename to WebHostLib/static/assets/faq/en.md index fb1ccd2d..e64535b4 100644 --- a/WebHostLib/static/assets/faq/faq_en.md +++ b/WebHostLib/static/assets/faq/en.md @@ -77,4 +77,4 @@ There, you will find examples of games in the `worlds` folder: You may also find developer documentation in the `docs` folder: [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). -If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord. +If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord. diff --git a/WebHostLib/static/assets/glossary.js b/WebHostLib/static/assets/glossary.js deleted file mode 100644 index 04a29200..00000000 --- a/WebHostLib/static/assets/glossary.js +++ /dev/null @@ -1,51 +0,0 @@ -window.addEventListener('load', () => { - const tutorialWrapper = document.getElementById('glossary-wrapper'); - new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } - if (ajax.status === 404) { - reject("Sorry, the glossary page is not available in that language yet."); - return; - } - if (ajax.status !== 200) { - reject("Something went wrong while loading the glossary."); - return; - } - resolve(ajax.responseText); - }; - ajax.open('GET', `${window.location.origin}/static/assets/faq/` + - `glossary_${tutorialWrapper.getAttribute('data-lang')}.md`, true); - ajax.send(); - }).then((results) => { - // Populate page with HTML generated from markdown - showdown.setOption('tables', true); - showdown.setOption('strikethrough', true); - showdown.setOption('literalMidWordUnderscores', true); - tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); - - // Reset the id of all header divs to something nicer - for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { - const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); - header.setAttribute('id', headerId); - header.addEventListener('click', () => { - window.location.hash = `#${headerId}`; - header.scrollIntoView(); - }); - } - - // Manually scroll the user to the appropriate header if anchor navigation is used - document.fonts.ready.finally(() => { - if (window.location.hash) { - const scrollTarget = document.getElementById(window.location.hash.substring(1)); - scrollTarget?.scrollIntoView(); - } - }); - }).catch((error) => { - console.error(error); - tutorialWrapper.innerHTML = - `

This page is out of logic!

-

Click here to return to safety.

`; - }); -}); diff --git a/WebHostLib/static/assets/faq/glossary_en.md b/WebHostLib/static/assets/glossary/en.md similarity index 100% rename from WebHostLib/static/assets/faq/glossary_en.md rename to WebHostLib/static/assets/glossary/en.md diff --git a/WebHostLib/static/assets/playerOptions.js b/WebHostLib/static/assets/playerOptions.js index d0f2e388..fbf96a3a 100644 --- a/WebHostLib/static/assets/playerOptions.js +++ b/WebHostLib/static/assets/playerOptions.js @@ -288,6 +288,11 @@ const applyPresets = (presetName) => { } }); namedRangeSelect.value = trueValue; + // It is also possible for a preset to use an unnamed value. If this happens, set the dropdown to "Custom" + if (namedRangeSelect.selectedIndex == -1) + { + namedRangeSelect.value = "custom"; + } } // Handle options whose presets are "random" diff --git a/WebHostLib/static/robots.txt b/WebHostLib/static/robots_file.txt similarity index 100% rename from WebHostLib/static/robots.txt rename to WebHostLib/static/robots_file.txt 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 00000000..537e2797 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 00000000..f34cd5ff 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 326670b7..a0b41b0f 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 00000000..4a5f2d75 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 c8297d34..6e1608d8 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 00000000..30bd2d04 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 2a28958e..3d3e089e 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 00000000..f575ac5d 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 9bc84ff6..08730d98 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 00000000..f9227e8f 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 a1e9c7c8..0bc82fa7 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 00000000..3c0a5774 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 a40bca60..05e675d6 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 00000000..4283cd42 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 b8a8c6a7..e0683a74 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 00000000..3075cec9 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 bb6ccec3..cded7ad1 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 00000000..781b8e4d 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.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png index dba338f5..1015819b 100644 Binary files a/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png and b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png 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 00000000..73e249f6 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.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png index 33f09b19..7b479bfe 100644 Binary files a/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png and b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png 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 00000000..e4ac19be 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.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png index f665015b..59844e31 100644 Binary files a/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png and b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png 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 00000000..36abe6e5 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp differ diff --git a/WebHostLib/static/static/backgrounds/dirt.png b/WebHostLib/static/static/backgrounds/dirt.png index 4ac930ed..db6bc346 100644 Binary files a/WebHostLib/static/static/backgrounds/dirt.png and b/WebHostLib/static/static/backgrounds/dirt.png differ diff --git a/WebHostLib/static/static/backgrounds/dirt.webp b/WebHostLib/static/static/backgrounds/dirt.webp new file mode 100644 index 00000000..5a863550 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 b863a3d4..6752ab4e 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 00000000..fb278c3b 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 90fdfe95..3bacab41 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 00000000..9b8e457c 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 5fc31d1e..f8223e69 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 00000000..c2ded775 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 4a95ce9a..d4476e53 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 00000000..a2100817 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 7b7cd502..79461596 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 00000000..c0ee5205 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0005.webp differ diff --git a/WebHostLib/static/static/backgrounds/grass-flowers.png b/WebHostLib/static/static/backgrounds/grass-flowers.png index 464fdbe5..ea39c541 100644 Binary files a/WebHostLib/static/static/backgrounds/grass-flowers.png and b/WebHostLib/static/static/backgrounds/grass-flowers.png differ diff --git a/WebHostLib/static/static/backgrounds/grass-flowers.webp b/WebHostLib/static/static/backgrounds/grass-flowers.webp new file mode 100644 index 00000000..1b8ebd77 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/grass-flowers.webp differ diff --git a/WebHostLib/static/static/backgrounds/grass.png b/WebHostLib/static/static/backgrounds/grass.png index b88c33de..6a99c4d9 100644 Binary files a/WebHostLib/static/static/backgrounds/grass.png and b/WebHostLib/static/static/backgrounds/grass.png differ diff --git a/WebHostLib/static/static/backgrounds/grass.webp b/WebHostLib/static/static/backgrounds/grass.webp new file mode 100644 index 00000000..212ab377 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 7c9e298e..8a9c0963 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 00000000..6c2b0bd8 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 c2acc588..6d620e50 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 00000000..ca5d1e23 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 a0ff51f9..1e1c18e9 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 00000000..fc1803ca 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 799f32f2..601ad829 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 00000000..0b3c7087 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 e0c9787e..f0d2f2fe 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 00000000..9f26d1a5 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/stone-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/ice.png b/WebHostLib/static/static/backgrounds/ice.png index fcf7299b..c64f1b20 100644 Binary files a/WebHostLib/static/static/backgrounds/ice.png and b/WebHostLib/static/static/backgrounds/ice.png differ diff --git a/WebHostLib/static/static/backgrounds/ice.webp b/WebHostLib/static/static/backgrounds/ice.webp new file mode 100644 index 00000000..a129d5f4 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/ice.webp differ diff --git a/WebHostLib/static/static/backgrounds/jungle.png b/WebHostLib/static/static/backgrounds/jungle.png index e27d7e99..c4ec5b96 100644 Binary files a/WebHostLib/static/static/backgrounds/jungle.png and b/WebHostLib/static/static/backgrounds/jungle.png differ diff --git a/WebHostLib/static/static/backgrounds/jungle.webp b/WebHostLib/static/static/backgrounds/jungle.webp new file mode 100644 index 00000000..d21edc8e 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 5c22c0b9..d6c9d285 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 00000000..a50b7b27 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/ocean.webp differ diff --git a/WebHostLib/static/static/backgrounds/party-time.png b/WebHostLib/static/static/backgrounds/party-time.png index ad00851b..3fcea8a4 100644 Binary files a/WebHostLib/static/static/backgrounds/party-time.png and b/WebHostLib/static/static/backgrounds/party-time.png differ diff --git a/WebHostLib/static/static/backgrounds/party-time.webp b/WebHostLib/static/static/backgrounds/party-time.webp new file mode 100644 index 00000000..7cd54732 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/party-time.webp differ diff --git a/WebHostLib/static/static/backgrounds/stone.png b/WebHostLib/static/static/backgrounds/stone.png index 9e15a343..2956beaa 100644 Binary files a/WebHostLib/static/static/backgrounds/stone.png and b/WebHostLib/static/static/backgrounds/stone.png differ diff --git a/WebHostLib/static/static/backgrounds/stone.webp b/WebHostLib/static/static/backgrounds/stone.webp new file mode 100644 index 00000000..96303c81 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/stone.webp differ diff --git a/WebHostLib/static/static/branding/header-logo-full.svg b/WebHostLib/static/static/branding/header-logo-full.svg new file mode 100644 index 00000000..3e225009 --- /dev/null +++ b/WebHostLib/static/static/branding/header-logo-full.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebHostLib/static/static/branding/header-logo.png b/WebHostLib/static/static/branding/header-logo.png index e5d7f9b4..5a3dbe7d 100644 Binary files a/WebHostLib/static/static/branding/header-logo.png and b/WebHostLib/static/static/branding/header-logo.png differ diff --git a/WebHostLib/static/static/branding/header-logo.svg b/WebHostLib/static/static/branding/header-logo.svg index 3e225009..ceedba43 100644 --- a/WebHostLib/static/static/branding/header-logo.svg +++ b/WebHostLib/static/static/branding/header-logo.svg @@ -1,66 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/WebHostLib/static/static/branding/header-logo.webp b/WebHostLib/static/static/branding/header-logo.webp new file mode 100644 index 00000000..c8088e82 Binary files /dev/null and b/WebHostLib/static/static/branding/header-logo.webp differ diff --git a/WebHostLib/static/static/branding/landing-logo.png b/WebHostLib/static/static/branding/landing-logo.png index 1f2b967a..d4845a47 100644 Binary files a/WebHostLib/static/static/branding/landing-logo.png and b/WebHostLib/static/static/branding/landing-logo.png differ diff --git a/WebHostLib/static/static/branding/landing-logo.webp b/WebHostLib/static/static/branding/landing-logo.webp new file mode 100644 index 00000000..7bd4673e 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.png b/WebHostLib/static/static/button-images/hamburger-menu-icon.png index f1c96316..c8345014 100644 Binary files a/WebHostLib/static/static/button-images/hamburger-menu-icon.png and b/WebHostLib/static/static/button-images/hamburger-menu-icon.png 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 00000000..970754d7 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 f3872dfd..552e4d8f 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 00000000..6da0c172 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 65008eaf..fd4a256c 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 00000000..6b7c3a27 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 9e5f9f50..2f10f458 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 00000000..83ce413d 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.png b/WebHostLib/static/static/button-images/popover.png index cbc86341..e3247194 100644 Binary files a/WebHostLib/static/static/button-images/popover.png and b/WebHostLib/static/static/button-images/popover.png differ diff --git a/WebHostLib/static/static/button-images/popover.webp b/WebHostLib/static/static/button-images/popover.webp new file mode 100644 index 00000000..cd1c0062 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 d931aed0..4f5d7c26 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 00000000..32c9cc8f 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 d6902281..cceb79af 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 00000000..3ec6aae4 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 790c7b01..2beedce1 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 00000000..98e1add9 Binary files /dev/null and b/WebHostLib/static/static/decorations/island-c.webp differ diff --git a/WebHostLib/static/static/decorations/rock-in-water.png b/WebHostLib/static/static/decorations/rock-in-water.png index 25c62acd..1320bef7 100644 Binary files a/WebHostLib/static/static/decorations/rock-in-water.png and b/WebHostLib/static/static/decorations/rock-in-water.png 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 00000000..2c8af460 Binary files /dev/null and b/WebHostLib/static/static/decorations/rock-in-water.webp differ diff --git a/WebHostLib/static/static/decorations/rock-single.png b/WebHostLib/static/static/decorations/rock-single.png index cc237d13..c003abe0 100644 Binary files a/WebHostLib/static/static/decorations/rock-single.png and b/WebHostLib/static/static/decorations/rock-single.png differ diff --git a/WebHostLib/static/static/decorations/rock-single.webp b/WebHostLib/static/static/decorations/rock-single.webp new file mode 100644 index 00000000..e53a2fb5 Binary files /dev/null and b/WebHostLib/static/static/decorations/rock-single.webp differ diff --git a/WebHostLib/static/styles/hostRoom.css b/WebHostLib/static/styles/hostRoom.css index 827f74c0..625b78cc 100644 --- a/WebHostLib/static/styles/hostRoom.css +++ b/WebHostLib/static/styles/hostRoom.css @@ -58,3 +58,28 @@ overflow-y: auto; max-height: 400px; } + +.loader{ + display: inline-block; + visibility: hidden; + margin-left: 5px; + width: 40px; + aspect-ratio: 4; + --_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0); + background: + var(--_g) 0 50%, + var(--_g) 50% 50%, + var(--_g) 100% 50%; + background-size: calc(100%/3) 100%; + animation: l7 1s infinite linear; +} + +.loader.loading{ + visibility: visible; +} + +@keyframes l7{ + 33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%} + 50%{background-size:calc(100%/3) 100%,calc(100%/3) 0 ,calc(100%/3) 100%} + 66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0 } +} diff --git a/WebHostLib/static/styles/markdown.css b/WebHostLib/static/styles/markdown.css index e0165b74..5ead2c60 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/static/styles/tooltip.css b/WebHostLib/static/styles/tooltip.css index 02992b18..dc9026ce 100644 --- a/WebHostLib/static/styles/tooltip.css +++ b/WebHostLib/static/styles/tooltip.css @@ -12,12 +12,12 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, */ /* Base styles for the element that has a tooltip */ -[data-tooltip], .tooltip { +[data-tooltip], .tooltip-container { position: relative; } /* Base styles for the entire tooltip */ -[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after { +[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip { position: absolute; visibility: hidden; opacity: 0; @@ -39,14 +39,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, pointer-events: none; } -[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{ +[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before, +.tooltip-container:hover .tooltip { visibility: visible; opacity: 1; word-break: break-word; } /** Directional arrow styles */ -.tooltip:before, [data-tooltip]:before { +[data-tooltip]:before, .tooltip-container:before { z-index: 10000; border: 6px solid transparent; background: transparent; @@ -54,7 +55,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, } /** Content styles */ -.tooltip:after, [data-tooltip]:after { +[data-tooltip]:after, .tooltip { width: 260px; z-index: 10000; padding: 8px; @@ -63,24 +64,26 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, background-color: hsla(0, 0%, 20%, 0.9); color: #fff; content: attr(data-tooltip); - white-space: pre-wrap; font-size: 14px; line-height: 1.2; } -[data-tooltip]:before, [data-tooltip]:after{ +[data-tooltip]:after { + white-space: pre-wrap; +} + +[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip { visibility: hidden; opacity: 0; pointer-events: none; } -[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after, -.tooltip-top:before, .tooltip-top:after { +[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip { bottom: 100%; left: 50%; } -[data-tooltip]:before, .tooltip:before, .tooltip-top:before { +[data-tooltip]:before, .tooltip-container:before { margin-left: -6px; margin-bottom: -12px; border-top-color: #000; @@ -88,19 +91,19 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, } /** Horizontally align tooltips on the top and bottom */ -[data-tooltip]:after, .tooltip:after, .tooltip-top:after { +[data-tooltip]:after, .tooltip { margin-left: -80px; } -[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after, -.tooltip-top:hover:before, .tooltip-top:hover:after { +[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before, +.tooltip-container:hover .tooltip { -webkit-transform: translateY(-12px); -moz-transform: translateY(-12px); transform: translateY(-12px); } /** Tooltips on the left */ -.tooltip-left:before, .tooltip-left:after { +.tooltip-left:before, [data-tooltip].tooltip-left:after, .tooltip-left .tooltip { right: 100%; bottom: 50%; left: auto; @@ -115,14 +118,14 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, border-left-color: hsla(0, 0%, 20%, 0.9); } -.tooltip-left:hover:before, .tooltip-left:hover:after { +.tooltip-left:hover:before, [data-tooltip].tooltip-left:hover:after, .tooltip-left:hover .tooltip { -webkit-transform: translateX(-12px); -moz-transform: translateX(-12px); transform: translateX(-12px); } /** Tooltips on the bottom */ -.tooltip-bottom:before, .tooltip-bottom:after { +.tooltip-bottom:before, [data-tooltip].tooltip-bottom:after, .tooltip-bottom .tooltip { top: 100%; bottom: auto; left: 50%; @@ -136,14 +139,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, border-bottom-color: hsla(0, 0%, 20%, 0.9); } -.tooltip-bottom:hover:before, .tooltip-bottom:hover:after { +.tooltip-bottom:hover:before, [data-tooltip].tooltip-bottom:hover:after, +.tooltip-bottom:hover .tooltip { -webkit-transform: translateY(12px); -moz-transform: translateY(12px); transform: translateY(12px); } /** Tooltips on the right */ -.tooltip-right:before, .tooltip-right:after { +.tooltip-right:before, [data-tooltip].tooltip-right:after, .tooltip-right .tooltip { bottom: 50%; left: 100%; } @@ -156,7 +160,8 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, border-right-color: hsla(0, 0%, 20%, 0.9); } -.tooltip-right:hover:before, .tooltip-right:hover:after { +.tooltip-right:hover:before, [data-tooltip].tooltip-right:hover:after, +.tooltip-right:hover .tooltip { -webkit-transform: translateX(12px); -moz-transform: translateX(12px); transform: translateX(12px); @@ -168,7 +173,16 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, } /** Center content vertically for tooltips ont he left and right */ -.tooltip-left:after, .tooltip-right:after { +[data-tooltip].tooltip-left:after, [data-tooltip].tooltip-right:after, +.tooltip-left .tooltip, .tooltip-right .tooltip { margin-left: 0; margin-bottom: -16px; } + +.tooltip ul, .tooltip ol { + padding-left: 1rem; +} + +.tooltip :last-child { + margin-bottom: 0; +} diff --git a/WebHostLib/templates/faq.html b/WebHostLib/templates/faq.html deleted file mode 100644 index 76bdb96d..00000000 --- a/WebHostLib/templates/faq.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {% include 'header/grassHeader.html' %} - Frequently Asked Questions - - - -{% endblock %} - -{% block body %} -
- -
-{% endblock %} diff --git a/WebHostLib/templates/gameInfo.html b/WebHostLib/templates/gameInfo.html index c5ebba82..3b908004 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 5a533204..b92097ce 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -98,15 +98,23 @@ {% 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)] }} + + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} {% 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)] }} + + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }} diff --git a/WebHostLib/templates/glossary.html b/WebHostLib/templates/glossary.html deleted file mode 100644 index 921f6781..00000000 --- a/WebHostLib/templates/glossary.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {% include 'header/grassHeader.html' %} - Glossary - - - -{% endblock %} - -{% block body %} -
- -
-{% endblock %} diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 2bbfe4ad..c5996d18 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -19,61 +19,186 @@ {% block body %} {% include 'header/grassHeader.html' %}
- {% if room.owner == session["_id"] %} - Room created from Seed #{{ room.seed.id|suuid }} -
- {% endif %} - {% if room.tracker %} - This room has a Multiworld Tracker - and a Sphere Tracker enabled. -
- {% endif %} - The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. - Should you wish to continue later, - anyone can simply refresh this page and the server will resume.
- {% if room.last_port == -1 %} - There was an error hosting this Room. Another attempt will be made on refreshing this page. - The most likely failure reason is that the multiworld is too old to be loaded now. - {% elif room.last_port %} - You can connect to this room by using - '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' - - in the client.
- {% endif %} + + {% if room.owner == session["_id"] %} + Room created from Seed #{{ room.seed.id|suuid }} +
+ {% endif %} + {% if room.tracker %} + This room has a Multiworld Tracker + and a Sphere Tracker enabled. +
+ {% endif %} + The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. + Should you wish to continue later, + anyone can simply refresh this page and the server will resume.
+ {% if room.last_port == -1 %} + There was an error hosting this Room. Another attempt will be made on refreshing this page. + The most likely failure reason is that the multiworld is too old to be loaded now. + {% elif room.last_port %} + You can connect to this room by using + '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' + + in the client.
+ {% endif %} +
{{ macros.list_patches_room(room) }} {% if room.owner == session["_id"] %} -
- {% endif %} +
{% endblock %} diff --git a/WebHostLib/templates/islandFooter.html b/WebHostLib/templates/islandFooter.html index 08cf2279..7de14f0d 100644 --- a/WebHostLib/templates/islandFooter.html +++ b/WebHostLib/templates/islandFooter.html @@ -1,6 +1,6 @@ {% block footer %}
- +
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index 55a56e32..d18d0f0b 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -19,7 +19,7 @@ {% for id, name in option.name_lookup.items() %} {% if name != 'random' %} {% if option.default != 'random' %} - {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }} + {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id else None) }} {% else %} {{ RangeRow(option_name, option, option.get_option_name(id), name) }} {% endif %} @@ -41,19 +41,19 @@ The following values have special meanings, and may fall outside the normal range.
    {% for name, value in option.special_range_names.items() %} -
  • {{ value }}: {{ name }}
  • +
  • {{ value }}: {{ name|replace("_", " ")|title }}
  • {% endfor %}
{% endif %}
- +
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }} - {% if option.range_start < option.default < option.range_end %} + {% if option.default is number and option.range_start < option.default < option.range_end %} {{ RangeRow(option_name, option, option.default, option.default, True) }} {% endif %} {{ RangeRow(option_name, option, option.range_end, option.range_end, True) }} @@ -72,7 +72,7 @@ This option allows custom values only. Please enter your desired values below.
- +
@@ -89,7 +89,7 @@ Custom values are also allowed for this option. To create one, enter it into the input box below.
- +
@@ -97,7 +97,7 @@ {% for id, name in option.name_lookup.items() %} {% if name != 'random' %} {% if option.default != 'random' %} - {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }} + {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id else None) }} {% else %} {{ RangeRow(option_name, option, option.get_option_name(id), name) }} {% endif %} @@ -138,7 +138,7 @@ id="{{ option_name }}-{{ key }}" name="{{ option_name }}||{{ key }}" value="1" - checked="{{ "checked" if key in option.default else "" }}" + {{ "checked" if key in option.default }} />
\n" + for (name, description) in sorted( + location_names_to_descriptions.items(), + key = lambda pair: pair[0] + ): + table += f"\n" + table += "
Location nameDetailed description
{html.escape(name)}{html.escape(description)}
\n" + + with open( + os.path.join(os.path.dirname(__file__), 'docs/locations_en.md'), + 'r+', + encoding='utf-8' + ) as f: + original = f.read() + start_flag = "\n" + start = original.index(start_flag) + len(start_flag) + end = original.index("") + + f.seek(0) + f.write(original[:start] + table + original[end:]) + f.truncate() + + print("Updated docs/locations_en.md!") diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md index f31358bb..06227226 100644 --- a/worlds/dark_souls_3/docs/en_Dark Souls III.md +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -1,28 +1,201 @@ # Dark Souls III +Game Page | [Items] | [Locations] + +[Items]: /tutorial/Dark%20Souls%20III/items/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en + +## What do I need to do to randomize DS3? + +See full instructions on [the setup page]. + +[the setup page]: /tutorial/Dark%20Souls%20III/setup/en + ## Where is the options page? -The [player options page for this game](../player-options) contains all the options you need to configure and export a -config file. +The [player options page for this game][options] contains all the options you +need to configure and export a config file. + +[options]: ../player-options ## What does randomization do to this game? -Items that can be picked up from static corpses, taken from chests, or earned from defeating enemies or NPCs can be -randomized. Common pickups like titanite shards or firebombs can be randomized as "progressive" items. That is, the -location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter where it was from. This is also what -happens when you randomize Estus Shards and Undead Bone Shards. +1. All item locations are randomized, including those in the overworld, in + shops, and dropped by enemies. Most locations can contain games from other + worlds, and any items from your world can appear in other players' worlds. -It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have -one). Additionally, there are options that can make the randomized experience more convenient or more interesting, such as -removing weapon requirements or auto-equipping whatever equipment you most recently received. +2. By default, all enemies and bosses are randomized. This can be disabled by + setting "Randomize Enemies" to false. -The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder. +3. By default, the starting equipment for each class is randomized. This can be + disabled by setting "Randomize Starting Loadout" to false. -## What Dark Souls III items can appear in other players' worlds? +4. By setting the "Randomize Weapon Level" or "Randomize Infusion" options, you + can randomize whether the weapons you find will be upgraded or infused. -Practically anything can be found in other worlds including pieces of armor, upgraded weapons, key items, consumables, -spells, upgrade materials, etc... +There are also options that can make playing the game more convenient or +bring a new experience, like removing equip loads or auto-equipping weapons as +you pick them up. Check out [the options page][options] for more! -## What does another world's item look like in Dark Souls III? +## What's the goal? -In Dark Souls III, items which are sent to other worlds appear as Prism Stones. +Your goal is to find the four "Cinders of a Lord" items randomized into the +multiworld and defeat the boss in the Kiln of the First Flame. + +## Do I have to check every item in every area? + +Dark Souls III has about 1500 item locations, which is a lot of checks for a +single run! But you don't necessarily need to check all of them. Locations that +you can potentially miss, such as rewards for failable quests or soul +transposition items, will _never_ have items required for any game to progress. +The following types of locations are also guaranteed not to contain progression +items by default: + +* **Hidden:** Locations that are particularly difficult to find, such as behind + illusory walls, down hidden drops, and so on. Does not include large locations + like Untended Graves or Archdragon Peak. + +* **Small Crystal Lizards:** Drops from small crystal lizards. + +* **Upgrade:** Locations that contain upgrade items in vanilla, including + titanite, gems, and Shriving Stones. + +* **Small Souls:** Locations that contain soul items in vanilla, not including + boss souls. + +* **Miscellaneous:** Locations that contain generic stackable items in vanilla, + such as arrows, firebombs, buffs, and so on. + +You can customize which locations are guaranteed not to contain progression +items by setting the `exclude_locations` field in your YAML to the [location +groups] you want to omit. For example, this is the default setting but without +"Hidden" so that hidden locations can contain progression items: + +[location groups]: /tutorial/Dark%20Souls%20III/locations/en#location-groups + +```yaml +Dark Souls III: + exclude_locations: + - Small Crystal Lizards + - Upgrade + - Small Souls + - Miscellaneous +``` + +This allows _all_ non-missable locations to have progression items, if you're in +for the long haul: + +```yaml +Dark Souls III: + exclude_locations: [] +``` + +## What if I don't want to do the whole game? + +If you want a shorter DS3 randomizer experience, you can exclude entire regions +from containing progression items. The items and enemies from those regions will +still be included in the randomization pool, but none of them will be mandatory. +For example, the following configuration just requires you to play the game +through Irithyll of the Boreal Valley: + +```yaml +Dark Souls III: + # Enable the DLC so it's included in the randomization pool + enable_dlc: true + + exclude_locations: + # Exclude late-game and DLC regions + - Anor Londo + - Lothric Castle + - Consumed King's Garden + - Untended Graves + - Grand Archives + - Archdragon Peak + - Painted World of Ariandel + - Dreg Heap + - Ringed City + + # Default exclusions + - Hidden + - Small Crystal Lizards + - Upgrade + - Small Souls + - Miscellaneous +``` + +## Where can I learn more about Dark Souls III locations? + +Location names have to pack a lot of information into very little space. To +better understand them, check out the [location guide], which explains all the +names used in locations and provides more detailed descriptions for each +individual location. + +[location guide]: /tutorial/Dark%20Souls%20III/locations/en + +## Where can I learn more about Dark Souls III items? + +Check out the [item guide], which explains the named groups available for items. + +[item guide]: /tutorial/Dark%20Souls%20III/items/en + +## What's new from 2.x.x? + +Version 3.0.0 of the Dark Souls III Archipelago client has a number of +substantial differences with the older 2.x.x versions. Improvements include: + +* Support for randomizing all item locations, not just unique items. + +* Support for randomizing items in shops, starting loadouts, Path of the Dragon, + and more. + +* Built-in integration with the enemy randomizer, including consistent seeding + for races. + +* Support for the latest patch for Dark Souls III, 1.15.2. Older patches are + *not* supported. + +* Optional smooth distribution for upgrade items, upgraded weapons, and soul + items so you're more likely to see weaker items earlier and more powerful + items later. + +* More detailed location names that indicate where a location is, not just what + it replaces. + +* Other players' item names are visible in DS3. + +* If you pick up items while static, they'll still send once you reconnect. + +However, 2.x.x YAMLs are not compatible with 3.0.0. You'll need to [generate a +new YAML configuration] for use with 3.x.x. + +[generating a new YAML configuration]: /games/Dark%20Souls%20III/player-options + +The following options have been removed: + +* `enable_boss_locations` is now controlled by the `soul_locations` option. + +* `enable_progressive_locations` was removed because all locations are now + individually randomized rather than replaced with a progressive list. + +* `pool_type` has been removed. Since there are no longer any non-randomized + items in randomized categories, there's not a meaningful distinction between + "shuffle" and "various" mode. + +* `enable_*_locations` options have all been removed. Instead, you can now add + [location group names] to the `exclude_locations` option to prevent them from + containing important items. + + [location group names]: /tutorial/Dark%20Souls%20III/locations/en#location-groups + + By default, the Hidden, Small Crystal Lizards, Upgrade, Small Souls, and + Miscellaneous groups are in `exclude_locations`. Once you've chosen your + excluded locations, you can set `excluded_locations: unrandomized` to preserve + the default vanilla item placements for all excluded locations. + +* `guaranteed_items`: In almost all cases, all items from the base game are now + included somewhere in the multiworld. + +In addition, the following options have changed: + +* The location names used in options like `exclude_locations` have changed. See + the [location guide] for a full description. diff --git a/worlds/dark_souls_3/docs/items_en.md b/worlds/dark_souls_3/docs/items_en.md new file mode 100644 index 00000000..b9de5e50 --- /dev/null +++ b/worlds/dark_souls_3/docs/items_en.md @@ -0,0 +1,24 @@ +# Dark Souls III Items + +[Game Page] | Items | [Locations] + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en + +## Item Groups + +The Dark Souls III randomizer supports a number of item group names, which can +be used in YAML options like `local_items` to refer to many items at once: + +* **Progression:** Items which unlock locations. +* **Cinders:** All four Cinders of a Lord. Once you have these four, you can + fight Soul of Cinder and win the game. +* **Miscellaneous:** Generic stackable items, such as arrows, firebombs, buffs, + and so on. +* **Unique:** Items that are unique per NG cycle, such as scrolls, keys, ashes, + and so on. Doesn't include equipment, spells, or souls. +* **Boss Souls:** Souls that can be traded with Ludleth, including Soul of + Rosaria. +* **Small Souls:** Soul items, not including boss souls. +* **Upgrade:** Upgrade items, including titanite, gems, and Shriving Stones. +* **Healing:** Undead Bone Shards and Estus Shards. diff --git a/worlds/dark_souls_3/docs/locations_en.md b/worlds/dark_souls_3/docs/locations_en.md new file mode 100644 index 00000000..8411b8c4 --- /dev/null +++ b/worlds/dark_souls_3/docs/locations_en.md @@ -0,0 +1,2276 @@ +# Dark Souls III Locations + +[Game Page] | [Items] | Locations + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Items]: /tutorial/Dark%20Souls%20III/items/en + +## Table of Contents + +* [Location Groups](#location-groups) +* [Understanding Location Names](#understanding-location-names) + * [HWL: High Wall of Lothric](#high-wall-of-lothric) + * [US: Undead Settlement](#undead-settlement) + * [RS: Road of Sacrifices](#road-of-sacrifices) + * [CD: Cathedral of the Deep](#cathedral-of-the-deep) + * [FK: Farron Keep](#farron-keep) + * [CC: Catacombs of Carthus](#catacombs-of-carthus) + * [SL: Smouldering Lake](#smouldering-lake) + * [IBV: Irithyll of the Boreal Valley](#irithyll-of-the-boreal-valley) + * [ID: Irithyll Dungeon](#irithyll-dungeon) + * [PC: Profaned Capital](#profaned-capital) + * [AL: Anor Londo](#anor-londo) + * [LC: Lothric Castle](#lothric-castle) + * [CKG: Consumed King's Garden](#consumed-kings-garden) + * [GA: Grand Archives](#grand-archives) + * [UG: Untended Graves](#untended-graves) + * [AP: Archdragon Peak](#archdragon-peak) + * [PW1: Painted World of Ariandel (Before Contraption)](#painted-world-of-ariandel-before-contraption) + * [PW2: Painted World of Ariandel (After Contraption)](#painted-world-of-ariandel-after-contraption) + * [DH: Dreg Heap](#dreg-heap) + * [RC: Ringed City](#ringed-city) +* [Detailed Location Descriptions](#detailed-location-descriptions) + +## Location Groups + +The Dark Souls III randomizer supports a number of location group names, which +can be used in YAML options like `exclude_locations` to refer to many locations +at once: + +* **Prominent:** A small number of locations that are in very obvious locations. + Mostly boss drops. Ideal for setting as priority locations. + +* **Progression:** Locations that contain items in vanilla which unlock other + locations. + +* **Boss Rewards:** Boss drops. Does not include soul transfusions or shop + items. + +* **Miniboss Rewards:** Miniboss drops. Minibosses are large enemies that don't + respawn after being killed and usually drop some sort of treasure, such as + Boreal Outrider Knights and Ravenous Crystal Lizards. Only includes enemies + considered minibosses by the enemy randomizer. + +* **Mimic Rewards:** Drops from enemies that are mimics in vanilla. + +* **Hostile NPC Rewards:** Drops from NPCs that are hostile to you. This + includes scripted invaders and initially-friendly NPCs that must be fought as + part of their quest. + +* **Friendly NPC Rewards:** Items given by friendly NPCs as part of their quests + or from non-violent interaction. + +* **Small Crystal Lizards:** Drops from small crystal lizards. + +* **Upgrade:** Locations that contain upgrade items in vanilla, including + titanite, gems, and Shriving Stones. + +* **Small Souls:** Locations that contain soul items in vanilla, not including + boss souls. + +* **Boss Souls:** Locations that contain boss souls in vanilla, as well as Soul + of Rosaria. + +* **Unique:** Locations that contain items in vanilla that are unique per NG + cycle, such as scrolls, keys, ashes, and so on. Doesn't cover equipment, + spells, or souls. + +* **Healing:** Locations that contain Undead Bone Shards and Estus Shards in + vanilla. + +* **Miscellaneous:** Locations that contain generic stackable items in vanilla, + such as arrows, firebombs, buffs, and so on. + +* **Hidden:** Locations that are particularly difficult to find, such as behind + illusory walls, down hidden drops, and so on. Does not include large locations + like Untended Graves or Archdragon Peak. + +* **Weapons:** Locations that contain weapons in vanilla. + +* **Shields:** Locations that contain shields in vanilla. + +* **Armor:** Locations that contain armor in vanilla. + +* **Rings:** Locations that contain rings in vanilla. + +* **Spells:** Locations that contain spells in vanilla. + +## Understanding Location Names + +All locations begin with an abbreviation indicating their general region. Most +locations have a set of landmarks that are used in location names to keep them +short. + +* **FS:** Firelink Shrine +* **FSBT:** Firelink Shrine belltower +* **HWL:** [High Wall of Lothric](#high-wall-of-lothric) +* **US:** [Undead Settlement](#undead-settlement) +* **RS:** [Road of Sacrifices](#road-of-sacrifices) +* **CD:** [Cathedral of the Deep](#cathedral-of-the-deep) +* **FK:** [Farron Keep](#farron-keep) +* **CC:** [Catacombs of Carthus](#catacombs-of-carthus) +* **SL:** [Smouldering Lake](#smouldering-lake) +* **IBV:** [Irithyll of the Boreal Valley](#irithyll-of-the-boreal-valley) +* **ID:** [Irithyll Dungeon](#irithyll-dungeon) +* **PC:** [Profaned Capital](#profaned-capital) +* **AL:** [Anor Londo](#anor-londo) +* **LC:** [Lothric Castle](#lothric-castle) +* **CKG:** [Consumed King's Garden](#consumed-kings-garden) +* **GA:** [Grand Archives](#grand-archives) +* **UG:** [Untended Graves](#untended-graves) +* **AP:** [Archdragon Peak](#archdragon-peak) +* **PW1:** [Painted World of Ariandel (Before Contraption)](#painted-world-of-ariandel-before-contraption) +* **PW2:** [Painted World of Ariandel (After Contraption)](#painted-world-of-ariandel-after-contraption) +* **DH:** [Dreg Heap](#dreg-heap) +* **RC:** [Ringed City](#ringed-city) + +General notes: + +* "Lizard" always refers to a small crystal lizard. + +* "Miniboss" are large enemies that don't respawn after being killed and usually + drop some sort of treasure, such as Boreal Outrider Knights and Ravenous + Crystal Lizards. + +* NPC quest items are always in the first location you can get them _without_ + killing the NPC or ending the quest early. + +### High Wall of Lothric + +* **Back tower:** The tower _behind_ the High Wall of Lothric bonfire, past the + path to the shortcut elevator. + +* **Corpse tower:** The first tower after the High Wall of Lothric bonfire, with + a dead Wyvern on top of it. + +* **Fire tower:** The second tower after the High Wall of Lothric bonfire, where + a living Wyvern lands and breathes fire at you. + +* **Flame plaza:** The open area with many items where the Wyvern breathes fire. + +* **Wall tower:** The third tower after the High Wall of Lothric bonfire, with + the Tower on the Wall bonfire. + +* **Fort:** The large building after the Tower on the Wall bonfire, with the + transforming hollow on top. + + * "Entry": The first room you enter after descending the ladder from the roof. + + * "Walkway": The top floor of the tall room, with a path around the edge + hidden by a large wheel. + + * "Mezzanine": The middle floor of the tall room, with a chest. + + * "Ground": The bottom floor of the tall room, with an anvil and many mobs. + +* **Fountain:** The large fountain with many dead knights around it, where the + Winged Knight patrols in vanilla. + +* **Shortcut:** The unlockable path between the promenade and the High Wall of + Lothric bonfire, including both the elevator and the area at its base. + +* **Promenade:** The long, wide path between the two boss arenas. + +### Undead Settlement + +* **Foot:** The area where you first appear, around the Foot of the High Wall + bonfire. + +* **Burning tree:** The tree near the beginning of the region, with the + Cathedral Evangelist in front of it in vanilla. + +* **Hanging corpse room:** The dark room to the left of the burning tree with + many hanging corpses inside, on the way to the Dilapidated Bridge bonfire. + +* **Back alley:** The path between buildings leading to the Dilapidated Bridge + bonfire. + +* **Stable:** The building complex across the bridge to the right of the burning + tree. + +* **White tree:** The birch tree by the Dilapidated Bridge bonfire, where the + giant shoots arrows. + +* **Sewer:** The underground passage between the chasm and the Dilapidated + Bridge bonfire. + +* **Chasm:** The chasm underneath the bridge on the way to the tower. It's + possible to get into the chasm without a key by dropping down next to Eygon of + Carim with a full health bar. + +* **Tower:** The tower at the end of the region with the giant archer at the + top. + +* **Tower village:** The village reachable from the tower, where the Fire Demon + patrols in vanilla. + +### Road of Sacrifices + +The area after the Crystal Sage is considered part of the Cathedral of the Deep +region. + +* **Road:** The path from the Road of Sacrifices bonfire to the Halfway Fortress + bonfire. + +* **Woods:** The wooded area on land, after the Halfway Fortress bonfire and + surrounding the Crucifixion Woods bonfire. + +* **Water:** The watery area, covered in crabs in vanilla. + +* **Deep water:** The area in the water near the ladder to Farron Keep, where + your walking is slowed. + +* **Stronghold:** The stone building complex on the way to Crystal Sage. + + * "Left room" is the room whose entrance is near the Crucifixion Woods + bonfire. + + * "Right room" is the room up the stairs closer to Farron Keep. + +* **Keep perimeter:** The building with the Black Knight and the locked door to + the Farron Keep Perimeter bonfire. + +### Cathedral of the Deep + +* **Path:** The path from Road of Sacrifices to the cathedral proper. + +* **Moat:** The circular path around the base of the front of the + cathedral, with the Ravenous Crystal Lizard and Corpse-Grubs in vanilla. + +* **Graveyard:** The area with respawning enemies up the hill from the Cleansing + Chapel bonfire. + +* **White tree:** The birch tree below the front doors of the chapel and across + the moat from the graveyard, where the giant shoots arrows if he's still + alive. + +* **Lower roofs:** The roofs, flying buttresses, and associated areas to the + right of the front door, which must be traversed before entering the + cathedral. + +* **Upper roofs:** The roofs, flying buttresses, and rafters leading to the + Rosaria's Bedchamber bonfire. + +* **Main hall:** The central and largest room in the cathedral, with the muck + that slows your movement. Divided into the south (with the sleeping giant in + vanilla) and east (with many items) wings, with north pointing towards the + door to the boss. + +* **Side chapel:** The room with rows of pews and the patrolling Cathedral + Knight in vanilla, to the side of the main hall. + +### Farron Keep + +* **Left island:** The large island with the ritual flame, to the left as you + leave the Farron Keep bonfire. + +* **Right island:** The large island with the ritual flame, to the right as you + leave the Farron Keep bonfire. + +* **Hidden cave:** A small cave in the far corner of the map, closest to the + right island. Near a bunch of basilisks in vanilla. + +* **Keep ruins:** The following two islands: + + * "Bonfire island": The island with the Keep Ruins bonfire. + * "Ritual island": The island with one of the three ritual fires. + +* **White tree**: The birch tree by the ramp down from the keep ruins bonfire + island, where the giant shoots arrows if he's still alive. + +* **Keep proper:** The building with the Old Wolf of Farron bonfire. + +* **Upper keep:** The area on top of the keep proper, reachable from the + elevator from the Old Wolf of Farron bonfire. + +* **Perimeter:** The area from near the Farron Keep Perimeter bonfire, including + the stone building and the path to the boss. + +### Catacombs of Carthus + +All the area up to the Small Doll wall into Irithyll is considered part of the +Catacombs of Carthus region. + +* **Atrium:** The large open area you first enter and the rooms attached to it. + + * "Upper" is the floor you begin on. + * "Lower" is the floor down the short stairs but at the top of the long + stairway that the skeleton ball rolls down. + +* **Crypt:** The enclosed area at the bottom of the long stairway that the + skeleton ball rolls down. + + * "Upper" is the floor the long stairway leads to that also contains the + Catacombs of Carthus bonfire. + * "Lower" is the floor with rats and bonewheels in vanilla. + * "Across" is the area reached by going up the set of stairs across from + the entrance downstairs. + +* **Cavern:** The even larger open area past the crypt with the rope bridge to + the boss arena. + +* **Tomb:** The area on the way to Smouldering Lake, reachable by cutting down + the rope bridge and climbing down it. + +* **Irithyll Bridge:** The outdoor bridge leading to Irithyll of the Boreal + Valley. + +### Smouldering Lake + +* **Lake:** The watery area you enter initially, where you get shot at by the + ballista. + +* **Side lake:** The small lake accessible via a passage from the larger one, in + which you face Horace the Hushed as part of his quest. + +* **Ruins main:** The area you first enter after the Demon Ruins bonfire. + + * "Upper" is the floor you begin on. + * "Lower" is the floor down the stairs. + +* **Antechamber:** The area up the flight of stairs near the +Old King's Antechamber bonfire. + +* **Ruins basement:** The area further down from ruins main lower, with many + basilisks and Knight Slayer Tsorig in vanilla. + +### Irithyll of the Boreal Valley + +This region starts _after_ the Small Doll wall and ends with Pontiff Sulyvahn. +Everything after that, including the contents of Sulyvahn's cathedral is +considered part of Anor Londo. + +* **Central:** The beginning of the region, from the Central Irithyll bonfire up + to the plaza. + +* **Dorhys:** The sobbing mob (a Cathedral Evangelist in vanilla) behind the + locked door opening onto central. Accessed through an illusory railing by the + crystal lizard just before the plaza. + +* **Plaza:** The area in front of and below the cathedral, with a locked door up + to the cathedral and a locked elevator to the Ascent. + +* **Descent:** The path from the Church of Yorshka bonfire down to the lake. + +* **Lake:** The open watery area outside the room with the Distant Manor + bonfire. + +* **Sewer:** The room between the lake and the beginning of the ascent, filled + with Sewer Centipedes in vanilla. + +* **Ascent:** The path up from the lake to the cathedral, through several + buildings and some open stairs. + +* **Great hall:** The building along the ascent with a large picture of + Gwynevere and several Silver Knights in vanilla. + +### Irithyll Dungeon + +In Irithyll Dungeon locations, "left" and "right" are always oriented as though +"near" is where you stand and "far" is where you're facing. (For example, you +enter the dungeon from the bonfire on the near left.) + +* **B1:** The floor on which the player enters the dungeon, with the Irithyll + Dungeon bonfire. + + * "Near" is the side of the dungeon with the bonfire. + * "Far" is the opposite side. + +* **B2:** The floor directly below B1, which can be reached by going down the + stairs or dropping. + + * "Near" is the same side of the dungeon as the bonfire. + * "Far" is the opposite side. + +* **Pit:** The large room with the Giant Slave and many Rats in vanilla. + +* **Pit lift:** The elevator from the pit up to B1 near, right to the Irithyll + Dungeon bonfire. + +* **B3:** The lowest floor, with Karla's cell, a lift back to B2, and the exit + onwards to the Profaned Capital. + + * "Near" is the side with Karla's cell and the path from the pit. + * "Far" is the opposite side with the mimic. + +* **B3 lift:** The elevator from B3 (near where you can use Path of the Dragon + to go to Archdragon Peak) up to B2. + +### Profaned Capital + +* **Tower:** The tower that contains the Profaned Capital bonfire. + +* **Swamp:** The pool of toxic liquid accessible by falling down out of the + lower floor of the tower, going into the corridor to the left, and falling + down a hole. + +* **Chapel:** The building in the swamp containing Monstrosities of Sin in + vanilla. + +* **Bridge:** The long bridge from the tower into the palace. + +* **Palace:** The large building carved into the wall of the cavern, full of + chalices and broken pillars. + +### Anor Londo + +This region includes everything after Sulyvahn's cathedral, including its upper +story. + +* **Light cathedral:** The cathedral in which you fight Pontiff Sulyvahn in + vanilla. + +* **Plaza:** The wide open area filled with Giant Slaves in vanilla. + +* **Walkway:** The path above the plaza leading to the second floor of the light + cathedral, with Deacons in vanilla. + +* **Buttresses:** The flying buttresses that you have to climb to get to the + spiral staircase. "Near" and "far" are relative to the light cathedral, so the + nearest buttress is the one that leads back to the walkway. + +* **Tomb:** The area past the illusory wall just before the spiral staircase, in + which you marry Anri during Yoel and Yuria's quest. + +* **Dark cathedral:** The darkened cathedral just before the Aldrich fight in + vanilla. + +### Lothric Castle + +This region covers everything up the ladder from the Dancer of the Boreal Valley +bonfire up to the door into Grand Archives, except the area to the left of the +ladder which is part of Consumed King's Garden. + +* **Lift:** The elevator from the room straight after the Dancer of the Boreal + Valley bonfire up to just before the boss fight. + +* **Ascent:** The set of stairways and turrets leading from the Lothric Castle + bonfire to the Dragon Barracks bonfire. + +* **Barracks:** The large building with two fire-breathing wyverns across from + the Dragon Barracks bonfire. + +* **Moat:** The ditch beneath the bridge leading to the barracks. + + * The "right path" leads to the right as you face the barracks, around and + above the stairs up to the Dragon Barracks bonfire. + +* **Plaza:** The open area in the center of the barracks, where the two wyverns + breathe fire. + + * "Left" is the enclosed area on the left as you're coming from the Dragon + Barracks bonfire, with the stairs down to the basement. + +* **Basement:** The room beneath plaza left, with the Boreal Outrider in + vanilla. + +* **Dark room:** The large darkened room on the right of the barracks as you're + coming from the Dragon Barracks bonfire, with firebomb-throwing Hollows in + vanilla. + + * "Lower" is the bottom floor that you enter onto from the plaza. + * "Upper" is the top floor with the door to the main hall. + * "Mid" is the middle floor accessible by climbing a ladder from lower or + going down stairs from upper. + +* **Main hall:** The central room of the barracks, behind the gate. + +* **Chapel:** The building to the right just before the stairs to the boss, with + a locked elevator to Grand Archives. + +* **Wyvern room:** The room where you can fight the Pus of Man infecting the + left wyvern, accessible by dropping down to the left of the stairs to the + boss. + +* **Altar:** The building containing the Altar of Sunlight, accessible by + climbing up a ladder onto a roof around the corner from the stairs to the + boss. + +### Consumed King's Garden + +This region covers everything to the left of the ladder up from the Dancer of +the Boreal Valley bonfire up to the illusory wall into Untended Graves. + +* **Balcony:** The walkway accessible by getting off the first elevator halfway + down. + +* **Rotunda:** The building in the center of the toxic pool, with a Cathedral + Knight on it in vanilla. + +* **Lone stairway:** A set of stairs leading nowhere in the far left of the main + area as you enter from the first elevator. + +* **Shortcut:** The path from the locked door into Lothric Castle, through the + room filled with thralls in vanilla, and down a lift. + +* **Tomb:** The area after the boss room. + +### Grand Archives + +* **1F:** The first floor of the Grand Archives, including the first wax pool. + +* **Dark room:** The unlit room on 1F to the right of the wax pool. + +* **2F:** The second floor of the grand archives. It's split into two sections + that are separated by retractable bookshelves. + + * "Early" is the first part you reach and has an outdoor balcony with a ladder + to 3F and a wax pool up a short set of stairs. + * "Late" is the part you can only reach by climbing down from F3, where you + encounter the teleporting miniboss for the final time. + +* **3F:** The third floor of the grand archives, where you encounter the + teleporting miniboss for the second time. Includes the area with a hidden room + with another miniboss. + +* **4F:** The topmost and most well-lit section of bookshelves, overlooking the + rest of the archives. + +* **Rooftops:** The outer rooftop area between 4F and 5F, with Gargoyles in + vanilla. + + * "Lower" is the balcony you can reach by dropping off the rooftops, as well + as the further rooftops leading down to the 2F early balcony. + +* **5F:** The topmost floor of the archives interior, accessible from the + rooftops, with a ladder down to 4F. + +* **Dome:** The domed roof of the Grand Archives, with Ascended Winged Knights + in vanilla. + +* **Rafters:** The narrow walkways above the Grand Archives, accessible by + dropping down from the dome. + +### Untended Graves + +* **Swamp:** The watery area immediately after the Untended graves bonfire, up + to the cemetery. + +* **Cemetery:** The area past where the Cemetery of Ash bonfire would be, up to + the boss arena. + +* **Environs:** The area after the boss and outside the abandoned Firelink + Shrine. + +* **Shrine:** The area inside the abandoned Firelink Shrine. + +### Archdragon Peak + +"Gesture" always means the Path of the Dragon gesture. + +* **Intro:** The first section, from where you warp in from Irithyll Dungeon up + to the first boss fight. + + * "Archway": The large stone archway in front of the boss door. + +* **Fort:** The arena where you fight Ancient Wyvern in vanilla. + + * "Overlook": The area down the stairs from where the Ancient Wyvern first + lands in vanilla, overlooking the fog. + + * "Rotunda": The top of the spiral staircase building, to the left before the + bridge with the chain-axe Man-Serpent in vanilla. + +* **Mausoleum:** The building with the Dragon-Kin Mausoleum bonfire, where + you're warped after the first boss fight. + +* **Walkway:** The path from the mausoleum to the belfry, looking out over + clouds. + + * "Building": The building along the walkway, just before the wyvern in + vanilla. + +* **Belfry:** The building with the Great Belfry bonfire, including the room + with the summoner. + +* **Plaza:** The arena that appears after you defeat Nameless King in vanilla. + +* **Summit:** The path up from the belfry to the final altar at the top of the + mountain. + +### Painted World of Ariandel (Before Contraption) + +This region covers the Ashes of Ariandel DLC up to the point where you must use +the Contraption Key to ascend to the second level of the building and first meet +the painter. + +* **Snowfield:** The area around the Snowfield bonfire, + + * "Upper": The area immediately after the Snowfield bonfire, before the + collapsing overhang, with the Followers in vanilla. + + * "Lower": The snowy tree-filled area after the collapsing overhang, with the + Wolves in vanilla. + + * "Village": The area with broken-down buildings and Millwood Knights in + vanilla. + + * "Tower": The tower by the village, with Millwood Knights in Vanilla. + +* **Bridge:** The rope bridge to the chapel. + + * "Near": The side of the bridge by the Rope Bridge Cave bonfire. + + * "Far": The side of the bridge by the Ariandel Chapel bonfire. + +* **Chapel:** The building with the Ariandel Chapel bonfire and Lady Friede. + +* **Depths:** The area reachable by cutting down the bridge and descending on + the far side, with the Depths of the Painting bonfire. + +* **Settlement:** The area reachable by cutting down the bridge and descending + on the near side, with the Corvian Settlement bonfire. Everything after the + slide down the hill is considered part of the settlement. + + * "Courtyard": The area in front of the settlement, immediately after the + slide. + + * "Main": The main road of the settlement leading up to the locked gate to the + library. Also includes the buildings that are immediately accessible from + this road. + + * "Loop": A side path that loops left from the main road and goes up and + behind the building with the bonfire. + + * "Back": The back alley of the settlement, accessible by dropping down to the + right of the locked gate to the library. Also includes the buildings that + are immediately accessible from this alley. + + * "Roofs": The village rooftops, first accessible by climbing a ladder from + the back alley. Also includes the buildings and items that are first + accessible from the roofs. + + * "Hall": The largest building in the settlement, with two Corvian Knights in + vanilla. + +* **Library:** The building where you use the contraption key, where Vilhelm + appears in vanilla. + +### Painted World of Ariandel (After Contraption) + +This region covers the Ashes of Ariandel DLC past the point where you must use +the Contraption Key to ascend to the second level of the building and first meet +the painter, including the basement beneath the chapel. + +* **Pass:** The mountainous area past the Snowy Mountain Pass bonfire. + +* **Pit:** The area with a large tree and numerous Millwood Knights in vanilla, + reached by a collapsing overhang in the pass. + +* **B1:** The floor immediately below the chapel, first accessible from the + pass. Filled with Giant Flies in vanilla. + +* **B2:** The floor below B1, with lots of fly eggs. Filled with even more Giant + Flies than B1 in vanilla. + +* **B3:** The floor below B2, accessible through an illusory wall. + +* **Rotunda:** The round arena out in the open, accessible by platforming down + tree roots from B3. + +### Dreg Heap + +* **Shop:** Items sold by the Stone-Humped Hag by The Dreg Heap bonfire. + +* **Castle:** The building with The Dreg Heap bonfire, up to the large fall into + the library. + +* **Library:** The building with the stained-glass window that you fall into + from the castle. + +* **Church:** The building below and to the right of the library, which the + pillar falls into to make a bridge. + +* **Pantry:** The set of rooms entered through a door near the fountain just + past the church, with boxes and barrels. + + * "Upstairs": The room with an open side, accessible through an illusory wall + in the furthest pantry room. + +* **Parapets:** The area with balconies and Overgrown Lothric Knights in + vanilla, accessible by taking the pillar bridge from the church, following + that path to the end, and dropping down to the right. + +* **Ruins:** The area around the Earthen Peak Ruins bonfire, up to the swamp. + +* **Swamp:** The area in and above the poisonous water, up to the point the + branches deposit you back on the ruins. + + * "Left": Left as you enter from the ruins, towards the cliff edge. + + * "Right": Right as you enter from the ruins, towards higher ground. + + * "Upper": The path up and over the swamp towards the Within Earthen Peak + Ruins bonfire. + +### Ringed City + +The "mid boss", "end boss", and "hidden boss" are the bosses who take the place +of Halflight, Gael, and Midir, respectively. + +* **Wall:** The large wall in which you spawn when you first enter the area, + with the Mausoleum Lookout bonfire. + + * "Top": The open-air top of the wall, where you first spawn in. + + * "Upper": The upper area of the wall, with the Ringed Inner Wall bonfire. + + * "Tower": The tiered tower leading down from the upper area to the stairs. + + * "Lower": The lower rooms of the wall, accessible from the lower cliff, with + an elevator back to upper. + + * "Hidden": The hidden floor accessible from the elevator from lower to upper, + from which you can reach Midir in vanilla. + +* **Streets:** The streets and skyways of the city proper. "Left" and "right" + are relative to the main staircase as you head down towards the swamp, "near" + and "far" are relative to Shira's chamber at the top of the stairs. + + * "Garden": The flower-filled back alley accessible from the left side of the + nearest bridge over the stairs. + + * "High": The higher areas in the far left where you can find the Locust + Preacher, accessible from a long ladder in the swamp. + + * "Monument": The area around the purging monument, which can only be accessed + by solving the "Show Your Humanity" puzzle. + +* **Swamp:** The wet area past the city streets. "Left" and "right" are relative + to heading out from the Ringed City Streets bonfire, and "near" and "far" are + relative to that bonfire as well. + +* **Lower cliff:** The cliffside path leading from the swamp into the shared + grave, where Midir breathes fire. + +* **Grave:** The cylindrical chamber with spiral stairs around the edges, + connecting the two cliffs, containing the Shared Grave bonfire. + +* **Upper cliff:** The cliffside path leading out of the grave to the lower + wall. + +* **Church path:** The sunlit path from the lower cliff up to the Church of + Filianore where you fight Halflight in vanilla. + +* **Ashes:** The final area, where you fight Gael in vanilla. + +## Detailed Location Descriptions + +These location descriptions were originally written by [Matt Gruen] for [the +static _Dark Souls III_ randomizer]. + +[Matt Gruen]: https://thefifthmatt.com/ +[the static _Dark Souls III_ randomizer]: https://www.nexusmods.com/darksouls3/mods/361 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Location nameDetailed description
AL: Aldrich Faithful - water reserves, talk to McDonnelGiven by Archdeacon McDonnel in Water Reserves.
AL: Aldrich's Ruby - dark cathedral, minibossDropped by the Deep Accursed who drops down when you open the Anor Londo Cathedral shortcut
AL: Anri's Straight Sword - Anri questDropped by Anri of Astora upon death or completing quest. In the Darkmoon Tomb with Lord of Hollows route, or given by Ludleth if summoned to defeat Aldrich.
AL: Blade of the Darkmoon - Yorshka with Darkmoon LoyaltyGiven by Yorshka after learning the Darkmoon Loyalty gesture from Sirris, or by killing her
AL: Brass Armor - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Gauntlets - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Helm - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Leggings - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Chameleon - tomb after marrying AnriDropped by the Stone-humped Hag assassin after Anri reaches the Church of Yorshka, either in the church or after marrying Anri
AL: Cinders of a Lord - AldrichDropped by Aldrich
AL: Crescent Moon Sword - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Dark Stoneplate Ring - by dark stairs up from plazaAfter the Pontiff fight, in the dark hallways to the left of the area with the Giant Slaves
AL: Deep Gem - water reservesIn the open in the Water Reserves
AL: Dragonslayer Greatarrow - drop from nearest buttressDropping down from about halfway down the flying buttress closest to the entrance to the Darkmoon Tomb
AL: Dragonslayer Greatbow - drop from nearest buttressDropping down from about halfway down the flying buttress closest to the entrance to the Darkmoon Tomb
AL: Drang Twinspears - plaza, NPC dropDropped by Drang Twinspears-wielding knight on the stairs leading up to the Anor Londo Silver Knights
AL: Easterner's Ashes - below top of furthest buttressDropping down from the rightmost flying buttress, or the rightmost set of stairs
AL: Ember - plaza, furtherAfter the Pontiff fight, in the middle of the area with the Giant Slaves
AL: Ember - plaza, right sideAfter the Pontiff fight, next to one of the Giant Slaves on the right side
AL: Ember - spiral staircase, bottomNext to the lever that summons the rotating Anor Londo stairs at the bottom
AL: Estus Shard - dark cathedral, by left stairsIn a chest on the floor of the Anor Londo cathedral
AL: Giant's Coal - by giant near dark cathedralOn the Giant Blacksmith's corpse in Anor Londo
AL: Golden Ritual Spear - light cathedral, mimic upstairsDrop from a mimic in the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Havel's Ring+2 - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Human Dregs - water reservesIn the open in the Water Reserves
AL: Large Soul of a Weary Warrior - left of dark cathedral entranceIn front of the Anor Londo cathedral, slightly to the left
AL: Large Titanite Shard - balcony by dead giantsAfter the Pontiff fight, on the balcony to the right of the area with the Giant Slaves
AL: Large Titanite Shard - bottom of the furthest buttressAt the base of the rightmost flying buttress leading up to Anor Londo
AL: Large Titanite Shard - bottom of the nearest buttressOn the tower leading back from Anor Londo to the shortcut to Irithyll, down the flying buttress closest to the Darkmoon Tomb entrance.
AL: Large Titanite Shard - right after light cathedralAfter Pontiff's cathedral, hugging the wall to the right
AL: Large Titanite Shard - walkway, side path by cathedralAfter the Pontiff fight, going back from the Deacons area to the original cathedral, before a dropdown
AL: Moonlight Arrow - dark cathedral, up right stairsIn the Anor Londo cathedral, up the stairs on the right side
AL: Painting Guardian Gloves - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Gown - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Hood - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Waistcloth - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian's Curved Sword - prison tower raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Proof of a Concord Kept - dark cathedral, up left stairsIn the Anor Londo cathedral, halfway down the stairs on the left side next to some Deacons
AL: Reversal Ring - tomb, chest in cornerIn a chest in Darkmoon Tomb
AL: Ring of Favor - water reserves, both minibossesDropped after killing both of Sulyvahn's Beasts in the Water Reserves
AL: Ring of Favor+1 - light cathedral, upstairsIn the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Silver Mask - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Simple Gem - light cathedral, lizard upstairsDropped by a Crystal Lizard in the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Soul of AldrichDropped by Aldrich
AL: Soul of Rosaria - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Soul of a Crestfallen Knight - right of dark cathedral entranceTo the right of the Anor Londo cathedral entrance, past the red-eyed Silver Knight
AL: Soul of a Weary Warrior - plaza, nearerAfter the Pontiff fight, in the middle of the area with the Giant Slaves
AL: Sun Princess Ring - dark cathedral, after bossIn the Anor Londo cathedral after defeating Aldrich, up the elevators in Gwynevere's Chamber
AL: Titanite Scale - top of ladder up to buttressesOn the platform after the stairs leading up to Anor Londo from the Water Reserves building
AL: Twinkling Titanite - lizard after light cathedral #1Dropped a Crystal Lizard straight after the Pontiff fight
AL: Twinkling Titanite - lizard after light cathedral #2Dropped a Crystal Lizard straight after the Pontiff fight
AL: Yorshka's Chime - kill YorshkaDropped by Yorshka upon death.
AP: Ancient Dragon Greatshield - intro, on archwayAfter the Archdragon Peak bonfire, on top of the arch in front of the Ancient Wyvern fight
AP: Calamity Ring - mausoleum, gesture at altarReceived using Path of the Dragon at the Altar by the Mausoleum bonfire
AP: Covetous Gold Serpent Ring+2 - plazaIn the Nameless King boss arena after he is defeated
AP: Dragon Chaser's Ashes - summit, side pathIn the run-up to the Dragon Altar after the Belfry bonfire, in a side path to the left side
AP: Dragon Head Stone - fort, boss dropDropped by Ancient Wyvern
AP: Dragon Tooth - belfry roof, NPC dropDropped from any of the Havel Knights
AP: Dragonslayer Armor - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Gauntlets - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Helm - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Leggings - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Spear - gate after mausoleumIn the gate connecting the Dragon-Kin Mausoleum area to the bridge where the Nameless King fight takes place
AP: Drakeblood Greatsword - mausoleum, NPC dropDropped by the Drakeblood Knight summoned by the Serpent-Man Summoner
AP: Dung Pie - fort, landing after second roomOn a landing going up the stairs from the Ancient Wyvern to the chainaxe Man-Serpent area
AP: Ember - belfry, below bellIn the area below the bell lever, either dropping down near the lever or going down the stairs from the open fountain area after the Belfry bonfire
AP: Ember - fort overlook #1From the right of where Ancient Wyvern first lands
AP: Ember - fort overlook #2From the right of where Ancient Wyvern first lands
AP: Ember - intro, by bonfireNext to the Archdragon Peak bonfire
AP: Great Magic Barrier - drop off belfry roofDropping down to the left from the area with the Havel Knight and the dead Wyvern
AP: Havel's Greatshield - belfry roof, NPC dropDropped from any of the Havel Knights
AP: Havel's Ring+1 - summit, after buildingJust past the building with all of the Man-Serpents on the way to the Dragon Altar, on the left side
AP: Homeward Bone - intro, path to bonfireFrom the start of the area, along the left path leading to the first bonfire
AP: Large Soul of a Crestfallen Knight - summit, by fountainIn the middle of the open fountain area after the Belfry bonfire
AP: Large Soul of a Nameless Soldier - fort, by stairs to first roomto the left of where the Ancient Wyvern lands
AP: Large Soul of a Weary Warrior - fort, centerWhere the Ancient Wyvern lands
AP: Lightning Bolt - rotundaOn top of the ruined dome found going up spiral stairs to the left before the bridge with the chainaxe Man-Serpent
AP: Lightning Clutch Ring - intro, left of boss doorTo the left of gate leading to Ancient Wyvern, past the Rock Lizard
AP: Lightning Gem - intro, side riseFrom the start of the area, up a ledge in between two forked paths toward the first bonfire
AP: Lightning Urn - fort, left of first room entranceOn the path to the left of where the Ancient Wyvern lands, left of the building entrance
AP: Ricard's Rapier - belfry, NPC dropDropped by the Richard Champion summoned by the Serpent-Man Summoner
AP: Ring of Steel Protection - fort overlook, beside stairsTo the right of the area where the Ancient Wyvern lands, dropping down onto the ledge
AP: Soul of a Crestfallen Knight - mausoleum, upstairsFrom the Mausoleum bonfire, up the second set of stairs to the right
AP: Soul of a Nameless Soldier - intro, right before archwayFrom the Archdragon Peak bonfire, going right before the arch before Ancient Wyvern
AP: Soul of a Weary Warrior - intro, first cliff edgeAt the very start of the area on the left cliff edge
AP: Soul of a Weary Warrior - walkway, building windowOn the way to the Belfry bonfire after the sagging wooden bridge, on a ledge visible in a room with a Crystal Lizard, accessible by a tricky jump or just going around the other side
AP: Soul of the Nameless KingDropped by Nameless King
AP: Stalk Dung Pie - fort overlookFrom the right of where Ancient Wyvern first lands
AP: Thunder Stoneplate Ring - walkway, up ladderAfter the long hallway after the Mausoleum bonfire, before the rope bridge, up the long ladder
AP: Titanite Chunk - fort, second room balconyAfter going left of where Ancient Wyvern lands and left again, rather than going up the stairs to the right, go to the open area to the left
AP: Titanite Chunk - intro, archway cornerFrom the Archdragon Peak bonfire, under the arch, immediately to the right
AP: Titanite Chunk - intro, behind rockAlmost at the Archdragon Peak bonfire, behind a rock in the area with many Man-Serpents
AP: Titanite Chunk - intro, left before archwayAfter the Archdragon Peak bonfire, going left before the arch before Ancient Wyvern
AP: Titanite Chunk - rotundaOn top of the ruined dome found going up spiral stairs to the left before the bridge with the chainaxe Man-Serpent
AP: Titanite Chunk - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
AP: Titanite Scale - mausoleum, downstairs balcony #1From the Mausoleum bonfire, up the stairs to the left, past the Rock Lizard
AP: Titanite Scale - mausoleum, downstairs balcony #2From the Mausoleum bonfire, up the stairs to the left, past the Rock Lizard
AP: Titanite Scale - mausoleum, upstairs balconyFrom the Mausoleum bonfire, up the first stairs to the right, going around toward the Man-Serpent Summoner, on the balcony on the side
AP: Titanite Scale - walkway buildingIn a chest after the sagging wooden bridge on the way to the Belfry, in the building with the Crystal Lizard
AP: Titanite Scale - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
AP: Titanite Slab - belfry roofNext to the Havel Knight by the dead Wyvern
AP: Titanite Slab - plazaIn the Nameless King boss arena after he is defeated
AP: Twinkling Dragon Torso Stone - summit, gesture at altarReceived using Path of the Dragon at the Altar after the Belfry bonfire. Hawkwood also uses the gesture there when summoned.
AP: Twinkling Titanite - belfry, by ladder to roofIn the chest before the ladder climbing up to the Havel Knight
AP: Twinkling Titanite - fort, down second room balcony ladderAfter going left of where Ancient Wyvern lands and left again, rather than going up the stairs to the right, go to the open area to the left and then down the ladder
AP: Twinkling Titanite - fort, end of raftersDropping down to the left of the Mausoleum bonfire, all the way down the wooden rafters
AP: Twinkling Titanite - walkway building, lizardDropped by Crystal Lizard in the building after the sagging wooden bridge toward the Belfry
AP: Twinkling Titanite - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
CA: Coiled Sword - boss dropDropped by Iudex Gundyr
CA: Firebomb - down the cliff edgeAlong the cliff edge before the Iudex Gundyr fight, to the right
CA: Soul of a Deserted Corpse - right of spawnAt the very start of the game
CA: Soul of an Unknown Traveler - by minibossIn the area with the Ravenous Crystal Lizard
CA: Speckled Stoneplate Ring+1 - by minibossIn the area with the Ravenous Crystal Lizard, along the right wall
CA: Titanite Scale - miniboss dropDropped by Ravenous Crystal Lizard
CA: Titanite Shard - jump to coffinMaking a jump to a coffin after the Cemetery of Ash bonfire
CC: Black Blade - tomb, mimicDropped by the mimic before Smouldering Lake
CC: Black Bug Pellet - cavern, before bridgeIn the area where many many skeletons are before the bridge you can cut
CC: Bloodred Moss Clump - atrium lower, down more stairsTo the left before going down the main stairwell in the Catacombs, past the skeleton ambush and where Anri is standing, near the Crystal Lizard
CC: Carthus Bloodring - crypt lower, end of side hallAt the very end of the Bonewheel Skeleton area
CC: Carthus Milkring - crypt upper, among potsAfter the first Skeleton Ball, in the hallway alcove with the many dark-exploding pots
CC: Carthus Pyromancy Tome - atrium lower, jump from bridgeDown the hallway to the right before going down the main stairwell in the Catacombs and through an illusory wall on the left, or making a difficult dropdown from the top-level platform
CC: Carthus Rouge - atrium upper, left after entranceTo the right after first entering the Catacombs
CC: Carthus Rouge - crypt across, cornerMaking a difficult jump between the hallway after the first Skeleton Ball and the area at the same level on the opposite side, or going up the stairs from the main hall
CC: Dark Gem - crypt lower, skeleton ball dropDropped by second Skeleton Ball after killing its sorcerer skeleton
CC: Ember - atrium, on long stairwayOn the main stairwell in Catacombs
CC: Ember - crypt lower, shortcut to cavernIn the short hallway with the level shortcut where Knight Slayer Tsorig invades
CC: Ember - crypt upper, end of hall past holeGoing right from the Catacombs bonfire, down the hall to the left, then to the right. After a hole that drops down into the Bonewheel Skeleton area.
CC: Fire Gem - cavern, lizardDropped by a Crystal Lizard found between the Catacombs main halls and the ledge overlooking the bridge you can cut down
CC: Grave Warden Pyromancy Tome - boss arenaIn Wolnir's arena, or in the back left of the room containing his bonfire if not picked up in the arena
CC: Grave Warden's Ashes - crypt across, cornerFrom the Catacombs bonfire, down the stairs into the main hall and up the stairs to the other side, on the far left side. Stairwell past the illusory wall is most direct.
CC: Homeward Bone - Irithyll bridgeFound right before the wall blocking access to Irithyll
CC: Large Soul of a Nameless Soldier - cavern, before bridgeIn the area where many many skeletons are before the bridge you can cut
CC: Large Soul of a Nameless Soldier - tomb lowerDown the ramp from the Fire Demon, where all the skeletons are
CC: Large Soul of an Unknown Traveler - crypt upper, hall middleGoing right from the Catacombs bonfire, then down the long hallway after the hallway to the left
CC: Large Titanite Shard - crypt across, middle hallFrom the Catacombs bonfire, down the stairs into the main hall and up the stairs to the other side, in a middle hallway
CC: Large Titanite Shard - crypt upper, skeleton ball hallGoing right from the Catacombs bonfire, to the end of the hallway where second Skeleton Ball rolls
CC: Large Titanite Shard - tomb lowerDown the ramp from the Fire Demon, where all the skeletons are
CC: Old Sage's Blindfold - tomb, hall before bonfireDown the ramp from the Fire Demon, straight down the hallway past the room with the Abandoned Tomb bonfire
CC: Pontiff's Right Eye - Irithyll bridge, miniboss dropDropped by killing Sulyvahn's Beast on the bridge to Irithyll or in the lake below
CC: Ring of Steel Protection+2 - atrium upper, drop onto pillarFrom the first bridge in Catacombs where the first skeletons are encountered, parallel to the long stairwell, walk off onto a pillar on the left side.
CC: Sharp Gem - atrium lower, right before exitDown the hallway to the right before going down the main stairwell in the Catacombs
CC: Soul of High Lord WolnirDropped by High Lord Wolnir
CC: Soul of a Demon - tomb, miniboss dropDropped by the Fire Demon before Smouldering Lake
CC: Soul of a Nameless Soldier - atrium lower, down hallAll the way down the hallway to the right before going down the main stairwell in the Catacombs
CC: Soul of a Nameless Soldier - atrium upper, up more stairsFrom the room before the Catacombs main stairwell, up the two ramps and to the end of the long hallway crossing the room
CC: Thunder Stoneplate Ring+1 - crypt upper, among potsAfter the first Skeleton Ball, in the hallway alcove with the many dark-exploding pots, behind one of the pillars
CC: Titanite Shard - atrium lower, corner by stairsTo the left before going down the main stairwell in the Catacombs, behind the pensive Carthus Cursed Sword Skeleton
CC: Titanite Shard - crypt lower, left of entranceIn the main hall after the Catacombs bonfire, down the stairs and to the left
CC: Titanite Shard - crypt lower, start of side hallIn the Bonewheel Skeleton area, on the left side under a Writhing Flesh
CC: Twinkling Titanite - atrium lower, lizard down more stairsDropped by a Crystal Lizard found to the left before going down the main stairwell in the Catacombs, past the skeleton ambush and past where Anri is standing
CC: Undead Bone Shard - crypt upper, skeleton ball dropDropped by first Skeleton Ball after killing its sorcerer skeleton
CC: Witch's Ring - tomb, hall before bonfireDown the ramp from the Fire Demon, straight down the hallway past the room with the Abandoned Tomb bonfire
CC: Yellow Bug Pellet - cavern, on overlookTo the right of the Carthus Curved Sword Skeleton overlooking the pit Horace falls into
CD: Aldrich's Sapphire - side chapel, miniboss dropDropped by the Deep Accursed
CD: Arbalest - upper roofs, end of furthest buttressBefore the rafters on the way to Rosaria, up a flying buttress, past a halberd-wielding Large Hollow Soldier to the right, and down another flying buttress to the right
CD: Archdeacon Holy Garb - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Archdeacon Skirt - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Archdeacon White Crown - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Armor of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Astora Greatsword - graveyard, left of entranceDown one of the side paths to the left in the Reanimated Corpse area
CD: Barbed Straight Sword - Kirk dropDropped by Longfinger Kirk when he invades in the cathedral central room
CD: Black Eye Orb - Rosaria from Leonhard's questOn Rosaria's corpse after joining Rosaria's Fingers, exhausting Leonhard's dialogue there and reaching the Profaned Capital bonfire.
CD: Blessed Gem - upper roofs, raftersIn the rafters leading to Rosaria, guarded by a Cathedral Knight to the right
CD: Crest Shield - path, drop down by Cathedral of the Deep bonfireOn a grave near the Cathedral of the Deep bonfire, accessed by dropping down to the right
CD: Curse Ward Greatshield - by ladder from white tree to moatTaking a right after the Infested Corpse graveyard, before the shortcut ladder down to the Ravenous Crystal Lizard area
CD: Deep Braille Divine Tome - mimic by side chapelDropped by the Mimic before the room with the patrolling Cathedral Knight and Deep Accursed
CD: Deep Gem - down stairs by first elevatorComing from the room where you first see deacons, go down instead of continuing to the main cathedral room. Guarded by a pensive Cathedral Evangelist.
CD: Deep Ring - upper roofs, passive mob drop in first towerDropped by the passive Deacon on the way to Rosaria
CD: Drang Armor - main hall, eastIn the Giant Slave muck pit leading up to Deacons
CD: Drang Gauntlets - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Drang Hammers - main hall eastIn the Giant Slave muck pit leading up to Deacons, underneath the stairwell
CD: Drang Shoes - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Duel Charm - by first elevatorAfter opening the cathedral's backdoor, where the Deacon enemies are first seen, under a fountain that spouts poison
CD: Duel Charm - next to Patches in onion armorTo the right of the bridge leading to Rosaria, from the Deacons side. Patches will lower the bridge if you try to cross from this side.
CD: Ember - PatchesSold by Patches in Firelink Shrine
CD: Ember - by back doorPast the pair of Grave Wardens and the Cathedral backdoor against a wall, guarded by a greataxe-wielding Large Hollow Soldier
CD: Ember - edge of platform before bossOn the edge of the chapel before Deacons overlooking the Giant Slaves
CD: Ember - side chapel upstairs, up ladderUp a ladder and past the Cathedral Evangelist from the top level of the room with the patrolling Cathedral Knight and Deep Accursed
CD: Ember - side chapel, miniboss roomIn the room with the Deep Accursed
CD: Estus Shard - monument outside Cleansing ChapelRight outside of the Cleansing Chapel. Requires killing praying hollows.
CD: Executioner's Greatsword - graveyard, far endIn an open area down one of the side paths to the left in the Reanimated Corpse area
CD: Exploding Bolt - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Fading Soul - graveyard, far endIn an open area down one of the side paths to the left in the Reanimated Corpse area, next to the Executioner's Greatsword
CD: Gauntlets of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Helm of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Herald Armor - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Gloves - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Helm - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Trousers - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Heysel Pick - Heysel Corpse-Grub in Rosaria's Bed ChamberDropped by the Heysel Corpse-grub in Rosaria's Bed Chamber
CD: Homeward Bone - outside main hall south doorPast the cathedral doors guarded by the Giant Slave opposite to the Deacons fight
CD: Horsehoof Ring - PatchesSold or dropped by Patches after he mentions Greirat
CD: Large Soul of an Unknown Traveler - by white tree #1In the graveyard with the White Birch and Infested Corpses
CD: Large Soul of an Unknown Traveler - by white tree #2In the graveyard with the White Birch and Infested Corpses
CD: Large Soul of an Unknown Traveler - lower roofs, semicircle balconyOn the cathedral roof after climbing up the flying buttresses, on the edge of the semicircle platform balcony
CD: Large Soul of an Unknown Traveler - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Large Soul of an Unknown Traveler - main hall south, side pathDown a side path with poison-spouting fountains in the main cathedral room, accessible from the Cleansing Chapel shortcut, patrolled by a Cathedral Knight
CD: Large Soul of an Unknown Traveler - path, against outer wallFrom the Cathedral of the Deep bonfire after the Brigand, against the wall in the area with the dogs and crossbowmen
CD: Leggings of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Lloyd's Sword Ring - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Maiden Gloves - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Hood - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Robe - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Skirt - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Notched Whip - Cleansing ChapelIn a corner of the Cleansing Chapel
CD: Paladin's Ashes - path, guarded by lower NPCAt the very start of the area, guarded by the Fallen Knight
CD: Pale Tongue - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Pale Tongue - upper roofs, outdoors far endBefore the rafters on the way to Rosaria, up a flying buttress and straight right, passing a halberd-wielding Large Hollow Soldier
CD: Poisonbite Ring - moat, hall past minibossIn the pit with the Infested Corpse, accessible from the Ravenous Crystal Lizard area or from dropping down near the second Cleansing Chapel shortcut
CD: Red Bug Pellet - lower roofs, up stairs between buttressesIn the area after the cathedral roof against the wall of the cathedral, down the path from the Cathedral Evangelist.
CD: Red Bug Pellet - right of cathedral front doorsUp the stairs past the Infested Corpse graveyard and the left, toward the roof path to the right of the cathedral doors
CD: Red Sign Soapstone - passive mob drop by Rosaria's Bed ChamberDropped by passive Corpse-grub against the wall near the entrance to Rosaria's Bed Chamber
CD: Repair Powder - by white treeIn the graveyard with the White Birch and Infested Corpses
CD: Ring of Favor+2 - upper roofs, on buttressBefore the rafters on the way to Rosaria, up a flying buttress, behind a greataxe-wielding Large Hollow Soldier to the left
CD: Ring of the Evil Eye+1 - by stairs to bossBefore the stairs leading down into the Deacons fight
CD: Rosaria's Fingers - RosariaGiven by Rosaria.
CD: Rusted Coin - don't forgive PatchesGiven by Patches after not forgiving him after he lowers the bridge in Cathedral of the Deep.
CD: Rusted Coin - left of cathedral front doors, behind cratesUp the stairs past the Infested Corpse graveyard and to the left, hidden behind some crates to the left of the cathedral door
CD: Saint Bident - outside main hall south doorPast the cathedral doors guarded by the Giant Slave opposite to the Deacons fight
CD: Saint-tree Bellvine - moat, by waterIn the Infested Corpse moat beneath the Cathedral
CD: Seek Guidance - side chapel upstairsAbove the room with the patrolling Cathedral Knight and Deep Accursed, below a writhing flesh on the ceiling.
CD: Shotel - PatchesSold by Patches
CD: Small Doll - boss dropDropped by Deacons of the Deep
CD: Soul of a Nameless Soldier - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Soul of a Nameless Soldier - lower roofs, side roomComing from the cathedral roof, past the three crossbowmen to the path patrolled by the halberd-wielding Large Hollow Soldier, in a room to the left with many thralls.
CD: Soul of a Nameless Soldier - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Soul of the Deacons of the DeepDropped by Deacons of the Deep
CD: Spider Shield - NPC drop on pathDropped by the brigand at the start of Cathedral of the Deep
CD: Spiked Shield - Kirk dropDropped by Longfinger Kirk when he invades in the cathedral central room
CD: Titanite Scale - moat, miniboss dropDropped by the Ravenous Crystal Lizard outside of the Cathedral
CD: Titanite Shard - Cleansing Chapel windowsill, by minibossOn the ledge dropping back down into Cleansing Chapel from the area with the Ravenous Crystal Lizard
CD: Titanite Shard - moat, far endBehind the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Titanite Shard - moat, up a slopeUp one of the slopes in the Ravenous Crystal Lizard area
CD: Titanite Shard - outside building by white treePast the Infested Corpse graveyard to the left, hidden along the left wall of the building with the shortcut ladder and Curse Ward Greatshield
CD: Titanite Shard - path, side path by Cathedral of the Deep bonfireUp a path to the left after the Cathedral of the Deep bonfire, after the Fallen Knight and before the Brigand
CD: Twinkling Titanite - moat, lizard #1Dropped by the Crystal Lizard behind the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Twinkling Titanite - moat, lizard #2Dropped by the Crystal Lizard under the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Twinkling Titanite - path, lizard #1Dropped by the first Crystal Lizard after the Crystal Sage fight
CD: Twinkling Titanite - path, lizard #2Dropped by the second Crystal Lizard after the Crystal Sage fight
CD: Undead Bone Shard - gravestone by white treeIn the graveyard with the Infested Corpses, on a coffin partly hanging off of the ledge
CD: Undead Hunter Charm - lower roofs, up stairs between buttressesIn the area after the cathedral roof guarded by a Cathedral Evangelist. Can be jumped to from a flying buttress or by going around and back
CD: Winged Spear - kill PatchesDropped by Patches when killed in his own armor.
CD: Xanthous Crown - Heysel Corpse-Grub in Rosaria's Bed ChamberDropped by the Heysel Corpse-grub in Rosaria's Bed Chamber
CD: Young White Branch - by white tree #1By the White Birch tree in the Infested Corpse graveyard
CD: Young White Branch - by white tree #2By the White Birch tree in the Infested Corpse graveyard
CKG: Black Firebomb - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Claw - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Dark Gem - under lone stairwayFollowing the left wall, behind the standalone set of stairs
CKG: Dragonscale Ring - shortcut, leave halfway down liftFrom the middle level of the second elevator, toward the Oceiros boss fight
CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Estus Shard - balconyFrom the middle level of the first Consumed King's Gardens elevator, out the balcony and to the right
CKG: Human Pine Resin - pool by liftOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, in a toxic pool
CKG: Human Pine Resin - toxic pool, past rotundaIn between two platforms near the middle of the garden, by a tree in a toxic pool
CKG: Magic Stoneplate Ring - mob drop before bossDropped by the Cathedral Knight closest to the Oceiros fog gate
CKG: Ring of Sacrifice - under balconyAlong the right wall of the garden, next to the first elevator building
CKG: Sage Ring+2 - balcony, drop onto rubble, jump backFrom the middle platform of the first elevator in the target, going out and dropping off to the left, and then running off onto the ruined arch behind.
CKG: Shadow Garb - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Gauntlets - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Leggings - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Mask - under center platformUnder the platform in the middle of the garden, in the toxic pool
CKG: Soul of Consumed OceirosDropped by Consumed King Oceiros
CKG: Soul of a Weary Warrior - before first liftOn the path leading to the first elevator from Lothric Castle
CKG: Titanite Chunk - balcony, drop onto rubbleFrom the middle platform of the first elevator, dropping down to the left
CKG: Titanite Chunk - right of shortcut lift bottomOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, all the way to the end
CKG: Titanite Chunk - shortcutRight inside of the shortcut door leading to Oceiros from Lothric/Dancer bonfire
CKG: Titanite Chunk - up lone stairwayFollowing the left wall of the garden, in and up the standalone set of stairs
CKG: Titanite Scale - shortcutIn the room leading to the Oceiros shortcut elevator from Lothric/Dancer, in the first floor alcove.
CKG: Titanite Scale - tomb, chest #1Chest after Oceiros fight
CKG: Titanite Scale - tomb, chest #2Chest after Oceiros fight
CKG: Wood Grain Ring+1 - by first elevator bottomBehind the first elevator going down into the garden, in the toxic pool
DH: Aquamarine Dagger - castle, up stairsUp the second flight of stairs to the left of the starting area with the murkmen, before the long drop
DH: Black Firebomb - ruins, up windmill from bonfireTo the left of the Earthen Peak Ruins bonfire, past the ruined windmill, next to many Poisonhorn bugs.
DH: Covetous Silver Serpent Ring+3 - pantry upstairs, drop downAfter exiting the building with the Lothric Knights where the front crumbles, to the last room of the building to the right, up stairs past an illusory wall to the left, then dropping down after exiting the building from the last room.
DH: Desert Pyromancer Garb - ruins, by shack near cliffBehind a shack near the edge of the cliff of the area targeted by the second angel.
DH: Desert Pyromancer Gloves - swamp, far rightAfter dropping down in the poison swamp area, against the wall straight to the right.
DH: Desert Pyromancer Hood - swamp upper, tunnel endAt the end of the tunnel with Desert Pyromancy Zoey, to the right of the final branches.
DH: Desert Pyromancer Skirt - swamp right, by rootsIn the poison swamp, against a tree guarded by a few Poisonhorn bugs in the front right.
DH: Divine Blessing - library, after dropAfter the dropdown where an angel first targets you, behind you
DH: Divine Blessing - shopSold by Stone-humped Hag, or in her ashes
DH: Divine Blessing - swamp upper, building roofOn a rooftop of one of the buildings bordering the poison swamp. Can be reached by dropping down from the final tree branch and accessing the roof to the right.
DH: Ember - castle, behind spireAt the start of the area, behind a spire to the right of first drop down
DH: Ember - pantry, behind crates just before upstairsAfter exiting the building with the Lothric Knights where the front crumbles, to the last room end of the building to the right, up stairs past an illusory wall to the left, in the second-to-last room of the sequence, behind some crates to the left.
DH: Ember - ruins, alcove before swampIn an alcove providing cover from the second angel's projectiles, before dropping down in the poison swamp area.
DH: Ember - ruins, alcove on cliffIn the area with the pilgrim responsible for the second angel, below the Within Earthen Peak Ruins bonfire. Can be accessed by dropping down from a cliff edge, dropping down to the right of the bonfire.
DH: Ember - shopSold by Stone-humped Hag, or in her ashes
DH: Flame Fan - swamp upper, NPC dropDropped by Desert Pyromancer Zoey
DH: Giant Door Shield - ruins, path below far shackDescending down a path from the edge of the cliff of the area targeted by the second angel, to the very end of the cliff.
DH: Great Soul Dregs - pantry upstairsAfter exiting the building with the Lothric Knights where the front crumbles, to the last room of the building to the right, up stairs past an illusory wall to the left, then all the way to the end of the last room.
DH: Harald Curved Greatsword - swamp left, under rootIn the back leftmost area of the poison swamp, underneath the tree branch leading up and out, guarded by a stationary Harald Legion Knight.
DH: Hidden Blessing - shopSold by Stone-humped Hag, or in her ashes
DH: Homeward Bone - end of path from churchImmediately before dropping into the area with the Earthen Peak Ruins bonfire, next to Gael's flag.
DH: Homeward Bone - swamp left, on rootAll the way to the end of a short path in the back leftmost area of the poison swamp, where you can plunge attack the stationary Harald Legion Knight.
DH: Large Soul of a Weary Warrior - parapets, hallAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman and dropping down again, in a corner to the left.
DH: Large Soul of a Weary Warrior - swamp centerIn the middle of the poison swamp.
DH: Large Soul of a Weary Warrior - swamp, under overhangIn the cavern adjacent to the poison swamp, surrounded by a few Poisonhorn bugs.
DH: Lightning Urn - wall outside churchAfter the dropdown where an angel first targets you, against the wall on the left.
DH: Loincloth - swamp, left edgeIn the leftmost edge of the poison swamp after dropping down, guarded by 6 Poisonhorn bugs.
DH: Lothric War Banner - parapets, end of hallAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman and dropping down again, at the end of the hallway to the right.
DH: Murky Hand Scythe - library, behind bookshelvesAfter the first long drop into the building which looks like Grand Archives, to the left up the bookshelf stairs and behind the bookshelves
DH: Murky Longstaff - pantry, last roomAfter exiting the building with the Lothric Knights where the front crumbles, in the third-furthest room in the building to the right.
DH: Prism Stone - swamp upper, tunnel startNear the start of the tunnel with Desert Pyromancer Zoey.
DH: Projected Heal - parapets balconyAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman, against a wall in the area with the Lothric War Banner Knight and many murkmen.
DH: Purple Moss Clump - swamp shackIn the ruined shack with Poisonhorn bugs straight ahead of the dropdown into the poison swamp area.
DH: Ring of Favor+3 - swamp right, up rootUp the long branch close to the dropdown into the poison swamp area, in front of the cavern.
DH: Ring of Steel Protection+3 - ledge before churchAfter the dropdown where an angel first targets you, on an exposed edge to the left. Difficult to get without killing the angel.
DH: Rusted Coin - behind fountain after churchAfter exiting the building with the Lothric Knights where the front crumbles, behind the fountain on the right side.
DH: Rusted Gold Coin - shopSold by Stone-humped Hag, or in her ashes
DH: Siegbräu - LappGiven by Lapp after collecting the Titanite Slab in Earthen Peak Ruins, or left after Demon Princes fight, or dropped upon death if not given.
DH: Small Envoy Banner - boss dropFound in the small room after beating Demon Prince.
DH: Soul of a Crestfallen Knight - church, altarIn the building where the front crumbles, guarded by the two Lothric Knights at the front of the chapel.
DH: Soul of a Weary Warrior - castle overhangThe bait item at the start of the area which falls down with you into the ruined building below.
DH: Soul of the Demon PrinceDropped by Demon Prince
DH: Splitleaf Greatsword - shopSold by Stone-humped Hag, or in her ashes
DH: Titanite Chunk - castle, up stairsUp first flight of stairs to the left of the starting area with the murkmen, before the long drop
DH: Titanite Chunk - pantry, first roomAfter exiting the building with the Lothric Knights where the front crumbles, on a ledge in the first room of the building to the right.
DH: Titanite Chunk - path from church, by pillarBefore dropping into the area with the Earthen Peak Ruins bonfire, behind a pillar in front of a murkman pool.
DH: Titanite Chunk - ruins, by far shackIn front of a shack at the far edge of the cliff of the area targeted by the second angel. There is a shortcut dropdown to the left of the building.
DH: Titanite Chunk - ruins, path from bonfireAt the Earthen Peak Ruins bonfire, straight a bit then all the way left, near the edge of the cliff in the area targeted by the second angel.
DH: Titanite Chunk - swamp right, drop partway up rootPartway up the long branch close to the dropdown into the poison swamp area, in front of the cavern, dropping down to a branch to the left.
DH: Titanite Chunk - swamp, along buildingsAfter dropping down into the poison swamp, along the buildings on the left side.
DH: Titanite Chunk - swamp, path to upperPartway up the branch that leads out of the poison swamp, on a very exposed branch jutting out to the left.
DH: Titanite Scale - library, back of roomAfter the first long drop into the building which looks like Grand Archives, behind you at the back of the room
DH: Titanite Scale - swamp upper, drop and jump into towerAt the very end of the last tree branch before dropping down toward the Within Earthen Peak Ruins bonfire, drop down to the left instead. Make a jump into the interior of the overturned tower to the left.
DH: Titanite Slab - swamp, path under overhangDeep within the cavern adjacent to the poison swamp, to the back and then left. Alternatively, given by Lapp after exhausting dialogue near the bonfire and dying, or left after he moves on, or dropped upon death if not given.
DH: Twinkling Titanite - library, chandelierAfter the first long drop into the building which looks like Grand Archives, straight ahead hanging from a chandelier on the ground
DH: Twinkling Titanite - path after church, mob dropDropped the pilgrim responsible for the first angel encountered, below the spire bridge that forms by crashing into the building.
DH: Twinkling Titanite - ruins, alcove on cliff, mob dropDropped by the pilgrim responsible for the second angel, below the Within Earthen Peak Ruins bonfire. Can be accessed by dropping down from a cliff edge, or dropping down to the right of the bonfire.
DH: Twinkling Titanite - ruins, root near bonfireTreasure visible straight ahead of the Earthen Peak Ruins bonfire on a branch. Can be accessed by following the right wall from the bonfire until a point of access onto the branch is found.
DH: Twinkling Titanite - swamp upper, drop onto rootOn the final tree branches before dropping down toward the Within Earthen Peak Ruins bonfire, drop down on a smaller branch to the right. This loops back to the original branch.
DH: Twinkling Titanite - swamp upper, mob drop on roofDropped by the pilgrim responsible for the third angel in the swamp. Rather than heading left into the tunnel with Desert Pyromancy Zoey, go right onto a shack roof. Drop down onto a tree branch at the end, then drop down to another roof.
FK: Antiquated Dress - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Antiquated Gloves - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Antiquated Skirt - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Atonement - perimeter, drop down into swampDropping down from the Farron Keep Perimeter building, to the right past the bonfire, before the stairs going up
FK: Black Bow of Pharis - miniboss drop, by keep ruins near wallDropped the Elder Ghru on the left side of the group of three to the left of the Keep Ruins bonfire, as approached from the ritual fire.
FK: Black Bug Pellet - perimeter, hill by boss doorOn the small hill to the right of the Abyss Watchers entrance, guarded by a spear-wielding Ghru Grunt
FK: Cinders of a Lord - Abyss WatcherDropped by Abyss Watchers
FK: Crown of Dusk - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Dark Stoneplate Ring+2 - keep ruins ritual island, behind wallHidden behind the right wall of the ritual fire before Keep Ruins
FK: Dragon Crest Shield - upper keep, far side of the wallUp the elevator from Old Wolf of Farron bonfire, and dropping down to Crystal Lizard area, in the open.
FK: Dreamchaser's Ashes - keep proper, illusory wallNear the Old Wolf of Farron bonfire, behind an illusory wall near the Crystal Lizard
FK: Ember - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Ember - perimeter, path to bossGuarded by a spear-wielding Ghru Grunt to the right of the main path leading up to Abyss Watchers
FK: Ember - upper keep, by miniboss #1Guarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Ember - upper keep, by miniboss #2Guarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Estus Shard - between Farron Keep bonfire and left islandStraight ahead from the Farron Keep bonfire to the ritual fire stairs, guarded by a slug
FK: Gold Pine Bundle - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Golden Scroll - hidden caveIn a cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Great Magic Weapon - perimeter, by door to Road of SacrificesNext to the shortcut leading from Farron Keep Perimeter back into Crucifixion Woods, past the Ravenous Crystal Lizard
FK: Greataxe - upper keep, by minibossGuarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Greatsword - ramp by keep ruins ritual islandIn the middle of the swamp, on the pair of long ramps furthest from the Farron Keep bonfire, going out forward and slightly right from the bonfire.
FK: Havel's Armor - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Gauntlets - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Helm - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Leggings - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Heavy Gem - upper keep, lizard on stairsDropped by the Crystal Lizard that scurries up the stairs in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Hollow Gem - perimeter, drop down into swampDropping down from the Farron Keep Perimeter building, to the right past the bonfire, before the stairs going up
FK: Homeward Bone - right island, behind fireBehind the ritual fire with stairs guarded by Elder Ghrus/basilisks
FK: Iron Flesh - Farron Keep bonfire, right after exitIn the open in the swamp, heading straight right from Farron Keep bonfire
FK: Large Soul of a Nameless Soldier - corner of keep and right islandHidden in a corner to the right of the stairs leading up to the ritual fire from the basilisk area
FK: Large Soul of a Nameless Soldier - near wall by right islandTo the left of the stairs leading up to the ritual fire from the Basilisk area, by the keep wall
FK: Large Soul of an Unknown Traveler - by white treeOn a tree close to the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Large Titanite Shard - upper keep, lizard by wyvernDropped by the farther Crystal Lizard in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Large Titanite Shard - upper keep, lizard in openDropped by the closer Crystal Lizard in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Lightning Spear - upper keep, far side of the wallUp the elevator from Old Wolf of Farron bonfire, and dropping down to Crystal Lizard area, in the open.
FK: Lingering Dragoncrest Ring - by white tree, miniboss dropDropped by the Greater Crab patrolling the birch tree where the Giant shoots arrows
FK: Magic Stoneplate Ring+1 - between right island and wallBehind a tree in the basilisk area, heading directly right from Farron Keep bonfire
FK: Manikin Claws - Londor Pale Shade dropDropped by Londor Pale Shade when he invades near the basilisks, if Yoel or Yuria have been betrayed
FK: Nameless Knight Armor - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Gauntlets - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Helm - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Leggings - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Pharis's Hat - miniboss drop, by keep ruins near wallDropped the Elder Ghru in the back of the group of three to the left of the Keep Ruins bonfire, as approached from the ritual fire.
FK: Poison Gem - near wall by keep ruins bridgeFrom the left of the bridge leading from the ritual fire to the Keep Ruins bonfire, guarded by the three Elder Ghru
FK: Prism Stone - by left island stairsOn an island to the left of the stairs leading up to the ritual fire straight ahead of the Farron Keep bonfire
FK: Purple Moss Clump - Farron Keep bonfire, around right cornerAlong the inner wall of the keep, making an immediate right from Farron Keep bonfire
FK: Purple Moss Clump - keep ruins, ritual islandClose to the ritual fire before the Keep Ruins bonfire
FK: Purple Moss Clump - ramp directly in front of Farron Keep bonfireIn the middle of the swamp, on the pair of long ramps closest to the Farron Keep bonfire, going out forward and slightly right from the bonfire.
FK: Ragged Mask - Farron Keep bonfire, around left cornerAlong the inner wall of the keep, making an immediate left from Farron Keep bonfire, guarded by slugs
FK: Repair Powder - outside hidden caveAlong the keep wall in the basilisk area, outside of the cave with the Elizabeth corpse and Golden Scroll
FK: Rotten Pine Resin - left island, behind fireIn the area behind the ritual fire which is straight ahead of the Farron Keep bonfire
FK: Rotten Pine Resin - outside pavilion by left islandFrom the Farron Keep bonfire straight ahead to the pavilion guarded by the Darkwraith, just to the left of the ritual fire stairs
FK: Rusted Gold Coin - right island, behind wallHidden behind the right wall of the ritual fire with stairs guarded by Elder Ghrus/basilisks
FK: Sage's Coal - pavilion by left islandIn the pavilion guarded by a Darkwraith, straight ahead from the Farron Keep bonfire to the left of the ritual fire stairs
FK: Sage's Scroll - near wall by keep ruins bonfire islandAlong the keep inner wall, heading left from the stone doors past the crab area, surrounded by many Ghru enemies
FK: Shriving Stone - perimeter, just past stone doorsPast the stone doors, on the path leading up to Abyss Watchers by the Corvians
FK: Soul of a Nameless Soldier - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Soul of a Stray Demon - upper keep, miniboss dropDropped by Stray Demon on the bridge above Farron Keep
FK: Soul of the Blood of the WolfDropped by Abyss Watchers
FK: Stone Parma - near wall by left islandAlong the inner wall of the keep, making a left from Farron Keep bonfire but before the area with the Darkwraith, guarded by a slug
FK: Sunlight Talisman - estus soup island, by ladder to keep properBy the pot of estus soup to the left of the stairs leading up to Old Wolf of Farron
FK: Titanite Scale - perimeter, miniboss dropDropped by Ravenous Crystal Lizard near the shortcut from Farron Keep back to Road of Sacrifices
FK: Titanite Shard - Farron Keep bonfire, left after exitAlong the inner wall of the keep, making a left from Farron Keep bonfire, by the second group of four slugs
FK: Titanite Shard - between left island and keep ruinsIn the swamp area with the Ghru Leaper between the Keep Ruins ritual fire and ritual fire straight ahead of Farron Keep bonfire, opposite from the keep wall
FK: Titanite Shard - by keep ruins ritual island stairsBy the stairs leading up to the Keep Ruins ritual fire from the middle of the swamp
FK: Titanite Shard - by ladder to keep properIn the swamp area close to the foot of the ladder leading to Old Wolf of Farron bonfire
FK: Titanite Shard - by left island stairsIn front of the stairs leading up to the ritual fire straight ahead of the Farron Keep bonfire
FK: Titanite Shard - keep ruins bonfire island, under rampUnder the ramp leading down from the Keep Ruins bonfire
FK: Titanite Shard - swamp by right islandBehind a tree patrolled by an Elder Ghru close to the ritual fire stairs
FK: Twinkling Dragon Head Stone - Hawkwood dropDropped by Hawkwood after killing him in the Abyss Watchers arena, after running up to the altar in Archdragon Peak. Twinkling Dragon Torso Stone needs to be acquired first.
FK: Twinkling Titanite - keep proper, lizardDropped by the Crystal Lizard on the balcony behind the Old Wolf of Farron bonfire
FK: Undead Bone Shard - pavilion by keep ruins bonfire islandIn a standalone pavilion down the ramp from Keep Ruins bonfire and to the right
FK: Watchdogs of Farron - Old WolfGiven by Old Wolf of Farron.
FK: Wolf Ring+1 - keep ruins bonfire island, outside buildingTo the right of the building with the Keep Ruins bonfire, when approached from the ritual fire
FK: Wolf's Blood Swordgrass - by ladder to keep properTo the left of the ladder leading up to the Old Wolf of Farron bonfire
FK: Young White Branch - by white tree #1Near the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Young White Branch - by white tree #2Near the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FS: Acid Surge - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Affinity - KarlaSold by Karla after recruiting her, or in her ashes
FS: Alluring Skull - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Arstor's Spear - Ludleth for GreatwoodBoss weapon for Curse-Rotted Greatwood
FS: Aural Decoy - OrbeckSold by Orbeck
FS: Billed Mask - shop after killing YuriaDropped by Yuria upon death or quest completion.
FS: Black Dress - shop after killing YuriaDropped by Yuria upon death or quest completion.
FS: Black Fire Orb - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome
FS: Black Flame - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome
FS: Black Gauntlets - shop after killing YuriaDropped by Yuria upon death or quest completion.
FS: Black Hand Armor - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai
FS: Black Hand Hat - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai
FS: Black Iron Armor - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Gauntlets - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Helm - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Leggings - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Leggings - shop after killing YuriaDropped by Yuria upon death or quest completion.
FS: Black Serpent - Ludleth for WolnirBoss weapon for High Lord Wolnir
FS: Blessed Weapon - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Blue Tearstone Ring - GreiratGiven by Greirat upon rescuing him from the High Wall cell
FS: Boulder Heave - Ludleth for Stray DemonBoss weapon for Stray Demon
FS: Bountiful Light - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Bountiful Sunlight - Ludleth for RosariaBoss weapon for Rosaria, available after Leonhard is killed
FS: Broken Straight Sword - gravestone after bossNear the grave after Iudex Gundyr fight
FS: Budding Green Blossom - shop after killing Creighton and AL bossSold by Handmaid after receiving Silvercat Ring item lot from Sirris and defeating Aldrich
FS: Bursting Fireball - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Caressing Tears - IrinaSold by Irina after recruiting her, or in her ashes
FS: Carthus Beacon - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Carthus Flame Arc - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Cast Light - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Chaos Bed Vestiges - Ludleth for Old Demon KingBoss weapon for Old Demon King
FS: Chaos Storm - Cornyx for Izalith TomeSold by Cornyx after giving him Izalith Pyromancy Tome
FS: Clandestine Coat - shop with Orbeck's AshesSold by Handmaid after giving Orbeck's Ashes and reloading
FS: Cleric's Candlestick - Ludleth for DeaconsBoss weapon for Deacons of the Deep
FS: Cracked Red Eye Orb - LeonhardGiven by Ringfinger Leonhard in Firelink Shrine after reaching Tower on the Wall bonfire
FS: Crystal Hail - Ludleth for SageBoss weapon for Crystal Sage
FS: Crystal Magic Weapon - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Crystal Sage's Rapier - Ludleth for SageBoss weapon for Crystal Sage
FS: Crystal Soul Spear - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Dancer's Armor - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Crown - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Enchanted Swords - Ludleth for DancerBoss weapon for Dancer of the Boreal Valley
FS: Dancer's Gauntlets - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Leggings - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dark Blade - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Dark Edge - KarlaSold by Karla after recruiting her, or in her ashes
FS: Dark Hand - Yuria shopSold by Yuria
FS: Darkdrift - kill YuriaDropped by Yuria upon death or quest completion.
FS: Darkmoon Longbow - Ludleth for AldrichBoss weapon for Aldrich
FS: Dead Again - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Deep Protection - Karla for Deep Braille TomeSold by Irina or Karla after giving one the Deep Braille Divine Tome
FS: Deep Soul - Ludleth for DeaconsBoss weapon for Deacons of the Deep
FS: Demon's Fist - Ludleth for Fire DemonBoss weapon for Fire Demon
FS: Demon's Greataxe - Ludleth for Fire DemonBoss weapon for Fire Demon
FS: Demon's Scar - Ludleth for Demon PrinceBoss weapon for Demon Prince
FS: Divine Blessing - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Divine Blessing - Greirat from USSold by Greirat after pillaging Undead Settlement
FS: Dragonscale Armor - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Dragonscale Waistcloth - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Dragonslayer Greataxe - Ludleth for DragonslayerBoss weapon for Dragonslayer Armour
FS: Dragonslayer Greatshield - Ludleth for DragonslayerBoss weapon for Dragonslayer Armour
FS: Dragonslayer Swordspear - Ludleth for NamelessBoss weapon for Nameless King
FS: Dried Finger - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: East-West Shield - tree by shrine entranceIn a tree to the left of the Firelink Shrine entrance
FS: Eastern Armor - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Gauntlets - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Helm - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Leggings - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Elite Knight Armor - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Gauntlets - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Helm - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Leggings - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Ember - Dragon Chaser's AshesSold by Handmaid after giving Dragon Chaser's Ashes
FS: Ember - Grave Warden's AshesSold by Handmaid after giving Grave Warden's Ashes
FS: Ember - GreiratSold by Greirat after recruiting him, or in his ashes
FS: Ember - Greirat from USSold by Greirat after pillaging Undead Settlement
FS: Ember - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Ember - above shrine entranceAbove the Firelink Shrine entrance, up the stairs/slope from either left or right of the entrance
FS: Ember - path right of Firelink entranceOn a cliffside to the right of the main path leading up to Firelink Shrine, guarded by a dog
FS: Ember - shopSold by Handmaid
FS: Ember - shop for Greirat's AshesSold by Handmaid after Greirat pillages Lothric Castle and handing in ashes
FS: Embraced Armor of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Executioner Armor - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Gauntlets - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Helm - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Leggings - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Exile Armor - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Gauntlets - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Leggings - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Mask - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Faraam Armor - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Faraam Boots - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Faraam Gauntlets - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Faraam Helm - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Farron Dart - OrbeckSold by Orbeck
FS: Farron Dart - shopSold by Handmaid
FS: Farron Flashsword - OrbeckSold by Orbeck
FS: Farron Greatsword - Ludleth for Abyss WatchersBoss weapon for Abyss Watchers
FS: Farron Hail - Orbeck for Sage's ScrollSold by Orbeck after giving him the Sage's Scroll
FS: Farron Ring - HawkwoodGiven by Hawkwood, or dropped upon death, after defeating Abyss Watchers.
FS: Fire Orb - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Fire Surge - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Fire Whip - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Fireball - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Firelink Armor - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Gauntlets - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Greatsword - Ludleth for CinderBoss weapon for Soul of Cinder
FS: Firelink Helm - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Leggings - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firestorm - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Flash Sweat - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Force - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Frayed Blade - Ludleth for MidirBoss weapon for Darkeater Midir
FS: Friede's Great Scythe - Ludleth for FriedeBoss weapon for Sister Friede
FS: Gael's Greatsword - Ludleth for GaelBoss weapon for Slave Knight Gael
FS: Gauntlets of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Gnaw - Karla for Deep Braille TomeSold by Irina or Karla after giving one the Deep Braille Divine Tome
FS: Golden Bracelets - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Golden Crown - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Grave Key - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Great Chaos Fire Orb - Cornyx for Izalith TomeSold by Cornyx after giving him Izalith Pyromancy Tome
FS: Great Combustion - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Great Farron Dart - Orbeck for Sage's ScrollSold by Orbeck after giving him the Sage's Scroll
FS: Great Heavy Soul Arrow - OrbeckSold by Orbeck
FS: Great Soul Arrow - OrbeckSold by Orbeck
FS: Greatsword of Judgment - Ludleth for PontiffBoss weapon for Pontiff Sulyvahn
FS: Gundyr's Armor - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Gauntlets - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Halberd - Ludleth for ChampionBoss weapon for Champion Gundyr
FS: Gundyr's Helm - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Leggings - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Havel's Ring - Ludleth for Stray DemonBoss weapon for Stray Demon
FS: Hawkwood's Shield - gravestone after Hawkwood leavesLeft by Hawkwood after defeating Abyss Watchers, Curse-Rotted Greatwood, Deacons of the Deep, and Crystal Sage
FS: Hawkwood's Swordgrass - Andre after gesture in AP summitGiven by Andre after praying at the Dragon Altar in Archdragon Peak, after acquiring Twinkling Dragon Torso Stone.
FS: Heal - IrinaSold by Irina after recruiting her, or in her ashes
FS: Heal Aid - shopSold by Handmaid
FS: Heavy Soul Arrow - OrbeckSold by Orbeck
FS: Heavy Soul Arrow - Yoel/Yuria shopSold by Yoel/Yuria
FS: Helm of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Hidden Blessing - Dreamchaser's AshesSold by Greirat after pillaging Irithyll
FS: Hidden Blessing - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Hidden Blessing - Patches after searching GASold by Handmaid after giving Dreamchaser's Ashes, saying where they were found
FS: Hidden Body - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Hidden Weapon - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Hollowslayer Greatsword - Ludleth for GreatwoodBoss weapon for Curse-Rotted Greatwood
FS: Homeward - IrinaSold by Irina after recruiting her, or in her ashes
FS: Homeward Bone - cliff edge after bossAlong the cliff edge straight ahead of the Iudex Gundyr fight
FS: Homeward Bone - path above shrine entranceTo the right of the Firelink Shrine entrance, up a slope and before the ledge on top of a coffin
FS: Homing Crystal Soulmass - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Homing Soulmass - Orbeck for Logan's ScrollSold by Orbeck after giving him Logan's Scroll
FS: Karla's Coat - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Coat - kill KarlaDropped from Karla upon death
FS: Karla's Gloves - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Gloves - kill KarlaDropped from Karla upon death
FS: Karla's Pointed Hat - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Pointed Hat - kill KarlaDropped from Karla upon death
FS: Karla's Trousers - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Trousers - kill KarlaDropped from Karla upon death
FS: Leggings of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Leonhard's Garb - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Leonhard's Gauntlets - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Leonhard's Trousers - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Life Ring - Dreamchaser's AshesSold by Handmaid after giving Dreamchaser's Ashes
FS: Lifehunt Scythe - Ludleth for AldrichBoss weapon for Aldrich
FS: Lift Chamber Key - LeonhardGiven by Ringfinger Leonhard after acquiring a Pale Tongue.
FS: Lightning Storm - Ludleth for NamelessBoss weapon for Nameless King
FS: Lloyd's Shield Ring - Paladin's AshesSold by Handmaid after giving Paladin's Ashes
FS: Londor Braille Divine Tome - Yuria shopSold by Yuria
FS: Lorian's Armor - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Gauntlets - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Greatsword - Ludleth for PrincesBoss weapon for Twin Princes
FS: Lorian's Helm - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Leggings - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lothric's Holy Sword - Ludleth for PrincesBoss weapon for Twin Princes
FS: Magic Barrier - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Magic Shield - OrbeckSold by Orbeck
FS: Magic Shield - Yoel/Yuria shopSold by Yoel/Yuria
FS: Magic Weapon - OrbeckSold by Orbeck
FS: Magic Weapon - Yoel/Yuria shopSold by Yoel/Yuria
FS: Mail Breaker - Sirris for killing CreightonGiven by Sirris talking to her in Firelink Shrine after invading and vanquishing Creighton.
FS: Master's Attire - NPC dropDropped by Sword Master
FS: Master's Gloves - NPC dropDropped by Sword Master
FS: Med Heal - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Millwood Knight Armor - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Gauntlets - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Helm - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Leggings - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Moaning Shield - EygonDropped by Eygon of Carim
FS: Moonlight Greatsword - Ludleth for OceirosBoss weapon for Oceiros, the Consumed King
FS: Morion Blade - Yuria for Orbeck's AshesGiven by Yuria after giving Orbeck's Ashes after she asks you to assassinate him, after he moves to Firelink Shrine. Can be done without killing Orbeck, by completing his questline.
FS: Morne's Armor - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Gauntlets - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Great Hammer - EygonDropped by Eygon of Carim
FS: Morne's Helm - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Leggings - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Old King's Great Hammer - Ludleth for Old Demon KingBoss weapon for Old Demon King
FS: Old Moonlight - Ludleth for MidirBoss weapon for Darkeater Midir
FS: Ordained Dress - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Ordained Hood - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Ordained Trousers - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Pale Shade Gloves - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pale Shade Robe - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pale Shade Trousers - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pestilent Mist - Orbeck for any scrollSold by Orbeck after giving him any scroll
FS: Poison Mist - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Pontiff's Left Eye - Ludleth for VordtBoss weapon for Vordt of the Boreal Valley
FS: Prisoner's Chain - Ludleth for ChampionBoss weapon for Champion Gundyr
FS: Profaned Greatsword - Ludleth for PontiffBoss weapon for Pontiff Sulyvahn
FS: Profuse Sweat - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Rapport - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Refined Gem - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Repair - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Repeating Crossbow - Ludleth for GaelBoss weapon for Slave Knight Gael
FS: Replenishment - IrinaSold by Irina after recruiting her, or in her ashes
FS: Ring of Sacrifice - Yuria shopSold by Yuria, or by Handmaid after giving Hollow's Ashes
FS: Rose of Ariandel - Ludleth for FriedeBoss weapon for Sister Friede
FS: Rusted Gold Coin - don't forgive PatchesGiven by Patches after not forgiving him after he locks you in the Bell Tower.
FS: Sage's Big Hat - shop after killing RS bossSold by Handmaid after defeating Crystal Sage
FS: Saint's Ring - IrinaSold by Irina after recruiting her, or in her ashes
FS: Seething Chaos - Ludleth for Demon PrinceBoss weapon for Demon Prince
FS: Silvercat Ring - Sirris for killing CreightonGiven by Sirris talking to her in Firelink Shrine after invading and vanquishing Creighton.
FS: Skull Ring - kill LudlethDropped by Ludleth upon death, including after placing all cinders. Note that if killed before giving Transposing Kiln, transposition is not possible.
FS: Slumbering Dragoncrest Ring - Orbeck for buying four specific spellsGiven by Orbeck after purchasing the shop items corresponding to Aural Decoy, Farron Flashsword, Spook (starting items), and Pestilent Mist (after giving one scroll).
FS: Smough's Armor - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Gauntlets - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Helm - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Leggings - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Sneering Mask - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Soothing Sunlight - Ludleth for DancerBoss weapon for Dancer of the Boreal Valley
FS: Soul Arrow - OrbeckSold by Orbeck
FS: Soul Arrow - Yoel/Yuria shopSold by Yoel/Yuria
FS: Soul Arrow - shopSold by Handmaid
FS: Soul Greatsword - OrbeckSold by Orbeck
FS: Soul Greatsword - Yoel/Yuria shopSold by Yoel/Yuria after using Draw Out True Strength
FS: Soul Spear - Orbeck for Logan's ScrollSold by Orbeck after giving him Logan's Scroll
FS: Soul of a Deserted Corpse - bell tower doorNext to the door requiring the Tower Key
FS: Spook - OrbeckSold by Orbeck
FS: Storm Curved Sword - Ludleth for NamelessBoss weapon for Nameless King
FS: Sunless Armor - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Gauntlets - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Leggings - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Talisman - Sirris, kill GA bossDropped by Sirris on death or quest completion.
FS: Sunless Veil - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunlight Spear - Ludleth for CinderBoss weapon for Soul of Cinder
FS: Sunset Shield - by grave after killing Hodrick w/SirrisLeft by Sirris upon quest completion.
FS: Tears of Denial - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Titanite Scale - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Titanite Slab - shop after placing all CindersSold by Handmaid after placing all Cinders of a Lord on their thrones
FS: Tower Key - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: Twinkling Titanite - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Twisted Wall of Light - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Uchigatana - NPC dropDropped by Sword Master
FS: Undead Legion Armor - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Gauntlet - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Helm - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Leggings - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Untrue Dark Ring - Yuria shopSold by Yuria
FS: Untrue White Ring - Yuria shopSold by Yuria
FS: Vordt's Great Hammer - Ludleth for VordtBoss weapon for Vordt of the Boreal Valley
FS: Vow of Silence - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Washing Pole - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: White Dragon Breath - Ludleth for OceirosBoss weapon for Oceiros, the Consumed King
FS: White Sign Soapstone - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: Wolf Knight's Greatsword - Ludleth for Abyss WatchersBoss weapon for Abyss Watchers
FS: Wolf Ring+2 - left of boss room exitAfter Iudex Gundyr on the left
FS: Wolnir's Crown - shop after killing CC bossSold by Handmaid after defeating High Lord Wolnir
FS: Wolnir's Holy Sword - Ludleth for WolnirBoss weapon for High Lord Wolnir
FS: Wood Grain Ring - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Xanthous Gloves - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Xanthous Overcoat - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Xanthous Trousers - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Yhorm's Great Machete - Ludleth for YhormBoss weapon for Yhorm the Giant
FS: Yhorm's Greatshield - Ludleth for YhormBoss weapon for Yhorm the Giant
FS: Young Dragon Ring - Orbeck for one scroll and buying three spellsGiven by Orbeck after purchasing four sorceries from him, and giving him one scroll, as a non-sorcerer.
FSBT: Armor of the Sun - crow for SiegbräuTrade Siegbräu with crow
FSBT: Blessed Gem - crow for Moaning ShieldTrade Moaning Shield with crow
FSBT: Covetous Silver Serpent Ring - illusory wall past raftersFrom the Firelink Shrine roof, past the rafters and an illusory wall
FSBT: Estus Ring - tower baseDropping down from the Bell Tower to where Irina eventually resides
FSBT: Estus Shard - raftersIn the Firelink Shrine rafters, accessible from the roof
FSBT: Fire Keeper Gloves - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Robe - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Skirt - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Soul - tower topAt the top of the Bell Tower
FSBT: Hello Carving - crow for Alluring SkullTrade Alluring Skull with crow
FSBT: Help me! Carving - crow for any sacred chimeTrade any Sacred Chime with crow
FSBT: Hollow Gem - crow for EleonoraTrade Eleonora with crow
FSBT: Homeward Bone - roofOn Firelink Shrine roof
FSBT: I'm sorry Carving - crow for Shriving StoneTrade Shriving Stone with crow
FSBT: Iron Bracelets - crow for Homeward BoneTrade Homeward Bone with crow
FSBT: Iron Helm - crow for Lightning UrnTrade Lightning Urn with crow
FSBT: Iron Leggings - crow for Seed of a Giant TreeTrade Seed of a Giant Tree with crow
FSBT: Large Titanite Shard - crow for FirebombTrade Firebomb or Rope Firebomb with crow
FSBT: Lightning Gem - crow for Xanthous CrownTrade Xanthous Crown with crow
FSBT: Lucatiel's Mask - crow for Vertebra ShackleTrade Vertebra Shackle with crow
FSBT: Porcine Shield - crow for Undead Bone ShardTrade Undead Bone Shard with crow
FSBT: Ring of Sacrifice - crow for Loretta's BoneTrade Loretta's Bone with crow
FSBT: Sunlight Shield - crow for Mendicant's StaffTrade Mendicant's Staff with crow
FSBT: Thank you Carving - crow for Hidden BlessingTrade Hidden Blessing with crow
FSBT: Titanite Chunk - crow for Black FirebombTrade Black Firebomb or Rope Black Firebomb with crow
FSBT: Titanite Scale - crow for Blacksmith HammerTrade Blacksmith Hammer with crow
FSBT: Titanite Slab - crow for Coiled Sword FragmentTrade Coiled Sword Fragment with crow
FSBT: Twinkling Titanite - crow for Large Leather ShieldTrade Large Leather Shield with crow
FSBT: Twinkling Titanite - crow for Prism StoneTrade Prism Stone with crow
FSBT: Twinkling Titanite - lizard behind FirelinkDropped by the Crystal Lizard behind Firelink Shrine. Can be accessed with tree jump by going all the way around the roof, left of the entrance to the rafters, or alternatively dropping down from the Bell Tower.
FSBT: Very good! Carving - crow for Divine BlessingTrade Divine Blessing with crow
GA: Avelyn - 1F, drop from 3F onto bookshelvesOn top of a bookshelf on the Archive first floor, accessible by going halfway up the stairs to the third floor, dropping down past the Grand Archives Scholar, and then dropping down again
GA: Blessed Gem - raftersOn the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area
GA: Chaos Gem - dark room, lizardDropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Cinders of a Lord - Lothric PrinceDropped by Twin Princes
GA: Crystal Chime - 1F, path from wax poolOn the Archives first floor, in the room with the Lothric Knight, to the right
GA: Crystal Gem - 1F, lizard by dropDropped by the Crystal Lizard on the Archives first floor along the left wall
GA: Crystal Scroll - 2F late, miniboss dropDropped by the Grand Archives Crystal Sage
GA: Divine Blessing - rafters, down lower level ladderIn a chest reachable after dropping down from the Archives rafters and down a ladder near the Corpse-grub
GA: Divine Pillars of Light - cage above raftersIn a cage above the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area
GA: Ember - 5F, by entranceOn a balcony high in the Archives overlooking the area with the Grand Archives Scholars with a shortcut ladder, on the opposite side from the wax pool
GA: Estus Shard - dome, far balconyOn the Archives roof near the three Winged Knights, in a side area overlooking the ocean.
GA: Fleshbite Ring - up stairs from 4FFrom the first shortcut elevator with the movable bookshelf, past the Scholars right before going outside onto the roof, in an alcove to the right with many Clawed Curse bookshelves
GA: Golden Wing Crest Shield - outside 5F, NPC dropDropped by Lion Knight Albert before the stairs leading up to Twin Princes
GA: Heavy Gem - rooftops, lizardDropped by one of the pair of Crystal Lizards, on the right side, found going up a slope past the gargoyle on the Archives roof
GA: Hollow Gem - rooftops lower, in hallGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, in a tunnel underneath the ledge
GA: Homeward Bone - 2F early balconyOn the Archives second floor, on the balcony with the ladder going up to the Crystal Sage
GA: Hunter's Ring - dome, very topAt the top of the ladder in roof the area with the Winged Knights
GA: Large Soul of a Crestfallen Knight - 4F, backIn the back of a Clawed Curse-heavy corridor of bookshelves, in the area with the Grand Archives Scholars and dropdown ladder, after the first shortcut elevator with the movable bookshelf
GA: Large Soul of a Crestfallen Knight - outside 5FIn the middle of the area with the three human NPCs attacking you, before the Grand Archives bonfire shortcut elevator
GA: Lingering Dragoncrest Ring+2 - dome, room behind spireNear the tower with the Winged Knights, up the stairs on the opposite side from the ladder leading up to the Hunter's Ring
GA: Onikiri and Ubadachi - outside 5F, NPC dropDropped by Black Hand Kamui before the stairs leading up to Twin Princes
GA: Outrider Knight Armor - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Gauntlets - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Helm - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Leggings - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Power Within - dark room, behind retractable bookshelfBehind a bookshelf in the dark room with the Crystal Lizards, moved by a lever in the same room
GA: Refined Gem - up stairs from 4F, lizardDropped by a Crystal Lizard found heading from the first elevator shortcut with the movable bookshelf, on the right side up the stairs before exiting to the roof
GA: Sage Ring+1 - rafters, second level downOn the rafters high above the Grand Archives, dropping down from the cage to the high rafters to the rafters below with the Corpse-grub
GA: Sage's Crystal Staff - outside 5F, NPC dropDropped by Daughter of Crystal Kriemhild before the stairs leading up to Twin Princes
GA: Scholar Ring - 2F, between late and earlyOn the corpse of a sitting Archives Scholar between two bookshelves, accessible by activating a lever before crossing the bridge that is the Crystal Sage's final location
GA: Sharp Gem - rooftops, lizardDropped by one of the pair of Crystal Lizards, on the left side, found going up a slope past the gargoyle on the Archives roof
GA: Shriving Stone - 2F late, by ladder from 3FGoing from the Crystal Sage's location on the third floor to its location on the bridge, after descending the ladder
GA: Soul Stream - 3F, behind illusory wallPast the Crystal Sage's third floor location, an illusory wall, and an Outrider Knight, on the corpse of a sitting Archives Scholar
GA: Soul of a Crestfallen Knight - 1F, loop left after dropOn the Archives first floor, hugging the left wall, on a ledge that loops back around to the left wall
GA: Soul of a Crestfallen Knight - path to domeOn balcony of the building with the second shortcut elevator down to the bonfire, accessible by going up the spiral stairs to the left
GA: Soul of a Nameless Soldier - dark roomOn the Archives first floor, after the wax pool, against a Clawed Curse bookshelf
GA: Soul of a Weary Warrior - rooftops, by lizardsOn the Archives roof, going up the first rooftop slope where a Gargoyle always attacks you
GA: Soul of the Twin PrincesDropped by Twin Princes
GA: Titanite Chunk - 1F, balconyOn the Archives first floor, on balcony overlooking the entrance opposite from the Grand Archives Scholars wax pool
GA: Titanite Chunk - 1F, path from wax poolOn the Archives first floor, toward the Lothric Knight, turning right to a ledge leading back to the entrance area
GA: Titanite Chunk - 1F, up right stairsGoing right after entering the Archives entrance and up the short flight of stairs
GA: Titanite Chunk - 2F, by wax poolUp the stairs from the Archives second floor on the right side from the entrance, in a corner near the small wax pool
GA: Titanite Chunk - 2F, right after dark roomExiting from the dark room with the Crystal Lizards on the first floor onto the second floor main room, then taking an immediate right
GA: Titanite Chunk - 5F, far balconyOn a balcony outside where Lothric Knight stands on the top floor of the Archives, accessing by going right from the final wax pool or by dropping down from the gargoyle area
GA: Titanite Chunk - rooftops lower, ledge by buttressGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, on a roof ledge to the right
GA: Titanite Chunk - rooftops, balconyGoing onto the roof and down the first ladder, all the way down the ledge facing the ocean to the right
GA: Titanite Chunk - rooftops, just before 5FOn the Archives roof, after a short dropdown, in the small area where the two Gargoyles attack you
GA: Titanite Scale - 1F, drop from 2F late onto bookshelves, lizardDropped by a Crystal Lizard on first floor bookshelves. Can be accessed by dropping down to the left at the end of the bridge which is the Crystal Sage's final location
GA: Titanite Scale - 1F, up stairs on bookshelfOn the Archives first floor, up a movable set of stairs near the large wax pool, on top of a bookshelf
GA: Titanite Scale - 2F, titanite scale atop bookshelfOn top of a bookshelf on the Archive second floor, accessible by going halfway up the stairs to the third floor and dropping down near a Grand Archives Scholar
GA: Titanite Scale - 3F, by ladder to 2F lateGoing from the Crystal Sage's location on the third floor to its location on the bridge, on the left side of the ladder you descend, behind a table
GA: Titanite Scale - 3F, corner up stairsFrom the Grand Archives third floor up past the thralls, in a corner with bookshelves to the left
GA: Titanite Scale - 4F, chest by exitIn a chest after the first elevator shortcut with the movable bookshelf, in the area with the Grand Archives Scholars, to the left of the stairwell leading up to the roof
GA: Titanite Scale - dark room, upstairsRight after going up the stairs to the Archives second floor, on the left guarded by a Grand Archives Scholar and a sequence of Clawed Curse bookshelves
GA: Titanite Scale - rooftops lower, path to 2FGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, then going past the corvians all the way to the left and making a jump
GA: Titanite Slab - 1F, after pulling 2F switchIn a chest on the Archives first floor, behind a bookshelf moved by pulling a lever in the middle of the second floor between two cursed bookshelves
GA: Titanite Slab - dome, kill all mobsDropped by killing all three Winged Knights on top of the Archives
GA: Titanite Slab - final elevator secretAt the bottom of the shortcut elevator right outside the Twin Princes fight. Requires sending the elevator up to the top from the middle, and then riding the lower elevator down.
GA: Twinkling Titanite - 1F, lizard by dropDropped by the Crystal Lizard on the Archives first floor along the left wall
GA: Twinkling Titanite - 2F, lizard by entranceDropped by the Crystal Lizard on the Archives second floor, going toward the stairs/balcony
GA: Twinkling Titanite - dark room, lizard #1Dropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Twinkling Titanite - dark room, lizard #2Dropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Twinkling Titanite - rafters, down lower level ladderIn a chest reachable after dropping down from the Archives rafters and down a ladder near the Corpse-grub
GA: Twinkling Titanite - rooftops, lizard #1Dropped by one of the pair of Crystal Lizards, on the right side, found going up a slope past the gargoyle on the Archives roof
GA: Twinkling Titanite - rooftops, lizard #2Dropped by one of the pair of Crystal Lizards, on the left side, found going up a slope past the gargoyle on the Archives roof
GA: Twinkling Titanite - up stairs from 4F, lizardDropped by a Crystal Lizard found heading from the first elevator shortcut with the movable bookshelf, on the right side up the stairs before exiting to the roof
GA: Undead Bone Shard - 5F, by entranceOn the corpse of a sitting Archives Scholar on a balcony high in the Archives overlooking the area with the Grand Archives Scholars with a shortcut ladder, near the final wax pool
GA: Witch's Locks - dark room, behind retractable bookshelfBehind a bookshelf in the dark room with the Crystal Lizards, moved by a lever in the same room
HWL: Astora Straight Sword - fort walkway, drop downIn the building with the Pus of Man on the roof, past the Lothric Knight down a hallway obscured by a wooden wheel, dropping down past the edge
HWL: Basin of Vows - EmmaDropped by Emma upon killing her. This is possible to do at any time
HWL: Battle Axe - flame tower, mimicDropped by mimic in the building guarded by the fire-breathing wyvern
HWL: Binoculars - corpse tower, upper platformIn the area with the dead wyvern, at the top of a set of stairs past a Hollow Soldier
HWL: Black Firebomb - small roof over fountainAfter roof with Pus of Man, on the edge of another rooftop to the left where you can drop down into Winged Knight area
HWL: Broadsword - fort, room off walkwayIn the building with the Pus of Man on the roof, past the Lothric Knight in an alcove to the left
HWL: Cell Key - fort ground, down stairsIn the basement of the building with Pus of Man on the roof, down the stairs guarded by a dog
HWL: Claymore - flame plazaIn the area where the wyvern breathes fire, farthest away from the door
HWL: Club - flame plazaIn the area where the wyvern breathes fire, in the open
HWL: Ember - back tower, transforming hollowDropped by the Pus of Man on the tower to the right of the High Wall bonfire after transformation
HWL: Ember - flame plazaIn the area where the wyvern breathes fire, in the open
HWL: Ember - fort roof, transforming hollowDropped by the Pus of Man on the roof after the Tower on the Wall bonfire after transformation
HWL: Ember - fountain #1In the area with the Winged Knight
HWL: Ember - fountain #2In the area with the Winged Knight
HWL: Estus Shard - fort ground, on anvilIn the basement of the building with the Pus of Man on the roof, on the blacksmith anvil
HWL: Firebomb - corpse tower, under tableIn the building near the dead wyvern, behind a table near the ladder you descend
HWL: Firebomb - fort roofNext to the Pus of Man on the roof
HWL: Firebomb - top of ladder to fountainBy the long ladder leading down to the area with the Winged Knight
HWL: Firebomb - wall tower, beamIn the building with the Tower on the Wall bonfire, on a wooden beam overhanging the lower levels
HWL: Fleshbite Ring+1 - fort roof, jump to other roofJumping from the roof with the Pus of Man to a nearby building with a fenced roof
HWL: Gold Pine Resin - corpse tower, dropDropping past the dead wyvern, down the left path from the High Wall bonfire
HWL: Green Blossom - fort walkway, hall behind wheelIn the building with the Pus of Man on the roof, past the Lothric Knight down a hallway obscured by a wooden wheel
HWL: Green Blossom - shortcut, lower courtyardIn the courtyard at the bottom of the shortcut elevator
HWL: Large Soul of a Deserted Corpse - flame plazaIn the area where the wyvern breathes fire, behind one of the praying statues
HWL: Large Soul of a Deserted Corpse - fort roofOn the edge of the roof with the Pus of Man
HWL: Large Soul of a Deserted Corpse - platform by fountainComing from the elevator shortcut, on a side path to the left (toward Winged Knight area)
HWL: Longbow - back towerDown the path from the right of the High Wall bonfire, where the Pus of Man and crossbowman are
HWL: Lucerne - promenade, side pathOn one of the side paths from the main path connecting Dancer and Vordt fights, patrolled by a Lothric Knight
HWL: Mail Breaker - wall tower, path to GreiratIn the basement of the building with the Tower on the Wall bonfire on the roof, before Greirat's cell
HWL: Rapier - fountain, cornerIn a corner in the area with the Winged Knight
HWL: Raw Gem - fort roof, lizardDropped by the Crystal Lizard on the rooftop after the Tower on the Wall bonfire
HWL: Red Eye Orb - wall tower, minibossDropped by the Darkwraith past the Lift Chamber Key
HWL: Refined Gem - promenade minibossDropped by the red-eyed Lothric Knight to the left of the Dancer's room entrance
HWL: Ring of Sacrifice - awning by fountainComing from the elevator shortcut, on a side path to the left (toward Winged Knight area), jumping onto a wooden support
HWL: Ring of the Evil Eye+2 - fort ground, far wallIn the basement of the building with the Pus of Man on the roof, on the far wall past the stairwell, behind some barrels
HWL: Silver Eagle Kite Shield - fort mezzanineIn the chest on the balcony overlooking the basement of the building with the Pus of Man on the roof
HWL: Small Lothric Banner - EmmaGiven by Emma, or dropped upon death
HWL: Soul of Boreal Valley VordtDropped by Vordt of the Boreal Valley
HWL: Soul of a Deserted Corpse - by wall tower doorRight before the entrance to the building with the Tower on the Wall bonfire
HWL: Soul of a Deserted Corpse - corpse tower, bottom floorDown the ladder of the building near the dead wyvern, on the way to the living wyvern
HWL: Soul of a Deserted Corpse - fort entry, cornerIn the corner of the room with a Lothric Knight, with the Pus of Man on the roof
HWL: Soul of a Deserted Corpse - fountain, path to promenadeIn between the Winged Knight area and the Dancer/Vordt corridor
HWL: Soul of a Deserted Corpse - path to back tower, by lift doorWhere the Greataxe Hollow Soldier patrols outside of the elevator shortcut entrance
HWL: Soul of a Deserted Corpse - path to corpse towerAt the very start, heading left from the High Wall bonfire
HWL: Soul of a Deserted Corpse - wall tower, right of exitExiting the building with the Tower on the Wall bonfire on the roof, immediately to the right
HWL: Soul of the DancerDropped by Dancer of the Boreal Valley
HWL: Standard Arrow - back towerDown the path from the right of the High Wall bonfire, where the Pus of Man and crossbowman are
HWL: Throwing Knife - shortcut, lift topAt the top of the elevator shortcut, opposite from the one-way door
HWL: Throwing Knife - wall tower, path to GreiratIn the basement of the building with the Tower on the Wall bonfire, in the room with the explosive barrels
HWL: Titanite Shard - back tower, transforming hollowDropped by the Pus of Man on the tower to the right of the High Wall bonfire after transformation
HWL: Titanite Shard - fort ground behind cratesBehind some wooden crates in the basement of the building with the Pus of Man on the roof
HWL: Titanite Shard - fort roof, transforming hollowDropped by the Pus of Man on the roof after the Tower on the Wall bonfire after transformation
HWL: Titanite Shard - fort, room off entryIn the building with the Pus of Man on the roof, in a room to the left and up the short stairs
HWL: Titanite Shard - wall tower, corner by bonfireOn the balcony with the Tower on the Wall bonfire
HWL: Undead Hunter Charm - fort, room off entry, in potIn the building with the Pus of Man on the roof, in a room to the left, in a pot you have to break
HWL: Way of Blue - EmmaGiven by Emma or dropped upon death.
IBV: Blood Gem - descent, platform before lakeIn front of the tree in the courtyard before going down the stairs to the lake leading to the Distant Manor bonfire
IBV: Blue Bug Pellet - ascent, in last buildingIn the final building before Pontiff's cathedral, coming from the sewer, on the first floor
IBV: Blue Bug Pellet - descent, dark roomIn the dark area with the Irithyllian slaves, to the left of the staircase
IBV: Budding Green Blossom - central, by second fountainNext to the fountain up the stairs from the Central Irithyll bonfire
IBV: Chloranthy Ring+1 - plaza, behind altarIn the area before and below Pontiff's cathedral, behind the central structure
IBV: Covetous Gold Serpent Ring+1 - descent, drop after dark roomAfter the dark area with the Irithyllian slaves, drop down to the right
IBV: Creighton's Steel Mask - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Divine Blessing - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Divine Blessing - great hall, mob dropOne-time drop from the Silver Knight staring at the painting in Irithyll
IBV: Dorhys' Gnawing - Dorhys dropDropped by Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches and to the left
IBV: Dragonslayer's Axe - Creighton dropFollowing Sirris' questline, dropped by Creighton the Wanderer when he invades in the graveyard after the Church of Yorshka.
IBV: Dung Pie - sewer #1In the area with the sewer centipedes
IBV: Dung Pie - sewer #2In the area with the sewer centipedes
IBV: Ember - shortcut from church to cathedralAfter the gate shortcut from Church of Yorshka to Pontiff's cathedral
IBV: Emit Force - SiegwardGiven by Siegward meeting him in the Irithyll kitchen after the Sewer Centipedes.
IBV: Excrement-covered Ashes - sewer, by stairsIn the area with the sewer centipedes, before going up the stairs to the kitchen
IBV: Fading Soul - descent, cliff edge #1In the graveyard down the stairs from the Church of Yorshka, at the cliff edge
IBV: Fading Soul - descent, cliff edge #2In the graveyard down the stairs from the Church of Yorshka, at the cliff edge
IBV: Great Heal - lake, dead Corpse-GrubOn the Corpse-grub at the edge of the lake leading to the Distant Manor bonfire
IBV: Green Blossom - lake wallOn the wall of the lake leading to the Distant Manor bonfire
IBV: Green Blossom - lake, by Distant ManorIn the lake close to the Distant Manor bonfire
IBV: Green Blossom - lake, by stairs from descentGoing down the stairs into the lake leading to the Distant Manor bonfire
IBV: Homeward Bone - descent, before gravestoneIn the graveyard down the stairs from the Church of Yorshka, in front of the grave with the Corvian
IBV: Kukri - descent, side pathDown the stairs from the graveyard after Church of Yorshka, before the group of dogs in the left path
IBV: Large Soul of a Nameless Soldier - ascent, after great hallBy the tree near the stairs from the sewer leading up to Pontiff's cathedral, where the first dogs attack you
IBV: Large Soul of a Nameless Soldier - central, by bonfireBy the Central Irithyll bonfire
IBV: Large Soul of a Nameless Soldier - central, by second fountainNext to the fountain up the stairs from the Central Irithyll bonfire
IBV: Large Soul of a Nameless Soldier - lake islandOn an island in the lake leading to the Distant Manor bonfire
IBV: Large Soul of a Nameless Soldier - path to plazaOn the path from Central Irithyll bonfire, before making the left toward Church of Yorshka
IBV: Large Titanite Shard - Distant Manor, under overhangUnder overhang next to second set of stairs leading from Distant Manor bonfire
IBV: Large Titanite Shard - ascent, by elevator doorOn the path from the sewer leading up to Pontiff's cathedral, to the right of the statue surrounded by dogs
IBV: Large Titanite Shard - ascent, down ladder in last buildingOutside the final building before Pontiff's cathedral, coming from the sewer, dropping down to the left before the entrance
IBV: Large Titanite Shard - central, balcony just before plazaFrom the Central Irithyll bonfire, on the balcony with the second Fire Witch.
IBV: Large Titanite Shard - central, side path after first fountainUp the stairs from the Central Irithyll bonfire, on a railing to the right
IBV: Large Titanite Shard - great hall, main floor mob dropOne-time drop from the Silver Knight staring at the painting in Irithyll
IBV: Large Titanite Shard - great hall, upstairs mob drop #1One-time drop from the Silver Knight on the balcony of the room with the painting
IBV: Large Titanite Shard - great hall, upstairs mob drop #2One-time drop from the Silver Knight on the balcony of the room with the painting
IBV: Large Titanite Shard - path to DorhysBefore the area with Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches
IBV: Large Titanite Shard - plaza, balcony overlooking ascentOn the path from Central Irithyll bonfire, instead of going left toward the Church of Yorshka, going right, on the balcony
IBV: Large Titanite Shard - plaza, by stairs to churchTo the left of the stairs leading up to the Church of Yorshka from Central Irithyll
IBV: Leo Ring - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Lightning Gem - plaza centerIn the area before and below Pontiff's cathedral, in the center guarded by the enemies
IBV: Magic Clutch Ring - plaza, illusory wallIn the area before and below Pontiff's cathedral, behind an illusory wall to the right
IBV: Mirrah Chain Gloves - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Mirrah Chain Leggings - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Mirrah Chain Mail - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Proof of a Concord Kept - Church of Yorshka altarAt the altar in the Church of Yorshka
IBV: Rime-blue Moss Clump - central, by bonfireBy the Central Irithyll bonfire
IBV: Rime-blue Moss Clump - central, past second fountainFrom the Central Irithyll bonfire, to the left before the first Fire Witch.
IBV: Ring of Sacrifice - lake, right of stairs from descentNear the sewer centipede at the start of the lake leading to the Distant Manor bonfire
IBV: Ring of the Evil Eye - AnriGiven by Anri of Astora in the Church of Yorshka, or if told of Horace's whereabouts in the Catacombs
IBV: Ring of the Sun's First Born - fall from in front of cathedralDropping down from in front of Pontiff Sulyvahn's church toward the Church of Yorshka
IBV: Roster of Knights - descent, first landingOn the landing going down the stairs from Church of Yorshka to the graveyard
IBV: Rusted Gold Coin - Distant Manor, drop after stairsDropping down after the first set of stairs leading from Distant Manor bonfire
IBV: Rusted Gold Coin - descent, side pathDown the stairs from the graveyard after Church of Yorshka, guarded by the group of dogs in the left path
IBV: Shriving Stone - descent, dark room raftersOn the rafters in the dark area with the Irithyllian slaves
IBV: Siegbräu - SiegwardGiven by Siegward meeting him in the Irithyll kitchen after the Sewer Centipedes.
IBV: Smough's Great Hammer - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Soul of Pontiff SulyvahnDropped by Pontiff Sulyvahn
IBV: Soul of a Weary Warrior - ascent, by final staircaseToward the end of the path from the sewer leading up to Pontiff's cathedral, to the left of the final staircase
IBV: Soul of a Weary Warrior - central, by first fountainBy the Central Irithyll bonfire
IBV: Soul of a Weary Warrior - central, railing by first fountainOn the railing overlooking the Central Irithyll bonfire, at the very start
IBV: Soul of a Weary Warrior - plaza, side room lowerDropping down from the path from Church of Yorshka to Pontiff, guarded by the pensive Fire Witch
IBV: Soul of a Weary Warrior - plaza, side room upperIn the path from Church of Yorshka to Pontiff's cathedral, at the broken ledge you can drop down onto the Fire Witch
IBV: Twinkling Titanite - central, lizard before plazaDropped by a Crystal Lizard past the Central Irithyll Fire Witches and to the left
IBV: Twinkling Titanite - descent, lizard behind illusory wallDropped by a Crystal Lizard behind an illusory wall before going down the stairs to the lake leading to the Distant Manor bonfire
IBV: Undead Bone Shard - descent, behind gravestoneIn the graveyard down the stairs from the Church of Yorshka, behind the grave with the Corvian
IBV: Witchtree Branch - by DorhysIn the area with Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches
IBV: Wood Grain Ring+2 - ascent, right after great hallLeaving the building with the Silver Knight staring at the painting, instead of going left up the stairs, go right
IBV: Yorshka's Spear - descent, dark room rafters chestIn a chest in the rafters of the dark area with the Irithyllian slaves
ID: Alva Armor - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Gauntlets - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Helm - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Leggings - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Bellowing Dragoncrest Ring - drop from B1 towards pitDropping down from the Jailbreaker's Key shortcut at the end of the top corridor on the bonfire side in Irithyll Dungeon
ID: Covetous Gold Serpent Ring - Siegward's cellIn the Old Cell where Siegward is rescued
ID: Covetous Silver Serpent Ring+1 - pit lift, middle platformOn one of the platforms in elevator shaft of the shortcut elevator from the Giant Slave area to the Irithyll Dungeon bonfire
ID: Dark Clutch Ring - stairs between pit and B3, mimicDropped by the mimic found going past the Giant Slave to the sewer with the rats and the basilisks, up the first flight of stairs, on the left side
ID: Dragon Torso Stone - B3, outside liftOn the balcony corpse in the Path of the Dragon pose
ID: Dragonslayer Lightning Arrow - pit, mimic in hallDropped by the mimic in the side corridor from where the Giant Slave is standing, before the long ladder
ID: Dung Pie - B3, by path from pitIn the room with the Giant Hound Rats
ID: Dung Pie - pit, miniboss dropDrop from the Giant Slave
ID: Dusk Crown Ring - B3 far, right cellIn the cell in the main Jailer cell block to the left of the Profaned Capital exit
ID: Ember - B3 centerAt the center pillar in the main Jailer cell block
ID: Ember - B3 far rightIn the main Jailer cell block, on the left side coming from the Profaned Capital
ID: Estus Shard - mimic on path from B2 to pitDropped by the mimic in the room after the outside area of Irithyll Dungeon overlooking Profaned Capital
ID: Fading Soul - B1 near, main hallOn the top corridor on the bonfire side in Irithyll Dungeon, close to the first Jailer
ID: Great Magic Shield - B2 near, mob drop in far left cellOne-time drop from the Infested Corpse in the bottom corridor on the bonfire side of Irithyll Dungeon, in the closest cell
ID: Homeward Bone - path from B2 to pitIn the part of Irithyll Dungeon overlooking the Profaned Capital, after exiting the last jail cell corridor
ID: Jailbreaker's Key - B1 far, cell after gateIn the cell of the top corridor opposite to the bonfire in Irithyll Dungeon
ID: Large Soul of a Nameless Soldier - B2 far, by liftTaking the elevator up from the area you can use Path of the Dragon, before the one-way door
ID: Large Soul of a Nameless Soldier - B2, hall by stairsAt the end of the bottom corridor on the bonfire side in Irithyll Dungeon
ID: Large Soul of a Weary Warrior - just before Profaned CapitalIn the open area before the bridge leading into Profaned Capital from Irithyll Dungeon
ID: Large Titanite Shard - B1 far, rightmost cellIn a cell on the far end of the top corridor opposite to the bonfire in Irithyll Dungeon, nearby the Jailer
ID: Large Titanite Shard - B1 near, by doorAt the end of the top corridor on the bonfire side in Irithyll Dungeon, before the Jailbreaker's Key door
ID: Large Titanite Shard - B3 near, right cornerIn the main Jailer cell block, to the left of the hallway leading to the Path of the Dragon area
ID: Large Titanite Shard - after bonfire, second cell on leftIn the second cell on the right after Irithyll Dungeon bonfire
ID: Large Titanite Shard - pit #1On the floor where the Giant Slave is standing
ID: Large Titanite Shard - pit #2On the floor where the Giant Slave is standing
ID: Lightning Blade - B3 lift, middle platformOn the middle platform riding the elevator up from the Path of the Dragon area
ID: Lightning Bolt - awning over pitOn the wooden overhangs above the Giant Slave. Can be reached by dropping down after climbing the long ladder around the area where the Giant stands.
ID: Murakumo - Alva dropDropped by Alva, Seeker of the Spurned when he invades in the cliffside path to Irithyll Dungeon
ID: Old Cell Key - stairs between pit and B3In a chest found going past the Giant Slave to the sewer with the rats and the basilisks, up the stairs to the end, on the right side
ID: Old Sorcerer Boots - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Coat - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Gauntlets - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Hat - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Pale Pine Resin - B1 far, cell with broken wallIn the jail cell with the broken wall in the top corridor opposite to the bonfire in Irithyll Dungeon, near the passive Wretch on the wall
ID: Pickaxe - path from pit to B3Passing by the Giant Slave, before the tunnel with the rats and basilisks
ID: Prisoner Chief's Ashes - B2 near, locked cell by stairsIn the cell at the far end of the bottom corridor on the bonfire side in Irithyll Dungeon
ID: Profaned Coal - B3 far, left cellIn the room with the Wretches next to the main Jailer cell block, guarded by a Wretch
ID: Profaned Flame - pitOn the floor where the Giant Slave is standing
ID: Rusted Coin - after bonfire, first cell on leftIn the first cell on the left from the Irithyll dungeon bonfire
ID: Rusted Gold Coin - after bonfire, last cell on rightIn the third cell on the right from the Irithyll Dungeon bonfire
ID: Simple Gem - B2 far, cell by stairsIn the cell near the bottom corridor opposite to the bonfire in Irithyll Dungeon, adjacent to the room with three Jailers and Cage Spiders
ID: Soul of a Crestfallen Knight - balcony above pitUnder whether the Giant Slave is resting his head
ID: Soul of a Weary Warrior - by drop to pitAt the end of the room with many peasant hollows after the Estus Shard mimic
ID: Soul of a Weary Warrior - stairs between pit and B3Going past the Giant Slave to the sewer with the rats and the basilisks, up the first flight of stairs
ID: Titanite Chunk - balcony above pit, lizardDropped by the Crystal Lizard where the Giant Slave is resting his head
ID: Titanite Chunk - pit, miniboss dropDrop from the Giant Slave
ID: Titanite Scale - B2 far, lizardDropped by the Crystal Lizard on the bottom corridor opposite from the bonfire in Irithyll Dungeon where a Wretch attacks you
ID: Titanite Scale - B3 far, mimic in hallDropped by the mimic in the main Jailer cell block
ID: Titanite Slab - SiegwardGiven by Siegward after unlocking Old Cell or on quest completion
ID: Xanthous Ashes - B3 far, right cellIn the cell in the main Jailer cell block to the left of the Profaned Capital exit
KFF: Soul of the LordsDropped by Soul of Cinder
LC: Black Firebomb - dark room lowerIn the room with the firebomb-throwing hollows, against the wall on the lowest level
LC: Braille Divine Tome of Lothric - wyvern roomIn the room next to the second Pus of Man wyvern
LC: Caitha's Chime - chapel, drop onto roofDropping down from the chapel balcony where the Red Tearstone Ring is found, and then dropping down again towards the Lothric knights
LC: Dark Stoneplate Ring+1 - wyvern room, balconyThrough the room next to the second Pus of Man wyvern, on the balcony outside
LC: Ember - by Dragon Barracks bonfireNear the Dragon Barracks bonfire
LC: Ember - dark room mid, pus of man mob dropDropped by the first Pus of Man wyvern
LC: Ember - main hall, left of stairsTo the left of the stairs past the Dragon Barracks grate
LC: Ember - plaza centerIn the area where the Pus of Man wyverns breathe fire
LC: Ember - plaza, by gateOn the railing near the area where the Pus of Man wyverns breathe fire, before the gate
LC: Ember - wyvern room, wyvern foot mob dropDropped by the second Pus of Man wyvern
LC: Gotthard Twinswords - by Grand Archives door, after PC and AL bossesBefore the door to the Grand Archives after Aldrich and Yhorm are killed
LC: Grand Archives Key - by Grand Archives door, after PC and AL bossesBefore the door to the Grand Archives after Aldrich and Yhorm are killed
LC: Greatlance - overlooking Dragon Barracks bonfireGuarded by a pensive Lothric Knight after the Dragon Barracks bonfire and continuing up the stairs
LC: Hood of PrayerIn a chest right after the Lothric Castle bonfire
LC: Irithyll Rapier - basement, miniboss dropDropped by the Boreal Outrider Knight in the basement
LC: Knight's Ring - altarClimbing the ladder to the rooftop outside the Dragonslayer Armour fight, past the Large Hollow Soldier, down into the room with the tables
LC: Large Soul of a Nameless Soldier - dark room midIn the room with the firebomb-throwing hollows, up the ladder
LC: Large Soul of a Nameless Soldier - moat, right pathFound on the ledge after dropping into the area with the Pus of Man transforming hollows and making the entire loop
LC: Large Soul of a Nameless Soldier - plaza left, by pillarIn the building to the left of the area where the Pus of Man wyverns breathe fire, against a pillar
LC: Large Soul of a Weary Warrior - ascent, last turretRather than going up the stairs to the Dragon Barracks bonfire, continue straight down the stairs and forwards
LC: Large Soul of a Weary Warrior - main hall, by leverOn a ledge to the right of the lever opening the grate
LC: Life Ring+2 - dark room mid, out door opposite wyvern, drop downPast the room with the firebomb-throwing hollows and Pus of Man wyvern, around to the front, dropping down past where the Titanite Chunk is
LC: Lightning Urn - moat, right path, first roomStarting the loop from where the Pus of Man hollows transform, behind some crates in the first room
LC: Lightning Urn - plazaIn the area where the Pus of Man wyverns breathe fire
LC: Pale Pine Resin - dark room upper, by mimicIn the room with the firebomb-throwing hollows, next to the mimic in the far back left
LC: Raw Gem - plaza leftOn a balcony to the left of the area where the Pus of Man wyverns breathe fire, where the Hollow Soldier throws Undead Hunter Charms
LC: Red Tearstone Ring - chapel, drop onto roofFrom the chapel to the right of the Dragonslayer Armour fight, on the balcony to the left
LC: Refined Gem - plazaIn the area where the Pus of Man wyverns breathe fire
LC: Robe of Prayer - ascent, chest at beginningIn a chest right after the Lothric Castle bonfire
LC: Rusted Coin - chapelIn the chapel to the right of the Dragonslayer Armour fight
LC: Sacred Bloom Shield - ascent, behind illusory wallUp the ladder where the Winged Knight is waiting, past an illusory wall
LC: Skirt of Prayer - ascent, chest at beginningIn a chest right after the Lothric Castle bonfire
LC: Sniper Bolt - moat, right path endHanging from the arch passed under on the way to the Dragon Barracks bonfire. Can be accessed by dropping into the area with the Pus of Man transforming hollows and making the entire loop, but going left at the end
LC: Sniper Crossbow - moat, right path endHanging from the arch passed under on the way to the Dragon Barracks bonfire. Can be accessed by dropping into the area with the Pus of Man transforming hollows and making the entire loop, but going left at the end
LC: Soul of Dragonslayer ArmourDropped by Dragonslayer Armour
LC: Soul of a Crestfallen Knight - by lift bottomGuarded by a buffed Lothric Knight straight from the Dancer bonfire
LC: Soul of a Crestfallen Knight - wyvern room, balconyOn a ledge accessible after the second Pus of Man wyvern is defeated
LC: Spirit Tree Crest Shield - basement, chestIn a chest in the basement with the Outrider Knight
LC: Sunlight Medal - by lift topNext to the shortcut elevator outside of the Dragonslayer Armour fight that goes down to the start of the area
LC: Sunlight Straight Sword - wyvern room, mimicDropped by the mimic in the room next to the second Pus of Man wyvern
LC: Thunder Stoneplate Ring+2 - chapel, drop onto roofDropping down from the chapel balcony where the Red Tearstone Ring is found, out on the edge
LC: Titanite Chunk - altar roofClimbing the ladder to the rooftop outside the Dragonslayer Armour fight, overlooking the tree
LC: Titanite Chunk - ascent, final turretRather than going up the stairs to the Dragon Barracks bonfire, continue straight down the stairs, then right
LC: Titanite Chunk - ascent, first balconyRight after the Lothric Castle bonfire, out on the balcony
LC: Titanite Chunk - ascent, turret before barricadesFrom the Lothric Castle bonfire, up the stairs, straight, and then down the stairs behind the barricade
LC: Titanite Chunk - dark room mid, out door opposite wyvernFrom the room with the firebomb-throwing hollows, past the Pus of Man Wyvern and back around the front, before the Crystal Lizard
LC: Titanite Chunk - dark room mid, pus of man mob dropDropped by the first Pus of Man wyvern
LC: Titanite Chunk - down stairs after bossDown the stairs to the right after Dragonslayer Armour
LC: Titanite Chunk - moat #1In the center of the area where the Pus of Man hollows transform
LC: Titanite Chunk - moat #2In the center of the area where the Pus of Man hollows transform
LC: Titanite Chunk - moat, near ledgeDropping down from the bridge where the Pus of Man wyverns breathe fire on the near side to the bonfire
LC: Titanite Chunk - wyvern room, wyvern foot mob dropDropped by the second Pus of Man wyvern
LC: Titanite Scale - altarIn a chest climbing the ladder to the rooftop outside the Dragonslayer Armour fight, continuing the loop past the Red-Eyed Lothric Knight
LC: Titanite Scale - basement, chestIn a chest in the basement with the Outrider Knight
LC: Titanite Scale - chapel, chestIn a chest in the chapel to the right of the Dragonslayer Armour fight
LC: Titanite Scale - dark room mid, out door opposite wyvernPassing through the room with the firebomb-throwing hollows and the Pus of Man wyvern around to the front, overlooking the area where the wyverns breathe fire
LC: Titanite Scale - dark room, upper balconyIn the room with the firebomb-throwing hollows, at the very top on a balcony to the right
LC: Titanite Scale - dark room, upper, mimicDropped by the crawling mimic at the top of the room with the firebomb-throwing hollows
LC: Twinkling Titanite - ascent, side roomIn the room where the Winged Knight drops down
LC: Twinkling Titanite - basement, chest #1In a chest in the basement with the Outrider Knight
LC: Twinkling Titanite - basement, chest #2In a chest in the basement with the Outrider Knight
LC: Twinkling Titanite - dark room mid, out door opposite wyvern, lizardDropped by the Crystal Lizard after the room with the firebomb-throwing hollows around the front
LC: Twinkling Titanite - moat, left sideBehind one of the Pus of Man transforming hollows, to the left of the bridge to the wyvern fire-breathing area
LC: Twinkling Titanite - moat, right path, lizardDropped by the Crystal Lizard near the thieves after dropping down to the area with the Pus of Man transforming hollows
LC: Undead Bone Shard - moat, far ledgeDropping down from the bridge where the Pus of Man wyverns breathe fire on the far side from the bonfire
LC: Winged Knight Armor - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Gauntlets - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Helm - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Leggings - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
PC: Blooming Purple Moss Clump - walkway above swampAt the right end of the plank before dropping down into the Profaned Capital toxic pool
PC: Cinders of a Lord - Yhorm the GiantDropped by Yhorm the Giant
PC: Court Sorcerer Gloves - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Hood - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Robe - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Trousers - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer's Staff - chapel, mimic on second floorDropped by the mimic on the second floor of the Monstrosity of Sin building
PC: Cursebite Ring - swamp, below hallsIn the inner cave of the Profaned Capital toxic pool
PC: Eleonora - chapel ground floor, kill mobDropped by the Monstrosity of Sin on the first floor, furthest away from the door
PC: Ember - palace, far roomTo the right of the Profaned Flame, in the room with the many Jailers looking at the mimics
PC: Flame Stoneplate Ring+1 - chapel, drop from roof towards entranceDropping down from the roof connected to the second floor of the Monstrosity of Sin building, above the main entrance to the building
PC: Greatshield of Glory - palace, mimic in far roomDropped by the left mimic surrounded by the Jailers to the right of the Profaned Flame
PC: Jailer's Key Ring - hall past chapelPast the Profaned Capital Court Sorcerer, in the corridor overlooking the Irithyll Dungeon Giant Slave area
PC: Large Soul of a Weary Warrior - bridge, far endOn the way from the Profaned Capital bonfire toward the Profaned Flame, crossing the bridge without dropping down
PC: Logan's Scroll - chapel roof, NPC dropDropped by the court sorcerer above the toxic pool
PC: Magic Stoneplate Ring+2 - tower baseAt the base of the Profaned Capital structure, going all the way around the outside wall clockwise
PC: Onislayer Greatarrow - bridgeItem on the bridge descending from the Profaned Capital bonfire into the Profaned Flame building
PC: Onislayer Greatbow - drop from bridgeFrom the bridge leading from the Profaned Capital bonfire to Yhorm, onto the ruined pillars shortcut to the right, behind you after the first dropdown.
PC: Pierce Shield - SiegwardDropped by Siegward upon death or quest completion, and sold by Patches while Siegward is in the well.
PC: Poison Arrow - chapel roofAt the far end of the roof with the Court Sorcerer
PC: Poison Gem - swamp, below hallsIn the inner cave of the Profaned Capital toxic pool
PC: Purging Stone - chapel ground floorAt the back of the room with the three Monstrosities of Sin on the first floor
PC: Purging Stone - swamp, by chapel ladderIn the middle of the Profaned Capital toxic pool, near the ladder to the Court Sorcerer
PC: Rubbish - chapel, down stairs from second floorHanging corpse visible from Profaned Capital accessible from the second floor of the building with the Monstrosities of Sin, in the back right
PC: Rusted Coin - below bridge #1Among the rubble before the steps leading up to the Profaned Flame
PC: Rusted Coin - below bridge #2Among the rubble before the steps leading up to the Profaned Flame
PC: Rusted Coin - tower exteriorTreasure visible on a ledge in the Profaned Capital bonfire. Can be accessed by climbing a ladder outside the main structure.
PC: Rusted Gold Coin - halls above swampIn the corridors leading to the Profaned Capital toxic pool
PC: Rusted Gold Coin - palace, mimic in far roomDropped by the right mimic surrounded by the Jailers to the right of the Profaned Flame
PC: Shriving Stone - swamp, by chapel doorAt the far end of the Profaned Capital toxic pool, to the left of the door leading to the Monstrosities of Sin
PC: Siegbräu - Siegward after killing bossGiven by Siegward after helping him defeat Yhorm the Giant. You must talk to him before Emma teleports you.
PC: Soul of Yhorm the GiantDropped by Yhorm the Giant
PC: Storm Ruler - SiegwardDropped by Siegward upon death or quest completion.
PC: Storm Ruler - boss roomTo the right of Yhorm's throne
PC: Twinkling Titanite - halls above swamp, lizard #1Dropped by the second Crystal Lizard in the corridors before the Profaned Capital toxic pool
PC: Twinkling Titanite - halls above swamp, lizard #2Dropped by the first Crystal Lizard in the corridors before the Profaned Capital toxic pool
PC: Undead Bone Shard - by bonfireOn the corpse of Laddersmith Gilligan next to the Profaned Capital bonfire
PC: Wrath of the Gods - chapel, drop from roofDropping down from the roof of the Monstrosity of Sin building where the Court Sorcerer is
PW1: Black Firebomb - snowfield lower, path to bonfireDropping down after the first snow overhang and following the wall on the left, past the rotting bed descending toward the second bonfire
PW1: Blessed Gem - snowfield, behind towerBehind the Millwood Knight tower in the first area, approach from the right side
PW1: Budding Green Blossom - settlement courtyard, ledgeAfter sliding down the slope on the way to Corvian Settlement, dropping down hugging the left wall
PW1: Captain's Ashes - snowfield tower, 6FAt the very top of the Millwood Knight tower after climbing up the second ladder
PW1: Chillbite Ring - FriedeGiven by Sister Friede while she is sitting in the Ariandel Chapel, or on the stool after she moves.
PW1: Contraption Key - library, NPC dropDropped by Sir Vilhelm
PW1: Crow Quills - settlement loop, jump into courtyardCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. Go right and jump past some barrels onto the central platform.
PW1: Crow Talons - settlement roofs, near bonfireAfter climbing the ladder onto Corvian Settlement rooftops, dropping down on a bridge to the left, into the building, then looping around onto its roof.
PW1: Dark Gem - settlement back, egg buildingDropping down to the right of the gate guarded by a Corvian Knight in Corvian Settlement, inside of the last building on the right
PW1: Ember - roots above depthsIn the tree branch area after climbing down the rope bridge, hugging a right wall past a Follower Javelin wielder
PW1: Ember - settlement main, left building after bridgeCrossing the bridge after Corvian Settlement bonfire, in the building to the left.
PW1: Ember - settlement, building near bonfireIn the first building in Corvian Settlement next to the bonfire building
PW1: Ethereal Oak Shield - snowfield tower, 3FIn the Millwood Knight tower on a Millwood Knight corpse, after climbing the first ladder, then going down the staircase
PW1: Follower Javelin - snowfield lower, path back upDropping down after the first snow overhang, follow the right wall around and up a slope, past the Followers
PW1: Follower Sabre - roots above depthsOn a tree branch after climbing down the rope bridge. Rather than hugging a right wall toward a Follower Javelin wielder, drop off to the left.
PW1: Frozen Weapon - snowfield lower, egg zoneDropping down after the first snow overhang, in the rotting bed along the left side
PW1: Heavy Gem - snowfield villageBefore the Millwood Knight tower, on the far side of one of the ruined walls targeted by the archer
PW1: Hollow Gem - beside chapelTo the right of the entrance to the Ariandel
PW1: Homeward Bone - depths, up hillIn the Depths of the Painting, up a hill next to the giant crabs.
PW1: Homeward Bone - snowfield village, outcroppingDropping down after the first snow overhang and following the cliff on the right, making a sharp right after a ruined wall segment before approaching the Millwood Knight tower
PW1: Large Soul of a Weary Warrior - settlement hall roofOn top of the chapel with the Corvian Knight to the left of Vilhelm's building
PW1: Large Soul of a Weary Warrior - snowfield tower, 6FAt the very top of the Millwood Knight tower after climbing up the second ladder, on a Millwood Knight corpse
PW1: Large Soul of an Unknown Traveler - below snowfield village overhangUp the slope to the left of the Millwood Knight tower, dropping down after a snow overhang, then several more ledges.
PW1: Large Soul of an Unknown Traveler - settlement backIn Corvian Settlement, on the ground before the ladder climbing onto the rooftops
PW1: Large Soul of an Unknown Traveler - settlement courtyard, cliffAfter sliding down the slope on the way to Corvian Settlement, on a cliff to the right and behind
PW1: Large Soul of an Unknown Traveler - settlement loop, by bonfireCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. On the corpse in a hole in the wall leading back to the bonfire.
PW1: Large Soul of an Unknown Traveler - settlement roofs, balconyAfter climbing the ladder onto Corvian Settlement rooftops, dropping down on a bridge to the left, on the other side of the bridge.
PW1: Large Soul of an Unknown Traveler - settlement, by ladder to bonfireTo the right of the ladder leading up to Corvian Settlement bonfire.
PW1: Large Soul of an Unknown Traveler - snowfield lower, by cliffDropping down after the first snow overhang, between the forest and the cliff edge, before where the large wolf drops down
PW1: Large Soul of an Unknown Traveler - snowfield lower, path back upDropping down after the first snow overhang, follow the right wall around and up a slope, past the Followers
PW1: Large Soul of an Unknown Traveler - snowfield lower, path to villageDropping down after the first snow overhang and following the cliff on the right, on a tree past where the large wolf jumps down
PW1: Large Soul of an Unknown Traveler - snowfield upperGoing straight after the first bonfire, to the left of the caving snow overhand
PW1: Large Titanite Shard - lizard under bridge nearDropped by a Crystal Lizard after the Rope Bridge Cave on the way to Corvian Settlement
PW1: Large Titanite Shard - settlement loop, lizardCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. Hug the bonfire building's outer wall along the right side.
PW1: Large Titanite Shard - snowfield lower, left from fallDropping down after the first snow overhang, guarded by a Tree Woman overlooking the rotting bed along the left wall
PW1: Millwood Battle Axe - snowfield tower, 5FIn the Milkwood Knight tower, either dropping down from rafters after climbing the second ladder or making a risky jump
PW1: Millwood Greatarrow - snowfield village, loop back to lowerDropping down after the first snow overhang and following the cliff on the right, making the full loop around, up the slope leading towards where the large wolf drops down
PW1: Millwood Greatbow - snowfield village, loop back to lowerDropping down after the first snow overhang and following the cliff on the right, making the full loop around, up the slope leading towards where the large wolf drops down
PW1: Onyx Blade - library, NPC dropDropped by Sir Vilhelm
PW1: Poison Gem - snowfield upper, forward from bonfireFollowing the left wall from the start, guarded by a Giant Fly
PW1: Rime-blue Moss Clump - below bridge farIn a small alcove to the right after climbing down the rope bridge
PW1: Rime-blue Moss Clump - snowfield upper, overhangOn the first snow overhang at the start. It drops down at the same time you do.
PW1: Rime-blue Moss Clump - snowfield upper, starting caveIn the starting cave
PW1: Rusted Coin - right of libraryTo the right of Vilhelm's building
PW1: Rusted Coin - snowfield lower, straight from fallDropping down after the first snow overhang, shortly straight ahead
PW1: Rusted Gold Coin - settlement roofs, roof near second ladderAfter climbing the second ladder on the Corvian Settlement rooftops, immediately dropping off the bridge to the right, on a rooftop
PW1: Shriving Stone - below bridge nearAfter the Rope Bridge Cave bonfire, dropping down before the bridge, following the ledge all the way to the right
PW1: Simple Gem - settlement, lowest level, behind gateCrossing the bridge after Corvian Settlement bonfire, follow the left edge until a bridge, then drop down on the right side. Guarded by a Sewer Centipede.
PW1: Slave Knight Armor - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Gauntlets - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Hood - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Leggings - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Snap Freeze - depths, far end, mob dropIn the Depths of the Painting, past the giant crabs, guarded by a special Tree Woman. Killing her drops down a very long nearby ladder.
PW1: Soul of a Crestfallen Knight - settlement hall, raftersIn the rafters of the chapel with the Corvian Knight to the left of Vilhelm's building. Can drop down from the windows exposed to the roof.
PW1: Soul of a Weary Warrior - snowfield tower, 1FAt the bottom of the Millwood Knight tower on a Millwood Knight corpse
PW1: Titanite Slab - CorvianGiven by the Corvian NPC in the building next to Corvian Settlement bonfire.
PW1: Titanite Slab - depths, up secret ladderIn the Depths of the Painting, past the giant crabs, killing a special Tree Woman drops down a very long nearby ladder. Climb the ladder and also the ladder after that one.
PW1: Twinkling Titanite - roots, lizardDropped by a Crystal Lizard in the tree branch area after climbing down the rope bridge, before the ledge with the Follower Javelin wielder
PW1: Twinkling Titanite - settlement roofs, lizard before hallDropped by a Crystal Lizard on a bridge in Corvian Settlement before the rooftop of the chapel with the Corvian Knight inside.
PW1: Twinkling Titanite - snowfield tower, 3F lizardDropped by a Crystal Lizard in the Millwood Knight tower, climbing up the first ladder and descending the stairs down
PW1: Valorheart - boss dropDropped by Champion's Gravetender
PW1: Way of White Corona - settlement hall, by altarIn the chapel with the Corvian Knight to the left of Vilhelm's building, in front of the altar.
PW1: Young White Branch - right of libraryTo the right of Vilhelm's building
PW2: Blood Gem - B2, centerOn the lower level of the Ariandel Chapel basement, in the middle
PW2: Dung Pie - B1On the higher level of the Ariandel Chapel basement, on a wooden beam overlooking the lower level
PW2: Earth Seeker - pit caveIn the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, in the cave
PW2: Ember - pass, central alcoveAfter the Snowy Mountain Pass bonfire, going left of the bell stuck in the ground, in a small alcove along the left wall
PW2: Floating Chaos - NPC dropDropped by Livid Pyromancer Dunnel when he invades while embered, whether boss is defeated or not. On the second level of Priscilla's building above the Gravetender fight, accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Follower Shield - pass, far cliffsideAfter the Snowy Mountain Pass bonfire, going left of the bell stuck in the ground, on the cliff ledge past the open area, to the left
PW2: Follower Torch - pass, far side pathOn the way to the Ariandel Chapel basement, where the first wolf enemies reappear, going all the way down the slope on the edge of the map. Guarded by a Follower
PW2: Homeward Bone - rotundaOn the second level of Priscilla's building above the Gravetender fight. Can be accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Large Soul of a Crestfallen Knight - pit, by treeIn the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, by the tree
PW2: Large Titanite Shard - pass, far side pathOn the way to the Ariandel Chapel basement, where the first wolf enemies reappear, going partway down the slope on the edge of the map
PW2: Large Titanite Shard - pass, just before B1On the way to Ariandel Chapel basement, past the Millwood Knights and before the first rotten tree that can be knocked down
PW2: Prism Stone - pass, tree by beginningUp the slope and to the left after the Snowy Mountain Pass, straight ahead by a tree
PW2: Pyromancer's Parting Flame - rotundaOn the second level of Priscilla's building above the Gravetender fight. Can be accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Quakestone Hammer - pass, side path near B1On the way to Ariandel Chapel basement, rather than going right past the two Millwood Knights, go left, guarded by a very strong Millwood Knight
PW2: Soul of Sister FriedeDropped by Sister Friede
PW2: Soul of a Crestfallen Knight - pit edge #1In the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, along the edge
PW2: Soul of a Crestfallen Knight - pit edge #2In the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, along the edge
PW2: Titanite Chunk - pass, by kickable treeAfter the Snowy Mountain Pass bonfire, on a ledge to the right of the slope with the bell stuck in the ground, behind a tree
PW2: Titanite Chunk - pass, cliff overlooking bonfireOn a cliff overlooking the Snowy Mountain Pass bonfire. Requires following the left wall
PW2: Titanite Slab - boss dropOne-time drop after killing Father Ariandel and Friede (phase 2) for the first time.
PW2: Twinkling Titanite - B3, lizard #1Dropped by a Crystal Lizard past an illusory wall nearly straight left of the mechanism that moves the statue in the lowest level of the Ariandel Chapel basement
PW2: Twinkling Titanite - B3, lizard #2Dropped by a Crystal Lizard past an illusory wall nearly straight left of the mechanism that moves the statue in the lowest level of the Ariandel Chapel basement
PW2: Vilhelm's Armor - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's Gauntlets - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's HelmOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's Leggings - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
RC: Antiquated Plain Garb - wall hidden, before bossIn the chapel before the Midir fight in the Ringed Inner Wall building.
RC: Black Witch Garb - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Hat - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Trousers - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Veil - swamp near right, by sunken churchTo the left of the submerged building with 4 Ringed Knights, near a spear-wielding knight.
RC: Black Witch Wrappings - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Blessed Gem - grave, down lowest stairsIn Shared Grave, after dropping down near Gael's flag and dropping down again, behind you. Or from the bonfire, go back through the side tunnel with the skeletons and down the stairs after that.
RC: Blindfold Mask - grave, NPC dropDropped by Moaning Knight (invades whether embered or not, or boss defeated or not) in Shared Grave.
RC: Blood of the Dark Soul - end boss dropDropped by Slave Knight Gael
RC: Budding Green Blossom - church pathOn the way to the Halflight building.
RC: Budding Green Blossom - wall top, flowers by stairsIn a patch of flowers to the right of the stairs leading up to the first Judicator along the left wall of the courtyard are Mausoleum Lookout.
RC: Budding Green Blossom - wall top, in flower clusterAlong the left wall of the courtyard after Mausoleum Lookout, in a patch of flowers.
RC: Chloranthy Ring+3 - wall hidden, drop onto statueFrom the mid level of the Ringed Inner Wall elevator that leads to the Midir fight, dropping back down toward the way to Filianore, onto a platform with a Gwyn statue. Try to land on the platform rather than the statue.
RC: Church Guardian Shiv - swamp far left, in buildingInside of the building at the remote end of the muck pit surrounded by praying Hollow Clerics.
RC: Covetous Gold Serpent Ring+3 - streets, by LappGoing up the very long ladder from the muck pit, then up some stairs, to the left, and across the bridge, in a building past the Ringed Knights. Also where Lapp can be found to tell him of the Purging Monument.
RC: Crucifix of the Mad King - ashes, NPC dropDropped by Shira, who invades you (ember not required) in the far-future version of her room
RC: Dark Gem - swamp near, by stairsIn the middle of the muck pit, close to the long stairs.
RC: Divine Blessing - streets monument, mob dropDropped by the Judicator near the Purging Monument area. Requires solving "Show Your Humanity" puzzle.
RC: Divine Blessing - wall top, mob dropDropped by the Judicator after the Mausoleum Lookup bonfire.
RC: Dragonhead Greatshield - lower cliff, under bridgeDown a slope to the right of the bridge where Midir first assaults you, past a sword-wielding Ringed Knight, under the bridge.
RC: Dragonhead Shield - streets monument, across bridgeFound in Purging Monument area, across the bridge from the monument. Requires solving "Show Your Humanity" puzzle.
RC: Ember - wall hidden, statue roomFrom the mid level of the Ringed Inner Wall elevator that leads to the Midir fight, in the room with the illusory statue.
RC: Ember - wall top, by statueAlong the left wall of the courtyard after Mausoleum Lookout, in front of a tall monument.
RC: Ember - wall upper, balconyOn the balcony attached to the room with the Ringed Inner Wall bonfire.
RC: Filianore's Spear Ornament - mid boss dropDropped by Halflight, Spear of the Church
RC: Filianore's Spear Ornament - wall hidden, by ladderNext the ladder leading down to the chapel before the Midir fight in the Ringed Inner Wall building.
RC: Havel's Ring+3 - streets high, drop from building oppositeDropping down from the building where Silver Knight Ledo invades. The building is up the very long ladder from the muck pit, down the path all the way to the right.
RC: Hidden Blessing - swamp center, mob dropDropped by Judicator patrolling the muck pit.
RC: Hidden Blessing - wall top, tomb under platformIn a tomb underneath the platform with the first Judicator, accessed by approaching from Mausoleum Lookout bonfire.
RC: Hollow Gem - wall upper, path to towerHeading down the cursed stairs after Ringed Inner Wall bonfire and another short flight of stairs, hanging on a balcony.
RC: Iron Dragonslayer Armor - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Gauntlets - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Helm - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Leggings - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Lapp's Armor - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Gauntlets - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Helm - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Leggings - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Large Soul of a Crestfallen Knight - streets monument, across bridgeFound in Purging Monument area, on the other side of the bridge leading to the monument. Requires solving "Show Your Humanity" puzzle.
RC: Large Soul of a Crestfallen Knight - streets, far stairsToward the bottom of the stairs leading down to the muck pit.
RC: Large Soul of a Weary Warrior - lower cliff, endToward the end of the upper path attacked Midir's fire-breathing.
RC: Large Soul of a Weary Warrior - swamp center, by peninsulaIn the muck pit approaching where the Judicator patrols from the stairs.
RC: Large Soul of a Weary Warrior - wall lower, past two illusory wallsIn the Ringed Inner Wall building coming from Shared Grave, past two illusory walls on the right side of the ascending stairs.
RC: Large Soul of a Weary Warrior - wall top, right of small tombIn the open toward the end of the courtyard after the Mausoleum Lookout bonfire, on the right side of the small tomb.
RC: Ledo's Great Hammer - streets high, opposite building, NPC dropDropped by Silver Knight Ledo (invades whether embered or not, or boss defeated or not) in the building down the path to the right after climbing the very long ladder from the muck area.
RC: Lightning Arrow - wall lower, past three illusory wallsIn the Ringed Inner Wall building coming from Shared Grave, past three illusory walls on the right side of the ascending stairs.
RC: Lightning Gem - grave, room after first dropIn Shared Grave, in the first room encountered after falling down from the crumbling stairs and continuing upward.
RC: Mossfruit - streets near left, path to gardenPartway down the stairs from Shira, across the bridge.
RC: Mossfruit - streets, far left alcoveNear the bottom of the stairs before the muck pit, in an alcove to the left.
RC: Preacher's Right Arm - swamp near right, by towerIn the muck pit behind a crystal-covered structure, close to the Ringed City Streets shortcut entrance.
RC: Prism Stone - swamp near, railing by bonfireOn the balcony of the path leading up to Ringed City Streets bonfire from the muck pit.
RC: Purging Stone - wall top, by door to upperAt the end of the path from Mausoleum Lookup to Ringed Inner Wall, just outside the door.
RC: Ring of the Evil Eye+3 - grave, mimicDropped by mimic in Shared Grave. In one of the rooms after dropping down near Gael's flag and then dropping down again.
RC: Ringed Knight Paired Greatswords - church path, mob dropDropped by Ringed Knight with paired greatswords before Filianore building.
RC: Ringed Knight Spear - streets, down far right hallIn a courtyard guarded by a spear-wielding Ringed Knight. Can be accessed from a hallway filled with cursed clerics on the right side going down the long stairs, or by climbing up the long ladder from the muck pit and dropping down past the Locust Preacher.
RC: Ringed Knight Straight Sword - swamp near, tower on peninsulaOn a monument next to the Ringed City Streets building. Can be easily accessed after unlocking the shortcut by following the left wall inside and then outside the building.
RC: Ritual Spear Fragment - church pathTo the right of the Paired Greatswords Ringed Knight on the way to Halflight.
RC: Rubbish - lower cliff, middleIn the middle of the upper path attacked Midir's fire-breathing, after the first alcove.
RC: Rubbish - swamp far, by crystalIn the remote end of the muck pit, next to a massive crystal structure between a giant tree and the building with praying Hollow Clerics, guarded by several Locust Preachers.
RC: Ruin Armor - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Gauntlets - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Helm - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Leggings - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Sacred Chime of Filianore - ashes, NPC dropGiven by Shira after accepting her request to kill Midir, or dropped by her in post-Filianore Ringed City.
RC: Shira's Armor - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Crown - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Gloves - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Trousers - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shriving Stone - wall tower, bottom floor centerIn the cylindrical building before the long stairs with many Harald Legion Knights, in the center structure on the first floor.
RC: Siegbräu - LappGiven by Lapp within the Ringed Inner Wall.
RC: Simple Gem - grave, up stairs after first dropIn Shared Grave, following the path after falling down from the crumbling stairs and continuing upward.
RC: Soul of Darkeater MidirDropped by Darkeater Midir
RC: Soul of Slave Knight GaelDropped by Slave Knight Gael
RC: Soul of a Crestfallen Knight - swamp far, behind crystalBehind a crystal structure at the far end of the muck pit, close to the building with the praying Hollow Clerics before Dragonslayer Armour.
RC: Soul of a Crestfallen Knight - swamp near left, nookIn the muck pit behind all of the Hollow Clerics near the very long ladder.
RC: Soul of a Crestfallen Knight - wall top, under dropAfter dropping down onto the side path on the right side of the Mausoleum Lookout courtyard to where the Crystal Lizard is, behind you.
RC: Soul of a Weary Warrior - lower cliff, by first alcoveIn front of the first alcove providing shelter from Midir's fire-breathing on the way to Shared Grave.
RC: Soul of a Weary Warrior - swamp centerIn the middle of the muck pit where the Judicator is patrolling.
RC: Soul of a Weary Warrior - swamp right, by sunken churchIn between where the Judicator patrols in the muck pit and the submerged building with the 4 Ringed Knights. Provides some shelter from his arrows.
RC: Spears of the Church - hidden boss dropDropped by Darkeater Midir
RC: Titanite Chunk - streets high, building oppositeDown a path past the room where Silver Knight Ledo invades. The building is up the very long ladder from the muck pit, down the path all the way to the right.
RC: Titanite Chunk - streets, near left dropNear the top of the stairs by Shira, dropping down in an alcove to the left.
RC: Titanite Chunk - swamp center, peninsula edgeAlong the edge of the muck pit close to where the Judicator patrols.
RC: Titanite Chunk - swamp far left, up hillUp a hill at the edge of the muck pit with the Hollow Clerics.
RC: Titanite Chunk - swamp near left, by spire topAt the edge of the muck pit, on the opposite side of the wall from the very long ladder.
RC: Titanite Chunk - swamp near right, behind rockAt the very edge of the muck pit, to the left of the submerged building with 4 Ringed Knights.
RC: Titanite Chunk - wall top, among gravesAlong the right edge of the courtyard after Mausoleum Lookout in a cluster of graves.
RC: Titanite Chunk - wall upper, courtyard alcoveIn the courtyard where the first Ringed Knight is seen, along the right wall into an alcove.
RC: Titanite Scale - grave, lizard past first dropDropped by the Crystal Lizard right after the crumbling stairs in Shared Grave.
RC: Titanite Scale - lower cliff, first alcoveIn the first alcove providing shelter from Midir's fire-breathing on the way to Shared Grave.
RC: Titanite Scale - lower cliff, lower pathAfter dropping down from the upper path attacked by Midir's fire-breathing to the lower path.
RC: Titanite Scale - lower cliff, path under bridgePartway down a slope to the right of the bridge where Midir first assaults you.
RC: Titanite Scale - swamp far, by minibossIn the area at the far end of the muck pit with the Dragonslayer Armour.
RC: Titanite Scale - swamp far, lagoon entranceIn the area at the far end of the muck pit with the Dragonslayer Armour.
RC: Titanite Scale - upper cliff, bridgeOn the final bridge where Midir attacks before you knock him off.
RC: Titanite Scale - wall lower, lizardDropped by the Crystal Lizard on the stairs going up from Shared Grave to Ringed Inner Wall elevator.
RC: Titanite Scale - wall top, behind spawnBehind you at the very start of the level.
RC: Titanite Slab - ashes, NPC dropGiven by Shira after defeating Midir, or dropped by her in post-Filianore Ringed City.
RC: Titanite Slab - ashes, mob dropDropped by the Ringed Knight wandering around near Gael's arena
RC: Titanite Slab - mid boss dropDropped by Halflight, Spear of the Church
RC: Twinkling Titanite - church path, left of boss doorDropping down to the left of the door leading to Halflight.
RC: Twinkling Titanite - grave, lizard past first dropDropped by the Crystal Lizard right after the crumbling stairs in Shared Grave.
RC: Twinkling Titanite - streets high, lizardDropped by the Crystal Lizard which runs across the bridge after climbing the very long ladder up from the muck pit.
RC: Twinkling Titanite - swamp near leftAt the left edge of the muck pit coming from the stairs, guarded by a Preacher Locust.
RC: Twinkling Titanite - swamp near right, on sunken churchFollowing the sloped roof of the submerged building with the 4 Ringed Knights, along the back wall
RC: Twinkling Titanite - wall top, lizard on side pathDropped by the first Crystal Lizard on the side path on the right side of the Mausoleum Lookout courtyard
RC: Twinkling Titanite - wall tower, jump from chandelierIn the cylindrical building before the long stairs with many Harald Legion Knights. Carefully drop down to the chandelier in the center, then jump to the second floor. The item is on a ledge.
RC: Violet Wrappings - wall hidden, before bossIn the chapel before the Midir fight in the Ringed Inner Wall building.
RC: White Birch Bow - swamp far left, up hillUp a hill at the edge of the muck pit with the Hollow Clerics.
RC: White Preacher Head - swamp near, nook right of stairsPast the balcony to the right of the Ringed City Streets bonfire room entrance. Can be accessed by dropping down straight after from the bonfire, then around to the left.
RC: Wolf Ring+3 - street gardens, NPC dropDropped by Alva (invades whether embered or not, or boss defeated or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Young White Branch - swamp far left, by white tree #1Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RC: Young White Branch - swamp far left, by white tree #2Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RC: Young White Branch - swamp far left, by white tree #3Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RS: Blue Bug Pellet - broken stairs by OrbeckOn the broken stairs leading down from Orbeck's area, on the opposite side from Orbeck
RS: Blue Sentinels - HoraceGiven by Horace the Hushed by first "talking" to him, or upon death.
RS: Braille Divine Tome of Carim - drop from bridge to Halfway FortressDropping down before the bridge leading up to Halfway Fortress from Road of Sacrifices, guarded by the maggot belly dog
RS: Brigand Armor - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Axe - beneath roadAt the start of the path leading down to the Madwoman in Road of Sacrifices
RS: Brigand Gauntlets - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Hood - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Trousers - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Twindaggers - beneath roadAt the end of the path guarded by the Madwoman in Road of Sacrifices
RS: Butcher Knife - NPC drop beneath roadDropped by the Butcher Knife-wielding madwoman near the start of Road of Sacrifices
RS: Chloranthy Ring+2 - road, drop across from carriageFound dropping down from the first Storyteller Corvian on the left side rather than the right side. You can then further drop down to where the madwoman is, after healing.
RS: Conjurator Boots - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Hood - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Manchettes - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Robe - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Crystal Gem - stronghold, lizardDropped by the Crystal Lizard in the building before Crystal Sage
RS: Ember - right of Halfway Fortress entranceOn the ledge with the Corvian with the Storyteller Staff, to the right of the Halfway Fortress entrance
RS: Ember - right of fire behind stronghold left roomBehind the building before Crystal Sage, approached from Crucifixion Woods bonfire. Can drop down on left side or go under bridge on right side
RS: Estus Shard - left of fire behind stronghold left roomBehind the building leading to Crystal Sage, approached from Crucifixion Woods bonfire. Can drop down on left side of go under bridge on right side
RS: Exile Greatsword - NPC drop by Farron KeepDropped by the greatsword-wielding Exile Knight before the ladder down to Farron Keep
RS: Fading Soul - woods by Crucifixion Woods bonfireDropping down from the Crucifixion Woods bonfire toward the Halfway Fortress, guarded by dogs
RS: Fallen Knight Armor - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Gauntlets - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Helm - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Trousers - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Farron Coal - keep perimeterAt the end of the Farron Keep Perimeter building on Crucifixion Woods side, behind the Black Knight
RS: Golden Falcon Shield - path from stronghold right room to Farron KeepHalfway up the stairs to the sorcerer in the building before Crystal Sage, entering from the stairs leading up from the crab area, go straight and follow the path down
RS: Grass Crest Shield - water by Crucifixion Woods bonfireDropping down into the crab area from Crucifixion Woods, on the other side of a tree from the greater crab
RS: Great Club - NPC drop by Farron KeepDropped by the club-wielding Exile Knight before the ladder down to Farron Keep
RS: Great Swamp Pyromancy Tome - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Great Swamp Ring - miniboss drop, by Farron KeepDropped by Greater Crab in Crucifixion Woods close to the Farron Keep outer wall
RS: Green Blossom - by deep waterIn the Crucifixion Woods crab area out in the open, close to the edge of the deep water area
RS: Green Blossom - water beneath strongholdIn the Crucifixion Woods crab area close to the Crucifixion Woods bonfire, along the left wall of the water area, to the right of the entrance to the building before Crystal Sage
RS: Heretic's Staff - stronghold left roomIn the building before Crystal Sage, entering from near Crucifixion Woods, in a corner under the first stairwell and balcony
RS: Heysel Pick - Heysel dropDropped by Heysel when she invades in Road of Sacrifices
RS: Homeward Bone - balcony by Farron KeepAt the far end of the building where you descend into Farron Keep, by the balcony
RS: Large Soul of an Unknown Traveler - left of stairs to Farron KeepIn the area before you descend into Farron Keep, before the stairs to the far left
RS: Lingering Dragoncrest Ring+1 - waterOn a tree by the greater crab near the Crucifixion Woods bonfire, after the Grass Crest Shield tree
RS: Morne's Ring - drop from bridge to Halfway FortressDropping down before the bridge leading up to Halfway Fortress from Road of Sacrifices, guarded by the maggot belly dog
RS: Ring of Sacrifice - stronghold, drop from right room balconyDrop down from the platform behind the sorcerer in the building before Crystal Sage, entering from the stairs leading up from the crab area
RS: Sage Ring - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sellsword Armor - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Gauntlet - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Helm - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Trousers - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Twinblades - keep perimeterIn the Farron Keep Perimeter building on Crucifixion Woods side, behind and to the right of the Black Knight
RS: Shriving Stone - road, by startDropping down to the left of the first Corvian enemy in Road of Sacrifices
RS: Sorcerer Gloves - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Hood - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Robe - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Trousers - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Soul of a Crystal SageDropped by Crystal Sage
RS: Soul of an Unknown Traveler - drop along wall from Halfway FortressFrom Halfway Fortress, hug the right wall and drop down twice on the way to the crab area
RS: Soul of an Unknown Traveler - right of door to stronghold leftOut in the open to the right of the building before Crystal Sage, as entered from Crucifixion Woods bonfire
RS: Soul of an Unknown Traveler - road, by wagonTo the right of the overturned wagon descending from the Road of Sacrifices bonfire
RS: Titanite Shard - road, on bridge after you go underCrossing the bridge you go under after the first Road of Sacrifices bonfire, after a sleeping Corvian and another Corvian guarding the pickup
RS: Titanite Shard - water by Halfway FortressDropping down into the Crucifixion Woods crab area right after Halfway Fortress, on the left wall heading toward the Black Knight building, guarded by dog
RS: Titanite Shard - woods, left of path from Halfway FortressHugging the left wall from Halfway Fortress to Crystal Sage, behind you after the first dropdown
RS: Titanite Shard - woods, surrounded by enemiesHugging the left wall from Halfway Fortress to the Crystal Sage bonfire, after a dropdown surrounded by seven Poisonhorn bugs
RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfireIn the middle of the area with the Poisonhorn bugs and Lycanthrope Hunters, following the wall where the bugs guard a Titanite Shard
RS: Xanthous Crown - Heysel dropDropped by Heysel when she invades in Road of Sacrifices
SL: Black Iron Greatshield - ruins basement, NPC dropDropped by Knight Slayer Tsorig in Smouldering Lake
SL: Black Knight Sword - ruins main lower, illusory wall in far hallOn the far exit of the Demon Ruins main hall, past an illusory wall, guarded by a Black Knight
SL: Bloodbite Ring+1 - behind ballistaBehind the ballista, overlooking Smouldering Lake
SL: Chaos Gem - antechamber, lizard at end of long hallDropped by the Crystal Lizard found from the Antechamber bonfire, toward the Demon Cleric and to the right, then all the way down
SL: Chaos Gem - lake, far end by mobIn Smouldering Lake along the wall underneath the ballista, all the way to the left past two crabs
SL: Dragonrider Bow - by ladder from ruins basement to ballistaAfter climbing up the ladder after the Black Knight in Demon Ruins, falling back down to a ledge
SL: Ember - ruins basement, in lavaIn the lava pit under the Black Knight, by Knight Slayer Tsorig
SL: Ember - ruins main lower, path to antechamberGoing down the stairs from the Antechamber bonfire, to the right, at the end of the short hallway to the next right
SL: Ember - ruins main upper, hall end by holeIn the Demon Ruins, hugging the right wall from the Demon Ruins bonfire, or making a jump from the illusory hall corridor from Antechamber bonfire
SL: Ember - ruins main upper, just after entranceBehind the first Demon Cleric from the Demon Ruins bonfire
SL: Estus Shard - antechamber, illusory wallBehind an illusory wall and Smouldering Writhing Flesh-filled corridor from Antechamber bonfire
SL: Flame Stoneplate Ring+2 - ruins main lower, illusory wall in far hallOn the far exit of the Demon Ruins main hall, past an illusory wall, past the Black Knight, hidden in a corner
SL: Fume Ultra Greatsword - ruins basement, NPC dropDropped by Knight Slayer Tsorig in Smouldering Lake
SL: Homeward Bone - path to ballistaIn the area targeted by the ballista after the long ladder guarded by the Black Knight, before the Bonewheel Skeletons
SL: Izalith Pyromancy Tome - antechamber, room near bonfireIn the room straight down from the Antechamber bonfire, past a Demon Cleric, surrounded by many Ghrus.
SL: Izalith Staff - ruins basement, second illusory wall behind chestPast an illusory wall to the left of the Large Hound Rat in Demon Ruins, and then past another illusory wall, before the basilisk area
SL: Knight Slayer's Ring - ruins basement, NPC dropDropped by Knight Slayer Tsorig after invading in the Catacombs
SL: Large Titanite Shard - lake, by entranceIn the middle of Smouldering Lake, close to the Abandoned Tomb
SL: Large Titanite Shard - lake, by minibossIn the middle of Smouldering Lake, under the Carthus Sandworm
SL: Large Titanite Shard - lake, by tree #1In the middle of Smouldering Lake, by a tree before the hallway to the pit
SL: Large Titanite Shard - lake, by tree #2In the middle of Smouldering Lake, by a tree before the hallway to the pit
SL: Large Titanite Shard - lake, straight from entranceIn the middle of Smouldering Lake, in between Abandoned Tomb and Demon Ruins
SL: Large Titanite Shard - ledge by Demon Ruins bonfireOn a corpse hanging off the ledge outside the Demon Ruins bonfire
SL: Large Titanite Shard - ruins basement, illusory wall in upper hallIn a chest past an illusory wall to the left of the Large Hound Rat in Demon Ruins, before the basilisk area
SL: Large Titanite Shard - side lake #1In the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
SL: Large Titanite Shard - side lake #2In the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
SL: Lightning Stake - lake, miniboss dropDropped by the giant Carthus Sandworm
SL: Llewellyn Shield - Horace dropDropped by Horace the Hushed upon death or quest completion.
SL: Quelana Pyromancy Tome - ruins main lower, illusory wall in grey roomAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall
SL: Sacred Flame - ruins basement, in lavaIn the lava pit under the Black Knight, by Knight Slayer Tsorig
SL: Shield of Want - lake, by minibossIn the middle of Smouldering Lake, under the Carthus Sandworm
SL: Soul of a Crestfallen Knight - ruins basement, above lavaNext to the Black Knight in Demon Ruins
SL: Soul of the Old Demon KingDropped by Old Demon King in Smouldering Lake
SL: Speckled Stoneplate Ring - lake, ballista breaks bricksBehind a destructible wall in Smouldering Lake which the ballista has to destroy
SL: Titanite Chunk - path to side lake, lizardDropped by the second Crystal Lizard in the cave leading to the pit where Horace can be found in Smouldering Lake
SL: Titanite Scale - ruins basement, path to lavaIn the area with Basilisks on the way to the ballista
SL: Toxic Mist - ruins main lower, in lavaAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall, in the middle of the lava pit.
SL: Twinkling Titanite - path to side lake, lizardDropped by the first Crystal Lizard in the cave leading to the pit where Horace can be found in Smouldering Lake
SL: Undead Bone Shard - lake, miniboss dropDropped by the giant Carthus Sandworm
SL: Undead Bone Shard - ruins main lower, left after stairsIn the close end of the Demon Ruins main hall, right below a Smouldering Writhing Flesh
SL: White Hair Talisman - ruins main lower, in lavaAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall, at the far end of the lava pit.
SL: Yellow Bug Pellet - side lakeIn the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
UG: Ashen Estus Ring - swamp, path opposite bonfireIn the coffin similar to your initial spawn location, guarded by Corvians
UG: Black Knight Glaive - boss arenaIn the Champion Gundyr boss area
UG: Blacksmith Hammer - shrine, Andre's roomWhere Andre sits in Firelink Shrine
UG: Chaos Blade - environs, left of shrineWhere Sword Master is in Firelink Shrine
UG: Coiled Sword Fragment - shrine, dead bonfireIn the dead Firelink Shrine bonfire
UG: Ember - shopSold by Untended Graves Handmaid
UG: Eyes of a Fire Keeper - shrine, Irina's roomBehind an illusory wall, in the same location Irina sits in Firelink Shrine
UG: Hidden Blessing - cemetery, behind coffinBehind the coffin that had a Titanite Shard in Cemetery of Ash
UG: Hornet Ring - environs, right of main path after killing FK bossOn a cliffside to the right of the main path leading up to dark Firelink Shrine, after Abyss Watchers is defeated.
UG: Life Ring+3 - shrine, behind big throneBehind Prince Lothric's throne
UG: Priestess Ring - shopSold or dropped by Untended Graves Handmaid. Killing her is not recommended
UG: Ring of Steel Protection+1 - environs, behind bell towerBehind Bell Tower to the right
UG: Shriving Stone - swamp, by bonfireAt the very start of the area
UG: Soul of Champion GundyrDropped by Champion Gundyr
UG: Soul of a Crestfallen Knight - environs, above shrine entranceAbove the Firelink Shrine entrance, up the stairs/slope from either left or right of the entrance
UG: Soul of a Crestfallen Knight - swamp, centerClose to where Ashen Estus Flask was in Cemetery of Ash
UG: Titanite Chunk - swamp, left path by fountainIn a path to the left of where Ashen Estus Flask was in Cemetery of Ash
UG: Titanite Chunk - swamp, right path by fountainIn a path to the right of where Ashen Estus Flask was in Cemetery of Ash
UG: Wolf Knight Armor - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Gauntlets - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Helm - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Leggings - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
US: Alluring Skull - foot, behind carriageGuarded by two dogs after the Foot of the High Wall bonfire
US: Alluring Skull - on the way to tower, behind buildingAfter the ravine bridge leading to Eygon and the Giant's tower, wrapping around the building to the right.
US: Alluring Skull - tower village building, upstairsUp the stairs of the building with Cage Spiders after the Fire Demon, before the dogs
US: Bloodbite Ring - miniboss in sewerDropped by the large rat in the sewers with grave access
US: Blue Wooden Shield - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Caduceus Round Shield - right after stable exitAfter exiting the building across the bridge to the right of the first Undead Settlement building, to the left
US: Caestus - sewerIn the tunnel with the Giant Hound Rat and Grave Key door, from the ravine bridge toward Dilapidated Bridge bonfire
US: Charcoal Pine Bundle - first building, bottom floorDown the stairs in the first building
US: Charcoal Pine Bundle - first building, middle floorOn the bottom floor of the first building
US: Charcoal Pine Resin - hanging corpse roomIn the building after the burning tree and Cathedral Evangelist, in the room with the many hanging corpses
US: Chloranthy Ring - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Cleric Blue Robe - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Gloves - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Hat - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Trousers - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cornyx's Garb - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Garb - kill CornyxDropped by Cornyx
US: Cornyx's Skirt - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Skirt - kill CornyxDropped by Cornyx
US: Cornyx's Wrap - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Wrap - kill CornyxDropped by Cornyx
US: Covetous Silver Serpent Ring+2 - tower village, drop down from roofAt the back of a roof near the end of the Fire Demon loop, dropping down past where Flynn's Ring is
US: Ember - behind burning treeBehind the burning tree with the Cathedral Evangelist
US: Ember - bridge on the way to towerOn the ravine bridge leading toward Eygon and the Giant's tower
US: Ember - by stairs to bossNext to the stairs leading up to Curse-Rotted Greatwood fight, near a tree guarded by a dog
US: Ember - by white treeNear the Birch Tree where giant shoots arrows
US: Ember - tower basement, minibossIn the room with the Outrider Knight
US: Estus Shard - under burning treeIn front of the burning tree guarded by the Cathedral Evangelist
US: Fading Soul - by white treeNear the Birch Tree where giant shoots arrows
US: Fading Soul - outside stableIn the thrall area to the right of the bridge to the right of the burning tree with the Cathedral Evangelist
US: Fire Clutch Ring - wooden walkway past stableFrom the area bombarded by firebombs above the Cliff Underside bonfire
US: Fire Gem - tower village, miniboss dropDropped by the Fire Demon you fight with Siegward
US: Firebomb - stable roofIn the thrall area across the bridge from the first Undead Settlement building, on a rooftop overlooking the Cliff Underside area.
US: Flame Stoneplate Ring - hanging corpse by Mound-Maker transportOn a hanging corpse in the area with the Pit of Hollows cage manservant, after the thrall area, overlooking the entrance to the Giant's tower.
US: Flynn's Ring - tower village, rooftopOn the roof toward the end of the Fire Demon loop, past the Cathedral Evangelists
US: Great Scythe - building by white tree, balconyOn the balcony of the building before Curse-Rotted Greatwood, coming from Dilapidated Bridge bonfire
US: Hand Axe - by CornyxNext to Cornyx's cell
US: Hawk Ring - Giant ArcherDropped by Giant, either by killing him or collecting all of the birch tree items locations in the base game.
US: Heavy Gem - HawkwoodGiven or dropped by Hawkwood after defeating Curse-Rotted Greatwood or Crystal Sage
US: Heavy Gem - chasm, lizardDrop by Crystal Lizard in ravine accessible by Grave Key or dropping down near Eygon.
US: Homeward Bone - foot, drop overlookUnder Foot of the High Wall bonfire, around where Yoel can be first met
US: Homeward Bone - stable roofIn the thrall area across the bridge from the first Undead Settlement building, on a roof overlooking the ravine bridge.
US: Homeward Bone - tower village, jump from roofAt the end of the loop from the Siegward Demon fight, after dropping down from the roof onto the tower with Chloranthy Ring, to the right of the tower entrance
US: Homeward Bone - tower village, right at startUnder Foot of the High Wall bonfire, around where Yoel can be first met
US: Human Pine Resin - tower village building, chest upstairsIn a chest after Fire Demon. Cage Spiders activate open opening it.
US: Irithyll Straight Sword - miniboss drop, by Road of SacrificesDropped by the Boreal Outright Knight before Road of Sacrifices
US: Kukri - hanging corpse above burning treeHanging corpse high above the burning tree with the Cathedral Evangelist. Must be shot down with an arrow or projective.
US: Large Club - tower village, by minibossIn the Fire Demon area
US: Large Soul of a Deserted Corpse - across from Foot of the High WallOn the opposite tower from the Foot of the High Wall bonfire
US: Large Soul of a Deserted Corpse - around corner by Cliff UndersideAfter going up the stairs from Curse-Rotted Greatwood to Cliff Underside area, on a cliff edge to the right
US: Large Soul of a Deserted Corpse - by white treeNear the Birch Tree where giant shoots arrows
US: Large Soul of a Deserted Corpse - hanging corpse room, over stairsOn a hanging corpse in the building after the burning tree. Can be knocked down by dropping onto the stairs through the broken railing.
US: Large Soul of a Deserted Corpse - on the way to tower, by wellAfter the ravine bridge leading toward Eygon and the Giant's tower, next to the well to the right
US: Large Soul of a Deserted Corpse - stableIn the building with stables across the bridge and to the right from the first Undead Settlement building
US: Life Ring+1 - tower on the way to villageOn the wooden rafters near where Siegward is waiting for Fire Demon
US: Loincloth - by Velka statueNext to the Velka statue. Requires Grave Key or dropping down near Eygon and backtracking through the skeleton area.
US: Loretta's Bone - first building, hanging corpse on balconyOn a hanging corpse after the first building, can be knocked down by rolling into it
US: Mirrah Gloves - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mirrah Trousers - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mirrah Vest - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mortician's Ashes - graveyard by white treeIn the area past the Dilapidated Bridge bonfire, where the Giant is shooting arrows, at the close end of the graveyard
US: Mound-makers - HodrickGiven by Hodrick if accessing the Pit of Hollows before fighting Curse-Rotted Greatwood, or dropped after invading him with Sirris.
US: Northern Armor - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Gloves - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Helm - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Trousers - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Old Sage's Blindfold - kill CornyxDropped by Cornyx
US: Pale Tongue - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Partizan - hanging corpse above Cliff UndersideOn a hanging corpse on the path from Cliff Underside to Cornyx's cage. Must be shot down with an arrow or projective.
US: Plank Shield - outside stable, by NPCIn the thrall area across the bridge from the first Undead Settlement building, on a cliff edge overlooking the ravine bridge.
US: Poisonbite Ring+1 - graveyard by white tree, near wellBehind the well in the back of area where the Giant shoots arrows, nearby where the flamberge-wielding thrall drops down.
US: Pyromancy Flame - CornyxGiven by Cornyx in Firelink Shrine or dropped.
US: Red Bug Pellet - tower village building, basementOn the floor of the building after the Fire Demon encounter
US: Red Hilted Halberd - chasm cryptIn the skeleton area accessible from Grave Key or dropping down from near Eygon
US: Red and White Shield - chasm, hanging corpseOn a hanging corpse in the ravine accessible with the Grave Key or dropping down near Eygon, to the entrance of Irina's prison. Must be shot down with an arrow or projective.
US: Reinforced Club - by white treeNear the Birch Tree where giant shoots arrows
US: Repair Powder - first building, balconyOn the balcony of the first Undead Settlement building
US: Rusted Coin - awning above Dilapidated BridgeOn a wooden ledge near the Dilapidated Bridge bonfire. Must be jumped to from near Cathedral Evangelist enemy
US: Saint's Talisman - chasm, by ladderFrom the ravine accessible via Grave Key or dropping near Eygon, before ladder leading up to Irina of Carim
US: Sharp Gem - lizard by Dilapidated BridgeDrop by Crystal Lizard near Dilapidated Bridge bonfire.
US: Siegbräu - SiegwardGiven by Siegward after helping him defeat the Fire Demon.
US: Small Leather Shield - first building, hanging corpse by entranceHanging corpse in the first building, to the right of the entrance
US: Soul of a Nameless Soldier - top of towerAt the top of the tower where Giant shoots arrows
US: Soul of an Unknown Traveler - back alley, past cratesAfter exiting the building after the burning tree on the way to the Dilapidated Bridge bonfire. Hidden behind some crates between two buildings on the right.
US: Soul of an Unknown Traveler - chasm cryptIn the skeleton area accessible Grave Key or dropping down from near Eygon
US: Soul of an Unknown Traveler - pillory past stableIn the area bombarded by firebombs above the Cliff Underside bonfire
US: Soul of an Unknown Traveler - portcullis by burning treeBehind a grate to the left of the burning tree and Cathedral Evangelist
US: Soul of the Rotted GreatwoodDropped by Curse Rotted Greatwood
US: Spotted Whip - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Sunset Armor - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Gauntlets - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Helm - Pit of Hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Leggings - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Titanite Shard - back alley, side pathOn a side path to the right of the Cathedral Evangelist before the Dilapidated Bridge bonfire
US: Titanite Shard - back alley, up ladderNext to the Cathedral Evangelist close to the Dilapidated Bridge bonfire
US: Titanite Shard - chasm #1In the ravine accessible from Grave Key or dropping down from near Eygon
US: Titanite Shard - chasm #2In the ravine accessible from Grave Key or dropping down from near Eygon
US: Titanite Shard - lower path to Cliff UndersideAt the end of the cliffside path next to Cliff Underside bonfire, guarded by a Hollow Peasant wielding a four-pronged plow.
US: Titanite Shard - porch after burning treeIn front of the building after the burning tree and Cathedral Evangelist
US: Tower Key - kill IrinaDropped by Irina of Carim
US: Transposing Kiln - boss dropDropped by Curse Rotted Greatwood
US: Undead Bone Shard - by white treeIn the area past the Dilapidated Bridge bonfire, where the Giant is shooting arrows, jumping to the floating platform on the right
US: Wargod Wooden Shield - Pit of HollowsIn the Pit of Hollows
US: Warrior of Sunlight - hanging corpse room, drop through holeDropping through a hole in the floor in the first building after the burning tree.
US: Whip - back alley, behind wooden wallIn one of the houses between building after the burning tree and the Dilapidated Bridge bonfire
US: Young White Branch - by white tree #1Near the Birch Tree where giant shoots arrows
US: Young White Branch - by white tree #2Near the Birch Tree where giant shoots arrows
+ diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index 61215dbc..484afdce 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -3,52 +3,73 @@ ## Required Software - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) -- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) +- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest) ## Optional Software -- [Dark Souls III Maptracker Pack](https://github.com/Br00ty/DS3_AP_Maptracker/releases/latest), for use with [Poptracker](https://github.com/black-sliver/PopTracker/releases) +- Map tracker not yet updated for 3.0.0 -## General Concept +## Setting Up - -**This mod can ban you permanently from the FromSoftware servers if used online.** - -The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command -prompt where you can read information about your run and write any command to interact with the Archipelago server. +First, download the client from the link above (`DS3.Archipelago.*.zip`). It doesn't need to go +into any particular directory; it'll automatically locate _Dark Souls III_ in your Steam +installation folder. -This client has only been tested with the Official Steam version of the game at version 1.15. It does not matter which DLCs are installed. However, you will have to downpatch your Dark Souls III installation from current patch. +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. You should also delete the `dinput8.dll` file if you still have +one from an older randomizer version. -## Downpatching Dark Souls III +### One-Time Setup -To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database. +Before you first connect to a multiworld, you need to generate the local data files for your world's +randomized item and (optionally) enemy locations. You only need to do this once per multiworld. -1. Launch Steam (in online mode). -2. Press the Windows Key + R. This will open the Run window. -3. Open the Steam console by typing the following string: `steam://open/console`. Steam should now open in Console Mode. -4. Insert the string of the depot you wish to download. For the AP-supported v1.15, you will want to use: `download_depot 374320 374321 4471176929659548333`. -5. Steam will now download the depot. Note: There is no progress bar for the download in Steam, but it is still downloading in the background. -6. Back up your existing game executable (`DarkSoulsIII.exe`) found in `\Steam\steamapps\common\DARK SOULS III\Game`. Easiest way to do this is to move it to another directory. If you have file extensions enabled, you can instead rename the executable to `DarkSoulsIII.exe.bak`. -7. Return to the Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like `\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX`. -8. Take the `DarkSoulsIII.exe` from that folder and place it in `\Steam\steamapps\common\DARK SOULS III\Game`. -9. Back up and delete your save file (`DS30000.sl2`) in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type `%appdata%` and hit enter. Alternatively: open File Explorer > View > Hidden Items and follow `C:\Users\\AppData\Roaming\DarkSoulsIII\`. -10. If you did all these steps correctly, you should be able to confirm your game version in the upper-left corner after launching Dark Souls III. +1. Before you first connect to a multiworld, run `randomizer\DS3Randomizer.exe`. +2. Put in your Archipelago room address (usually something like `archipelago.gg:12345`), your player + name (also known as your "slot name"), and your password if you have one. -## Installing the Archipelago mod +3. Click "Load" and wait a minute or two. -Get the `dinput8.dll` from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and -add it at the root folder of your game (e.g. `SteamLibrary\steamapps\common\DARK SOULS III\Game`) +### Running and Connecting the Game -## Joining a MultiWorld Game +To run _Dark Souls III_ in Archipelago mode: -1. Run Steam in offline mode to avoid being banned. -2. Launch Dark Souls III. -3. Type in `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME} password:{PASSWORD}` in the "Windows Command Prompt" that opened. For example: `/connect archipelago.gg:38281 "Example Name" password:"Example Password"`. The password parameter is only necessary if your game requires one. -4. Once connected, create a new game, choose a class and wait for the others before starting. -5. You can quit and launch at anytime during a game. +1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain + scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu + screen. -## Where do I get a config file? +2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that + you can use to interact with the Archipelago server. + +3. Type `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}` into the command prompt, with the + appropriate values filled in. For example: `/connect archipelago.gg:24242 PlayerName`. + +4. Start playing as normal. An "Archipelago connected" message will appear onscreen once you have + control of your character and the connection is established. + +## Frequently Asked Questions + +### Where do I get a config file? The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to -configure your personal options and export them into a config file. +configure your personal options and export them into a config file. The [AP client archive] also +includes an options template. + +[AP client archive]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest + +### Does this work with Proton? + +The *Dark Souls III* Archipelago randomizer supports running on Linux under Proton. There are a few +things to keep in mind: + +* Because `DS3Randomizer.exe` relies on the .NET runtime, you'll need to install + the [.NET Runtime] under **plain [WINE]**, then run `DS3Randomizer.exe` under + plain WINE as well. It won't work as a Proton app! + +* To run the game itself, just run `launchmod_darksouls3.bat` under Proton. + +[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0 +[WINE]: https://www.winehq.org/ diff --git a/worlds/dark_souls_3/test/TestDarkSouls3.py b/worlds/dark_souls_3/test/TestDarkSouls3.py new file mode 100644 index 00000000..7acdad46 --- /dev/null +++ b/worlds/dark_souls_3/test/TestDarkSouls3.py @@ -0,0 +1,27 @@ +from test.bases import WorldTestBase + +from worlds.dark_souls_3.Items import item_dictionary +from worlds.dark_souls_3.Locations import location_tables +from worlds.dark_souls_3.Bosses import all_bosses + +class DarkSouls3Test(WorldTestBase): + game = "Dark Souls III" + + def testLocationDefaultItems(self): + for locations in location_tables.values(): + for location in locations: + if location.default_item_name: + self.assertIn(location.default_item_name, item_dictionary) + + def testLocationsUnique(self): + names = set() + for locations in location_tables.values(): + for location in locations: + self.assertNotIn(location.name, names) + names.add(location.name) + + def testBossLocations(self): + all_locations = {location.name for locations in location_tables.values() for location in locations} + for boss in all_bosses: + for location in boss.locations: + self.assertIn(location, all_locations) diff --git a/worlds/kdl3/Names/__init__.py b/worlds/dark_souls_3/test/__init__.py similarity index 100% rename from worlds/kdl3/Names/__init__.py rename to worlds/dark_souls_3/test/__init__.py diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index 8e4a1bf2..40216e81 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -1,5 +1,4 @@ import logging -import asyncio from NetUtils import ClientStatus, color from worlds.AutoSNIClient import SNIClient @@ -32,7 +31,7 @@ class DKC3SNIClient(SNIClient): async def validate_rom(self, ctx): - from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + from SNIClient import snes_read rom_name = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:2] != b"D3": @@ -60,7 +59,7 @@ class DKC3SNIClient(SNIClient): return new_checks = [] - from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map + from .Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81) for loc_id, loc_data in location_rom_data.items(): if loc_id not in ctx.locations_checked: @@ -86,7 +85,7 @@ class DKC3SNIClient(SNIClient): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names.lookup_in_slot(new_check_id) + location = ctx.location_names.lookup_in_game(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -99,7 +98,7 @@ class DKC3SNIClient(SNIClient): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) diff --git a/worlds/dkc3/Items.py b/worlds/dkc3/Items.py index 358873cd..e6cac91e 100644 --- a/worlds/dkc3/Items.py +++ b/worlds/dkc3/Items.py @@ -1,6 +1,6 @@ import typing -from BaseClasses import Item, ItemClassification +from BaseClasses import Item from .Names import ItemName diff --git a/worlds/dkc3/Options.py b/worlds/dkc3/Options.py index b114a503..3f220bce 100644 --- a/worlds/dkc3/Options.py +++ b/worlds/dkc3/Options.py @@ -1,7 +1,6 @@ from dataclasses import dataclass -import typing -from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions +from Options import Choice, Range, Toggle, DefaultOnToggle, OptionGroup, PerGameCommonOptions class Goal(Choice): diff --git a/worlds/dkc3/Regions.py b/worlds/dkc3/Regions.py index ae505b78..6e968dbe 100644 --- a/worlds/dkc3/Regions.py +++ b/worlds/dkc3/Regions.py @@ -1,10 +1,9 @@ import typing -from BaseClasses import MultiWorld, Region, Entrance -from .Items import DKC3Item +from BaseClasses import Region, Entrance +from worlds.AutoWorld import World from .Locations import DKC3Location from .Names import LocationName, ItemName -from worlds.AutoWorld import World def create_regions(world: World, active_locations): diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 0dc722a7..fb8bc2b1 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -2,7 +2,6 @@ import Utils from Utils import read_snes_rom from worlds.AutoWorld import World from worlds.Files import APDeltaPatch -from .Locations import lookup_id_to_name, all_locations from .Levels import level_list, level_dict USHASH = '120abf304f0c40fe059f6a192ed4f947' @@ -436,7 +435,7 @@ level_music_ids = [ class LocalRom: - def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None): + def __init__(self, file, name=None, hash=None): self.name = name self.hash = hash self.orig_buffer = None diff --git a/worlds/dkc3/Rules.py b/worlds/dkc3/Rules.py index cc45e4ef..3d68aefb 100644 --- a/worlds/dkc3/Rules.py +++ b/worlds/dkc3/Rules.py @@ -1,8 +1,8 @@ import math +from worlds.AutoWorld import World +from worlds.generic.Rules import add_rule from .Names import LocationName, ItemName -from worlds.AutoWorld import LogicMixin, World -from worlds.generic.Rules import add_rule, set_rule def set_rules(world: World): diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index de6fb4a4..1dabeb05 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -1,15 +1,13 @@ import dataclasses -import os -import typing import math +import os import threading +import typing +import settings from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification from Options import PerGameCommonOptions -import Patch -import settings from worlds.AutoWorld import WebWorld, World - from .Client import DKC3SNIClient from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table from .Levels import level_list diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index a9dfcc50..37eae9b4 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -8,11 +8,15 @@ from .Locations import DLCQuestLocation, location_table from .Options import DLCQuestOptions from .Regions import create_regions from .Rules import set_rules +from .presets import dlcq_options_presets +from .option_groups import dlcq_option_groups client_version = 0 class DLCqwebworld(WebWorld): + options_presets = dlcq_options_presets + option_groups = dlcq_option_groups setup_en = Tutorial( "Multiworld Setup Guide", "A guide to setting up the Archipelago DLCQuest game on your computer.", @@ -68,8 +72,16 @@ class DLCqworld(World): self.multiworld.itempool += created_items - if self.options.campaign == Options.Campaign.option_basic or self.options.campaign == Options.Campaign.option_both: - self.multiworld.early_items[self.player]["Movement Pack"] = 1 + campaign = self.options.campaign + has_both = campaign == Options.Campaign.option_both + has_base = campaign == Options.Campaign.option_basic or has_both + has_big_bundles = self.options.coinsanity and self.options.coinbundlequantity > 50 + early_items = self.multiworld.early_items + if has_base: + if has_both and has_big_bundles: + early_items[self.player]["Incredibly Important Pack"] = 1 + else: + early_items[self.player]["Movement Pack"] = 1 for item in items_to_exclude: if item in self.multiworld.itempool: @@ -78,7 +90,7 @@ class DLCqworld(World): def precollect_coinsanity(self): if self.options.campaign == Options.Campaign.option_basic: if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5: - self.multiworld.push_precollected(self.create_item("Movement Pack")) + self.multiworld.push_precollected(self.create_item("DLC Quest: Coin Bundle")) def create_item(self, item: Union[str, ItemData], classification: ItemClassification = None) -> DLCQuestItem: if isinstance(item, str): diff --git a/worlds/dlcquest/option_groups.py b/worlds/dlcquest/option_groups.py new file mode 100644 index 00000000..9510c061 --- /dev/null +++ b/worlds/dlcquest/option_groups.py @@ -0,0 +1,27 @@ +from typing import List + +from Options import ProgressionBalancing, Accessibility, OptionGroup +from .Options import (Campaign, ItemShuffle, TimeIsMoney, EndingChoice, PermanentCoins, DoubleJumpGlitch, CoinSanity, + CoinSanityRange, DeathLink) + +dlcq_option_groups: List[OptionGroup] = [ + OptionGroup("General", [ + Campaign, + ItemShuffle, + CoinSanity, + ]), + OptionGroup("Customization", [ + EndingChoice, + PermanentCoins, + CoinSanityRange, + ]), + OptionGroup("Tedious and Grind", [ + TimeIsMoney, + DoubleJumpGlitch, + ]), + OptionGroup("Advanced Options", [ + DeathLink, + ProgressionBalancing, + Accessibility, + ]), +] diff --git a/worlds/dlcquest/presets.py b/worlds/dlcquest/presets.py new file mode 100644 index 00000000..ccfd7939 --- /dev/null +++ b/worlds/dlcquest/presets.py @@ -0,0 +1,68 @@ +from typing import Any, Dict + +from .Options import DoubleJumpGlitch, CoinSanity, CoinSanityRange, PermanentCoins, TimeIsMoney, EndingChoice, Campaign, ItemShuffle + +all_random_settings = { + DoubleJumpGlitch.internal_name: "random", + CoinSanity.internal_name: "random", + CoinSanityRange.internal_name: "random", + PermanentCoins.internal_name: "random", + TimeIsMoney.internal_name: "random", + EndingChoice.internal_name: "random", + Campaign.internal_name: "random", + ItemShuffle.internal_name: "random", + "death_link": "random", +} + +main_campaign_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_basic, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +lfod_campaign_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_live_freemium_or_die, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +easy_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_none, + CoinSanityRange.internal_name: 40, + PermanentCoins.internal_name: PermanentCoins.option_true, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_both, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +hard_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_simple, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_optional, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_both, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + + +dlcq_options_presets: Dict[str, Dict[str, Any]] = { + "All random": all_random_settings, + "Main campaign": main_campaign_settings, + "LFOD campaign": lfod_campaign_settings, + "Both easy": easy_settings, + "Both hard": hard_settings, +} diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py index 3e9acac7..c6c594b6 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 8a39b43a..0432ae8b 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 cc2fa7f5..48c919c0 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/doom_1993/Locations.py b/worlds/doom_1993/Locations.py index 2cbb9b9d..90a6916c 100644 --- a/worlds/doom_1993/Locations.py +++ b/worlds/doom_1993/Locations.py @@ -2214,13 +2214,13 @@ location_table: Dict[int, LocationDict] = { 'map': 2, 'index': 217, 'doom_type': 2006, - 'region': "Perfect Hatred (E4M2) Blue"}, + 'region': "Perfect Hatred (E4M2) Upper"}, 351367: {'name': 'Perfect Hatred (E4M2) - Exit', 'episode': 4, 'map': 2, 'index': -1, 'doom_type': -1, - 'region': "Perfect Hatred (E4M2) Blue"}, + 'region': "Perfect Hatred (E4M2) Upper"}, 351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability', 'episode': 4, 'map': 3, diff --git a/worlds/doom_1993/Options.py b/worlds/doom_1993/Options.py index f65952d3..c9c61110 100644 --- a/worlds/doom_1993/Options.py +++ b/worlds/doom_1993/Options.py @@ -16,9 +16,9 @@ class Goal(Choice): class Difficulty(Choice): """ - Choose the difficulty option. Those match DOOM's difficulty options. - baby (I'm too young to die.) double ammos, half damage, less monsters or strength. - easy (Hey, not too rough.) less monsters or strength. + Choose the game difficulty. These options match DOOM's skill levels. + baby (I'm too young to die.) Same as easy, with double ammo pickups and half damage taken. + easy (Hey, not too rough.) Less monsters or strength. medium (Hurt me plenty.) Default. hard (Ultra-Violence.) More monsters or strength. nightmare (Nightmare!) Monsters attack more rapidly and respawn. @@ -29,6 +29,11 @@ class Difficulty(Choice): option_medium = 2 option_hard = 3 option_nightmare = 4 + alias_itytd = 0 + alias_hntr = 1 + alias_hmp = 2 + alias_uv = 3 + alias_nm = 4 default = 2 @@ -112,7 +117,7 @@ class StartWithComputerAreaMaps(Toggle): class ResetLevelOnDeath(DefaultOnToggle): """When dying, levels are reset and monsters respawned. But inventory and checks are kept. Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" - display_name="Reset Level on Death" + display_name = "Reset Level on Death" class Episode1(DefaultOnToggle): diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py index f013bdce..c32f7b47 100644 --- a/worlds/doom_1993/Regions.py +++ b/worlds/doom_1993/Regions.py @@ -502,13 +502,12 @@ regions:List[RegionDict] = [ "episode":4, "connections":[ {"target":"Perfect Hatred (E4M2) Blue","pro":False}, - {"target":"Perfect Hatred (E4M2) Yellow","pro":False}]}, + {"target":"Perfect Hatred (E4M2) Yellow","pro":False}, + {"target":"Perfect Hatred (E4M2) Upper","pro":True}]}, {"name":"Perfect Hatred (E4M2) Blue", "connects_to_hub":False, "episode":4, - "connections":[ - {"target":"Perfect Hatred (E4M2) Main","pro":False}, - {"target":"Perfect Hatred (E4M2) Cave","pro":False}]}, + "connections":[{"target":"Perfect Hatred (E4M2) Upper","pro":False}]}, {"name":"Perfect Hatred (E4M2) Yellow", "connects_to_hub":False, "episode":4, @@ -518,7 +517,13 @@ regions:List[RegionDict] = [ {"name":"Perfect Hatred (E4M2) Cave", "connects_to_hub":False, "episode":4, - "connections":[]}, + "connections":[{"target":"Perfect Hatred (E4M2) Main","pro":False}]}, + {"name":"Perfect Hatred (E4M2) Upper", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Perfect Hatred (E4M2) Cave","pro":False}, + {"target":"Perfect Hatred (E4M2) Main","pro":False}]}, # Sever the Wicked (E4M3) {"name":"Sever the Wicked (E4M3) Main", diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py index 4faeb4a2..89b09ff9 100644 --- a/worlds/doom_1993/Rules.py +++ b/worlds/doom_1993/Rules.py @@ -403,9 +403,8 @@ def set_episode4_rules(player, multiworld, pro): state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or state.has("BFG9000", player, 1))) set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state: - state.has("Shotgun", player, 1) or - state.has("Chaingun", player, 1) or - state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) + (state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) and (state.has("Shotgun", player, 1) or + state.has("Chaingun", player, 1))) # Perfect Hatred (E4M2) set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state: diff --git a/worlds/doom_1993/docs/setup_en.md b/worlds/doom_1993/docs/setup_en.md index 8906efac..85061609 100644 --- a/worlds/doom_1993/docs/setup_en.md +++ b/worlds/doom_1993/docs/setup_en.md @@ -2,7 +2,7 @@ ## Required Software -- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM_1993/) +- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/) - [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) ## Optional Software @@ -17,7 +17,7 @@ You can find the folder in steam by finding the game in your library, right-clicking it and choosing **Manage -> Browse Local Files**. The WAD file is in the `/base/` folder. -## Joining a MultiWorld Game +## Joining a MultiWorld Game (via Launcher) 1. Launch apdoom-launcher.exe 2. Select `Ultimate DOOM` from the drop-down @@ -28,6 +28,24 @@ To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. +## Joining a MultiWorld Game (via command line) + +1. In your command line, navigate to the directory where APDOOM is installed. +2. Run `crispy-apdoom -game doom -apserver -applayer `, where: + - `` is the Archipelago server address, e.g. "`archipelago.gg:38281`" + - `` is your slot name; if it contains spaces, surround it with double quotes + - If the server has a password, add `-password`, followed by the server password +3. Enjoy! + +Optionally, you can override some randomization settings from the command line: +- `-apmonsterrando 0` will disable monster rando. +- `-apitemrando 0` will disable item rando. +- `-apmusicrando 0` will disable music rando. +- `-apfliplevels 0` will disable flipping levels. +- `-apresetlevelondeath 0` will disable resetting the level on death. +- `-apdeathlinkoff` will force DeathLink off if it's enabled. +- `-skill <1-5>` changes the game difficulty, from 1 (I'm too young to die) to 5 (Nightmare!) + ## Archipelago Text Client We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. diff --git a/worlds/doom_ii/Locations.py b/worlds/doom_ii/Locations.py index 3ce87b8a..376f1944 100644 --- a/worlds/doom_ii/Locations.py +++ b/worlds/doom_ii/Locations.py @@ -1470,7 +1470,7 @@ location_table: Dict[int, LocationDict] = { 'map': 6, 'index': 102, 'doom_type': 2006, - 'region': "Tenements (MAP17) Main"}, + 'region': "Tenements (MAP17) Yellow"}, 361243: {'name': 'Tenements (MAP17) - Plasma gun', 'episode': 2, 'map': 6, diff --git a/worlds/doom_ii/Options.py b/worlds/doom_ii/Options.py index cc39512a..98c8ebc5 100644 --- a/worlds/doom_ii/Options.py +++ b/worlds/doom_ii/Options.py @@ -6,9 +6,9 @@ from dataclasses import dataclass class Difficulty(Choice): """ - Choose the difficulty option. Those match DOOM's difficulty options. - baby (I'm too young to die.) double ammos, half damage, less monsters or strength. - easy (Hey, not too rough.) less monsters or strength. + Choose the game difficulty. These options match DOOM's skill levels. + baby (I'm too young to die.) Same as easy, with double ammo pickups and half damage taken. + easy (Hey, not too rough.) Less monsters or strength. medium (Hurt me plenty.) Default. hard (Ultra-Violence.) More monsters or strength. nightmare (Nightmare!) Monsters attack more rapidly and respawn. @@ -19,6 +19,11 @@ class Difficulty(Choice): option_medium = 2 option_hard = 3 option_nightmare = 4 + alias_itytd = 0 + alias_hntr = 1 + alias_hmp = 2 + alias_uv = 3 + alias_nm = 4 default = 2 @@ -102,7 +107,7 @@ class StartWithComputerAreaMaps(Toggle): class ResetLevelOnDeath(DefaultOnToggle): """When dying, levels are reset and monsters respawned. But inventory and checks are kept. Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" - display_message="Reset level on death" + display_name = "Reset Level on Death" class Episode1(DefaultOnToggle): diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py index 38840f55..32c3cbd5 100644 --- a/worlds/doom_ii/__init__.py +++ b/worlds/doom_ii/__init__.py @@ -60,17 +60,18 @@ class DOOM2World(World): # Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1. # The ratio have been tweaked seem, and feel good. items_ratio: Dict[str, float] = { - "Armor": 41, - "Mega Armor": 25, - "Berserk": 12, + "Armor": 39, + "Mega Armor": 23, + "Berserk": 11, "Invulnerability": 10, "Partial invisibility": 18, - "Supercharge": 28, + "Supercharge": 26, "Medikit": 15, "Box of bullets": 13, "Box of rockets": 13, "Box of shotgun shells": 13, - "Energy cell pack": 10 + "Energy cell pack": 10, + "Megasphere": 7 } def __init__(self, multiworld: MultiWorld, player: int): @@ -233,6 +234,7 @@ class DOOM2World(World): self.create_ratioed_items("Invulnerability", itempool) self.create_ratioed_items("Partial invisibility", itempool) self.create_ratioed_items("Supercharge", itempool) + self.create_ratioed_items("Megasphere", itempool) while len(itempool) < self.location_count: itempool.append(self.create_item(self.get_filler_item_name())) diff --git a/worlds/doom_ii/docs/setup_en.md b/worlds/doom_ii/docs/setup_en.md index 87054ab3..e444f85b 100644 --- a/worlds/doom_ii/docs/setup_en.md +++ b/worlds/doom_ii/docs/setup_en.md @@ -2,7 +2,7 @@ ## Required Software -- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2300/DOOM_II/) +- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/) - [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) ## Optional Software @@ -15,7 +15,7 @@ You can find the folder in steam by finding the game in your library, right clicking it and choosing *Manage→Browse Local Files*. The WAD file is in the `/base/` folder. -## Joining a MultiWorld Game +## Joining a MultiWorld Game (via Launcher) 1. Launch apdoom-launcher.exe 2. Select `DOOM II` from the drop-down @@ -26,6 +26,24 @@ To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. +## Joining a MultiWorld Game (via command line) + +1. In your command line, navigate to the directory where APDOOM is installed. +2. Run `crispy-apdoom -game doom2 -apserver -applayer `, where: + - `` is the Archipelago server address, e.g. "`archipelago.gg:38281`" + - `` is your slot name; if it contains spaces, surround it with double quotes + - If the server has a password, add `-password`, followed by the server password +3. Enjoy! + +Optionally, you can override some randomization settings from the command line: +- `-apmonsterrando 0` will disable monster rando. +- `-apitemrando 0` will disable item rando. +- `-apmusicrando 0` will disable music rando. +- `-apfliplevels 0` will disable flipping levels. +- `-apresetlevelondeath 0` will disable resetting the level on death. +- `-apdeathlinkoff` will force DeathLink off if it's enabled. +- `-skill <1-5>` changes the game difficulty, from 1 (I'm too young to die) to 5 (Nightmare!) + ## Archipelago Text Client We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index 258a5445..ac58339c 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -234,8 +234,7 @@ async def game_watcher(ctx: FactorioContext): f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}") else: data = data["info"] - research_data = data["research_done"] - research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} + research_data: set[int] = {int(tech_name.split("-")[1]) for tech_name in data["research_done"]} victory = data["victory"] await ctx.update_death_link(data["death_link"]) ctx.multiplayer = data.get("multiplayer", False) @@ -247,9 +246,9 @@ async def game_watcher(ctx: FactorioContext): if ctx.locations_checked != research_data: bridge_logger.debug( f"New researches done: " - f"{[ctx.location_names.lookup_in_slot(rid) for rid in research_data - ctx.locations_checked]}") + f"{[ctx.location_names.lookup_in_game(rid) for rid in research_data - ctx.locations_checked]}") ctx.locations_checked = research_data - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) + await ctx.check_locations(research_data) death_link_tick = data.get("death_link_tick", 0) if death_link_tick != ctx.death_link_tick: ctx.death_link_tick = death_link_tick @@ -304,13 +303,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 +330,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") @@ -360,7 +360,7 @@ async def factorio_server_watcher(ctx: FactorioContext): transfer_item: NetworkItem = ctx.items_received[ctx.send_index] item_id = transfer_item.item player_name = ctx.player_names[transfer_item.player] - item_name = ctx.item_names.lookup_in_slot(item_id) + item_name = ctx.item_names.lookup_in_game(item_id) factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") commands[ctx.send_index] = f"/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}" ctx.send_index += 1 @@ -405,8 +405,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 d7b3d4b1..8ea0b24c 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -1,5 +1,6 @@ """Outputs a Factorio Mod to facilitate integration with Archipelago""" +import dataclasses import json import os import shutil @@ -34,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.28", + "? quality >= 2.0.28", + "! space-age", "? science-not-invited", "? factory-levels" ] @@ -88,6 +91,8 @@ class FactorioModFile(worlds.Files.APContainer): def generate_mod(world: "Factorio", output_directory: str): player = world.player multiworld = world.multiworld + random = world.random + global data_final_template, locale_template, control_template, data_template, settings_template with template_load_lock: if not data_final_template: @@ -110,8 +115,6 @@ def generate_mod(world: "Factorio", output_directory: str): mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" versioned_mod_name = mod_name + "_" + Utils.__version__ - random = multiworld.per_slot_randoms[player] - def flop_random(low, high, base=None): """Guarantees 50% below base and 50% above base, uniform distribution in each direction.""" if base: @@ -129,43 +132,43 @@ def generate_mod(world: "Factorio", output_directory: str): "base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup, "mod_name": mod_name, - "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), - "custom_technologies": multiworld.worlds[player].custom_technologies, + "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": multiworld.player_name[player], "seed_name": multiworld.seed_name, + "slot_name": world.player_name, + "seed_name": multiworld.seed_name, "slot_player": player, - "starting_items": multiworld.starting_items[player], "recipes": recipes, - "random": random, "flop_random": flop_random, - "recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None), - "recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None), + "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": multiworld.max_science_pack[player].value, "liquids": fluids, - "goal": multiworld.goal[player].value, - "energy_link": multiworld.energy_link[player].value, - "useless_technologies": useless_technologies, - "chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0, + "removed_technologies": world.removed_technologies, + "chunk_shuffle": 0, } - for factorio_option in Options.factorio_options: + for factorio_option, factorio_option_instance in dataclasses.asdict(world.options).items(): if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]: continue - template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value + template_data[factorio_option] = factorio_option_instance.value - if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe: + if world.options.silo == Options.Silo.option_randomize_recipe: template_data["free_sample_blacklist"]["rocket-silo"] = 1 - if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe: + if world.options.satellite == Options.Satellite.option_randomize_recipe: template_data["free_sample_blacklist"]["satellite"] = 1 - template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) - template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) + template_data["free_sample_blacklist"].update({item: 1 for item in world.options.free_sample_blacklist.value}) + template_data["free_sample_blacklist"].update({item: 0 for item in world.options.free_sample_whitelist.value}) zf_path = os.path.join(output_directory, versioned_mod_name + ".zip") - mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) + mod = FactorioModFile(zf_path, player=player, player_name=world.player_name) if world.zip_path: with zipfile.ZipFile(world.zip_path) as zf: diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 3429ebbd..4848cd99 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,13 +1,25 @@ from __future__ import annotations -import typing -import datetime -from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ - StartInventoryPool -from schema import Schema, Optional, And, Or +from dataclasses import dataclass +import typing + +from schema import Schema, Optional, And, Or, SchemaError + +from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \ + StartInventoryPool, PerGameCommonOptions, OptionGroup # schema helpers -FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high) +class FloatRange: + def __init__(self, low, high): + self._low = low + self._high = high + + def validate(self, value): + if not isinstance(value, (float, int)): + raise SchemaError(f"should be instance of float or int, but was {value!r}") + if not self._low <= value <= self._high: + raise SchemaError(f"{value} is not between {self._low} and {self._high}") + LuaBool = Or(bool, And(int, lambda n: n in (0, 1))) @@ -119,6 +131,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 @@ -258,6 +282,12 @@ class AtomicRocketTrapCount(TrapCount): display_name = "Atomic Rocket Traps" +class AtomicCliffRemoverTrapCount(TrapCount): + """Trap items that when received trigger an atomic rocket explosion on a random cliff. + Warning: there is no warning. The launch is instantaneous.""" + display_name = "Atomic Cliff Remover Traps" + + class EvolutionTrapCount(TrapCount): """Trap items that when received increase the enemy evolution.""" display_name = "Evolution Traps" @@ -274,24 +304,33 @@ class EvolutionTrapIncrease(Range): range_end = 100 +class InventorySpillTrapCount(TrapCount): + """Trap items that when received trigger dropping your main inventory and trash inventory onto the ground.""" + display_name = "Inventory Spill Traps" + + class FactorioWorldGen(OptionDict): """World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator, with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings""" display_name = "World Generation" # FIXME: do we want default be a rando-optimized default or in-game DS? - value: typing.Dict[str, typing.Dict[str, typing.Any]] + value: dict[str, 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, @@ -333,8 +372,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), @@ -386,7 +423,7 @@ class FactorioWorldGen(OptionDict): } }) - def __init__(self, value: typing.Dict[str, typing.Any]): + def __init__(self, value: dict[str, typing.Any]): advanced = {"pollution", "enemy_evolution", "enemy_expansion"} self.value = { "basic": {k: v for k, v in value.items() if k not in advanced}, @@ -405,7 +442,7 @@ class FactorioWorldGen(OptionDict): optional_min_lte_max(enemy_expansion, "min_expansion_cooldown", "max_expansion_cooldown") @classmethod - def from_any(cls, data: typing.Dict[str, typing.Any]) -> FactorioWorldGen: + def from_any(cls, data: dict[str, typing.Any]) -> FactorioWorldGen: if type(data) == dict: return cls(data) else: @@ -419,53 +456,76 @@ class ImportedBlueprint(DefaultOnToggle): class EnergyLink(Toggle): """Allow sending energy to other worlds. 25% of the energy is lost in the transfer.""" - display_name = "EnergyLink" + display_name = "Energy Link" -factorio_options: typing.Dict[str, type(Option)] = { - "max_science_pack": MaxSciencePack, - "goal": Goal, - "tech_tree_layout": TechTreeLayout, - "min_tech_cost": MinTechCost, - "max_tech_cost": MaxTechCost, - "tech_cost_distribution": TechCostDistribution, - "tech_cost_mix": TechCostMix, - "ramping_tech_costs": RampingTechCosts, - "silo": Silo, - "satellite": Satellite, - "free_samples": FreeSamples, - "tech_tree_information": TechTreeInformation, - "starting_items": FactorioStartItems, - "free_sample_blacklist": FactorioFreeSampleBlacklist, - "free_sample_whitelist": FactorioFreeSampleWhitelist, - "recipe_time": RecipeTime, - "recipe_ingredients": RecipeIngredients, - "recipe_ingredients_offset": RecipeIngredientsOffset, - "imported_blueprints": ImportedBlueprint, - "world_gen": FactorioWorldGen, - "progressive": Progressive, - "teleport_traps": TeleportTrapCount, - "grenade_traps": GrenadeTrapCount, - "cluster_grenade_traps": ClusterGrenadeTrapCount, - "artillery_traps": ArtilleryTrapCount, - "atomic_rocket_traps": AtomicRocketTrapCount, - "attack_traps": AttackTrapCount, - "evolution_traps": EvolutionTrapCount, - "evolution_trap_increase": EvolutionTrapIncrease, - "death_link": DeathLink, - "energy_link": EnergyLink, - "start_inventory_from_pool": StartInventoryPool, -} - -# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else. -if datetime.datetime.today().month == 4: - - class ChunkShuffle(Toggle): - """Entrance Randomizer.""" - display_name = "Chunk Shuffle" +@dataclass +class FactorioOptions(PerGameCommonOptions): + max_science_pack: MaxSciencePack + goal: Goal + tech_tree_layout: TechTreeLayout + min_tech_cost: MinTechCost + max_tech_cost: MaxTechCost + tech_cost_distribution: TechCostDistribution + tech_cost_mix: TechCostMix + ramping_tech_costs: RampingTechCosts + silo: Silo + satellite: Satellite + free_samples: FreeSamples + free_samples_quality: FreeSamplesQuality + tech_tree_information: TechTreeInformation + starting_items: FactorioStartItems + free_sample_blacklist: FactorioFreeSampleBlacklist + free_sample_whitelist: FactorioFreeSampleWhitelist + recipe_time: RecipeTime + recipe_ingredients: RecipeIngredients + recipe_ingredients_offset: RecipeIngredientsOffset + imported_blueprints: ImportedBlueprint + world_gen: FactorioWorldGen + progressive: Progressive + teleport_traps: TeleportTrapCount + grenade_traps: GrenadeTrapCount + cluster_grenade_traps: ClusterGrenadeTrapCount + artillery_traps: ArtilleryTrapCount + atomic_rocket_traps: AtomicRocketTrapCount + atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount + inventory_spill_traps: InventorySpillTrapCount + attack_traps: AttackTrapCount + evolution_traps: EvolutionTrapCount + evolution_trap_increase: EvolutionTrapIncrease + death_link: DeathLink + energy_link: EnergyLink + start_inventory_from_pool: StartInventoryPool - if datetime.datetime.today().day > 1: - ChunkShuffle.__doc__ += """ - 2023 April Fool's option. Shuffles chunk border transitions.""" - factorio_options["chunk_shuffle"] = ChunkShuffle +option_groups: list[OptionGroup] = [ + OptionGroup( + "Technologies", + [ + TechTreeLayout, + Progressive, + MinTechCost, + MaxTechCost, + TechCostDistribution, + TechCostMix, + RampingTechCosts, + TechTreeInformation, + ] + ), + OptionGroup( + "Traps", + [ + AttackTrapCount, + EvolutionTrapCount, + EvolutionTrapIncrease, + TeleportTrapCount, + GrenadeTrapCount, + ClusterGrenadeTrapCount, + ArtilleryTrapCount, + AtomicRocketTrapCount, + AtomicCliffRemoverTrapCount, + InventorySpillTrapCount, + ], + start_collapsed=True + ), +] diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index d40871f7..2a81cc3f 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -19,12 +19,10 @@ def _sorter(location: "FactorioScienceLocation"): return location.complexity, location.rel_cost -def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: - world = factorio_world.multiworld - player = factorio_world.player +def get_shapes(world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} - layout = world.tech_tree_layout[player].value - locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name) + layout = world.options.tech_tree_layout.value + locations: List["FactorioScienceLocation"] = sorted(world.science_locations, key=lambda loc: loc.name) world.random.shuffle(locations) if layout == TechTreeLayout.option_single: @@ -247,5 +245,5 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se else: raise NotImplementedError(f"Layout {layout} is not implemented.") - factorio_world.tech_tree_layout_prerequisites = prerequisites + world.tech_tree_layout_prerequisites = prerequisites return prerequisites diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 096396c0..192cd1fe 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -1,24 +1,23 @@ 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 factorio_tech_id = factorio_base_id = 2 ** 17 -# Factorio technologies are imported from a .json document in /data -source_folder = os.path.join(os.path.dirname(__file__), "data") pool = ThreadPoolExecutor(1) +# Factorio technologies are imported from a .json document in /data def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json")) @@ -33,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 @@ -49,40 +63,31 @@ 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 + modifiers: list[str] - def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = (), - has_modifier: bool = False, unlocks: Union[Set[str], bool] = None): - self.name = name + def __init__(self, technology_name: str, factorio_id: int, progressive: Tuple[str] = (), + modifiers: list[str] = None, unlocks: Union[Set[str], bool] = None): + self.name = technology_name self.factorio_id = factorio_id - self.ingredients = ingredients self.progressive = progressive - self.has_modifier = has_modifier + if modifiers is None: + modifiers = [] + self.modifiers = modifiers if unlocks: self.unlocks = unlocks 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 + @property + def has_modifier(self) -> bool: + return bool(self.modifiers) + def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology: return CustomTechnology(self, world, allowed_packs, player) @@ -92,22 +97,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.worlds[player].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) + if origin.name not in world.special_nodes: + 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): @@ -150,19 +155,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 @@ -189,18 +197,23 @@ class Machine(FactorioElement): recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source +mining_with_fluid_sources: set[str] = set() # 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, + modifiers=data.get("modifiers", []), + unlocks=set(data["unlocks"]) - start_unlocked_recipes, + ) factorio_tech_id += 1 tech_table[technology_name] = technology.factorio_id technology_table[technology_name] = technology for recipe_name in technology.unlocks: recipe_sources.setdefault(recipe_name, set()).add(technology_name) - + if "mining-with-fluid" in technology.modifiers: + mining_with_fluid_sources.add(technology_name) del techs_future recipes = {} @@ -216,6 +229,8 @@ for resource_name, resource_data in resources_future.result().items(): "energy": resource_data["mining_time"], "category": resource_data["category"] } + if "required_fluid" in resource_data: + recipe_sources.setdefault(f"mining-{resource_name}", set()).update(mining_with_fluid_sources) del resources_future for recipe_name, recipe_data in raw_recipes.items(): @@ -227,11 +242,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] = {} @@ -249,9 +265,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]: @@ -320,13 +334,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: @@ -383,15 +401,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", @@ -403,7 +421,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(): @@ -417,12 +435,16 @@ 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, - has_modifier=any(technology_table[tech].has_modifier for tech in progressive), - unlocks=any(technology_table[tech].unlocks for tech in progressive)) + progressive_technology = Technology(root, factorio_tech_id, + tuple(progressive), + modifiers=sorted(set.union( + *(set(technology_table[tech].modifiers) 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 1ea2f6e4..ca9f12f1 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -2,19 +2,21 @@ 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 from .Locations import location_pools, location_table from .Mod import generate_mod -from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution +from .Options import (FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, + TechCostDistribution, option_groups) 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 @@ -60,6 +62,7 @@ class FactorioWeb(WebWorld): "setup/en", ["Berserker, Farrak Kilhn"] )] + option_groups = option_groups class FactorioItem(Item): @@ -74,6 +77,8 @@ all_items["Grenade Trap"] = factorio_base_id - 4 all_items["Cluster Grenade Trap"] = factorio_base_id - 5 all_items["Artillery Trap"] = factorio_base_id - 6 all_items["Atomic Rocket Trap"] = factorio_base_id - 7 +all_items["Atomic Cliff Remover Trap"] = factorio_base_id - 8 +all_items["Inventory Spill Trap"] = factorio_base_id - 9 class Factorio(World): @@ -89,24 +94,31 @@ class Factorio(World): advancement_technologies: typing.Set[str] web = FactorioWeb() + options_dataclass = FactorioOptions + options: FactorioOptions item_name_to_id = all_items location_name_to_id = location_table item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - required_client_version = (0, 4, 2) - + 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] + trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", + "Atomic Rocket", "Atomic Cliff Remover", "Inventory Spill") 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 = [] @@ -116,35 +128,29 @@ class Factorio(World): def generate_early(self) -> None: # if max < min, then swap max and min - if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]: - self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \ - self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value - self.tech_mix = self.multiworld.tech_cost_mix[self.player] - self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn + if self.options.max_tech_cost < self.options.min_tech_cost: + self.options.min_tech_cost.value, self.options.max_tech_cost.value = \ + self.options.max_tech_cost.value, self.options.min_tech_cost.value + self.tech_mix = self.options.tech_cost_mix.value + self.skip_silo = self.options.silo.value == Silo.option_spawn def create_regions(self): player = self.player - random = self.multiworld.random - menu = Region("Menu", player, self.multiworld) - crash = Entrance(player, "Crash Land", menu) - menu.exits.append(crash) + random = self.random nauvis = Region("Nauvis", player, self.multiworld) - location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ - self.multiworld.evolution_traps[player] + \ - self.multiworld.attack_traps[player] + \ - self.multiworld.teleport_traps[player] + \ - self.multiworld.grenade_traps[player] + \ - self.multiworld.cluster_grenade_traps[player] + \ - self.multiworld.atomic_rocket_traps[player] + \ - self.multiworld.artillery_traps[player] + location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + + for name in self.trap_names: + name = name.replace(" ", "_").lower()+"_traps" + location_count += getattr(self.options, name) location_pool = [] - for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for pack in sorted(self.options.max_science_pack.get_allowed_packs()): location_pool.extend(location_pools[pack]) try: - location_names = self.multiworld.random.sample(location_pool, location_count) + location_names = random.sample(location_pool, location_count) except ValueError as e: # should be "ValueError: Sample larger than population or is negative" raise Exception("Too many traps for too few locations. Either decrease the trap count, " @@ -152,9 +158,9 @@ class Factorio(World): self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) for loc_name in location_names] - distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] - min_cost = self.multiworld.min_tech_cost[self.player] - max_cost = self.multiworld.max_tech_cost[self.player] + distribution: TechCostDistribution = self.options.tech_cost_distribution + min_cost = self.options.min_tech_cost.value + max_cost = self.options.max_tech_cost.value if distribution == distribution.option_even: rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations) else: @@ -163,7 +169,7 @@ class Factorio(World): distribution.option_high: max_cost}[distribution.value] rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations) rand_values = sorted(rand_values) - if self.multiworld.ramping_tech_costs[self.player]: + if self.options.ramping_tech_costs: def sorter(loc: FactorioScienceLocation): return loc.complexity, loc.rel_cost else: @@ -178,43 +184,39 @@ class Factorio(World): event = FactorioItem("Victory", ItemClassification.progression, None, player) location.place_locked_item(event) - for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()): location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis) nauvis.locations.append(location) event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player) location.place_locked_item(event) - crash.connect(nauvis) - self.multiworld.regions += [menu, nauvis] + self.multiworld.regions.append(nauvis) def create_items(self) -> None: - player = self.player self.custom_technologies = self.set_custom_technologies() self.set_custom_recipes() - traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") - for trap_name in traps: - self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in - range(getattr(self.multiworld, - f"{trap_name.lower().replace(' ', '_')}_traps")[player])) - want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player]. - want_progressives(self.multiworld.random)) + for trap_name in self.trap_names: + self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in + range(getattr(self.options, + f"{trap_name.lower().replace(' ', '_')}_traps"))) + + want_progressives = collections.defaultdict(lambda: self.options.progressive. + want_progressives(self.random)) cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name) special_index = {"automation": 0, "logistics": 1, "rocket-silo": -1} loc: FactorioScienceLocation - if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full: + if self.options.tech_tree_information == TechTreeInformation.option_full: # mark all locations as pre-hinted 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 @@ -232,58 +234,63 @@ class Factorio(World): loc.revealed = True def set_rules(self): - world = self.multiworld player = self.player shapes = get_shapes(self) - for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs(): - location = world.get_location(f"Automate {ingredient}", player) + for ingredient in self.options.max_science_pack.get_allowed_packs(): + location = self.get_location(f"Automate {ingredient}") - if self.multiworld.recipe_ingredients[self.player]: + if self.options.recipe_ingredients: 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 - if self.multiworld.silo[self.player] == 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"))) + cargo_pad_recipe = None + if self.options.silo == Silo.option_spawn: + 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.multiworld.goal[self.player] == 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.multiworld.silo[self.player] != Silo.option_spawn: - victory_tech_names.add("rocket-silo") - world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) - for technology in - victory_tech_names) + if self.options.goal == Goal.option_satellite: + 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) + self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) - world.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.multiworld.world_gen[self.player].value["basic"] + map_basic_settings = self.options.world_gen.value["basic"] if map_basic_settings.get("seed", None) is None: # allow seed 0 # 32 bit uint - map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1) + map_basic_settings["seed"] = self.random.randint(0, 2 ** 32 - 1) - start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value + start_location_hints: typing.Set[str] = self.options.start_location_hints.value for loc in self.science_locations: # show start_location_hints ingame @@ -307,8 +314,6 @@ class Factorio(World): return super(Factorio, self).collect_item(state, item, remove) - option_definitions = factorio_options - @classmethod def stage_write_spoiler(cls, world, spoiler_handle): factorio_players = world.get_game_players(cls.game) @@ -326,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: @@ -348,7 +355,7 @@ class Factorio(World): # have to first sort for determinism, while filtering out non-stacking items pool: typing.List[str] = sorted(pool & valid_ingredients) # then sort with random data to shuffle - self.multiworld.random.shuffle(pool) + self.random.shuffle(pool) target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor) target_energy = original.total_energy * factor target_num_ingredients = len(original.ingredients) + ingredients_offset @@ -392,7 +399,7 @@ class Factorio(World): if min_num > max_num: fallback_pool.append(ingredient) continue # can't use that ingredient - num = self.multiworld.random.randint(min_num, max_num) + num = self.random.randint(min_num, max_num) new_ingredients[ingredient] = num remaining_raw -= num * ingredient_raw remaining_energy -= num * ingredient_energy @@ -436,66 +443,67 @@ class Factorio(World): def set_custom_technologies(self): custom_technologies = {} - allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs() + allowed_packs = self.options.max_science_pack.get_allowed_packs() for technology_name, technology in base_technology_table.items(): - custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player) + custom_technologies[technology_name] = technology.get_custom(self, allowed_packs, self.player) return custom_technologies def set_custom_recipes(self): - ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player] + 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.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients) - self.multiworld.random.shuffle(valid_pool) + 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)}, original_rocket_part.products, original_rocket_part.energy)} - if self.multiworld.recipe_ingredients[self.player]: + if self.options.recipe_ingredients: valid_pool = [] - for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs(): + for pack in self.options.max_science_pack.get_ordered_science_packs(): valid_pool += sorted(science_pack_pools[pack]) - self.multiworld.random.shuffle(valid_pool) + self.random.shuffle(valid_pool) if pack in recipes: # skips over space science pack new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset= - ingredients_offset) + ingredients_offset.value) self.custom_recipes[pack] = new_recipe - if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \ - or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: + if self.options.silo.value == Silo.option_randomize_recipe \ + or self.options.satellite.value == Satellite.option_randomize_recipe: valid_pool = set() - for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for pack in sorted(self.options.max_science_pack.get_allowed_packs()): valid_pool |= science_pack_pools[pack] - if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe: + if self.options.silo.value == Silo.option_randomize_recipe: new_recipe = self.make_balanced_recipe( recipes["rocket-silo"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, - ingredients_offset=ingredients_offset) + factor=(self.options.max_science_pack.value + 1) / 7, + ingredients_offset=ingredients_offset.value) self.custom_recipes["rocket-silo"] = new_recipe - if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: + if self.options.satellite.value == Satellite.option_randomize_recipe: new_recipe = self.make_balanced_recipe( recipes["satellite"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, - ingredients_offset=ingredients_offset) + factor=(self.options.max_science_pack.value + 1) / 7, + ingredients_offset=ingredients_offset.value) self.custom_recipes["satellite"] = new_recipe bridge = "ap-energy-bridge" new_recipe = self.make_quick_recipe( Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1, "replace_4": 1, "replace_5": 1, "replace_6": 1}, {bridge: 1}, 10), - sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]), - ingredients_offset=ingredients_offset) + sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]), + ingredients_offset=ingredients_offset.value) for ingredient_name in new_recipe.ingredients: - new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500) + new_recipe.ingredients[ingredient_name] = self.random.randint(50, 500) self.custom_recipes[bridge] = new_recipe - needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"} - if self.multiworld.silo[self.player] != Silo.option_spawn: - needed_recipes |= {"rocket-silo"} - if self.multiworld.goal[self.player].value == Goal.option_satellite: + needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"} + if self.options.silo != Silo.option_spawn: + needed_recipes |= {"rocket-silo", "cargo-landing-pad"} + if self.options.goal.value == Goal.option_satellite: needed_recipes |= {"satellite"} for recipe in needed_recipes: @@ -545,7 +553,8 @@ class FactorioScienceLocation(FactorioLocation): self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1} for complexity in range(self.complexity): - if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99): + if (parent.multiworld.worlds[self.player].options.tech_cost_mix > + parent.multiworld.worlds[self.player].random.randint(0, 99)): self.ingredients[Factorio.ordered_science_packs[complexity]] = 1 @property diff --git a/worlds/factorio/data/fluids.json b/worlds/factorio/data/fluids.json index 448ccf4e..6972690f 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 fa34430f..d9ec7bef 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 15a79580..c8629ab8 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 2b18f119..edec5b7a 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,16 +24,64 @@ 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) + local entities = {} for _, player in ipairs(game.forces["player"].players) do - current_character = player.character - if current_character ~= nil then - current_character.surface.create_entity{name=entity_name, - position=random_offset_position(current_character.position, 128), - target=current_character, speed=speed} + if player.character ~= nil then + table.insert(entities, player.character) end end -end \ No newline at end of file + return fire_entity_at_entities(entity_name, entities, speed) +end + +function fire_entity_at_entities(entity_name, entities, speed) + for _, current_entity in ipairs(entities) do + local target = current_entity + if target.health == nil then + target = target.position + end + current_entity.surface.create_entity{name=entity_name, + position=random_offset_position(current_entity.position, 128), + target=target, speed=speed} + end +end + +function spill_character_inventory(character) + if not (character and character.valid) then + return false + end + + -- grab attrs once pre-loop + local position = character.position + local surface = character.surface + + local inventories_to_spill = { + defines.inventory.character_main, -- Main inventory + defines.inventory.character_trash, -- Logistic trash slots + } + + for _, inventory_type in pairs(inventories_to_spill) do + local inventory = character.get_inventory(inventory_type) + if inventory and inventory.valid then + -- Spill each item stack onto the ground + for i = 1, #inventory do + local stack = inventory[i] + if stack and stack.valid_for_read then + local spilled_items = surface.spill_item_stack{ + position = position, + stack = stack, + enable_looted = false, -- do not mark for auto-pickup + force = nil, -- do not mark for auto-deconstruction + allow_belts = true, -- do mark for putting it onto belts + } + if #spilled_items > 0 then + stack.clear() -- only delete if spilled successfully + end + end + end + end + end +end diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 8ce0b45a..07fd4c04 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. @@ -364,6 +445,10 @@ end script.on_event(defines.events.on_player_main_inventory_changed, update_player_event) +-- Update players when the cutscene is cancelled or finished. (needed for skins_factored) +script.on_event(defines.events.on_cutscene_cancelled, update_player_event) +script.on_event(defines.events.on_cutscene_finished, update_player_event) + function add_samples(force, name, count) local function add_to_table(t) if count <= 0 then @@ -372,19 +457,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 +506,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 +563,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 +623,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 +646,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 +674,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 +689,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 +705,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) @@ -628,8 +717,10 @@ TRAP_TABLE = { game.surfaces["nauvis"].build_enemy_base(game.forces["player"].get_spawn_position(game.get_surface(1)), 25) end, ["Evolution Trap"] = function () - game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor)) - game.print({"", "New evolution factor:", game.forces["enemy"].evolution_factor}) + local new_factor = game.forces["enemy"].get_evolution_factor("nauvis") + + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].get_evolution_factor("nauvis"))) + game.forces["enemy"].set_evolution_factor(new_factor, "nauvis") + game.print({"", "New evolution factor:", new_factor}) end, ["Teleport Trap"] = function () for _, player in ipairs(game.forces["player"].players) do @@ -652,19 +743,38 @@ end, ["Atomic Rocket Trap"] = function () fire_entity_at_players("atomic-rocket", 0.1) end, +["Atomic Cliff Remover Trap"] = function () + local cliffs = game.surfaces["nauvis"].find_entities_filtered{type = "cliff"} + + if #cliffs > 0 then + fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1) + end +end, +["Inventory Spill Trap"] = function () + for _, player in ipairs(game.forces["player"].players) do + spill_character_inventory(player.character) + end +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"] + if call.parameter == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + end chunks = split(call.parameter, "\t") local item_name = chunks[1] local index = chunks[2] local source = chunks[3] or "Archipelago" - if index == -1 then -- for coop sync and restoring from an older savegame + if index == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + elseif index == -1 then -- for coop sync and restoring from an older savegame tech = force.technologies[item_name] if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."}) @@ -673,8 +783,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] @@ -689,7 +799,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"}) @@ -697,8 +807,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 @@ -709,7 +819,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, @@ -719,7 +829,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 -%} @@ -735,7 +845,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 3021fd5d..8092062b 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -1,48 +1,52 @@ {% from "macros.lua" import dict_to_recipe, variable_to_lua %} -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template require('lib') +data.raw["item"]["rocket-part"].hidden = false 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}}" @@ -159,6 +163,7 @@ data.raw["ammo"]["artillery-shell"].stack_size = 10 {# each randomized tech gets set to be invisible, with new nodes added that trigger those #} {%- for original_tech_name in base_tech_table -%} technologies["{{ original_tech_name }}"].hidden = true +technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true {% endfor %} {%- for location, item in locations %} {#- the tech researched by the local player #} diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua index 82053453..43151ff0 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 1b271031..f1530359 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 73e131a6..41d30e58 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 4c4ab815..b0633b49 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 10279db3..80c00fe3 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 d9977f29..ced2b631 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"],"infinite":false},"advanced-combinators":{"unlocks":["selector-combinator"],"requires":["circuit-network","chemical-science-pack"],"infinite":false},"advanced-material-processing":{"unlocks":["steel-furnace"],"requires":["steel-processing","logistic-science-pack"],"infinite":false},"advanced-material-processing-2":{"unlocks":["electric-furnace"],"requires":["advanced-material-processing","chemical-science-pack"],"infinite":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"],"infinite":false},"artillery":{"unlocks":["artillery-wagon","artillery-turret","artillery-shell"],"requires":["military-4","tank","concrete","radar"],"infinite":false},"atomic-bomb":{"unlocks":["atomic-bomb"],"requires":["military-4","kovarex-enrichment-process","rocketry"],"infinite":false},"automated-rail-transportation":{"unlocks":["train-stop","rail-signal","rail-chain-signal"],"requires":["railway"],"infinite":false},"automation":{"unlocks":["assembling-machine-1","long-handed-inserter"],"requires":["automation-science-pack"],"infinite":false},"automation-2":{"unlocks":["assembling-machine-2"],"requires":["automation","steel-processing","logistic-science-pack"],"infinite":false},"automation-3":{"unlocks":["assembling-machine-3"],"requires":["speed-module","production-science-pack","electric-engine"],"infinite":false},"automation-science-pack":{"unlocks":["automation-science-pack"],"requires":["steam-power","electronics"],"infinite":false},"automobilism":{"unlocks":["car"],"requires":["logistics-2","engine"],"infinite":false},"battery":{"unlocks":["battery"],"requires":["sulfur-processing"],"infinite":false},"battery-equipment":{"unlocks":["battery-equipment"],"requires":["battery","solar-panel-equipment"],"infinite":false},"battery-mk2-equipment":{"unlocks":["battery-mk2-equipment"],"requires":["battery-equipment","low-density-structure","power-armor"],"infinite":false},"belt-immunity-equipment":{"unlocks":["belt-immunity-equipment"],"requires":["solar-panel-equipment"],"infinite":false},"braking-force-1":{"unlocks":{},"requires":["railway","chemical-science-pack"],"infinite":false,"modifiers":["train-braking-force-bonus"]},"braking-force-2":{"unlocks":{},"requires":["braking-force-1"],"infinite":false,"modifiers":["train-braking-force-bonus"]},"braking-force-3":{"unlocks":{},"requires":["braking-force-2","production-science-pack"],"infinite":false,"modifiers":["train-braking-force-bonus"]},"braking-force-4":{"unlocks":{},"requires":["braking-force-3"],"infinite":false,"modifiers":["train-braking-force-bonus"]},"braking-force-5":{"unlocks":{},"requires":["braking-force-4"],"infinite":false,"modifiers":["train-braking-force-bonus"]},"braking-force-6":{"unlocks":{},"requires":["braking-force-5","utility-science-pack"],"infinite":false,"modifiers":["train-braking-force-bonus"]},"braking-force-7":{"unlocks":{},"requires":["braking-force-6"],"infinite":false,"modifiers":["train-braking-force-bonus"]},"bulk-inserter":{"unlocks":["bulk-inserter"],"requires":["fast-inserter","logistics-2","advanced-circuit"],"infinite":false,"modifiers":["bulk-inserter-capacity-bonus"]},"chemical-science-pack":{"unlocks":["chemical-science-pack"],"requires":["advanced-circuit","sulfur-processing"],"infinite":false},"circuit-network":{"unlocks":["arithmetic-combinator","decider-combinator","constant-combinator","power-switch","programmable-speaker","display-panel","iron-stick"],"requires":["logistic-science-pack"],"infinite":false,"modifiers":["unlock-circuit-network"]},"cliff-explosives":{"unlocks":["cliff-explosives"],"requires":["explosives","military-2"],"infinite":false,"modifiers":["cliff-deconstruction-enabled"]},"coal-liquefaction":{"unlocks":["coal-liquefaction"],"requires":["advanced-oil-processing","production-science-pack"],"infinite":false},"concrete":{"unlocks":["concrete","hazard-concrete","refined-concrete","refined-hazard-concrete","iron-stick"],"requires":["advanced-material-processing","automation-2"],"infinite":false},"construction-robotics":{"unlocks":["roboport","passive-provider-chest","storage-chest","construction-robot"],"requires":["robotics"],"infinite":false,"modifiers":["create-ghost-on-entity-death"]},"defender":{"unlocks":["defender-capsule"],"requires":["military-science-pack"],"infinite":false,"modifiers":["maximum-following-robots-count"]},"destroyer":{"unlocks":["destroyer-capsule"],"requires":["military-4","distractor","speed-module"],"infinite":false},"discharge-defense-equipment":{"unlocks":["discharge-defense-equipment"],"requires":["laser-turret","military-3","power-armor","solar-panel-equipment"],"infinite":false},"distractor":{"unlocks":["distractor-capsule"],"requires":["defender","military-3","laser"],"infinite":false},"effect-transmission":{"unlocks":["beacon"],"requires":["processing-unit","production-science-pack"],"infinite":false},"efficiency-module":{"unlocks":["efficiency-module"],"requires":["modules"],"infinite":false},"efficiency-module-2":{"unlocks":["efficiency-module-2"],"requires":["efficiency-module","processing-unit"],"infinite":false},"efficiency-module-3":{"unlocks":["efficiency-module-3"],"requires":["efficiency-module-2","production-science-pack"],"infinite":false},"electric-energy-accumulators":{"unlocks":["accumulator"],"requires":["electric-energy-distribution-1","battery"],"infinite":false},"electric-energy-distribution-1":{"unlocks":["medium-electric-pole","big-electric-pole","iron-stick"],"requires":["steel-processing","logistic-science-pack"],"infinite":false},"electric-energy-distribution-2":{"unlocks":["substation"],"requires":["electric-energy-distribution-1","chemical-science-pack"],"infinite":false},"electric-engine":{"unlocks":["electric-engine-unit"],"requires":["lubricant"],"infinite":false},"electric-mining-drill":{"unlocks":["electric-mining-drill"],"requires":["automation-science-pack"],"infinite":false},"electronics":{"unlocks":["copper-cable","electronic-circuit","lab","inserter","small-electric-pole"],"requires":{},"infinite":false},"energy-shield-equipment":{"unlocks":["energy-shield-equipment"],"requires":["solar-panel-equipment","military-science-pack"],"infinite":false},"energy-shield-mk2-equipment":{"unlocks":["energy-shield-mk2-equipment"],"requires":["energy-shield-equipment","military-3","low-density-structure","power-armor"],"infinite":false},"engine":{"unlocks":["engine-unit"],"requires":["steel-processing","logistic-science-pack"],"infinite":false},"exoskeleton-equipment":{"unlocks":["exoskeleton-equipment"],"requires":["processing-unit","electric-engine","solar-panel-equipment"],"infinite":false},"explosive-rocketry":{"unlocks":["explosive-rocket"],"requires":["rocketry","military-3"],"infinite":false},"explosives":{"unlocks":["explosives"],"requires":["sulfur-processing"],"infinite":false},"fast-inserter":{"unlocks":["fast-inserter"],"requires":["automation-science-pack"],"infinite":false},"fission-reactor-equipment":{"unlocks":["fission-reactor-equipment"],"requires":["utility-science-pack","power-armor","military-science-pack","nuclear-power"],"infinite":false},"flamethrower":{"unlocks":["flamethrower","flamethrower-ammo","flamethrower-turret"],"requires":["flammables","military-science-pack"],"infinite":false},"flammables":{"unlocks":{},"requires":["oil-processing"],"infinite":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"],"infinite":false},"fluid-wagon":{"unlocks":["fluid-wagon"],"requires":["railway","fluid-handling"],"infinite":false},"follower-robot-count-1":{"unlocks":{},"requires":["defender"],"infinite":false,"modifiers":["maximum-following-robots-count"]},"follower-robot-count-2":{"unlocks":{},"requires":["follower-robot-count-1"],"infinite":false,"modifiers":["maximum-following-robots-count"]},"follower-robot-count-3":{"unlocks":{},"requires":["follower-robot-count-2","chemical-science-pack"],"infinite":false,"modifiers":["maximum-following-robots-count"]},"follower-robot-count-4":{"unlocks":{},"requires":["follower-robot-count-3","destroyer"],"infinite":false,"modifiers":["maximum-following-robots-count"]},"gate":{"unlocks":["gate"],"requires":["stone-wall","military-2"],"infinite":false},"gun-turret":{"unlocks":["gun-turret"],"requires":["automation-science-pack"],"infinite":false},"heavy-armor":{"unlocks":["heavy-armor"],"requires":["military","steel-processing"],"infinite":false},"inserter-capacity-bonus-1":{"unlocks":{},"requires":["bulk-inserter"],"infinite":false,"modifiers":["bulk-inserter-capacity-bonus"]},"inserter-capacity-bonus-2":{"unlocks":{},"requires":["inserter-capacity-bonus-1"],"infinite":false,"modifiers":["inserter-stack-size-bonus","bulk-inserter-capacity-bonus"]},"inserter-capacity-bonus-3":{"unlocks":{},"requires":["inserter-capacity-bonus-2","chemical-science-pack"],"infinite":false,"modifiers":["bulk-inserter-capacity-bonus"]},"inserter-capacity-bonus-4":{"unlocks":{},"requires":["inserter-capacity-bonus-3","production-science-pack"],"infinite":false,"modifiers":["bulk-inserter-capacity-bonus"]},"inserter-capacity-bonus-5":{"unlocks":{},"requires":["inserter-capacity-bonus-4"],"infinite":false,"modifiers":["bulk-inserter-capacity-bonus"]},"inserter-capacity-bonus-6":{"unlocks":{},"requires":["inserter-capacity-bonus-5"],"infinite":false,"modifiers":["bulk-inserter-capacity-bonus"]},"inserter-capacity-bonus-7":{"unlocks":{},"requires":["inserter-capacity-bonus-6","utility-science-pack"],"infinite":false,"modifiers":["inserter-stack-size-bonus","bulk-inserter-capacity-bonus"]},"kovarex-enrichment-process":{"unlocks":["kovarex-enrichment-process","nuclear-fuel"],"requires":["production-science-pack","uranium-processing","rocket-fuel"],"infinite":false},"lamp":{"unlocks":["small-lamp"],"requires":["automation-science-pack"],"infinite":false},"land-mine":{"unlocks":["land-mine"],"requires":["explosives","military-science-pack"],"infinite":false},"landfill":{"unlocks":["landfill"],"requires":["logistic-science-pack"],"infinite":false},"laser":{"unlocks":{},"requires":["battery","chemical-science-pack"],"infinite":false},"laser-shooting-speed-1":{"unlocks":{},"requires":["laser","military-science-pack"],"infinite":false,"modifiers":["gun-speed"]},"laser-shooting-speed-2":{"unlocks":{},"requires":["laser-shooting-speed-1"],"infinite":false,"modifiers":["gun-speed"]},"laser-shooting-speed-3":{"unlocks":{},"requires":["laser-shooting-speed-2"],"infinite":false,"modifiers":["gun-speed"]},"laser-shooting-speed-4":{"unlocks":{},"requires":["laser-shooting-speed-3"],"infinite":false,"modifiers":["gun-speed"]},"laser-shooting-speed-5":{"unlocks":{},"requires":["laser-shooting-speed-4","utility-science-pack"],"infinite":false,"modifiers":["gun-speed"]},"laser-shooting-speed-6":{"unlocks":{},"requires":["laser-shooting-speed-5"],"infinite":false,"modifiers":["gun-speed"]},"laser-shooting-speed-7":{"unlocks":{},"requires":["laser-shooting-speed-6"],"infinite":false,"modifiers":["gun-speed"]},"laser-turret":{"unlocks":["laser-turret"],"requires":["laser","military-science-pack"],"infinite":false},"laser-weapons-damage-1":{"unlocks":{},"requires":["laser","military-science-pack"],"infinite":false,"modifiers":["ammo-damage"]},"laser-weapons-damage-2":{"unlocks":{},"requires":["laser-weapons-damage-1"],"infinite":false,"modifiers":["ammo-damage"]},"laser-weapons-damage-3":{"unlocks":{},"requires":["laser-weapons-damage-2"],"infinite":false,"modifiers":["ammo-damage"]},"laser-weapons-damage-4":{"unlocks":{},"requires":["laser-weapons-damage-3"],"infinite":false,"modifiers":["ammo-damage"]},"laser-weapons-damage-5":{"unlocks":{},"requires":["laser-weapons-damage-4","utility-science-pack"],"infinite":false,"modifiers":["ammo-damage","ammo-damage"]},"laser-weapons-damage-6":{"unlocks":{},"requires":["laser-weapons-damage-5"],"infinite":false,"modifiers":["ammo-damage","ammo-damage","ammo-damage"]},"logistic-robotics":{"unlocks":["roboport","passive-provider-chest","storage-chest","logistic-robot"],"requires":["robotics"],"infinite":false,"modifiers":["character-logistic-requests","character-logistic-trash-slots"]},"logistic-science-pack":{"unlocks":["logistic-science-pack"],"requires":["automation-science-pack"],"infinite":false},"logistic-system":{"unlocks":["active-provider-chest","requester-chest","buffer-chest"],"requires":["utility-science-pack","logistic-robotics"],"infinite":false,"modifiers":["vehicle-logistics"]},"logistics":{"unlocks":["underground-belt","splitter"],"requires":["automation-science-pack"],"infinite":false},"logistics-2":{"unlocks":["fast-transport-belt","fast-underground-belt","fast-splitter"],"requires":["logistics","logistic-science-pack"],"infinite":false},"logistics-3":{"unlocks":["express-transport-belt","express-underground-belt","express-splitter"],"requires":["production-science-pack","lubricant"],"infinite":false},"low-density-structure":{"unlocks":["low-density-structure"],"requires":["advanced-material-processing","chemical-science-pack"],"infinite":false},"lubricant":{"unlocks":["lubricant"],"requires":["advanced-oil-processing"],"infinite":false},"military":{"unlocks":["submachine-gun","shotgun","shotgun-shell"],"requires":["automation-science-pack"],"infinite":false},"military-2":{"unlocks":["piercing-rounds-magazine","grenade"],"requires":["military","steel-processing","logistic-science-pack"],"infinite":false},"military-3":{"unlocks":["poison-capsule","slowdown-capsule","combat-shotgun"],"requires":["chemical-science-pack","military-science-pack"],"infinite":false},"military-4":{"unlocks":["piercing-shotgun-shell","cluster-grenade"],"requires":["military-3","utility-science-pack","explosives"],"infinite":false},"military-science-pack":{"unlocks":["military-science-pack"],"requires":["military-2","stone-wall"],"infinite":false},"mining-productivity-1":{"unlocks":{},"requires":["advanced-circuit"],"infinite":false,"modifiers":["mining-drill-productivity-bonus"]},"mining-productivity-2":{"unlocks":{},"requires":["mining-productivity-1","chemical-science-pack"],"infinite":false,"modifiers":["mining-drill-productivity-bonus"]},"mining-productivity-3":{"unlocks":{},"requires":["mining-productivity-2","production-science-pack","utility-science-pack"],"infinite":false,"modifiers":["mining-drill-productivity-bonus"]},"modular-armor":{"unlocks":["modular-armor"],"requires":["heavy-armor","advanced-circuit"],"infinite":false},"modules":{"unlocks":{},"requires":["advanced-circuit"],"infinite":false},"night-vision-equipment":{"unlocks":["night-vision-equipment"],"requires":["solar-panel-equipment"],"infinite":false},"nuclear-fuel-reprocessing":{"unlocks":["nuclear-fuel-reprocessing"],"requires":["nuclear-power","production-science-pack"],"infinite":false},"nuclear-power":{"unlocks":["nuclear-reactor","heat-exchanger","heat-pipe","steam-turbine","uranium-fuel-cell"],"requires":["uranium-processing"],"infinite":false},"oil-gathering":{"unlocks":["pumpjack"],"requires":["fluid-handling"],"infinite":false},"oil-processing":{"unlocks":["oil-refinery","chemical-plant","basic-oil-processing","solid-fuel-from-petroleum-gas"],"requires":["oil-gathering"],"infinite":false},"personal-laser-defense-equipment":{"unlocks":["personal-laser-defense-equipment"],"requires":["laser-turret","military-3","low-density-structure","power-armor","solar-panel-equipment"],"infinite":false},"personal-roboport-equipment":{"unlocks":["personal-roboport-equipment"],"requires":["construction-robotics","solar-panel-equipment"],"infinite":false},"personal-roboport-mk2-equipment":{"unlocks":["personal-roboport-mk2-equipment"],"requires":["personal-roboport-equipment","utility-science-pack"],"infinite":false},"physical-projectile-damage-1":{"unlocks":{},"requires":["military"],"infinite":false,"modifiers":["ammo-damage","turret-attack","ammo-damage"]},"physical-projectile-damage-2":{"unlocks":{},"requires":["physical-projectile-damage-1","logistic-science-pack"],"infinite":false,"modifiers":["ammo-damage","turret-attack","ammo-damage"]},"physical-projectile-damage-3":{"unlocks":{},"requires":["physical-projectile-damage-2","military-science-pack"],"infinite":false,"modifiers":["ammo-damage","turret-attack","ammo-damage"]},"physical-projectile-damage-4":{"unlocks":{},"requires":["physical-projectile-damage-3"],"infinite":false,"modifiers":["ammo-damage","turret-attack","ammo-damage"]},"physical-projectile-damage-5":{"unlocks":{},"requires":["physical-projectile-damage-4","chemical-science-pack"],"infinite":false,"modifiers":["ammo-damage","turret-attack","ammo-damage","ammo-damage"]},"physical-projectile-damage-6":{"unlocks":{},"requires":["physical-projectile-damage-5","utility-science-pack"],"infinite":false,"modifiers":["ammo-damage","turret-attack","ammo-damage","ammo-damage"]},"plastics":{"unlocks":["plastic-bar"],"requires":["oil-processing"],"infinite":false},"power-armor":{"unlocks":["power-armor"],"requires":["modular-armor","electric-engine","processing-unit"],"infinite":false},"power-armor-mk2":{"unlocks":["power-armor-mk2"],"requires":["power-armor","military-4","speed-module-2","efficiency-module-2"],"infinite":false},"processing-unit":{"unlocks":["processing-unit"],"requires":["chemical-science-pack"],"infinite":false},"production-science-pack":{"unlocks":["production-science-pack"],"requires":["productivity-module","advanced-material-processing-2","railway"],"infinite":false},"productivity-module":{"unlocks":["productivity-module"],"requires":["modules"],"infinite":false},"productivity-module-2":{"unlocks":["productivity-module-2"],"requires":["productivity-module","processing-unit"],"infinite":false},"productivity-module-3":{"unlocks":["productivity-module-3"],"requires":["productivity-module-2","production-science-pack"],"infinite":false},"radar":{"unlocks":["radar"],"requires":["automation-science-pack"],"infinite":false},"railway":{"unlocks":["rail","locomotive","cargo-wagon","iron-stick"],"requires":["logistics-2","engine"],"infinite":false},"refined-flammables-1":{"unlocks":{},"requires":["flamethrower"],"infinite":false,"modifiers":["ammo-damage","turret-attack"]},"refined-flammables-2":{"unlocks":{},"requires":["refined-flammables-1"],"infinite":false,"modifiers":["ammo-damage","turret-attack"]},"refined-flammables-3":{"unlocks":{},"requires":["refined-flammables-2","chemical-science-pack"],"infinite":false,"modifiers":["ammo-damage","turret-attack"]},"refined-flammables-4":{"unlocks":{},"requires":["refined-flammables-3","utility-science-pack"],"infinite":false,"modifiers":["ammo-damage","turret-attack"]},"refined-flammables-5":{"unlocks":{},"requires":["refined-flammables-4"],"infinite":false,"modifiers":["ammo-damage","turret-attack"]},"refined-flammables-6":{"unlocks":{},"requires":["refined-flammables-5"],"infinite":false,"modifiers":["ammo-damage","turret-attack"]},"repair-pack":{"unlocks":["repair-pack"],"requires":["automation-science-pack"],"infinite":false},"research-speed-1":{"unlocks":{},"requires":["automation-2"],"infinite":false,"modifiers":["laboratory-speed"]},"research-speed-2":{"unlocks":{},"requires":["research-speed-1"],"infinite":false,"modifiers":["laboratory-speed"]},"research-speed-3":{"unlocks":{},"requires":["research-speed-2","chemical-science-pack"],"infinite":false,"modifiers":["laboratory-speed"]},"research-speed-4":{"unlocks":{},"requires":["research-speed-3"],"infinite":false,"modifiers":["laboratory-speed"]},"research-speed-5":{"unlocks":{},"requires":["research-speed-4","production-science-pack"],"infinite":false,"modifiers":["laboratory-speed"]},"research-speed-6":{"unlocks":{},"requires":["research-speed-5","utility-science-pack"],"infinite":false,"modifiers":["laboratory-speed"]},"robotics":{"unlocks":["flying-robot-frame"],"requires":["electric-engine","battery"],"infinite":false},"rocket-fuel":{"unlocks":["rocket-fuel"],"requires":["flammables","advanced-oil-processing"],"infinite":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"],"infinite":false},"rocketry":{"unlocks":["rocket-launcher","rocket"],"requires":["explosives","flammables","military-science-pack"],"infinite":false},"solar-energy":{"unlocks":["solar-panel"],"requires":["steel-processing","logistic-science-pack"],"infinite":false},"solar-panel-equipment":{"unlocks":["solar-panel-equipment"],"requires":["modular-armor","solar-energy"],"infinite":false},"space-science-pack":{"unlocks":{},"requires":["rocket-silo"],"infinite":false},"speed-module":{"unlocks":["speed-module"],"requires":["modules"],"infinite":false},"speed-module-2":{"unlocks":["speed-module-2"],"requires":["speed-module","processing-unit"],"infinite":false},"speed-module-3":{"unlocks":["speed-module-3"],"requires":["speed-module-2","production-science-pack"],"infinite":false},"spidertron":{"unlocks":["spidertron"],"requires":["military-4","exoskeleton-equipment","fission-reactor-equipment","rocketry","efficiency-module-3","radar"],"infinite":false},"steam-power":{"unlocks":["pipe","pipe-to-ground","offshore-pump","boiler","steam-engine"],"requires":{},"infinite":false},"steel-axe":{"unlocks":{},"requires":["steel-processing"],"infinite":false,"modifiers":["character-mining-speed"]},"steel-processing":{"unlocks":["steel-plate","steel-chest"],"requires":["automation-science-pack"],"infinite":false},"stone-wall":{"unlocks":["stone-wall"],"requires":["automation-science-pack"],"infinite":false},"stronger-explosives-1":{"unlocks":{},"requires":["military-2"],"infinite":false,"modifiers":["ammo-damage"]},"stronger-explosives-2":{"unlocks":{},"requires":["stronger-explosives-1","military-science-pack"],"infinite":false,"modifiers":["ammo-damage","ammo-damage"]},"stronger-explosives-3":{"unlocks":{},"requires":["stronger-explosives-2","chemical-science-pack"],"infinite":false,"modifiers":["ammo-damage","ammo-damage","ammo-damage"]},"stronger-explosives-4":{"unlocks":{},"requires":["stronger-explosives-3","utility-science-pack"],"infinite":false,"modifiers":["ammo-damage","ammo-damage","ammo-damage"]},"stronger-explosives-5":{"unlocks":{},"requires":["stronger-explosives-4"],"infinite":false,"modifiers":["ammo-damage","ammo-damage","ammo-damage"]},"stronger-explosives-6":{"unlocks":{},"requires":["stronger-explosives-5"],"infinite":false,"modifiers":["ammo-damage","ammo-damage","ammo-damage"]},"sulfur-processing":{"unlocks":["sulfuric-acid","sulfur"],"requires":["oil-processing"],"infinite":false},"tank":{"unlocks":["tank","cannon-shell","explosive-cannon-shell"],"requires":["automobilism","military-3","explosives"],"infinite":false},"toolbelt":{"unlocks":{},"requires":["logistic-science-pack"],"infinite":false,"modifiers":["character-inventory-slots-bonus"]},"uranium-ammo":{"unlocks":["uranium-rounds-magazine","uranium-cannon-shell","explosive-uranium-cannon-shell"],"requires":["uranium-processing","military-4","tank"],"infinite":false},"uranium-mining":{"unlocks":{},"requires":["chemical-science-pack","concrete"],"infinite":false,"modifiers":["mining-with-fluid"]},"uranium-processing":{"unlocks":["centrifuge","uranium-processing"],"requires":["uranium-mining"],"infinite":false},"utility-science-pack":{"unlocks":["utility-science-pack"],"requires":["robotics","processing-unit","low-density-structure"],"infinite":false},"weapon-shooting-speed-1":{"unlocks":{},"requires":["military"],"infinite":false,"modifiers":["gun-speed","gun-speed"]},"weapon-shooting-speed-2":{"unlocks":{},"requires":["weapon-shooting-speed-1","logistic-science-pack"],"infinite":false,"modifiers":["gun-speed","gun-speed"]},"weapon-shooting-speed-3":{"unlocks":{},"requires":["weapon-shooting-speed-2","military-science-pack"],"infinite":false,"modifiers":["gun-speed","gun-speed","gun-speed"]},"weapon-shooting-speed-4":{"unlocks":{},"requires":["weapon-shooting-speed-3"],"infinite":false,"modifiers":["gun-speed","gun-speed","gun-speed"]},"weapon-shooting-speed-5":{"unlocks":{},"requires":["weapon-shooting-speed-4","chemical-science-pack"],"infinite":false,"modifiers":["gun-speed","gun-speed","gun-speed","gun-speed"]},"weapon-shooting-speed-6":{"unlocks":{},"requires":["weapon-shooting-speed-5","utility-science-pack"],"infinite":false,"modifiers":["gun-speed","gun-speed","gun-speed","gun-speed"]},"worker-robots-speed-1":{"unlocks":{},"requires":["robotics"],"infinite":false,"modifiers":["worker-robot-speed"]},"worker-robots-speed-2":{"unlocks":{},"requires":["worker-robots-speed-1"],"infinite":false,"modifiers":["worker-robot-speed"]},"worker-robots-speed-3":{"unlocks":{},"requires":["worker-robots-speed-2","utility-science-pack"],"infinite":false,"modifiers":["worker-robot-speed"]},"worker-robots-speed-4":{"unlocks":{},"requires":["worker-robots-speed-3"],"infinite":false,"modifiers":["worker-robot-speed"]},"worker-robots-speed-5":{"unlocks":{},"requires":["worker-robots-speed-4","production-science-pack"],"infinite":false,"modifiers":["worker-robot-speed"]},"worker-robots-storage-1":{"unlocks":{},"requires":["robotics"],"infinite":false,"modifiers":["worker-robot-storage"]},"worker-robots-storage-2":{"unlocks":{},"requires":["worker-robots-storage-1","production-science-pack"],"infinite":false,"modifiers":["worker-robot-storage"]},"worker-robots-storage-3":{"unlocks":{},"requires":["worker-robots-storage-2","utility-science-pack"],"infinite":false,"modifiers":["worker-robot-storage"]}} \ No newline at end of file diff --git a/worlds/factorio/test_file_validation.py b/worlds/factorio/test_file_validation.py new file mode 100644 index 00000000..df56ec60 --- /dev/null +++ b/worlds/factorio/test_file_validation.py @@ -0,0 +1,39 @@ +"""Tests for error messages from YAML validation.""" + +import os +import unittest + +import WebHostLib.check + +FACTORIO_YAML=""" +game: Factorio +Factorio: + world_gen: + autoplace_controls: + coal: + richness: 1 + frequency: {} + size: 1 +""" + +def yamlWithFrequency(f): + return FACTORIO_YAML.format(f) + + +class TestFileValidation(unittest.TestCase): + def test_out_of_range(self): + results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1000)}) + self.assertIn("between 0 and 6", results["bob.yaml"]) + + def test_bad_non_numeric(self): + results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency("not numeric")}) + self.assertIn("float", results["bob.yaml"]) + self.assertIn("int", results["bob.yaml"]) + + def test_good_float(self): + results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1.0)}) + self.assertIs(results["bob.yaml"], True) + + def test_good_int(self): + results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1)}) + self.assertIs(results["bob.yaml"], True) diff --git a/worlds/faxanadu/Items.py b/worlds/faxanadu/Items.py new file mode 100644 index 00000000..4815fde9 --- /dev/null +++ b/worlds/faxanadu/Items.py @@ -0,0 +1,58 @@ +from BaseClasses import ItemClassification +from typing import List, Optional + + +class ItemDef: + def __init__(self, + id: Optional[int], + name: str, + classification: ItemClassification, + count: int, + progression_count: int, + prefill_location: Optional[str]): + self.id = id + self.name = name + self.classification = classification + self.count = count + self.progression_count = progression_count + self.prefill_location = prefill_location + + +items: List[ItemDef] = [ + ItemDef(400000, 'Progressive Sword', ItemClassification.progression, 4, 0, None), + ItemDef(400001, 'Progressive Armor', ItemClassification.progression, 3, 0, None), + ItemDef(400002, 'Progressive Shield', ItemClassification.useful, 4, 0, None), + ItemDef(400003, 'Spring Elixir', ItemClassification.progression, 1, 0, None), + ItemDef(400004, 'Mattock', ItemClassification.progression, 1, 0, None), + ItemDef(400005, 'Unlock Wingboots', ItemClassification.progression, 1, 0, None), + ItemDef(400006, 'Key Jack', ItemClassification.progression, 1, 0, None), + ItemDef(400007, 'Key Queen', ItemClassification.progression, 1, 0, None), + ItemDef(400008, 'Key King', ItemClassification.progression, 1, 0, None), + ItemDef(400009, 'Key Joker', ItemClassification.progression, 1, 0, None), + ItemDef(400010, 'Key Ace', ItemClassification.progression, 1, 0, None), + ItemDef(400011, 'Ring of Ruby', ItemClassification.progression, 1, 0, None), + ItemDef(400012, 'Ring of Dworf', ItemClassification.progression, 1, 0, None), + ItemDef(400013, 'Demons Ring', ItemClassification.progression, 1, 0, None), + ItemDef(400014, 'Black Onyx', ItemClassification.progression, 1, 0, None), + ItemDef(None, 'Sky Spring Flow', ItemClassification.progression, 1, 0, 'Sky Spring'), + ItemDef(None, 'Tower of Fortress Spring Flow', ItemClassification.progression, 1, 0, 'Tower of Fortress Spring'), + ItemDef(None, 'Joker Spring Flow', ItemClassification.progression, 1, 0, 'Joker Spring'), + ItemDef(400015, 'Deluge', ItemClassification.progression, 1, 0, None), + ItemDef(400016, 'Thunder', ItemClassification.useful, 1, 0, None), + ItemDef(400017, 'Fire', ItemClassification.useful, 1, 0, None), + ItemDef(400018, 'Death', ItemClassification.useful, 1, 0, None), + ItemDef(400019, 'Tilte', ItemClassification.useful, 1, 0, None), + ItemDef(400020, 'Ring of Elf', ItemClassification.useful, 1, 0, None), + ItemDef(400021, 'Magical Rod', ItemClassification.useful, 1, 0, None), + ItemDef(400022, 'Pendant', ItemClassification.useful, 1, 0, None), + ItemDef(400023, 'Hourglass', ItemClassification.filler, 6, 0, None), + # We need at least 4 red potions for the Tower of Red Potion. Up to the player to save them up! + ItemDef(400024, 'Red Potion', ItemClassification.filler, 15, 4, None), + ItemDef(400025, 'Elixir', ItemClassification.filler, 4, 0, None), + ItemDef(400026, 'Glove', ItemClassification.filler, 5, 0, None), + ItemDef(400027, 'Ointment', ItemClassification.filler, 8, 0, None), + ItemDef(400028, 'Poison', ItemClassification.trap, 13, 0, None), + ItemDef(None, 'Killed Evil One', ItemClassification.progression, 1, 0, 'Evil One'), + # Placeholder item so the game knows which shop slot to prefill wingboots + ItemDef(400029, 'Wingboots', ItemClassification.useful, 0, 0, None), +] diff --git a/worlds/faxanadu/Locations.py b/worlds/faxanadu/Locations.py new file mode 100644 index 00000000..ebb785f9 --- /dev/null +++ b/worlds/faxanadu/Locations.py @@ -0,0 +1,199 @@ +from typing import List, Optional + + +class LocationType(): + world = 1 # Just standing there in the world + hidden = 2 # Kill all monsters in the room to reveal, each "item room" counter tick. + boss_reward = 3 # Kill a boss to reveal the item + shop = 4 # Buy at a shop + give = 5 # Given by an NPC + spring = 6 # Activatable spring + boss = 7 # Entity to kill to trigger the check + + +class ItemType(): + unknown = 0 # Or don't care + red_potion = 1 + + +class LocationDef: + def __init__(self, id: Optional[int], name: str, region: str, type: int, original_item: int): + self.id = id + self.name = name + self.region = region + self.type = type + self.original_item = original_item + + +locations: List[LocationDef] = [ + # Eolis + LocationDef(400100, 'Eolis Guru', 'Eolis', LocationType.give, ItemType.unknown), + LocationDef(400101, 'Eolis Key Jack', 'Eolis', LocationType.shop, ItemType.unknown), + LocationDef(400102, 'Eolis Hand Dagger', 'Eolis', LocationType.shop, ItemType.unknown), + LocationDef(400103, 'Eolis Red Potion', 'Eolis', LocationType.shop, ItemType.red_potion), + LocationDef(400104, 'Eolis Elixir', 'Eolis', LocationType.shop, ItemType.unknown), + LocationDef(400105, 'Eolis Deluge', 'Eolis', LocationType.shop, ItemType.unknown), + + # Path to Apolune + LocationDef(400106, 'Path to Apolune Magic Shield', 'Path to Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400107, 'Path to Apolune Death', 'Path to Apolune', LocationType.shop, ItemType.unknown), + + # Apolune + LocationDef(400108, 'Apolune Small Shield', 'Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400109, 'Apolune Hand Dagger', 'Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400110, 'Apolune Deluge', 'Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400111, 'Apolune Red Potion', 'Apolune', LocationType.shop, ItemType.red_potion), + LocationDef(400112, 'Apolune Key Jack', 'Apolune', LocationType.shop, ItemType.unknown), + + # Tower of Trunk + LocationDef(400113, 'Tower of Trunk Hidden Mattock', 'Tower of Trunk', LocationType.hidden, ItemType.unknown), + LocationDef(400114, 'Tower of Trunk Hidden Hourglass', 'Tower of Trunk', LocationType.hidden, ItemType.unknown), + LocationDef(400115, 'Tower of Trunk Boss Mattock', 'Tower of Trunk', LocationType.boss_reward, ItemType.unknown), + + # Path to Forepaw + LocationDef(400116, 'Path to Forepaw Hidden Red Potion', 'Path to Forepaw', LocationType.hidden, ItemType.red_potion), + LocationDef(400117, 'Path to Forepaw Glove', 'Path to Forepaw', LocationType.world, ItemType.unknown), + + # Forepaw + LocationDef(400118, 'Forepaw Long Sword', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400119, 'Forepaw Studded Mail', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400120, 'Forepaw Small Shield', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400121, 'Forepaw Red Potion', 'Forepaw', LocationType.shop, ItemType.red_potion), + LocationDef(400122, 'Forepaw Wingboots', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400123, 'Forepaw Key Jack', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400124, 'Forepaw Key Queen', 'Forepaw', LocationType.shop, ItemType.unknown), + + # Trunk + LocationDef(400125, 'Trunk Hidden Ointment', 'Trunk', LocationType.hidden, ItemType.unknown), + LocationDef(400126, 'Trunk Hidden Red Potion', 'Trunk', LocationType.hidden, ItemType.red_potion), + LocationDef(400127, 'Trunk Red Potion', 'Trunk', LocationType.world, ItemType.red_potion), + LocationDef(None, 'Sky Spring', 'Trunk', LocationType.spring, ItemType.unknown), + + # Joker Spring + LocationDef(400128, 'Joker Spring Ruby Ring', 'Joker Spring', LocationType.give, ItemType.unknown), + LocationDef(None, 'Joker Spring', 'Joker Spring', LocationType.spring, ItemType.unknown), + + # Tower of Fortress + LocationDef(400129, 'Tower of Fortress Poison 1', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400130, 'Tower of Fortress Poison 2', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400131, 'Tower of Fortress Hidden Wingboots', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400132, 'Tower of Fortress Ointment', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400133, 'Tower of Fortress Boss Wingboots', 'Tower of Fortress', LocationType.boss_reward, ItemType.unknown), + LocationDef(400134, 'Tower of Fortress Elixir', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400135, 'Tower of Fortress Guru', 'Tower of Fortress', LocationType.give, ItemType.unknown), + LocationDef(None, 'Tower of Fortress Spring', 'Tower of Fortress', LocationType.spring, ItemType.unknown), + + # Path to Mascon + LocationDef(400136, 'Path to Mascon Hidden Wingboots', 'Path to Mascon', LocationType.hidden, ItemType.unknown), + + # Tower of Red Potion + LocationDef(400137, 'Tower of Red Potion', 'Tower of Red Potion', LocationType.world, ItemType.red_potion), + + # Mascon + LocationDef(400138, 'Mascon Large Shield', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400139, 'Mascon Thunder', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400140, 'Mascon Mattock', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400141, 'Mascon Red Potion', 'Mascon', LocationType.shop, ItemType.red_potion), + LocationDef(400142, 'Mascon Key Jack', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400143, 'Mascon Key Queen', 'Mascon', LocationType.shop, ItemType.unknown), + + # Path to Victim + LocationDef(400144, 'Misty Shop Death', 'Path to Victim', LocationType.shop, ItemType.unknown), + LocationDef(400145, 'Misty Shop Hourglass', 'Path to Victim', LocationType.shop, ItemType.unknown), + LocationDef(400146, 'Misty Shop Elixir', 'Path to Victim', LocationType.shop, ItemType.unknown), + LocationDef(400147, 'Misty Shop Red Potion', 'Path to Victim', LocationType.shop, ItemType.red_potion), + LocationDef(400148, 'Misty Doctor Office', 'Path to Victim', LocationType.hidden, ItemType.unknown), + + # Tower of Suffer + LocationDef(400149, 'Tower of Suffer Hidden Wingboots', 'Tower of Suffer', LocationType.hidden, ItemType.unknown), + LocationDef(400150, 'Tower of Suffer Hidden Hourglass', 'Tower of Suffer', LocationType.hidden, ItemType.unknown), + LocationDef(400151, 'Tower of Suffer Pendant', 'Tower of Suffer', LocationType.boss_reward, ItemType.unknown), + + # Victim + LocationDef(400152, 'Victim Full Plate', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400153, 'Victim Mattock', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400154, 'Victim Red Potion', 'Victim', LocationType.shop, ItemType.red_potion), + LocationDef(400155, 'Victim Key King', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400156, 'Victim Key Queen', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400157, 'Victim Tavern', 'Mist', LocationType.give, ItemType.unknown), + + # Mist + LocationDef(400158, 'Mist Hidden Poison 1', 'Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400159, 'Mist Hidden Poison 2', 'Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400160, 'Mist Hidden Wingboots', 'Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400161, 'Misty Magic Hall', 'Mist', LocationType.give, ItemType.unknown), + LocationDef(400162, 'Misty House', 'Mist', LocationType.give, ItemType.unknown), + + # Useless Tower + LocationDef(400163, 'Useless Tower', 'Useless Tower', LocationType.hidden, ItemType.unknown), + + # Tower of Mist + LocationDef(400164, 'Tower of Mist Hidden Ointment', 'Tower of Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400165, 'Tower of Mist Elixir', 'Tower of Mist', LocationType.world, ItemType.unknown), + LocationDef(400166, 'Tower of Mist Black Onyx', 'Tower of Mist', LocationType.boss_reward, ItemType.unknown), + + # Path to Conflate + LocationDef(400167, 'Path to Conflate Hidden Ointment', 'Path to Conflate', LocationType.hidden, ItemType.unknown), + LocationDef(400168, 'Path to Conflate Poison', 'Path to Conflate', LocationType.hidden, ItemType.unknown), + + # Helm Branch + LocationDef(400169, 'Helm Branch Hidden Glove', 'Helm Branch', LocationType.hidden, ItemType.unknown), + LocationDef(400170, 'Helm Branch Battle Helmet', 'Helm Branch', LocationType.boss_reward, ItemType.unknown), + + # Conflate + LocationDef(400171, 'Conflate Giant Blade', 'Conflate', LocationType.shop, ItemType.unknown), + LocationDef(400172, 'Conflate Magic Shield', 'Conflate', LocationType.shop, ItemType.unknown), + LocationDef(400173, 'Conflate Wingboots', 'Conflate', LocationType.shop, ItemType.unknown), + LocationDef(400174, 'Conflate Red Potion', 'Conflate', LocationType.shop, ItemType.red_potion), + LocationDef(400175, 'Conflate Guru', 'Conflate', LocationType.give, ItemType.unknown), + + # Branches + LocationDef(400176, 'Branches Hidden Ointment', 'Branches', LocationType.hidden, ItemType.unknown), + LocationDef(400177, 'Branches Poison', 'Branches', LocationType.world, ItemType.unknown), + LocationDef(400178, 'Branches Hidden Mattock', 'Branches', LocationType.hidden, ItemType.unknown), + LocationDef(400179, 'Branches Hidden Hourglass', 'Branches', LocationType.hidden, ItemType.unknown), + + # Path to Daybreak + LocationDef(400180, 'Path to Daybreak Hidden Wingboots 1', 'Path to Daybreak', LocationType.hidden, ItemType.unknown), + LocationDef(400181, 'Path to Daybreak Magical Rod', 'Path to Daybreak', LocationType.world, ItemType.unknown), + LocationDef(400182, 'Path to Daybreak Hidden Wingboots 2', 'Path to Daybreak', LocationType.hidden, ItemType.unknown), + LocationDef(400183, 'Path to Daybreak Poison', 'Path to Daybreak', LocationType.world, ItemType.unknown), + LocationDef(400184, 'Path to Daybreak Glove', 'Path to Daybreak', LocationType.world, ItemType.unknown), + LocationDef(400185, 'Path to Daybreak Battle Suit', 'Path to Daybreak', LocationType.boss_reward, ItemType.unknown), + + # Daybreak + LocationDef(400186, 'Daybreak Tilte', 'Daybreak', LocationType.shop, ItemType.unknown), + LocationDef(400187, 'Daybreak Giant Blade', 'Daybreak', LocationType.shop, ItemType.unknown), + LocationDef(400188, 'Daybreak Red Potion', 'Daybreak', LocationType.shop, ItemType.red_potion), + LocationDef(400189, 'Daybreak Key King', 'Daybreak', LocationType.shop, ItemType.unknown), + LocationDef(400190, 'Daybreak Key Queen', 'Daybreak', LocationType.shop, ItemType.unknown), + + # Dartmoor Castle + LocationDef(400191, 'Dartmoor Castle Hidden Hourglass', 'Dartmoor Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400192, 'Dartmoor Castle Hidden Red Potion', 'Dartmoor Castle', LocationType.hidden, ItemType.red_potion), + + # Dartmoor + LocationDef(400193, 'Dartmoor Giant Blade', 'Dartmoor', LocationType.shop, ItemType.unknown), + LocationDef(400194, 'Dartmoor Red Potion', 'Dartmoor', LocationType.shop, ItemType.red_potion), + LocationDef(400195, 'Dartmoor Key King', 'Dartmoor', LocationType.shop, ItemType.unknown), + + # Fraternal Castle + LocationDef(400196, 'Fraternal Castle Hidden Ointment', 'Fraternal Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400197, 'Fraternal Castle Shop Hidden Ointment', 'Fraternal Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400198, 'Fraternal Castle Poison 1', 'Fraternal Castle', LocationType.world, ItemType.unknown), + LocationDef(400199, 'Fraternal Castle Poison 2', 'Fraternal Castle', LocationType.world, ItemType.unknown), + LocationDef(400200, 'Fraternal Castle Poison 3', 'Fraternal Castle', LocationType.world, ItemType.unknown), + # LocationDef(400201, 'Fraternal Castle Red Potion', 'Fraternal Castle', LocationType.world, ItemType.red_potion), # This location is inaccessible. Keeping commented for context. + LocationDef(400202, 'Fraternal Castle Hidden Hourglass', 'Fraternal Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400203, 'Fraternal Castle Dragon Slayer', 'Fraternal Castle', LocationType.boss_reward, ItemType.unknown), + LocationDef(400204, 'Fraternal Castle Guru', 'Fraternal Castle', LocationType.give, ItemType.unknown), + + # Evil Fortress + LocationDef(400205, 'Evil Fortress Ointment', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400206, 'Evil Fortress Poison 1', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400207, 'Evil Fortress Glove', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400208, 'Evil Fortress Poison 2', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400209, 'Evil Fortress Poison 3', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400210, 'Evil Fortress Hidden Glove', 'Evil Fortress', LocationType.hidden, ItemType.unknown), + LocationDef(None, 'Evil One', 'Evil Fortress', LocationType.boss, ItemType.unknown), +] diff --git a/worlds/faxanadu/Options.py b/worlds/faxanadu/Options.py new file mode 100644 index 00000000..dbcb5789 --- /dev/null +++ b/worlds/faxanadu/Options.py @@ -0,0 +1,107 @@ +from Options import PerGameCommonOptions, Toggle, DefaultOnToggle, StartInventoryPool, Choice +from dataclasses import dataclass + + +class KeepShopRedPotions(Toggle): + """ + Prevents the Shop's Red Potions from being shuffled. Those locations + will have purchasable Red Potion as usual for their usual price. + """ + display_name = "Keep Shop Red Potions" + + +class IncludePendant(Toggle): + """ + Pendant is an item that boosts your attack power permanently when picked up. + However, due to a programming error in the original game, it has the reverse + effect. You start with the Pendant power, and lose it when picking + it up. So this item is essentially a trap. + There is a setting in the client to reverse the effect back to its original intend. + This could be used in conjunction with this option to increase or lower difficulty. + """ + display_name = "Include Pendant" + + +class IncludePoisons(DefaultOnToggle): + """ + Whether or not to include Poison Potions in the pool of items. Including them + effectively turn them into traps in multiplayer. + """ + display_name = "Include Poisons" + + +class RequireDragonSlayer(Toggle): + """ + Requires the Dragon Slayer to be available before fighting the final boss is required. + Turning this on will turn Progressive Shields into progression items. + + This setting does not force you to use Dragon Slayer to kill the final boss. + Instead, it ensures that you will have the Dragon Slayer and be able to equip + it before you are expected to beat the final boss. + """ + display_name = "Require Dragon Slayer" + + +class RandomMusic(Toggle): + """ + All levels' music is shuffled. Except the title screen because it's finite. + This is an aesthetic option and doesn't affect gameplay. + """ + display_name = "Random Musics" + + +class RandomSound(Toggle): + """ + All sounds are shuffled. + This is an aesthetic option and doesn't affect gameplay. + """ + display_name = "Random Sounds" + + +class RandomNPC(Toggle): + """ + NPCs and their portraits are shuffled. + This is an aesthetic option and doesn't affect gameplay. + """ + display_name = "Random NPCs" + + +class RandomMonsters(Choice): + """ + Choose how monsters are randomized. + "Vanilla": No randomization + "Level Shuffle": Monsters are shuffled within a level + "Level Random": Monsters are picked randomly, balanced based on the ratio of the current level + "World Shuffle": Monsters are shuffled across the entire world + "World Random": Monsters are picked randomly, balanced based on the ratio of the entire world + "Chaotic": Completely random, except big vs small ratio is kept. Big are mini-bosses. + """ + display_name = "Random Monsters" + option_vanilla = 0 + option_level_shuffle = 1 + option_level_random = 2 + option_world_shuffle = 3 + option_world_random = 4 + option_chaotic = 5 + default = 0 + + +class RandomRewards(Toggle): + """ + Monsters drops are shuffled. + """ + display_name = "Random Rewards" + + +@dataclass +class FaxanaduOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + keep_shop_red_potions: KeepShopRedPotions + include_pendant: IncludePendant + include_poisons: IncludePoisons + require_dragon_slayer: RequireDragonSlayer + random_musics: RandomMusic + random_sounds: RandomSound + random_npcs: RandomNPC + random_monsters: RandomMonsters + random_rewards: RandomRewards diff --git a/worlds/faxanadu/Regions.py b/worlds/faxanadu/Regions.py new file mode 100644 index 00000000..9db11d8e --- /dev/null +++ b/worlds/faxanadu/Regions.py @@ -0,0 +1,66 @@ +from BaseClasses import Region +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import FaxanaduWorld + + +def create_region(name, player, multiworld): + region = Region(name, player, multiworld) + multiworld.regions.append(region) + return region + + +def create_regions(faxanadu_world: "FaxanaduWorld"): + player = faxanadu_world.player + multiworld = faxanadu_world.multiworld + + # Create regions + menu = create_region("Menu", player, multiworld) + eolis = create_region("Eolis", player, multiworld) + path_to_apolune = create_region("Path to Apolune", player, multiworld) + apolune = create_region("Apolune", player, multiworld) + create_region("Tower of Trunk", player, multiworld) + path_to_forepaw = create_region("Path to Forepaw", player, multiworld) + forepaw = create_region("Forepaw", player, multiworld) + trunk = create_region("Trunk", player, multiworld) + create_region("Joker Spring", player, multiworld) + create_region("Tower of Fortress", player, multiworld) + path_to_mascon = create_region("Path to Mascon", player, multiworld) + create_region("Tower of Red Potion", player, multiworld) + mascon = create_region("Mascon", player, multiworld) + path_to_victim = create_region("Path to Victim", player, multiworld) + create_region("Tower of Suffer", player, multiworld) + victim = create_region("Victim", player, multiworld) + mist = create_region("Mist", player, multiworld) + create_region("Useless Tower", player, multiworld) + create_region("Tower of Mist", player, multiworld) + path_to_conflate = create_region("Path to Conflate", player, multiworld) + create_region("Helm Branch", player, multiworld) + create_region("Conflate", player, multiworld) + branches = create_region("Branches", player, multiworld) + path_to_daybreak = create_region("Path to Daybreak", player, multiworld) + daybreak = create_region("Daybreak", player, multiworld) + dartmoor_castle = create_region("Dartmoor Castle", player, multiworld) + create_region("Dartmoor", player, multiworld) + create_region("Fraternal Castle", player, multiworld) + create_region("Evil Fortress", player, multiworld) + + # Create connections + menu.add_exits(["Eolis"]) + eolis.add_exits(["Path to Apolune"]) + path_to_apolune.add_exits(["Apolune"]) + apolune.add_exits(["Tower of Trunk", "Path to Forepaw"]) + path_to_forepaw.add_exits(["Forepaw"]) + forepaw.add_exits(["Trunk"]) + trunk.add_exits(["Joker Spring", "Tower of Fortress", "Path to Mascon"]) + path_to_mascon.add_exits(["Tower of Red Potion", "Mascon"]) + mascon.add_exits(["Path to Victim"]) + path_to_victim.add_exits(["Tower of Suffer", "Victim"]) + victim.add_exits(["Mist"]) + mist.add_exits(["Useless Tower", "Tower of Mist", "Path to Conflate"]) + path_to_conflate.add_exits(["Helm Branch", "Conflate", "Branches"]) + branches.add_exits(["Path to Daybreak"]) + path_to_daybreak.add_exits(["Daybreak"]) + daybreak.add_exits(["Dartmoor Castle"]) + dartmoor_castle.add_exits(["Dartmoor", "Fraternal Castle", "Evil Fortress"]) diff --git a/worlds/faxanadu/Rules.py b/worlds/faxanadu/Rules.py new file mode 100644 index 00000000..a48b442c --- /dev/null +++ b/worlds/faxanadu/Rules.py @@ -0,0 +1,79 @@ +from typing import TYPE_CHECKING +from worlds.generic.Rules import set_rule + +if TYPE_CHECKING: + from . import FaxanaduWorld + + +def can_buy_in_eolis(state, player): + # Sword or Deluge so we can farm for gold. + # Ring of Elf so we can get 1500 from the King. + return state.has_any(["Progressive Sword", "Deluge", "Ring of Elf"], player) + + +def has_any_magic(state, player): + return state.has_any(["Deluge", "Thunder", "Fire", "Death", "Tilte"], player) + + +def set_rules(faxanadu_world: "FaxanaduWorld"): + player = faxanadu_world.player + multiworld = faxanadu_world.multiworld + + # Region rules + set_rule(multiworld.get_entrance("Eolis -> Path to Apolune", player), lambda state: + state.has_all(["Key Jack", "Progressive Sword"], player)) # You can't go far with magic only + set_rule(multiworld.get_entrance("Apolune -> Tower of Trunk", player), lambda state: state.has("Key Jack", player)) + set_rule(multiworld.get_entrance("Apolune -> Path to Forepaw", player), lambda state: state.has("Mattock", player)) + set_rule(multiworld.get_entrance("Trunk -> Joker Spring", player), lambda state: state.has("Key Joker", player)) + set_rule(multiworld.get_entrance("Trunk -> Tower of Fortress", player), lambda state: state.has("Key Jack", player)) + set_rule(multiworld.get_entrance("Trunk -> Path to Mascon", player), lambda state: + state.has_all(["Key Queen", "Ring of Ruby", "Sky Spring Flow", "Tower of Fortress Spring Flow", "Joker Spring Flow"], player) and + state.has("Progressive Sword", player, 2)) + set_rule(multiworld.get_entrance("Path to Mascon -> Tower of Red Potion", player), lambda state: + state.has("Key Queen", player) and + state.has("Red Potion", player, 4)) # It's impossible to go through the tower of Red Potion without at least 1-2 potions. Give them 4 for good measure. + set_rule(multiworld.get_entrance("Path to Victim -> Tower of Suffer", player), lambda state: state.has("Key Queen", player)) + set_rule(multiworld.get_entrance("Path to Victim -> Victim", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_entrance("Mist -> Useless Tower", player), lambda state: + state.has_all(["Key King", "Unlock Wingboots"], player)) + set_rule(multiworld.get_entrance("Mist -> Tower of Mist", player), lambda state: state.has("Key King", player)) + set_rule(multiworld.get_entrance("Mist -> Path to Conflate", player), lambda state: state.has("Key Ace", player)) + set_rule(multiworld.get_entrance("Path to Conflate -> Helm Branch", player), lambda state: state.has("Key King", player)) + set_rule(multiworld.get_entrance("Path to Conflate -> Branches", player), lambda state: state.has("Key King", player)) + set_rule(multiworld.get_entrance("Daybreak -> Dartmoor Castle", player), lambda state: state.has("Ring of Dworf", player)) + set_rule(multiworld.get_entrance("Dartmoor Castle -> Evil Fortress", player), lambda state: state.has("Demons Ring", player)) + + # Location rules + set_rule(multiworld.get_location("Eolis Key Jack", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Hand Dagger", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Elixir", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Deluge", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Red Potion", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Path to Apolune Magic Shield", player), lambda state: state.has("Key King", player)) # Mid-late cost, make sure we've progressed + set_rule(multiworld.get_location("Path to Apolune Death", player), lambda state: state.has("Key Ace", player)) # Mid-late cost, make sure we've progressed + set_rule(multiworld.get_location("Tower of Trunk Hidden Mattock", player), lambda state: + # This is actually possible if the monster drop into the stairs and kill it with dagger. But it's a "pro move" + state.has("Deluge", player, 1) or + state.has("Progressive Sword", player, 2)) + set_rule(multiworld.get_location("Path to Forepaw Glove", player), lambda state: + state.has_all(["Deluge", "Unlock Wingboots"], player)) + set_rule(multiworld.get_location("Trunk Red Potion", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Sky Spring", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Tower of Fortress Spring", player), lambda state: state.has("Spring Elixir", player)) + set_rule(multiworld.get_location("Tower of Fortress Guru", player), lambda state: state.has("Sky Spring Flow", player)) + set_rule(multiworld.get_location("Tower of Suffer Hidden Wingboots", player), lambda state: + state.has("Deluge", player) or + state.has("Progressive Sword", player, 2)) + set_rule(multiworld.get_location("Misty House", player), lambda state: state.has("Black Onyx", player)) + set_rule(multiworld.get_location("Misty Doctor Office", player), lambda state: has_any_magic(state, player)) + set_rule(multiworld.get_location("Conflate Guru", player), lambda state: state.has("Progressive Armor", player, 3)) + set_rule(multiworld.get_location("Branches Hidden Mattock", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Path to Daybreak Glove", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Dartmoor Castle Hidden Hourglass", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Dartmoor Castle Hidden Red Potion", player), lambda state: has_any_magic(state, player)) + set_rule(multiworld.get_location("Fraternal Castle Guru", player), lambda state: state.has("Progressive Sword", player, 4)) + set_rule(multiworld.get_location("Fraternal Castle Shop Hidden Ointment", player), lambda state: has_any_magic(state, player)) + + if faxanadu_world.options.require_dragon_slayer.value: + set_rule(multiworld.get_location("Evil One", player), lambda state: + state.has_all_counts({"Progressive Sword": 4, "Progressive Armor": 3, "Progressive Shield": 4}, player)) diff --git a/worlds/faxanadu/__init__.py b/worlds/faxanadu/__init__.py new file mode 100644 index 00000000..ca17c067 --- /dev/null +++ b/worlds/faxanadu/__init__.py @@ -0,0 +1,189 @@ +from typing import Any, Dict, List + +from BaseClasses import Item, Location, Tutorial, ItemClassification, MultiWorld +from worlds.AutoWorld import WebWorld, World +from . import Items, Locations, Regions, Rules +from .Options import FaxanaduOptions +from worlds.generic.Rules import set_rule + + +DAXANADU_VERSION = "0.3.0" + + +class FaxanaduLocation(Location): + game: str = "Faxanadu" + + +class FaxanaduItem(Item): + game: str = "Faxanadu" + + +class FaxanaduWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Faxanadu randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["Daivuk"] + )] + theme = "dirt" + + +class FaxanaduWorld(World): + """ + Faxanadu is an action role-playing platform video game developed by Hudson Soft for the Nintendo Entertainment System + """ + options_dataclass = FaxanaduOptions + options: FaxanaduOptions + game = "Faxanadu" + web = FaxanaduWeb() + + item_name_to_id = {item.name: item.id for item in Items.items if item.id is not None} + item_name_to_item = {item.name: item for item in Items.items} + location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None} + + def __init__(self, world: MultiWorld, player: int): + self.filler_ratios: Dict[str, int] = { + item.name: item.count + for item in Items.items + if item.classification in [ItemClassification.filler, ItemClassification.trap] + } + # Remove poison by default to respect itemlinking + self.filler_ratios["Poison"] = 0 + super().__init__(world, player) + + def create_regions(self): + Regions.create_regions(self) + + # Add locations into regions + for region in self.multiworld.get_regions(self.player): + for loc in [location for location in Locations.locations if location.region == region.name]: + location = FaxanaduLocation(self.player, loc.name, loc.id, region) + + # In Faxanadu, Poison hurts you when picked up. It makes no sense to sell them in shops + if loc.type == Locations.LocationType.shop: + location.item_rule = lambda item, player=self.player: not (player == item.player and item.name == "Poison") + + region.locations.append(location) + + def set_rules(self): + Rules.set_rules(self) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Killed Evil One", self.player) + + def create_item(self, name: str) -> FaxanaduItem: + item: Items.ItemDef = self.item_name_to_item[name] + return FaxanaduItem(name, item.classification, item.id, self.player) + + # Returns how many red potions were prefilled into shops + def prefill_shop_red_potions(self) -> int: + red_potion_in_shop_count = 0 + if self.options.keep_shop_red_potions: + red_potion_item = self.item_name_to_item["Red Potion"] + red_potion_shop_locations = [ + loc + for loc in Locations.locations + if loc.type == Locations.LocationType.shop and loc.original_item == Locations.ItemType.red_potion + ] + for loc in red_potion_shop_locations: + location = self.get_location(loc.name) + location.place_locked_item(FaxanaduItem(red_potion_item.name, red_potion_item.classification, red_potion_item.id, self.player)) + red_potion_in_shop_count += 1 + return red_potion_in_shop_count + + def put_wingboot_in_shop(self, shops, region_name): + item = self.item_name_to_item["Wingboots"] + shop = shops.pop(region_name) + slot = self.random.randint(0, len(shop) - 1) + loc = shop[slot] + location = self.get_location(loc.name) + location.place_locked_item(FaxanaduItem(item.name, item.classification, item.id, self.player)) + + # Put a rule right away that we need to have to unlocked. + set_rule(location, lambda state: state.has("Unlock Wingboots", self.player)) + + # Returns how many wingboots were prefilled into shops + def prefill_shop_wingboots(self) -> int: + # Collect shops + shops: Dict[str, List[Locations.LocationDef]] = {} + for loc in Locations.locations: + if loc.type == Locations.LocationType.shop: + if self.options.keep_shop_red_potions and loc.original_item == Locations.ItemType.red_potion: + continue # Don't override our red potions + shops.setdefault(loc.region, []).append(loc) + + shop_count = len(shops) + wingboots_count = round(shop_count / 2.5) # On 10 shops, we should have about 4 shops with wingboots + + # At least one should be in the first 4 shops. Because we require wingboots to progress past that point. + must_have_regions = [region for i, region in enumerate(shops) if i < 4] + self.put_wingboot_in_shop(shops, self.random.choice(must_have_regions)) + + # Fill in the rest randomly in remaining shops + for i in range(wingboots_count - 1): # -1 because we added one already + region = self.random.choice(list(shops.keys())) + self.put_wingboot_in_shop(shops, region) + + return wingboots_count + + def create_items(self) -> None: + itempool: List[FaxanaduItem] = [] + + # Prefill red potions in shops if option is set + red_potion_in_shop_count = self.prefill_shop_red_potions() + + # Prefill wingboots in shops + wingboots_in_shop_count = self.prefill_shop_wingboots() + + # Create the item pool, excluding fillers. + prefilled_count = red_potion_in_shop_count + wingboots_in_shop_count + for item in Items.items: + # Ignore pendant if turned off + if item.name == "Pendant" and not self.options.include_pendant: + continue + + # ignore fillers for now, we will fill them later + if item.classification in [ItemClassification.filler, ItemClassification.trap] and \ + item.progression_count == 0: + continue + + prefill_loc = None + if item.prefill_location: + prefill_loc = self.get_location(item.prefill_location) + + # if require dragon slayer is turned on, we need progressive shields to be progression + item_classification = item.classification + if self.options.require_dragon_slayer and item.name == "Progressive Shield": + item_classification = ItemClassification.progression + + if prefill_loc: + prefill_loc.place_locked_item(FaxanaduItem(item.name, item_classification, item.id, self.player)) + prefilled_count += 1 + else: + for i in range(item.count - item.progression_count): + itempool.append(FaxanaduItem(item.name, item_classification, item.id, self.player)) + for i in range(item.progression_count): + itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player)) + + # Adjust filler ratios + # If red potions are locked in shops, remove the count from the ratio. + self.filler_ratios["Red Potion"] -= red_potion_in_shop_count + + # Add poisons if desired + if self.options.include_poisons: + self.filler_ratios["Poison"] = self.item_name_to_item["Poison"].count + + # Randomly add fillers to the pool with ratios based on og game occurrence counts. + filler_count = len(Locations.locations) - len(itempool) - prefilled_count + for i in range(filler_count): + itempool.append(self.create_item(self.get_filler_item_name())) + + self.multiworld.itempool += itempool + + def get_filler_item_name(self) -> str: + return self.random.choices(list(self.filler_ratios.keys()), weights=list(self.filler_ratios.values()))[0] + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data = self.options.as_dict("keep_shop_red_potions", "random_musics", "random_sounds", "random_npcs", "random_monsters", "random_rewards") + slot_data["daxanadu_version"] = DAXANADU_VERSION + return slot_data diff --git a/worlds/faxanadu/docs/en_Faxanadu.md b/worlds/faxanadu/docs/en_Faxanadu.md new file mode 100644 index 00000000..7f5c4ab2 --- /dev/null +++ b/worlds/faxanadu/docs/en_Faxanadu.md @@ -0,0 +1,27 @@ +# Faxanadu + +## Where is the settings page? + +The [player options page](../player-options) contains the options needed to configure your game session. + +## What does randomization do to this game? + +All game items collected in the map, shops, and boss drops are randomized. + +Keys are unique. Once you get the Jack Key, you can open all Jack doors; the key stays in your inventory. + +Wingboots are randomized across shops only. They are LOCKED and cannot be bought until you get the item that unlocks them. + +Normal Elixirs don't revive the tower spring. A new item, Spring Elixir, is necessary. This new item is unique. + +## What is the goal? + +The goal is to kill the Evil One. + +## What is a "check" in The Faxanadu? + +Shop items, item locations in the world, boss drops, and secret items. + +## What "items" can you unlock in Faxanadu? + +Keys, Armors, Weapons, Potions, Shields, Magics, Poisons, Gloves, etc. diff --git a/worlds/faxanadu/docs/setup_en.md b/worlds/faxanadu/docs/setup_en.md new file mode 100644 index 00000000..4ff714c6 --- /dev/null +++ b/worlds/faxanadu/docs/setup_en.md @@ -0,0 +1,32 @@ +# Faxanadu Randomizer Setup + +## Required Software + +- [Daxanadu](https://github.com/Daivuk/Daxanadu/releases/) +- Faxanadu ROM, English version + +## Optional Software + +- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Installing Daxanadu +1. Download [Daxanadu.zip](https://github.com/Daivuk/Daxanadu/releases/) and extract it. +2. Copy your rom `Faxanadu (U).nes` into the newly extracted folder. + +## Joining a MultiWorld Game + +1. Launch Daxanadu.exe +2. From the Main menu, go to the `ARCHIPELAGO` menu. Enter the server's address, slot name, and password. Then select `PLAY`. +3. Enjoy! + +To continue a game, follow the same connection steps. +Connecting with a different seed won't erase your progress in other seeds. + +## Archipelago Text Client + +We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. +Daxanadu doesn't display messages. You'll only get popups when picking them up. + +## Auto-Tracking + +Daxanadu has an integrated tracker that can be toggled in the options. diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index 7de48631..401c240a 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -47,6 +47,17 @@ def get_flag(data, flag): bit = int(0x80 / (2 ** (flag % 8))) return (data[byte] & bit) > 0 +def validate_read_state(data1, data2): + validation_array = bytes([0x01, 0x46, 0x46, 0x4D, 0x51, 0x52]) + + if data1 is None or data2 is None: + return False + for i in range(6): + if data1[i] != validation_array[i] or data2[i] != validation_array[i]: + return False; + return True + + class FFMQClient(SNIClient): game = "Final Fantasy Mystic Quest" @@ -67,11 +78,11 @@ class FFMQClient(SNIClient): async def game_watcher(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - check_1 = await snes_read(ctx, 0xF53749, 1) + check_1 = await snes_read(ctx, 0xF53749, 6) received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) - check_2 = await snes_read(ctx, 0xF53749, 1) - if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'): + check_2 = await snes_read(ctx, 0xF53749, 6) + if not validate_read_state(check_1, check_2): return def get_range(data_range): diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py index d0898d7e..31453a0f 100644 --- a/worlds/ffmq/Items.py +++ b/worlds/ffmq/Items.py @@ -222,10 +222,10 @@ for item, data in item_table.items(): def create_items(self) -> None: items = [] - starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ") + starting_weapon = self.options.starting_weapon.current_key.title().replace("_", " ") self.multiworld.push_precollected(self.create_item(starting_weapon)) self.multiworld.push_precollected(self.create_item("Steel Armor")) - if self.multiworld.sky_coin_mode[self.player] == "start_with": + if self.options.sky_coin_mode == "start_with": self.multiworld.push_precollected(self.create_item("Sky Coin")) precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]} @@ -233,41 +233,42 @@ def create_items(self) -> None: def add_item(item_name): if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name: return - if item_name.lower().replace(" ", "_") == self.multiworld.starting_weapon[self.player].current_key: + if item_name.lower().replace(" ", "_") == self.options.starting_weapon.current_key: return - if self.multiworld.progressive_gear[self.player]: + if self.options.progressive_gear: for item_group in prog_map: if item_name in self.item_name_groups[item_group]: item_name = prog_map[item_group] break if item_name == "Sky Coin": - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": for _ in range(40): items.append(self.create_item("Sky Fragment")) return - elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + elif self.options.sky_coin_mode == "save_the_crystals": items.append(self.create_filler()) return if item_name in precollected_item_names: items.append(self.create_filler()) return i = self.create_item(item_name) - if self.multiworld.logic[self.player] != "friendly" and item_name in ("Magic Mirror", "Mask"): + if self.options.logic != "friendly" and item_name in ("Magic Mirror", "Mask"): i.classification = ItemClassification.useful - if (self.multiworld.logic[self.player] == "expert" and self.multiworld.map_shuffle[self.player] == "none" and + if (self.options.logic == "expert" and self.options.map_shuffle == "none" and item_name == "Exit Book"): i.classification = ItemClassification.progression items.append(i) for item_group in ("Key Items", "Spells", "Armors", "Helms", "Shields", "Accessories", "Weapons"): - for item in self.item_name_groups[item_group]: + # Sort for deterministic order + for item in sorted(self.item_name_groups[item_group]): add_item(item) - if self.multiworld.brown_boxes[self.player] == "include": + if self.options.brown_boxes == "include": filler_items = [] for item, count in fillers.items(): filler_items += [self.create_item(item) for _ in range(count)] - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": self.multiworld.random.shuffle(filler_items) filler_items = filler_items[39:] items += filler_items diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index af3625f2..4dcf1467 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -1,4 +1,5 @@ -from Options import Choice, FreeText, Toggle, Range +from Options import Choice, FreeText, ItemsAccessibility, Toggle, Range, PerGameCommonOptions +from dataclasses import dataclass class Logic(Choice): @@ -321,36 +322,37 @@ class KaelisMomFightsMinotaur(Toggle): default = 0 -option_definitions = { - "logic": Logic, - "brown_boxes": BrownBoxes, - "sky_coin_mode": SkyCoinMode, - "shattered_sky_coin_quantity": ShatteredSkyCoinQuantity, - "starting_weapon": StartingWeapon, - "progressive_gear": ProgressiveGear, - "leveling_curve": LevelingCurve, - "starting_companion": StartingCompanion, - "available_companions": AvailableCompanions, - "companions_locations": CompanionsLocations, - "kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur, - "companion_leveling_type": CompanionLevelingType, - "companion_spellbook_type": CompanionSpellbookType, - "enemies_density": EnemiesDensity, - "enemies_scaling_lower": EnemiesScalingLower, - "enemies_scaling_upper": EnemiesScalingUpper, - "bosses_scaling_lower": BossesScalingLower, - "bosses_scaling_upper": BossesScalingUpper, - "enemizer_attacks": EnemizerAttacks, - "enemizer_groups": EnemizerGroups, - "shuffle_res_weak_types": ShuffleResWeakType, - "shuffle_enemies_position": ShuffleEnemiesPositions, - "progressive_formations": ProgressiveFormations, - "doom_castle_mode": DoomCastle, - "doom_castle_shortcut": DoomCastleShortcut, - "tweak_frustrating_dungeons": TweakFrustratingDungeons, - "map_shuffle": MapShuffle, - "crest_shuffle": CrestShuffle, - "shuffle_battlefield_rewards": ShuffleBattlefieldRewards, - "map_shuffle_seed": MapShuffleSeed, - "battlefields_battles_quantities": BattlefieldsBattlesQuantities, -} +@dataclass +class FFMQOptions(PerGameCommonOptions): + accessibility: ItemsAccessibility + logic: Logic + brown_boxes: BrownBoxes + sky_coin_mode: SkyCoinMode + shattered_sky_coin_quantity: ShatteredSkyCoinQuantity + starting_weapon: StartingWeapon + progressive_gear: ProgressiveGear + leveling_curve: LevelingCurve + starting_companion: StartingCompanion + available_companions: AvailableCompanions + companions_locations: CompanionsLocations + kaelis_mom_fight_minotaur: KaelisMomFightsMinotaur + companion_leveling_type: CompanionLevelingType + companion_spellbook_type: CompanionSpellbookType + enemies_density: EnemiesDensity + enemies_scaling_lower: EnemiesScalingLower + enemies_scaling_upper: EnemiesScalingUpper + bosses_scaling_lower: BossesScalingLower + bosses_scaling_upper: BossesScalingUpper + enemizer_attacks: EnemizerAttacks + enemizer_groups: EnemizerGroups + shuffle_res_weak_types: ShuffleResWeakType + shuffle_enemies_position: ShuffleEnemiesPositions + progressive_formations: ProgressiveFormations + doom_castle_mode: DoomCastle + doom_castle_shortcut: DoomCastleShortcut + tweak_frustrating_dungeons: TweakFrustratingDungeons + map_shuffle: MapShuffle + crest_shuffle: CrestShuffle + shuffle_battlefield_rewards: ShuffleBattlefieldRewards + map_shuffle_seed: MapShuffleSeed + battlefields_battles_quantities: BattlefieldsBattlesQuantities diff --git a/worlds/ffmq/Output.py b/worlds/ffmq/Output.py index 1b17aaa9..1e436a90 100644 --- a/worlds/ffmq/Output.py +++ b/worlds/ffmq/Output.py @@ -1,13 +1,13 @@ import yaml import os import zipfile +import Utils from copy import deepcopy from .Regions import object_id_table -from Utils import __version__ from worlds.Files import APPatch import pkgutil -settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader) +settings_template = Utils.parse_yaml(pkgutil.get_data(__name__, "data/settings.yaml")) def generate_output(self, output_directory): @@ -21,7 +21,7 @@ def generate_output(self, output_directory): item_name = "".join(item_name.split(" ")) else: if item.advancement or item.useful or (item.trap and - self.multiworld.per_slot_randoms[self.player].randint(0, 1)): + self.random.randint(0, 1)): item_name = "APItem" else: item_name = "APItemFiller" @@ -46,60 +46,60 @@ def generate_output(self, output_directory): options = deepcopy(settings_template) options["name"] = self.multiworld.player_name[self.player] option_writes = { - "enemies_density": cc(self.multiworld.enemies_density[self.player]), + "enemies_density": cc(self.options.enemies_density), "chests_shuffle": "Include", - "shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle", + "shuffle_boxes_content": self.options.brown_boxes == "shuffle", "npcs_shuffle": "Include", "battlefields_shuffle": "Include", - "logic_options": cc(self.multiworld.logic[self.player]), - "shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]), - "enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]), - "enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]), - "bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]), - "bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]), - "enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]), - "leveling_curve": cc(self.multiworld.leveling_curve[self.player]), - "battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if - self.multiworld.battlefields_battles_quantities[self.player].value < 5 else + "logic_options": cc(self.options.logic), + "shuffle_enemies_position": tf(self.options.shuffle_enemies_position), + "enemies_scaling_lower": cc(self.options.enemies_scaling_lower), + "enemies_scaling_upper": cc(self.options.enemies_scaling_upper), + "bosses_scaling_lower": cc(self.options.bosses_scaling_lower), + "bosses_scaling_upper": cc(self.options.bosses_scaling_upper), + "enemizer_attacks": cc(self.options.enemizer_attacks), + "leveling_curve": cc(self.options.leveling_curve), + "battles_quantity": cc(self.options.battlefields_battles_quantities) if + self.options.battlefields_battles_quantities.value < 5 else "RandomLow" if - self.multiworld.battlefields_battles_quantities[self.player].value == 5 else + self.options.battlefields_battles_quantities.value == 5 else "RandomHigh", - "shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]), + "shuffle_battlefield_rewards": tf(self.options.shuffle_battlefield_rewards), "random_starting_weapon": True, - "progressive_gear": tf(self.multiworld.progressive_gear[self.player]), - "tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]), - "doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]), - "doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]), - "sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]), - "sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]), + "progressive_gear": tf(self.options.progressive_gear), + "tweaked_dungeons": tf(self.options.tweak_frustrating_dungeons), + "doom_castle_mode": cc(self.options.doom_castle_mode), + "doom_castle_shortcut": tf(self.options.doom_castle_shortcut), + "sky_coin_mode": cc(self.options.sky_coin_mode), + "sky_coin_fragments_qty": cc(self.options.shattered_sky_coin_quantity), "enable_spoilers": False, - "progressive_formations": cc(self.multiworld.progressive_formations[self.player]), - "map_shuffling": cc(self.multiworld.map_shuffle[self.player]), - "crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]), - "enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]), - "shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]), - "companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]), - "companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]), - "starting_companion": cc(self.multiworld.starting_companion[self.player]), + "progressive_formations": cc(self.options.progressive_formations), + "map_shuffling": cc(self.options.map_shuffle), + "crest_shuffle": tf(self.options.crest_shuffle), + "enemizer_groups": cc(self.options.enemizer_groups), + "shuffle_res_weak_type": tf(self.options.shuffle_res_weak_types), + "companion_leveling_type": cc(self.options.companion_leveling_type), + "companion_spellbook_type": cc(self.options.companion_spellbook_type), + "starting_companion": cc(self.options.starting_companion), "available_companions": ["Zero", "One", "Two", - "Three", "Four"][self.multiworld.available_companions[self.player].value], - "companions_locations": cc(self.multiworld.companions_locations[self.player]), - "kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]), + "Three", "Four"][self.options.available_companions.value], + "companions_locations": cc(self.options.companions_locations), + "kaelis_mom_fight_minotaur": tf(self.options.kaelis_mom_fight_minotaur), } for option, data in option_writes.items(): options["Final Fantasy Mystic Quest"][option][data] = 1 - rom_name = f'MQ{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] + rom_name = f'MQ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] self.rom_name = bytearray(rom_name, 'utf8') self.rom_name_available_event.set() setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed": - hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} + hex(self.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]] - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": starting_items.append("SkyCoin") file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq") diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index 8b83c88e..4e26be16 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -1,11 +1,9 @@ from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule +from .data.rooms import rooms, entrances from .Items import item_groups, yaml_item -import pkgutil -import yaml -rooms = yaml.load(pkgutil.get_data(__name__, "data/rooms.yaml"), yaml.Loader) -entrance_names = {entrance["id"]: entrance["name"] for entrance in yaml.load(pkgutil.get_data(__name__, "data/entrances.yaml"), yaml.Loader)} +entrance_names = {entrance["id"]: entrance["name"] for entrance in entrances} object_id_table = {} object_type_table = {} @@ -69,7 +67,7 @@ def create_regions(self): location_table else None, object["type"], object["access"], self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp", - "BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and + "BattlefieldXp") and (object["type"] != "Box" or self.options.brown_boxes == "include") and not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"])) dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player) @@ -91,15 +89,13 @@ def create_regions(self): if "entrance" in link and link["entrance"] != -1: spoiler = False if link["entrance"] in crest_warps: - if self.multiworld.crest_shuffle[self.player]: + if self.options.crest_shuffle: spoiler = True - elif self.multiworld.map_shuffle[self.player] == "everything": + elif self.options.map_shuffle == "everything": spoiler = True - elif "Subregion" in region.name and self.multiworld.map_shuffle[self.player] not in ("dungeons", - "none"): + elif "Subregion" in region.name and self.options.map_shuffle not in ("dungeons", "none"): spoiler = True - elif "Subregion" not in region.name and self.multiworld.map_shuffle[self.player] not in ("none", - "overworld"): + elif "Subregion" not in region.name and self.options.map_shuffle not in ("none", "overworld"): spoiler = True if spoiler: @@ -111,6 +107,7 @@ def create_regions(self): connection.connect(connect_room) break + non_dead_end_crest_rooms = [ 'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room', 'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge', @@ -140,7 +137,7 @@ def set_rules(self) -> None: add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic) add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic) - if self.multiworld.map_shuffle[self.player]: + if self.options.map_shuffle: for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"): loc = self.multiworld.get_location(boss, self.player) checked_regions = {loc.parent_region} @@ -158,12 +155,12 @@ def set_rules(self) -> None: return True check_foresta(loc.parent_region) - if self.multiworld.logic[self.player] == "friendly": + if self.options.logic == "friendly": process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player), ["MagicMirror"]) process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player), ["Mask"]) - if self.multiworld.map_shuffle[self.player] in ("none", "overworld"): + if self.options.map_shuffle in ("none", "overworld"): process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player), ["Bomb"]) process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player), @@ -185,8 +182,8 @@ def set_rules(self) -> None: process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player), ["DragonClaw", "CaptainCap"]) - if self.multiworld.logic[self.player] == "expert": - if self.multiworld.map_shuffle[self.player] == "none" and not self.multiworld.crest_shuffle[self.player]: + if self.options.logic == "expert": + if self.options.map_shuffle == "none" and not self.options.crest_shuffle: inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player) connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room) connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player)) @@ -198,14 +195,14 @@ def set_rules(self) -> None: if entrance.connected_region.name in non_dead_end_crest_rooms: entrance.access_rule = lambda state: False - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": - logic_coins = [16, 24, 32, 32, 38][self.multiworld.shattered_sky_coin_quantity[self.player].value] + if self.options.sky_coin_mode == "shattered_sky_coin": + logic_coins = [16, 24, 32, 32, 38][self.options.shattered_sky_coin_quantity.value] self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has("Sky Fragment", self.player, logic_coins) - elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + elif self.options.sky_coin_mode == "save_the_crystals": self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player) - elif self.multiworld.sky_coin_mode[self.player] in ("standard", "start_with"): + elif self.options.sky_coin_mode in ("standard", "start_with"): self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has("Sky Coin", self.player) @@ -213,24 +210,22 @@ def set_rules(self) -> None: def stage_set_rules(multiworld): # If there's no enemies, there's no repeatable income sources no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest") - if multiworld.enemies_density[player] == "none"] - if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler, - ItemClassification.trap)]) > len([player for player in no_enemies_players if - multiworld.accessibility[player] == "minimal"]) * 3): + if multiworld.worlds[player].options.enemies_density == "none"] + if ( + len([item for item in multiworld.itempool if item.excludable]) > + len([player + for player in no_enemies_players + if multiworld.worlds[player].options.accessibility != "minimal"]) * 3 + ): for player in no_enemies_players: for location in vendor_locations: - if multiworld.accessibility[player] == "locations": + if multiworld.worlds[player].options.accessibility == "full": multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED else: multiworld.get_location(location, player).access_rule = lambda state: False else: - # There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing - # advancement items so that useful items can be placed - for player in no_enemies_players: - for location in vendor_locations: - multiworld.get_location(location, player).item_rule = lambda item: not item.advancement - - + raise Exception(f"Not enough filler/trap items for FFMQ players with full and items accessibility. " + f"Add more items or change the 'Enemies Density' option to something besides 'none'") class FFMQLocation(Location): diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index ac3e9137..58dc4bf1 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -10,7 +10,7 @@ from .Regions import create_regions, location_table, set_rules, stage_set_rules, non_dead_end_crest_warps from .Items import item_table, item_groups, create_items, FFMQItem, fillers from .Output import generate_output -from .Options import option_definitions +from .Options import FFMQOptions from .Client import FFMQClient @@ -25,14 +25,25 @@ from .Client import FFMQClient class FFMQWebWorld(WebWorld): - tutorials = [Tutorial( + setup_en = Tutorial( "Multiworld Setup Guide", "A guide to playing Final Fantasy Mystic Quest with Archipelago.", "English", "setup_en.md", "setup/en", ["Alchav"] - )] + ) + + setup_fr = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Français", + "setup_fr.md", + "setup/fr", + ["Artea"] + ) + + tutorials = [setup_en, setup_fr] class FFMQWorld(World): @@ -45,7 +56,8 @@ class FFMQWorld(World): item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None} location_name_to_id = location_table - option_definitions = option_definitions + options_dataclass = FFMQOptions + options: FFMQOptions topology_present = True @@ -67,20 +79,14 @@ class FFMQWorld(World): super().__init__(world, player) def generate_early(self): - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": - self.multiworld.brown_boxes[self.player].value = 1 - if self.multiworld.enemies_scaling_lower[self.player].value > \ - self.multiworld.enemies_scaling_upper[self.player].value: - (self.multiworld.enemies_scaling_lower[self.player].value, - self.multiworld.enemies_scaling_upper[self.player].value) =\ - (self.multiworld.enemies_scaling_upper[self.player].value, - self.multiworld.enemies_scaling_lower[self.player].value) - if self.multiworld.bosses_scaling_lower[self.player].value > \ - self.multiworld.bosses_scaling_upper[self.player].value: - (self.multiworld.bosses_scaling_lower[self.player].value, - self.multiworld.bosses_scaling_upper[self.player].value) =\ - (self.multiworld.bosses_scaling_upper[self.player].value, - self.multiworld.bosses_scaling_lower[self.player].value) + if self.options.sky_coin_mode == "shattered_sky_coin": + self.options.brown_boxes.value = 1 + if self.options.enemies_scaling_lower.value > self.options.enemies_scaling_upper.value: + self.options.enemies_scaling_lower.value, self.options.enemies_scaling_upper.value = \ + self.options.enemies_scaling_upper.value, self.options.enemies_scaling_lower.value + if self.options.bosses_scaling_lower.value > self.options.bosses_scaling_upper.value: + self.options.bosses_scaling_lower.value, self.options.bosses_scaling_upper.value = \ + self.options.bosses_scaling_upper.value, self.options.bosses_scaling_lower.value @classmethod def stage_generate_early(cls, multiworld): @@ -94,20 +100,20 @@ class FFMQWorld(World): rooms_data = {} for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"): - if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or - world.multiworld.crest_shuffle[world.player]): - if world.multiworld.map_shuffle_seed[world.player].value.isdigit(): - multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value)) - elif world.multiworld.map_shuffle_seed[world.player].value != "random": - multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value)) - + int(world.multiworld.seed)) + if (world.options.map_shuffle or world.options.crest_shuffle or world.options.shuffle_battlefield_rewards + or world.options.companions_locations): + if world.options.map_shuffle_seed.value.isdigit(): + multiworld.random.seed(int(world.options.map_shuffle_seed.value)) + elif world.options.map_shuffle_seed.value != "random": + multiworld.random.seed(int(hash(world.options.map_shuffle_seed.value)) + + int(world.multiworld.seed)) seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper() - map_shuffle = multiworld.map_shuffle[world.player].value - crest_shuffle = multiworld.crest_shuffle[world.player].current_key - battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key - companion_shuffle = multiworld.companions_locations[world.player].value - kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key + map_shuffle = world.options.map_shuffle.value + crest_shuffle = world.options.crest_shuffle.current_key + battlefield_shuffle = world.options.shuffle_battlefield_rewards.current_key + companion_shuffle = world.options.companions_locations.value + kaeli_mom = world.options.kaelis_mom_fight_minotaur.current_key query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}" @@ -146,14 +152,23 @@ class FFMQWorld(World): return FFMQItem(name, self.player) def collect_item(self, state, item, remove=False): + if not item.advancement: + return None if "Progressive" in item.name: i = item.code - 256 + if remove: + if state.has(self.item_id_to_name[i+1], self.player): + if state.has(self.item_id_to_name[i+2], self.player): + return self.item_id_to_name[i+2] + return self.item_id_to_name[i+1] + return self.item_id_to_name[i] + if state.has(self.item_id_to_name[i], self.player): if state.has(self.item_id_to_name[i+1], self.player): return self.item_id_to_name[i+2] return self.item_id_to_name[i+1] return self.item_id_to_name[i] - return item.name if item.advancement else None + return item.name def modify_multidata(self, multidata): # wait for self.rom_name to be available. @@ -175,14 +190,14 @@ class FFMQWorld(World): def extend_hint_information(self, hint_data): hint_data[self.player] = {} - if self.multiworld.map_shuffle[self.player]: + if self.options.map_shuffle: single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"] for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg", "Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship", "Subregion Doom Castle"]: region = self.multiworld.get_region(subregion, self.player) for location in region.locations: - if location.address and self.multiworld.map_shuffle[self.player] != "dungeons": + if location.address and self.options.map_shuffle != "dungeons": hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else "")) @@ -202,14 +217,13 @@ class FFMQWorld(World): for location in exit_check.connected_region.locations: if location.address: hint = [] - if self.multiworld.map_shuffle[self.player] != "dungeons": + if self.options.map_shuffle != "dungeons": hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else ""))) - if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \ - ("Subregion Mac's Ship", "Subregion Doom Castle"): + if self.options.map_shuffle != "overworld": hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu", "Pazuzu's")) - hint = " - ".join(hint) + hint = " - ".join(hint).replace(" - Mac Ship", "") if location.address in hint_data[self.player]: hint_data[self.player][location.address] += f"/{hint}" else: diff --git a/worlds/ffmq/data/entrances.yaml b/worlds/ffmq/data/entrances.yaml deleted file mode 100644 index 1dfef265..00000000 --- a/worlds/ffmq/data/entrances.yaml +++ /dev/null @@ -1,2450 +0,0 @@ -- name: Doom Castle - Sand Floor - To Sky Door - Sand Floor - id: 0 - area: 7 - coordinates: [24, 19] - teleporter: [0, 0] -- name: Doom Castle - Sand Floor - Main Entrance - Sand Floor - id: 1 - area: 7 - coordinates: [19, 43] - teleporter: [1, 6] -- name: Doom Castle - Aero Room - Aero Room Entrance - id: 2 - area: 7 - coordinates: [27, 39] - teleporter: [1, 0] -- name: Focus Tower B1 - Main Loop - South Entrance - id: 3 - area: 8 - coordinates: [43, 60] - teleporter: [2, 6] -- name: Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall - id: 4 - area: 8 - coordinates: [37, 41] - teleporter: [4, 0] -- name: Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room - id: 5 - area: 8 - coordinates: [59, 35] - teleporter: [5, 0] -- name: Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest - id: 6 - area: 8 - coordinates: [57, 59] - teleporter: [8, 0] -- name: Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door - id: 7 - area: 8 - coordinates: [51, 49] - teleporter: [6, 0] -- name: Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor - id: 8 - area: 8 - coordinates: [51, 45] - teleporter: [7, 0] -- name: Focus Tower 1F - Focus Tower West Entrance - id: 9 - area: 9 - coordinates: [25, 29] - teleporter: [3, 6] -- name: Focus Tower 1F - To Focus Tower 2F - From SandCoin - id: 10 - area: 9 - coordinates: [16, 4] - teleporter: [10, 0] -- name: Focus Tower 1F - To Focus Tower B1 - Main Hall - id: 11 - area: 9 - coordinates: [4, 23] - teleporter: [11, 0] -- name: Focus Tower 1F - To Focus Tower B1 - To Aero Chest - id: 12 - area: 9 - coordinates: [26, 17] - teleporter: [12, 0] -- name: Focus Tower 1F - Sky Door - id: 13 - area: 9 - coordinates: [16, 24] - teleporter: [13, 0] -- name: Focus Tower 1F - To Focus Tower 2F - From RiverCoin - id: 14 - area: 9 - coordinates: [16, 10] - teleporter: [14, 0] -- name: Focus Tower 1F - To Focus Tower B1 - From Sky Door - id: 15 - area: 9 - coordinates: [16, 29] - teleporter: [15, 0] -- name: Focus Tower 2F - Sand Coin Passage - North Entrance - id: 16 - area: 10 - coordinates: [49, 30] - teleporter: [4, 6] -- name: Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin - id: 17 - area: 10 - coordinates: [47, 33] - teleporter: [17, 0] -- name: Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin - id: 18 - area: 10 - coordinates: [47, 41] - teleporter: [18, 0] -- name: Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor - id: 19 - area: 10 - coordinates: [38, 40] - teleporter: [20, 0] -- name: Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor - id: 20 - area: 10 - coordinates: [56, 40] - teleporter: [19, 0] -- name: Focus Tower 2F - Venus Chest Room - Pillar Script - id: 21 - area: 10 - coordinates: [48, 53] - teleporter: [13, 8] -- name: Focus Tower 3F - Lower Floor - To Fireburg Entrance - id: 22 - area: 11 - coordinates: [11, 39] - teleporter: [6, 6] -- name: Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar - id: 23 - area: 11 - coordinates: [6, 47] - teleporter: [24, 0] -- name: Focus Tower 3F - Upper Floor - To Aquaria Entrance - id: 24 - area: 11 - coordinates: [21, 38] - teleporter: [5, 6] -- name: Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room - id: 25 - area: 11 - coordinates: [24, 47] - teleporter: [23, 0] -- name: Level Forest - Boulder Script - id: 26 - area: 14 - coordinates: [52, 15] - teleporter: [0, 8] -- name: Level Forest - Rotten Tree Script - id: 27 - area: 14 - coordinates: [47, 6] - teleporter: [2, 8] -- name: Level Forest - Exit Level Forest 1 - id: 28 - area: 14 - coordinates: [46, 25] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 2 - id: 29 - area: 14 - coordinates: [46, 26] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 3 - id: 30 - area: 14 - coordinates: [47, 25] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 4 - id: 31 - area: 14 - coordinates: [47, 26] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 5 - id: 32 - area: 14 - coordinates: [60, 14] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 6 - id: 33 - area: 14 - coordinates: [61, 14] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 7 - id: 34 - area: 14 - coordinates: [46, 4] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 8 - id: 35 - area: 14 - coordinates: [46, 3] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 9 - id: 36 - area: 14 - coordinates: [47, 4] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest A - id: 37 - area: 14 - coordinates: [47, 3] - teleporter: [25, 0] -- name: Foresta - Exit Foresta 1 - id: 38 - area: 15 - coordinates: [10, 25] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 2 - id: 39 - area: 15 - coordinates: [10, 26] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 3 - id: 40 - area: 15 - coordinates: [11, 25] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 4 - id: 41 - area: 15 - coordinates: [11, 26] - teleporter: [31, 0] -- name: Foresta - Old Man House - Front Door - id: 42 - area: 15 - coordinates: [25, 17] - teleporter: [32, 4] -- name: Foresta - Old Man House - Back Door - id: 43 - area: 15 - coordinates: [25, 14] - teleporter: [33, 0] -- name: Foresta - Kaeli's House - id: 44 - area: 15 - coordinates: [7, 21] - teleporter: [0, 5] -- name: Foresta - Rest House - id: 45 - area: 15 - coordinates: [23, 23] - teleporter: [1, 5] -- name: Kaeli's House - Kaeli's House Entrance - id: 46 - area: 16 - coordinates: [11, 20] - teleporter: [86, 3] -- name: Foresta Houses - Old Man's House - Old Man Front Exit - id: 47 - area: 17 - coordinates: [35, 44] - teleporter: [34, 0] -- name: Foresta Houses - Old Man's House - Old Man Back Exit - id: 48 - area: 17 - coordinates: [35, 27] - teleporter: [35, 0] -- name: Foresta - Old Man House - Barrel Tile Script # New, use the focus tower column's script - id: 483 - area: 17 - coordinates: [0x23, 0x1E] - teleporter: [0x0D, 8] -- name: Foresta Houses - Rest House - Bed Script - id: 49 - area: 17 - coordinates: [30, 6] - teleporter: [1, 8] -- name: Foresta Houses - Rest House - Rest House Exit - id: 50 - area: 17 - coordinates: [35, 20] - teleporter: [87, 3] -- name: Foresta Houses - Libra House - Libra House Script - id: 51 - area: 17 - coordinates: [8, 49] - teleporter: [67, 8] -- name: Foresta Houses - Gemini House - Gemini House Script - id: 52 - area: 17 - coordinates: [26, 55] - teleporter: [68, 8] -- name: Foresta Houses - Mobius House - Mobius House Script - id: 53 - area: 17 - coordinates: [14, 33] - teleporter: [69, 8] -- name: Sand Temple - Sand Temple Entrance - id: 54 - area: 18 - coordinates: [56, 27] - teleporter: [36, 0] -- name: Bone Dungeon 1F - Bone Dungeon Entrance - id: 55 - area: 19 - coordinates: [13, 60] - teleporter: [37, 0] -- name: Bone Dungeon 1F - To Bone Dungeon B1 - id: 56 - area: 19 - coordinates: [13, 39] - teleporter: [2, 2] -- name: Bone Dungeon B1 - Waterway - Exit Waterway - id: 57 - area: 20 - coordinates: [27, 39] - teleporter: [3, 2] -- name: Bone Dungeon B1 - Waterway - Tristam's Script - id: 58 - area: 20 - coordinates: [27, 45] - teleporter: [3, 8] -- name: Bone Dungeon B1 - Waterway - To Bone Dungeon 1F - id: 59 - area: 20 - coordinates: [54, 61] - teleporter: [88, 3] -- name: Bone Dungeon B1 - Checker Room - Exit Checker Room - id: 60 - area: 20 - coordinates: [23, 40] - teleporter: [4, 2] -- name: Bone Dungeon B1 - Checker Room - To Waterway - id: 61 - area: 20 - coordinates: [39, 49] - teleporter: [89, 3] -- name: Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room - id: 62 - area: 20 - coordinates: [5, 33] - teleporter: [91, 3] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage - id: 63 - area: 21 - coordinates: [19, 13] - teleporter: [5, 2] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room - id: 64 - area: 21 - coordinates: [29, 15] - teleporter: [6, 2] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Checker Room - id: 65 - area: 21 - coordinates: [8, 25] - teleporter: [90, 3] -- name: Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room - id: 66 - area: 21 - coordinates: [59, 12] - teleporter: [93, 3] -- name: Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room - id: 67 - area: 21 - coordinates: [59, 28] - teleporter: [94, 3] -- name: Bonne Dungeon B2 - Two Skulls Room - To Box Room - id: 68 - area: 21 - coordinates: [53, 7] - teleporter: [7, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To Quake Room - id: 69 - area: 21 - coordinates: [41, 3] - teleporter: [8, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To Boss Room - id: 70 - area: 21 - coordinates: [47, 57] - teleporter: [9, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room - id: 71 - area: 21 - coordinates: [54, 23] - teleporter: [92, 3] -- name: Bone Dungeon B2 - Boss Room - Flamerus Rex Script - id: 72 - area: 22 - coordinates: [29, 19] - teleporter: [4, 8] -- name: Bone Dungeon B2 - Boss Room - Tristam Leave Script - id: 73 - area: 22 - coordinates: [29, 23] - teleporter: [75, 8] -- name: Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room - id: 74 - area: 22 - coordinates: [30, 27] - teleporter: [95, 3] -- name: Libra Temple - Entrance - id: 75 - area: 23 - coordinates: [10, 15] - teleporter: [13, 6] -- name: Libra Temple - Libra Tile Script - id: 76 - area: 23 - coordinates: [9, 8] - teleporter: [59, 8] -- name: Aquaria Winter - Winter Entrance 1 - id: 77 - area: 24 - coordinates: [25, 25] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 2 - id: 78 - area: 24 - coordinates: [25, 26] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 3 - id: 79 - area: 24 - coordinates: [26, 25] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 4 - id: 80 - area: 24 - coordinates: [26, 26] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Phoebe's House Entrance Script #Modified to not be a script - id: 81 - area: 24 - coordinates: [8, 19] - teleporter: [10, 5] # original value [5, 8] -- name: Aquaria Winter - Winter Vendor House Entrance - id: 82 - area: 24 - coordinates: [8, 5] - teleporter: [44, 4] -- name: Aquaria Winter - Winter INN Entrance - id: 83 - area: 24 - coordinates: [26, 17] - teleporter: [11, 5] -- name: Aquaria Summer - Summer Entrance 1 - id: 84 - area: 25 - coordinates: [57, 25] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 2 - id: 85 - area: 25 - coordinates: [57, 26] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 3 - id: 86 - area: 25 - coordinates: [58, 25] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 4 - id: 87 - area: 25 - coordinates: [58, 26] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Phoebe's House Entrance - id: 88 - area: 25 - coordinates: [40, 19] - teleporter: [10, 5] -- name: Aquaria Summer - Spencer's Place Entrance Top - id: 89 - area: 25 - coordinates: [40, 16] - teleporter: [42, 0] -- name: Aquaria Summer - Spencer's Place Entrance Side - id: 90 - area: 25 - coordinates: [41, 18] - teleporter: [43, 0] -- name: Aquaria Summer - Summer Vendor House Entrance - id: 91 - area: 25 - coordinates: [40, 5] - teleporter: [44, 4] -- name: Aquaria Summer - Summer INN Entrance - id: 92 - area: 25 - coordinates: [58, 17] - teleporter: [11, 5] -- name: Phoebe's House - Entrance # Change to a script, same as vendor house - id: 93 - area: 26 - coordinates: [29, 14] - teleporter: [5, 8] # Original Value [11,3] -- name: Aquaria Vendor House - Vendor House Entrance's Script - id: 94 - area: 27 - coordinates: [7, 10] - teleporter: [40, 8] -- name: Aquaria Vendor House - Vendor House Stairs - id: 95 - area: 27 - coordinates: [1, 4] - teleporter: [47, 0] -- name: Aquaria Gemini Room - Gemini Script - id: 96 - area: 27 - coordinates: [2, 40] - teleporter: [72, 8] -- name: Aquaria Gemini Room - Gemini Room Stairs - id: 97 - area: 27 - coordinates: [4, 39] - teleporter: [48, 0] -- name: Aquaria INN - Aquaria INN entrance # Change to a script, same as vendor house - id: 98 - area: 27 - coordinates: [51, 46] - teleporter: [75, 8] # Original value [48,3] -- name: Wintry Cave 1F - Main Entrance - id: 99 - area: 28 - coordinates: [50, 58] - teleporter: [49, 0] -- name: Wintry Cave 1F - To 3F Top - id: 100 - area: 28 - coordinates: [40, 25] - teleporter: [14, 2] -- name: Wintry Cave 1F - To 2F - id: 101 - area: 28 - coordinates: [10, 43] - teleporter: [15, 2] -- name: Wintry Cave 1F - Phoebe's Script - id: 102 - area: 28 - coordinates: [44, 37] - teleporter: [6, 8] -- name: Wintry Cave 2F - To 3F Bottom - id: 103 - area: 29 - coordinates: [58, 5] - teleporter: [50, 0] -- name: Wintry Cave 2F - To 1F - id: 104 - area: 29 - coordinates: [38, 18] - teleporter: [97, 3] -- name: Wintry Cave 3F Top - Exit from 3F Top - id: 105 - area: 30 - coordinates: [24, 6] - teleporter: [96, 3] -- name: Wintry Cave 3F Bottom - Exit to 2F - id: 106 - area: 31 - coordinates: [4, 29] - teleporter: [51, 0] -- name: Life Temple - Entrance - id: 107 - area: 32 - coordinates: [9, 60] - teleporter: [14, 6] -- name: Life Temple - Libra Tile Script - id: 108 - area: 32 - coordinates: [3, 55] - teleporter: [60, 8] -- name: Life Temple - Mysterious Man Script - id: 109 - area: 32 - coordinates: [9, 44] - teleporter: [78, 8] -- name: Fall Basin - Back Exit Script - id: 110 - area: 33 - coordinates: [17, 5] - teleporter: [9, 0] # Remove script [42, 8] for overworld teleport (but not main exit) -- name: Fall Basin - Main Exit - id: 111 - area: 33 - coordinates: [15, 26] - teleporter: [53, 0] -- name: Fall Basin - Phoebe's Script - id: 112 - area: 33 - coordinates: [17, 6] - teleporter: [9, 8] -- name: Ice Pyramid B1 Taunt Room - To Climbing Wall Room - id: 113 - area: 34 - coordinates: [43, 6] - teleporter: [55, 0] -- name: Ice Pyramid 1F Maze - Main Entrance 1 - id: 114 - area: 35 - coordinates: [18, 36] - teleporter: [56, 0] -- name: Ice Pyramid 1F Maze - Main Entrance 2 - id: 115 - area: 35 - coordinates: [19, 36] - teleporter: [56, 0] -- name: Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room - id: 116 - area: 35 - coordinates: [3, 27] - teleporter: [57, 0] -- name: Ice Pyramid 1F Maze - West Center Stairs to 2F West Room - id: 117 - area: 35 - coordinates: [11, 15] - teleporter: [58, 0] -- name: Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room - id: 118 - area: 35 - coordinates: [25, 16] - teleporter: [59, 0] -- name: Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room - id: 119 - area: 35 - coordinates: [31, 1] - teleporter: [60, 0] -- name: Ice Pyramid 1F Maze - East Stairs to 2F North Corridor - id: 120 - area: 35 - coordinates: [34, 9] - teleporter: [61, 0] -- name: Ice Pyramid 1F Maze - Statue's Script - id: 121 - area: 35 - coordinates: [21, 32] - teleporter: [77, 8] -- name: Ice Pyramid 2F South Tiled Room - To 1F - id: 122 - area: 36 - coordinates: [4, 26] - teleporter: [62, 0] -- name: Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room - id: 123 - area: 36 - coordinates: [22, 17] - teleporter: [67, 0] -- name: Ice Pyramid 2F West Room - To 1F - id: 124 - area: 36 - coordinates: [9, 10] - teleporter: [63, 0] -- name: Ice Pyramid 2F Center Room - To 1F - id: 125 - area: 36 - coordinates: [22, 14] - teleporter: [64, 0] -- name: Ice Pyramid 2F Small North Room - To 1F - id: 126 - area: 36 - coordinates: [26, 4] - teleporter: [65, 0] -- name: Ice Pyramid 2F North Corridor - To 1F - id: 127 - area: 36 - coordinates: [32, 8] - teleporter: [66, 0] -- name: Ice Pyramid 2F North Corridor - To 3F Main Loop - id: 128 - area: 36 - coordinates: [12, 7] - teleporter: [68, 0] -- name: Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room - id: 129 - area: 37 - coordinates: [24, 54] - teleporter: [69, 0] -- name: Ice Pyramid 3F Main Loop - To 2F Corridor - id: 130 - area: 37 - coordinates: [16, 45] - teleporter: [70, 0] -- name: Ice Pyramid 3F Main Loop - To 4F - id: 131 - area: 37 - coordinates: [19, 43] - teleporter: [71, 0] -- name: Ice Pyramid 4F Treasure Room - To 3F Main Loop - id: 132 - area: 38 - coordinates: [52, 5] - teleporter: [72, 0] -- name: Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room - id: 133 - area: 38 - coordinates: [62, 19] - teleporter: [73, 0] -- name: Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room - id: 134 - area: 39 - coordinates: [54, 63] - teleporter: [74, 0] -- name: Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate - id: 135 - area: 39 - coordinates: [47, 54] - teleporter: [77, 8] -- name: Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room - id: 136 - area: 39 - coordinates: [39, 43] - teleporter: [75, 0] -- name: Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room - id: 137 - area: 39 - coordinates: [39, 60] - teleporter: [76, 0] -- name: Ice Pyramid - Duplicate Ice Golem Room # not used? - id: 138 - area: 40 - coordinates: [44, 43] - teleporter: [77, 0] -- name: Ice Pyramid Climbing Wall Room - To Taunt Room - id: 139 - area: 41 - coordinates: [4, 59] - teleporter: [78, 0] -- name: Ice Pyramid Climbing Wall Room - To 5F Stairs - id: 140 - area: 41 - coordinates: [4, 45] - teleporter: [79, 0] -- name: Ice Pyramid Ice Golem Room - To 5F Stairs - id: 141 - area: 42 - coordinates: [44, 43] - teleporter: [80, 0] -- name: Ice Pyramid Ice Golem Room - Ice Golem Script - id: 142 - area: 42 - coordinates: [53, 32] - teleporter: [10, 8] -- name: Spencer Waterfall - To Spencer Cave - id: 143 - area: 43 - coordinates: [48, 57] - teleporter: [81, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 1 - id: 144 - area: 43 - coordinates: [40, 5] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 2 - id: 145 - area: 43 - coordinates: [40, 6] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 3 - id: 146 - area: 43 - coordinates: [41, 5] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 4 - id: 147 - area: 43 - coordinates: [41, 6] - teleporter: [82, 0] -- name: Spencer Waterfall - Right Exit to Aquaria 1 - id: 148 - area: 43 - coordinates: [46, 8] - teleporter: [83, 0] -- name: Spencer Waterfall - Right Exit to Aquaria 2 - id: 149 - area: 43 - coordinates: [47, 8] - teleporter: [83, 0] -- name: Spencer Cave Normal Main - To Waterfall - id: 150 - area: 44 - coordinates: [14, 39] - teleporter: [85, 0] -- name: Spencer Cave Normal From Overworld - Exit to Overworld - id: 151 - area: 44 - coordinates: [15, 57] - teleporter: [7, 6] -- name: Spencer Cave Unplug - Exit to Overworld - id: 152 - area: 45 - coordinates: [40, 29] - teleporter: [7, 6] -- name: Spencer Cave Unplug - Libra Teleporter Start Script - id: 153 - area: 45 - coordinates: [28, 21] - teleporter: [33, 8] -- name: Spencer Cave Unplug - Libra Teleporter End Script - id: 154 - area: 45 - coordinates: [46, 4] - teleporter: [34, 8] -- name: Spencer Cave Unplug - Mobius Teleporter Chest Script - id: 155 - area: 45 - coordinates: [21, 9] - teleporter: [35, 8] -- name: Spencer Cave Unplug - Mobius Teleporter Start Script - id: 156 - area: 45 - coordinates: [29, 28] - teleporter: [36, 8] -- name: Wintry Temple Outer Room - Main Entrance - id: 157 - area: 46 - coordinates: [8, 31] - teleporter: [15, 6] -- name: Wintry Temple Inner Room - Gemini Tile to Sealed temple - id: 158 - area: 46 - coordinates: [9, 24] - teleporter: [62, 8] -- name: Fireburg - To Overworld - id: 159 - area: 47 - coordinates: [4, 13] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 160 - area: 47 - coordinates: [5, 13] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 161 - area: 47 - coordinates: [28, 15] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 162 - area: 47 - coordinates: [27, 15] - teleporter: [9, 6] -- name: Fireburg - Vendor House - id: 163 - area: 47 - coordinates: [10, 24] - teleporter: [91, 0] -- name: Fireburg - Reuben House - id: 164 - area: 47 - coordinates: [14, 6] - teleporter: [98, 8] # Script for reuben, original value [16, 2] -- name: Fireburg - Hotel - id: 165 - area: 47 - coordinates: [20, 8] - teleporter: [96, 8] # It's a script now for tristam, original value [17, 2] -- name: Fireburg - GrenadeMan House Script - id: 166 - area: 47 - coordinates: [12, 18] - teleporter: [11, 8] -- name: Reuben House - Main Entrance - id: 167 - area: 48 - coordinates: [33, 46] - teleporter: [98, 3] -- name: GrenadeMan House - Entrance Script - id: 168 - area: 49 - coordinates: [55, 60] - teleporter: [9, 8] -- name: GrenadeMan House - To Mobius Crest Room - id: 169 - area: 49 - coordinates: [57, 52] - teleporter: [93, 0] -- name: GrenadeMan Mobius Room - Stairs to House - id: 170 - area: 49 - coordinates: [39, 26] - teleporter: [94, 0] -- name: GrenadeMan Mobius Room - Mobius Teleporter Script - id: 171 - area: 49 - coordinates: [39, 23] - teleporter: [54, 8] -- name: Fireburg Vendor House - Entrance Script # No use to be a script - id: 172 - area: 49 - coordinates: [7, 10] - teleporter: [95, 0] # Original value [39, 8] -- name: Fireburg Vendor House - Stairs to Gemini Room - id: 173 - area: 49 - coordinates: [1, 4] - teleporter: [96, 0] -- name: Fireburg Gemini Room - Stairs to Vendor House - id: 174 - area: 49 - coordinates: [4, 39] - teleporter: [97, 0] -- name: Fireburg Gemini Room - Gemini Teleporter Script - id: 175 - area: 49 - coordinates: [2, 40] - teleporter: [45, 8] -- name: Fireburg Hotel Lobby - Stairs to beds - id: 176 - area: 49 - coordinates: [4, 50] - teleporter: [213, 0] -- name: Fireburg Hotel Lobby - Entrance - id: 177 - area: 49 - coordinates: [17, 56] - teleporter: [99, 3] -- name: Fireburg Hotel Beds - Stairs to Hotel Lobby - id: 178 - area: 49 - coordinates: [45, 59] - teleporter: [214, 0] -- name: Mine Exterior - Main Entrance - id: 179 - area: 50 - coordinates: [5, 28] - teleporter: [98, 0] -- name: Mine Exterior - To Cliff - id: 180 - area: 50 - coordinates: [58, 29] - teleporter: [99, 0] -- name: Mine Exterior - To Parallel Room - id: 181 - area: 50 - coordinates: [8, 7] - teleporter: [20, 2] -- name: Mine Exterior - To Crescent Room - id: 182 - area: 50 - coordinates: [26, 15] - teleporter: [21, 2] -- name: Mine Exterior - To Climbing Room - id: 183 - area: 50 - coordinates: [21, 35] - teleporter: [22, 2] -- name: Mine Exterior - Jinn Fight Script - id: 184 - area: 50 - coordinates: [58, 31] - teleporter: [74, 8] -- name: Mine Parallel Room - To Mine Exterior - id: 185 - area: 51 - coordinates: [7, 60] - teleporter: [100, 3] -- name: Mine Crescent Room - To Mine Exterior - id: 186 - area: 51 - coordinates: [22, 61] - teleporter: [101, 3] -- name: Mine Climbing Room - To Mine Exterior - id: 187 - area: 51 - coordinates: [56, 21] - teleporter: [102, 3] -- name: Mine Cliff - Entrance - id: 188 - area: 52 - coordinates: [9, 5] - teleporter: [100, 0] -- name: Mine Cliff - Reuben Grenade Script - id: 189 - area: 52 - coordinates: [15, 7] - teleporter: [12, 8] -- name: Sealed Temple - To Overworld - id: 190 - area: 53 - coordinates: [58, 43] - teleporter: [16, 6] -- name: Sealed Temple - Gemini Tile Script - id: 191 - area: 53 - coordinates: [56, 38] - teleporter: [63, 8] -- name: Volcano Base - Main Entrance 1 - id: 192 - area: 54 - coordinates: [23, 25] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 2 - id: 193 - area: 54 - coordinates: [23, 26] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 3 - id: 194 - area: 54 - coordinates: [24, 25] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 4 - id: 195 - area: 54 - coordinates: [24, 26] - teleporter: [103, 0] -- name: Volcano Base - Left Stairs Script - id: 196 - area: 54 - coordinates: [20, 5] - teleporter: [31, 8] -- name: Volcano Base - Right Stairs Script - id: 197 - area: 54 - coordinates: [32, 5] - teleporter: [30, 8] -- name: Volcano Top Right - Top Exit - id: 198 - area: 55 - coordinates: [44, 8] - teleporter: [9, 0] # Original value [103, 0] changed to volcano escape so floor shuffling doesn't pick it up -- name: Volcano Top Left - To Right-Left Path Script - id: 199 - area: 55 - coordinates: [40, 24] - teleporter: [26, 8] -- name: Volcano Top Right - To Left-Right Path Script - id: 200 - area: 55 - coordinates: [52, 24] - teleporter: [79, 8] # Original Value [26, 8] -- name: Volcano Right Path - To Volcano Base Script - id: 201 - area: 56 - coordinates: [48, 42] - teleporter: [15, 8] # Original Value [27, 8] -- name: Volcano Left Path - To Volcano Cross Left-Right - id: 202 - area: 56 - coordinates: [40, 31] - teleporter: [25, 2] -- name: Volcano Left Path - To Volcano Cross Right-Left - id: 203 - area: 56 - coordinates: [52, 29] - teleporter: [26, 2] -- name: Volcano Left Path - To Volcano Base Script - id: 204 - area: 56 - coordinates: [36, 42] - teleporter: [27, 8] -- name: Volcano Cross Left-Right - To Volcano Left Path - id: 205 - area: 56 - coordinates: [10, 42] - teleporter: [103, 3] -- name: Volcano Cross Left-Right - To Volcano Top Right Script - id: 206 - area: 56 - coordinates: [16, 24] - teleporter: [29, 8] -- name: Volcano Cross Right-Left - To Volcano Top Left Script - id: 207 - area: 56 - coordinates: [8, 22] - teleporter: [28, 8] -- name: Volcano Cross Right-Left - To Volcano Left Path - id: 208 - area: 56 - coordinates: [16, 42] - teleporter: [104, 3] -- name: Lava Dome Inner Ring Main Loop - Main Entrance 1 - id: 209 - area: 57 - coordinates: [32, 5] - teleporter: [104, 0] -- name: Lava Dome Inner Ring Main Loop - Main Entrance 2 - id: 210 - area: 57 - coordinates: [33, 5] - teleporter: [104, 0] -- name: Lava Dome Inner Ring Main Loop - To Three Steps Room - id: 211 - area: 57 - coordinates: [14, 5] - teleporter: [105, 0] -- name: Lava Dome Inner Ring Main Loop - To Life Chest Room Lower - id: 212 - area: 57 - coordinates: [40, 17] - teleporter: [106, 0] -- name: Lava Dome Inner Ring Main Loop - To Big Jump Room Left - id: 213 - area: 57 - coordinates: [8, 11] - teleporter: [108, 0] -- name: Lava Dome Inner Ring Main Loop - To Split Corridor Room - id: 214 - area: 57 - coordinates: [11, 19] - teleporter: [111, 0] -- name: Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher - id: 215 - area: 57 - coordinates: [32, 11] - teleporter: [107, 0] -- name: Lava Dome Inner Ring Plate Ledge - To Plate Corridor - id: 216 - area: 57 - coordinates: [12, 23] - teleporter: [109, 0] -- name: Lava Dome Inner Ring Plate Ledge - Plate Script - id: 217 - area: 57 - coordinates: [5, 23] - teleporter: [47, 8] -- name: Lava Dome Inner Ring Upper Ledges - To Pointless Room - id: 218 - area: 57 - coordinates: [0, 9] - teleporter: [110, 0] -- name: Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room - id: 219 - area: 57 - coordinates: [0, 15] - teleporter: [112, 0] -- name: Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor - id: 220 - area: 57 - coordinates: [54, 5] - teleporter: [113, 0] -- name: Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II - id: 221 - area: 57 - coordinates: [54, 21] - teleporter: [114, 0] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1 - id: 222 - area: 57 - coordinates: [62, 20] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2 - id: 223 - area: 57 - coordinates: [63, 20] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3 - id: 224 - area: 57 - coordinates: [62, 21] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4 - id: 225 - area: 57 - coordinates: [63, 21] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor - id: 226 - area: 57 - coordinates: [50, 25] - teleporter: [115, 0] -- name: Lava Dome Jump Maze II - Lower Right Entrance - id: 227 - area: 58 - coordinates: [55, 28] - teleporter: [116, 0] -- name: Lava Dome Jump Maze II - Upper Entrance - id: 228 - area: 58 - coordinates: [35, 3] - teleporter: [119, 0] -- name: Lava Dome Jump Maze II - Lower Left Entrance - id: 229 - area: 58 - coordinates: [34, 27] - teleporter: [120, 0] -- name: Lava Dome Up-Down Corridor - Upper Entrance - id: 230 - area: 58 - coordinates: [29, 8] - teleporter: [117, 0] -- name: Lava Dome Up-Down Corridor - Lower Entrance - id: 231 - area: 58 - coordinates: [28, 25] - teleporter: [118, 0] -- name: Lava Dome Jump Maze I - South Entrance - id: 232 - area: 59 - coordinates: [20, 27] - teleporter: [121, 0] -- name: Lava Dome Jump Maze I - North Entrance - id: 233 - area: 59 - coordinates: [7, 3] - teleporter: [122, 0] -- name: Lava Dome Pointless Room - Entrance - id: 234 - area: 60 - coordinates: [2, 7] - teleporter: [123, 0] -- name: Lava Dome Pointless Room - Visit Quest Script 1 - id: 490 - area: 60 - coordinates: [4, 4] - teleporter: [99, 8] -- name: Lava Dome Pointless Room - Visit Quest Script 2 - id: 491 - area: 60 - coordinates: [4, 5] - teleporter: [99, 8] -- name: Lava Dome Lower Moon Helm Room - Left Entrance - id: 235 - area: 60 - coordinates: [2, 19] - teleporter: [124, 0] -- name: Lava Dome Lower Moon Helm Room - Right Entrance - id: 236 - area: 60 - coordinates: [11, 21] - teleporter: [125, 0] -- name: Lava Dome Moon Helm Room - Entrance - id: 237 - area: 60 - coordinates: [15, 23] - teleporter: [126, 0] -- name: Lava Dome Three Jumps Room - To Main Loop - id: 238 - area: 61 - coordinates: [58, 15] - teleporter: [127, 0] -- name: Lava Dome Life Chest Room - Lower South Entrance - id: 239 - area: 61 - coordinates: [38, 27] - teleporter: [128, 0] -- name: Lava Dome Life Chest Room - Upper South Entrance - id: 240 - area: 61 - coordinates: [28, 23] - teleporter: [129, 0] -- name: Lava Dome Big Jump Room - Left Entrance - id: 241 - area: 62 - coordinates: [42, 51] - teleporter: [133, 0] -- name: Lava Dome Big Jump Room - North Entrance - id: 242 - area: 62 - coordinates: [30, 29] - teleporter: [131, 0] -- name: Lava Dome Big Jump Room - Lower Right Stairs - id: 243 - area: 62 - coordinates: [61, 59] - teleporter: [132, 0] -- name: Lava Dome Split Corridor - Upper Stairs - id: 244 - area: 62 - coordinates: [30, 43] - teleporter: [130, 0] -- name: Lava Dome Split Corridor - Lower Stairs - id: 245 - area: 62 - coordinates: [36, 61] - teleporter: [134, 0] -- name: Lava Dome Plate Corridor - Right Entrance - id: 246 - area: 63 - coordinates: [19, 29] - teleporter: [135, 0] -- name: Lava Dome Plate Corridor - Left Entrance - id: 247 - area: 63 - coordinates: [60, 21] - teleporter: [137, 0] -- name: Lava Dome Four Boxes Stairs - Upper Entrance - id: 248 - area: 63 - coordinates: [22, 3] - teleporter: [136, 0] -- name: Lava Dome Four Boxes Stairs - Lower Entrance - id: 249 - area: 63 - coordinates: [22, 17] - teleporter: [16, 0] -- name: Lava Dome Hydra Room - South Entrance - id: 250 - area: 64 - coordinates: [14, 59] - teleporter: [105, 3] -- name: Lava Dome Hydra Room - North Exit - id: 251 - area: 64 - coordinates: [25, 31] - teleporter: [138, 0] -- name: Lava Dome Hydra Room - Hydra Script - id: 252 - area: 64 - coordinates: [14, 36] - teleporter: [14, 8] -- name: Lava Dome Escape Corridor - South Entrance - id: 253 - area: 65 - coordinates: [22, 17] - teleporter: [139, 0] -- name: Lava Dome Escape Corridor - North Entrance - id: 254 - area: 65 - coordinates: [22, 3] - teleporter: [9, 0] -- name: Rope Bridge - West Entrance 1 - id: 255 - area: 66 - coordinates: [3, 10] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 2 - id: 256 - area: 66 - coordinates: [3, 11] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 3 - id: 257 - area: 66 - coordinates: [3, 12] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 4 - id: 258 - area: 66 - coordinates: [3, 13] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 5 - id: 259 - area: 66 - coordinates: [4, 10] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 6 - id: 260 - area: 66 - coordinates: [4, 11] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 7 - id: 261 - area: 66 - coordinates: [4, 12] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 8 - id: 262 - area: 66 - coordinates: [4, 13] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 1 - id: 263 - area: 66 - coordinates: [59, 10] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 2 - id: 264 - area: 66 - coordinates: [59, 11] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 3 - id: 265 - area: 66 - coordinates: [59, 12] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 4 - id: 266 - area: 66 - coordinates: [59, 13] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 5 - id: 267 - area: 66 - coordinates: [60, 10] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 6 - id: 268 - area: 66 - coordinates: [60, 11] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 7 - id: 269 - area: 66 - coordinates: [60, 12] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 8 - id: 270 - area: 66 - coordinates: [60, 13] - teleporter: [140, 0] -- name: Rope Bridge - Reuben Fall Script - id: 271 - area: 66 - coordinates: [13, 12] - teleporter: [15, 8] -- name: Alive Forest - West Entrance 1 - id: 272 - area: 67 - coordinates: [8, 13] - teleporter: [142, 0] -- name: Alive Forest - West Entrance 2 - id: 273 - area: 67 - coordinates: [9, 13] - teleporter: [142, 0] -- name: Alive Forest - Giant Tree Entrance - id: 274 - area: 67 - coordinates: [42, 42] - teleporter: [143, 0] -- name: Alive Forest - Libra Teleporter Script - id: 275 - area: 67 - coordinates: [8, 52] - teleporter: [64, 8] -- name: Alive Forest - Gemini Teleporter Script - id: 276 - area: 67 - coordinates: [57, 49] - teleporter: [65, 8] -- name: Alive Forest - Mobius Teleporter Script - id: 277 - area: 67 - coordinates: [24, 10] - teleporter: [66, 8] -- name: Giant Tree 1F - Entrance Script 1 - id: 278 - area: 68 - coordinates: [18, 31] - teleporter: [56, 1] # The script is restored if no map shuffling [49, 8] -- name: Giant Tree 1F - Entrance Script 2 - id: 279 - area: 68 - coordinates: [19, 31] - teleporter: [56, 1] # Same [49, 8] -- name: Giant Tree 1F - North Entrance To 2F - id: 280 - area: 68 - coordinates: [16, 1] - teleporter: [144, 0] -- name: Giant Tree 2F Main Lobby - North Entrance to 1F - id: 281 - area: 69 - coordinates: [44, 33] - teleporter: [145, 0] -- name: Giant Tree 2F Main Lobby - Central Entrance to 3F - id: 282 - area: 69 - coordinates: [42, 47] - teleporter: [146, 0] -- name: Giant Tree 2F Main Lobby - West Entrance to Mushroom Room - id: 283 - area: 69 - coordinates: [58, 49] - teleporter: [149, 0] -- name: Giant Tree 2F West Ledge - To 3F Northwest Ledge - id: 284 - area: 69 - coordinates: [34, 37] - teleporter: [147, 0] -- name: Giant Tree 2F Fall From Vine Script - id: 482 - area: 69 - coordinates: [0x2E, 0x33] - teleporter: [76, 8] -- name: Giant Tree Meteor Chest Room - To 2F Mushroom Room - id: 285 - area: 69 - coordinates: [58, 44] - teleporter: [148, 0] -- name: Giant Tree 2F Mushroom Room - Entrance - id: 286 - area: 70 - coordinates: [55, 18] - teleporter: [150, 0] -- name: Giant Tree 2F Mushroom Room - North Face to Meteor - id: 287 - area: 70 - coordinates: [56, 7] - teleporter: [151, 0] -- name: Giant Tree 3F Central Room - Central Entrance to 2F - id: 288 - area: 71 - coordinates: [46, 53] - teleporter: [152, 0] -- name: Giant Tree 3F Central Room - East Entrance to Worm Room - id: 289 - area: 71 - coordinates: [58, 39] - teleporter: [153, 0] -- name: Giant Tree 3F Lower Corridor - Entrance from Worm Room - id: 290 - area: 71 - coordinates: [45, 39] - teleporter: [154, 0] -- name: Giant Tree 3F West Platform - Lower Entrance - id: 291 - area: 71 - coordinates: [33, 43] - teleporter: [155, 0] -- name: Giant Tree 3F West Platform - Top Entrance - id: 292 - area: 71 - coordinates: [52, 25] - teleporter: [156, 0] -- name: Giant Tree Worm Room - East Entrance - id: 293 - area: 72 - coordinates: [20, 58] - teleporter: [157, 0] -- name: Giant Tree Worm Room - West Entrance - id: 294 - area: 72 - coordinates: [6, 56] - teleporter: [158, 0] -- name: Giant Tree 4F Lower Floor - Entrance - id: 295 - area: 73 - coordinates: [20, 7] - teleporter: [159, 0] -- name: Giant Tree 4F Lower Floor - Lower West Mouth - id: 296 - area: 73 - coordinates: [8, 23] - teleporter: [160, 0] -- name: Giant Tree 4F Lower Floor - Lower Central Mouth - id: 297 - area: 73 - coordinates: [14, 25] - teleporter: [161, 0] -- name: Giant Tree 4F Lower Floor - Lower East Mouth - id: 298 - area: 73 - coordinates: [20, 25] - teleporter: [162, 0] -- name: Giant Tree 4F Upper Floor - Upper West Mouth - id: 299 - area: 73 - coordinates: [8, 19] - teleporter: [163, 0] -- name: Giant Tree 4F Upper Floor - Upper Central Mouth - id: 300 - area: 73 - coordinates: [12, 17] - teleporter: [164, 0] -- name: Giant Tree 4F Slime Room - Exit - id: 301 - area: 74 - coordinates: [47, 10] - teleporter: [165, 0] -- name: Giant Tree 4F Slime Room - West Entrance - id: 302 - area: 74 - coordinates: [45, 24] - teleporter: [166, 0] -- name: Giant Tree 4F Slime Room - Central Entrance - id: 303 - area: 74 - coordinates: [50, 24] - teleporter: [167, 0] -- name: Giant Tree 4F Slime Room - East Entrance - id: 304 - area: 74 - coordinates: [57, 28] - teleporter: [168, 0] -- name: Giant Tree 5F - Entrance - id: 305 - area: 75 - coordinates: [14, 51] - teleporter: [169, 0] -- name: Giant Tree 5F - Giant Tree Face # Unused - id: 306 - area: 75 - coordinates: [14, 37] - teleporter: [170, 0] -- name: Kaidge Temple - Entrance - id: 307 - area: 77 - coordinates: [44, 63] - teleporter: [18, 6] -- name: Kaidge Temple - Mobius Teleporter Script - id: 308 - area: 77 - coordinates: [35, 57] - teleporter: [71, 8] -- name: Windhole Temple - Entrance - id: 309 - area: 78 - coordinates: [10, 29] - teleporter: [173, 0] -- name: Mount Gale - Entrance 1 - id: 310 - area: 79 - coordinates: [1, 45] - teleporter: [174, 0] -- name: Mount Gale - Entrance 2 - id: 311 - area: 79 - coordinates: [2, 45] - teleporter: [174, 0] -- name: Mount Gale - Visit Quest - id: 494 - area: 79 - coordinates: [44, 7] - teleporter: [101, 8] -- name: Windia - Main Entrance 1 - id: 312 - area: 80 - coordinates: [12, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 2 - id: 313 - area: 80 - coordinates: [13, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 3 - id: 314 - area: 80 - coordinates: [14, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 4 - id: 315 - area: 80 - coordinates: [15, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 5 - id: 316 - area: 80 - coordinates: [12, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 6 - id: 317 - area: 80 - coordinates: [13, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 7 - id: 318 - area: 80 - coordinates: [14, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 8 - id: 319 - area: 80 - coordinates: [15, 41] - teleporter: [10, 6] -- name: Windia - Otto's House - id: 320 - area: 80 - coordinates: [21, 39] - teleporter: [30, 5] -- name: Windia - INN's Script # Change to teleporter / Change back to script! - id: 321 - area: 80 - coordinates: [18, 34] - teleporter: [97, 8] # Original value [79, 8] > [31, 2] -- name: Windia - Vendor House - id: 322 - area: 80 - coordinates: [8, 36] - teleporter: [32, 5] -- name: Windia - Kid House - id: 323 - area: 80 - coordinates: [7, 23] - teleporter: [176, 4] -- name: Windia - Old People House - id: 324 - area: 80 - coordinates: [19, 21] - teleporter: [177, 4] -- name: Windia - Rainbow Bridge Script - id: 325 - area: 80 - coordinates: [21, 9] - teleporter: [10, 6] # Change to entrance, usually a script [41, 8] -- name: Otto's House - Attic Stairs - id: 326 - area: 81 - coordinates: [2, 19] - teleporter: [33, 2] -- name: Otto's House - Entrance - id: 327 - area: 81 - coordinates: [9, 30] - teleporter: [106, 3] -- name: Otto's Attic - Stairs - id: 328 - area: 81 - coordinates: [26, 23] - teleporter: [107, 3] -- name: Windia Kid House - Entrance Script # Change to teleporter - id: 329 - area: 82 - coordinates: [7, 10] - teleporter: [178, 0] # Original value [38, 8] -- name: Windia Kid House - Basement Stairs - id: 330 - area: 82 - coordinates: [1, 4] - teleporter: [180, 0] -- name: Windia Old People House - Entrance - id: 331 - area: 82 - coordinates: [55, 12] - teleporter: [179, 0] -- name: Windia Old People House - Basement Stairs - id: 332 - area: 82 - coordinates: [60, 5] - teleporter: [181, 0] -- name: Windia Kid House Basement - Stairs - id: 333 - area: 82 - coordinates: [43, 8] - teleporter: [182, 0] -- name: Windia Kid House Basement - Mobius Teleporter - id: 334 - area: 82 - coordinates: [41, 9] - teleporter: [44, 8] -- name: Windia Old People House Basement - Stairs - id: 335 - area: 82 - coordinates: [39, 26] - teleporter: [183, 0] -- name: Windia Old People House Basement - Mobius Teleporter Script - id: 336 - area: 82 - coordinates: [39, 23] - teleporter: [43, 8] -- name: Windia Inn Lobby - Stairs to Beds - id: 337 - area: 82 - coordinates: [45, 24] - teleporter: [102, 8] # Changed to script, original value [215, 0] -- name: Windia Inn Lobby - Exit - id: 338 - area: 82 - coordinates: [53, 30] - teleporter: [135, 3] -- name: Windia Inn Beds - Stairs to Lobby - id: 339 - area: 82 - coordinates: [33, 59] - teleporter: [216, 0] -- name: Windia Vendor House - Entrance - id: 340 - area: 82 - coordinates: [29, 14] - teleporter: [108, 3] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 1 - id: 341 - area: 83 - coordinates: [47, 29] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 2 - id: 342 - area: 83 - coordinates: [47, 30] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 3 - id: 343 - area: 83 - coordinates: [48, 29] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 4 - id: 344 - area: 83 - coordinates: [48, 30] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - East Entrance - id: 345 - area: 83 - coordinates: [55, 12] - teleporter: [185, 0] -- name: Pazuzu Tower 1F Main Lobby - South Stairs - id: 346 - area: 83 - coordinates: [51, 25] - teleporter: [186, 0] -- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 1 - id: 347 - area: 83 - coordinates: [47, 8] - teleporter: [16, 8] -- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 2 - id: 348 - area: 83 - coordinates: [48, 8] - teleporter: [16, 8] -- name: Pazuzu Tower 1F Boxes Room - West Stairs - id: 349 - area: 83 - coordinates: [38, 17] - teleporter: [187, 0] -- name: Pazuzu 2F - West Upper Stairs - id: 350 - area: 84 - coordinates: [7, 11] - teleporter: [188, 0] -- name: Pazuzu 2F - South Stairs - id: 351 - area: 84 - coordinates: [20, 24] - teleporter: [189, 0] -- name: Pazuzu 2F - West Lower Stairs - id: 352 - area: 84 - coordinates: [6, 17] - teleporter: [190, 0] -- name: Pazuzu 2F - Central Stairs - id: 353 - area: 84 - coordinates: [15, 15] - teleporter: [191, 0] -- name: Pazuzu 2F - Pazuzu 1 - id: 354 - area: 84 - coordinates: [15, 8] - teleporter: [17, 8] -- name: Pazuzu 2F - Pazuzu 2 - id: 355 - area: 84 - coordinates: [16, 8] - teleporter: [17, 8] -- name: Pazuzu 3F Main Room - North Stairs - id: 356 - area: 85 - coordinates: [23, 11] - teleporter: [192, 0] -- name: Pazuzu 3F Main Room - West Stairs - id: 357 - area: 85 - coordinates: [7, 15] - teleporter: [193, 0] -- name: Pazuzu 3F Main Room - Pazuzu Script 1 - id: 358 - area: 85 - coordinates: [15, 8] - teleporter: [18, 8] -- name: Pazuzu 3F Main Room - Pazuzu Script 2 - id: 359 - area: 85 - coordinates: [16, 8] - teleporter: [18, 8] -- name: Pazuzu 3F Central Island - Central Stairs - id: 360 - area: 85 - coordinates: [15, 14] - teleporter: [194, 0] -- name: Pazuzu 3F Central Island - South Stairs - id: 361 - area: 85 - coordinates: [17, 25] - teleporter: [195, 0] -- name: Pazuzu 4F - Northwest Stairs - id: 362 - area: 86 - coordinates: [39, 12] - teleporter: [196, 0] -- name: Pazuzu 4F - Southwest Stairs - id: 363 - area: 86 - coordinates: [39, 19] - teleporter: [197, 0] -- name: Pazuzu 4F - South Stairs - id: 364 - area: 86 - coordinates: [47, 24] - teleporter: [198, 0] -- name: Pazuzu 4F - Northeast Stairs - id: 365 - area: 86 - coordinates: [54, 9] - teleporter: [199, 0] -- name: Pazuzu 4F - Pazuzu Script 1 - id: 366 - area: 86 - coordinates: [47, 8] - teleporter: [19, 8] -- name: Pazuzu 4F - Pazuzu Script 2 - id: 367 - area: 86 - coordinates: [48, 8] - teleporter: [19, 8] -- name: Pazuzu 5F Pazuzu Loop - West Stairs - id: 368 - area: 87 - coordinates: [9, 49] - teleporter: [200, 0] -- name: Pazuzu 5F Pazuzu Loop - South Stairs - id: 369 - area: 87 - coordinates: [16, 55] - teleporter: [201, 0] -- name: Pazuzu 5F Upper Loop - Northeast Stairs - id: 370 - area: 87 - coordinates: [22, 40] - teleporter: [202, 0] -- name: Pazuzu 5F Upper Loop - Northwest Stairs - id: 371 - area: 87 - coordinates: [9, 40] - teleporter: [203, 0] -- name: Pazuzu 5F Upper Loop - Pazuzu Script 1 - id: 372 - area: 87 - coordinates: [15, 40] - teleporter: [20, 8] -- name: Pazuzu 5F Upper Loop - Pazuzu Script 2 - id: 373 - area: 87 - coordinates: [16, 40] - teleporter: [20, 8] -- name: Pazuzu 6F - West Stairs - id: 374 - area: 88 - coordinates: [41, 47] - teleporter: [204, 0] -- name: Pazuzu 6F - Northwest Stairs - id: 375 - area: 88 - coordinates: [41, 40] - teleporter: [205, 0] -- name: Pazuzu 6F - Northeast Stairs - id: 376 - area: 88 - coordinates: [54, 40] - teleporter: [206, 0] -- name: Pazuzu 6F - South Stairs - id: 377 - area: 88 - coordinates: [52, 56] - teleporter: [207, 0] -- name: Pazuzu 6F - Pazuzu Script 1 - id: 378 - area: 88 - coordinates: [47, 40] - teleporter: [21, 8] -- name: Pazuzu 6F - Pazuzu Script 2 - id: 379 - area: 88 - coordinates: [48, 40] - teleporter: [21, 8] -- name: Pazuzu 7F Main Room - Southwest Stairs - id: 380 - area: 89 - coordinates: [15, 54] - teleporter: [26, 0] -- name: Pazuzu 7F Main Room - Northeast Stairs - id: 381 - area: 89 - coordinates: [21, 40] - teleporter: [27, 0] -- name: Pazuzu 7F Main Room - Southeast Stairs - id: 382 - area: 89 - coordinates: [21, 56] - teleporter: [28, 0] -- name: Pazuzu 7F Main Room - Pazuzu Script 1 - id: 383 - area: 89 - coordinates: [15, 44] - teleporter: [22, 8] -- name: Pazuzu 7F Main Room - Pazuzu Script 2 - id: 384 - area: 89 - coordinates: [16, 44] - teleporter: [22, 8] -- name: Pazuzu 7F Main Room - Crystal Script # Added for floor shuffle - id: 480 - area: 89 - coordinates: [15, 40] - teleporter: [38, 8] -- name: Pazuzu 1F to 3F - South Stairs - id: 385 - area: 90 - coordinates: [43, 60] - teleporter: [29, 0] -- name: Pazuzu 1F to 3F - North Stairs - id: 386 - area: 90 - coordinates: [43, 36] - teleporter: [30, 0] -- name: Pazuzu 3F to 5F - South Stairs - id: 387 - area: 91 - coordinates: [43, 60] - teleporter: [40, 0] -- name: Pazuzu 3F to 5F - North Stairs - id: 388 - area: 91 - coordinates: [43, 36] - teleporter: [41, 0] -- name: Pazuzu 5F to 7F - South Stairs - id: 389 - area: 92 - coordinates: [43, 60] - teleporter: [38, 0] -- name: Pazuzu 5F to 7F - North Stairs - id: 390 - area: 92 - coordinates: [43, 36] - teleporter: [39, 0] -- name: Pazuzu 2F to 4F - South Stairs - id: 391 - area: 93 - coordinates: [43, 60] - teleporter: [21, 0] -- name: Pazuzu 2F to 4F - North Stairs - id: 392 - area: 93 - coordinates: [43, 36] - teleporter: [22, 0] -- name: Pazuzu 4F to 6F - South Stairs - id: 393 - area: 94 - coordinates: [43, 60] - teleporter: [2, 0] -- name: Pazuzu 4F to 6F - North Stairs - id: 394 - area: 94 - coordinates: [43, 36] - teleporter: [3, 0] -- name: Light Temple - Entrance - id: 395 - area: 95 - coordinates: [28, 57] - teleporter: [19, 6] -- name: Light Temple - Mobius Teleporter Script - id: 396 - area: 95 - coordinates: [29, 37] - teleporter: [70, 8] -- name: Light Temple - Visit Quest Script 1 - id: 492 - area: 95 - coordinates: [34, 39] - teleporter: [100, 8] -- name: Light Temple - Visit Quest Script 2 - id: 493 - area: 95 - coordinates: [35, 39] - teleporter: [100, 8] -- name: Ship Dock - Mobius Teleporter Script - id: 397 - area: 96 - coordinates: [15, 18] - teleporter: [61, 8] -- name: Ship Dock - From Overworld - id: 398 - area: 96 - coordinates: [15, 11] - teleporter: [73, 0] -- name: Ship Dock - Entrance - id: 399 - area: 96 - coordinates: [15, 23] - teleporter: [17, 6] -- name: Mac Ship Deck - East Entrance Script - id: 400 - area: 97 - coordinates: [26, 40] - teleporter: [37, 8] -- name: Mac Ship Deck - Central Stairs Script - id: 401 - area: 97 - coordinates: [16, 47] - teleporter: [50, 8] -- name: Mac Ship Deck - West Stairs Script - id: 402 - area: 97 - coordinates: [8, 34] - teleporter: [51, 8] -- name: Mac Ship Deck - East Stairs Script - id: 403 - area: 97 - coordinates: [24, 36] - teleporter: [52, 8] -- name: Mac Ship Deck - North Stairs Script - id: 404 - area: 97 - coordinates: [12, 9] - teleporter: [53, 8] -- name: Mac Ship B1 Outer Ring - South Stairs - id: 405 - area: 98 - coordinates: [16, 45] - teleporter: [208, 0] -- name: Mac Ship B1 Outer Ring - West Stairs - id: 406 - area: 98 - coordinates: [8, 35] - teleporter: [175, 0] -- name: Mac Ship B1 Outer Ring - East Stairs - id: 407 - area: 98 - coordinates: [25, 37] - teleporter: [172, 0] -- name: Mac Ship B1 Outer Ring - Northwest Stairs - id: 408 - area: 98 - coordinates: [10, 23] - teleporter: [88, 0] -- name: Mac Ship B1 Square Room - North Stairs - id: 409 - area: 98 - coordinates: [14, 9] - teleporter: [141, 0] -- name: Mac Ship B1 Square Room - South Stairs - id: 410 - area: 98 - coordinates: [16, 12] - teleporter: [87, 0] -- name: Mac Ship B1 Mac Room - Stairs # Unused? - id: 411 - area: 98 - coordinates: [16, 51] - teleporter: [101, 0] -- name: Mac Ship B1 Central Corridor - South Stairs - id: 412 - area: 98 - coordinates: [16, 38] - teleporter: [102, 0] -- name: Mac Ship B1 Central Corridor - North Stairs - id: 413 - area: 98 - coordinates: [16, 26] - teleporter: [86, 0] -- name: Mac Ship B2 South Corridor - South Stairs - id: 414 - area: 99 - coordinates: [48, 51] - teleporter: [57, 1] -- name: Mac Ship B2 South Corridor - North Stairs Script - id: 415 - area: 99 - coordinates: [48, 38] - teleporter: [55, 8] -- name: Mac Ship B2 North Corridor - South Stairs Script - id: 416 - area: 99 - coordinates: [48, 27] - teleporter: [56, 8] -- name: Mac Ship B2 North Corridor - North Stairs Script - id: 417 - area: 99 - coordinates: [48, 12] - teleporter: [57, 8] -- name: Mac Ship B2 Outer Ring - Northwest Stairs Script - id: 418 - area: 99 - coordinates: [55, 11] - teleporter: [58, 8] -- name: Mac Ship B1 Outer Ring Cleared - South Stairs - id: 419 - area: 100 - coordinates: [16, 45] - teleporter: [208, 0] -- name: Mac Ship B1 Outer Ring Cleared - West Stairs - id: 420 - area: 100 - coordinates: [8, 35] - teleporter: [175, 0] -- name: Mac Ship B1 Outer Ring Cleared - East Stairs - id: 421 - area: 100 - coordinates: [25, 37] - teleporter: [172, 0] -- name: Mac Ship B1 Square Room Cleared - North Stairs - id: 422 - area: 100 - coordinates: [14, 9] - teleporter: [141, 0] -- name: Mac Ship B1 Square Room Cleared - South Stairs - id: 423 - area: 100 - coordinates: [16, 12] - teleporter: [87, 0] -- name: Mac Ship B1 Mac Room Cleared - Main Stairs - id: 424 - area: 100 - coordinates: [16, 51] - teleporter: [101, 0] -- name: Mac Ship B1 Central Corridor Cleared - South Stairs - id: 425 - area: 100 - coordinates: [16, 38] - teleporter: [102, 0] -- name: Mac Ship B1 Central Corridor Cleared - North Stairs - id: 426 - area: 100 - coordinates: [16, 26] - teleporter: [86, 0] -- name: Mac Ship B1 Central Corridor Cleared - Northwest Stairs - id: 427 - area: 100 - coordinates: [23, 10] - teleporter: [88, 0] -- name: Doom Castle Corridor of Destiny - South Entrance - id: 428 - area: 101 - coordinates: [59, 29] - teleporter: [84, 0] -- name: Doom Castle Corridor of Destiny - Ice Floor Entrance - id: 429 - area: 101 - coordinates: [59, 21] - teleporter: [35, 2] -- name: Doom Castle Corridor of Destiny - Lava Floor Entrance - id: 430 - area: 101 - coordinates: [59, 13] - teleporter: [209, 0] -- name: Doom Castle Corridor of Destiny - Sky Floor Entrance - id: 431 - area: 101 - coordinates: [59, 5] - teleporter: [211, 0] -- name: Doom Castle Corridor of Destiny - Hero Room Entrance - id: 432 - area: 101 - coordinates: [59, 61] - teleporter: [13, 2] -- name: Doom Castle Ice Floor - Entrance - id: 433 - area: 102 - coordinates: [23, 42] - teleporter: [109, 3] -- name: Doom Castle Lava Floor - Entrance - id: 434 - area: 103 - coordinates: [23, 40] - teleporter: [210, 0] -- name: Doom Castle Sky Floor - Entrance - id: 435 - area: 104 - coordinates: [24, 41] - teleporter: [212, 0] -- name: Doom Castle Hero Room - Dark King Entrance 1 - id: 436 - area: 106 - coordinates: [15, 5] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 2 - id: 437 - area: 106 - coordinates: [16, 5] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 3 - id: 438 - area: 106 - coordinates: [15, 4] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 4 - id: 439 - area: 106 - coordinates: [16, 4] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Hero Statue Script - id: 440 - area: 106 - coordinates: [15, 17] - teleporter: [24, 8] -- name: Doom Castle Hero Room - Entrance - id: 441 - area: 106 - coordinates: [15, 24] - teleporter: [110, 3] -- name: Doom Castle Dark King Room - Entrance - id: 442 - area: 107 - coordinates: [14, 26] - teleporter: [52, 0] -- name: Doom Castle Dark King Room - Dark King Script - id: 443 - area: 107 - coordinates: [14, 15] - teleporter: [25, 8] -- name: Doom Castle Dark King Room - Unknown - id: 444 - area: 107 - coordinates: [47, 54] - teleporter: [77, 0] -- name: Overworld - Level Forest - id: 445 - area: 0 - type: "Overworld" - teleporter: [0x2E, 8] -- name: Overworld - Foresta - id: 446 - area: 0 - type: "Overworld" - teleporter: [0x02, 1] -- name: Overworld - Sand Temple - id: 447 - area: 0 - type: "Overworld" - teleporter: [0x03, 1] -- name: Overworld - Bone Dungeon - id: 448 - area: 0 - type: "Overworld" - teleporter: [0x04, 1] -- name: Overworld - Focus Tower Foresta - id: 449 - area: 0 - type: "Overworld" - teleporter: [0x05, 1] -- name: Overworld - Focus Tower Aquaria - id: 450 - area: 0 - type: "Overworld" - teleporter: [0x13, 1] -- name: Overworld - Libra Temple - id: 451 - area: 0 - type: "Overworld" - teleporter: [0x07, 1] -- name: Overworld - Aquaria - id: 452 - area: 0 - type: "Overworld" - teleporter: [0x08, 8] -- name: Overworld - Wintry Cave - id: 453 - area: 0 - type: "Overworld" - teleporter: [0x0A, 1] -- name: Overworld - Life Temple - id: 454 - area: 0 - type: "Overworld" - teleporter: [0x0B, 1] -- name: Overworld - Falls Basin - id: 455 - area: 0 - type: "Overworld" - teleporter: [0x0C, 1] -- name: Overworld - Ice Pyramid - id: 456 - area: 0 - type: "Overworld" - teleporter: [0x0D, 1] # Will be switched to a script -- name: Overworld - Spencer's Place - id: 457 - area: 0 - type: "Overworld" - teleporter: [0x30, 8] -- name: Overworld - Wintry Temple - id: 458 - area: 0 - type: "Overworld" - teleporter: [0x10, 1] -- name: Overworld - Focus Tower Frozen Strip - id: 459 - area: 0 - type: "Overworld" - teleporter: [0x11, 1] -- name: Overworld - Focus Tower Fireburg - id: 460 - area: 0 - type: "Overworld" - teleporter: [0x12, 1] -- name: Overworld - Fireburg - id: 461 - area: 0 - type: "Overworld" - teleporter: [0x14, 1] -- name: Overworld - Mine - id: 462 - area: 0 - type: "Overworld" - teleporter: [0x15, 1] -- name: Overworld - Sealed Temple - id: 463 - area: 0 - type: "Overworld" - teleporter: [0x16, 1] -- name: Overworld - Volcano - id: 464 - area: 0 - type: "Overworld" - teleporter: [0x17, 1] -- name: Overworld - Lava Dome - id: 465 - area: 0 - type: "Overworld" - teleporter: [0x18, 1] -- name: Overworld - Focus Tower Windia - id: 466 - area: 0 - type: "Overworld" - teleporter: [0x06, 1] -- name: Overworld - Rope Bridge - id: 467 - area: 0 - type: "Overworld" - teleporter: [0x19, 1] -- name: Overworld - Alive Forest - id: 468 - area: 0 - type: "Overworld" - teleporter: [0x1A, 1] -- name: Overworld - Giant Tree - id: 469 - area: 0 - type: "Overworld" - teleporter: [0x1B, 1] -- name: Overworld - Kaidge Temple - id: 470 - area: 0 - type: "Overworld" - teleporter: [0x1C, 1] -- name: Overworld - Windia - id: 471 - area: 0 - type: "Overworld" - teleporter: [0x1D, 1] -- name: Overworld - Windhole Temple - id: 472 - area: 0 - type: "Overworld" - teleporter: [0x1E, 1] -- name: Overworld - Mount Gale - id: 473 - area: 0 - type: "Overworld" - teleporter: [0x1F, 1] -- name: Overworld - Pazuzu Tower - id: 474 - area: 0 - type: "Overworld" - teleporter: [0x20, 1] -- name: Overworld - Ship Dock - id: 475 - area: 0 - type: "Overworld" - teleporter: [0x3E, 1] -- name: Overworld - Doom Castle - id: 476 - area: 0 - type: "Overworld" - teleporter: [0x21, 1] -- name: Overworld - Light Temple - id: 477 - area: 0 - type: "Overworld" - teleporter: [0x22, 1] -- name: Overworld - Mac Ship - id: 478 - area: 0 - type: "Overworld" - teleporter: [0x24, 1] -- name: Overworld - Mac Ship Doom - id: 479 - area: 0 - type: "Overworld" - teleporter: [0x24, 1] -- name: Dummy House - Bed Script - id: 480 - area: 17 - coordinates: [0x28, 0x38] - teleporter: [1, 8] -- name: Dummy House - Entrance - id: 481 - area: 17 - coordinates: [0x29, 0x3B] - teleporter: [0, 10] #None diff --git a/worlds/ffmq/data/rooms.py b/worlds/ffmq/data/rooms.py new file mode 100644 index 00000000..38634f10 --- /dev/null +++ b/worlds/ffmq/data/rooms.py @@ -0,0 +1,2 @@ +rooms = [{'name': 'Overworld', 'id': 0, 'type': 'Overworld', 'game_objects': [], 'links': [{'target_room': 220, 'access': []}]}, {'name': 'Subregion Foresta', 'id': 220, 'type': 'Subregion', 'region': 'Foresta', 'game_objects': [{'name': 'Foresta South Battlefield', 'object_id': 1, 'location': 'ForestaSouthBattlefield', 'location_slot': 'ForestaSouthBattlefield', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Foresta West Battlefield', 'object_id': 2, 'location': 'ForestaWestBattlefield', 'location_slot': 'ForestaWestBattlefield', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Foresta East Battlefield', 'object_id': 3, 'location': 'ForestaEastBattlefield', 'location_slot': 'ForestaEastBattlefield', 'type': 'BattlefieldGp', 'access': []}], 'links': [{'target_room': 15, 'location': 'LevelForest', 'location_slot': 'LevelForest', 'entrance': 445, 'teleporter': [46, 8], 'access': []}, {'target_room': 16, 'location': 'Foresta', 'location_slot': 'Foresta', 'entrance': 446, 'teleporter': [2, 1], 'access': []}, {'target_room': 24, 'location': 'SandTemple', 'location_slot': 'SandTemple', 'entrance': 447, 'teleporter': [3, 1], 'access': []}, {'target_room': 25, 'location': 'BoneDungeon', 'location_slot': 'BoneDungeon', 'entrance': 448, 'teleporter': [4, 1], 'access': []}, {'target_room': 3, 'location': 'FocusTowerForesta', 'location_slot': 'FocusTowerForesta', 'entrance': 449, 'teleporter': [5, 1], 'access': []}, {'target_room': 221, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['RiverCoin']}, {'target_room': 226, 'access': ['SunCoin']}]}, {'name': 'Subregion Aquaria', 'id': 221, 'type': 'Subregion', 'region': 'Aquaria', 'game_objects': [{'name': 'South of Libra Temple Battlefield', 'object_id': 4, 'location': 'AquariaBattlefield01', 'location_slot': 'AquariaBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'East of Libra Temple Battlefield', 'object_id': 5, 'location': 'AquariaBattlefield02', 'location_slot': 'AquariaBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'South of Aquaria Battlefield', 'object_id': 6, 'location': 'AquariaBattlefield03', 'location_slot': 'AquariaBattlefield03', 'type': 'BattlefieldItem', 'access': []}, {'name': 'South of Wintry Cave Battlefield', 'object_id': 7, 'location': 'WintryBattlefield01', 'location_slot': 'WintryBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'West of Wintry Cave Battlefield', 'object_id': 8, 'location': 'WintryBattlefield02', 'location_slot': 'WintryBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Ice Pyramid Battlefield', 'object_id': 9, 'location': 'PyramidBattlefield01', 'location_slot': 'PyramidBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 10, 'location': 'FocusTowerAquaria', 'location_slot': 'FocusTowerAquaria', 'entrance': 450, 'teleporter': [19, 1], 'access': []}, {'target_room': 39, 'location': 'LibraTemple', 'location_slot': 'LibraTemple', 'entrance': 451, 'teleporter': [7, 1], 'access': []}, {'target_room': 40, 'location': 'Aquaria', 'location_slot': 'Aquaria', 'entrance': 452, 'teleporter': [8, 8], 'access': []}, {'target_room': 45, 'location': 'WintryCave', 'location_slot': 'WintryCave', 'entrance': 453, 'teleporter': [10, 1], 'access': []}, {'target_room': 52, 'location': 'FallsBasin', 'location_slot': 'FallsBasin', 'entrance': 455, 'teleporter': [12, 1], 'access': []}, {'target_room': 54, 'location': 'IcePyramid', 'location_slot': 'IcePyramid', 'entrance': 456, 'teleporter': [13, 1], 'access': []}, {'target_room': 220, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Life Temple', 'id': 222, 'type': 'Subregion', 'region': 'LifeTemple', 'game_objects': [], 'links': [{'target_room': 51, 'location': 'LifeTemple', 'location_slot': 'LifeTemple', 'entrance': 454, 'teleporter': [11, 1], 'access': []}]}, {'name': 'Subregion Frozen Fields', 'id': 223, 'type': 'Subregion', 'region': 'AquariaFrozenField', 'game_objects': [{'name': 'North of Libra Temple Battlefield', 'object_id': 10, 'location': 'LibraBattlefield01', 'location_slot': 'LibraBattlefield01', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Aquaria Frozen Field Battlefield', 'object_id': 11, 'location': 'LibraBattlefield02', 'location_slot': 'LibraBattlefield02', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 74, 'location': 'WintryTemple', 'location_slot': 'WintryTemple', 'entrance': 458, 'teleporter': [16, 1], 'access': []}, {'target_room': 14, 'location': 'FocusTowerFrozen', 'location_slot': 'FocusTowerFrozen', 'entrance': 459, 'teleporter': [17, 1], 'access': []}, {'target_room': 221, 'access': []}, {'target_room': 225, 'access': ['SummerAquaria', 'DualheadHydra']}]}, {'name': 'Subregion Fireburg', 'id': 224, 'type': 'Subregion', 'region': 'Fireburg', 'game_objects': [{'name': 'Path to Fireburg Southern Battlefield', 'object_id': 12, 'location': 'FireburgBattlefield01', 'location_slot': 'FireburgBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Path to Fireburg Central Battlefield', 'object_id': 13, 'location': 'FireburgBattlefield02', 'location_slot': 'FireburgBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Path to Fireburg Northern Battlefield', 'object_id': 14, 'location': 'FireburgBattlefield03', 'location_slot': 'FireburgBattlefield03', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Sealed Temple Battlefield', 'object_id': 15, 'location': 'MineBattlefield01', 'location_slot': 'MineBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Mine Battlefield', 'object_id': 16, 'location': 'MineBattlefield02', 'location_slot': 'MineBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Boulder Battlefield', 'object_id': 17, 'location': 'MineBattlefield03', 'location_slot': 'MineBattlefield03', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 13, 'location': 'FocusTowerFireburg', 'location_slot': 'FocusTowerFireburg', 'entrance': 460, 'teleporter': [18, 1], 'access': []}, {'target_room': 76, 'location': 'Fireburg', 'location_slot': 'Fireburg', 'entrance': 461, 'teleporter': [20, 1], 'access': []}, {'target_room': 84, 'location': 'Mine', 'location_slot': 'Mine', 'entrance': 462, 'teleporter': [21, 1], 'access': []}, {'target_room': 92, 'location': 'SealedTemple', 'location_slot': 'SealedTemple', 'entrance': 463, 'teleporter': [22, 1], 'access': []}, {'target_room': 93, 'location': 'Volcano', 'location_slot': 'Volcano', 'entrance': 464, 'teleporter': [23, 1], 'access': []}, {'target_room': 100, 'location': 'LavaDome', 'location_slot': 'LavaDome', 'entrance': 465, 'teleporter': [24, 1], 'access': []}, {'target_room': 220, 'access': ['RiverCoin']}, {'target_room': 221, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 225, 'access': ['DualheadHydra']}]}, {'name': 'Subregion Volcano Battlefield', 'id': 225, 'type': 'Subregion', 'region': 'VolcanoBattlefield', 'game_objects': [{'name': 'Volcano Battlefield', 'object_id': 18, 'location': 'VolcanoBattlefield01', 'location_slot': 'VolcanoBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 224, 'access': ['DualheadHydra']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Windia', 'id': 226, 'type': 'Subregion', 'region': 'Windia', 'game_objects': [{'name': 'Kaidge Temple Battlefield', 'object_id': 19, 'location': 'WindiaBattlefield01', 'location_slot': 'WindiaBattlefield01', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}, {'name': 'South of Windia Battlefield', 'object_id': 20, 'location': 'WindiaBattlefield02', 'location_slot': 'WindiaBattlefield02', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}], 'links': [{'target_room': 9, 'location': 'FocusTowerWindia', 'location_slot': 'FocusTowerWindia', 'entrance': 466, 'teleporter': [6, 1], 'access': []}, {'target_room': 123, 'location': 'RopeBridge', 'location_slot': 'RopeBridge', 'entrance': 467, 'teleporter': [25, 1], 'access': []}, {'target_room': 124, 'location': 'AliveForest', 'location_slot': 'AliveForest', 'entrance': 468, 'teleporter': [26, 1], 'access': []}, {'target_room': 125, 'location': 'GiantTree', 'location_slot': 'GiantTree', 'entrance': 469, 'teleporter': [27, 1], 'access': ['Barred']}, {'target_room': 152, 'location': 'KaidgeTemple', 'location_slot': 'KaidgeTemple', 'entrance': 470, 'teleporter': [28, 1], 'access': []}, {'target_room': 156, 'location': 'Windia', 'location_slot': 'Windia', 'entrance': 471, 'teleporter': [29, 1], 'access': []}, {'target_room': 154, 'location': 'WindholeTemple', 'location_slot': 'WindholeTemple', 'entrance': 472, 'teleporter': [30, 1], 'access': []}, {'target_room': 155, 'location': 'MountGale', 'location_slot': 'MountGale', 'entrance': 473, 'teleporter': [31, 1], 'access': []}, {'target_room': 166, 'location': 'PazuzusTower', 'location_slot': 'PazuzusTower', 'entrance': 474, 'teleporter': [32, 1], 'access': []}, {'target_room': 220, 'access': ['SunCoin']}, {'target_room': 221, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 224, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 227, 'access': ['RainbowBridge']}]}, {'name': "Subregion Spencer's Cave", 'id': 227, 'type': 'Subregion', 'region': 'SpencerCave', 'game_objects': [], 'links': [{'target_room': 73, 'location': 'SpencersPlace', 'location_slot': 'SpencersPlace', 'entrance': 457, 'teleporter': [48, 8], 'access': []}, {'target_room': 226, 'access': ['RainbowBridge']}]}, {'name': 'Subregion Ship Dock', 'id': 228, 'type': 'Subregion', 'region': 'ShipDock', 'game_objects': [], 'links': [{'target_room': 186, 'location': 'ShipDock', 'location_slot': 'ShipDock', 'entrance': 475, 'teleporter': [62, 1], 'access': []}, {'target_room': 229, 'access': ['ShipLiberated', 'ShipDockAccess']}]}, {'name': "Subregion Mac's Ship", 'id': 229, 'type': 'Subregion', 'region': 'MacShip', 'game_objects': [], 'links': [{'target_room': 187, 'location': 'MacsShip', 'location_slot': 'MacsShip', 'entrance': 478, 'teleporter': [36, 1], 'access': []}, {'target_room': 228, 'access': ['ShipLiberated', 'ShipDockAccess']}, {'target_room': 231, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Subregion Light Temple', 'id': 230, 'type': 'Subregion', 'region': 'LightTemple', 'game_objects': [], 'links': [{'target_room': 185, 'location': 'LightTemple', 'location_slot': 'LightTemple', 'entrance': 477, 'teleporter': [35, 1], 'access': []}]}, {'name': 'Subregion Doom Castle', 'id': 231, 'type': 'Subregion', 'region': 'DoomCastle', 'game_objects': [], 'links': [{'target_room': 1, 'location': 'DoomCastle', 'location_slot': 'DoomCastle', 'entrance': 476, 'teleporter': [33, 1], 'access': []}, {'target_room': 187, 'location': 'MacsShipDoom', 'location_slot': 'MacsShipDoom', 'entrance': 479, 'teleporter': [36, 1], 'access': ['Barred']}, {'target_room': 229, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Doom Castle - Sand Floor', 'id': 1, 'game_objects': [{'name': 'Doom Castle B2 - Southeast Chest', 'object_id': 1, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Doom Castle B2 - Bone Ledge Box', 'object_id': 30, 'type': 'Box', 'access': []}, {'name': 'Doom Castle B2 - Hook Platform Box', 'object_id': 31, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 231, 'entrance': 1, 'teleporter': [1, 6], 'access': []}, {'target_room': 5, 'entrance': 0, 'teleporter': [0, 0], 'access': ['DragonClaw', 'MegaGrenade']}]}, {'name': 'Doom Castle - Aero Room', 'id': 2, 'game_objects': [{'name': 'Doom Castle B2 - Sun Door Chest', 'object_id': 0, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 4, 'entrance': 2, 'teleporter': [1, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Main Loop', 'id': 3, 'game_objects': [], 'links': [{'target_room': 220, 'entrance': 3, 'teleporter': [2, 6], 'access': []}, {'target_room': 6, 'entrance': 4, 'teleporter': [4, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Aero Corridor', 'id': 4, 'game_objects': [], 'links': [{'target_room': 9, 'entrance': 5, 'teleporter': [5, 0], 'access': []}, {'target_room': 2, 'entrance': 6, 'teleporter': [8, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Inner Loop', 'id': 5, 'game_objects': [], 'links': [{'target_room': 1, 'entrance': 8, 'teleporter': [7, 0], 'access': []}, {'target_room': 201, 'entrance': 7, 'teleporter': [6, 0], 'access': []}]}, {'name': 'Focus Tower 1F Main Lobby', 'id': 6, 'game_objects': [{'name': 'Focus Tower 1F - Main Lobby Box', 'object_id': 33, 'type': 'Box', 'access': []}], 'links': [{'target_room': 3, 'entrance': 11, 'teleporter': [11, 0], 'access': []}, {'target_room': 7, 'access': ['SandCoin']}, {'target_room': 8, 'access': ['RiverCoin']}, {'target_room': 9, 'access': ['SunCoin']}]}, {'name': 'Focus Tower 1F SandCoin Room', 'id': 7, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SandCoin']}, {'target_room': 10, 'entrance': 10, 'teleporter': [10, 0], 'access': []}]}, {'name': 'Focus Tower 1F RiverCoin Room', 'id': 8, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['RiverCoin']}, {'target_room': 11, 'entrance': 14, 'teleporter': [14, 0], 'access': []}]}, {'name': 'Focus Tower 1F SunCoin Room', 'id': 9, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SunCoin']}, {'target_room': 4, 'entrance': 12, 'teleporter': [12, 0], 'access': []}, {'target_room': 226, 'entrance': 9, 'teleporter': [3, 6], 'access': []}]}, {'name': 'Focus Tower 1F SkyCoin Room', 'id': 201, 'game_objects': [], 'links': [{'target_room': 195, 'entrance': 13, 'teleporter': [13, 0], 'access': ['SkyCoin', 'FlamerusRex', 'IceGolem', 'DualheadHydra', 'Pazuzu']}, {'target_room': 5, 'entrance': 15, 'teleporter': [15, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Sand Coin Passage', 'id': 10, 'game_objects': [{'name': 'Focus Tower 2F - Sand Door Chest', 'object_id': 3, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 221, 'entrance': 16, 'teleporter': [4, 6], 'access': []}, {'target_room': 7, 'entrance': 17, 'teleporter': [17, 0], 'access': []}]}, {'name': 'Focus Tower 2F - River Coin Passage', 'id': 11, 'game_objects': [], 'links': [{'target_room': 8, 'entrance': 18, 'teleporter': [18, 0], 'access': []}, {'target_room': 13, 'entrance': 19, 'teleporter': [20, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Venus Chest Room', 'id': 12, 'game_objects': [{'name': 'Focus Tower 2F - Back Door Chest', 'object_id': 2, 'type': 'Chest', 'access': []}, {'name': 'Focus Tower 2F - Venus Chest', 'object_id': 9, 'type': 'NPC', 'access': ['Bomb', 'VenusKey']}], 'links': [{'target_room': 14, 'entrance': 20, 'teleporter': [19, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Lower Floor', 'id': 13, 'game_objects': [{'name': 'Focus Tower 3F - River Door Box', 'object_id': 34, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 22, 'teleporter': [6, 6], 'access': []}, {'target_room': 11, 'entrance': 23, 'teleporter': [24, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Upper Floor', 'id': 14, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 24, 'teleporter': [5, 6], 'access': []}, {'target_room': 12, 'entrance': 25, 'teleporter': [23, 0], 'access': []}]}, {'name': 'Level Forest', 'id': 15, 'game_objects': [{'name': 'Level Forest - Northwest Box', 'object_id': 40, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Northeast Box', 'object_id': 41, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Middle Box', 'object_id': 42, 'type': 'Box', 'access': []}, {'name': 'Level Forest - Southwest Box', 'object_id': 43, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Southeast Box', 'object_id': 44, 'type': 'Box', 'access': ['Axe']}, {'name': 'Minotaur', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Minotaur'], 'access': ['Kaeli1']}, {'name': 'Level Forest - Old Man', 'object_id': 0, 'type': 'NPC', 'access': []}, {'name': 'Level Forest - Kaeli', 'object_id': 1, 'type': 'NPC', 'access': ['Kaeli1', 'Minotaur']}], 'links': [{'target_room': 220, 'entrance': 28, 'teleporter': [25, 0], 'access': []}]}, {'name': 'Foresta', 'id': 16, 'game_objects': [{'name': 'Foresta - Outside Box', 'object_id': 45, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 220, 'entrance': 38, 'teleporter': [31, 0], 'access': []}, {'target_room': 17, 'entrance': 44, 'teleporter': [0, 5], 'access': []}, {'target_room': 18, 'entrance': 42, 'teleporter': [32, 4], 'access': []}, {'target_room': 19, 'entrance': 43, 'teleporter': [33, 0], 'access': []}, {'target_room': 20, 'entrance': 45, 'teleporter': [1, 5], 'access': []}]}, {'name': "Kaeli's House", 'id': 17, 'game_objects': [{'name': "Foresta - Kaeli's House Box", 'object_id': 46, 'type': 'Box', 'access': []}, {'name': 'Kaeli Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli1'], 'access': ['TreeWither']}, {'name': 'Kaeli 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli2'], 'access': ['Kaeli1', 'Minotaur', 'Elixir']}], 'links': [{'target_room': 16, 'entrance': 46, 'teleporter': [86, 3], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Main", 'id': 18, 'game_objects': [], 'links': [{'target_room': 19, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 47, 'teleporter': [34, 0], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Back", 'id': 19, 'game_objects': [{'name': 'Foresta - Old Man House Chest', 'object_id': 5, 'type': 'Chest', 'access': []}, {'name': 'Old Man Barrel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['BarrelPushed'], 'access': []}], 'links': [{'target_room': 18, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 48, 'teleporter': [35, 0], 'access': []}]}, {'name': 'Foresta Houses - Rest House', 'id': 20, 'game_objects': [{'name': 'Foresta - Rest House Box', 'object_id': 47, 'type': 'Box', 'access': []}], 'links': [{'target_room': 16, 'entrance': 50, 'teleporter': [87, 3], 'access': []}]}, {'name': 'Libra Treehouse', 'id': 21, 'game_objects': [{'name': 'Alive Forest - Libra Treehouse Box', 'object_id': 50, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 51, 'teleporter': [67, 8], 'access': ['LibraCrest']}]}, {'name': 'Gemini Treehouse', 'id': 22, 'game_objects': [{'name': 'Alive Forest - Gemini Treehouse Box', 'object_id': 51, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 52, 'teleporter': [68, 8], 'access': ['GeminiCrest']}]}, {'name': 'Mobius Treehouse', 'id': 23, 'game_objects': [{'name': 'Alive Forest - Mobius Treehouse West Box', 'object_id': 48, 'type': 'Box', 'access': []}, {'name': 'Alive Forest - Mobius Treehouse East Box', 'object_id': 49, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 53, 'teleporter': [69, 8], 'access': ['MobiusCrest']}]}, {'name': 'Sand Temple', 'id': 24, 'game_objects': [{'name': 'Tristam Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Tristam'], 'access': []}], 'links': [{'target_room': 220, 'entrance': 54, 'teleporter': [36, 0], 'access': []}]}, {'name': 'Bone Dungeon 1F', 'id': 25, 'game_objects': [{'name': 'Bone Dungeon 1F - Entrance Room West Box', 'object_id': 53, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room Middle Box', 'object_id': 54, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room East Box', 'object_id': 55, 'type': 'Box', 'access': []}], 'links': [{'target_room': 220, 'entrance': 55, 'teleporter': [37, 0], 'access': []}, {'target_room': 26, 'entrance': 56, 'teleporter': [2, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Waterway', 'id': 26, 'game_objects': [{'name': 'Bone Dungeon B1 - Skull Chest', 'object_id': 6, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Bone Dungeon B1 - Tristam', 'object_id': 2, 'type': 'NPC', 'access': ['Tristam']}, {'name': 'Tristam Bone Dungeon Item Given', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TristamBoneItemGiven'], 'access': ['Tristam']}], 'links': [{'target_room': 25, 'entrance': 59, 'teleporter': [88, 3], 'access': []}, {'target_room': 28, 'entrance': 57, 'teleporter': [3, 2], 'access': ['Bomb']}]}, {'name': 'Bone Dungeon B1 - Checker Room', 'id': 28, 'game_objects': [{'name': 'Bone Dungeon B1 - Checker Room Box', 'object_id': 56, 'type': 'Box', 'access': ['Bomb']}], 'links': [{'target_room': 26, 'entrance': 61, 'teleporter': [89, 3], 'access': []}, {'target_room': 30, 'entrance': 60, 'teleporter': [4, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Hidden Room', 'id': 29, 'game_objects': [{'name': 'Bone Dungeon B1 - Ribcage Waterway Box', 'object_id': 57, 'type': 'Box', 'access': []}], 'links': [{'target_room': 31, 'entrance': 62, 'teleporter': [91, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - First Room', 'id': 30, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Alcove Box', 'object_id': 59, 'type': 'Box', 'access': []}, {'name': 'Long Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LongSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 28, 'entrance': 65, 'teleporter': [90, 3], 'access': []}, {'target_room': 31, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Second Room', 'id': 31, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Looped Hallway Box', 'object_id': 58, 'type': 'Box', 'access': []}, {'name': 'Short Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShortSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 29, 'entrance': 63, 'teleporter': [5, 2], 'access': ['LongSpineBombed']}, {'target_room': 32, 'access': ['ShortSpineBombed']}, {'target_room': 30, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Third Room', 'id': 32, 'game_objects': [], 'links': [{'target_room': 35, 'entrance': 64, 'teleporter': [6, 2], 'access': []}, {'target_room': 31, 'access': ['ShortSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Box Room', 'id': 33, 'game_objects': [{'name': 'Bone Dungeon B2 - Lone Room Box', 'object_id': 61, 'type': 'Box', 'access': []}], 'links': [{'target_room': 36, 'entrance': 66, 'teleporter': [93, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Quake Room', 'id': 34, 'game_objects': [{'name': 'Bone Dungeon B2 - Penultimate Room Chest', 'object_id': 7, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 37, 'entrance': 67, 'teleporter': [94, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - First Room', 'id': 35, 'game_objects': [{'name': 'Bone Dungeon B2 - Two Skulls Room Box', 'object_id': 60, 'type': 'Box', 'access': []}, {'name': 'Skull 1', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull1Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 32, 'entrance': 71, 'teleporter': [92, 3], 'access': []}, {'target_room': 36, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Second Room', 'id': 36, 'game_objects': [{'name': 'Skull 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull2Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 33, 'entrance': 68, 'teleporter': [7, 2], 'access': []}, {'target_room': 37, 'access': ['Skull2Bombed']}, {'target_room': 35, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Third Room', 'id': 37, 'game_objects': [], 'links': [{'target_room': 34, 'entrance': 69, 'teleporter': [8, 2], 'access': []}, {'target_room': 38, 'entrance': 70, 'teleporter': [9, 2], 'access': ['Bomb']}, {'target_room': 36, 'access': ['Skull2Bombed']}]}, {'name': 'Bone Dungeon B2 - Boss Room', 'id': 38, 'game_objects': [{'name': 'Bone Dungeon B2 - North Box', 'object_id': 62, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - South Box', 'object_id': 63, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - Flamerus Rex Chest', 'object_id': 8, 'type': 'Chest', 'access': []}, {'name': "Bone Dungeon B2 - Tristam's Treasure Chest", 'object_id': 4, 'type': 'Chest', 'access': []}, {'name': 'Flamerus Rex', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FlamerusRex'], 'access': []}], 'links': [{'target_room': 37, 'entrance': 74, 'teleporter': [95, 3], 'access': []}]}, {'name': 'Libra Temple', 'id': 39, 'game_objects': [{'name': 'Libra Temple - Box', 'object_id': 64, 'type': 'Box', 'access': []}, {'name': 'Phoebe Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phoebe1'], 'access': []}], 'links': [{'target_room': 221, 'entrance': 75, 'teleporter': [13, 6], 'access': []}, {'target_room': 51, 'entrance': 76, 'teleporter': [59, 8], 'access': ['LibraCrest']}]}, {'name': 'Aquaria', 'id': 40, 'game_objects': [{'name': 'Summer Aquaria', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SummerAquaria'], 'access': ['WakeWater']}], 'links': [{'target_room': 221, 'entrance': 77, 'teleporter': [8, 6], 'access': []}, {'target_room': 41, 'entrance': 81, 'teleporter': [10, 5], 'access': []}, {'target_room': 42, 'entrance': 82, 'teleporter': [44, 4], 'access': []}, {'target_room': 44, 'entrance': 83, 'teleporter': [11, 5], 'access': []}, {'target_room': 71, 'entrance': 89, 'teleporter': [42, 0], 'access': ['SummerAquaria']}, {'target_room': 71, 'entrance': 90, 'teleporter': [43, 0], 'access': ['SummerAquaria']}]}, {'name': "Phoebe's House", 'id': 41, 'game_objects': [{'name': "Aquaria - Phoebe's House Chest", 'object_id': 65, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 93, 'teleporter': [5, 8], 'access': []}]}, {'name': 'Aquaria Vendor House', 'id': 42, 'game_objects': [{'name': 'Aquaria - Vendor', 'object_id': 4, 'type': 'NPC', 'access': []}, {'name': 'Aquaria - Vendor House Box', 'object_id': 66, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 94, 'teleporter': [40, 8], 'access': []}, {'target_room': 43, 'entrance': 95, 'teleporter': [47, 0], 'access': []}]}, {'name': 'Aquaria Gemini Room', 'id': 43, 'game_objects': [], 'links': [{'target_room': 42, 'entrance': 97, 'teleporter': [48, 0], 'access': []}, {'target_room': 81, 'entrance': 96, 'teleporter': [72, 8], 'access': ['GeminiCrest']}]}, {'name': 'Aquaria INN', 'id': 44, 'game_objects': [], 'links': [{'target_room': 40, 'entrance': 98, 'teleporter': [75, 8], 'access': []}]}, {'name': 'Wintry Cave 1F - East Ledge', 'id': 45, 'game_objects': [{'name': 'Wintry Cave 1F - North Box', 'object_id': 67, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Entrance Box', 'object_id': 70, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Slippery Cliff Box', 'object_id': 68, 'type': 'Box', 'access': ['Claw']}, {'name': 'Wintry Cave 1F - Phoebe', 'object_id': 5, 'type': 'NPC', 'access': ['Phoebe1']}], 'links': [{'target_room': 221, 'entrance': 99, 'teleporter': [49, 0], 'access': []}, {'target_room': 49, 'entrance': 100, 'teleporter': [14, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - Central Space', 'id': 46, 'game_objects': [{'name': 'Wintry Cave 1F - Scenic Overlook Box', 'object_id': 69, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 45, 'access': ['Claw']}, {'target_room': 47, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - West Ledge', 'id': 47, 'game_objects': [], 'links': [{'target_room': 48, 'entrance': 101, 'teleporter': [15, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 2F', 'id': 48, 'game_objects': [{'name': 'Wintry Cave 2F - West Left Box', 'object_id': 71, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - West Right Box', 'object_id': 72, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Left Box', 'object_id': 73, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Right Box', 'object_id': 74, 'type': 'Box', 'access': []}], 'links': [{'target_room': 47, 'entrance': 104, 'teleporter': [97, 3], 'access': []}, {'target_room': 50, 'entrance': 103, 'teleporter': [50, 0], 'access': []}]}, {'name': 'Wintry Cave 3F Top', 'id': 49, 'game_objects': [{'name': 'Wintry Cave 3F - West Box', 'object_id': 75, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 3F - East Box', 'object_id': 76, 'type': 'Box', 'access': []}], 'links': [{'target_room': 45, 'entrance': 105, 'teleporter': [96, 3], 'access': []}]}, {'name': 'Wintry Cave 3F Bottom', 'id': 50, 'game_objects': [{'name': 'Wintry Cave 3F - Squidite Chest', 'object_id': 9, 'type': 'Chest', 'access': ['Phanquid']}, {'name': 'Phanquid', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phanquid'], 'access': []}, {'name': 'Wintry Cave 3F - Before Boss Box', 'object_id': 77, 'type': 'Box', 'access': []}], 'links': [{'target_room': 48, 'entrance': 106, 'teleporter': [51, 0], 'access': []}]}, {'name': 'Life Temple', 'id': 51, 'game_objects': [{'name': 'Life Temple - Box', 'object_id': 78, 'type': 'Box', 'access': []}, {'name': 'Life Temple - Mysterious Man', 'object_id': 6, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 222, 'entrance': 107, 'teleporter': [14, 6], 'access': []}, {'target_room': 39, 'entrance': 108, 'teleporter': [60, 8], 'access': ['LibraCrest']}]}, {'name': 'Fall Basin', 'id': 52, 'game_objects': [{'name': 'Falls Basin - Snow Crab Chest', 'object_id': 10, 'type': 'Chest', 'access': ['FreezerCrab']}, {'name': 'Freezer Crab', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FreezerCrab'], 'access': []}, {'name': 'Falls Basin - Box', 'object_id': 79, 'type': 'Box', 'access': []}], 'links': [{'target_room': 221, 'entrance': 111, 'teleporter': [53, 0], 'access': []}]}, {'name': 'Ice Pyramid B1 Taunt Room', 'id': 53, 'game_objects': [{'name': 'Ice Pyramid B1 - Chest', 'object_id': 11, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid B1 - West Box', 'object_id': 80, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - North Box', 'object_id': 81, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - East Box', 'object_id': 82, 'type': 'Box', 'access': []}], 'links': [{'target_room': 68, 'entrance': 113, 'teleporter': [55, 0], 'access': []}]}, {'name': 'Ice Pyramid 1F Maze Lobby', 'id': 54, 'game_objects': [{'name': 'Ice Pyramid 1F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid1FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 221, 'entrance': 114, 'teleporter': [56, 0], 'access': []}, {'target_room': 55, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 1F Maze', 'id': 55, 'game_objects': [{'name': 'Ice Pyramid 1F - East Alcove Chest', 'object_id': 13, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 1F - Sandwiched Alcove Box', 'object_id': 83, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Left Box', 'object_id': 84, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Right Box', 'object_id': 85, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 116, 'teleporter': [57, 0], 'access': []}, {'target_room': 57, 'entrance': 117, 'teleporter': [58, 0], 'access': []}, {'target_room': 58, 'entrance': 118, 'teleporter': [59, 0], 'access': []}, {'target_room': 59, 'entrance': 119, 'teleporter': [60, 0], 'access': []}, {'target_room': 60, 'entrance': 120, 'teleporter': [61, 0], 'access': []}, {'target_room': 54, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 2F South Tiled Room', 'id': 56, 'game_objects': [{'name': 'Ice Pyramid 2F - South Side Glass Door Box', 'object_id': 87, 'type': 'Box', 'access': ['Sword']}, {'name': 'Ice Pyramid 2F - South Side East Box', 'object_id': 91, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 122, 'teleporter': [62, 0], 'access': []}, {'target_room': 61, 'entrance': 123, 'teleporter': [67, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F West Room', 'id': 57, 'game_objects': [{'name': 'Ice Pyramid 2F - Northwest Room Box', 'object_id': 90, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 124, 'teleporter': [63, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Center Room', 'id': 58, 'game_objects': [{'name': 'Ice Pyramid 2F - Center Room Box', 'object_id': 86, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 125, 'teleporter': [64, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Small North Room', 'id': 59, 'game_objects': [{'name': 'Ice Pyramid 2F - North Room Glass Door Box', 'object_id': 88, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 126, 'teleporter': [65, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F North Corridor', 'id': 60, 'game_objects': [{'name': 'Ice Pyramid 2F - North Corridor Glass Door Box', 'object_id': 89, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 127, 'teleporter': [66, 0], 'access': []}, {'target_room': 62, 'entrance': 128, 'teleporter': [68, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Two Boxes Room', 'id': 61, 'game_objects': [{'name': 'Ice Pyramid 3F - Staircase Dead End Left Box', 'object_id': 94, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Staircase Dead End Right Box', 'object_id': 95, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 129, 'teleporter': [69, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Main Loop', 'id': 62, 'game_objects': [{'name': 'Ice Pyramid 3F - Inner Room North Box', 'object_id': 92, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Inner Room South Box', 'object_id': 93, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - East Alcove Box', 'object_id': 96, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Leapfrog Box', 'object_id': 97, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid3FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 60, 'entrance': 130, 'teleporter': [70, 0], 'access': []}, {'target_room': 63, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 3F Blocked Room', 'id': 63, 'game_objects': [], 'links': [{'target_room': 64, 'entrance': 131, 'teleporter': [71, 0], 'access': []}, {'target_room': 62, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 4F Main Loop', 'id': 64, 'game_objects': [], 'links': [{'target_room': 66, 'entrance': 133, 'teleporter': [73, 0], 'access': []}, {'target_room': 63, 'entrance': 132, 'teleporter': [72, 0], 'access': []}, {'target_room': 65, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 4F Treasure Room', 'id': 65, 'game_objects': [{'name': 'Ice Pyramid 4F - Chest', 'object_id': 12, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 4F - Northwest Box', 'object_id': 98, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Left Box', 'object_id': 99, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Right Box', 'object_id': 100, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Left Box', 'object_id': 101, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Right Box', 'object_id': 102, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Left Box', 'object_id': 103, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Right Box', 'object_id': 104, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid4FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 64, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 5F Leap of Faith Room', 'id': 66, 'game_objects': [{'name': 'Ice Pyramid 5F - Glass Door Left Box', 'object_id': 105, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - West Ledge Box', 'object_id': 106, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Shelf Box', 'object_id': 107, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Leapfrog Box', 'object_id': 108, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - Glass Door Right Box', 'object_id': 109, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - North Box', 'object_id': 110, 'type': 'Box', 'access': []}], 'links': [{'target_room': 64, 'entrance': 134, 'teleporter': [74, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 53, 'access': ['Bomb', 'Claw', 'Sword']}]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem', 'id': 67, 'game_objects': [{'name': 'Ice Pyramid 5F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid5FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 69, 'entrance': 137, 'teleporter': [76, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 70, 'entrance': 136, 'teleporter': [75, 0], 'access': []}]}, {'name': 'Ice Pyramid Climbing Wall Room Lower Space', 'id': 68, 'game_objects': [], 'links': [{'target_room': 53, 'entrance': 139, 'teleporter': [78, 0], 'access': []}, {'target_room': 69, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Climbing Wall Room Upper Space', 'id': 69, 'game_objects': [], 'links': [{'target_room': 67, 'entrance': 140, 'teleporter': [79, 0], 'access': []}, {'target_room': 68, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Ice Golem Room', 'id': 70, 'game_objects': [{'name': 'Ice Pyramid 6F - Ice Golem Chest', 'object_id': 14, 'type': 'Chest', 'access': ['IceGolem']}, {'name': 'Ice Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IceGolem'], 'access': []}], 'links': [{'target_room': 67, 'entrance': 141, 'teleporter': [80, 0], 'access': []}, {'target_room': 66, 'access': []}]}, {'name': 'Spencer Waterfall', 'id': 71, 'game_objects': [], 'links': [{'target_room': 72, 'entrance': 143, 'teleporter': [81, 0], 'access': []}, {'target_room': 40, 'entrance': 145, 'teleporter': [82, 0], 'access': []}, {'target_room': 40, 'entrance': 148, 'teleporter': [83, 0], 'access': []}]}, {'name': 'Spencer Cave Normal Main', 'id': 72, 'game_objects': [{'name': "Spencer's Cave - Box", 'object_id': 111, 'type': 'Box', 'access': ['Claw']}, {'name': "Spencer's Cave - Spencer", 'object_id': 8, 'type': 'NPC', 'access': []}, {'name': "Spencer's Cave - Locked Chest", 'object_id': 13, 'type': 'NPC', 'access': ['VenusKey']}], 'links': [{'target_room': 71, 'entrance': 150, 'teleporter': [85, 0], 'access': []}]}, {'name': 'Spencer Cave Normal South Ledge', 'id': 73, 'game_objects': [{'name': "Collapse Spencer's Cave", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLiberated'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 227, 'entrance': 151, 'teleporter': [7, 6], 'access': []}, {'target_room': 203, 'access': ['MegaGrenade']}]}, {'name': 'Spencer Cave Caved In Main Loop', 'id': 203, 'game_objects': [], 'links': [{'target_room': 73, 'access': []}, {'target_room': 207, 'entrance': 156, 'teleporter': [36, 8], 'access': ['MobiusCrest']}, {'target_room': 204, 'access': ['Claw']}, {'target_room': 205, 'access': ['Bomb']}]}, {'name': 'Spencer Cave Caved In Waters', 'id': 204, 'game_objects': [{'name': 'Bomb Libra Block', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SpencerCaveLibraBlockBombed'], 'access': ['MegaGrenade', 'Claw']}], 'links': [{'target_room': 203, 'access': ['Claw']}]}, {'name': 'Spencer Cave Caved In Libra Nook', 'id': 205, 'game_objects': [], 'links': [{'target_room': 206, 'entrance': 153, 'teleporter': [33, 8], 'access': ['LibraCrest']}]}, {'name': 'Spencer Cave Caved In Libra Corridor', 'id': 206, 'game_objects': [], 'links': [{'target_room': 205, 'entrance': 154, 'teleporter': [34, 8], 'access': ['LibraCrest']}, {'target_room': 207, 'access': ['SpencerCaveLibraBlockBombed']}]}, {'name': 'Spencer Cave Caved In Mobius Chest', 'id': 207, 'game_objects': [{'name': "Spencer's Cave - Mobius Chest", 'object_id': 15, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 203, 'entrance': 155, 'teleporter': [35, 8], 'access': ['MobiusCrest']}, {'target_room': 206, 'access': ['Bomb']}]}, {'name': 'Wintry Temple Outer Room', 'id': 74, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 157, 'teleporter': [15, 6], 'access': []}]}, {'name': 'Wintry Temple Inner Room', 'id': 75, 'game_objects': [{'name': 'Wintry Temple - West Box', 'object_id': 112, 'type': 'Box', 'access': []}, {'name': 'Wintry Temple - North Box', 'object_id': 113, 'type': 'Box', 'access': []}], 'links': [{'target_room': 92, 'entrance': 158, 'teleporter': [62, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Upper Plaza', 'id': 76, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 159, 'teleporter': [9, 6], 'access': []}, {'target_room': 80, 'entrance': 163, 'teleporter': [91, 0], 'access': []}, {'target_room': 77, 'entrance': 164, 'teleporter': [98, 8], 'access': []}, {'target_room': 82, 'entrance': 165, 'teleporter': [96, 8], 'access': []}, {'target_room': 208, 'access': ['Claw']}]}, {'name': 'Fireburg Lower Plaza', 'id': 208, 'game_objects': [{'name': 'Fireburg - Hidden Tunnel Box', 'object_id': 116, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'access': ['Claw']}, {'target_room': 78, 'entrance': 166, 'teleporter': [11, 8], 'access': ['MultiKey']}]}, {'name': "Reuben's House", 'id': 77, 'game_objects': [{'name': "Fireburg - Reuben's House Arion", 'object_id': 14, 'type': 'NPC', 'access': ['ReubenDadSaved']}, {'name': 'Reuben Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Reuben1'], 'access': []}, {'name': "Fireburg - Reuben's House Box", 'object_id': 117, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'entrance': 167, 'teleporter': [98, 3], 'access': []}]}, {'name': "GrenadeMan's House", 'id': 78, 'game_objects': [{'name': 'Fireburg - Locked House Man', 'object_id': 12, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 208, 'entrance': 168, 'teleporter': [9, 8], 'access': ['MultiKey']}, {'target_room': 79, 'entrance': 169, 'teleporter': [93, 0], 'access': []}]}, {'name': "GrenadeMan's Mobius Room", 'id': 79, 'game_objects': [], 'links': [{'target_room': 78, 'entrance': 170, 'teleporter': [94, 0], 'access': []}, {'target_room': 161, 'entrance': 171, 'teleporter': [54, 8], 'access': ['MobiusCrest']}]}, {'name': 'Fireburg Vendor House', 'id': 80, 'game_objects': [{'name': 'Fireburg - Vendor', 'object_id': 11, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 76, 'entrance': 172, 'teleporter': [95, 0], 'access': []}, {'target_room': 81, 'entrance': 173, 'teleporter': [96, 0], 'access': []}]}, {'name': 'Fireburg Gemini Room', 'id': 81, 'game_objects': [], 'links': [{'target_room': 80, 'entrance': 174, 'teleporter': [97, 0], 'access': []}, {'target_room': 43, 'entrance': 175, 'teleporter': [45, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Hotel Lobby', 'id': 82, 'game_objects': [{'name': 'Fireburg - Tristam', 'object_id': 10, 'type': 'NPC', 'access': ['Tristam', 'TristamBoneItemGiven']}], 'links': [{'target_room': 76, 'entrance': 177, 'teleporter': [99, 3], 'access': []}, {'target_room': 83, 'entrance': 176, 'teleporter': [213, 0], 'access': []}]}, {'name': 'Fireburg Hotel Beds', 'id': 83, 'game_objects': [], 'links': [{'target_room': 82, 'entrance': 178, 'teleporter': [214, 0], 'access': []}]}, {'name': 'Mine Exterior North West Platforms', 'id': 84, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 179, 'teleporter': [98, 0], 'access': []}, {'target_room': 88, 'entrance': 181, 'teleporter': [20, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}, {'target_room': 86, 'access': ['Claw']}, {'target_room': 87, 'access': ['Claw']}]}, {'name': 'Mine Exterior Central Ledge', 'id': 85, 'game_objects': [], 'links': [{'target_room': 90, 'entrance': 183, 'teleporter': [22, 2], 'access': ['Bomb']}, {'target_room': 84, 'access': ['Claw']}]}, {'name': 'Mine Exterior North Ledge', 'id': 86, 'game_objects': [], 'links': [{'target_room': 89, 'entrance': 182, 'teleporter': [21, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Exterior South East Platforms', 'id': 87, 'game_objects': [{'name': 'Jinn', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Jinn'], 'access': []}], 'links': [{'target_room': 91, 'entrance': 180, 'teleporter': [99, 0], 'access': ['Jinn']}, {'target_room': 86, 'access': []}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Parallel Room', 'id': 88, 'game_objects': [{'name': 'Mine - Parallel Room West Box', 'object_id': 119, 'type': 'Box', 'access': ['Claw']}, {'name': 'Mine - Parallel Room East Box', 'object_id': 120, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 84, 'entrance': 185, 'teleporter': [100, 3], 'access': []}]}, {'name': 'Mine Crescent Room', 'id': 89, 'game_objects': [{'name': 'Mine - Crescent Room Chest', 'object_id': 16, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 86, 'entrance': 186, 'teleporter': [101, 3], 'access': []}]}, {'name': 'Mine Climbing Room', 'id': 90, 'game_objects': [{'name': 'Mine - Glitchy Collision Cave Box', 'object_id': 118, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 85, 'entrance': 187, 'teleporter': [102, 3], 'access': []}]}, {'name': 'Mine Cliff', 'id': 91, 'game_objects': [{'name': 'Mine - Cliff Southwest Box', 'object_id': 121, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northwest Box', 'object_id': 122, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northeast Box', 'object_id': 123, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Southeast Box', 'object_id': 124, 'type': 'Box', 'access': []}, {'name': 'Mine - Reuben', 'object_id': 7, 'type': 'NPC', 'access': ['Reuben1']}, {'name': "Reuben's dad Saved", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ReubenDadSaved'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 87, 'entrance': 188, 'teleporter': [100, 0], 'access': []}]}, {'name': 'Sealed Temple', 'id': 92, 'game_objects': [{'name': 'Sealed Temple - West Box', 'object_id': 125, 'type': 'Box', 'access': []}, {'name': 'Sealed Temple - East Box', 'object_id': 126, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 190, 'teleporter': [16, 6], 'access': []}, {'target_room': 75, 'entrance': 191, 'teleporter': [63, 8], 'access': ['GeminiCrest']}]}, {'name': 'Volcano Base', 'id': 93, 'game_objects': [{'name': 'Volcano - Base Chest', 'object_id': 17, 'type': 'Chest', 'access': []}, {'name': 'Volcano - Base West Box', 'object_id': 127, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Left Box', 'object_id': 128, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Right Box', 'object_id': 129, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 192, 'teleporter': [103, 0], 'access': []}, {'target_room': 98, 'entrance': 196, 'teleporter': [31, 8], 'access': []}, {'target_room': 96, 'entrance': 197, 'teleporter': [30, 8], 'access': []}]}, {'name': 'Volcano Top Left', 'id': 94, 'game_objects': [{'name': 'Volcano - Medusa Chest', 'object_id': 18, 'type': 'Chest', 'access': ['Medusa']}, {'name': 'Medusa', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Medusa'], 'access': []}, {'name': 'Volcano - Behind Medusa Box', 'object_id': 130, 'type': 'Box', 'access': []}], 'links': [{'target_room': 209, 'entrance': 199, 'teleporter': [26, 8], 'access': []}]}, {'name': 'Volcano Top Right', 'id': 95, 'game_objects': [{'name': 'Volcano - Top of the Volcano Left Box', 'object_id': 131, 'type': 'Box', 'access': []}, {'name': 'Volcano - Top of the Volcano Right Box', 'object_id': 132, 'type': 'Box', 'access': []}], 'links': [{'target_room': 99, 'entrance': 200, 'teleporter': [79, 8], 'access': []}]}, {'name': 'Volcano Right Path', 'id': 96, 'game_objects': [{'name': 'Volcano - Right Path Box', 'object_id': 135, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 201, 'teleporter': [15, 8], 'access': []}]}, {'name': 'Volcano Left Path', 'id': 98, 'game_objects': [{'name': 'Volcano - Left Path Box', 'object_id': 134, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 204, 'teleporter': [27, 8], 'access': []}, {'target_room': 99, 'entrance': 202, 'teleporter': [25, 2], 'access': []}, {'target_room': 209, 'entrance': 203, 'teleporter': [26, 2], 'access': []}]}, {'name': 'Volcano Cross Left-Right', 'id': 99, 'game_objects': [], 'links': [{'target_room': 95, 'entrance': 206, 'teleporter': [29, 8], 'access': []}, {'target_room': 98, 'entrance': 205, 'teleporter': [103, 3], 'access': []}]}, {'name': 'Volcano Cross Right-Left', 'id': 209, 'game_objects': [{'name': 'Volcano - Crossover Section Box', 'object_id': 133, 'type': 'Box', 'access': []}], 'links': [{'target_room': 98, 'entrance': 208, 'teleporter': [104, 3], 'access': []}, {'target_room': 94, 'entrance': 207, 'teleporter': [28, 8], 'access': []}]}, {'name': 'Lava Dome Inner Ring Main Loop', 'id': 100, 'game_objects': [{'name': 'Lava Dome - Exterior Caldera Near Switch Cliff Box', 'object_id': 136, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Exterior South Cliff Box', 'object_id': 137, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 209, 'teleporter': [104, 0], 'access': []}, {'target_room': 113, 'entrance': 211, 'teleporter': [105, 0], 'access': []}, {'target_room': 114, 'entrance': 212, 'teleporter': [106, 0], 'access': []}, {'target_room': 116, 'entrance': 213, 'teleporter': [108, 0], 'access': []}, {'target_room': 118, 'entrance': 214, 'teleporter': [111, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Center Ledge', 'id': 101, 'game_objects': [{'name': 'Lava Dome - Exterior Center Dropoff Ledge Box', 'object_id': 138, 'type': 'Box', 'access': []}], 'links': [{'target_room': 115, 'entrance': 215, 'teleporter': [107, 0], 'access': []}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Plate Ledge', 'id': 102, 'game_objects': [{'name': 'Lava Dome Plate', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LavaDomePlate'], 'access': []}], 'links': [{'target_room': 119, 'entrance': 216, 'teleporter': [109, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Upper Ledge West', 'id': 103, 'game_objects': [], 'links': [{'target_room': 111, 'entrance': 219, 'teleporter': [112, 0], 'access': []}, {'target_room': 108, 'entrance': 220, 'teleporter': [113, 0], 'access': []}, {'target_room': 104, 'access': ['Claw']}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Upper Ledge East', 'id': 104, 'game_objects': [], 'links': [{'target_room': 110, 'entrance': 218, 'teleporter': [110, 0], 'access': []}, {'target_room': 103, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Big Door Ledge', 'id': 105, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 221, 'teleporter': [114, 0], 'access': []}, {'target_room': 121, 'entrance': 222, 'teleporter': [29, 2], 'access': ['LavaDomePlate']}]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge', 'id': 106, 'game_objects': [{'name': 'Lava Dome - Exterior Dead End Caldera Box', 'object_id': 139, 'type': 'Box', 'access': []}], 'links': [{'target_room': 120, 'entrance': 226, 'teleporter': [115, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze II', 'id': 107, 'game_objects': [{'name': 'Lava Dome - Gold Maze Northwest Box', 'object_id': 140, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southwest Box', 'object_id': 246, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Northeast Box', 'object_id': 247, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze North Box', 'object_id': 248, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Center Box', 'object_id': 249, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southeast Box', 'object_id': 250, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 227, 'teleporter': [116, 0], 'access': []}, {'target_room': 108, 'entrance': 228, 'teleporter': [119, 0], 'access': []}, {'target_room': 120, 'entrance': 229, 'teleporter': [120, 0], 'access': []}]}, {'name': 'Lava Dome Up-Down Corridor', 'id': 108, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 231, 'teleporter': [118, 0], 'access': []}, {'target_room': 103, 'entrance': 230, 'teleporter': [117, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze I', 'id': 109, 'game_objects': [{'name': 'Lava Dome - Bare Maze Leapfrog Alcove North Box', 'object_id': 141, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Leapfrog Alcove South Box', 'object_id': 142, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Center Box', 'object_id': 143, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Southwest Box', 'object_id': 144, 'type': 'Box', 'access': []}], 'links': [{'target_room': 118, 'entrance': 232, 'teleporter': [121, 0], 'access': []}, {'target_room': 111, 'entrance': 233, 'teleporter': [122, 0], 'access': []}]}, {'name': 'Lava Dome Pointless Room', 'id': 110, 'game_objects': [], 'links': [{'target_room': 104, 'entrance': 234, 'teleporter': [123, 0], 'access': []}]}, {'name': 'Lava Dome Lower Moon Helm Room', 'id': 111, 'game_objects': [{'name': 'Lava Dome - U-Bend Room North Box', 'object_id': 146, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - U-Bend Room South Box', 'object_id': 147, 'type': 'Box', 'access': []}], 'links': [{'target_room': 103, 'entrance': 235, 'teleporter': [124, 0], 'access': []}, {'target_room': 109, 'entrance': 236, 'teleporter': [125, 0], 'access': []}]}, {'name': 'Lava Dome Moon Helm Room', 'id': 112, 'game_objects': [{'name': 'Lava Dome - Beyond River Room Chest', 'object_id': 19, 'type': 'Chest', 'access': []}, {'name': 'Lava Dome - Beyond River Room Box', 'object_id': 145, 'type': 'Box', 'access': []}], 'links': [{'target_room': 117, 'entrance': 237, 'teleporter': [126, 0], 'access': []}]}, {'name': 'Lava Dome Three Jumps Room', 'id': 113, 'game_objects': [{'name': 'Lava Dome - Three Jumps Room Box', 'object_id': 150, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 238, 'teleporter': [127, 0], 'access': []}]}, {'name': 'Lava Dome Life Chest Room Lower Ledge', 'id': 114, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Boulder Chest', 'object_id': 28, 'type': 'Chest', 'access': ['MegaGrenade']}], 'links': [{'target_room': 100, 'entrance': 239, 'teleporter': [128, 0], 'access': []}, {'target_room': 115, 'access': ['Claw']}]}, {'name': 'Lava Dome Life Chest Room Upper Ledge', 'id': 115, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box West', 'object_id': 148, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box East', 'object_id': 149, 'type': 'Box', 'access': []}], 'links': [{'target_room': 101, 'entrance': 240, 'teleporter': [129, 0], 'access': []}, {'target_room': 114, 'access': ['Claw']}]}, {'name': 'Lava Dome Big Jump Room Main Area', 'id': 116, 'game_objects': [{'name': 'Lava Dome - Lava River Room North Box', 'object_id': 152, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room East Box', 'object_id': 153, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room South Box', 'object_id': 154, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 241, 'teleporter': [133, 0], 'access': []}, {'target_room': 119, 'entrance': 243, 'teleporter': [132, 0], 'access': []}, {'target_room': 117, 'access': ['MegaGrenade']}]}, {'name': 'Lava Dome Big Jump Room MegaGrenade Area', 'id': 117, 'game_objects': [], 'links': [{'target_room': 112, 'entrance': 242, 'teleporter': [131, 0], 'access': []}, {'target_room': 116, 'access': ['Bomb']}]}, {'name': 'Lava Dome Split Corridor', 'id': 118, 'game_objects': [{'name': 'Lava Dome - Split Corridor Box', 'object_id': 151, 'type': 'Box', 'access': []}], 'links': [{'target_room': 109, 'entrance': 244, 'teleporter': [130, 0], 'access': []}, {'target_room': 100, 'entrance': 245, 'teleporter': [134, 0], 'access': []}]}, {'name': 'Lava Dome Plate Corridor', 'id': 119, 'game_objects': [], 'links': [{'target_room': 102, 'entrance': 246, 'teleporter': [135, 0], 'access': []}, {'target_room': 116, 'entrance': 247, 'teleporter': [137, 0], 'access': []}]}, {'name': 'Lava Dome Four Boxes Stairs', 'id': 120, 'game_objects': [{'name': 'Lava Dome - Caldera Stairway West Left Box', 'object_id': 155, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway West Right Box', 'object_id': 156, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Left Box', 'object_id': 157, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Right Box', 'object_id': 158, 'type': 'Box', 'access': []}], 'links': [{'target_room': 107, 'entrance': 248, 'teleporter': [136, 0], 'access': []}, {'target_room': 106, 'entrance': 249, 'teleporter': [16, 0], 'access': []}]}, {'name': 'Lava Dome Hydra Room', 'id': 121, 'game_objects': [{'name': 'Lava Dome - Dualhead Hydra Chest', 'object_id': 20, 'type': 'Chest', 'access': ['DualheadHydra']}, {'name': 'Dualhead Hydra', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['DualheadHydra'], 'access': []}, {'name': 'Lava Dome - Hydra Room Northwest Box', 'object_id': 159, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Hydra Room Southweast Box', 'object_id': 160, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 250, 'teleporter': [105, 3], 'access': []}, {'target_room': 122, 'entrance': 251, 'teleporter': [138, 0], 'access': ['DualheadHydra']}]}, {'name': 'Lava Dome Escape Corridor', 'id': 122, 'game_objects': [], 'links': [{'target_room': 121, 'entrance': 253, 'teleporter': [139, 0], 'access': []}]}, {'name': 'Rope Bridge', 'id': 123, 'game_objects': [{'name': 'Rope Bridge - West Box', 'object_id': 163, 'type': 'Box', 'access': []}, {'name': 'Rope Bridge - East Box', 'object_id': 164, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 255, 'teleporter': [140, 0], 'access': []}]}, {'name': 'Alive Forest', 'id': 124, 'game_objects': [{'name': 'Alive Forest - Tree Stump Chest', 'object_id': 21, 'type': 'Chest', 'access': ['Axe']}, {'name': 'Alive Forest - Near Entrance Box', 'object_id': 165, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - After Bridge Box', 'object_id': 166, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - Gemini Stump Box', 'object_id': 167, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 226, 'entrance': 272, 'teleporter': [142, 0], 'access': ['Axe']}, {'target_room': 21, 'entrance': 275, 'teleporter': [64, 8], 'access': ['LibraCrest', 'Axe']}, {'target_room': 22, 'entrance': 276, 'teleporter': [65, 8], 'access': ['GeminiCrest', 'Axe']}, {'target_room': 23, 'entrance': 277, 'teleporter': [66, 8], 'access': ['MobiusCrest', 'Axe']}, {'target_room': 125, 'entrance': 274, 'teleporter': [143, 0], 'access': ['Axe']}]}, {'name': 'Giant Tree 1F Main Area', 'id': 125, 'game_objects': [{'name': 'Giant Tree 1F - Northwest Box', 'object_id': 168, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Southwest Box', 'object_id': 169, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Center Box', 'object_id': 170, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - East Box', 'object_id': 171, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 278, 'teleporter': [56, 1], 'access': []}, {'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F North Island', 'id': 202, 'game_objects': [], 'links': [{'target_room': 127, 'entrance': 280, 'teleporter': [144, 0], 'access': []}, {'target_room': 125, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F Central Island', 'id': 126, 'game_objects': [], 'links': [{'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Main Lobby', 'id': 127, 'game_objects': [{'name': 'Giant Tree 2F - North Box', 'object_id': 172, 'type': 'Box', 'access': []}], 'links': [{'target_room': 126, 'access': ['DragonClaw']}, {'target_room': 125, 'entrance': 281, 'teleporter': [145, 0], 'access': []}, {'target_room': 133, 'entrance': 283, 'teleporter': [149, 0], 'access': []}, {'target_room': 129, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F West Ledge', 'id': 128, 'game_objects': [{'name': 'Giant Tree 2F - Dropdown Ledge Box', 'object_id': 174, 'type': 'Box', 'access': []}], 'links': [{'target_room': 140, 'entrance': 284, 'teleporter': [147, 0], 'access': ['Sword']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Lower Area', 'id': 129, 'game_objects': [{'name': 'Giant Tree 2F - South Box', 'object_id': 173, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'access': ['Claw']}, {'target_room': 131, 'access': ['Claw']}]}, {'name': 'Giant Tree 2F Central Island', 'id': 130, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 135, 'entrance': 282, 'teleporter': [146, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 2F East Ledge', 'id': 131, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Meteor Chest Room', 'id': 132, 'game_objects': [{'name': 'Giant Tree 2F - Gidrah Chest', 'object_id': 22, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 133, 'entrance': 285, 'teleporter': [148, 0], 'access': []}]}, {'name': 'Giant Tree 2F Mushroom Room', 'id': 133, 'game_objects': [{'name': 'Giant Tree 2F - Mushroom Tunnel West Box', 'object_id': 175, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 2F - Mushroom Tunnel East Box', 'object_id': 176, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 127, 'entrance': 286, 'teleporter': [150, 0], 'access': ['Axe']}, {'target_room': 132, 'entrance': 287, 'teleporter': [151, 0], 'access': ['Axe', 'Gidrah']}]}, {'name': 'Giant Tree 3F Central Island', 'id': 135, 'game_objects': [{'name': 'Giant Tree 3F - Central Island Box', 'object_id': 179, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'entrance': 288, 'teleporter': [152, 0], 'access': []}, {'target_room': 136, 'access': ['Claw']}, {'target_room': 137, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 3F Central Area', 'id': 136, 'game_objects': [{'name': 'Giant Tree 3F - Center North Box', 'object_id': 177, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 3F - Center West Box', 'object_id': 178, 'type': 'Box', 'access': []}], 'links': [{'target_room': 135, 'access': ['Claw']}, {'target_room': 127, 'access': []}, {'target_room': 131, 'access': []}]}, {'name': 'Giant Tree 3F Lower Ledge', 'id': 137, 'game_objects': [], 'links': [{'target_room': 135, 'access': ['DragonClaw']}, {'target_room': 142, 'entrance': 289, 'teleporter': [153, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 3F West Area', 'id': 138, 'game_objects': [{'name': 'Giant Tree 3F - West Side Box', 'object_id': 180, 'type': 'Box', 'access': []}], 'links': [{'target_room': 128, 'access': []}, {'target_room': 210, 'entrance': 290, 'teleporter': [154, 0], 'access': []}]}, {'name': 'Giant Tree 3F Middle Up Island', 'id': 139, 'game_objects': [], 'links': [{'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree 3F West Platform', 'id': 140, 'game_objects': [], 'links': [{'target_room': 139, 'access': ['Claw']}, {'target_room': 141, 'access': ['Claw']}, {'target_room': 128, 'entrance': 291, 'teleporter': [155, 0], 'access': []}]}, {'name': 'Giant Tree 3F North Ledge', 'id': 141, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 292, 'teleporter': [156, 0], 'access': ['Sword']}, {'target_room': 139, 'access': ['Claw']}, {'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree Worm Room Upper Ledge', 'id': 142, 'game_objects': [{'name': 'Giant Tree 3F - Worm Room North Box', 'object_id': 181, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 3F - Worm Room South Box', 'object_id': 182, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 137, 'entrance': 293, 'teleporter': [157, 0], 'access': ['Axe']}, {'target_room': 210, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree Worm Room Lower Ledge', 'id': 210, 'game_objects': [], 'links': [{'target_room': 138, 'entrance': 294, 'teleporter': [158, 0], 'access': []}]}, {'name': 'Giant Tree 4F Lower Floor', 'id': 143, 'game_objects': [], 'links': [{'target_room': 141, 'entrance': 295, 'teleporter': [159, 0], 'access': []}, {'target_room': 148, 'entrance': 296, 'teleporter': [160, 0], 'access': []}, {'target_room': 148, 'entrance': 297, 'teleporter': [161, 0], 'access': []}, {'target_room': 147, 'entrance': 298, 'teleporter': [162, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 4F Middle Floor', 'id': 144, 'game_objects': [{'name': 'Giant Tree 4F - Highest Platform North Box', 'object_id': 183, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Highest Platform South Box', 'object_id': 184, 'type': 'Box', 'access': []}], 'links': [{'target_room': 149, 'entrance': 299, 'teleporter': [163, 0], 'access': []}, {'target_room': 145, 'access': ['Claw']}, {'target_room': 146, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Upper Floor', 'id': 145, 'game_objects': [], 'links': [{'target_room': 150, 'entrance': 300, 'teleporter': [164, 0], 'access': ['Sword']}, {'target_room': 144, 'access': ['Claw']}]}, {'name': 'Giant Tree 4F South Ledge', 'id': 146, 'game_objects': [{'name': 'Giant Tree 4F - Hook Ledge Northeast Box', 'object_id': 185, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Hook Ledge Southwest Box', 'object_id': 186, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Slime Room East Area', 'id': 147, 'game_objects': [{'name': 'Giant Tree 4F - East Slime Room Box', 'object_id': 188, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 143, 'entrance': 304, 'teleporter': [168, 0], 'access': []}]}, {'name': 'Giant Tree 4F Slime Room West Area', 'id': 148, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 303, 'teleporter': [167, 0], 'access': ['Axe']}, {'target_room': 143, 'entrance': 302, 'teleporter': [166, 0], 'access': ['Axe']}, {'target_room': 149, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree 4F Slime Room Platform', 'id': 149, 'game_objects': [{'name': 'Giant Tree 4F - West Slime Room Box', 'object_id': 187, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'entrance': 301, 'teleporter': [165, 0], 'access': []}, {'target_room': 148, 'access': ['Claw']}]}, {'name': 'Giant Tree 5F Lower Area', 'id': 150, 'game_objects': [{'name': 'Giant Tree 5F - Northwest Left Box', 'object_id': 189, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - Northwest Right Box', 'object_id': 190, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Left Box', 'object_id': 191, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Right Box', 'object_id': 192, 'type': 'Box', 'access': []}], 'links': [{'target_room': 145, 'entrance': 305, 'teleporter': [169, 0], 'access': []}, {'target_room': 151, 'access': ['Claw']}, {'target_room': 143, 'access': []}]}, {'name': 'Giant Tree 5F Gidrah Platform', 'id': 151, 'game_objects': [{'name': 'Gidrah', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Gidrah'], 'access': []}], 'links': [{'target_room': 150, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Lower Ledge', 'id': 152, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 307, 'teleporter': [18, 6], 'access': []}, {'target_room': 153, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Upper Ledge', 'id': 153, 'game_objects': [{'name': 'Kaidge Temple - Box', 'object_id': 193, 'type': 'Box', 'access': []}], 'links': [{'target_room': 185, 'entrance': 308, 'teleporter': [71, 8], 'access': ['MobiusCrest']}, {'target_room': 152, 'access': ['Claw']}]}, {'name': 'Windhole Temple', 'id': 154, 'game_objects': [{'name': 'Windhole Temple - Box', 'object_id': 194, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 309, 'teleporter': [173, 0], 'access': []}]}, {'name': 'Mount Gale', 'id': 155, 'game_objects': [{'name': 'Mount Gale - Dullahan Chest', 'object_id': 23, 'type': 'Chest', 'access': ['DragonClaw', 'Dullahan']}, {'name': 'Dullahan', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Dullahan'], 'access': ['DragonClaw']}, {'name': 'Mount Gale - East Box', 'object_id': 195, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Mount Gale - West Box', 'object_id': 196, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 310, 'teleporter': [174, 0], 'access': []}]}, {'name': 'Windia', 'id': 156, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 312, 'teleporter': [10, 6], 'access': []}, {'target_room': 157, 'entrance': 320, 'teleporter': [30, 5], 'access': []}, {'target_room': 163, 'entrance': 321, 'teleporter': [97, 8], 'access': []}, {'target_room': 165, 'entrance': 322, 'teleporter': [32, 5], 'access': []}, {'target_room': 159, 'entrance': 323, 'teleporter': [176, 4], 'access': []}, {'target_room': 160, 'entrance': 324, 'teleporter': [177, 4], 'access': []}]}, {'name': "Otto's House", 'id': 157, 'game_objects': [{'name': 'Otto', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['RainbowBridge'], 'access': ['ThunderRock']}], 'links': [{'target_room': 156, 'entrance': 327, 'teleporter': [106, 3], 'access': []}, {'target_room': 158, 'entrance': 326, 'teleporter': [33, 2], 'access': []}]}, {'name': "Otto's Attic", 'id': 158, 'game_objects': [{'name': "Windia - Otto's Attic Box", 'object_id': 197, 'type': 'Box', 'access': []}], 'links': [{'target_room': 157, 'entrance': 328, 'teleporter': [107, 3], 'access': []}]}, {'name': 'Windia Kid House', 'id': 159, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 329, 'teleporter': [178, 0], 'access': []}, {'target_room': 161, 'entrance': 330, 'teleporter': [180, 0], 'access': []}]}, {'name': 'Windia Old People House', 'id': 160, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 331, 'teleporter': [179, 0], 'access': []}, {'target_room': 162, 'entrance': 332, 'teleporter': [181, 0], 'access': []}]}, {'name': 'Windia Kid House Basement', 'id': 161, 'game_objects': [], 'links': [{'target_room': 159, 'entrance': 333, 'teleporter': [182, 0], 'access': []}, {'target_room': 79, 'entrance': 334, 'teleporter': [44, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Old People House Basement', 'id': 162, 'game_objects': [{'name': 'Windia - Mobius Basement West Box', 'object_id': 200, 'type': 'Box', 'access': []}, {'name': 'Windia - Mobius Basement East Box', 'object_id': 201, 'type': 'Box', 'access': []}], 'links': [{'target_room': 160, 'entrance': 335, 'teleporter': [183, 0], 'access': []}, {'target_room': 186, 'entrance': 336, 'teleporter': [43, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Inn Lobby', 'id': 163, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 338, 'teleporter': [135, 3], 'access': []}, {'target_room': 164, 'entrance': 337, 'teleporter': [102, 8], 'access': []}]}, {'name': 'Windia Inn Beds', 'id': 164, 'game_objects': [{'name': 'Windia - Inn Bedroom North Box', 'object_id': 198, 'type': 'Box', 'access': []}, {'name': 'Windia - Inn Bedroom South Box', 'object_id': 199, 'type': 'Box', 'access': []}, {'name': 'Windia - Kaeli', 'object_id': 15, 'type': 'NPC', 'access': ['Kaeli2']}], 'links': [{'target_room': 163, 'entrance': 339, 'teleporter': [216, 0], 'access': []}]}, {'name': 'Windia Vendor House', 'id': 165, 'game_objects': [{'name': 'Windia - Vendor', 'object_id': 16, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 156, 'entrance': 340, 'teleporter': [108, 3], 'access': []}]}, {'name': 'Pazuzu Tower 1F Main Lobby', 'id': 166, 'game_objects': [{'name': 'Pazuzu 1F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu1F'], 'access': []}], 'links': [{'target_room': 226, 'entrance': 341, 'teleporter': [184, 0], 'access': []}, {'target_room': 180, 'entrance': 345, 'teleporter': [185, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Boxes Room', 'id': 167, 'game_objects': [{'name': "Pazuzu's Tower 1F - Descent Bomb Wall West Box", 'object_id': 202, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall Center Box", 'object_id': 203, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall East Box", 'object_id': 204, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Box", 'object_id': 205, 'type': 'Box', 'access': []}], 'links': [{'target_room': 169, 'entrance': 349, 'teleporter': [187, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Southern Platform', 'id': 168, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 346, 'teleporter': [186, 0], 'access': []}, {'target_room': 166, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 2F', 'id': 169, 'game_objects': [{'name': "Pazuzu's Tower 2F - East Room West Box", 'object_id': 206, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 2F - East Room East Box", 'object_id': 207, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 2F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 2F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 350, 'teleporter': [188, 0], 'access': []}, {'target_room': 168, 'entrance': 351, 'teleporter': [189, 0], 'access': []}, {'target_room': 167, 'entrance': 352, 'teleporter': [190, 0], 'access': []}, {'target_room': 171, 'entrance': 353, 'teleporter': [191, 0], 'access': []}]}, {'name': 'Pazuzu 3F Main Room', 'id': 170, 'game_objects': [{'name': "Pazuzu's Tower 3F - Guest Room West Box", 'object_id': 208, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 3F - Guest Room East Box", 'object_id': 209, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 3F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu3F'], 'access': []}], 'links': [{'target_room': 180, 'entrance': 356, 'teleporter': [192, 0], 'access': []}, {'target_room': 181, 'entrance': 357, 'teleporter': [193, 0], 'access': []}]}, {'name': 'Pazuzu 3F Central Island', 'id': 171, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 360, 'teleporter': [194, 0], 'access': []}, {'target_room': 170, 'access': ['DragonClaw']}, {'target_room': 172, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 3F Southern Island', 'id': 172, 'game_objects': [{'name': "Pazuzu's Tower 3F - South Ledge Box", 'object_id': 210, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 361, 'teleporter': [195, 0], 'access': []}, {'target_room': 171, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 4F', 'id': 173, 'game_objects': [{'name': "Pazuzu's Tower 4F - Elevator West Box", 'object_id': 211, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - Elevator East Box", 'object_id': 212, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - East Storage Room Chest", 'object_id': 24, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 4F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 4F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 362, 'teleporter': [196, 0], 'access': []}, {'target_room': 184, 'entrance': 363, 'teleporter': [197, 0], 'access': []}, {'target_room': 172, 'entrance': 364, 'teleporter': [198, 0], 'access': []}, {'target_room': 175, 'entrance': 365, 'teleporter': [199, 0], 'access': []}]}, {'name': 'Pazuzu 5F Pazuzu Loop', 'id': 174, 'game_objects': [{'name': 'Pazuzu 5F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu5F'], 'access': []}], 'links': [{'target_room': 181, 'entrance': 368, 'teleporter': [200, 0], 'access': []}, {'target_room': 182, 'entrance': 369, 'teleporter': [201, 0], 'access': []}]}, {'name': 'Pazuzu 5F Upper Loop', 'id': 175, 'game_objects': [{'name': "Pazuzu's Tower 5F - North Box", 'object_id': 213, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 5F - South Box", 'object_id': 214, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 370, 'teleporter': [202, 0], 'access': []}, {'target_room': 176, 'entrance': 371, 'teleporter': [203, 0], 'access': []}]}, {'name': 'Pazuzu 6F', 'id': 176, 'game_objects': [{'name': "Pazuzu's Tower 6F - Box", 'object_id': 215, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 6F - Chest", 'object_id': 25, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 6F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6FLock'], 'access': ['Bomb', 'Axe']}, {'name': 'Pazuzu 6F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6F'], 'access': ['Bomb']}], 'links': [{'target_room': 184, 'entrance': 374, 'teleporter': [204, 0], 'access': []}, {'target_room': 175, 'entrance': 375, 'teleporter': [205, 0], 'access': []}, {'target_room': 178, 'entrance': 376, 'teleporter': [206, 0], 'access': []}, {'target_room': 178, 'entrance': 377, 'teleporter': [207, 0], 'access': []}]}, {'name': 'Pazuzu 7F Southwest Area', 'id': 177, 'game_objects': [], 'links': [{'target_room': 182, 'entrance': 380, 'teleporter': [26, 0], 'access': []}, {'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 7F Rest of the Area', 'id': 178, 'game_objects': [], 'links': [{'target_room': 177, 'access': ['DragonClaw']}, {'target_room': 176, 'entrance': 381, 'teleporter': [27, 0], 'access': []}, {'target_room': 176, 'entrance': 382, 'teleporter': [28, 0], 'access': []}, {'target_room': 179, 'access': ['DragonClaw', 'Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}]}, {'name': 'Pazuzu 7F Sky Room', 'id': 179, 'game_objects': [{'name': "Pazuzu's Tower 7F - Pazuzu Chest", 'object_id': 26, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu'], 'access': ['Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}], 'links': [{'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 1F to 3F', 'id': 180, 'game_objects': [], 'links': [{'target_room': 166, 'entrance': 385, 'teleporter': [29, 0], 'access': []}, {'target_room': 170, 'entrance': 386, 'teleporter': [30, 0], 'access': []}]}, {'name': 'Pazuzu 3F to 5F', 'id': 181, 'game_objects': [], 'links': [{'target_room': 170, 'entrance': 387, 'teleporter': [40, 0], 'access': []}, {'target_room': 174, 'entrance': 388, 'teleporter': [41, 0], 'access': []}]}, {'name': 'Pazuzu 5F to 7F', 'id': 182, 'game_objects': [], 'links': [{'target_room': 174, 'entrance': 389, 'teleporter': [38, 0], 'access': []}, {'target_room': 177, 'entrance': 390, 'teleporter': [39, 0], 'access': []}]}, {'name': 'Pazuzu 2F to 4F', 'id': 183, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 391, 'teleporter': [21, 0], 'access': []}, {'target_room': 173, 'entrance': 392, 'teleporter': [22, 0], 'access': []}]}, {'name': 'Pazuzu 4F to 6F', 'id': 184, 'game_objects': [], 'links': [{'target_room': 173, 'entrance': 393, 'teleporter': [2, 0], 'access': []}, {'target_room': 176, 'entrance': 394, 'teleporter': [3, 0], 'access': []}]}, {'name': 'Light Temple', 'id': 185, 'game_objects': [{'name': 'Light Temple - Box', 'object_id': 216, 'type': 'Box', 'access': []}], 'links': [{'target_room': 230, 'entrance': 395, 'teleporter': [19, 6], 'access': []}, {'target_room': 153, 'entrance': 396, 'teleporter': [70, 8], 'access': ['MobiusCrest']}]}, {'name': 'Ship Dock', 'id': 186, 'game_objects': [{'name': 'Ship Dock Access', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipDockAccess'], 'access': []}], 'links': [{'target_room': 228, 'entrance': 399, 'teleporter': [17, 6], 'access': []}, {'target_room': 162, 'entrance': 397, 'teleporter': [61, 8], 'access': ['MobiusCrest']}]}, {'name': 'Mac Ship Deck', 'id': 187, 'game_objects': [{'name': 'Mac Ship Steering Wheel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipSteeringWheel'], 'access': []}, {'name': "Mac's Ship Deck - North Box", 'object_id': 217, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - Center Box", 'object_id': 218, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - South Box", 'object_id': 219, 'type': 'Box', 'access': []}], 'links': [{'target_room': 229, 'entrance': 400, 'teleporter': [37, 8], 'access': []}, {'target_room': 188, 'entrance': 401, 'teleporter': [50, 8], 'access': []}, {'target_room': 188, 'entrance': 402, 'teleporter': [51, 8], 'access': []}, {'target_room': 188, 'entrance': 403, 'teleporter': [52, 8], 'access': []}, {'target_room': 189, 'entrance': 404, 'teleporter': [53, 8], 'access': []}]}, {'name': 'Mac Ship B1 Outer Ring', 'id': 188, 'game_objects': [{'name': "Mac's Ship B1 - Northwest Hook Platform Box", 'object_id': 228, 'type': 'Box', 'access': ['DragonClaw']}, {'name': "Mac's Ship B1 - Center Hook Platform Box", 'object_id': 229, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 187, 'entrance': 405, 'teleporter': [208, 0], 'access': []}, {'target_room': 187, 'entrance': 406, 'teleporter': [175, 0], 'access': []}, {'target_room': 187, 'entrance': 407, 'teleporter': [172, 0], 'access': []}, {'target_room': 193, 'entrance': 408, 'teleporter': [88, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B1 Square Room', 'id': 189, 'game_objects': [], 'links': [{'target_room': 187, 'entrance': 409, 'teleporter': [141, 0], 'access': []}, {'target_room': 192, 'entrance': 410, 'teleporter': [87, 0], 'access': []}]}, {'name': 'Mac Ship B1 Central Corridor', 'id': 190, 'game_objects': [{'name': "Mac's Ship B1 - Central Corridor Box", 'object_id': 230, 'type': 'Box', 'access': []}], 'links': [{'target_room': 192, 'entrance': 413, 'teleporter': [86, 0], 'access': []}, {'target_room': 191, 'entrance': 412, 'teleporter': [102, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B2 South Corridor', 'id': 191, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 415, 'teleporter': [55, 8], 'access': []}, {'target_room': 194, 'entrance': 414, 'teleporter': [57, 1], 'access': []}]}, {'name': 'Mac Ship B2 North Corridor', 'id': 192, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 416, 'teleporter': [56, 8], 'access': []}, {'target_room': 189, 'entrance': 417, 'teleporter': [57, 8], 'access': []}]}, {'name': 'Mac Ship B2 Outer Ring', 'id': 193, 'game_objects': [{'name': "Mac's Ship B2 - Barrel Room South Box", 'object_id': 223, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Barrel Room North Box", 'object_id': 224, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southwest Room Box", 'object_id': 225, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southeast Room Box", 'object_id': 226, 'type': 'Box', 'access': []}], 'links': [{'target_room': 188, 'entrance': 418, 'teleporter': [58, 8], 'access': []}]}, {'name': 'Mac Ship B1 Mac Room', 'id': 194, 'game_objects': [{'name': "Mac's Ship B1 - Mac Room Chest", 'object_id': 27, 'type': 'Chest', 'access': []}, {'name': 'Captain Mac', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLoaned'], 'access': ['CaptainCap']}], 'links': [{'target_room': 191, 'entrance': 424, 'teleporter': [101, 0], 'access': []}]}, {'name': 'Doom Castle Corridor of Destiny', 'id': 195, 'game_objects': [], 'links': [{'target_room': 201, 'entrance': 428, 'teleporter': [84, 0], 'access': []}, {'target_room': 196, 'entrance': 429, 'teleporter': [35, 2], 'access': []}, {'target_room': 197, 'entrance': 430, 'teleporter': [209, 0], 'access': ['StoneGolem']}, {'target_room': 198, 'entrance': 431, 'teleporter': [211, 0], 'access': ['StoneGolem', 'TwinheadWyvern']}, {'target_room': 199, 'entrance': 432, 'teleporter': [13, 2], 'access': ['StoneGolem', 'TwinheadWyvern', 'Zuh']}]}, {'name': 'Doom Castle Ice Floor', 'id': 196, 'game_objects': [{'name': 'Doom Castle 4F - Northwest Room Box', 'object_id': 231, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Southwest Room Box', 'object_id': 232, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Northeast Room Box', 'object_id': 233, 'type': 'Box', 'access': ['Sword']}, {'name': 'Doom Castle 4F - Southeast Room Box', 'object_id': 234, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Stone Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['StoneGolem'], 'access': ['Sword', 'DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 433, 'teleporter': [109, 3], 'access': []}]}, {'name': 'Doom Castle Lava Floor', 'id': 197, 'game_objects': [{'name': 'Doom Castle 5F - North Left Box', 'object_id': 235, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - North Right Box', 'object_id': 236, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Left Box', 'object_id': 237, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Right Box', 'object_id': 238, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Twinhead Wyvern', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TwinheadWyvern'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 434, 'teleporter': [210, 0], 'access': []}]}, {'name': 'Doom Castle Sky Floor', 'id': 198, 'game_objects': [{'name': 'Doom Castle 6F - West Box', 'object_id': 239, 'type': 'Box', 'access': []}, {'name': 'Doom Castle 6F - East Box', 'object_id': 240, 'type': 'Box', 'access': []}, {'name': 'Zuh', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Zuh'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 435, 'teleporter': [212, 0], 'access': []}, {'target_room': 197, 'access': []}]}, {'name': 'Doom Castle Hero Room', 'id': 199, 'game_objects': [{'name': 'Doom Castle Hero Chest 01', 'object_id': 242, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 02', 'object_id': 243, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 03', 'object_id': 244, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 04', 'object_id': 245, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 200, 'entrance': 436, 'teleporter': [54, 0], 'access': []}, {'target_room': 195, 'entrance': 441, 'teleporter': [110, 3], 'access': []}]}, {'name': 'Doom Castle Dark King Room', 'id': 200, 'game_objects': [], 'links': [{'target_room': 199, 'entrance': 442, 'teleporter': [52, 0], 'access': []}]}] +entrances = [{'name': 'Doom Castle - Sand Floor - To Sky Door - Sand Floor', 'id': 0, 'area': 7, 'coordinates': [24, 19], 'teleporter': [0, 0]}, {'name': 'Doom Castle - Sand Floor - Main Entrance - Sand Floor', 'id': 1, 'area': 7, 'coordinates': [19, 43], 'teleporter': [1, 6]}, {'name': 'Doom Castle - Aero Room - Aero Room Entrance', 'id': 2, 'area': 7, 'coordinates': [27, 39], 'teleporter': [1, 0]}, {'name': 'Focus Tower B1 - Main Loop - South Entrance', 'id': 3, 'area': 8, 'coordinates': [43, 60], 'teleporter': [2, 6]}, {'name': 'Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall', 'id': 4, 'area': 8, 'coordinates': [37, 41], 'teleporter': [4, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room', 'id': 5, 'area': 8, 'coordinates': [59, 35], 'teleporter': [5, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest', 'id': 6, 'area': 8, 'coordinates': [57, 59], 'teleporter': [8, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door', 'id': 7, 'area': 8, 'coordinates': [51, 49], 'teleporter': [6, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor', 'id': 8, 'area': 8, 'coordinates': [51, 45], 'teleporter': [7, 0]}, {'name': 'Focus Tower 1F - Focus Tower West Entrance', 'id': 9, 'area': 9, 'coordinates': [25, 29], 'teleporter': [3, 6]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From SandCoin', 'id': 10, 'area': 9, 'coordinates': [16, 4], 'teleporter': [10, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - Main Hall', 'id': 11, 'area': 9, 'coordinates': [4, 23], 'teleporter': [11, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - To Aero Chest', 'id': 12, 'area': 9, 'coordinates': [26, 17], 'teleporter': [12, 0]}, {'name': 'Focus Tower 1F - Sky Door', 'id': 13, 'area': 9, 'coordinates': [16, 24], 'teleporter': [13, 0]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From RiverCoin', 'id': 14, 'area': 9, 'coordinates': [16, 10], 'teleporter': [14, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - From Sky Door', 'id': 15, 'area': 9, 'coordinates': [16, 29], 'teleporter': [15, 0]}, {'name': 'Focus Tower 2F - Sand Coin Passage - North Entrance', 'id': 16, 'area': 10, 'coordinates': [49, 30], 'teleporter': [4, 6]}, {'name': 'Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin', 'id': 17, 'area': 10, 'coordinates': [47, 33], 'teleporter': [17, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin', 'id': 18, 'area': 10, 'coordinates': [47, 41], 'teleporter': [18, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor', 'id': 19, 'area': 10, 'coordinates': [38, 40], 'teleporter': [20, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor', 'id': 20, 'area': 10, 'coordinates': [56, 40], 'teleporter': [19, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - Pillar Script', 'id': 21, 'area': 10, 'coordinates': [48, 53], 'teleporter': [13, 8]}, {'name': 'Focus Tower 3F - Lower Floor - To Fireburg Entrance', 'id': 22, 'area': 11, 'coordinates': [11, 39], 'teleporter': [6, 6]}, {'name': 'Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar', 'id': 23, 'area': 11, 'coordinates': [6, 47], 'teleporter': [24, 0]}, {'name': 'Focus Tower 3F - Upper Floor - To Aquaria Entrance', 'id': 24, 'area': 11, 'coordinates': [21, 38], 'teleporter': [5, 6]}, {'name': 'Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room', 'id': 25, 'area': 11, 'coordinates': [24, 47], 'teleporter': [23, 0]}, {'name': 'Level Forest - Boulder Script', 'id': 26, 'area': 14, 'coordinates': [52, 15], 'teleporter': [0, 8]}, {'name': 'Level Forest - Rotten Tree Script', 'id': 27, 'area': 14, 'coordinates': [47, 6], 'teleporter': [2, 8]}, {'name': 'Level Forest - Exit Level Forest 1', 'id': 28, 'area': 14, 'coordinates': [46, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 2', 'id': 29, 'area': 14, 'coordinates': [46, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 3', 'id': 30, 'area': 14, 'coordinates': [47, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 4', 'id': 31, 'area': 14, 'coordinates': [47, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 5', 'id': 32, 'area': 14, 'coordinates': [60, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 6', 'id': 33, 'area': 14, 'coordinates': [61, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 7', 'id': 34, 'area': 14, 'coordinates': [46, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 8', 'id': 35, 'area': 14, 'coordinates': [46, 3], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 9', 'id': 36, 'area': 14, 'coordinates': [47, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest A', 'id': 37, 'area': 14, 'coordinates': [47, 3], 'teleporter': [25, 0]}, {'name': 'Foresta - Exit Foresta 1', 'id': 38, 'area': 15, 'coordinates': [10, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 2', 'id': 39, 'area': 15, 'coordinates': [10, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 3', 'id': 40, 'area': 15, 'coordinates': [11, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 4', 'id': 41, 'area': 15, 'coordinates': [11, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Old Man House - Front Door', 'id': 42, 'area': 15, 'coordinates': [25, 17], 'teleporter': [32, 4]}, {'name': 'Foresta - Old Man House - Back Door', 'id': 43, 'area': 15, 'coordinates': [25, 14], 'teleporter': [33, 0]}, {'name': "Foresta - Kaeli's House", 'id': 44, 'area': 15, 'coordinates': [7, 21], 'teleporter': [0, 5]}, {'name': 'Foresta - Rest House', 'id': 45, 'area': 15, 'coordinates': [23, 23], 'teleporter': [1, 5]}, {'name': "Kaeli's House - Kaeli's House Entrance", 'id': 46, 'area': 16, 'coordinates': [11, 20], 'teleporter': [86, 3]}, {'name': "Foresta Houses - Old Man's House - Old Man Front Exit", 'id': 47, 'area': 17, 'coordinates': [35, 44], 'teleporter': [34, 0]}, {'name': "Foresta Houses - Old Man's House - Old Man Back Exit", 'id': 48, 'area': 17, 'coordinates': [35, 27], 'teleporter': [35, 0]}, {'name': 'Foresta - Old Man House - Barrel Tile Script', 'id': 483, 'area': 17, 'coordinates': [35, 30], 'teleporter': [13, 8]}, {'name': 'Foresta Houses - Rest House - Bed Script', 'id': 49, 'area': 17, 'coordinates': [30, 6], 'teleporter': [1, 8]}, {'name': 'Foresta Houses - Rest House - Rest House Exit', 'id': 50, 'area': 17, 'coordinates': [35, 20], 'teleporter': [87, 3]}, {'name': 'Foresta Houses - Libra House - Libra House Script', 'id': 51, 'area': 17, 'coordinates': [8, 49], 'teleporter': [67, 8]}, {'name': 'Foresta Houses - Gemini House - Gemini House Script', 'id': 52, 'area': 17, 'coordinates': [26, 55], 'teleporter': [68, 8]}, {'name': 'Foresta Houses - Mobius House - Mobius House Script', 'id': 53, 'area': 17, 'coordinates': [14, 33], 'teleporter': [69, 8]}, {'name': 'Sand Temple - Sand Temple Entrance', 'id': 54, 'area': 18, 'coordinates': [56, 27], 'teleporter': [36, 0]}, {'name': 'Bone Dungeon 1F - Bone Dungeon Entrance', 'id': 55, 'area': 19, 'coordinates': [13, 60], 'teleporter': [37, 0]}, {'name': 'Bone Dungeon 1F - To Bone Dungeon B1', 'id': 56, 'area': 19, 'coordinates': [13, 39], 'teleporter': [2, 2]}, {'name': 'Bone Dungeon B1 - Waterway - Exit Waterway', 'id': 57, 'area': 20, 'coordinates': [27, 39], 'teleporter': [3, 2]}, {'name': "Bone Dungeon B1 - Waterway - Tristam's Script", 'id': 58, 'area': 20, 'coordinates': [27, 45], 'teleporter': [3, 8]}, {'name': 'Bone Dungeon B1 - Waterway - To Bone Dungeon 1F', 'id': 59, 'area': 20, 'coordinates': [54, 61], 'teleporter': [88, 3]}, {'name': 'Bone Dungeon B1 - Checker Room - Exit Checker Room', 'id': 60, 'area': 20, 'coordinates': [23, 40], 'teleporter': [4, 2]}, {'name': 'Bone Dungeon B1 - Checker Room - To Waterway', 'id': 61, 'area': 20, 'coordinates': [39, 49], 'teleporter': [89, 3]}, {'name': 'Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room', 'id': 62, 'area': 20, 'coordinates': [5, 33], 'teleporter': [91, 3]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage', 'id': 63, 'area': 21, 'coordinates': [19, 13], 'teleporter': [5, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room', 'id': 64, 'area': 21, 'coordinates': [29, 15], 'teleporter': [6, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Checker Room', 'id': 65, 'area': 21, 'coordinates': [8, 25], 'teleporter': [90, 3]}, {'name': 'Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room', 'id': 66, 'area': 21, 'coordinates': [59, 12], 'teleporter': [93, 3]}, {'name': 'Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room', 'id': 67, 'area': 21, 'coordinates': [59, 28], 'teleporter': [94, 3]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Box Room', 'id': 68, 'area': 21, 'coordinates': [53, 7], 'teleporter': [7, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Quake Room', 'id': 69, 'area': 21, 'coordinates': [41, 3], 'teleporter': [8, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Boss Room', 'id': 70, 'area': 21, 'coordinates': [47, 57], 'teleporter': [9, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room', 'id': 71, 'area': 21, 'coordinates': [54, 23], 'teleporter': [92, 3]}, {'name': 'Bone Dungeon B2 - Boss Room - Flamerus Rex Script', 'id': 72, 'area': 22, 'coordinates': [29, 19], 'teleporter': [4, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - Tristam Leave Script', 'id': 73, 'area': 22, 'coordinates': [29, 23], 'teleporter': [75, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room', 'id': 74, 'area': 22, 'coordinates': [30, 27], 'teleporter': [95, 3]}, {'name': 'Libra Temple - Entrance', 'id': 75, 'area': 23, 'coordinates': [10, 15], 'teleporter': [13, 6]}, {'name': 'Libra Temple - Libra Tile Script', 'id': 76, 'area': 23, 'coordinates': [9, 8], 'teleporter': [59, 8]}, {'name': 'Aquaria Winter - Winter Entrance 1', 'id': 77, 'area': 24, 'coordinates': [25, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 2', 'id': 78, 'area': 24, 'coordinates': [25, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 3', 'id': 79, 'area': 24, 'coordinates': [26, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 4', 'id': 80, 'area': 24, 'coordinates': [26, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Winter - Winter Phoebe's House Entrance Script", 'id': 81, 'area': 24, 'coordinates': [8, 19], 'teleporter': [10, 5]}, {'name': 'Aquaria Winter - Winter Vendor House Entrance', 'id': 82, 'area': 24, 'coordinates': [8, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Winter - Winter INN Entrance', 'id': 83, 'area': 24, 'coordinates': [26, 17], 'teleporter': [11, 5]}, {'name': 'Aquaria Summer - Summer Entrance 1', 'id': 84, 'area': 25, 'coordinates': [57, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 2', 'id': 85, 'area': 25, 'coordinates': [57, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 3', 'id': 86, 'area': 25, 'coordinates': [58, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 4', 'id': 87, 'area': 25, 'coordinates': [58, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Summer - Summer Phoebe's House Entrance", 'id': 88, 'area': 25, 'coordinates': [40, 19], 'teleporter': [10, 5]}, {'name': "Aquaria Summer - Spencer's Place Entrance Top", 'id': 89, 'area': 25, 'coordinates': [40, 16], 'teleporter': [42, 0]}, {'name': "Aquaria Summer - Spencer's Place Entrance Side", 'id': 90, 'area': 25, 'coordinates': [41, 18], 'teleporter': [43, 0]}, {'name': 'Aquaria Summer - Summer Vendor House Entrance', 'id': 91, 'area': 25, 'coordinates': [40, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Summer - Summer INN Entrance', 'id': 92, 'area': 25, 'coordinates': [58, 17], 'teleporter': [11, 5]}, {'name': "Phoebe's House - Entrance", 'id': 93, 'area': 26, 'coordinates': [29, 14], 'teleporter': [5, 8]}, {'name': "Aquaria Vendor House - Vendor House Entrance's Script", 'id': 94, 'area': 27, 'coordinates': [7, 10], 'teleporter': [40, 8]}, {'name': 'Aquaria Vendor House - Vendor House Stairs', 'id': 95, 'area': 27, 'coordinates': [1, 4], 'teleporter': [47, 0]}, {'name': 'Aquaria Gemini Room - Gemini Script', 'id': 96, 'area': 27, 'coordinates': [2, 40], 'teleporter': [72, 8]}, {'name': 'Aquaria Gemini Room - Gemini Room Stairs', 'id': 97, 'area': 27, 'coordinates': [4, 39], 'teleporter': [48, 0]}, {'name': 'Aquaria INN - Aquaria INN entrance', 'id': 98, 'area': 27, 'coordinates': [51, 46], 'teleporter': [75, 8]}, {'name': 'Wintry Cave 1F - Main Entrance', 'id': 99, 'area': 28, 'coordinates': [50, 58], 'teleporter': [49, 0]}, {'name': 'Wintry Cave 1F - To 3F Top', 'id': 100, 'area': 28, 'coordinates': [40, 25], 'teleporter': [14, 2]}, {'name': 'Wintry Cave 1F - To 2F', 'id': 101, 'area': 28, 'coordinates': [10, 43], 'teleporter': [15, 2]}, {'name': "Wintry Cave 1F - Phoebe's Script", 'id': 102, 'area': 28, 'coordinates': [44, 37], 'teleporter': [6, 8]}, {'name': 'Wintry Cave 2F - To 3F Bottom', 'id': 103, 'area': 29, 'coordinates': [58, 5], 'teleporter': [50, 0]}, {'name': 'Wintry Cave 2F - To 1F', 'id': 104, 'area': 29, 'coordinates': [38, 18], 'teleporter': [97, 3]}, {'name': 'Wintry Cave 3F Top - Exit from 3F Top', 'id': 105, 'area': 30, 'coordinates': [24, 6], 'teleporter': [96, 3]}, {'name': 'Wintry Cave 3F Bottom - Exit to 2F', 'id': 106, 'area': 31, 'coordinates': [4, 29], 'teleporter': [51, 0]}, {'name': 'Life Temple - Entrance', 'id': 107, 'area': 32, 'coordinates': [9, 60], 'teleporter': [14, 6]}, {'name': 'Life Temple - Libra Tile Script', 'id': 108, 'area': 32, 'coordinates': [3, 55], 'teleporter': [60, 8]}, {'name': 'Life Temple - Mysterious Man Script', 'id': 109, 'area': 32, 'coordinates': [9, 44], 'teleporter': [78, 8]}, {'name': 'Fall Basin - Back Exit Script', 'id': 110, 'area': 33, 'coordinates': [17, 5], 'teleporter': [9, 0]}, {'name': 'Fall Basin - Main Exit', 'id': 111, 'area': 33, 'coordinates': [15, 26], 'teleporter': [53, 0]}, {'name': "Fall Basin - Phoebe's Script", 'id': 112, 'area': 33, 'coordinates': [17, 6], 'teleporter': [9, 8]}, {'name': 'Ice Pyramid B1 Taunt Room - To Climbing Wall Room', 'id': 113, 'area': 34, 'coordinates': [43, 6], 'teleporter': [55, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 1', 'id': 114, 'area': 35, 'coordinates': [18, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 2', 'id': 115, 'area': 35, 'coordinates': [19, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room', 'id': 116, 'area': 35, 'coordinates': [3, 27], 'teleporter': [57, 0]}, {'name': 'Ice Pyramid 1F Maze - West Center Stairs to 2F West Room', 'id': 117, 'area': 35, 'coordinates': [11, 15], 'teleporter': [58, 0]}, {'name': 'Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room', 'id': 118, 'area': 35, 'coordinates': [25, 16], 'teleporter': [59, 0]}, {'name': 'Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room', 'id': 119, 'area': 35, 'coordinates': [31, 1], 'teleporter': [60, 0]}, {'name': 'Ice Pyramid 1F Maze - East Stairs to 2F North Corridor', 'id': 120, 'area': 35, 'coordinates': [34, 9], 'teleporter': [61, 0]}, {'name': "Ice Pyramid 1F Maze - Statue's Script", 'id': 121, 'area': 35, 'coordinates': [21, 32], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 1F', 'id': 122, 'area': 36, 'coordinates': [4, 26], 'teleporter': [62, 0]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room', 'id': 123, 'area': 36, 'coordinates': [22, 17], 'teleporter': [67, 0]}, {'name': 'Ice Pyramid 2F West Room - To 1F', 'id': 124, 'area': 36, 'coordinates': [9, 10], 'teleporter': [63, 0]}, {'name': 'Ice Pyramid 2F Center Room - To 1F', 'id': 125, 'area': 36, 'coordinates': [22, 14], 'teleporter': [64, 0]}, {'name': 'Ice Pyramid 2F Small North Room - To 1F', 'id': 126, 'area': 36, 'coordinates': [26, 4], 'teleporter': [65, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 1F', 'id': 127, 'area': 36, 'coordinates': [32, 8], 'teleporter': [66, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 3F Main Loop', 'id': 128, 'area': 36, 'coordinates': [12, 7], 'teleporter': [68, 0]}, {'name': 'Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room', 'id': 129, 'area': 37, 'coordinates': [24, 54], 'teleporter': [69, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 2F Corridor', 'id': 130, 'area': 37, 'coordinates': [16, 45], 'teleporter': [70, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 4F', 'id': 131, 'area': 37, 'coordinates': [19, 43], 'teleporter': [71, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 3F Main Loop', 'id': 132, 'area': 38, 'coordinates': [52, 5], 'teleporter': [72, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room', 'id': 133, 'area': 38, 'coordinates': [62, 19], 'teleporter': [73, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room', 'id': 134, 'area': 39, 'coordinates': [54, 63], 'teleporter': [74, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate', 'id': 135, 'area': 39, 'coordinates': [47, 54], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room', 'id': 136, 'area': 39, 'coordinates': [39, 43], 'teleporter': [75, 0]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room', 'id': 137, 'area': 39, 'coordinates': [39, 60], 'teleporter': [76, 0]}, {'name': 'Ice Pyramid - Duplicate Ice Golem Room', 'id': 138, 'area': 40, 'coordinates': [44, 43], 'teleporter': [77, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To Taunt Room', 'id': 139, 'area': 41, 'coordinates': [4, 59], 'teleporter': [78, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To 5F Stairs', 'id': 140, 'area': 41, 'coordinates': [4, 45], 'teleporter': [79, 0]}, {'name': 'Ice Pyramid Ice Golem Room - To 5F Stairs', 'id': 141, 'area': 42, 'coordinates': [44, 43], 'teleporter': [80, 0]}, {'name': 'Ice Pyramid Ice Golem Room - Ice Golem Script', 'id': 142, 'area': 42, 'coordinates': [53, 32], 'teleporter': [10, 8]}, {'name': 'Spencer Waterfall - To Spencer Cave', 'id': 143, 'area': 43, 'coordinates': [48, 57], 'teleporter': [81, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 1', 'id': 144, 'area': 43, 'coordinates': [40, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 2', 'id': 145, 'area': 43, 'coordinates': [40, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 3', 'id': 146, 'area': 43, 'coordinates': [41, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 4', 'id': 147, 'area': 43, 'coordinates': [41, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 1', 'id': 148, 'area': 43, 'coordinates': [46, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 2', 'id': 149, 'area': 43, 'coordinates': [47, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Cave Normal Main - To Waterfall', 'id': 150, 'area': 44, 'coordinates': [14, 39], 'teleporter': [85, 0]}, {'name': 'Spencer Cave Normal From Overworld - Exit to Overworld', 'id': 151, 'area': 44, 'coordinates': [15, 57], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Exit to Overworld', 'id': 152, 'area': 45, 'coordinates': [40, 29], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Libra Teleporter Start Script', 'id': 153, 'area': 45, 'coordinates': [28, 21], 'teleporter': [33, 8]}, {'name': 'Spencer Cave Unplug - Libra Teleporter End Script', 'id': 154, 'area': 45, 'coordinates': [46, 4], 'teleporter': [34, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Chest Script', 'id': 155, 'area': 45, 'coordinates': [21, 9], 'teleporter': [35, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Start Script', 'id': 156, 'area': 45, 'coordinates': [29, 28], 'teleporter': [36, 8]}, {'name': 'Wintry Temple Outer Room - Main Entrance', 'id': 157, 'area': 46, 'coordinates': [8, 31], 'teleporter': [15, 6]}, {'name': 'Wintry Temple Inner Room - Gemini Tile to Sealed temple', 'id': 158, 'area': 46, 'coordinates': [9, 24], 'teleporter': [62, 8]}, {'name': 'Fireburg - To Overworld', 'id': 159, 'area': 47, 'coordinates': [4, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 160, 'area': 47, 'coordinates': [5, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 161, 'area': 47, 'coordinates': [28, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 162, 'area': 47, 'coordinates': [27, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - Vendor House', 'id': 163, 'area': 47, 'coordinates': [10, 24], 'teleporter': [91, 0]}, {'name': 'Fireburg - Reuben House', 'id': 164, 'area': 47, 'coordinates': [14, 6], 'teleporter': [98, 8]}, {'name': 'Fireburg - Hotel', 'id': 165, 'area': 47, 'coordinates': [20, 8], 'teleporter': [96, 8]}, {'name': 'Fireburg - GrenadeMan House Script', 'id': 166, 'area': 47, 'coordinates': [12, 18], 'teleporter': [11, 8]}, {'name': 'Reuben House - Main Entrance', 'id': 167, 'area': 48, 'coordinates': [33, 46], 'teleporter': [98, 3]}, {'name': 'GrenadeMan House - Entrance Script', 'id': 168, 'area': 49, 'coordinates': [55, 60], 'teleporter': [9, 8]}, {'name': 'GrenadeMan House - To Mobius Crest Room', 'id': 169, 'area': 49, 'coordinates': [57, 52], 'teleporter': [93, 0]}, {'name': 'GrenadeMan Mobius Room - Stairs to House', 'id': 170, 'area': 49, 'coordinates': [39, 26], 'teleporter': [94, 0]}, {'name': 'GrenadeMan Mobius Room - Mobius Teleporter Script', 'id': 171, 'area': 49, 'coordinates': [39, 23], 'teleporter': [54, 8]}, {'name': 'Fireburg Vendor House - Entrance Script', 'id': 172, 'area': 49, 'coordinates': [7, 10], 'teleporter': [95, 0]}, {'name': 'Fireburg Vendor House - Stairs to Gemini Room', 'id': 173, 'area': 49, 'coordinates': [1, 4], 'teleporter': [96, 0]}, {'name': 'Fireburg Gemini Room - Stairs to Vendor House', 'id': 174, 'area': 49, 'coordinates': [4, 39], 'teleporter': [97, 0]}, {'name': 'Fireburg Gemini Room - Gemini Teleporter Script', 'id': 175, 'area': 49, 'coordinates': [2, 40], 'teleporter': [45, 8]}, {'name': 'Fireburg Hotel Lobby - Stairs to beds', 'id': 176, 'area': 49, 'coordinates': [4, 50], 'teleporter': [213, 0]}, {'name': 'Fireburg Hotel Lobby - Entrance', 'id': 177, 'area': 49, 'coordinates': [17, 56], 'teleporter': [99, 3]}, {'name': 'Fireburg Hotel Beds - Stairs to Hotel Lobby', 'id': 178, 'area': 49, 'coordinates': [45, 59], 'teleporter': [214, 0]}, {'name': 'Mine Exterior - Main Entrance', 'id': 179, 'area': 50, 'coordinates': [5, 28], 'teleporter': [98, 0]}, {'name': 'Mine Exterior - To Cliff', 'id': 180, 'area': 50, 'coordinates': [58, 29], 'teleporter': [99, 0]}, {'name': 'Mine Exterior - To Parallel Room', 'id': 181, 'area': 50, 'coordinates': [8, 7], 'teleporter': [20, 2]}, {'name': 'Mine Exterior - To Crescent Room', 'id': 182, 'area': 50, 'coordinates': [26, 15], 'teleporter': [21, 2]}, {'name': 'Mine Exterior - To Climbing Room', 'id': 183, 'area': 50, 'coordinates': [21, 35], 'teleporter': [22, 2]}, {'name': 'Mine Exterior - Jinn Fight Script', 'id': 184, 'area': 50, 'coordinates': [58, 31], 'teleporter': [74, 8]}, {'name': 'Mine Parallel Room - To Mine Exterior', 'id': 185, 'area': 51, 'coordinates': [7, 60], 'teleporter': [100, 3]}, {'name': 'Mine Crescent Room - To Mine Exterior', 'id': 186, 'area': 51, 'coordinates': [22, 61], 'teleporter': [101, 3]}, {'name': 'Mine Climbing Room - To Mine Exterior', 'id': 187, 'area': 51, 'coordinates': [56, 21], 'teleporter': [102, 3]}, {'name': 'Mine Cliff - Entrance', 'id': 188, 'area': 52, 'coordinates': [9, 5], 'teleporter': [100, 0]}, {'name': 'Mine Cliff - Reuben Grenade Script', 'id': 189, 'area': 52, 'coordinates': [15, 7], 'teleporter': [12, 8]}, {'name': 'Sealed Temple - To Overworld', 'id': 190, 'area': 53, 'coordinates': [58, 43], 'teleporter': [16, 6]}, {'name': 'Sealed Temple - Gemini Tile Script', 'id': 191, 'area': 53, 'coordinates': [56, 38], 'teleporter': [63, 8]}, {'name': 'Volcano Base - Main Entrance 1', 'id': 192, 'area': 54, 'coordinates': [23, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 2', 'id': 193, 'area': 54, 'coordinates': [23, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 3', 'id': 194, 'area': 54, 'coordinates': [24, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 4', 'id': 195, 'area': 54, 'coordinates': [24, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Left Stairs Script', 'id': 196, 'area': 54, 'coordinates': [20, 5], 'teleporter': [31, 8]}, {'name': 'Volcano Base - Right Stairs Script', 'id': 197, 'area': 54, 'coordinates': [32, 5], 'teleporter': [30, 8]}, {'name': 'Volcano Top Right - Top Exit', 'id': 198, 'area': 55, 'coordinates': [44, 8], 'teleporter': [9, 0]}, {'name': 'Volcano Top Left - To Right-Left Path Script', 'id': 199, 'area': 55, 'coordinates': [40, 24], 'teleporter': [26, 8]}, {'name': 'Volcano Top Right - To Left-Right Path Script', 'id': 200, 'area': 55, 'coordinates': [52, 24], 'teleporter': [79, 8]}, {'name': 'Volcano Right Path - To Volcano Base Script', 'id': 201, 'area': 56, 'coordinates': [48, 42], 'teleporter': [15, 8]}, {'name': 'Volcano Left Path - To Volcano Cross Left-Right', 'id': 202, 'area': 56, 'coordinates': [40, 31], 'teleporter': [25, 2]}, {'name': 'Volcano Left Path - To Volcano Cross Right-Left', 'id': 203, 'area': 56, 'coordinates': [52, 29], 'teleporter': [26, 2]}, {'name': 'Volcano Left Path - To Volcano Base Script', 'id': 204, 'area': 56, 'coordinates': [36, 42], 'teleporter': [27, 8]}, {'name': 'Volcano Cross Left-Right - To Volcano Left Path', 'id': 205, 'area': 56, 'coordinates': [10, 42], 'teleporter': [103, 3]}, {'name': 'Volcano Cross Left-Right - To Volcano Top Right Script', 'id': 206, 'area': 56, 'coordinates': [16, 24], 'teleporter': [29, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Top Left Script', 'id': 207, 'area': 56, 'coordinates': [8, 22], 'teleporter': [28, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Left Path', 'id': 208, 'area': 56, 'coordinates': [16, 42], 'teleporter': [104, 3]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 1', 'id': 209, 'area': 57, 'coordinates': [32, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 2', 'id': 210, 'area': 57, 'coordinates': [33, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Three Steps Room', 'id': 211, 'area': 57, 'coordinates': [14, 5], 'teleporter': [105, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Life Chest Room Lower', 'id': 212, 'area': 57, 'coordinates': [40, 17], 'teleporter': [106, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Big Jump Room Left', 'id': 213, 'area': 57, 'coordinates': [8, 11], 'teleporter': [108, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Split Corridor Room', 'id': 214, 'area': 57, 'coordinates': [11, 19], 'teleporter': [111, 0]}, {'name': 'Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher', 'id': 215, 'area': 57, 'coordinates': [32, 11], 'teleporter': [107, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - To Plate Corridor', 'id': 216, 'area': 57, 'coordinates': [12, 23], 'teleporter': [109, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - Plate Script', 'id': 217, 'area': 57, 'coordinates': [5, 23], 'teleporter': [47, 8]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Pointless Room', 'id': 218, 'area': 57, 'coordinates': [0, 9], 'teleporter': [110, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room', 'id': 219, 'area': 57, 'coordinates': [0, 15], 'teleporter': [112, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor', 'id': 220, 'area': 57, 'coordinates': [54, 5], 'teleporter': [113, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II', 'id': 221, 'area': 57, 'coordinates': [54, 21], 'teleporter': [114, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1', 'id': 222, 'area': 57, 'coordinates': [62, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2', 'id': 223, 'area': 57, 'coordinates': [63, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3', 'id': 224, 'area': 57, 'coordinates': [62, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4', 'id': 225, 'area': 57, 'coordinates': [63, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor', 'id': 226, 'area': 57, 'coordinates': [50, 25], 'teleporter': [115, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Right Entrance', 'id': 227, 'area': 58, 'coordinates': [55, 28], 'teleporter': [116, 0]}, {'name': 'Lava Dome Jump Maze II - Upper Entrance', 'id': 228, 'area': 58, 'coordinates': [35, 3], 'teleporter': [119, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Left Entrance', 'id': 229, 'area': 58, 'coordinates': [34, 27], 'teleporter': [120, 0]}, {'name': 'Lava Dome Up-Down Corridor - Upper Entrance', 'id': 230, 'area': 58, 'coordinates': [29, 8], 'teleporter': [117, 0]}, {'name': 'Lava Dome Up-Down Corridor - Lower Entrance', 'id': 231, 'area': 58, 'coordinates': [28, 25], 'teleporter': [118, 0]}, {'name': 'Lava Dome Jump Maze I - South Entrance', 'id': 232, 'area': 59, 'coordinates': [20, 27], 'teleporter': [121, 0]}, {'name': 'Lava Dome Jump Maze I - North Entrance', 'id': 233, 'area': 59, 'coordinates': [7, 3], 'teleporter': [122, 0]}, {'name': 'Lava Dome Pointless Room - Entrance', 'id': 234, 'area': 60, 'coordinates': [2, 7], 'teleporter': [123, 0]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 1', 'id': 490, 'area': 60, 'coordinates': [4, 4], 'teleporter': [99, 8]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 2', 'id': 491, 'area': 60, 'coordinates': [4, 5], 'teleporter': [99, 8]}, {'name': 'Lava Dome Lower Moon Helm Room - Left Entrance', 'id': 235, 'area': 60, 'coordinates': [2, 19], 'teleporter': [124, 0]}, {'name': 'Lava Dome Lower Moon Helm Room - Right Entrance', 'id': 236, 'area': 60, 'coordinates': [11, 21], 'teleporter': [125, 0]}, {'name': 'Lava Dome Moon Helm Room - Entrance', 'id': 237, 'area': 60, 'coordinates': [15, 23], 'teleporter': [126, 0]}, {'name': 'Lava Dome Three Jumps Room - To Main Loop', 'id': 238, 'area': 61, 'coordinates': [58, 15], 'teleporter': [127, 0]}, {'name': 'Lava Dome Life Chest Room - Lower South Entrance', 'id': 239, 'area': 61, 'coordinates': [38, 27], 'teleporter': [128, 0]}, {'name': 'Lava Dome Life Chest Room - Upper South Entrance', 'id': 240, 'area': 61, 'coordinates': [28, 23], 'teleporter': [129, 0]}, {'name': 'Lava Dome Big Jump Room - Left Entrance', 'id': 241, 'area': 62, 'coordinates': [42, 51], 'teleporter': [133, 0]}, {'name': 'Lava Dome Big Jump Room - North Entrance', 'id': 242, 'area': 62, 'coordinates': [30, 29], 'teleporter': [131, 0]}, {'name': 'Lava Dome Big Jump Room - Lower Right Stairs', 'id': 243, 'area': 62, 'coordinates': [61, 59], 'teleporter': [132, 0]}, {'name': 'Lava Dome Split Corridor - Upper Stairs', 'id': 244, 'area': 62, 'coordinates': [30, 43], 'teleporter': [130, 0]}, {'name': 'Lava Dome Split Corridor - Lower Stairs', 'id': 245, 'area': 62, 'coordinates': [36, 61], 'teleporter': [134, 0]}, {'name': 'Lava Dome Plate Corridor - Right Entrance', 'id': 246, 'area': 63, 'coordinates': [19, 29], 'teleporter': [135, 0]}, {'name': 'Lava Dome Plate Corridor - Left Entrance', 'id': 247, 'area': 63, 'coordinates': [60, 21], 'teleporter': [137, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Upper Entrance', 'id': 248, 'area': 63, 'coordinates': [22, 3], 'teleporter': [136, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Lower Entrance', 'id': 249, 'area': 63, 'coordinates': [22, 17], 'teleporter': [16, 0]}, {'name': 'Lava Dome Hydra Room - South Entrance', 'id': 250, 'area': 64, 'coordinates': [14, 59], 'teleporter': [105, 3]}, {'name': 'Lava Dome Hydra Room - North Exit', 'id': 251, 'area': 64, 'coordinates': [25, 31], 'teleporter': [138, 0]}, {'name': 'Lava Dome Hydra Room - Hydra Script', 'id': 252, 'area': 64, 'coordinates': [14, 36], 'teleporter': [14, 8]}, {'name': 'Lava Dome Escape Corridor - South Entrance', 'id': 253, 'area': 65, 'coordinates': [22, 17], 'teleporter': [139, 0]}, {'name': 'Lava Dome Escape Corridor - North Entrance', 'id': 254, 'area': 65, 'coordinates': [22, 3], 'teleporter': [9, 0]}, {'name': 'Rope Bridge - West Entrance 1', 'id': 255, 'area': 66, 'coordinates': [3, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 2', 'id': 256, 'area': 66, 'coordinates': [3, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 3', 'id': 257, 'area': 66, 'coordinates': [3, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 4', 'id': 258, 'area': 66, 'coordinates': [3, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 5', 'id': 259, 'area': 66, 'coordinates': [4, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 6', 'id': 260, 'area': 66, 'coordinates': [4, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 7', 'id': 261, 'area': 66, 'coordinates': [4, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 8', 'id': 262, 'area': 66, 'coordinates': [4, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 1', 'id': 263, 'area': 66, 'coordinates': [59, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 2', 'id': 264, 'area': 66, 'coordinates': [59, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 3', 'id': 265, 'area': 66, 'coordinates': [59, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 4', 'id': 266, 'area': 66, 'coordinates': [59, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 5', 'id': 267, 'area': 66, 'coordinates': [60, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 6', 'id': 268, 'area': 66, 'coordinates': [60, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 7', 'id': 269, 'area': 66, 'coordinates': [60, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 8', 'id': 270, 'area': 66, 'coordinates': [60, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - Reuben Fall Script', 'id': 271, 'area': 66, 'coordinates': [13, 12], 'teleporter': [15, 8]}, {'name': 'Alive Forest - West Entrance 1', 'id': 272, 'area': 67, 'coordinates': [8, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - West Entrance 2', 'id': 273, 'area': 67, 'coordinates': [9, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - Giant Tree Entrance', 'id': 274, 'area': 67, 'coordinates': [42, 42], 'teleporter': [143, 0]}, {'name': 'Alive Forest - Libra Teleporter Script', 'id': 275, 'area': 67, 'coordinates': [8, 52], 'teleporter': [64, 8]}, {'name': 'Alive Forest - Gemini Teleporter Script', 'id': 276, 'area': 67, 'coordinates': [57, 49], 'teleporter': [65, 8]}, {'name': 'Alive Forest - Mobius Teleporter Script', 'id': 277, 'area': 67, 'coordinates': [24, 10], 'teleporter': [66, 8]}, {'name': 'Giant Tree 1F - Entrance Script 1', 'id': 278, 'area': 68, 'coordinates': [18, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - Entrance Script 2', 'id': 279, 'area': 68, 'coordinates': [19, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - North Entrance To 2F', 'id': 280, 'area': 68, 'coordinates': [16, 1], 'teleporter': [144, 0]}, {'name': 'Giant Tree 2F Main Lobby - North Entrance to 1F', 'id': 281, 'area': 69, 'coordinates': [44, 33], 'teleporter': [145, 0]}, {'name': 'Giant Tree 2F Main Lobby - Central Entrance to 3F', 'id': 282, 'area': 69, 'coordinates': [42, 47], 'teleporter': [146, 0]}, {'name': 'Giant Tree 2F Main Lobby - West Entrance to Mushroom Room', 'id': 283, 'area': 69, 'coordinates': [58, 49], 'teleporter': [149, 0]}, {'name': 'Giant Tree 2F West Ledge - To 3F Northwest Ledge', 'id': 284, 'area': 69, 'coordinates': [34, 37], 'teleporter': [147, 0]}, {'name': 'Giant Tree 2F Fall From Vine Script', 'id': 482, 'area': 69, 'coordinates': [46, 51], 'teleporter': [76, 8]}, {'name': 'Giant Tree Meteor Chest Room - To 2F Mushroom Room', 'id': 285, 'area': 69, 'coordinates': [58, 44], 'teleporter': [148, 0]}, {'name': 'Giant Tree 2F Mushroom Room - Entrance', 'id': 286, 'area': 70, 'coordinates': [55, 18], 'teleporter': [150, 0]}, {'name': 'Giant Tree 2F Mushroom Room - North Face to Meteor', 'id': 287, 'area': 70, 'coordinates': [56, 7], 'teleporter': [151, 0]}, {'name': 'Giant Tree 3F Central Room - Central Entrance to 2F', 'id': 288, 'area': 71, 'coordinates': [46, 53], 'teleporter': [152, 0]}, {'name': 'Giant Tree 3F Central Room - East Entrance to Worm Room', 'id': 289, 'area': 71, 'coordinates': [58, 39], 'teleporter': [153, 0]}, {'name': 'Giant Tree 3F Lower Corridor - Entrance from Worm Room', 'id': 290, 'area': 71, 'coordinates': [45, 39], 'teleporter': [154, 0]}, {'name': 'Giant Tree 3F West Platform - Lower Entrance', 'id': 291, 'area': 71, 'coordinates': [33, 43], 'teleporter': [155, 0]}, {'name': 'Giant Tree 3F West Platform - Top Entrance', 'id': 292, 'area': 71, 'coordinates': [52, 25], 'teleporter': [156, 0]}, {'name': 'Giant Tree Worm Room - East Entrance', 'id': 293, 'area': 72, 'coordinates': [20, 58], 'teleporter': [157, 0]}, {'name': 'Giant Tree Worm Room - West Entrance', 'id': 294, 'area': 72, 'coordinates': [6, 56], 'teleporter': [158, 0]}, {'name': 'Giant Tree 4F Lower Floor - Entrance', 'id': 295, 'area': 73, 'coordinates': [20, 7], 'teleporter': [159, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower West Mouth', 'id': 296, 'area': 73, 'coordinates': [8, 23], 'teleporter': [160, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower Central Mouth', 'id': 297, 'area': 73, 'coordinates': [14, 25], 'teleporter': [161, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower East Mouth', 'id': 298, 'area': 73, 'coordinates': [20, 25], 'teleporter': [162, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper West Mouth', 'id': 299, 'area': 73, 'coordinates': [8, 19], 'teleporter': [163, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper Central Mouth', 'id': 300, 'area': 73, 'coordinates': [12, 17], 'teleporter': [164, 0]}, {'name': 'Giant Tree 4F Slime Room - Exit', 'id': 301, 'area': 74, 'coordinates': [47, 10], 'teleporter': [165, 0]}, {'name': 'Giant Tree 4F Slime Room - West Entrance', 'id': 302, 'area': 74, 'coordinates': [45, 24], 'teleporter': [166, 0]}, {'name': 'Giant Tree 4F Slime Room - Central Entrance', 'id': 303, 'area': 74, 'coordinates': [50, 24], 'teleporter': [167, 0]}, {'name': 'Giant Tree 4F Slime Room - East Entrance', 'id': 304, 'area': 74, 'coordinates': [57, 28], 'teleporter': [168, 0]}, {'name': 'Giant Tree 5F - Entrance', 'id': 305, 'area': 75, 'coordinates': [14, 51], 'teleporter': [169, 0]}, {'name': 'Giant Tree 5F - Giant Tree Face', 'id': 306, 'area': 75, 'coordinates': [14, 37], 'teleporter': [170, 0]}, {'name': 'Kaidge Temple - Entrance', 'id': 307, 'area': 77, 'coordinates': [44, 63], 'teleporter': [18, 6]}, {'name': 'Kaidge Temple - Mobius Teleporter Script', 'id': 308, 'area': 77, 'coordinates': [35, 57], 'teleporter': [71, 8]}, {'name': 'Windhole Temple - Entrance', 'id': 309, 'area': 78, 'coordinates': [10, 29], 'teleporter': [173, 0]}, {'name': 'Mount Gale - Entrance 1', 'id': 310, 'area': 79, 'coordinates': [1, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Entrance 2', 'id': 311, 'area': 79, 'coordinates': [2, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Visit Quest', 'id': 494, 'area': 79, 'coordinates': [44, 7], 'teleporter': [101, 8]}, {'name': 'Windia - Main Entrance 1', 'id': 312, 'area': 80, 'coordinates': [12, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 2', 'id': 313, 'area': 80, 'coordinates': [13, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 3', 'id': 314, 'area': 80, 'coordinates': [14, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 4', 'id': 315, 'area': 80, 'coordinates': [15, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 5', 'id': 316, 'area': 80, 'coordinates': [12, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 6', 'id': 317, 'area': 80, 'coordinates': [13, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 7', 'id': 318, 'area': 80, 'coordinates': [14, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 8', 'id': 319, 'area': 80, 'coordinates': [15, 41], 'teleporter': [10, 6]}, {'name': "Windia - Otto's House", 'id': 320, 'area': 80, 'coordinates': [21, 39], 'teleporter': [30, 5]}, {'name': "Windia - INN's Script", 'id': 321, 'area': 80, 'coordinates': [18, 34], 'teleporter': [97, 8]}, {'name': 'Windia - Vendor House', 'id': 322, 'area': 80, 'coordinates': [8, 36], 'teleporter': [32, 5]}, {'name': 'Windia - Kid House', 'id': 323, 'area': 80, 'coordinates': [7, 23], 'teleporter': [176, 4]}, {'name': 'Windia - Old People House', 'id': 324, 'area': 80, 'coordinates': [19, 21], 'teleporter': [177, 4]}, {'name': 'Windia - Rainbow Bridge Script', 'id': 325, 'area': 80, 'coordinates': [21, 9], 'teleporter': [10, 6]}, {'name': "Otto's House - Attic Stairs", 'id': 326, 'area': 81, 'coordinates': [2, 19], 'teleporter': [33, 2]}, {'name': "Otto's House - Entrance", 'id': 327, 'area': 81, 'coordinates': [9, 30], 'teleporter': [106, 3]}, {'name': "Otto's Attic - Stairs", 'id': 328, 'area': 81, 'coordinates': [26, 23], 'teleporter': [107, 3]}, {'name': 'Windia Kid House - Entrance Script', 'id': 329, 'area': 82, 'coordinates': [7, 10], 'teleporter': [178, 0]}, {'name': 'Windia Kid House - Basement Stairs', 'id': 330, 'area': 82, 'coordinates': [1, 4], 'teleporter': [180, 0]}, {'name': 'Windia Old People House - Entrance', 'id': 331, 'area': 82, 'coordinates': [55, 12], 'teleporter': [179, 0]}, {'name': 'Windia Old People House - Basement Stairs', 'id': 332, 'area': 82, 'coordinates': [60, 5], 'teleporter': [181, 0]}, {'name': 'Windia Kid House Basement - Stairs', 'id': 333, 'area': 82, 'coordinates': [43, 8], 'teleporter': [182, 0]}, {'name': 'Windia Kid House Basement - Mobius Teleporter', 'id': 334, 'area': 82, 'coordinates': [41, 9], 'teleporter': [44, 8]}, {'name': 'Windia Old People House Basement - Stairs', 'id': 335, 'area': 82, 'coordinates': [39, 26], 'teleporter': [183, 0]}, {'name': 'Windia Old People House Basement - Mobius Teleporter Script', 'id': 336, 'area': 82, 'coordinates': [39, 23], 'teleporter': [43, 8]}, {'name': 'Windia Inn Lobby - Stairs to Beds', 'id': 337, 'area': 82, 'coordinates': [45, 24], 'teleporter': [102, 8]}, {'name': 'Windia Inn Lobby - Exit', 'id': 338, 'area': 82, 'coordinates': [53, 30], 'teleporter': [135, 3]}, {'name': 'Windia Inn Beds - Stairs to Lobby', 'id': 339, 'area': 82, 'coordinates': [33, 59], 'teleporter': [216, 0]}, {'name': 'Windia Vendor House - Entrance', 'id': 340, 'area': 82, 'coordinates': [29, 14], 'teleporter': [108, 3]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 1', 'id': 341, 'area': 83, 'coordinates': [47, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 2', 'id': 342, 'area': 83, 'coordinates': [47, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 3', 'id': 343, 'area': 83, 'coordinates': [48, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 4', 'id': 344, 'area': 83, 'coordinates': [48, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - East Entrance', 'id': 345, 'area': 83, 'coordinates': [55, 12], 'teleporter': [185, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - South Stairs', 'id': 346, 'area': 83, 'coordinates': [51, 25], 'teleporter': [186, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 1', 'id': 347, 'area': 83, 'coordinates': [47, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 2', 'id': 348, 'area': 83, 'coordinates': [48, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Boxes Room - West Stairs', 'id': 349, 'area': 83, 'coordinates': [38, 17], 'teleporter': [187, 0]}, {'name': 'Pazuzu 2F - West Upper Stairs', 'id': 350, 'area': 84, 'coordinates': [7, 11], 'teleporter': [188, 0]}, {'name': 'Pazuzu 2F - South Stairs', 'id': 351, 'area': 84, 'coordinates': [20, 24], 'teleporter': [189, 0]}, {'name': 'Pazuzu 2F - West Lower Stairs', 'id': 352, 'area': 84, 'coordinates': [6, 17], 'teleporter': [190, 0]}, {'name': 'Pazuzu 2F - Central Stairs', 'id': 353, 'area': 84, 'coordinates': [15, 15], 'teleporter': [191, 0]}, {'name': 'Pazuzu 2F - Pazuzu 1', 'id': 354, 'area': 84, 'coordinates': [15, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 2F - Pazuzu 2', 'id': 355, 'area': 84, 'coordinates': [16, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 3F Main Room - North Stairs', 'id': 356, 'area': 85, 'coordinates': [23, 11], 'teleporter': [192, 0]}, {'name': 'Pazuzu 3F Main Room - West Stairs', 'id': 357, 'area': 85, 'coordinates': [7, 15], 'teleporter': [193, 0]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 1', 'id': 358, 'area': 85, 'coordinates': [15, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 2', 'id': 359, 'area': 85, 'coordinates': [16, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Central Island - Central Stairs', 'id': 360, 'area': 85, 'coordinates': [15, 14], 'teleporter': [194, 0]}, {'name': 'Pazuzu 3F Central Island - South Stairs', 'id': 361, 'area': 85, 'coordinates': [17, 25], 'teleporter': [195, 0]}, {'name': 'Pazuzu 4F - Northwest Stairs', 'id': 362, 'area': 86, 'coordinates': [39, 12], 'teleporter': [196, 0]}, {'name': 'Pazuzu 4F - Southwest Stairs', 'id': 363, 'area': 86, 'coordinates': [39, 19], 'teleporter': [197, 0]}, {'name': 'Pazuzu 4F - South Stairs', 'id': 364, 'area': 86, 'coordinates': [47, 24], 'teleporter': [198, 0]}, {'name': 'Pazuzu 4F - Northeast Stairs', 'id': 365, 'area': 86, 'coordinates': [54, 9], 'teleporter': [199, 0]}, {'name': 'Pazuzu 4F - Pazuzu Script 1', 'id': 366, 'area': 86, 'coordinates': [47, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 4F - Pazuzu Script 2', 'id': 367, 'area': 86, 'coordinates': [48, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 5F Pazuzu Loop - West Stairs', 'id': 368, 'area': 87, 'coordinates': [9, 49], 'teleporter': [200, 0]}, {'name': 'Pazuzu 5F Pazuzu Loop - South Stairs', 'id': 369, 'area': 87, 'coordinates': [16, 55], 'teleporter': [201, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northeast Stairs', 'id': 370, 'area': 87, 'coordinates': [22, 40], 'teleporter': [202, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northwest Stairs', 'id': 371, 'area': 87, 'coordinates': [9, 40], 'teleporter': [203, 0]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 1', 'id': 372, 'area': 87, 'coordinates': [15, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 2', 'id': 373, 'area': 87, 'coordinates': [16, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 6F - West Stairs', 'id': 374, 'area': 88, 'coordinates': [41, 47], 'teleporter': [204, 0]}, {'name': 'Pazuzu 6F - Northwest Stairs', 'id': 375, 'area': 88, 'coordinates': [41, 40], 'teleporter': [205, 0]}, {'name': 'Pazuzu 6F - Northeast Stairs', 'id': 376, 'area': 88, 'coordinates': [54, 40], 'teleporter': [206, 0]}, {'name': 'Pazuzu 6F - South Stairs', 'id': 377, 'area': 88, 'coordinates': [52, 56], 'teleporter': [207, 0]}, {'name': 'Pazuzu 6F - Pazuzu Script 1', 'id': 378, 'area': 88, 'coordinates': [47, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 6F - Pazuzu Script 2', 'id': 379, 'area': 88, 'coordinates': [48, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 7F Main Room - Southwest Stairs', 'id': 380, 'area': 89, 'coordinates': [15, 54], 'teleporter': [26, 0]}, {'name': 'Pazuzu 7F Main Room - Northeast Stairs', 'id': 381, 'area': 89, 'coordinates': [21, 40], 'teleporter': [27, 0]}, {'name': 'Pazuzu 7F Main Room - Southeast Stairs', 'id': 382, 'area': 89, 'coordinates': [21, 56], 'teleporter': [28, 0]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 1', 'id': 383, 'area': 89, 'coordinates': [15, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 2', 'id': 384, 'area': 89, 'coordinates': [16, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Crystal Script', 'id': 480, 'area': 89, 'coordinates': [15, 40], 'teleporter': [38, 8]}, {'name': 'Pazuzu 1F to 3F - South Stairs', 'id': 385, 'area': 90, 'coordinates': [43, 60], 'teleporter': [29, 0]}, {'name': 'Pazuzu 1F to 3F - North Stairs', 'id': 386, 'area': 90, 'coordinates': [43, 36], 'teleporter': [30, 0]}, {'name': 'Pazuzu 3F to 5F - South Stairs', 'id': 387, 'area': 91, 'coordinates': [43, 60], 'teleporter': [40, 0]}, {'name': 'Pazuzu 3F to 5F - North Stairs', 'id': 388, 'area': 91, 'coordinates': [43, 36], 'teleporter': [41, 0]}, {'name': 'Pazuzu 5F to 7F - South Stairs', 'id': 389, 'area': 92, 'coordinates': [43, 60], 'teleporter': [38, 0]}, {'name': 'Pazuzu 5F to 7F - North Stairs', 'id': 390, 'area': 92, 'coordinates': [43, 36], 'teleporter': [39, 0]}, {'name': 'Pazuzu 2F to 4F - South Stairs', 'id': 391, 'area': 93, 'coordinates': [43, 60], 'teleporter': [21, 0]}, {'name': 'Pazuzu 2F to 4F - North Stairs', 'id': 392, 'area': 93, 'coordinates': [43, 36], 'teleporter': [22, 0]}, {'name': 'Pazuzu 4F to 6F - South Stairs', 'id': 393, 'area': 94, 'coordinates': [43, 60], 'teleporter': [2, 0]}, {'name': 'Pazuzu 4F to 6F - North Stairs', 'id': 394, 'area': 94, 'coordinates': [43, 36], 'teleporter': [3, 0]}, {'name': 'Light Temple - Entrance', 'id': 395, 'area': 95, 'coordinates': [28, 57], 'teleporter': [19, 6]}, {'name': 'Light Temple - Mobius Teleporter Script', 'id': 396, 'area': 95, 'coordinates': [29, 37], 'teleporter': [70, 8]}, {'name': 'Light Temple - Visit Quest Script 1', 'id': 492, 'area': 95, 'coordinates': [34, 39], 'teleporter': [100, 8]}, {'name': 'Light Temple - Visit Quest Script 2', 'id': 493, 'area': 95, 'coordinates': [35, 39], 'teleporter': [100, 8]}, {'name': 'Ship Dock - Mobius Teleporter Script', 'id': 397, 'area': 96, 'coordinates': [15, 18], 'teleporter': [61, 8]}, {'name': 'Ship Dock - From Overworld', 'id': 398, 'area': 96, 'coordinates': [15, 11], 'teleporter': [73, 0]}, {'name': 'Ship Dock - Entrance', 'id': 399, 'area': 96, 'coordinates': [15, 23], 'teleporter': [17, 6]}, {'name': 'Mac Ship Deck - East Entrance Script', 'id': 400, 'area': 97, 'coordinates': [26, 40], 'teleporter': [37, 8]}, {'name': 'Mac Ship Deck - Central Stairs Script', 'id': 401, 'area': 97, 'coordinates': [16, 47], 'teleporter': [50, 8]}, {'name': 'Mac Ship Deck - West Stairs Script', 'id': 402, 'area': 97, 'coordinates': [8, 34], 'teleporter': [51, 8]}, {'name': 'Mac Ship Deck - East Stairs Script', 'id': 403, 'area': 97, 'coordinates': [24, 36], 'teleporter': [52, 8]}, {'name': 'Mac Ship Deck - North Stairs Script', 'id': 404, 'area': 97, 'coordinates': [12, 9], 'teleporter': [53, 8]}, {'name': 'Mac Ship B1 Outer Ring - South Stairs', 'id': 405, 'area': 98, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring - West Stairs', 'id': 406, 'area': 98, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring - East Stairs', 'id': 407, 'area': 98, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Outer Ring - Northwest Stairs', 'id': 408, 'area': 98, 'coordinates': [10, 23], 'teleporter': [88, 0]}, {'name': 'Mac Ship B1 Square Room - North Stairs', 'id': 409, 'area': 98, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room - South Stairs', 'id': 410, 'area': 98, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room - Stairs', 'id': 411, 'area': 98, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor - South Stairs', 'id': 412, 'area': 98, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor - North Stairs', 'id': 413, 'area': 98, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B2 South Corridor - South Stairs', 'id': 414, 'area': 99, 'coordinates': [48, 51], 'teleporter': [57, 1]}, {'name': 'Mac Ship B2 South Corridor - North Stairs Script', 'id': 415, 'area': 99, 'coordinates': [48, 38], 'teleporter': [55, 8]}, {'name': 'Mac Ship B2 North Corridor - South Stairs Script', 'id': 416, 'area': 99, 'coordinates': [48, 27], 'teleporter': [56, 8]}, {'name': 'Mac Ship B2 North Corridor - North Stairs Script', 'id': 417, 'area': 99, 'coordinates': [48, 12], 'teleporter': [57, 8]}, {'name': 'Mac Ship B2 Outer Ring - Northwest Stairs Script', 'id': 418, 'area': 99, 'coordinates': [55, 11], 'teleporter': [58, 8]}, {'name': 'Mac Ship B1 Outer Ring Cleared - South Stairs', 'id': 419, 'area': 100, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - West Stairs', 'id': 420, 'area': 100, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - East Stairs', 'id': 421, 'area': 100, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - North Stairs', 'id': 422, 'area': 100, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - South Stairs', 'id': 423, 'area': 100, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room Cleared - Main Stairs', 'id': 424, 'area': 100, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - South Stairs', 'id': 425, 'area': 100, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - North Stairs', 'id': 426, 'area': 100, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - Northwest Stairs', 'id': 427, 'area': 100, 'coordinates': [23, 10], 'teleporter': [88, 0]}, {'name': 'Doom Castle Corridor of Destiny - South Entrance', 'id': 428, 'area': 101, 'coordinates': [59, 29], 'teleporter': [84, 0]}, {'name': 'Doom Castle Corridor of Destiny - Ice Floor Entrance', 'id': 429, 'area': 101, 'coordinates': [59, 21], 'teleporter': [35, 2]}, {'name': 'Doom Castle Corridor of Destiny - Lava Floor Entrance', 'id': 430, 'area': 101, 'coordinates': [59, 13], 'teleporter': [209, 0]}, {'name': 'Doom Castle Corridor of Destiny - Sky Floor Entrance', 'id': 431, 'area': 101, 'coordinates': [59, 5], 'teleporter': [211, 0]}, {'name': 'Doom Castle Corridor of Destiny - Hero Room Entrance', 'id': 432, 'area': 101, 'coordinates': [59, 61], 'teleporter': [13, 2]}, {'name': 'Doom Castle Ice Floor - Entrance', 'id': 433, 'area': 102, 'coordinates': [23, 42], 'teleporter': [109, 3]}, {'name': 'Doom Castle Lava Floor - Entrance', 'id': 434, 'area': 103, 'coordinates': [23, 40], 'teleporter': [210, 0]}, {'name': 'Doom Castle Sky Floor - Entrance', 'id': 435, 'area': 104, 'coordinates': [24, 41], 'teleporter': [212, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 1', 'id': 436, 'area': 106, 'coordinates': [15, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 2', 'id': 437, 'area': 106, 'coordinates': [16, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 3', 'id': 438, 'area': 106, 'coordinates': [15, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 4', 'id': 439, 'area': 106, 'coordinates': [16, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Hero Statue Script', 'id': 440, 'area': 106, 'coordinates': [15, 17], 'teleporter': [24, 8]}, {'name': 'Doom Castle Hero Room - Entrance', 'id': 441, 'area': 106, 'coordinates': [15, 24], 'teleporter': [110, 3]}, {'name': 'Doom Castle Dark King Room - Entrance', 'id': 442, 'area': 107, 'coordinates': [14, 26], 'teleporter': [52, 0]}, {'name': 'Doom Castle Dark King Room - Dark King Script', 'id': 443, 'area': 107, 'coordinates': [14, 15], 'teleporter': [25, 8]}, {'name': 'Doom Castle Dark King Room - Unknown', 'id': 444, 'area': 107, 'coordinates': [47, 54], 'teleporter': [77, 0]}, {'name': 'Overworld - Level Forest', 'id': 445, 'area': 0, 'type': 'Overworld', 'teleporter': [46, 8]}, {'name': 'Overworld - Foresta', 'id': 446, 'area': 0, 'type': 'Overworld', 'teleporter': [2, 1]}, {'name': 'Overworld - Sand Temple', 'id': 447, 'area': 0, 'type': 'Overworld', 'teleporter': [3, 1]}, {'name': 'Overworld - Bone Dungeon', 'id': 448, 'area': 0, 'type': 'Overworld', 'teleporter': [4, 1]}, {'name': 'Overworld - Focus Tower Foresta', 'id': 449, 'area': 0, 'type': 'Overworld', 'teleporter': [5, 1]}, {'name': 'Overworld - Focus Tower Aquaria', 'id': 450, 'area': 0, 'type': 'Overworld', 'teleporter': [19, 1]}, {'name': 'Overworld - Libra Temple', 'id': 451, 'area': 0, 'type': 'Overworld', 'teleporter': [7, 1]}, {'name': 'Overworld - Aquaria', 'id': 452, 'area': 0, 'type': 'Overworld', 'teleporter': [8, 8]}, {'name': 'Overworld - Wintry Cave', 'id': 453, 'area': 0, 'type': 'Overworld', 'teleporter': [10, 1]}, {'name': 'Overworld - Life Temple', 'id': 454, 'area': 0, 'type': 'Overworld', 'teleporter': [11, 1]}, {'name': 'Overworld - Falls Basin', 'id': 455, 'area': 0, 'type': 'Overworld', 'teleporter': [12, 1]}, {'name': 'Overworld - Ice Pyramid', 'id': 456, 'area': 0, 'type': 'Overworld', 'teleporter': [13, 1]}, {'name': "Overworld - Spencer's Place", 'id': 457, 'area': 0, 'type': 'Overworld', 'teleporter': [48, 8]}, {'name': 'Overworld - Wintry Temple', 'id': 458, 'area': 0, 'type': 'Overworld', 'teleporter': [16, 1]}, {'name': 'Overworld - Focus Tower Frozen Strip', 'id': 459, 'area': 0, 'type': 'Overworld', 'teleporter': [17, 1]}, {'name': 'Overworld - Focus Tower Fireburg', 'id': 460, 'area': 0, 'type': 'Overworld', 'teleporter': [18, 1]}, {'name': 'Overworld - Fireburg', 'id': 461, 'area': 0, 'type': 'Overworld', 'teleporter': [20, 1]}, {'name': 'Overworld - Mine', 'id': 462, 'area': 0, 'type': 'Overworld', 'teleporter': [21, 1]}, {'name': 'Overworld - Sealed Temple', 'id': 463, 'area': 0, 'type': 'Overworld', 'teleporter': [22, 1]}, {'name': 'Overworld - Volcano', 'id': 464, 'area': 0, 'type': 'Overworld', 'teleporter': [23, 1]}, {'name': 'Overworld - Lava Dome', 'id': 465, 'area': 0, 'type': 'Overworld', 'teleporter': [24, 1]}, {'name': 'Overworld - Focus Tower Windia', 'id': 466, 'area': 0, 'type': 'Overworld', 'teleporter': [6, 1]}, {'name': 'Overworld - Rope Bridge', 'id': 467, 'area': 0, 'type': 'Overworld', 'teleporter': [25, 1]}, {'name': 'Overworld - Alive Forest', 'id': 468, 'area': 0, 'type': 'Overworld', 'teleporter': [26, 1]}, {'name': 'Overworld - Giant Tree', 'id': 469, 'area': 0, 'type': 'Overworld', 'teleporter': [27, 1]}, {'name': 'Overworld - Kaidge Temple', 'id': 470, 'area': 0, 'type': 'Overworld', 'teleporter': [28, 1]}, {'name': 'Overworld - Windia', 'id': 471, 'area': 0, 'type': 'Overworld', 'teleporter': [29, 1]}, {'name': 'Overworld - Windhole Temple', 'id': 472, 'area': 0, 'type': 'Overworld', 'teleporter': [30, 1]}, {'name': 'Overworld - Mount Gale', 'id': 473, 'area': 0, 'type': 'Overworld', 'teleporter': [31, 1]}, {'name': 'Overworld - Pazuzu Tower', 'id': 474, 'area': 0, 'type': 'Overworld', 'teleporter': [32, 1]}, {'name': 'Overworld - Ship Dock', 'id': 475, 'area': 0, 'type': 'Overworld', 'teleporter': [62, 1]}, {'name': 'Overworld - Doom Castle', 'id': 476, 'area': 0, 'type': 'Overworld', 'teleporter': [33, 1]}, {'name': 'Overworld - Light Temple', 'id': 477, 'area': 0, 'type': 'Overworld', 'teleporter': [34, 1]}, {'name': 'Overworld - Mac Ship', 'id': 478, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Overworld - Mac Ship Doom', 'id': 479, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Dummy House - Bed Script', 'id': 480, 'area': 17, 'coordinates': [40, 56], 'teleporter': [1, 8]}, {'name': 'Dummy House - Entrance', 'id': 481, 'area': 17, 'coordinates': [41, 59], 'teleporter': [0, 10]}] \ No newline at end of file diff --git a/worlds/ffmq/data/rooms.yaml b/worlds/ffmq/data/rooms.yaml deleted file mode 100644 index e0c2e8d7..00000000 --- a/worlds/ffmq/data/rooms.yaml +++ /dev/null @@ -1,4026 +0,0 @@ -- name: Overworld - id: 0 - type: "Overworld" - game_objects: [] - links: - - target_room: 220 # To Forest Subregion - access: [] -- name: Subregion Foresta - id: 220 - type: "Subregion" - region: "Foresta" - game_objects: - - name: "Foresta South Battlefield" - object_id: 0x01 - location: "ForestaSouthBattlefield" - location_slot: "ForestaSouthBattlefield" - type: "BattlefieldXp" - access: [] - - name: "Foresta West Battlefield" - object_id: 0x02 - location: "ForestaWestBattlefield" - location_slot: "ForestaWestBattlefield" - type: "BattlefieldItem" - access: [] - - name: "Foresta East Battlefield" - object_id: 0x03 - location: "ForestaEastBattlefield" - location_slot: "ForestaEastBattlefield" - type: "BattlefieldGp" - access: [] - links: - - target_room: 15 # Level Forest - location: "LevelForest" - location_slot: "LevelForest" - entrance: 445 - teleporter: [0x2E, 8] - access: [] - - target_room: 16 # Foresta - location: "Foresta" - location_slot: "Foresta" - entrance: 446 - teleporter: [0x02, 1] - access: [] - - target_room: 24 # Sand Temple - location: "SandTemple" - location_slot: "SandTemple" - entrance: 447 - teleporter: [0x03, 1] - access: [] - - target_room: 25 # Bone Dungeon - location: "BoneDungeon" - location_slot: "BoneDungeon" - entrance: 448 - teleporter: [0x04, 1] - access: [] - - target_room: 3 # Focus Tower Foresta - location: "FocusTowerForesta" - location_slot: "FocusTowerForesta" - entrance: 449 - teleporter: [0x05, 1] - access: [] - - target_room: 221 - access: ["SandCoin"] - - target_room: 224 - access: ["RiverCoin"] - - target_room: 226 - access: ["SunCoin"] -- name: Subregion Aquaria - id: 221 - type: "Subregion" - region: "Aquaria" - game_objects: - - name: "South of Libra Temple Battlefield" - object_id: 0x04 - location: "AquariaBattlefield01" - location_slot: "AquariaBattlefield01" - type: "BattlefieldXp" - access: [] - - name: "East of Libra Temple Battlefield" - object_id: 0x05 - location: "AquariaBattlefield02" - location_slot: "AquariaBattlefield02" - type: "BattlefieldGp" - access: [] - - name: "South of Aquaria Battlefield" - object_id: 0x06 - location: "AquariaBattlefield03" - location_slot: "AquariaBattlefield03" - type: "BattlefieldItem" - access: [] - - name: "South of Wintry Cave Battlefield" - object_id: 0x07 - location: "WintryBattlefield01" - location_slot: "WintryBattlefield01" - type: "BattlefieldXp" - access: [] - - name: "West of Wintry Cave Battlefield" - object_id: 0x08 - location: "WintryBattlefield02" - location_slot: "WintryBattlefield02" - type: "BattlefieldGp" - access: [] - - name: "Ice Pyramid Battlefield" - object_id: 0x09 - location: "PyramidBattlefield01" - location_slot: "PyramidBattlefield01" - type: "BattlefieldXp" - access: [] - links: - - target_room: 10 # Focus Tower Aquaria - location: "FocusTowerAquaria" - location_slot: "FocusTowerAquaria" - entrance: 450 - teleporter: [0x13, 1] - access: [] - - target_room: 39 # Libra Temple - location: "LibraTemple" - location_slot: "LibraTemple" - entrance: 451 - teleporter: [0x07, 1] - access: [] - - target_room: 40 # Aquaria - location: "Aquaria" - location_slot: "Aquaria" - entrance: 452 - teleporter: [0x08, 8] - access: [] - - target_room: 45 # Wintry Cave - location: "WintryCave" - location_slot: "WintryCave" - entrance: 453 - teleporter: [0x0A, 1] - access: [] - - target_room: 52 # Falls Basin - location: "FallsBasin" - location_slot: "FallsBasin" - entrance: 455 - teleporter: [0x0C, 1] - access: [] - - target_room: 54 # Ice Pyramid - location: "IcePyramid" - location_slot: "IcePyramid" - entrance: 456 - teleporter: [0x0D, 1] # Will be switched to a script - access: [] - - target_room: 220 - access: ["SandCoin"] - - target_room: 224 - access: ["SandCoin", "RiverCoin"] - - target_room: 226 - access: ["SandCoin", "SunCoin"] - - target_room: 223 - access: ["SummerAquaria"] -- name: Subregion Life Temple - id: 222 - type: "Subregion" - region: "LifeTemple" - game_objects: [] - links: - - target_room: 51 # Life Temple - location: "LifeTemple" - location_slot: "LifeTemple" - entrance: 454 - teleporter: [0x0B, 1] - access: [] -- name: Subregion Frozen Fields - id: 223 - type: "Subregion" - region: "AquariaFrozenField" - game_objects: - - name: "North of Libra Temple Battlefield" - object_id: 0x0A - location: "LibraBattlefield01" - location_slot: "LibraBattlefield01" - type: "BattlefieldItem" - access: [] - - name: "Aquaria Frozen Field Battlefield" - object_id: 0x0B - location: "LibraBattlefield02" - location_slot: "LibraBattlefield02" - type: "BattlefieldXp" - access: [] - links: - - target_room: 74 # Wintry Temple - location: "WintryTemple" - location_slot: "WintryTemple" - entrance: 458 - teleporter: [0x10, 1] - access: [] - - target_room: 14 # Focus Tower Frozen Strip - location: "FocusTowerFrozen" - location_slot: "FocusTowerFrozen" - entrance: 459 - teleporter: [0x11, 1] - access: [] - - target_room: 221 - access: [] - - target_room: 225 - access: ["SummerAquaria", "DualheadHydra"] -- name: Subregion Fireburg - id: 224 - type: "Subregion" - region: "Fireburg" - game_objects: - - name: "Path to Fireburg Southern Battlefield" - object_id: 0x0C - location: "FireburgBattlefield01" - location_slot: "FireburgBattlefield01" - type: "BattlefieldGp" - access: [] - - name: "Path to Fireburg Central Battlefield" - object_id: 0x0D - location: "FireburgBattlefield02" - location_slot: "FireburgBattlefield02" - type: "BattlefieldItem" - access: [] - - name: "Path to Fireburg Northern Battlefield" - object_id: 0x0E - location: "FireburgBattlefield03" - location_slot: "FireburgBattlefield03" - type: "BattlefieldXp" - access: [] - - name: "Sealed Temple Battlefield" - object_id: 0x0F - location: "MineBattlefield01" - location_slot: "MineBattlefield01" - type: "BattlefieldGp" - access: [] - - name: "Mine Battlefield" - object_id: 0x10 - location: "MineBattlefield02" - location_slot: "MineBattlefield02" - type: "BattlefieldItem" - access: [] - - name: "Boulder Battlefield" - object_id: 0x11 - location: "MineBattlefield03" - location_slot: "MineBattlefield03" - type: "BattlefieldXp" - access: [] - links: - - target_room: 13 # Focus Tower Fireburg - location: "FocusTowerFireburg" - location_slot: "FocusTowerFireburg" - entrance: 460 - teleporter: [0x12, 1] - access: [] - - target_room: 76 # Fireburg - location: "Fireburg" - location_slot: "Fireburg" - entrance: 461 - teleporter: [0x14, 1] - access: [] - - target_room: 84 # Mine - location: "Mine" - location_slot: "Mine" - entrance: 462 - teleporter: [0x15, 1] - access: [] - - target_room: 92 # Sealed Temple - location: "SealedTemple" - location_slot: "SealedTemple" - entrance: 463 - teleporter: [0x16, 1] - access: [] - - target_room: 93 # Volcano - location: "Volcano" - location_slot: "Volcano" - entrance: 464 - teleporter: [0x17, 1] # Also this one / 0x0F, 8 - access: [] - - target_room: 100 # Lava Dome - location: "LavaDome" - location_slot: "LavaDome" - entrance: 465 - teleporter: [0x18, 1] - access: [] - - target_room: 220 - access: ["RiverCoin"] - - target_room: 221 - access: ["SandCoin", "RiverCoin"] - - target_room: 226 - access: ["RiverCoin", "SunCoin"] - - target_room: 225 - access: ["DualheadHydra"] -- name: Subregion Volcano Battlefield - id: 225 - type: "Subregion" - region: "VolcanoBattlefield" - game_objects: - - name: "Volcano Battlefield" - object_id: 0x12 - location: "VolcanoBattlefield01" - location_slot: "VolcanoBattlefield01" - type: "BattlefieldXp" - access: [] - links: - - target_room: 224 - access: ["DualheadHydra"] - - target_room: 223 - access: ["SummerAquaria"] -- name: Subregion Windia - id: 226 - type: "Subregion" - region: "Windia" - game_objects: - - name: "Kaidge Temple Battlefield" - object_id: 0x13 - location: "WindiaBattlefield01" - location_slot: "WindiaBattlefield01" - type: "BattlefieldXp" - access: ["SandCoin", "RiverCoin"] - - name: "South of Windia Battlefield" - object_id: 0x14 - location: "WindiaBattlefield02" - location_slot: "WindiaBattlefield02" - type: "BattlefieldXp" - access: ["SandCoin", "RiverCoin"] - links: - - target_room: 9 # Focus Tower Windia - location: "FocusTowerWindia" - location_slot: "FocusTowerWindia" - entrance: 466 - teleporter: [0x06, 1] - access: [] - - target_room: 123 # Rope Bridge - location: "RopeBridge" - location_slot: "RopeBridge" - entrance: 467 - teleporter: [0x19, 1] - access: [] - - target_room: 124 # Alive Forest - location: "AliveForest" - location_slot: "AliveForest" - entrance: 468 - teleporter: [0x1A, 1] - access: [] - - target_room: 125 # Giant Tree - location: "GiantTree" - location_slot: "GiantTree" - entrance: 469 - teleporter: [0x1B, 1] - access: ["Barred"] - - target_room: 152 # Kaidge Temple - location: "KaidgeTemple" - location_slot: "KaidgeTemple" - entrance: 470 - teleporter: [0x1C, 1] - access: [] - - target_room: 156 # Windia - location: "Windia" - location_slot: "Windia" - entrance: 471 - teleporter: [0x1D, 1] - access: [] - - target_room: 154 # Windhole Temple - location: "WindholeTemple" - location_slot: "WindholeTemple" - entrance: 472 - teleporter: [0x1E, 1] - access: [] - - target_room: 155 # Mount Gale - location: "MountGale" - location_slot: "MountGale" - entrance: 473 - teleporter: [0x1F, 1] - access: [] - - target_room: 166 # Pazuzu Tower - location: "PazuzusTower" - location_slot: "PazuzusTower" - entrance: 474 - teleporter: [0x20, 1] - access: [] - - target_room: 220 - access: ["SunCoin"] - - target_room: 221 - access: ["SandCoin", "SunCoin"] - - target_room: 224 - access: ["RiverCoin", "SunCoin"] - - target_room: 227 - access: ["RainbowBridge"] -- name: Subregion Spencer's Cave - id: 227 - type: "Subregion" - region: "SpencerCave" - game_objects: [] - links: - - target_room: 73 # Spencer's Place - location: "SpencersPlace" - location_slot: "SpencersPlace" - entrance: 457 - teleporter: [0x30, 8] - access: [] - - target_room: 226 - access: ["RainbowBridge"] -- name: Subregion Ship Dock - id: 228 - type: "Subregion" - region: "ShipDock" - game_objects: [] - links: - - target_room: 186 # Ship Dock - location: "ShipDock" - location_slot: "ShipDock" - entrance: 475 - teleporter: [0x3E, 1] - access: [] - - target_room: 229 - access: ["ShipLiberated", "ShipDockAccess"] -- name: Subregion Mac's Ship - id: 229 - type: "Subregion" - region: "MacShip" - game_objects: [] - links: - - target_room: 187 # Mac Ship - location: "MacsShip" - location_slot: "MacsShip" - entrance: 478 - teleporter: [0x24, 1] - access: [] - - target_room: 228 - access: ["ShipLiberated", "ShipDockAccess"] - - target_room: 231 - access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] -- name: Subregion Light Temple - id: 230 - type: "Subregion" - region: "LightTemple" - game_objects: [] - links: - - target_room: 185 # Light Temple - location: "LightTemple" - location_slot: "LightTemple" - entrance: 477 - teleporter: [0x23, 1] - access: [] -- name: Subregion Doom Castle - id: 231 - type: "Subregion" - region: "DoomCastle" - game_objects: [] - links: - - target_room: 1 # Doom Castle - location: "DoomCastle" - location_slot: "DoomCastle" - entrance: 476 - teleporter: [0x21, 1] - access: [] - - target_room: 187 # Mac Ship Doom - location: "MacsShipDoom" - location_slot: "MacsShipDoom" - entrance: 479 - teleporter: [0x24, 1] - access: ["Barred"] - - target_room: 229 - access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] -- name: Doom Castle - Sand Floor - id: 1 - game_objects: - - name: "Doom Castle B2 - Southeast Chest" - object_id: 0x01 - type: "Chest" - access: ["Bomb"] - - name: "Doom Castle B2 - Bone Ledge Box" - object_id: 0x1E - type: "Box" - access: [] - - name: "Doom Castle B2 - Hook Platform Box" - object_id: 0x1F - type: "Box" - access: ["DragonClaw"] - links: - - target_room: 231 - entrance: 1 - teleporter: [1, 6] - access: [] - - target_room: 5 - entrance: 0 - teleporter: [0, 0] - access: ["DragonClaw", "MegaGrenade"] -- name: Doom Castle - Aero Room - id: 2 - game_objects: - - name: "Doom Castle B2 - Sun Door Chest" - object_id: 0x00 - type: "Chest" - access: [] - links: - - target_room: 4 - entrance: 2 - teleporter: [1, 0] - access: [] -- name: Focus Tower B1 - Main Loop - id: 3 - game_objects: [] - links: - - target_room: 220 - entrance: 3 - teleporter: [2, 6] - access: [] - - target_room: 6 - entrance: 4 - teleporter: [4, 0] - access: [] -- name: Focus Tower B1 - Aero Corridor - id: 4 - game_objects: [] - links: - - target_room: 9 - entrance: 5 - teleporter: [5, 0] - access: [] - - target_room: 2 - entrance: 6 - teleporter: [8, 0] - access: [] -- name: Focus Tower B1 - Inner Loop - id: 5 - game_objects: [] - links: - - target_room: 1 - entrance: 8 - teleporter: [7, 0] - access: [] - - target_room: 201 - entrance: 7 - teleporter: [6, 0] - access: [] -- name: Focus Tower 1F Main Lobby - id: 6 - game_objects: - - name: "Focus Tower 1F - Main Lobby Box" - object_id: 0x21 - type: "Box" - access: [] - links: - - target_room: 3 - entrance: 11 - teleporter: [11, 0] - access: [] - - target_room: 7 - access: ["SandCoin"] - - target_room: 8 - access: ["RiverCoin"] - - target_room: 9 - access: ["SunCoin"] -- name: Focus Tower 1F SandCoin Room - id: 7 - game_objects: [] - links: - - target_room: 6 - access: ["SandCoin"] - - target_room: 10 - entrance: 10 - teleporter: [10, 0] - access: [] -- name: Focus Tower 1F RiverCoin Room - id: 8 - game_objects: [] - links: - - target_room: 6 - access: ["RiverCoin"] - - target_room: 11 - entrance: 14 - teleporter: [14, 0] - access: [] -- name: Focus Tower 1F SunCoin Room - id: 9 - game_objects: [] - links: - - target_room: 6 - access: ["SunCoin"] - - target_room: 4 - entrance: 12 - teleporter: [12, 0] - access: [] - - target_room: 226 - entrance: 9 - teleporter: [3, 6] - access: [] -- name: Focus Tower 1F SkyCoin Room - id: 201 - game_objects: [] - links: - - target_room: 195 - entrance: 13 - teleporter: [13, 0] - access: ["SkyCoin", "FlamerusRex", "IceGolem", "DualheadHydra", "Pazuzu"] - - target_room: 5 - entrance: 15 - teleporter: [15, 0] - access: [] -- name: Focus Tower 2F - Sand Coin Passage - id: 10 - game_objects: - - name: "Focus Tower 2F - Sand Door Chest" - object_id: 0x03 - type: "Chest" - access: [] - links: - - target_room: 221 - entrance: 16 - teleporter: [4, 6] - access: [] - - target_room: 7 - entrance: 17 - teleporter: [17, 0] - access: [] -- name: Focus Tower 2F - River Coin Passage - id: 11 - game_objects: [] - links: - - target_room: 8 - entrance: 18 - teleporter: [18, 0] - access: [] - - target_room: 13 - entrance: 19 - teleporter: [20, 0] - access: [] -- name: Focus Tower 2F - Venus Chest Room - id: 12 - game_objects: - - name: "Focus Tower 2F - Back Door Chest" - object_id: 0x02 - type: "Chest" - access: [] - - name: "Focus Tower 2F - Venus Chest" - object_id: 9 - type: "NPC" - access: ["Bomb", "VenusKey"] - links: - - target_room: 14 - entrance: 20 - teleporter: [19, 0] - access: [] -- name: Focus Tower 3F - Lower Floor - id: 13 - game_objects: - - name: "Focus Tower 3F - River Door Box" - object_id: 0x22 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 22 - teleporter: [6, 6] - access: [] - - target_room: 11 - entrance: 23 - teleporter: [24, 0] - access: [] -- name: Focus Tower 3F - Upper Floor - id: 14 - game_objects: [] - links: - - target_room: 223 - entrance: 24 - teleporter: [5, 6] - access: [] - - target_room: 12 - entrance: 25 - teleporter: [23, 0] - access: [] -- name: Level Forest - id: 15 - game_objects: - - name: "Level Forest - Northwest Box" - object_id: 0x28 - type: "Box" - access: ["Axe"] - - name: "Level Forest - Northeast Box" - object_id: 0x29 - type: "Box" - access: ["Axe"] - - name: "Level Forest - Middle Box" - object_id: 0x2A - type: "Box" - access: [] - - name: "Level Forest - Southwest Box" - object_id: 0x2B - type: "Box" - access: ["Axe"] - - name: "Level Forest - Southeast Box" - object_id: 0x2C - type: "Box" - access: ["Axe"] - - name: "Minotaur" - object_id: 0 - type: "Trigger" - on_trigger: ["Minotaur"] - access: ["Kaeli1"] - - name: "Level Forest - Old Man" - object_id: 0 - type: "NPC" - access: [] - - name: "Level Forest - Kaeli" - object_id: 1 - type: "NPC" - access: ["Kaeli1", "Minotaur"] - links: - - target_room: 220 - entrance: 28 - teleporter: [25, 0] - access: [] -- name: Foresta - id: 16 - game_objects: - - name: "Foresta - Outside Box" - object_id: 0x2D - type: "Box" - access: ["Axe"] - links: - - target_room: 220 - entrance: 38 - teleporter: [31, 0] - access: [] - - target_room: 17 - entrance: 44 - teleporter: [0, 5] - access: [] - - target_room: 18 - entrance: 42 - teleporter: [32, 4] - access: [] - - target_room: 19 - entrance: 43 - teleporter: [33, 0] - access: [] - - target_room: 20 - entrance: 45 - teleporter: [1, 5] - access: [] -- name: Kaeli's House - id: 17 - game_objects: - - name: "Foresta - Kaeli's House Box" - object_id: 0x2E - type: "Box" - access: [] - - name: "Kaeli Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Kaeli1"] - access: ["TreeWither"] - - name: "Kaeli 2" - object_id: 0 - type: "Trigger" - on_trigger: ["Kaeli2"] - access: ["Kaeli1", "Minotaur", "Elixir"] - links: - - target_room: 16 - entrance: 46 - teleporter: [86, 3] - access: [] -- name: Foresta Houses - Old Man's House Main - id: 18 - game_objects: [] - links: - - target_room: 19 - access: ["BarrelPushed"] - - target_room: 16 - entrance: 47 - teleporter: [34, 0] - access: [] -- name: Foresta Houses - Old Man's House Back - id: 19 - game_objects: - - name: "Foresta - Old Man House Chest" - object_id: 0x05 - type: "Chest" - access: [] - - name: "Old Man Barrel" - object_id: 0 - type: "Trigger" - on_trigger: ["BarrelPushed"] - access: [] - links: - - target_room: 18 - access: ["BarrelPushed"] - - target_room: 16 - entrance: 48 - teleporter: [35, 0] - access: [] -- name: Foresta Houses - Rest House - id: 20 - game_objects: - - name: "Foresta - Rest House Box" - object_id: 0x2F - type: "Box" - access: [] - links: - - target_room: 16 - entrance: 50 - teleporter: [87, 3] - access: [] -- name: Libra Treehouse - id: 21 - game_objects: - - name: "Alive Forest - Libra Treehouse Box" - object_id: 0x32 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 51 - teleporter: [67, 8] - access: ["LibraCrest"] -- name: Gemini Treehouse - id: 22 - game_objects: - - name: "Alive Forest - Gemini Treehouse Box" - object_id: 0x33 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 52 - teleporter: [68, 8] - access: ["GeminiCrest"] -- name: Mobius Treehouse - id: 23 - game_objects: - - name: "Alive Forest - Mobius Treehouse West Box" - object_id: 0x30 - type: "Box" - access: [] - - name: "Alive Forest - Mobius Treehouse East Box" - object_id: 0x31 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 53 - teleporter: [69, 8] - access: ["MobiusCrest"] -- name: Sand Temple - id: 24 - game_objects: - - name: "Tristam Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Tristam"] - access: [] - links: - - target_room: 220 - entrance: 54 - teleporter: [36, 0] - access: [] -- name: Bone Dungeon 1F - id: 25 - game_objects: - - name: "Bone Dungeon 1F - Entrance Room West Box" - object_id: 0x35 - type: "Box" - access: [] - - name: "Bone Dungeon 1F - Entrance Room Middle Box" - object_id: 0x36 - type: "Box" - access: [] - - name: "Bone Dungeon 1F - Entrance Room East Box" - object_id: 0x37 - type: "Box" - access: [] - links: - - target_room: 220 - entrance: 55 - teleporter: [37, 0] - access: [] - - target_room: 26 - entrance: 56 - teleporter: [2, 2] - access: [] -- name: Bone Dungeon B1 - Waterway - id: 26 - game_objects: - - name: "Bone Dungeon B1 - Skull Chest" - object_id: 0x06 - type: "Chest" - access: ["Bomb"] - - name: "Bone Dungeon B1 - Tristam" - object_id: 2 - type: "NPC" - access: ["Tristam"] - - name: "Tristam Bone Dungeon Item Given" - object_id: 0 - type: "Trigger" - on_trigger: ["TristamBoneItemGiven"] - access: ["Tristam"] - links: - - target_room: 25 - entrance: 59 - teleporter: [88, 3] - access: [] - - target_room: 28 - entrance: 57 - teleporter: [3, 2] - access: ["Bomb"] -- name: Bone Dungeon B1 - Checker Room - id: 28 - game_objects: - - name: "Bone Dungeon B1 - Checker Room Box" - object_id: 0x38 - type: "Box" - access: ["Bomb"] - links: - - target_room: 26 - entrance: 61 - teleporter: [89, 3] - access: [] - - target_room: 30 - entrance: 60 - teleporter: [4, 2] - access: [] -- name: Bone Dungeon B1 - Hidden Room - id: 29 - game_objects: - - name: "Bone Dungeon B1 - Ribcage Waterway Box" - object_id: 0x39 - type: "Box" - access: [] - links: - - target_room: 31 - entrance: 62 - teleporter: [91, 3] - access: [] -- name: Bone Dungeon B2 - Exploding Skull Room - First Room - id: 30 - game_objects: - - name: "Bone Dungeon B2 - Spines Room Alcove Box" - object_id: 0x3B - type: "Box" - access: [] - - name: "Long Spine" - object_id: 0 - type: "Trigger" - on_trigger: ["LongSpineBombed"] - access: ["Bomb"] - links: - - target_room: 28 - entrance: 65 - teleporter: [90, 3] - access: [] - - target_room: 31 - access: ["LongSpineBombed"] -- name: Bone Dungeon B2 - Exploding Skull Room - Second Room - id: 31 - game_objects: - - name: "Bone Dungeon B2 - Spines Room Looped Hallway Box" - object_id: 0x3A - type: "Box" - access: [] - - name: "Short Spine" - object_id: 0 - type: "Trigger" - on_trigger: ["ShortSpineBombed"] - access: ["Bomb"] - links: - - target_room: 29 - entrance: 63 - teleporter: [5, 2] - access: ["LongSpineBombed"] - - target_room: 32 - access: ["ShortSpineBombed"] - - target_room: 30 - access: ["LongSpineBombed"] -- name: Bone Dungeon B2 - Exploding Skull Room - Third Room - id: 32 - game_objects: [] - links: - - target_room: 35 - entrance: 64 - teleporter: [6, 2] - access: [] - - target_room: 31 - access: ["ShortSpineBombed"] -- name: Bone Dungeon B2 - Box Room - id: 33 - game_objects: - - name: "Bone Dungeon B2 - Lone Room Box" - object_id: 0x3D - type: "Box" - access: [] - links: - - target_room: 36 - entrance: 66 - teleporter: [93, 3] - access: [] -- name: Bone Dungeon B2 - Quake Room - id: 34 - game_objects: - - name: "Bone Dungeon B2 - Penultimate Room Chest" - object_id: 0x07 - type: "Chest" - access: [] - links: - - target_room: 37 - entrance: 67 - teleporter: [94, 3] - access: [] -- name: Bone Dungeon B2 - Two Skulls Room - First Room - id: 35 - game_objects: - - name: "Bone Dungeon B2 - Two Skulls Room Box" - object_id: 0x3C - type: "Box" - access: [] - - name: "Skull 1" - object_id: 0 - type: "Trigger" - on_trigger: ["Skull1Bombed"] - access: ["Bomb"] - links: - - target_room: 32 - entrance: 71 - teleporter: [92, 3] - access: [] - - target_room: 36 - access: ["Skull1Bombed"] -- name: Bone Dungeon B2 - Two Skulls Room - Second Room - id: 36 - game_objects: - - name: "Skull 2" - object_id: 0 - type: "Trigger" - on_trigger: ["Skull2Bombed"] - access: ["Bomb"] - links: - - target_room: 33 - entrance: 68 - teleporter: [7, 2] - access: [] - - target_room: 37 - access: ["Skull2Bombed"] - - target_room: 35 - access: ["Skull1Bombed"] -- name: Bone Dungeon B2 - Two Skulls Room - Third Room - id: 37 - game_objects: [] - links: - - target_room: 34 - entrance: 69 - teleporter: [8, 2] - access: [] - - target_room: 38 - entrance: 70 - teleporter: [9, 2] - access: ["Bomb"] - - target_room: 36 - access: ["Skull2Bombed"] -- name: Bone Dungeon B2 - Boss Room - id: 38 - game_objects: - - name: "Bone Dungeon B2 - North Box" - object_id: 0x3E - type: "Box" - access: [] - - name: "Bone Dungeon B2 - South Box" - object_id: 0x3F - type: "Box" - access: [] - - name: "Bone Dungeon B2 - Flamerus Rex Chest" - object_id: 0x08 - type: "Chest" - access: [] - - name: "Bone Dungeon B2 - Tristam's Treasure Chest" - object_id: 0x04 - type: "Chest" - access: [] - - name: "Flamerus Rex" - object_id: 0 - type: "Trigger" - on_trigger: ["FlamerusRex"] - access: [] - links: - - target_room: 37 - entrance: 74 - teleporter: [95, 3] - access: [] -- name: Libra Temple - id: 39 - game_objects: - - name: "Libra Temple - Box" - object_id: 0x40 - type: "Box" - access: [] - - name: "Phoebe Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Phoebe1"] - access: [] - links: - - target_room: 221 - entrance: 75 - teleporter: [13, 6] - access: [] - - target_room: 51 - entrance: 76 - teleporter: [59, 8] - access: ["LibraCrest"] -- name: Aquaria - id: 40 - game_objects: - - name: "Summer Aquaria" - object_id: 0 - type: "Trigger" - on_trigger: ["SummerAquaria"] - access: ["WakeWater"] - links: - - target_room: 221 - entrance: 77 - teleporter: [8, 6] - access: [] - - target_room: 41 - entrance: 81 - teleporter: [10, 5] - access: [] - - target_room: 42 - entrance: 82 - teleporter: [44, 4] - access: [] - - target_room: 44 - entrance: 83 - teleporter: [11, 5] - access: [] - - target_room: 71 - entrance: 89 - teleporter: [42, 0] - access: ["SummerAquaria"] - - target_room: 71 - entrance: 90 - teleporter: [43, 0] - access: ["SummerAquaria"] -- name: Phoebe's House - id: 41 - game_objects: - - name: "Aquaria - Phoebe's House Chest" - object_id: 0x41 - type: "Box" - access: [] - links: - - target_room: 40 - entrance: 93 - teleporter: [5, 8] - access: [] -- name: Aquaria Vendor House - id: 42 - game_objects: - - name: "Aquaria - Vendor" - object_id: 4 - type: "NPC" - access: [] - - name: "Aquaria - Vendor House Box" - object_id: 0x42 - type: "Box" - access: [] - links: - - target_room: 40 - entrance: 94 - teleporter: [40, 8] - access: [] - - target_room: 43 - entrance: 95 - teleporter: [47, 0] - access: [] -- name: Aquaria Gemini Room - id: 43 - game_objects: [] - links: - - target_room: 42 - entrance: 97 - teleporter: [48, 0] - access: [] - - target_room: 81 - entrance: 96 - teleporter: [72, 8] - access: ["GeminiCrest"] -- name: Aquaria INN - id: 44 - game_objects: [] - links: - - target_room: 40 - entrance: 98 - teleporter: [75, 8] - access: [] -- name: Wintry Cave 1F - East Ledge - id: 45 - game_objects: - - name: "Wintry Cave 1F - North Box" - object_id: 0x43 - type: "Box" - access: [] - - name: "Wintry Cave 1F - Entrance Box" - object_id: 0x46 - type: "Box" - access: [] - - name: "Wintry Cave 1F - Slippery Cliff Box" - object_id: 0x44 - type: "Box" - access: ["Claw"] - - name: "Wintry Cave 1F - Phoebe" - object_id: 5 - type: "NPC" - access: ["Phoebe1"] - links: - - target_room: 221 - entrance: 99 - teleporter: [49, 0] - access: [] - - target_room: 49 - entrance: 100 - teleporter: [14, 2] - access: ["Bomb"] - - target_room: 46 - access: ["Claw"] -- name: Wintry Cave 1F - Central Space - id: 46 - game_objects: - - name: "Wintry Cave 1F - Scenic Overlook Box" - object_id: 0x45 - type: "Box" - access: ["Claw"] - links: - - target_room: 45 - access: ["Claw"] - - target_room: 47 - access: ["Claw"] -- name: Wintry Cave 1F - West Ledge - id: 47 - game_objects: [] - links: - - target_room: 48 - entrance: 101 - teleporter: [15, 2] - access: ["Bomb"] - - target_room: 46 - access: ["Claw"] -- name: Wintry Cave 2F - id: 48 - game_objects: - - name: "Wintry Cave 2F - West Left Box" - object_id: 0x47 - type: "Box" - access: [] - - name: "Wintry Cave 2F - West Right Box" - object_id: 0x48 - type: "Box" - access: [] - - name: "Wintry Cave 2F - East Left Box" - object_id: 0x49 - type: "Box" - access: [] - - name: "Wintry Cave 2F - East Right Box" - object_id: 0x4A - type: "Box" - access: [] - links: - - target_room: 47 - entrance: 104 - teleporter: [97, 3] - access: [] - - target_room: 50 - entrance: 103 - teleporter: [50, 0] - access: [] -- name: Wintry Cave 3F Top - id: 49 - game_objects: - - name: "Wintry Cave 3F - West Box" - object_id: 0x4B - type: "Box" - access: [] - - name: "Wintry Cave 3F - East Box" - object_id: 0x4C - type: "Box" - access: [] - links: - - target_room: 45 - entrance: 105 - teleporter: [96, 3] - access: [] -- name: Wintry Cave 3F Bottom - id: 50 - game_objects: - - name: "Wintry Cave 3F - Squidite Chest" - object_id: 0x09 - type: "Chest" - access: ["Phanquid"] - - name: "Phanquid" - object_id: 0 - type: "Trigger" - on_trigger: ["Phanquid"] - access: [] - - name: "Wintry Cave 3F - Before Boss Box" - object_id: 0x4D - type: "Box" - access: [] - links: - - target_room: 48 - entrance: 106 - teleporter: [51, 0] - access: [] -- name: Life Temple - id: 51 - game_objects: - - name: "Life Temple - Box" - object_id: 0x4E - type: "Box" - access: [] - - name: "Life Temple - Mysterious Man" - object_id: 6 - type: "NPC" - access: [] - links: - - target_room: 222 - entrance: 107 - teleporter: [14, 6] - access: [] - - target_room: 39 - entrance: 108 - teleporter: [60, 8] - access: ["LibraCrest"] -- name: Fall Basin - id: 52 - game_objects: - - name: "Falls Basin - Snow Crab Chest" - object_id: 0x0A - type: "Chest" - access: ["FreezerCrab"] - - name: "Freezer Crab" - object_id: 0 - type: "Trigger" - on_trigger: ["FreezerCrab"] - access: [] - - name: "Falls Basin - Box" - object_id: 0x4F - type: "Box" - access: [] - links: - - target_room: 221 - entrance: 111 - teleporter: [53, 0] - access: [] -- name: Ice Pyramid B1 Taunt Room - id: 53 - game_objects: - - name: "Ice Pyramid B1 - Chest" - object_id: 0x0B - type: "Chest" - access: [] - - name: "Ice Pyramid B1 - West Box" - object_id: 0x50 - type: "Box" - access: [] - - name: "Ice Pyramid B1 - North Box" - object_id: 0x51 - type: "Box" - access: [] - - name: "Ice Pyramid B1 - East Box" - object_id: 0x52 - type: "Box" - access: [] - links: - - target_room: 68 - entrance: 113 - teleporter: [55, 0] - access: [] -- name: Ice Pyramid 1F Maze Lobby - id: 54 - game_objects: - - name: "Ice Pyramid 1F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid1FStatue"] - access: ["Sword"] - links: - - target_room: 221 - entrance: 114 - teleporter: [56, 0] - access: [] - - target_room: 55 - access: ["IcePyramid1FStatue"] -- name: Ice Pyramid 1F Maze - id: 55 - game_objects: - - name: "Ice Pyramid 1F - East Alcove Chest" - object_id: 0x0D - type: "Chest" - access: [] - - name: "Ice Pyramid 1F - Sandwiched Alcove Box" - object_id: 0x53 - type: "Box" - access: [] - - name: "Ice Pyramid 1F - Southwest Left Box" - object_id: 0x54 - type: "Box" - access: [] - - name: "Ice Pyramid 1F - Southwest Right Box" - object_id: 0x55 - type: "Box" - access: [] - links: - - target_room: 56 - entrance: 116 - teleporter: [57, 0] - access: [] - - target_room: 57 - entrance: 117 - teleporter: [58, 0] - access: [] - - target_room: 58 - entrance: 118 - teleporter: [59, 0] - access: [] - - target_room: 59 - entrance: 119 - teleporter: [60, 0] - access: [] - - target_room: 60 - entrance: 120 - teleporter: [61, 0] - access: [] - - target_room: 54 - access: ["IcePyramid1FStatue"] -- name: Ice Pyramid 2F South Tiled Room - id: 56 - game_objects: - - name: "Ice Pyramid 2F - South Side Glass Door Box" - object_id: 0x57 - type: "Box" - access: ["Sword"] - - name: "Ice Pyramid 2F - South Side East Box" - object_id: 0x5B - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 122 - teleporter: [62, 0] - access: [] - - target_room: 61 - entrance: 123 - teleporter: [67, 0] - access: [] -- name: Ice Pyramid 2F West Room - id: 57 - game_objects: - - name: "Ice Pyramid 2F - Northwest Room Box" - object_id: 0x5A - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 124 - teleporter: [63, 0] - access: [] -- name: Ice Pyramid 2F Center Room - id: 58 - game_objects: - - name: "Ice Pyramid 2F - Center Room Box" - object_id: 0x56 - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 125 - teleporter: [64, 0] - access: [] -- name: Ice Pyramid 2F Small North Room - id: 59 - game_objects: - - name: "Ice Pyramid 2F - North Room Glass Door Box" - object_id: 0x58 - type: "Box" - access: ["Sword"] - links: - - target_room: 55 - entrance: 126 - teleporter: [65, 0] - access: [] -- name: Ice Pyramid 2F North Corridor - id: 60 - game_objects: - - name: "Ice Pyramid 2F - North Corridor Glass Door Box" - object_id: 0x59 - type: "Box" - access: ["Sword"] - links: - - target_room: 55 - entrance: 127 - teleporter: [66, 0] - access: [] - - target_room: 62 - entrance: 128 - teleporter: [68, 0] - access: [] -- name: Ice Pyramid 3F Two Boxes Room - id: 61 - game_objects: - - name: "Ice Pyramid 3F - Staircase Dead End Left Box" - object_id: 0x5E - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Staircase Dead End Right Box" - object_id: 0x5F - type: "Box" - access: [] - links: - - target_room: 56 - entrance: 129 - teleporter: [69, 0] - access: [] -- name: Ice Pyramid 3F Main Loop - id: 62 - game_objects: - - name: "Ice Pyramid 3F - Inner Room North Box" - object_id: 0x5C - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Inner Room South Box" - object_id: 0x5D - type: "Box" - access: [] - - name: "Ice Pyramid 3F - East Alcove Box" - object_id: 0x60 - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Leapfrog Box" - object_id: 0x61 - type: "Box" - access: [] - - name: "Ice Pyramid 3F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid3FStatue"] - access: ["Sword"] - links: - - target_room: 60 - entrance: 130 - teleporter: [70, 0] - access: [] - - target_room: 63 - access: ["IcePyramid3FStatue"] -- name: Ice Pyramid 3F Blocked Room - id: 63 - game_objects: [] - links: - - target_room: 64 - entrance: 131 - teleporter: [71, 0] - access: [] - - target_room: 62 - access: ["IcePyramid3FStatue"] -- name: Ice Pyramid 4F Main Loop - id: 64 - game_objects: [] - links: - - target_room: 66 - entrance: 133 - teleporter: [73, 0] - access: [] - - target_room: 63 - entrance: 132 - teleporter: [72, 0] - access: [] - - target_room: 65 - access: ["IcePyramid4FStatue"] -- name: Ice Pyramid 4F Treasure Room - id: 65 - game_objects: - - name: "Ice Pyramid 4F - Chest" - object_id: 0x0C - type: "Chest" - access: [] - - name: "Ice Pyramid 4F - Northwest Box" - object_id: 0x62 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - West Left Box" - object_id: 0x63 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - West Right Box" - object_id: 0x64 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - South Left Box" - object_id: 0x65 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - South Right Box" - object_id: 0x66 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - East Left Box" - object_id: 0x67 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - East Right Box" - object_id: 0x68 - type: "Box" - access: [] - - name: "Ice Pyramid 4F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid4FStatue"] - access: ["Sword"] - links: - - target_room: 64 - access: ["IcePyramid4FStatue"] -- name: Ice Pyramid 5F Leap of Faith Room - id: 66 - game_objects: - - name: "Ice Pyramid 5F - Glass Door Left Box" - object_id: 0x69 - type: "Box" - access: ["IcePyramid5FStatue"] - - name: "Ice Pyramid 5F - West Ledge Box" - object_id: 0x6A - type: "Box" - access: [] - - name: "Ice Pyramid 5F - South Shelf Box" - object_id: 0x6B - type: "Box" - access: [] - - name: "Ice Pyramid 5F - South Leapfrog Box" - object_id: 0x6C - type: "Box" - access: [] - - name: "Ice Pyramid 5F - Glass Door Right Box" - object_id: 0x6D - type: "Box" - access: ["IcePyramid5FStatue"] - - name: "Ice Pyramid 5F - North Box" - object_id: 0x6E - type: "Box" - access: [] - links: - - target_room: 64 - entrance: 134 - teleporter: [74, 0] - access: [] - - target_room: 65 - access: [] - - target_room: 53 - access: ["Bomb", "Claw", "Sword"] -- name: Ice Pyramid 5F Stairs to Ice Golem - id: 67 - game_objects: - - name: "Ice Pyramid 5F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid5FStatue"] - access: ["Sword"] - links: - - target_room: 69 - entrance: 137 - teleporter: [76, 0] - access: [] - - target_room: 65 - access: [] - - target_room: 70 - entrance: 136 - teleporter: [75, 0] - access: [] -- name: Ice Pyramid Climbing Wall Room Lower Space - id: 68 - game_objects: [] - links: - - target_room: 53 - entrance: 139 - teleporter: [78, 0] - access: [] - - target_room: 69 - access: ["Claw"] -- name: Ice Pyramid Climbing Wall Room Upper Space - id: 69 - game_objects: [] - links: - - target_room: 67 - entrance: 140 - teleporter: [79, 0] - access: [] - - target_room: 68 - access: ["Claw"] -- name: Ice Pyramid Ice Golem Room - id: 70 - game_objects: - - name: "Ice Pyramid 6F - Ice Golem Chest" - object_id: 0x0E - type: "Chest" - access: ["IceGolem"] - - name: "Ice Golem" - object_id: 0 - type: "Trigger" - on_trigger: ["IceGolem"] - access: [] - links: - - target_room: 67 - entrance: 141 - teleporter: [80, 0] - access: [] - - target_room: 66 - access: [] -- name: Spencer Waterfall - id: 71 - game_objects: [] - links: - - target_room: 72 - entrance: 143 - teleporter: [81, 0] - access: [] - - target_room: 40 - entrance: 145 - teleporter: [82, 0] - access: [] - - target_room: 40 - entrance: 148 - teleporter: [83, 0] - access: [] -- name: Spencer Cave Normal Main - id: 72 - game_objects: - - name: "Spencer's Cave - Box" - object_id: 0x6F - type: "Box" - access: ["Claw"] - - name: "Spencer's Cave - Spencer" - object_id: 8 - type: "NPC" - access: [] - - name: "Spencer's Cave - Locked Chest" - object_id: 13 - type: "NPC" - access: ["VenusKey"] - links: - - target_room: 71 - entrance: 150 - teleporter: [85, 0] - access: [] -- name: Spencer Cave Normal South Ledge - id: 73 - game_objects: - - name: "Collapse Spencer's Cave" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipLiberated"] - access: ["MegaGrenade"] - links: - - target_room: 227 - entrance: 151 - teleporter: [7, 6] - access: [] - - target_room: 203 - access: ["MegaGrenade"] -# - target_room: 72 # access to spencer? -# access: ["MegaGrenade"] -- name: Spencer Cave Caved In Main Loop - id: 203 - game_objects: [] - links: - - target_room: 73 - access: [] - - target_room: 207 - entrance: 156 - teleporter: [36, 8] - access: ["MobiusCrest"] - - target_room: 204 - access: ["Claw"] - - target_room: 205 - access: ["Bomb"] -- name: Spencer Cave Caved In Waters - id: 204 - game_objects: - - name: "Bomb Libra Block" - object_id: 0 - type: "Trigger" - on_trigger: ["SpencerCaveLibraBlockBombed"] - access: ["MegaGrenade", "Claw"] - links: - - target_room: 203 - access: ["Claw"] -- name: Spencer Cave Caved In Libra Nook - id: 205 - game_objects: [] - links: - - target_room: 206 - entrance: 153 - teleporter: [33, 8] - access: ["LibraCrest"] -- name: Spencer Cave Caved In Libra Corridor - id: 206 - game_objects: [] - links: - - target_room: 205 - entrance: 154 - teleporter: [34, 8] - access: ["LibraCrest"] - - target_room: 207 - access: ["SpencerCaveLibraBlockBombed"] -- name: Spencer Cave Caved In Mobius Chest - id: 207 - game_objects: - - name: "Spencer's Cave - Mobius Chest" - object_id: 0x0F - type: "Chest" - access: [] - links: - - target_room: 203 - entrance: 155 - teleporter: [35, 8] - access: ["MobiusCrest"] - - target_room: 206 - access: ["Bomb"] -- name: Wintry Temple Outer Room - id: 74 - game_objects: [] - links: - - target_room: 223 - entrance: 157 - teleporter: [15, 6] - access: [] -- name: Wintry Temple Inner Room - id: 75 - game_objects: - - name: "Wintry Temple - West Box" - object_id: 0x70 - type: "Box" - access: [] - - name: "Wintry Temple - North Box" - object_id: 0x71 - type: "Box" - access: [] - links: - - target_room: 92 - entrance: 158 - teleporter: [62, 8] - access: ["GeminiCrest"] -- name: Fireburg Upper Plaza - id: 76 - game_objects: [] - links: - - target_room: 224 - entrance: 159 - teleporter: [9, 6] - access: [] - - target_room: 80 - entrance: 163 - teleporter: [91, 0] - access: [] - - target_room: 77 - entrance: 164 - teleporter: [98, 8] # original value [16, 2] - access: [] - - target_room: 82 - entrance: 165 - teleporter: [96, 8] # original value [17, 2] - access: [] - - target_room: 208 - access: ["Claw"] -- name: Fireburg Lower Plaza - id: 208 - game_objects: - - name: "Fireburg - Hidden Tunnel Box" - object_id: 0x74 - type: "Box" - access: [] - links: - - target_room: 76 - access: ["Claw"] - - target_room: 78 - entrance: 166 - teleporter: [11, 8] - access: ["MultiKey"] -- name: Reuben's House - id: 77 - game_objects: - - name: "Fireburg - Reuben's House Arion" - object_id: 14 - type: "NPC" - access: ["ReubenDadSaved"] - - name: "Reuben Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Reuben1"] - access: [] - - name: "Fireburg - Reuben's House Box" - object_id: 0x75 - type: "Box" - access: [] - links: - - target_room: 76 - entrance: 167 - teleporter: [98, 3] - access: [] -- name: GrenadeMan's House - id: 78 - game_objects: - - name: "Fireburg - Locked House Man" - object_id: 12 - type: "NPC" - access: [] - links: - - target_room: 208 - entrance: 168 - teleporter: [9, 8] - access: ["MultiKey"] - - target_room: 79 - entrance: 169 - teleporter: [93, 0] - access: [] -- name: GrenadeMan's Mobius Room - id: 79 - game_objects: [] - links: - - target_room: 78 - entrance: 170 - teleporter: [94, 0] - access: [] - - target_room: 161 - entrance: 171 - teleporter: [54, 8] - access: ["MobiusCrest"] -- name: Fireburg Vendor House - id: 80 - game_objects: - - name: "Fireburg - Vendor" - object_id: 11 - type: "NPC" - access: [] - links: - - target_room: 76 - entrance: 172 - teleporter: [95, 0] - access: [] - - target_room: 81 - entrance: 173 - teleporter: [96, 0] - access: [] -- name: Fireburg Gemini Room - id: 81 - game_objects: [] - links: - - target_room: 80 - entrance: 174 - teleporter: [97, 0] - access: [] - - target_room: 43 - entrance: 175 - teleporter: [45, 8] - access: ["GeminiCrest"] -- name: Fireburg Hotel Lobby - id: 82 - game_objects: - - name: "Fireburg - Tristam" - object_id: 10 - type: "NPC" - access: ["Tristam", "TristamBoneItemGiven"] - links: - - target_room: 76 - entrance: 177 - teleporter: [99, 3] - access: [] - - target_room: 83 - entrance: 176 - teleporter: [213, 0] - access: [] -- name: Fireburg Hotel Beds - id: 83 - game_objects: [] - links: - - target_room: 82 - entrance: 178 - teleporter: [214, 0] - access: [] -- name: Mine Exterior North West Platforms - id: 84 - game_objects: [] - links: - - target_room: 224 - entrance: 179 - teleporter: [98, 0] - access: [] - - target_room: 88 - entrance: 181 - teleporter: [20, 2] - access: ["Bomb"] - - target_room: 85 - access: ["Claw"] - - target_room: 86 - access: ["Claw"] - - target_room: 87 - access: ["Claw"] -- name: Mine Exterior Central Ledge - id: 85 - game_objects: [] - links: - - target_room: 90 - entrance: 183 - teleporter: [22, 2] - access: ["Bomb"] - - target_room: 84 - access: ["Claw"] -- name: Mine Exterior North Ledge - id: 86 - game_objects: [] - links: - - target_room: 89 - entrance: 182 - teleporter: [21, 2] - access: ["Bomb"] - - target_room: 85 - access: ["Claw"] -- name: Mine Exterior South East Platforms - id: 87 - game_objects: - - name: "Jinn" - object_id: 0 - type: "Trigger" - on_trigger: ["Jinn"] - access: [] - links: - - target_room: 91 - entrance: 180 - teleporter: [99, 0] - access: ["Jinn"] - - target_room: 86 - access: [] - - target_room: 85 - access: ["Claw"] -- name: Mine Parallel Room - id: 88 - game_objects: - - name: "Mine - Parallel Room West Box" - object_id: 0x77 - type: "Box" - access: ["Claw"] - - name: "Mine - Parallel Room East Box" - object_id: 0x78 - type: "Box" - access: ["Claw"] - links: - - target_room: 84 - entrance: 185 - teleporter: [100, 3] - access: [] -- name: Mine Crescent Room - id: 89 - game_objects: - - name: "Mine - Crescent Room Chest" - object_id: 0x10 - type: "Chest" - access: [] - links: - - target_room: 86 - entrance: 186 - teleporter: [101, 3] - access: [] -- name: Mine Climbing Room - id: 90 - game_objects: - - name: "Mine - Glitchy Collision Cave Box" - object_id: 0x76 - type: "Box" - access: ["Claw"] - links: - - target_room: 85 - entrance: 187 - teleporter: [102, 3] - access: [] -- name: Mine Cliff - id: 91 - game_objects: - - name: "Mine - Cliff Southwest Box" - object_id: 0x79 - type: "Box" - access: [] - - name: "Mine - Cliff Northwest Box" - object_id: 0x7A - type: "Box" - access: [] - - name: "Mine - Cliff Northeast Box" - object_id: 0x7B - type: "Box" - access: [] - - name: "Mine - Cliff Southeast Box" - object_id: 0x7C - type: "Box" - access: [] - - name: "Mine - Reuben" - object_id: 7 - type: "NPC" - access: ["Reuben1"] - - name: "Reuben's dad Saved" - object_id: 0 - type: "Trigger" - on_trigger: ["ReubenDadSaved"] - access: ["MegaGrenade"] - links: - - target_room: 87 - entrance: 188 - teleporter: [100, 0] - access: [] -- name: Sealed Temple - id: 92 - game_objects: - - name: "Sealed Temple - West Box" - object_id: 0x7D - type: "Box" - access: [] - - name: "Sealed Temple - East Box" - object_id: 0x7E - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 190 - teleporter: [16, 6] - access: [] - - target_room: 75 - entrance: 191 - teleporter: [63, 8] - access: ["GeminiCrest"] -- name: Volcano Base - id: 93 - game_objects: - - name: "Volcano - Base Chest" - object_id: 0x11 - type: "Chest" - access: [] - - name: "Volcano - Base West Box" - object_id: 0x7F - type: "Box" - access: [] - - name: "Volcano - Base East Left Box" - object_id: 0x80 - type: "Box" - access: [] - - name: "Volcano - Base East Right Box" - object_id: 0x81 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 192 - teleporter: [103, 0] - access: [] - - target_room: 98 - entrance: 196 - teleporter: [31, 8] - access: [] - - target_room: 96 - entrance: 197 - teleporter: [30, 8] - access: [] -- name: Volcano Top Left - id: 94 - game_objects: - - name: "Volcano - Medusa Chest" - object_id: 0x12 - type: "Chest" - access: ["Medusa"] - - name: "Medusa" - object_id: 0 - type: "Trigger" - on_trigger: ["Medusa"] - access: [] - - name: "Volcano - Behind Medusa Box" - object_id: 0x82 - type: "Box" - access: [] - links: - - target_room: 209 - entrance: 199 - teleporter: [26, 8] - access: [] -- name: Volcano Top Right - id: 95 - game_objects: - - name: "Volcano - Top of the Volcano Left Box" - object_id: 0x83 - type: "Box" - access: [] - - name: "Volcano - Top of the Volcano Right Box" - object_id: 0x84 - type: "Box" - access: [] - links: - - target_room: 99 - entrance: 200 - teleporter: [79, 8] - access: [] -- name: Volcano Right Path - id: 96 - game_objects: - - name: "Volcano - Right Path Box" - object_id: 0x87 - type: "Box" - access: [] - links: - - target_room: 93 - entrance: 201 - teleporter: [15, 8] - access: [] -- name: Volcano Left Path - id: 98 - game_objects: - - name: "Volcano - Left Path Box" - object_id: 0x86 - type: "Box" - access: [] - links: - - target_room: 93 - entrance: 204 - teleporter: [27, 8] - access: [] - - target_room: 99 - entrance: 202 - teleporter: [25, 2] - access: [] - - target_room: 209 - entrance: 203 - teleporter: [26, 2] - access: [] -- name: Volcano Cross Left-Right - id: 99 - game_objects: [] - links: - - target_room: 95 - entrance: 206 - teleporter: [29, 8] - access: [] - - target_room: 98 - entrance: 205 - teleporter: [103, 3] - access: [] -- name: Volcano Cross Right-Left - id: 209 - game_objects: - - name: "Volcano - Crossover Section Box" - object_id: 0x85 - type: "Box" - access: [] - links: - - target_room: 98 - entrance: 208 - teleporter: [104, 3] - access: [] - - target_room: 94 - entrance: 207 - teleporter: [28, 8] - access: [] -- name: Lava Dome Inner Ring Main Loop - id: 100 - game_objects: - - name: "Lava Dome - Exterior Caldera Near Switch Cliff Box" - object_id: 0x88 - type: "Box" - access: [] - - name: "Lava Dome - Exterior South Cliff Box" - object_id: 0x89 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 209 - teleporter: [104, 0] - access: [] - - target_room: 113 - entrance: 211 - teleporter: [105, 0] - access: [] - - target_room: 114 - entrance: 212 - teleporter: [106, 0] - access: [] - - target_room: 116 - entrance: 213 - teleporter: [108, 0] - access: [] - - target_room: 118 - entrance: 214 - teleporter: [111, 0] - access: [] -- name: Lava Dome Inner Ring Center Ledge - id: 101 - game_objects: - - name: "Lava Dome - Exterior Center Dropoff Ledge Box" - object_id: 0x8A - type: "Box" - access: [] - links: - - target_room: 115 - entrance: 215 - teleporter: [107, 0] - access: [] - - target_room: 100 - access: ["Claw"] -- name: Lava Dome Inner Ring Plate Ledge - id: 102 - game_objects: - - name: "Lava Dome Plate" - object_id: 0 - type: "Trigger" - on_trigger: ["LavaDomePlate"] - access: [] - links: - - target_room: 119 - entrance: 216 - teleporter: [109, 0] - access: [] -- name: Lava Dome Inner Ring Upper Ledge West - id: 103 - game_objects: [] - links: - - target_room: 111 - entrance: 219 - teleporter: [112, 0] - access: [] - - target_room: 108 - entrance: 220 - teleporter: [113, 0] - access: [] - - target_room: 104 - access: ["Claw"] - - target_room: 100 - access: ["Claw"] -- name: Lava Dome Inner Ring Upper Ledge East - id: 104 - game_objects: [] - links: - - target_room: 110 - entrance: 218 - teleporter: [110, 0] - access: [] - - target_room: 103 - access: ["Claw"] -- name: Lava Dome Inner Ring Big Door Ledge - id: 105 - game_objects: [] - links: - - target_room: 107 - entrance: 221 - teleporter: [114, 0] - access: [] - - target_room: 121 - entrance: 222 - teleporter: [29, 2] - access: ["LavaDomePlate"] -- name: Lava Dome Inner Ring Tiny Bottom Ledge - id: 106 - game_objects: - - name: "Lava Dome - Exterior Dead End Caldera Box" - object_id: 0x8B - type: "Box" - access: [] - links: - - target_room: 120 - entrance: 226 - teleporter: [115, 0] - access: [] -- name: Lava Dome Jump Maze II - id: 107 - game_objects: - - name: "Lava Dome - Gold Maze Northwest Box" - object_id: 0x8C - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Southwest Box" - object_id: 0xF6 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Northeast Box" - object_id: 0xF7 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze North Box" - object_id: 0xF8 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Center Box" - object_id: 0xF9 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Southeast Box" - object_id: 0xFA - type: "Box" - access: [] - links: - - target_room: 105 - entrance: 227 - teleporter: [116, 0] - access: [] - - target_room: 108 - entrance: 228 - teleporter: [119, 0] - access: [] - - target_room: 120 - entrance: 229 - teleporter: [120, 0] - access: [] -- name: Lava Dome Up-Down Corridor - id: 108 - game_objects: [] - links: - - target_room: 107 - entrance: 231 - teleporter: [118, 0] - access: [] - - target_room: 103 - entrance: 230 - teleporter: [117, 0] - access: [] -- name: Lava Dome Jump Maze I - id: 109 - game_objects: - - name: "Lava Dome - Bare Maze Leapfrog Alcove North Box" - object_id: 0x8D - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Leapfrog Alcove South Box" - object_id: 0x8E - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Center Box" - object_id: 0x8F - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Southwest Box" - object_id: 0x90 - type: "Box" - access: [] - links: - - target_room: 118 - entrance: 232 - teleporter: [121, 0] - access: [] - - target_room: 111 - entrance: 233 - teleporter: [122, 0] - access: [] -- name: Lava Dome Pointless Room - id: 110 - game_objects: [] - links: - - target_room: 104 - entrance: 234 - teleporter: [123, 0] - access: [] -- name: Lava Dome Lower Moon Helm Room - id: 111 - game_objects: - - name: "Lava Dome - U-Bend Room North Box" - object_id: 0x92 - type: "Box" - access: [] - - name: "Lava Dome - U-Bend Room South Box" - object_id: 0x93 - type: "Box" - access: [] - links: - - target_room: 103 - entrance: 235 - teleporter: [124, 0] - access: [] - - target_room: 109 - entrance: 236 - teleporter: [125, 0] - access: [] -- name: Lava Dome Moon Helm Room - id: 112 - game_objects: - - name: "Lava Dome - Beyond River Room Chest" - object_id: 0x13 - type: "Chest" - access: [] - - name: "Lava Dome - Beyond River Room Box" - object_id: 0x91 - type: "Box" - access: [] - links: - - target_room: 117 - entrance: 237 - teleporter: [126, 0] - access: [] -- name: Lava Dome Three Jumps Room - id: 113 - game_objects: - - name: "Lava Dome - Three Jumps Room Box" - object_id: 0x96 - type: "Box" - access: [] - links: - - target_room: 100 - entrance: 238 - teleporter: [127, 0] - access: [] -- name: Lava Dome Life Chest Room Lower Ledge - id: 114 - game_objects: - - name: "Lava Dome - Gold Bar Room Boulder Chest" - object_id: 0x1C - type: "Chest" - access: ["MegaGrenade"] - links: - - target_room: 100 - entrance: 239 - teleporter: [128, 0] - access: [] - - target_room: 115 - access: ["Claw"] -- name: Lava Dome Life Chest Room Upper Ledge - id: 115 - game_objects: - - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box West" - object_id: 0x94 - type: "Box" - access: [] - - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box East" - object_id: 0x95 - type: "Box" - access: [] - links: - - target_room: 101 - entrance: 240 - teleporter: [129, 0] - access: [] - - target_room: 114 - access: ["Claw"] -- name: Lava Dome Big Jump Room Main Area - id: 116 - game_objects: - - name: "Lava Dome - Lava River Room North Box" - object_id: 0x98 - type: "Box" - access: [] - - name: "Lava Dome - Lava River Room East Box" - object_id: 0x99 - type: "Box" - access: [] - - name: "Lava Dome - Lava River Room South Box" - object_id: 0x9A - type: "Box" - access: [] - links: - - target_room: 100 - entrance: 241 - teleporter: [133, 0] - access: [] - - target_room: 119 - entrance: 243 - teleporter: [132, 0] - access: [] - - target_room: 117 - access: ["MegaGrenade"] -- name: Lava Dome Big Jump Room MegaGrenade Area - id: 117 - game_objects: [] - links: - - target_room: 112 - entrance: 242 - teleporter: [131, 0] - access: [] - - target_room: 116 - access: ["Bomb"] -- name: Lava Dome Split Corridor - id: 118 - game_objects: - - name: "Lava Dome - Split Corridor Box" - object_id: 0x97 - type: "Box" - access: [] - links: - - target_room: 109 - entrance: 244 - teleporter: [130, 0] - access: [] - - target_room: 100 - entrance: 245 - teleporter: [134, 0] - access: [] -- name: Lava Dome Plate Corridor - id: 119 - game_objects: [] - links: - - target_room: 102 - entrance: 246 - teleporter: [135, 0] - access: [] - - target_room: 116 - entrance: 247 - teleporter: [137, 0] - access: [] -- name: Lava Dome Four Boxes Stairs - id: 120 - game_objects: - - name: "Lava Dome - Caldera Stairway West Left Box" - object_id: 0x9B - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway West Right Box" - object_id: 0x9C - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway East Left Box" - object_id: 0x9D - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway East Right Box" - object_id: 0x9E - type: "Box" - access: [] - links: - - target_room: 107 - entrance: 248 - teleporter: [136, 0] - access: [] - - target_room: 106 - entrance: 249 - teleporter: [16, 0] - access: [] -- name: Lava Dome Hydra Room - id: 121 - game_objects: - - name: "Lava Dome - Dualhead Hydra Chest" - object_id: 0x14 - type: "Chest" - access: ["DualheadHydra"] - - name: "Dualhead Hydra" - object_id: 0 - type: "Trigger" - on_trigger: ["DualheadHydra"] - access: [] - - name: "Lava Dome - Hydra Room Northwest Box" - object_id: 0x9F - type: "Box" - access: [] - - name: "Lava Dome - Hydra Room Southweast Box" - object_id: 0xA0 - type: "Box" - access: [] - links: - - target_room: 105 - entrance: 250 - teleporter: [105, 3] - access: [] - - target_room: 122 - entrance: 251 - teleporter: [138, 0] - access: ["DualheadHydra"] -- name: Lava Dome Escape Corridor - id: 122 - game_objects: [] - links: - - target_room: 121 - entrance: 253 - teleporter: [139, 0] - access: [] -- name: Rope Bridge - id: 123 - game_objects: - - name: "Rope Bridge - West Box" - object_id: 0xA3 - type: "Box" - access: [] - - name: "Rope Bridge - East Box" - object_id: 0xA4 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 255 - teleporter: [140, 0] - access: [] -- name: Alive Forest - id: 124 - game_objects: - - name: "Alive Forest - Tree Stump Chest" - object_id: 0x15 - type: "Chest" - access: ["Axe"] - - name: "Alive Forest - Near Entrance Box" - object_id: 0xA5 - type: "Box" - access: ["Axe"] - - name: "Alive Forest - After Bridge Box" - object_id: 0xA6 - type: "Box" - access: ["Axe"] - - name: "Alive Forest - Gemini Stump Box" - object_id: 0xA7 - type: "Box" - access: ["Axe"] - links: - - target_room: 226 - entrance: 272 - teleporter: [142, 0] - access: ["Axe"] - - target_room: 21 - entrance: 275 - teleporter: [64, 8] - access: ["LibraCrest", "Axe"] - - target_room: 22 - entrance: 276 - teleporter: [65, 8] - access: ["GeminiCrest", "Axe"] - - target_room: 23 - entrance: 277 - teleporter: [66, 8] - access: ["MobiusCrest", "Axe"] - - target_room: 125 - entrance: 274 - teleporter: [143, 0] - access: ["Axe"] -- name: Giant Tree 1F Main Area - id: 125 - game_objects: - - name: "Giant Tree 1F - Northwest Box" - object_id: 0xA8 - type: "Box" - access: [] - - name: "Giant Tree 1F - Southwest Box" - object_id: 0xA9 - type: "Box" - access: [] - - name: "Giant Tree 1F - Center Box" - object_id: 0xAA - type: "Box" - access: [] - - name: "Giant Tree 1F - East Box" - object_id: 0xAB - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 278 - teleporter: [56, 1] # [49, 8] script restored if no map shuffling - access: [] - - target_room: 202 - access: ["DragonClaw"] -- name: Giant Tree 1F North Island - id: 202 - game_objects: [] - links: - - target_room: 127 - entrance: 280 - teleporter: [144, 0] - access: [] - - target_room: 125 - access: ["DragonClaw"] -- name: Giant Tree 1F Central Island - id: 126 - game_objects: [] - links: - - target_room: 202 - access: ["DragonClaw"] -- name: Giant Tree 2F Main Lobby - id: 127 - game_objects: - - name: "Giant Tree 2F - North Box" - object_id: 0xAC - type: "Box" - access: [] - links: - - target_room: 126 - access: ["DragonClaw"] - - target_room: 125 - entrance: 281 - teleporter: [145, 0] - access: [] - - target_room: 133 - entrance: 283 - teleporter: [149, 0] - access: [] - - target_room: 129 - access: ["DragonClaw"] -- name: Giant Tree 2F West Ledge - id: 128 - game_objects: - - name: "Giant Tree 2F - Dropdown Ledge Box" - object_id: 0xAE - type: "Box" - access: [] - links: - - target_room: 140 - entrance: 284 - teleporter: [147, 0] - access: ["Sword"] - - target_room: 130 - access: ["DragonClaw"] -- name: Giant Tree 2F Lower Area - id: 129 - game_objects: - - name: "Giant Tree 2F - South Box" - object_id: 0xAD - type: "Box" - access: [] - links: - - target_room: 130 - access: ["Claw"] - - target_room: 131 - access: ["Claw"] -- name: Giant Tree 2F Central Island - id: 130 - game_objects: [] - links: - - target_room: 129 - access: ["Claw"] - - target_room: 135 - entrance: 282 - teleporter: [146, 0] - access: ["Sword"] -- name: Giant Tree 2F East Ledge - id: 131 - game_objects: [] - links: - - target_room: 129 - access: ["Claw"] - - target_room: 130 - access: ["DragonClaw"] -- name: Giant Tree 2F Meteor Chest Room - id: 132 - game_objects: - - name: "Giant Tree 2F - Gidrah Chest" - object_id: 0x16 - type: "Chest" - access: [] - links: - - target_room: 133 - entrance: 285 - teleporter: [148, 0] - access: [] -- name: Giant Tree 2F Mushroom Room - id: 133 - game_objects: - - name: "Giant Tree 2F - Mushroom Tunnel West Box" - object_id: 0xAF - type: "Box" - access: ["Axe"] - - name: "Giant Tree 2F - Mushroom Tunnel East Box" - object_id: 0xB0 - type: "Box" - access: ["Axe"] - links: - - target_room: 127 - entrance: 286 - teleporter: [150, 0] - access: ["Axe"] - - target_room: 132 - entrance: 287 - teleporter: [151, 0] - access: ["Axe", "Gidrah"] -- name: Giant Tree 3F Central Island - id: 135 - game_objects: - - name: "Giant Tree 3F - Central Island Box" - object_id: 0xB3 - type: "Box" - access: [] - links: - - target_room: 130 - entrance: 288 - teleporter: [152, 0] - access: [] - - target_room: 136 - access: ["Claw"] - - target_room: 137 - access: ["DragonClaw"] -- name: Giant Tree 3F Central Area - id: 136 - game_objects: - - name: "Giant Tree 3F - Center North Box" - object_id: 0xB1 - type: "Box" - access: [] - - name: "Giant Tree 3F - Center West Box" - object_id: 0xB2 - type: "Box" - access: [] - links: - - target_room: 135 - access: ["Claw"] - - target_room: 127 - access: [] - - target_room: 131 - access: [] -- name: Giant Tree 3F Lower Ledge - id: 137 - game_objects: [] - links: - - target_room: 135 - access: ["DragonClaw"] - - target_room: 142 - entrance: 289 - teleporter: [153, 0] - access: ["Sword"] -- name: Giant Tree 3F West Area - id: 138 - game_objects: - - name: "Giant Tree 3F - West Side Box" - object_id: 0xB4 - type: "Box" - access: [] - links: - - target_room: 128 - access: [] - - target_room: 210 - entrance: 290 - teleporter: [154, 0] - access: [] -- name: Giant Tree 3F Middle Up Island - id: 139 - game_objects: [] - links: - - target_room: 136 - access: ["Claw"] -- name: Giant Tree 3F West Platform - id: 140 - game_objects: [] - links: - - target_room: 139 - access: ["Claw"] - - target_room: 141 - access: ["Claw"] - - target_room: 128 - entrance: 291 - teleporter: [155, 0] - access: [] -- name: Giant Tree 3F North Ledge - id: 141 - game_objects: [] - links: - - target_room: 143 - entrance: 292 - teleporter: [156, 0] - access: ["Sword"] - - target_room: 139 - access: ["Claw"] - - target_room: 136 - access: ["Claw"] -- name: Giant Tree Worm Room Upper Ledge - id: 142 - game_objects: - - name: "Giant Tree 3F - Worm Room North Box" - object_id: 0xB5 - type: "Box" - access: ["Axe"] - - name: "Giant Tree 3F - Worm Room South Box" - object_id: 0xB6 - type: "Box" - access: ["Axe"] - links: - - target_room: 137 - entrance: 293 - teleporter: [157, 0] - access: ["Axe"] - - target_room: 210 - access: ["Axe", "Claw"] -- name: Giant Tree Worm Room Lower Ledge - id: 210 - game_objects: [] - links: - - target_room: 138 - entrance: 294 - teleporter: [158, 0] - access: [] -- name: Giant Tree 4F Lower Floor - id: 143 - game_objects: [] - links: - - target_room: 141 - entrance: 295 - teleporter: [159, 0] - access: [] - - target_room: 148 - entrance: 296 - teleporter: [160, 0] - access: [] - - target_room: 148 - entrance: 297 - teleporter: [161, 0] - access: [] - - target_room: 147 - entrance: 298 - teleporter: [162, 0] - access: ["Sword"] -- name: Giant Tree 4F Middle Floor - id: 144 - game_objects: - - name: "Giant Tree 4F - Highest Platform North Box" - object_id: 0xB7 - type: "Box" - access: [] - - name: "Giant Tree 4F - Highest Platform South Box" - object_id: 0xB8 - type: "Box" - access: [] - links: - - target_room: 149 - entrance: 299 - teleporter: [163, 0] - access: [] - - target_room: 145 - access: ["Claw"] - - target_room: 146 - access: ["DragonClaw"] -- name: Giant Tree 4F Upper Floor - id: 145 - game_objects: [] - links: - - target_room: 150 - entrance: 300 - teleporter: [164, 0] - access: ["Sword"] - - target_room: 144 - access: ["Claw"] -- name: Giant Tree 4F South Ledge - id: 146 - game_objects: - - name: "Giant Tree 4F - Hook Ledge Northeast Box" - object_id: 0xB9 - type: "Box" - access: [] - - name: "Giant Tree 4F - Hook Ledge Southwest Box" - object_id: 0xBA - type: "Box" - access: [] - links: - - target_room: 144 - access: ["DragonClaw"] -- name: Giant Tree 4F Slime Room East Area - id: 147 - game_objects: - - name: "Giant Tree 4F - East Slime Room Box" - object_id: 0xBC - type: "Box" - access: ["Axe"] - links: - - target_room: 143 - entrance: 304 - teleporter: [168, 0] - access: [] -- name: Giant Tree 4F Slime Room West Area - id: 148 - game_objects: [] - links: - - target_room: 143 - entrance: 303 - teleporter: [167, 0] - access: ["Axe"] - - target_room: 143 - entrance: 302 - teleporter: [166, 0] - access: ["Axe"] - - target_room: 149 - access: ["Axe", "Claw"] -- name: Giant Tree 4F Slime Room Platform - id: 149 - game_objects: - - name: "Giant Tree 4F - West Slime Room Box" - object_id: 0xBB - type: "Box" - access: [] - links: - - target_room: 144 - entrance: 301 - teleporter: [165, 0] - access: [] - - target_room: 148 - access: ["Claw"] -- name: Giant Tree 5F Lower Area - id: 150 - game_objects: - - name: "Giant Tree 5F - Northwest Left Box" - object_id: 0xBD - type: "Box" - access: [] - - name: "Giant Tree 5F - Northwest Right Box" - object_id: 0xBE - type: "Box" - access: [] - - name: "Giant Tree 5F - South Left Box" - object_id: 0xBF - type: "Box" - access: [] - - name: "Giant Tree 5F - South Right Box" - object_id: 0xC0 - type: "Box" - access: [] - links: - - target_room: 145 - entrance: 305 - teleporter: [169, 0] - access: [] - - target_room: 151 - access: ["Claw"] - - target_room: 143 - access: [] -- name: Giant Tree 5F Gidrah Platform - id: 151 - game_objects: - - name: "Gidrah" - object_id: 0 - type: "Trigger" - on_trigger: ["Gidrah"] - access: [] - links: - - target_room: 150 - access: ["Claw"] -- name: Kaidge Temple Lower Ledge - id: 152 - game_objects: [] - links: - - target_room: 226 - entrance: 307 - teleporter: [18, 6] - access: [] - - target_room: 153 - access: ["Claw"] -- name: Kaidge Temple Upper Ledge - id: 153 - game_objects: - - name: "Kaidge Temple - Box" - object_id: 0xC1 - type: "Box" - access: [] - links: - - target_room: 185 - entrance: 308 - teleporter: [71, 8] - access: ["MobiusCrest"] - - target_room: 152 - access: ["Claw"] -- name: Windhole Temple - id: 154 - game_objects: - - name: "Windhole Temple - Box" - object_id: 0xC2 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 309 - teleporter: [173, 0] - access: [] -- name: Mount Gale - id: 155 - game_objects: - - name: "Mount Gale - Dullahan Chest" - object_id: 0x17 - type: "Chest" - access: ["DragonClaw", "Dullahan"] - - name: "Dullahan" - object_id: 0 - type: "Trigger" - on_trigger: ["Dullahan"] - access: ["DragonClaw"] - - name: "Mount Gale - East Box" - object_id: 0xC3 - type: "Box" - access: ["DragonClaw"] - - name: "Mount Gale - West Box" - object_id: 0xC4 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 310 - teleporter: [174, 0] - access: [] -- name: Windia - id: 156 - game_objects: [] - links: - - target_room: 226 - entrance: 312 - teleporter: [10, 6] - access: [] - - target_room: 157 - entrance: 320 - teleporter: [30, 5] - access: [] - - target_room: 163 - entrance: 321 - teleporter: [97, 8] - access: [] - - target_room: 165 - entrance: 322 - teleporter: [32, 5] - access: [] - - target_room: 159 - entrance: 323 - teleporter: [176, 4] - access: [] - - target_room: 160 - entrance: 324 - teleporter: [177, 4] - access: [] -- name: Otto's House - id: 157 - game_objects: - - name: "Otto" - object_id: 0 - type: "Trigger" - on_trigger: ["RainbowBridge"] - access: ["ThunderRock"] - links: - - target_room: 156 - entrance: 327 - teleporter: [106, 3] - access: [] - - target_room: 158 - entrance: 326 - teleporter: [33, 2] - access: [] -- name: Otto's Attic - id: 158 - game_objects: - - name: "Windia - Otto's Attic Box" - object_id: 0xC5 - type: "Box" - access: [] - links: - - target_room: 157 - entrance: 328 - teleporter: [107, 3] - access: [] -- name: Windia Kid House - id: 159 - game_objects: [] - links: - - target_room: 156 - entrance: 329 - teleporter: [178, 0] - access: [] - - target_room: 161 - entrance: 330 - teleporter: [180, 0] - access: [] -- name: Windia Old People House - id: 160 - game_objects: [] - links: - - target_room: 156 - entrance: 331 - teleporter: [179, 0] - access: [] - - target_room: 162 - entrance: 332 - teleporter: [181, 0] - access: [] -- name: Windia Kid House Basement - id: 161 - game_objects: [] - links: - - target_room: 159 - entrance: 333 - teleporter: [182, 0] - access: [] - - target_room: 79 - entrance: 334 - teleporter: [44, 8] - access: ["MobiusCrest"] -- name: Windia Old People House Basement - id: 162 - game_objects: - - name: "Windia - Mobius Basement West Box" - object_id: 0xC8 - type: "Box" - access: [] - - name: "Windia - Mobius Basement East Box" - object_id: 0xC9 - type: "Box" - access: [] - links: - - target_room: 160 - entrance: 335 - teleporter: [183, 0] - access: [] - - target_room: 186 - entrance: 336 - teleporter: [43, 8] - access: ["MobiusCrest"] -- name: Windia Inn Lobby - id: 163 - game_objects: [] - links: - - target_room: 156 - entrance: 338 - teleporter: [135, 3] - access: [] - - target_room: 164 - entrance: 337 - teleporter: [102, 8] - access: [] -- name: Windia Inn Beds - id: 164 - game_objects: - - name: "Windia - Inn Bedroom North Box" - object_id: 0xC6 - type: "Box" - access: [] - - name: "Windia - Inn Bedroom South Box" - object_id: 0xC7 - type: "Box" - access: [] - - name: "Windia - Kaeli" - object_id: 15 - type: "NPC" - access: ["Kaeli2"] - links: - - target_room: 163 - entrance: 339 - teleporter: [216, 0] - access: [] -- name: Windia Vendor House - id: 165 - game_objects: - - name: "Windia - Vendor" - object_id: 16 - type: "NPC" - access: [] - links: - - target_room: 156 - entrance: 340 - teleporter: [108, 3] - access: [] -- name: Pazuzu Tower 1F Main Lobby - id: 166 - game_objects: - - name: "Pazuzu 1F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu1F"] - access: [] - links: - - target_room: 226 - entrance: 341 - teleporter: [184, 0] - access: [] - - target_room: 180 - entrance: 345 - teleporter: [185, 0] - access: [] -- name: Pazuzu Tower 1F Boxes Room - id: 167 - game_objects: - - name: "Pazuzu's Tower 1F - Descent Bomb Wall West Box" - object_id: 0xCA - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Bomb Wall Center Box" - object_id: 0xCB - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Bomb Wall East Box" - object_id: 0xCC - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Box" - object_id: 0xCD - type: "Box" - access: [] - links: - - target_room: 169 - entrance: 349 - teleporter: [187, 0] - access: [] -- name: Pazuzu Tower 1F Southern Platform - id: 168 - game_objects: [] - links: - - target_room: 169 - entrance: 346 - teleporter: [186, 0] - access: [] - - target_room: 166 - access: ["DragonClaw"] -- name: Pazuzu 2F - id: 169 - game_objects: - - name: "Pazuzu's Tower 2F - East Room West Box" - object_id: 0xCE - type: "Box" - access: [] - - name: "Pazuzu's Tower 2F - East Room East Box" - object_id: 0xCF - type: "Box" - access: [] - - name: "Pazuzu 2F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu2FLock"] - access: ["Axe"] - - name: "Pazuzu 2F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu2F"] - access: ["Bomb"] - links: - - target_room: 183 - entrance: 350 - teleporter: [188, 0] - access: [] - - target_room: 168 - entrance: 351 - teleporter: [189, 0] - access: [] - - target_room: 167 - entrance: 352 - teleporter: [190, 0] - access: [] - - target_room: 171 - entrance: 353 - teleporter: [191, 0] - access: [] -- name: Pazuzu 3F Main Room - id: 170 - game_objects: - - name: "Pazuzu's Tower 3F - Guest Room West Box" - object_id: 0xD0 - type: "Box" - access: [] - - name: "Pazuzu's Tower 3F - Guest Room East Box" - object_id: 0xD1 - type: "Box" - access: [] - - name: "Pazuzu 3F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu3F"] - access: [] - links: - - target_room: 180 - entrance: 356 - teleporter: [192, 0] - access: [] - - target_room: 181 - entrance: 357 - teleporter: [193, 0] - access: [] -- name: Pazuzu 3F Central Island - id: 171 - game_objects: [] - links: - - target_room: 169 - entrance: 360 - teleporter: [194, 0] - access: [] - - target_room: 170 - access: ["DragonClaw"] - - target_room: 172 - access: ["DragonClaw"] -- name: Pazuzu 3F Southern Island - id: 172 - game_objects: - - name: "Pazuzu's Tower 3F - South Ledge Box" - object_id: 0xD2 - type: "Box" - access: [] - links: - - target_room: 173 - entrance: 361 - teleporter: [195, 0] - access: [] - - target_room: 171 - access: ["DragonClaw"] -- name: Pazuzu 4F - id: 173 - game_objects: - - name: "Pazuzu's Tower 4F - Elevator West Box" - object_id: 0xD3 - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 4F - Elevator East Box" - object_id: 0xD4 - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 4F - East Storage Room Chest" - object_id: 0x18 - type: "Chest" - access: [] - - name: "Pazuzu 4F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu4FLock"] - access: ["Axe"] - - name: "Pazuzu 4F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu4F"] - access: ["Bomb"] - links: - - target_room: 183 - entrance: 362 - teleporter: [196, 0] - access: [] - - target_room: 184 - entrance: 363 - teleporter: [197, 0] - access: [] - - target_room: 172 - entrance: 364 - teleporter: [198, 0] - access: [] - - target_room: 175 - entrance: 365 - teleporter: [199, 0] - access: [] -- name: Pazuzu 5F Pazuzu Loop - id: 174 - game_objects: - - name: "Pazuzu 5F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu5F"] - access: [] - links: - - target_room: 181 - entrance: 368 - teleporter: [200, 0] - access: [] - - target_room: 182 - entrance: 369 - teleporter: [201, 0] - access: [] -- name: Pazuzu 5F Upper Loop - id: 175 - game_objects: - - name: "Pazuzu's Tower 5F - North Box" - object_id: 0xD5 - type: "Box" - access: [] - - name: "Pazuzu's Tower 5F - South Box" - object_id: 0xD6 - type: "Box" - access: [] - links: - - target_room: 173 - entrance: 370 - teleporter: [202, 0] - access: [] - - target_room: 176 - entrance: 371 - teleporter: [203, 0] - access: [] -- name: Pazuzu 6F - id: 176 - game_objects: - - name: "Pazuzu's Tower 6F - Box" - object_id: 0xD7 - type: "Box" - access: [] - - name: "Pazuzu's Tower 6F - Chest" - object_id: 0x19 - type: "Chest" - access: [] - - name: "Pazuzu 6F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu6FLock"] - access: ["Bomb", "Axe"] - - name: "Pazuzu 6F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu6F"] - access: ["Bomb"] - links: - - target_room: 184 - entrance: 374 - teleporter: [204, 0] - access: [] - - target_room: 175 - entrance: 375 - teleporter: [205, 0] - access: [] - - target_room: 178 - entrance: 376 - teleporter: [206, 0] - access: [] - - target_room: 178 - entrance: 377 - teleporter: [207, 0] - access: [] -- name: Pazuzu 7F Southwest Area - id: 177 - game_objects: [] - links: - - target_room: 182 - entrance: 380 - teleporter: [26, 0] - access: [] - - target_room: 178 - access: ["DragonClaw"] -- name: Pazuzu 7F Rest of the Area - id: 178 - game_objects: [] - links: - - target_room: 177 - access: ["DragonClaw"] - - target_room: 176 - entrance: 381 - teleporter: [27, 0] - access: [] - - target_room: 176 - entrance: 382 - teleporter: [28, 0] - access: [] - - target_room: 179 - access: ["DragonClaw", "Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] -- name: Pazuzu 7F Sky Room - id: 179 - game_objects: - - name: "Pazuzu's Tower 7F - Pazuzu Chest" - object_id: 0x1A - type: "Chest" - access: [] - - name: "Pazuzu" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu"] - access: ["Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] - links: - - target_room: 178 - access: ["DragonClaw"] -- name: Pazuzu 1F to 3F - id: 180 - game_objects: [] - links: - - target_room: 166 - entrance: 385 - teleporter: [29, 0] - access: [] - - target_room: 170 - entrance: 386 - teleporter: [30, 0] - access: [] -- name: Pazuzu 3F to 5F - id: 181 - game_objects: [] - links: - - target_room: 170 - entrance: 387 - teleporter: [40, 0] - access: [] - - target_room: 174 - entrance: 388 - teleporter: [41, 0] - access: [] -- name: Pazuzu 5F to 7F - id: 182 - game_objects: [] - links: - - target_room: 174 - entrance: 389 - teleporter: [38, 0] - access: [] - - target_room: 177 - entrance: 390 - teleporter: [39, 0] - access: [] -- name: Pazuzu 2F to 4F - id: 183 - game_objects: [] - links: - - target_room: 169 - entrance: 391 - teleporter: [21, 0] - access: [] - - target_room: 173 - entrance: 392 - teleporter: [22, 0] - access: [] -- name: Pazuzu 4F to 6F - id: 184 - game_objects: [] - links: - - target_room: 173 - entrance: 393 - teleporter: [2, 0] - access: [] - - target_room: 176 - entrance: 394 - teleporter: [3, 0] - access: [] -- name: Light Temple - id: 185 - game_objects: - - name: "Light Temple - Box" - object_id: 0xD8 - type: "Box" - access: [] - links: - - target_room: 230 - entrance: 395 - teleporter: [19, 6] - access: [] - - target_room: 153 - entrance: 396 - teleporter: [70, 8] - access: ["MobiusCrest"] -- name: Ship Dock - id: 186 - game_objects: - - name: "Ship Dock Access" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipDockAccess"] - access: [] - links: - - target_room: 228 - entrance: 399 - teleporter: [17, 6] - access: [] - - target_room: 162 - entrance: 397 - teleporter: [61, 8] - access: ["MobiusCrest"] -- name: Mac Ship Deck - id: 187 - game_objects: - - name: "Mac Ship Steering Wheel" - object_id: 00 - type: "Trigger" - on_trigger: ["ShipSteeringWheel"] - access: [] - - name: "Mac's Ship Deck - North Box" - object_id: 0xD9 - type: "Box" - access: [] - - name: "Mac's Ship Deck - Center Box" - object_id: 0xDA - type: "Box" - access: [] - - name: "Mac's Ship Deck - South Box" - object_id: 0xDB - type: "Box" - access: [] - links: - - target_room: 229 - entrance: 400 - teleporter: [37, 8] - access: [] - - target_room: 188 - entrance: 401 - teleporter: [50, 8] - access: [] - - target_room: 188 - entrance: 402 - teleporter: [51, 8] - access: [] - - target_room: 188 - entrance: 403 - teleporter: [52, 8] - access: [] - - target_room: 189 - entrance: 404 - teleporter: [53, 8] - access: [] -- name: Mac Ship B1 Outer Ring - id: 188 - game_objects: - - name: "Mac's Ship B1 - Northwest Hook Platform Box" - object_id: 0xE4 - type: "Box" - access: ["DragonClaw"] - - name: "Mac's Ship B1 - Center Hook Platform Box" - object_id: 0xE5 - type: "Box" - access: ["DragonClaw"] - links: - - target_room: 187 - entrance: 405 - teleporter: [208, 0] - access: [] - - target_room: 187 - entrance: 406 - teleporter: [175, 0] - access: [] - - target_room: 187 - entrance: 407 - teleporter: [172, 0] - access: [] - - target_room: 193 - entrance: 408 - teleporter: [88, 0] - access: [] - - target_room: 193 - access: [] -- name: Mac Ship B1 Square Room - id: 189 - game_objects: [] - links: - - target_room: 187 - entrance: 409 - teleporter: [141, 0] - access: [] - - target_room: 192 - entrance: 410 - teleporter: [87, 0] - access: [] -- name: Mac Ship B1 Central Corridor - id: 190 - game_objects: - - name: "Mac's Ship B1 - Central Corridor Box" - object_id: 0xE6 - type: "Box" - access: [] - links: - - target_room: 192 - entrance: 413 - teleporter: [86, 0] - access: [] - - target_room: 191 - entrance: 412 - teleporter: [102, 0] - access: [] - - target_room: 193 - access: [] -- name: Mac Ship B2 South Corridor - id: 191 - game_objects: [] - links: - - target_room: 190 - entrance: 415 - teleporter: [55, 8] - access: [] - - target_room: 194 - entrance: 414 - teleporter: [57, 1] - access: [] -- name: Mac Ship B2 North Corridor - id: 192 - game_objects: [] - links: - - target_room: 190 - entrance: 416 - teleporter: [56, 8] - access: [] - - target_room: 189 - entrance: 417 - teleporter: [57, 8] - access: [] -- name: Mac Ship B2 Outer Ring - id: 193 - game_objects: - - name: "Mac's Ship B2 - Barrel Room South Box" - object_id: 0xDF - type: "Box" - access: [] - - name: "Mac's Ship B2 - Barrel Room North Box" - object_id: 0xE0 - type: "Box" - access: [] - - name: "Mac's Ship B2 - Southwest Room Box" - object_id: 0xE1 - type: "Box" - access: [] - - name: "Mac's Ship B2 - Southeast Room Box" - object_id: 0xE2 - type: "Box" - access: [] - links: - - target_room: 188 - entrance: 418 - teleporter: [58, 8] - access: [] -- name: Mac Ship B1 Mac Room - id: 194 - game_objects: - - name: "Mac's Ship B1 - Mac Room Chest" - object_id: 0x1B - type: "Chest" - access: [] - - name: "Captain Mac" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipLoaned"] - access: ["CaptainCap"] - links: - - target_room: 191 - entrance: 424 - teleporter: [101, 0] - access: [] -- name: Doom Castle Corridor of Destiny - id: 195 - game_objects: [] - links: - - target_room: 201 - entrance: 428 - teleporter: [84, 0] - access: [] - - target_room: 196 - entrance: 429 - teleporter: [35, 2] - access: [] - - target_room: 197 - entrance: 430 - teleporter: [209, 0] - access: ["StoneGolem"] - - target_room: 198 - entrance: 431 - teleporter: [211, 0] - access: ["StoneGolem", "TwinheadWyvern"] - - target_room: 199 - entrance: 432 - teleporter: [13, 2] - access: ["StoneGolem", "TwinheadWyvern", "Zuh"] -- name: Doom Castle Ice Floor - id: 196 - game_objects: - - name: "Doom Castle 4F - Northwest Room Box" - object_id: 0xE7 - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Doom Castle 4F - Southwest Room Box" - object_id: 0xE8 - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Doom Castle 4F - Northeast Room Box" - object_id: 0xE9 - type: "Box" - access: ["Sword"] - - name: "Doom Castle 4F - Southeast Room Box" - object_id: 0xEA - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Stone Golem" - object_id: 0 - type: "Trigger" - on_trigger: ["StoneGolem"] - access: ["Sword", "DragonClaw"] - links: - - target_room: 195 - entrance: 433 - teleporter: [109, 3] - access: [] -- name: Doom Castle Lava Floor - id: 197 - game_objects: - - name: "Doom Castle 5F - North Left Box" - object_id: 0xEB - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - North Right Box" - object_id: 0xEC - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - South Left Box" - object_id: 0xED - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - South Right Box" - object_id: 0xEE - type: "Box" - access: ["DragonClaw"] - - name: "Twinhead Wyvern" - object_id: 0 - type: "Trigger" - on_trigger: ["TwinheadWyvern"] - access: ["DragonClaw"] - links: - - target_room: 195 - entrance: 434 - teleporter: [210, 0] - access: [] -- name: Doom Castle Sky Floor - id: 198 - game_objects: - - name: "Doom Castle 6F - West Box" - object_id: 0xEF - type: "Box" - access: [] - - name: "Doom Castle 6F - East Box" - object_id: 0xF0 - type: "Box" - access: [] - - name: "Zuh" - object_id: 0 - type: "Trigger" - on_trigger: ["Zuh"] - access: ["DragonClaw"] - links: - - target_room: 195 - entrance: 435 - teleporter: [212, 0] - access: [] - - target_room: 197 - access: [] -- name: Doom Castle Hero Room - id: 199 - game_objects: - - name: "Doom Castle Hero Chest 01" - object_id: 0xF2 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 02" - object_id: 0xF3 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 03" - object_id: 0xF4 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 04" - object_id: 0xF5 - type: "Chest" - access: [] - links: - - target_room: 200 - entrance: 436 - teleporter: [54, 0] - access: [] - - target_room: 195 - entrance: 441 - teleporter: [110, 3] - access: [] -- name: Doom Castle Dark King Room - id: 200 - game_objects: [] - links: - - target_room: 199 - entrance: 442 - teleporter: [52, 0] - access: [] diff --git a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md index a652d4e5..e1316b65 100644 --- a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md +++ b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md @@ -1,5 +1,8 @@ # Final Fantasy Mystic Quest +## Game page in other languages: +* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr) + ## Where is the options page? The [player options page for this game](../player-options) contains all the options you need to configure and export a diff --git a/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md new file mode 100644 index 00000000..70c2d938 --- /dev/null +++ b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md @@ -0,0 +1,36 @@ +# Final Fantasy Mystic Quest + +## Page d'info dans d'autres langues : +* [English](/games/Final%20Fantasy%20Mystic%20Quest/info/en) + +## OÚ se situe la page d'options? + +La [page de configuration](../player-options) contient toutes les options nÊcessaires pour crÊer un fichier de configuration. + +## Qu'est-ce qui est rendu alÊatoire dans ce jeu? + +Outre les objets mÊlangÊs, il y a plusieurs options pour aussi mÊlanger les villes et donjons, les pièces dans les donjons, les tÊlÊporteurs et les champs de bataille. +Il y a aussi plusieurs autres options afin d'ajuster la difficultÊ du jeu et la vitesse d'une partie. + +## Quels objets et emplacements sont mÊlangÊs? + +Les objets normalement reçus des coffres rouges, des PNJ et des champs de bataille sont mÊlangÊs. Vous pouvez aussi +inclure les objets des coffres bruns (qui contiennent normalement des consommables) dans les objets mÊlangÊs. + +## Quels objets peuvent ÃĒtre dans les mondes des autres joueurs? + +Tous les objets qui ont ÊtÊ dÊterminÊs mÊlangÊs dans les options peuvent ÃĒtre placÊs dans d'autres mondes. + +## À quoi ressemblent les objets des autres joueurs dans Final Fantasy Mystic Quest? + +Les emplacements qui Êtaient à l'origine des coffres (rouges ou bruns si ceux-ci sont inclus) apparaÃŽtront comme des coffres. +Les coffres rouges seront des objets utiles ou de progression, alors que les coffres bruns seront des objets de remplissage. +Les pièges peuvent apparaÃŽtre comme des coffres rouges ou bruns. +Lorsque vous ouvrirez un coffre contenant un objet d'un autre joueur, vous recevrez l'icône d'Archipelago et +la boÃŽte de dialogue vous indiquera avoir reçu un "Archipelago Item". + + +## Lorsqu'un joueur reçoit un objet, qu'arrive-t-il? + +Une boÃŽte de dialogue apparaÃŽtra pour vous montrer l'objet que vous avez reçu. Vous ne pourrez pas recevoir d'objet si vous ÃĒtes +en combat, dans la mappemonde ou dans les menus (à l'exception de lorsque vous fermez le menu). diff --git a/worlds/ffmq/docs/setup_en.md b/worlds/ffmq/docs/setup_en.md index 35d775f1..77569c93 100644 --- a/worlds/ffmq/docs/setup_en.md +++ b/worlds/ffmq/docs/setup_en.md @@ -17,6 +17,12 @@ The Archipelago community cannot supply you with this. ## Installation Procedures +### Linux Setup + +1. Download and install [Archipelago](). **The installer + file is located in the assets section at the bottom of the version information. You'll likely be looking for the `.AppImage`.** +2. It is recommended to use either RetroArch or BizHawk if you run on linux, as snes9x-rr isn't compatible. + ### Windows Setup 1. Download and install [Archipelago](). **The installer @@ -75,8 +81,7 @@ Manually launch the SNI Client, and run the patched ROM in your chosen software #### With an emulator -When the client launched automatically, SNI should have also automatically launched in the background. If this is its -first time launching, you may be prompted to allow it to communicate through the Windows Firewall. +If this is the first time SNI launches, you may be prompted to allow it to communicate through the Windows Firewall. ##### snes9x-rr @@ -133,10 +138,10 @@ page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platfor ### Connect to the Archipelago Server -The patch file which launched your client should have automatically connected you to the AP Server. There are a few -reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the -client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it -into the "Server" input field then press enter. +SNI serves as the interface between your emulator and the server. Since you launched it manually, you need to tell it what server to connect to. +If the server is hosted on Archipelago.gg, get the port the server hosts your game on at the top of the game room (last line before the worlds are listed). +In the SNI client, either type `/connect address` (where `address` is the address of the server, for example `/connect archipelago.gg:12345`), or type the address and port on the "Server" input field, then press `Connect`. +If the server is hosted locally, simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press `Connect`. The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". diff --git a/worlds/ffmq/docs/setup_fr.md b/worlds/ffmq/docs/setup_fr.md new file mode 100644 index 00000000..12ea41c6 --- /dev/null +++ b/worlds/ffmq/docs/setup_fr.md @@ -0,0 +1,178 @@ +# Final Fantasy Mystic Quest Setup Guide + +## Logiciels requis + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- Une solution logicielle ou matÊrielle capable de charger et de lancer des fichiers ROM de SNES + - Un Êmulateur capable d'ÊxÊcuter des scripts Lua + - snes9x-rr de: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html), + - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Ou, + - Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matÊrielle + compatible +- Le fichier ROM de la v1.0 ou v1.1 NA de Final Fantasy Mystic Quest obtenu lÊgalement, sÃģrement nommÊ `Final Fantasy - Mystic Quest (U) (V1.0).sfc` ou `Final Fantasy - Mystic Quest (U) (V1.1).sfc` +La communautÊ d'Archipelago ne peut vous fournir avec ce fichier. + +## ProcÊdure d'installation + +### Installation sur Linux + +1. TÊlÊchargez et installez [Archipelago](). +** Le fichier d'installation est situÊ dans la section "assets" dans le bas de la fenÃĒtre d'information de la version. Vous voulez probablement le `.AppImage`** +2. L'utilisation de RetroArch ou BizHawk est recommandÊ pour les utilisateurs linux, puisque snes9x-rr n'est pas compatible. + +### Installation sur Windows + +1. TÊlÊchargez et installez [Archipelago](). +** Le fichier d'installation est situÊ dans la section "assets" dans le bas de la fenÃĒtre d'information de la version.** +2. Si vous utilisez un Êmulateur, il est recommandÊ d'assigner votre Êmulateur capable d'ÊxÊcuter des scripts Lua comme + programme par dÊfaut pour ouvrir vos ROMs. + 1. Extrayez votre dossier d'Êmulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez. + 2. Faites un clic droit sur un fichier ROM et sÊlectionnez **Ouvrir avec...** + 3. Cochez la case à côtÊ de **Toujours utiliser cette application pour ouvrir les fichiers `.sfc`** + 4. Descendez jusqu'en bas de la liste et sÊlectionnez **Rechercher une autre application sur ce PC** + 5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre Êmulateur et choisissez **Ouvrir**. Ce fichier + devrait se trouver dans le dossier que vous avez extrait à la première Êtape. + + +## CrÊer son fichier de configuration (.yaml) + +### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ? + +Votre fichier de configuration contient un ensemble d'options de configuration pour indiquer au gÊnÊrateur +comment il devrait gÊnÊrer votre seed. Chaque joueur d'un multiworld devra fournir son propre fichier de configuration. Cela permet +à chaque joueur d'apprÊcier une expÊrience personalisÊe. Les diffÊrents joueurs d'un mÃĒme multiworld +pouront avoir des options de gÊnÊration diffÊrentes. +Vous pouvez lire le [guide pour crÊer un YAML de base](/tutorial/Archipelago/setup/en) en anglais. + +### OÚ est-ce que j'obtiens un fichier de configuration ? + +La [page d'options sur le site](/games/Final%20Fantasy%20Mystic%20Quest/player-options) vous permet de choisir vos +options de gÊnÊration et de les exporter vers un fichier de configuration. +Il vous est aussi possible de trouver le fichier de configuration modèle de Mystic Quest dans votre rÊpertoire d'installation d'Archipelago, +dans le dossier Players/Templates. + +### VÊrifier son fichier de configuration + +Si vous voulez valider votre fichier de configuration pour ÃĒtre sÃģr qu'il fonctionne, vous pouvez le vÊrifier sur la page du +[Validateur de YAML](/mysterycheck). + +## GÊnÊrer une partie pour un joueur + +1. Aller sur la page [GÊnÊration de partie](/games/Final%20Fantasy%20Mystic%20Quest/player-options), configurez vos options, + et cliquez sur le bouton "Generate Game". +2. Il vous sera alors prÊsentÊ une page d'informations sur la seed +3. Cliquez sur le lien "Create New Room". +4. Vous verrez s'afficher la page du server, de laquelle vous pourrez tÊlÊcharger votre fichier patch `.apmq`. +5. Rendez-vous sur le [site FFMQR](https://ffmqrando.net/Archipelago). +Sur cette page, sÊlectionnez votre ROM Final Fantasy Mystic Quest original dans le boÃŽte "ROM", puis votre ficher patch `.apmq` dans la boÃŽte "Load Archipelago Config File". +Cliquez sur "Generate". Un tÊlÊchargement avec votre ROM alÊatoire devrait s'amorcer. +6. Puisque cette partie est à un seul joueur, vous n'avez plus besoin du client Archipelago ni du serveur, sentez-vous libre de les fermer. + +## Rejoindre un MultiWorld + +### Obtenir son patch et crÊer sa ROM + +Quand vous rejoignez un multiworld, il vous sera demandÊ de fournir votre fichier de configuration à celui qui hÊberge la partie ou +s'occupe de la gÊnÊration. Une fois cela fait, l'hôte vous fournira soit un lien pour tÊlÊcharger votre patch, soit un +fichier `.zip` contenant les patchs de tous les joueurs. Votre patch devrait avoir l'extension `.apmq`. + +Allez au [site FFMQR](https://ffmqrando.net/Archipelago) et sÊlectionnez votre ROM Final Fantasy Mystic Quest original dans le boÃŽte "ROM", puis votre ficher patch `.apmq` dans la boÃŽte "Load Archipelago Config File". +Cliquez sur "Generate". Un tÊlÊchargement avec votre ROM alÊatoire devrait s'amorcer. + +Ouvrez le client SNI (sur Windows ArchipelagoSNIClient.exe, sur Linux ouvrez le `.appImage` puis cliquez sur SNI Client), puis ouvrez le ROM tÊlÊchargÊ avec votre Êmulateur choisi. + +### Se connecter au client + +#### Avec un Êmulateur + +Quand le client se lance automatiquement, QUsb2Snes devrait Êgalement se lancer automatiquement en arrière-plan. Si +c'est la première fois qu'il dÊmarre, il vous sera peut-ÃĒtre demandÊ de l'autoriser à communiquer à travers le pare-feu +Windows. + +##### snes9x-rr + +1. Chargez votre ROM si ce n'est pas dÊjà fait. +2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting** +3. Cliquez alors sur **New Lua Script Window...** +4. Dans la nouvelle fenÃĒtre, sÊlectionnez **Browse...** +5. SÊlectionnez le fichier connecteur lua fourni avec votre client + - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dÊpendemment de si votre emulateur + est 64-bit ou 32-bit. +6. Si vous obtenez une erreur `socket.dll missing` ou une erreur similaire lorsque vous chargez le script lua, vous devez naviguer dans le dossier +contenant le script lua, puis copier le fichier `socket.dll` dans le dossier d'installation de votre emulateur snes9x. + +##### BizHawk + +1. Assurez vous d'avoir le coeur BSNES chargÊ. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant + ces options de menu : + `Config --> Cores --> SNES --> BSNES` + Une fois le coeur changÊ, vous devez redÊmarrer BizHawk. +2. Chargez votre ROM si ce n'est pas dÊjà fait. +3. Cliquez sur le menu "Tools" et cliquez sur **Lua Console** +4. Cliquez sur le bouton pour ouvrir un nouveau script Lua, soit par le bouton avec un icône "Ouvrir un dossier", + en cliquant `Open Script...` dans le menu Script ou en appuyant sur `ctrl-O`. +5. SÊlectionnez le fichier `Connector.lua` inclus avec le client + - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dÊpendemment de si votre emulateur + est 64-bit ou 32-bit. Notez que les versions les plus rÊcentes de BizHawk ne sont que 64-bit. + +##### RetroArch 1.10.1 ou plus rÊcent + +Vous ne devez faire ces Êtapes qu'une fois. À noter que RetroArch 1.9.x ne fonctionnera pas puisqu'il s'agit d'une version moins rÊcente que 1.10.1. + +1. Entrez dans le menu principal de RetroArch. +2. Allez dans Settings --> User Interface. Activez l'option "Show Advanced Settings". +3. Allez dans Settings --> Network. Activez l'option "Network Commands", qui se trouve sous "Request Device 16". + Laissez le "Network Command Port" à sa valeur par defaut, qui devrait ÃĒtre 55355. + + +![Capture d'Êcran du menu Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Allez dans le Menu Principal --> Online Updater --> Core Downloader. Trouvez et sÊlectionnez "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +Lorsque vous chargez un ROM pour Archipelago, assurez vous de toujours sÊlectionner le coeur **bsnes-mercury**. +Ce sont les seuls coeurs qui permettent à des outils extÊrieurs de lire les donnÊes du ROM. + +#### Avec une solution matÊrielle + +Ce guide suppose que vous avez tÊlÊchargÊ le bon micro-logiciel pour votre appareil. Si ce n'est pas dÊjà le cas, faites +le maintenant. Les utilisateurs de SD2SNES et de FXPak Pro peuvent tÊlÊcharger le micro-logiciel appropriÊ +[ici](https://github.com/RedGuyyyy/sd2snes/releases). Pour les autres solutions, de l'aide peut ÃĒtre trouvÊe +[sur cette page](http://usb2snes.com/#supported-platforms). + +1. Fermez votre Êmulateur, qui s'est potentiellement lancÊ automatiquement. +2. Ouvrez votre appareil et chargez le ROM. + +### Se connecter au MultiServer + +Puisque vous avez lancÊ SNI manuellement, vous devrez probablement lui indiquer l'adresse à laquelle il doit se connecter. +Si le serveur est hÊbergÊ sur le site d'Archipelago, vous verrez l'adresse à laquelle vous connecter dans le haut de la page, dernière ligne avant la liste des mondes. +Tapez `/connect adresse` (ou le "adresse" est remplacÊ par l'adresse archipelago, par exemple `/connect archipelago.gg:12345`) dans la boÃŽte de commande au bas de votre client SNI, ou encore Êcrivez l'adresse dans la boÃŽte "server" dans le haut du client, puis cliquez `Connect`. +Si le serveur n'est pas hÊbergÊ sur le site d'Archipelago, demandez à l'hôte l'adresse du serveur, puis tapez `/connect adresse` (ou "adresse" est remplacÊ par l'adresse fourni par l'hôte) ou copiez/collez cette adresse dans le champ "Server" puis appuyez sur "Connect". + +Le client essaiera de vous reconnecter à la nouvelle adresse du serveur, et devrait mentionner "Server Status: +Connected". Si le client ne se connecte pas après quelques instants, il faudra peut-ÃĒtre rafraÃŽchir la page de +l'interface Web. + +### Jouer au jeu + +Une fois que l'interface Web affiche que la SNES et le serveur sont connectÊs, vous ÃĒtes prÃĒt à jouer. FÊlicitations +pour avoir rejoint un multiworld ! + +## HÊberger un MultiWorld + +La mÊthode recommandÊe pour hÊberger une partie est d'utiliser le service d'hÊbergement fourni par +Archipelago. Le processus est relativement simple : + +1. RÊcupÊrez les fichiers de configuration (.yaml) des joueurs. +2. CrÊez une archive zip contenant ces fichiers de configuration. +3. TÊlÊversez l'archive zip sur le lien ci-dessous. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Attendez un moment que la seed soit gÊnÊrÊe. +5. Lorsque la seed est gÊnÊrÊe, vous serez redirigÊ vers une page d'informations "Seed Info". +6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres + joueurs afin qu'ils puissent rÊcupÊrer leurs patchs. +7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez Êgalement + fournir ce lien aux joueurs pour qu'ils puissent suivre la progression de la partie. N'importe quelle personne voulant + observer devrait avoir accès à ce lien. +8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer. diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index e930c4b8..31d725bf 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/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 37467eeb..e78eb915 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -102,10 +102,10 @@ See the plando guide for more info on plando options. Plando guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) * `accessibility` determines the level of access to the game the generation will expect you to have in order to reach - your completion goal. This supports `items`, `locations`, and `minimal` and is set to `locations` by default. - * `locations` will guarantee all locations are accessible in your world. + your completion goal. This supports `full`, `items`, and `minimal` and is set to `full` by default. + * `full` will guarantee all locations are accessible in your world. * `items` will guarantee you can acquire all logically relevant items in your world. Some items, such as keys, may - be self-locking. + be self-locking. This value only exists in and affects some worlds. * `minimal` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon. @@ -131,8 +131,8 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) the location without using any hint points. * `start_location_hints` is the same as `start_hints` but for locations, allowing you to hint for the item contained there without using any hint points. -* `exclude_locations` lets you define any locations that you don't want to do and forces a filler or trap item which - isn't necessary for progression into these locations. +* `exclude_locations` lets you define any locations that you don't want to do and prevents items classified as + "progression" or "useful" from being placed on them. * `priority_locations` lets you define any locations that you want to do and forces a progression item into these locations. * `item_links` allows players to link their items into a group with the same item link name and game. The items declared diff --git a/worlds/generic/docs/commands_en.md b/worlds/generic/docs/commands_en.md index 317f7241..3848bb4c 100644 --- a/worlds/generic/docs/commands_en.md +++ b/worlds/generic/docs/commands_en.md @@ -27,6 +27,7 @@ including the exclamation point. - `!countdown ` Starts a countdown using the given seconds value. Useful for synchronizing starts. Defaults to 10 seconds if no argument is provided. - `!alias ` Sets your alias, which allows you to use commands with the alias rather than your provided name. + `!alias` on its own will reset the alias to the player's original name. - `!admin ` Executes a command as if you typed it into the server console. Remote administration must be enabled. @@ -65,6 +66,7 @@ including the exclamation point. argument is provided. - `/option