Compare commits

..

58 Commits

Author SHA1 Message Date
Uriel e6e0bc3042 WebHost: Config option for custom port ranges v2 (#6009)
* Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random.

* - Added better fallback to default port range when a custom range fails
- Updated config to be clearer

* Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random.

* - Added better fallback to default port range when a custom range fails
- Updated config to be clearer

* Updated soft-fail message

* Removed dead import from customserver.py

* Update requirements.txt

Settings requirements to main core branch

* fix what reviewers said and add some improvements

* remove unused argument

* try fixing test with try

* use yaml lists instead of string for config

* fix value type bug on ephemeral type

* reuse sockets with websockets api instead of opening and closing them

* add used ports cache and filter used ports when looking for ports

* fix port randomizer

* Apply suggestions from code review

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* fix some reviews

* use weights for random port and remove more-itertools

* fix net_connections not working on macOS

* rename variables and functions

* lazy init `get_used_ports`

* change `game_ports` to be `tuple`

* fix last_used_ports not being updated locally

* fix random choices and move game_port conversion into tuple

* Apply suggestions from code review

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* use a named tuple on parse_game_ports

* only use ranges

* do it the duck way

* this should check all usable ports before failing

* fix while loop

* add return type to weighted random

* Update WebHostLib/customserver.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* simplify tuple conversion check

* add tests

* reformat file and change `create_random_port_socket` test

* add more test cases for parse_game_ports

* try to prevent busy-looping on create random port socket when doing test

* simplify parse game port tests to one assertListEqual

* make the range lesser for port test

* reduce range on macOS

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update WebHostLib/customserver.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* remove unused import

* Update WebHostLib/customserver.py

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* use generator expressions

* check for 0-tuple

* use some kind of shuffled queue

* update tests

* refactor new port handling into a class (#1)

* change time to monotonic

* Update docs/webhost configuration sample.yaml

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* add psutil 7.2.2 as requirement

* Update WebHostLib/requirements.txt

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: Lexipherous <jasonnlelong@gmail.com>
Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2026-06-20 17:36:18 +02:00
Emily 5ccef9802a WebHost: fix Authors on supported game page (#6218) 2026-06-07 18:21:30 +02:00
CosmicWolf 09aba95c03 Factorio: Fixed a needlessly small bug that does not affect gameplay (#6147) 2026-06-03 17:53:37 +02:00
Ishigh1 2a8ebe2399 Core+RB: Make has_from_list, has_from_list_unique, has_group and has_group_unique return True if count <= 0 and list is empty (#6240) 2026-06-03 08:20:04 +02:00
Ishigh1 ad0af18f51 Rule Builder: Resolve empty and() to True_() instead of False_()(#6239) 2026-06-02 09:10:43 +02:00
Justus Lind 65277a50ce Muse Dash: Update to Touhou Mugakudan -V- (#6234) 2026-06-01 22:38:23 +02:00
dependabot[bot] d241829585 Bump mistune from 3.2.0 to 3.2.1 in /WebHostLib (#6185)
Bumps [mistune](https://github.com/lepture/mistune) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/lepture/mistune/releases)
- [Changelog](https://github.com/lepture/mistune/blob/main/docs/changes.rst)
- [Commits](https://github.com/lepture/mistune/compare/v3.2.0...v3.2.1)

---
updated-dependencies:
- dependency-name: mistune
  dependency-version: 3.2.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 23:55:57 +02:00
CosmicWolf b24b3d10b6 Factorio: Locale changes (#5908)
---------

Co-authored-by: Joris Santen <joris.j.vansanten@student.hu.nl>
Co-authored-by: lepideble <147614625+lepideble@users.noreply.github.com>
2026-05-31 01:49:39 +02:00
Fabian Dill 787ddc400c Core: Bump version from 0.6.7 to 0.6.8 (#6114) 2026-05-27 03:26:30 +02:00
qwint 545171f3f4 Core: reset log level when using pyximport (#6212) 2026-05-20 09:12:54 +02:00
Jérémie Bolduc d0abfeb88b The Messenger: Fix portal shuffle logic not using unlocked portals (#6194) 2026-05-18 13:53:48 +02:00
Will Morrow b575983599 SM64: Change power star item classification to be deprioritized (#6191) 2026-05-17 23:44:40 +02:00
Jérémie Bolduc d8d148ac13 Stardew Valley: Bypass CollectionState overhead by just calling prog_items (#6065) 2026-05-17 23:41:34 +02:00
Silvris 24f75ba072 Settings: validate FilePath hash on use (#5854) 2026-05-17 23:40:08 +02:00
Katelyn Gigante 38e77e1b46 Factorio: Remove the need to have two separate installs (#4221) 2026-05-16 18:29:30 +02:00
JaredWeakStrike 799e0b7b0f KH2: fix ice cream double counting for logic (#6068) 2026-05-11 02:16:20 +02:00
Justus Lind 4ef1fb7630 Muse Dash: Convert to Rule Builder (#6166)
---------

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
2026-05-09 21:25:34 +02:00
Alchav 0cd81ff500 A Link to the Past: We Have Enemizer at Home (#6153) 2026-05-09 17:07:46 +02:00
Duck 55a1b12cb7 CommonClient: Make hint cost (shown in server label) update (#6149) 2026-05-09 17:01:22 +02:00
Duck 3fcd337f65 WebHost: Add authors to supported games page (#6123) 2026-05-09 17:00:00 +02:00
Duck 7a5acfeceb Core: Make manifest accessible as a world attribute (#6122) 2026-05-09 16:59:05 +02:00
Bryce Wilson 06dc1b897c Pokemon Emerald: Switch to rule builder (#5923) 2026-05-09 16:58:01 +02:00
Ishigh1 4a28888a66 Rule Builder: Implement AtLeast (#6085)
---------

Co-authored-by: Ian Robinson <ianronrobinson@gmail.com>
2026-05-09 16:56:10 +02:00
Christina d0407a74d6 Docs: Added new documentation for DS3's enemy randomization (random_enemy_preset) (#5738)
---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2026-05-09 16:54:20 +02:00
josephwhite e0810022f8 WebHost: Fix adding weighted text options (#5116) 2026-05-09 16:51:56 +02:00
Mysteryem 29a6f40c2b Blasphemous: Increase logic performance (#4585)
---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2026-05-09 16:50:34 +02:00
josephwhite 2bd572c23d Super Mario 64: Basic testing (#4335)
---------

Co-authored-by: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2026-05-09 16:46:53 +02:00
Scipio Wright b59e52a103 Options: Remove the "" from the start inventory examples (#6165)
---------

Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com>
2026-05-05 16:14:19 +02:00
Silvris 6d9d340c71 Launcher: display window for failed world loads if any (#6058) 2026-05-05 07:30:47 +02:00
qwint 4486510fbc Options: make template generation log the failing world if one fails (#6176) 2026-05-02 17:53:37 +02:00
Fabian Dill a68109f5a7 WebHost: Add deletion of old content to cleanup function (#6119)
---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2026-05-01 22:38:46 +02:00
Alchav 234b0581c9 ALttP: Fix starting mail and shield equipment from start inventory (#6154) 2026-05-01 11:30:35 +02:00
Justus Lind 798eeaad91 Muse Dash: Update to DASH AND SHOOT (#6163) 2026-05-01 11:29:59 +02:00
qwint 1875062099 Undertale: Handle zero arg /auto_patch (#5130) 2026-04-29 22:29:41 +02:00
Seldom 9da3c29990 Terraria: fix failing to generate with Calamity enabled and full accessibility on Mechanical Bosses goal #5787 2026-04-29 21:25:38 +02:00
palex00 90b72c0fa5 Docs: Add missing space to plando doc #6060 2026-04-29 21:25:14 +02:00
Mysteryem fd8f3fcd8b The Messenger: Add missing indirect conditions (#6101) 2026-04-29 21:24:41 +02:00
Jérémie Bolduc d36ba62243 The Messenger: Add missing rules for Key of Strength#6124 2026-04-29 21:24:10 +02:00
Ixrec 8ac335f92d Docs: Rewrite get_filler_item_name's docstring to avoid implying it's for filling all unfilled locations #5956 2026-04-29 21:22:35 +02:00
qwint 74a0cd6022 MultiServer: update help doc on auto_shutdown#5917 2026-04-29 21:21:42 +02:00
Sophia Caspe 31f5a6c4ea Docs: Rule Builder Filtered Example Fix (#6152) 2026-04-29 21:19:32 +02:00
River Buizel be9c7b1728 KH2: Update setup_en.md (#6129) 2026-04-29 21:18:59 +02:00
massimilianodelliubaldini a0236b2d74 Jak1: yamlless UT support (#6066) 2026-04-29 21:18:21 +02:00
threeandthreee af39d7926a LADX: doc update (#6050)
* update gfxmod doc

* romless generation

* Update setup_en.md

Co-authored-by: RoobyRoo <thegreenrobby@gmail.com>

---------

Co-authored-by: RoobyRoo <thegreenrobby@gmail.com>
2026-04-29 21:15:57 +02:00
Ian Robinson a48bf0195f Docs: Make RB OptionFilter docs clearer (#6028)
* add more docs for option filters

* reword the whole things

* style guide
2026-04-29 21:14:34 +02:00
Sophira 73856f63c8 Docs: Mention APQuest as a reference (#6000)
The APQuest implementation of an APWorld is incredibly useful and should
really have a mention here.
2026-04-29 21:12:04 +02:00
CookieCat d5cc201138 AHIT: Update FAQ in setup guide (#5975) 2026-04-29 21:11:36 +02:00
Ixrec 4346191c55 Docs: Add a warning to world api.md#events that you want StatusUpdate, not events, for actually sending the goal (#5957) 2026-04-29 21:10:26 +02:00
Mysteryem 0601494e39 Pokemon RB: Fix mutating global item_groups in get_filler_item_name() (#5947)
The `banned_items` variable was the `"Unique"`list within the
`item_groups` global. `get_filler_item_name()` could then mutate
`banned_items` through `banned_items.append("Poke Doll")` and
`banned_items += item_groups["Vending Machine Drinks"]`, causing
the contents of the `item_groups` global to be mutated.

This has been fixed by making `banned_items` a shallow copy of
`item_groups["Unique"]`.
2026-04-29 21:08:16 +02:00
Yussur Mustafa Oraji 78937054de sm64ex: Recommend Flatpak for Linux users (#5897) 2026-04-29 21:07:52 +02:00
UCSA 59674926ed Docs: added an italian translation of the setup guide for Muse Dash (#5834)
* Create setup_it.md

Created an italian translation of the setup for muse dash. I could not find direct references to translating a setup so I am assuming this is the correct way to submit a translation.
While I am a native in italian and I would consider myself fluent in english, I am not a translator so mistakes could have been made.

* Update __init__.py
2026-04-29 21:07:32 +02:00
Oshroth b716096a4b SML2: Send logic affecting randomiser options as slot_data (#5751)
* Send logic affecting randomiser options as slot_data

* SML2 - Universal Tracker support

* Remove debug logs

* Only check generation_is_fake once

---------

Co-authored-by: alchav <alchav@jalchavware.com>
2026-04-29 21:01:23 +02:00
el-u c851eff521 lufia2ac/docs: fix some errors (#5618)
Co-authored-by: Ludwig <80899010+wordfcuk@users.noreply.github.com>
2026-04-29 20:56:11 +02:00
Duck 01033940d6 MMBN3: Add spaces in concatenated string#5689 2026-04-29 20:55:32 +02:00
Mrks 15561e1e2d Generate: Added "quantity" option for player yamls to use multiple times a single yaml (#4948)
* Added  option for player yamls.

* Extended documentation

* Minimized value extraction.

* Added allow_quantity option to host.yaml

* Added  option for player yamls.

* Extended documentation

* Minimized value extraction.

* Added allow_quantity option to host.yaml

* Update settings.py

Co-authored-by: qwint <qwint.42@gmail.com>

* Update Generate.py

Co-authored-by: qwint <qwint.42@gmail.com>

* Added allow_quantity as application argument.

* Quantity > 1, allow_quantity = false -> error instead of silent 1

* Added check for quantity <= 0; reverted settings import change

* Update Generate.py

---------

Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2026-04-29 20:49:19 +02:00
Ian Robinson 9c78edc764 Rule Builder: Fix rule hash collisions (#6169)
* fix rule hash collisions

* just let the dict do the work
2026-04-29 08:21:09 +02:00
lepideble d2395de9fa Factorio: move communication savegame to user dir (#5646) 2026-04-27 14:06:19 +02:00
black-sliver d41cec6494 CI: update softprops/action-gh-release to v3.0.0 (#6162)
The update will be required once GH drops support for Node 20.

Also enables new options the action has now.
2026-04-26 00:44:48 +02:00
122 changed files with 7916 additions and 2448 deletions
-1
View File
@@ -46,7 +46,6 @@ dist
/prof/
README.html
.vs/
EnemizerCLI/
/Players/
/SNI/
/sni-*/
+7 -3
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z"
- name: Create Release
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
draft: true # don't publish right away, especially since windows build is added by hand
prerelease: false
@@ -97,13 +97,15 @@ jobs:
build/exe.*/ArchipelagoServer.exe
setups/*
- name: Add to Release
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
draft: true # see above
prerelease: false
name: Archipelago ${{ env.RELEASE_VERSION }}
files: |
setups/*
fail_on_unmatched_files: true
overwrite_files: false # Windows release is usually built by hand
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -165,12 +167,14 @@ jobs:
build/exe.*/ArchipelagoServer
dist/*
- name: Add to Release
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
draft: true # see above
prerelease: false
name: Archipelago ${{ env.RELEASE_VERSION }}
files: |
dist/*
fail_on_unmatched_files: true
overwrite_files: false # should never happen; avoids accidentally changing a release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+9 -2
View File
@@ -374,8 +374,7 @@ class MultiWorld():
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,
AutoWorld.FillerReason.item_link))
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)])
@@ -1039,6 +1038,8 @@ class CollectionState():
def has_from_list(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."""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
@@ -1050,6 +1051,8 @@ class CollectionState():
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."""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
@@ -1078,6 +1081,8 @@ class CollectionState():
# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group."""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
@@ -1090,6 +1095,8 @@ class CollectionState():
"""Returns True if the state contains at least `count` items present in a specified item group.
Ignores duplicates of the same item.
"""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
+5 -1
View File
@@ -1069,7 +1069,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
if "players" in args:
ctx.consume_players_package(args["players"])
if "hint_points" in args:
ctx.hint_points = args['hint_points']
ctx.hint_points = args["hint_points"]
if "checked_locations" in args:
checked = set(args["checked_locations"])
ctx.checked_locations |= checked
@@ -1077,6 +1077,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
if "permissions" in args:
ctx.update_permissions(args["permissions"])
# Update hint info for local display
if "hint_cost" in args:
ctx.hint_cost = int(args["hint_cost"])
elif cmd == 'Print':
ctx.on_print(args)
-27
View File
@@ -1,23 +1,5 @@
# hadolint global ignore=SC1090,SC1091
# Source
FROM scratch AS release
WORKDIR /release
ADD https://github.com/Ijwu/Enemizer/releases/latest/download/ubuntu.16.04-x64.zip Enemizer.zip
# Enemizer
FROM alpine:3.21 AS enemizer
ARG TARGETARCH
WORKDIR /release
COPY --from=release /release/Enemizer.zip .
# No release for arm architecture. Skip.
RUN if [ "$TARGETARCH" = "amd64" ]; then \
apk add unzip=6.0-r15 --no-cache && \
unzip -u Enemizer.zip -d EnemizerCLI && \
chmod -R 777 EnemizerCLI; \
else touch EnemizerCLI; fi
# Cython builder stage
FROM python:3.12 AS cython-builder
@@ -81,15 +63,6 @@ RUN apt-get purge -y \
g++ && \
apt-get autoremove -y
# Copy necessary components
COPY --from=enemizer /release/EnemizerCLI /tmp/EnemizerCLI
# No release for arm architecture. Skip.
RUN if [ "$TARGETARCH" = "amd64" ]; then \
cp -r /tmp/EnemizerCLI EnemizerCLI; \
fi; \
rm -rf /tmp/EnemizerCLI
# Define health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:${PORT:-80} || exit 1
+5 -5
View File
@@ -7,7 +7,7 @@ from collections import Counter, deque
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, PlandoItemBlock
from Options import Accessibility
from worlds.AutoWorld import call_all, FillerReason
from worlds.AutoWorld import call_all
from worlds.generic.Rules import add_item_rule
@@ -340,7 +340,7 @@ def remaining_fill(multiworld: MultiWorld,
for item in unplaced_items:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
multiworld.push_precollected(item)
last_batch.append(multiworld.worlds[item.player].create_filler(FillerReason.panic_fill))
last_batch.append(multiworld.worlds[item.player].create_filler())
remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry")
else:
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
@@ -599,7 +599,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
multiworld.push_precollected(item)
filleritempool.append(multiworld.worlds[item.player].create_filler(FillerReason.panic_fill))
filleritempool.append(multiworld.worlds[item.player].create_filler())
logging.warning(f"{len(progitempool)} items moved to start inventory,"
f" due to failure in Progression fill step.")
progitempool[:] = []
@@ -623,7 +623,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded",
move_unplaceable_to_start_inventory=panic_method == "start_inventory")
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
if excludedlocations:
raise FillError(
@@ -635,7 +635,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
restitempool = filleritempool + usefulitempool
remaining_fill(multiworld, defaultlocations, restitempool,
move_unplaceable_to_start_inventory=panic_method == "start_inventory")
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
unplaced = restitempool
unfilled = defaultlocations
+13 -2
View File
@@ -40,6 +40,8 @@ def mystery_argparse(argv: list[str] | None = None) -> argparse.Namespace:
parser.add_argument('--spoiler', type=int, default=defaults.spoiler)
parser.add_argument('--outputpath', default=settings.general_options.output_path,
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--allow_quantity', action="store_true", default=defaults.allow_quantity,
help='Allows the use of the quantity option in yamls. Default is the set value in the host.yaml.')
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=defaults.loglevel, help='Sets log level')
@@ -123,6 +125,7 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
player_id: int = 1
player_files: dict[int, str] = {}
player_errors: list[str] = []
allow_quantity = args.allow_quantity
for file in os.scandir(args.player_files_path):
fname = file.name
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
@@ -134,7 +137,14 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
if yaml is None:
logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}")
else:
weights_for_file.append(yaml)
quantity = yaml.get("quantity", 1)
if quantity <= 0:
raise ValueError("A quantity of 0 or less is invalid. Please change it to at least 1.")
if not allow_quantity and quantity > 1:
raise ValueError("Quantity greater than 1 is deactivated by host settings.")
for _ in range(quantity):
weights_for_file.append(yaml)
weights_cache[fname] = tuple(weights_for_file)
except Exception as e:
@@ -575,7 +585,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
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]
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + list(failed_world_loads.keys()),
limit=1)[0]
if picks[0] in failed_world_loads:
raise Exception(f"No functional world found to handle game {ret.game}. "
f"Did you mean '{picks[0]}' ({picks[1]}% sure)? "
+35
View File
@@ -36,6 +36,7 @@ if __name__ == "__main__":
init_logging('Launcher')
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
from worlds import failed_world_loads
def open_host_yaml():
@@ -275,6 +276,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
search_box: MDTextField = ObjectProperty(None)
cards: list[LauncherCard]
current_filter: Sequence[str | Type] | None
failed_worlds: bool = bool(failed_world_loads)
def __init__(self, ctx=None, components=None, args=None):
self.title = self.base_title + " " + Utils.__version__
@@ -422,6 +424,39 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
MDSnackbar(MDSnackbarText(text=open_text), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
@staticmethod
def copy_to_clipboard(text):
from kivy.core.clipboard import Clipboard
Clipboard.copy(text)
MDSnackbar(MDSnackbarText(text="Copied to clipboard."), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
def display_failed(self):
"""Display a dialog showing the exceptions produced by any world that failed to load during
initialization."""
if not self.failed_worlds:
return
from kivymd.uix.dialog import MDDialog, MDDialogIcon, MDDialogHeadlineText, MDDialogContentContainer
from kivymd.uix.divider import MDDivider
from kivymd.uix.list import MDListItem, MDListItemHeadlineText, MDListItemSupportingText
entries = []
for world, reason in failed_world_loads.items():
entries.append(MDListItem(
MDListItemHeadlineText(text=world),
MDListItemSupportingText(text=reason),
on_release=lambda x, r=reason: self.copy_to_clipboard(r)
))
dialog = MDDialog(
MDDialogIcon(icon="alert"),
MDDialogHeadlineText(text="Failed World Loads"),
MDDialogContentContainer(
MDDivider(),
*entries,
orientation="vertical",
)
)
dialog.open()
def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
""" When a patch file is dropped into the window, run the associated component. """
file, component = identify(filename.decode())
+2 -2
View File
@@ -241,8 +241,8 @@ async def gba_sync_task(ctx: MMBN3Context):
await ctx.server_auth(False)
else:
if not ctx.version_warning:
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}."
"Please update to the latest version."
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}. "
"Please update to the latest version. "
"Your connection to the Archipelago server will not be accepted.")
ctx.version_warning = True
except asyncio.TimeoutError:
+1 -2
View File
@@ -174,8 +174,7 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
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(AutoWorld.FillerReason.start_inventory_from_pool)
for _ in range(needed_items)]
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
+2 -2
View File
@@ -2633,8 +2633,8 @@ def parse_args() -> argparse.Namespace:
goal: !remaining can be used after goal completion
''')
parser.add_argument('--auto_shutdown', default=defaults["auto_shutdown"], type=int,
help="automatically shut down the server after this many minutes without new location checks. "
"0 to keep running. Not yet implemented.")
help="automatically shut down the server after this many seconds without new location checks. "
"0 to keep running.")
parser.add_argument('--use_embedded_options', action="store_true",
help='retrieve release, remaining and hint options from the multidata file,'
' instead of host.yaml')
+4
View File
@@ -527,7 +527,11 @@ else:
except ImportError:
pyximport = None
try:
import logging
logger = logging.getLogger()
old_level = logger.level
from _speedups import LocationStore
logger.setLevel(old_level)
except ImportError:
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
"Install a matching C++ compiler for your platform to compile _speedups.")
+25 -22
View File
@@ -1469,7 +1469,7 @@ class NonLocalItems(ItemSet):
class StartInventory(ItemDict):
"""Start with the specified amount of these items. Example: "Bomb: 1" """
"""Start with the specified amount of these items. Example: {Bomb: 1, Arrow: 3} """
verify_item_name = True
display_name = "Start Inventory"
rich_text_doc = True
@@ -1477,7 +1477,7 @@ class StartInventory(ItemDict):
class StartInventoryPool(StartInventory):
"""Start with the specified amount of these items and don't place them in the world. Example: "Bomb: 1"
"""Start with the specified amount of these items and don't place them in the world. Example: {Bomb: 1, Arrow: 3}
The game decides what the replacement items will be.
"""
@@ -1856,27 +1856,30 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
for game_name, world in AutoWorldRegister.world_types.items():
if not world.hidden or generate_hidden:
presets = world.web.options_presets.copy()
presets.update({"": {}})
try:
presets = world.web.options_presets.copy()
presets.update({"": {}})
option_groups = get_option_groups(world)
for name, preset in presets.items():
res = template.render(
option_groups=option_groups,
__version__=__version__,
game=game_name,
world_version=world.world_version.as_simple_string(),
yaml_dump=yaml_dump_scalar,
dictify_range=dictify_range,
cleandoc=cleandoc,
preset_name=name,
preset=preset,
)
preset_name = f" - {name}" if name else ""
with open(os.path.join(preset_folder if name else target_folder,
get_file_safe_name(game_name + preset_name) + ".yaml"),
"w", encoding="utf-8-sig") as f:
f.write(res)
option_groups = get_option_groups(world)
for name, preset in presets.items():
res = template.render(
option_groups=option_groups,
__version__=__version__,
game=game_name,
world_version=world.world_version.as_simple_string(),
yaml_dump=yaml_dump_scalar,
dictify_range=dictify_range,
cleandoc=cleandoc,
preset_name=name,
preset=preset,
)
preset_name = f" - {name}" if name else ""
with open(os.path.join(preset_folder if name else target_folder,
get_file_safe_name(game_name + preset_name) + ".yaml"),
"w", encoding="utf-8-sig") as f:
f.write(res)
except Exception as ex:
raise Exception(f"Template generation failed for world {game_name}") from ex
def dump_player_options(multiworld: MultiWorld) -> None:
+1 -1
View File
@@ -49,7 +49,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
if isinstance(self.ctx, UndertaleContext):
os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True)
tempInstall = steaminstall
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
if tempInstall and not os.path.isfile(os.path.join(tempInstall, "data.win")):
tempInstall = None
if tempInstall is None:
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
+1 -1
View File
@@ -52,7 +52,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.6.7"
__version__ = "0.6.8"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
+3
View File
@@ -42,12 +42,15 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["GAME_PORTS"] = ["49152-65535", 0]
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
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
# maximum time in seconds since last activity for a room to be hosted
app.config["MAX_ROOM_TIMEOUT"] = 259200
# minimum time in days since last activity for a room to be deleted. 0 to disable.
app.config["ROOM_AUTO_DELETE"] = 0
# memory limit for generator processes in bytes
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
+10 -4
View File
@@ -100,13 +100,18 @@ def init_generator(config: dict[str, Any]) -> None:
db.generate_mapping()
def cleanup():
"""delete unowned user-content"""
def cleanup(config: dict[str, Any]):
"""delete unowned or old user-content"""
auto_delete: int = config.get("ROOM_AUTO_DELETE", 0)
with db_session:
# >>> bool(uuid.UUID(int=0))
# True
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
if auto_delete > 0:
cutoff = utcnow() - timedelta(days=auto_delete)
rooms += Room.select(lambda room: room.last_activity < cutoff).delete(bulk=True)
seeds += Seed.select(lambda seed: not seed.rooms and seed.creation_time < cutoff).delete(bulk=True)
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
if rooms or seeds or slots:
@@ -118,7 +123,7 @@ def autohost(config: dict):
stop_event = _stop_event
try:
with Locker("autohost"):
cleanup()
cleanup(config)
hosters = []
for x in range(config["HOSTERS"]):
hoster = MultiworldInstance(config, x)
@@ -188,6 +193,7 @@ class MultiworldInstance():
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
self.game_ports = config["GAME_PORTS"]
self.rooms_to_start = multiprocessing.Queue()
self.rooms_shutting_down = multiprocessing.Queue()
self.name = f"MultiHoster{id}"
@@ -198,7 +204,7 @@ class MultiworldInstance():
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.name, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host,
self.cert, self.key, self.host, self.game_ports,
self.rooms_to_start, self.rooms_shutting_down),
name=self.name)
process.start()
+116 -19
View File
@@ -4,6 +4,7 @@ import asyncio
import collections
import datetime
import functools
import itertools
import logging
import multiprocessing
import pickle
@@ -13,7 +14,9 @@ import threading
import time
import typing
import sys
from collections.abc import Iterable
import psutil
import websockets
from pony.orm import commit, db_session, select
@@ -24,6 +27,7 @@ from MultiServer import (
server_per_message_deflate_factory,
)
from Utils import restricted_loads, cache_argsless
from .locker import Locker
from .models import Command, GameDataPackage, Room, db
@@ -76,12 +80,8 @@ class WebHostContext(Context):
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")
from Utils import format_SI_prefix
self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB")
def _load_game_data(self):
for key, value in self.static_server_data.items():
@@ -115,7 +115,7 @@ class WebHostContext(Context):
if room.last_port:
self.port = room.last_port
else:
self.port = get_random_port()
self.port = 0
multidata = self.decompress(room.seed.multidata)
game_data_packages = {}
@@ -181,8 +181,97 @@ class WebHostContext(Context):
return d
def get_random_port():
return random.randint(49152, 65535)
class GameRangePorts(typing.NamedTuple):
valid_ports: list[int]
ephemeral_allowed: bool
class RandomPortSocketCreator:
""" Creates server sockets on random available ports from a configured range. """
_next_port_index: int
_used_ports_cache: tuple[frozenset[int], int] | None
_parsed_ports: GameRangePorts
def __init__(self, game_ports: Iterable[str | int]) -> None:
self._next_port_index = 0
self._used_ports_cache = None
self._parsed_ports = self._parse_game_ports(game_ports)
@staticmethod
def _parse_game_ports(game_ports: Iterable[str | int]) -> GameRangePorts:
""" Parse the game ports configuration into a structured format. """
valid_ports: list[int] = []
ephemeral_allowed = False
for item in game_ports:
if isinstance(item, str) and "-" in item:
start, end = map(int, item.split("-"))
x = range(start, end + 1)
valid_ports.extend(x)
elif int(item) == 0:
ephemeral_allowed = True
else:
valid_ports.append(int(item))
random.shuffle(valid_ports)
return GameRangePorts(valid_ports, ephemeral_allowed)
@staticmethod
def _try_conns_per_process(p: psutil.Process) -> Iterable[int]:
""" Get ports from a single process's connections. """
try:
return (c.laddr.port for c in p.net_connections("tcp4") if c.laddr)
except psutil.AccessDenied:
return ()
@staticmethod
def _get_active_net_connections() -> Iterable[int]:
""" Get all active TCP4 connections on the system. """
# Don't even try to check if system using AIX
if psutil.AIX:
return ()
try:
return (c.laddr.port for c in psutil.net_connections("tcp4") if c.laddr)
# raises AccessDenied when done on macOS
except psutil.AccessDenied:
# flatten the list of iterables
return itertools.chain.from_iterable(map(
RandomPortSocketCreator._try_conns_per_process,
psutil.process_iter(["net_connections"])
))
def _get_used_ports(self) -> frozenset[int]:
""" Get currently used ports with 90-second caching. """
t_hash = round(time.monotonic() / 90)
if self._used_ports_cache is None or self._used_ports_cache[1] != t_hash:
self._used_ports_cache = (frozenset(self._get_active_net_connections()), t_hash)
return self._used_ports_cache[0]
def create(self, host: str) -> socket.socket:
""" Create a server socket on an available port. """
valid_ports, ephemeral_allowed = self._parsed_ports
used_ports = self._get_used_ports()
next_index = self._next_port_index
for i, port in enumerate(itertools.chain(valid_ports[next_index:], valid_ports[:next_index])):
if port in used_ports:
continue
try:
res = socket.create_server((host, port))
next_index = (next_index + i + 1) % len(valid_ports)
self._next_port_index = next_index
return res
except OSError:
pass
if ephemeral_allowed:
return socket.create_server((host, 0))
raise OSError(98, "No available ports")
@cache_argsless
@@ -247,7 +336,8 @@ def tear_down_logging(room_id):
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
host: str, game_ports: Iterable[str | int],
rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
from setproctitle import setproctitle
setproctitle(name)
@@ -291,6 +381,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
gc.collect() # free intermediate objects used during setup
loop = asyncio.get_event_loop()
socket_creator = RandomPortSocketCreator(game_ports)
async def start_room(room_id):
with Locker(f"RoomLocker {room_id}"):
@@ -300,20 +391,26 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
ctx.load(room_id)
ctx.init_save()
assert ctx.server is None
try:
if ctx.port != 0:
try:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
ctx.host,
ctx.port,
ssl=get_ssl_context(),
extensions=[server_per_message_deflate_factory],
)
await ctx.server
except OSError:
ctx.port = 0
if ctx.port == 0:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
ctx.host,
ctx.port,
sock=socket_creator.create(ctx.host),
ssl=get_ssl_context(),
extensions=[server_per_message_deflate_factory],
)
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context())
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
@@ -388,7 +485,7 @@ 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)
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)
+2 -1
View File
@@ -10,5 +10,6 @@ Flask-Cors==6.0.2
bokeh==3.8.2
markupsafe==3.0.3
setproctitle==1.3.7
mistune==3.2.0
mistune==3.2.1
docutils==0.22.4
psutil==7.2.2
+18 -4
View File
@@ -123,12 +123,26 @@ window.addEventListener('load', () => {
});
const addRangeRow = (optionName) => {
const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
const inputQuery = `input[data-option="${optionName}"]`;
const inputTarget = document.querySelector(inputQuery);
const newValue = inputTarget.value;
if (!/^-?\d+$/.test(newValue)) {
alert('Range values must be a positive or negative integer!');
return;
switch (inputTarget.type) {
case 'number':
if (!/^-?\d+$/.test(newValue)) {
alert('Range values must be a positive or negative integer!');
return;
}
break;
case 'text':
if (newValue === "") {
alert('Range values for text must be a non-empty string!');
return;
}
break;
default:
console.error(`Found unsupported input type: ${inputTarget.type}`);
return;
break;
}
inputTarget.value = '';
const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
@@ -42,3 +42,7 @@
#games .page-controls button{
margin-left: 0.5rem;
}
#games .author-label{
margin-top: 0.5rem;
}
+7
View File
@@ -49,6 +49,13 @@
<details data-game="{{ game_name }}">
<summary class="h2">{{ game_name }}</summary>
{{ world.__doc__ | default("No description provided.", true) }}<br />
{% if "authors" in world.manifest %}
{% if world.manifest["authors"]|length == 1 %}
<p class="author-label">Author: {{ world.manifest["authors"][0] }}</p>
{% else %}
<p class="author-label">Authors: {{ world.manifest["authors"] | join(", ") }}</p>
{% endif %}
{% endif %}
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
{% if world.web.tutorials %}
<span class="link-spacer">|</span>
@@ -71,10 +71,10 @@
<div class="hint-text">
This option allows custom values only. Please enter your desired values below.
<div class="custom-value-wrapper">
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" data-option="{{ option_name }}">Add</button>
<input type="text" class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" class="add-range-option-button" data-option="{{ option_name }}">Add</button>
</div>
<table>
<table class="range-rows" data-option="{{ option_name }}">
<tbody>
{% if option.default %}
{{ RangeRow(option_name, option, option.default, option.default) }}
@@ -88,11 +88,11 @@
<div class="hint-text">
Custom values are also allowed for this option. To create one, enter it into the input box below.
<div class="custom-value-wrapper">
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" data-option="{{ option_name }}">Add</button>
<input type="text" class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" class="add-range-option-button" data-option="{{ option_name }}">Add</button>
</div>
</div>
<table>
<table class="range-rows" data-option="{{ option_name }}">
<tbody>
{% for id, name in option.name_lookup.items() %}
{% if name != 'random' %}
+9
View File
@@ -140,6 +140,15 @@ MDFloatLayout:
MDNavigationDrawerDivider:
MDBoxLayout:
orientation: "horizontal"
MDIconButton:
icon: "alert" if app.failed_worlds else ""
theme_text_color: "Custom"
text_color: "D23C42"
disabled: not app.failed_worlds
on_release: app.display_failed()
MDGridLayout:
id: main_layout
+3 -2
View File
@@ -92,8 +92,9 @@ for setup).
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
check out [world maintainer.md](/docs/world%20maintainer.md).
regarding the API can be found in the [world api doc](/docs/world%20api.md), and the [APQuest](/worlds/apquest/) world
is a complete world implementation that functions as an introduction to world development. Before publishing, make sure
to also check out [world maintainer.md](/docs/world%20maintainer.md).
### Hard Requirements
+2 -2
View File
@@ -35,8 +35,8 @@ There are also the following optional fields:
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
An APWorld without a world_version is always treated as older than one with a version
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)
* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and
package managers. Should always be a list of strings.
* `authors` - a list of authors of the world. Displayed in user-facing places like the Supported Games page
on WebHost. Should always be a list of strings.
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
-9
View File
@@ -77,15 +77,6 @@ Changes made to `docker-compose.yaml` can be applied by running `docker compose
It is possible to carry out these deployment steps on Windows under [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install).
## Optional: A Link to the Past Enemizer
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
error if it is required.
Enemizer can be enabled on `x86_64` platform architecture, and is included in the image build process. Enemizer requires a version 1.0 Japanese "Zelda no Densetsu" `.sfc` rom file to be placed in the application directory:
`docker run archipelago -v "/path/to/zelda.sfc:/app/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"`.
Enemizer is not currently available for `aarch64`.
## Optional: Git
Building the image requires a local copy of the ArchipelagoMW source code.
+162 -61
View File
@@ -1,6 +1,7 @@
# Rule Builder
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface to define rules and the following advantages:
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface
to define rules and the following advantages:
- Rule classes that avoid all the common pitfalls
- Logic optimization
@@ -12,13 +13,21 @@ This document describes the API provided for the rule builder. Using this API pr
The rule builder consists of 3 main parts:
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. They can be combined and take into account your world's options. There are a number of default rules listed below, and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance they must be resolved.
1. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly creating these but they'll be created when assigning rules to locations or entrances. These are what power the human-readable logic explanations.
1. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic.
They can be combined and take into account your world's options. There are a number of default rules listed below,
and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance
they must be resolved.
2. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules
specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly
creating these but they'll be created when assigning rules to locations or entrances. These are what power the
human-readable logic explanations.
3. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from
instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
## Usage
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule
objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
```python
# In your world's create_regions method
@@ -32,6 +41,7 @@ The rule builder comes with a number of rules by default:
- `False_`: Always returns false
- `And`: Checks that all child rules are true (also provided by `&` operator)
- `Or`: Checks that at least one child rule is true (also provided by `|` operator)
- `AtLeast`: Checks that at least some count of rules is true
- `Has`: Checks that the player has the given item with the given count (default 1)
- `HasAll`: Checks that the player has all given items
- `HasAny`: Checks that the player has at least one of the given items
@@ -40,18 +50,22 @@ The rule builder comes with a number of rules by default:
- `HasFromList`: Checks that the player has some number of given items
- `HasFromListUnique`: Checks that the player has some number of given items, ignoring duplicates of the same item
- `HasGroup`: Checks that the player has some number of items from a given item group
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the same item
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the
same item
- `CanReachLocation`: Checks that the player can logically reach the given location
- `CanReachRegion`: Checks that the player can logically reach the given region
- `CanReachEntrance`: Checks that the player can logically reach the given entrance
You can combine these rules together to describe the logic required for something. For example, to check if a player either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
You can combine these rules together to describe the logic required for something. For example, to check if a player
either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
```python
rule = Has("Movement ability") | HasAll("Key 1", "Key 2")
```
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check if a rule is defined you must use `if rule is not None`.
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In
> order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check
> if a rule is defined you must use `if rule is not None`.
### Assigning rules
@@ -61,13 +75,16 @@ When assigning the rule you must use the `set_rule` helper to correctly resolve
self.set_rule(location_or_entrance, rule)
```
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify `force_creation=True` if you would like to create the entrance even if the rule is `False`.
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the
entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify
`force_creation=True` if you would like to create the entrance even if the rule is `False`.
```python
self.create_entrance(from_region, to_region, rule)
```
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify
> the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
You can also set a rule for your world's completion condition:
@@ -77,21 +94,42 @@ self.set_completion_rule(rule)
### Restricting options
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an iterable of `OptionFilter` instances. Rules that pass the options check will be resolved as normal, and those that fail will be resolved as `False`.
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an
iterable of `OptionFilter` instances. When resolved, if no filters are provided or all of them pass then the rule will
resolve as normal. Otherwise, the rule will be replaced with `True` or `False` depending on what `filtered_resolution`
is set to, which defaults to `False`.
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are allowed:
```python
rule1 = Has(
"Fast Travel Spell",
options=[OptionFilter(RandoFastTravel, RandoFastTravel.option_true)],
)
rule2 = Has(
"Starting Party Member",
options=[OptionFilter(RandoParty, 1)], # option attributes are suggested but any value works
filtered_resolution=True,
)
```
- `eq`: `==`
- `ne`: `!=`
- `gt`: `>`
- `lt`: `<`
- `ge`: `>=`
- `le`: `<=`
- `contains`: `in`
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are
allowed:
By default rules that are excluded by their options will default to `False`. If you want to default to `True` instead, you can specify `filtered_resolution=True` on your rule.
- `eq`: `option_value == filter_value`
- `ne`: `option_value != filter_value`
- `gt`: `option_value > filter_value`
- `lt`: `option_value < filter_value`
- `ge`: `option_value >= filter_value`
- `le`: `option_value <= filter_value`
- `in`: `option_value in filter_value`
- `contains`: `filter_value in option_value` (note reversed operands)
To check if the player can reach a switch, or if they've received the switch item if switches are randomized:
```python
rule1 = Has("Movement Ability", options=[OptionFilter(SkipsLevel, SkipsLevel.option_hard, operator="lt")])
rule2 = Has("Item", options=[OptionFilter(ChoiceOption, [1, 5], operator="in")])
```
To check if the player has received the switch item if switches are randomized, or if they can reach the switch when not
randomized:
```python
rule = (
@@ -115,12 +153,12 @@ If you would like to provide option filters when reusing or composing rules, you
common_rule = Has("A") | HasAny("B", "C")
...
rule = (
Filtered(common_rule, options=[OptionFilter(Opt, 0)]),
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]),
Filtered(common_rule, options=[OptionFilter(Opt, 0)])
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)])
)
```
You can also use the & and | operators to apply options to rules:
For convenience, you can also use the `&` and `|` operators to apply options to rules:
```python
common_rule = Has("A")
@@ -129,14 +167,22 @@ common_rule_only_on_easy = common_rule & easy_filter
common_rule_skipped_on_easy = common_rule | easy_filter
```
Combining the above, you can easily bypass a requirement based on option choices:
```python
rule = Has("Some Upgrade") | OptionFilter(CombatDifficulty, CombatDifficulty.option_medium, operator="ge")
```
### Field resolvers
When creating rules you may sometimes need to set a field to a value that depends on the world instance. You can use a `FieldResolver` to define how to populate that field when the rule is being resolved.
When creating rules you may sometimes need to set a field to a value that depends on the world instance. You can use a
`FieldResolver` to define how to populate that field when the rule is being resolved.
There are two build-in field resolvers:
- `FromOption`: Resolves to the value of the given option
- `FromWorldAttr`: Resolves to the value of the given world instance attribute, can specify a dotted path `a.b.c` to get a nested attribute or dict item
- `FromWorldAttr`: Resolves to the value of the given world instance attribute, can specify a dotted path `a.b.c` to get
a nested attribute or dict item
```python
world.options.mcguffin_count = 5
@@ -148,7 +194,8 @@ rule = (
# Results in Has("A", count=5) | HasGroup("Important items", count=99)
```
You can define your own resolvers by creating a class that inherits from `FieldResolver`, provides your game name, and implements a `resolve` function:
You can define your own resolvers by creating a class that inherits from `FieldResolver`, provides your game name, and
implements a `resolve` function:
```python
@dataclasses.dataclass(frozen=True)
@@ -163,24 +210,30 @@ class FromCustomResolution(FieldResolver, game="MyGame"):
rule = Has("Combat Level", count=FromCustomResolution("combat"))
```
If you want to support rule serialization and your resolver contains non-serializable properties you may need to override `to_dict` or `from_dict`.
If you want to support rule serialization and your resolver contains non-serializable properties you may need to
override `to_dict` or `from_dict`.
## Enabling caching
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your
rules.
```python
class MyWorld(CachedRuleBuilderWorld):
game = "My Game"
```
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead
cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
### Item name mapping
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps actual item names to real item names so the cache system knows what to invalidate.
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps
actual item names to real item names so the cache system knows what to invalidate.
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical `Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical `Currency`.
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical
`Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical
`Currency`.
```python
class MyWorld(CachedRuleBuilderWorld):
@@ -194,9 +247,13 @@ class MyWorld(CachedRuleBuilderWorld):
## Defining custom rules
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide
the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and
to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more dependencies functions as outlined below.
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically
be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more
dependencies functions as outlined below.
To add a rule that checks if the user has enough mcguffins to goal, with a randomized requirement:
@@ -245,7 +302,10 @@ class ComplicatedFilter(Rule["MyWorld"], game="My Game"):
### Item dependencies
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this function even when caching is disabled as more things may use it in the future.
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of
your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id
of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this
function even when caching is disabled as more things may use it in the future.
```python
@dataclasses.dataclass()
@@ -262,7 +322,10 @@ All of the default `Has*` rules define this function already.
### Region dependencies
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of
region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
caching system if applicable.
```python
@dataclasses.dataclass()
@@ -279,7 +342,10 @@ The default `CanReachLocation`, `CanReachRegion`, and `CanReachEntrance` rules d
### Location dependencies
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping
of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
caching system if applicable.
```python
@dataclasses.dataclass()
@@ -296,7 +362,10 @@ The default `CanReachLocation` rule defines this function already.
### Entrance dependencies
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping
of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
caching system if applicable.
```python
@dataclasses.dataclass()
@@ -313,9 +382,13 @@ The default `CanReachEntrance` rule defines this function already.
### Rule explanations
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally
accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will
display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is
useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your `Resolved` class:
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your
`Resolved` class:
```python
class MyRule(Rule, game="My Game"):
@@ -352,22 +425,35 @@ class MyRule(Rule, game="My Game"):
### Cache control
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two class attributes on the `Resolved` class you can override to change this behavior.
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two
class attributes on the `Resolved` class you can override to change this behavior.
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your rule should be marked as stale.
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the overhead of the caching system will slow it down.
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and
always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will
cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your
rule should be marked as stale.
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when
being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still
define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the
overhead of the caching system will slow it down.
### Caveats
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if your world has opted into caching.
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if
your world has opted into caching.
- Resolved rules are forced to be frozen dataclasses. They and all their attributes must be immutable and hashable.
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` instances directly.
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved`
instances directly.
## Serialization
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the
rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and
entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule would look like:
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and
an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule
would look like:
```python
{
@@ -380,7 +466,8 @@ The dict contains a `rule` key with the name of the rule, an `options` key with
}
```
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in the same serializable format:
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in
the same serializable format:
```python
{
@@ -464,7 +551,8 @@ class BasicLogicRule(Rule, game="My Game"):
}
```
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it correctly:
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it
correctly:
```python
class BasicLogicRule(Rule, game="My Game"):
@@ -485,10 +573,14 @@ These are properties and helpers that are available to you in your world.
#### Methods
- `rule_from_dict(data)`: Create a rule instance from a deserialized dict representation
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the inherited dependencies, gets called automatically after set_rules
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given location or entrance
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the
inherited dependencies, gets called automatically after set_rules
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given
location or entrance
- `set_completion_rule(rule: Rule)`: Sets the completion condition for this world
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates to `False_()` unless force_creation is `True`
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`:
Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates
to `False_()` unless force_creation is `True`
#### CachedRuleBuilderWorld Properties
@@ -501,18 +593,27 @@ The following property is only available when inheriting from `CachedRuleBuilder
These are properties and helpers that you can use or override for custom rules.
- `_instantiate(world: World)`: Create a new resolved rule instance, override for custom rules as required
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's serialization
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if you've overridden `to_dict`
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's
serialization
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if
you've overridden `to_dict`
- `__str__()`: Basic string representation of a rule, useful for debugging
#### Resolved rule API
- `player: int`: The slot this rule is resolved for
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for this rule
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item collection
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching regions
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on reaching locations
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on reaching entrances
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for
this rule
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item
collection
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching
regions
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on
reaching locations
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on
reaching entrances
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic
(and if state is defined its evaluation) in a human readable way, override to explain custom rules
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is
defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
- `__str__()`: A string describing this rule's logic without its evaluation, override to explain custom rules
-10
View File
@@ -78,16 +78,6 @@ first generate the binary distribution and then run `python setup.py bdist_appim
put an `appimagetool` into the directory you run the command from, rename it to `appimagetool` and make it executable.
## Optional: A Link to the Past Enemizer
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
error if it is required.
You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases).
It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer
setting in host.yaml at your Enemizer executable.
## Optional: SNI
[SNI](https://github.com/alttpo/sni/blob/main/README.md) is required to use SNIClient. If not integrated into the project, it has to be started manually.
+6
View File
@@ -17,6 +17,12 @@
# Web hosting port
#PORT: 80
# Ports used for game hosting. Values can be specific ports, port ranges or both. Default is: [49152-65535, 0]
# Zero means it will use a random free port if there is no free port in the ranges specified
# Examples of valid values: [40000-41000, 49152-65535]
# If ports within the range(s) are already in use, the WebHost will fallback to the default [49152-65535, 0] range.
#GAME_PORTS: [49152-65535, 0]
# Place where uploads go.
#UPLOAD_FOLDER: uploads
+5
View File
@@ -327,6 +327,11 @@ reject the placement of an item there.
### Events (or "generation-only items/locations")
> **Warning:** If you're trying to tell the Archipelago server that the player has achieved their goal, you want to send
a [StatusUpdate packet](network%20protocol.md#statusupdate), or however [your client library](network%20protocol.md)
wraps it. Despite the popularity of "victory events" during generation, events have nothing to do with how goals are
triggered during gameplay.
An event item or location is one that only exists during multiworld generation; the server is never made aware of them.
Event locations can never be checked by the player, and event items cannot be received during play.
+1 -3
View File
@@ -57,9 +57,8 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs;
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs;
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
[Icons]
@@ -83,7 +82,6 @@ Type: files; Name: "{app}\*.exe"
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
Type: files; Name: "{app}\data\lua\connector_ff1.lua"
Type: filesandordirs; Name: "{app}\SNI\lua*"
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
#include "installdelete.iss"
[Registry]
+10 -9
View File
@@ -363,15 +363,16 @@ class ServerLabel(HoverBehavior, MDTooltip, MDBoxLayout):
text += "\nPermissions:"
for permission_name, permission_data in ctx.permissions.items():
text += f"\n {permission_name}: {permission_data}"
if ctx.hint_cost is not None and ctx.total_locations:
min_cost = int(ctx.server_version >= (0, 3, 9))
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
f"For you this means every " \
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
"location checks." \
f"\nYou currently have {ctx.hint_points} points."
elif ctx.hint_cost == 0:
text += "\n!hint is free to use."
if ctx.total_locations and ctx.hint_cost is not None:
if ctx.hint_cost == 0:
text += "\n!hint is free to use."
else:
min_cost = int(ctx.server_version >= (0, 3, 9))
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
f"For you this means every " \
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
"location checks." \
f"\nYou currently have {ctx.hint_points} points."
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
text += "\nRace mode is enabled." \
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
+156 -14
View File
@@ -36,7 +36,7 @@ def _create_hash_fn(resolved_rule_cls: "CustomRuleRegister") -> Callable[..., in
class CustomRuleRegister(type):
"""A metaclass to contain world custom rules and automatically convert resolved rules to frozen dataclasses"""
resolved_rules: ClassVar[dict[int, "Rule.Resolved"]] = {}
resolved_rules: ClassVar[dict["Rule.Resolved", "Rule.Resolved"]] = {}
"""A cached of resolved rules to turn each unique one into a singleton"""
custom_rules: ClassVar[dict[str, dict[str, type["Rule[Any]"]]]] = {}
@@ -64,10 +64,9 @@ class CustomRuleRegister(type):
@override
def __call__(cls, *args: Any, **kwds: Any) -> Any:
rule = super().__call__(*args, **kwds)
rule_hash = hash(rule)
if rule_hash in cls.resolved_rules:
return cls.resolved_rules[rule_hash]
cls.resolved_rules[rule_hash] = rule
if rule in cls.resolved_rules:
return cls.resolved_rules[rule]
cls.resolved_rules[rule] = rule
return rule
@classmethod
@@ -426,13 +425,142 @@ class NestedRule(Rule[TWorld], game="Archipelago"):
return combined_deps
class AtLeast(NestedRule[TWorld], game="Archipelago"):
"""A rule that returns true when at least N child rules evaluate as true"""
count: int | FieldResolver
def __init__(
self,
count: int | FieldResolver,
*children: Rule[TWorld],
options: Iterable[OptionFilter] = (),
filtered_resolution: bool = False,
) -> None:
super().__init__(*children, options=options, filtered_resolution=filtered_resolution)
self.count = count
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if count == 0:
return True_().resolve(world)
children_to_process = [c.resolve(world) for c in self.children]
return AtLeast.from_resolved(count, world, children_to_process)
@classmethod
def from_resolved(cls, count: int, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
clauses: list[Rule.Resolved] = []
while children_to_process:
child = children_to_process.pop(0)
if child.always_true:
if count == 1:
return child
count -= 1
continue
if child.always_false:
# falses can be ignored
continue
clauses.append(child)
if len(clauses) < count:
return False_().resolve(world)
if count == 1:
# Switch to Or which has more optimized handling
return Or.from_resolved(world, clauses)
if count == len(clauses):
# Switch to And which has more optimized handling
return And.from_resolved(world, clauses)
return AtLeast.Resolved(
tuple(clauses),
count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@override
def to_dict(self) -> dict[str, Any]:
output = super().to_dict()
count = self.count
output["count"] = count.to_dict() if isinstance(count, FieldResolver) else count
return output
@override
@classmethod
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
args = cls._parse_field_resolvers(data, world_cls.game)
options = OptionFilter.multiple_from_dict(data.get("options", ()))
children = [world_cls.rule_from_dict(c) for c in data.get("children", ())]
return cls(
args.pop("count"),
*children,
options=options,
filtered_resolution=data.get("filtered_resolution", False),
)
class Resolved(NestedRule.Resolved):
count: int
@override
def _evaluate(self, state: CollectionState) -> bool:
count = self.count
for rule in self.children:
if rule(state):
if count == 1:
return True
count -= 1
return False
@override
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
messages: list[JSONMessagePart] = []
if state is None:
messages = [
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": str(self.count)},
{"type": "text", "text": " of ("},
]
else:
satisfied_count = sum(1 if child(state) else 0 for child in self.children)
messages = [
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": f"{satisfied_count}/{self.count}"},
{"type": "text", "text": " of ("},
]
for i, child in enumerate(self.children):
if i > 0:
messages.append({"type": "text", "text": ", "})
messages.extend(child.explain_json(state))
messages.append({"type": "text", "text": ")"})
return messages
@override
def explain_str(self, state: CollectionState | None = None) -> str:
clauses = ", ".join([c.explain_str(state) for c in self.children])
if state is None:
return f"At least {self.count} of ({clauses})"
satisfied_count = sum(1 if child(state) else 0 for child in self.children)
return f"At least {satisfied_count}/{self.count} of ({clauses})"
@override
def __str__(self) -> str:
clauses = ", ".join([str(c) for c in self.children])
return f"At least {self.count} of ({clauses})"
@dataclasses.dataclass(init=False)
class And(NestedRule[TWorld], game="Archipelago"):
"""A rule that only returns true when all child rules evaluate as true"""
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
children_to_process = [c.resolve(world) for c in self.children]
return And.from_resolved(world, [c.resolve(world) for c in self.children])
@classmethod
def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
clauses: list[Rule.Resolved] = []
items: dict[str, int] = {}
true_rule: Rule.Resolved | None = None
@@ -465,7 +593,7 @@ class And(NestedRule[TWorld], game="Archipelago"):
clauses.append(child)
if not clauses and not items:
return true_rule or False_().resolve(world)
return true_rule or True_().resolve(world)
if len(items) == 1:
item, count = next(iter(items.items()))
@@ -519,7 +647,10 @@ class Or(NestedRule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
children_to_process = [c.resolve(world) for c in self.children]
return Or.from_resolved(world, [c.resolve(world) for c in self.children])
@classmethod
def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
clauses: list[Rule.Resolved] = []
items: dict[str, int] = {}
@@ -1272,14 +1403,16 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if count <= 0:
return True_().resolve(world)
if len(self.item_names) == 0:
# match state.has_from_list
return False_().resolve(world)
if len(self.item_names) == 1:
return Has(self.item_names[0], self.count).resolve(world)
return self.Resolved(
self.item_names,
count=resolve_field(self.count, world, int),
count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@@ -1407,8 +1540,9 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if len(self.item_names) == 0 or len(self.item_names) < count:
# match state.has_from_list_unique
if count <= 0:
return True_().resolve(world)
if len(self.item_names) < count:
return False_().resolve(world)
if len(self.item_names) == 1:
return Has(self.item_names[0]).resolve(world)
@@ -1526,11 +1660,14 @@ class HasGroup(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if count <= 0:
return True_().resolve(world)
item_names = tuple(sorted(world.item_name_groups[self.item_name_group]))
return self.Resolved(
self.item_name_group,
item_names,
count=resolve_field(self.count, world, int),
count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@@ -1600,11 +1737,16 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if count <= 0:
return True_().resolve(world)
item_names = tuple(sorted(world.item_name_groups[self.item_name_group]))
if len(item_names) < count:
return False_().resolve(world)
return self.Resolved(
self.item_name_group,
item_names,
count=resolve_field(self.count, world, int),
count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
+9 -5
View File
@@ -98,6 +98,8 @@ class Group:
self._changed = True
attr = new
# resolve the path immediately when accessing it
if attr.exists():
attr.__class__.validate(attr.resolve())
return attr.__class__(attr.resolve())
return attr
@@ -633,10 +635,6 @@ class ServerOptions(Group):
class GeneratorOptions(Group):
"""Options for Generation"""
class EnemizerPath(LocalFilePath):
"""Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases"""
is_exe = True
class PlayerFilesPath(OptionalUserFolderPath):
"""Folder from which the player yaml files are pulled from"""
# created on demand, so marked as optional
@@ -644,6 +642,12 @@ class GeneratorOptions(Group):
class Players(int):
"""amount of players, 0 to infer from player files"""
class AllowQuantity(Bool):
"""
allow players to set an individual quantity for their yaml settings
with 'false' any amounts from the players will be ignored and set to 1
"""
class WeightsFilePath(str):
"""
general weights file, within the stated player_files_path location
@@ -687,9 +691,9 @@ class GeneratorOptions(Group):
start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
"""
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
players: Players = Players(0)
allow_quantity: AllowQuantity | bool = False
weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml")
meta_file_path: MetaFilePath = MetaFilePath("meta.yaml")
spoiler: Spoiler = Spoiler(3)
+2 -3
View File
@@ -201,7 +201,7 @@ if is_windows:
icon=resolve_icon(c.icon),
))
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
extra_data = ["LICENSE", "data", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
@@ -456,9 +456,8 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
for world_directory in folders_to_remove)
else:
# make sure extra programs are executable
enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core'
sni_exe = self.buildfolder / 'SNI/sni'
extra_exes = (enemizer_exe, sni_exe)
extra_exes = (sni_exe,)
for extra_exe in extra_exes:
if extra_exe.is_file():
extra_exe.chmod(0o755)
+1 -1
View File
@@ -54,7 +54,7 @@ class TestImplemented(unittest.TestCase):
def test_no_failed_world_loads(self):
if failed_world_loads:
self.fail(f"The following worlds failed to load: {failed_world_loads}")
self.fail(f"The following worlds failed to load: {failed_world_loads.keys()}")
def test_prefill_items(self):
"""Test that every world can reach every location from allstate before pre_fill."""
+145 -29
View File
@@ -12,6 +12,7 @@ from rule_builder.field_resolvers import FieldResolver, FromOption, FromWorldAtt
from rule_builder.options import Operator, OptionFilter
from rule_builder.rules import (
And,
AtLeast,
CanReachEntrance,
CanReachLocation,
CanReachRegion,
@@ -158,6 +159,14 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase):
@classvar_matrix(
rules=(
(
And(),
True_.Resolved(player=1)
),
(
Or(),
False_.Resolved(player=1)
),
(
And(Has("A", 1), Has("A", 2)),
Has.Resolved("A", 2, player=1),
@@ -250,6 +259,40 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase):
Or(HasAnyCount({"A": 1, "B": 2}), HasAnyCount({"A": 2, "B": 2})),
HasAnyCount.Resolved((("A", 1), ("B", 2)), player=1),
),
(
AtLeast(0, Has("A")),
True_.Resolved(player=1),
),
(
AtLeast(3, True_(), Has("A"), Has("B"), Has("C")),
AtLeast.Resolved(
(Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
),
),
(
AtLeast(2, False_(), Has("A"), Has("B"), Has("C")),
AtLeast.Resolved(
(Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
),
),
(
AtLeast(2, True_(), True_(), Has("A")),
True_.Resolved(player=1),
),
(
AtLeast(3, Has("A"), Has("B")),
False_.Resolved(player=1),
),
(
# This test will fail when Or(Rule, Rule) will be optimized to Rule
AtLeast(1, Rule(), Rule()),
Or.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
),
(
# This test will fail when And(Rule, Rule) will be optimized to Rule
AtLeast(2, Rule(), Rule()),
And.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
),
)
)
class TestSimplify(RuleBuilderTestCase):
@@ -416,6 +459,15 @@ class TestHashes(RuleBuilderTestCase):
rule2 = HasAll("2", "2", "2", "1")
self.assertEqual(hash(rule1.resolve(world)), hash(rule2.resolve(world)))
def test_hash_collision(self) -> None:
multiworld = setup_solo_multiworld(self.world_cls, steps=("generate_early",), seed=0)
world = multiworld.worlds[1]
rule1 = Has("A", count=1).resolve(world)
rule2 = Has("A", count=1 << 61).resolve(world)
self.assertEqual(hash(rule1), hash(rule2))
self.assertNotEqual(rule1, rule2)
self.assertNotEqual(id(rule1), id(rule2))
class TestCaching(CachedRuleBuilderTestCase):
multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable]
@@ -622,6 +674,24 @@ class TestRules(RuleBuilderTestCase):
self.state.remove(item)
self.assertFalse(resolved_rule(self.state))
def test_at_least(self) -> None:
# Has has to be relied on as True_ and False_ would be optimized out
rule = AtLeast(2, Has("Item 1"), Has("Item 1"), Has("Item 2"), Has("Item 3"))
resolved_rule = rule.resolve(self.world)
self.world.register_rule_dependencies(resolved_rule)
item1 = self.world.create_item("Item 1")
item2 = self.world.create_item("Item 2")
item3 = self.world.create_item("Item 3")
self.assertFalse(resolved_rule(self.state))
self.state.collect(item1)
self.assertTrue(resolved_rule(self.state))
self.state.collect(item2)
self.assertTrue(resolved_rule(self.state))
self.state.remove(item1)
self.assertFalse(resolved_rule(self.state))
self.state.collect(item3)
self.assertTrue(resolved_rule(self.state))
def test_has_all(self) -> None:
rule = HasAll("Item 1", "Item 2")
resolved_rule = rule.resolve(self.world)
@@ -797,8 +867,13 @@ class TestSerialization(RuleBuilderTestCase):
OptionFilter(ChoiceOption, ChoiceOption.option_second, "ge"),
],
),
AtLeast(
FromWorldAttr("instance_data.at_least_requirement"),
Has("i15", count=2),
HasGroup("g2", count=3),
),
CanReachEntrance("e1"),
HasGroupUnique("g2", count=5),
HasGroupUnique("g3", count=5),
)
rule_dict: ClassVar[dict[str, Any]] = {
@@ -922,6 +997,29 @@ class TestSerialization(RuleBuilderTestCase):
},
],
},
{
"rule": "AtLeast",
"options": [],
"filtered_resolution": False,
"count": {"resolver": "FromWorldAttr", "name": "instance_data.at_least_requirement"},
"children": [
{
"rule": "Has",
"options": [],
"filtered_resolution": False,
"args": {
"item_name": "i15",
"count": 2,
},
},
{
"rule": "HasGroup",
"options": [],
"filtered_resolution": False,
"args": {"item_name_group": "g2", "count": 3},
},
],
},
{
"rule": "CanReachEntrance",
"options": [],
@@ -932,7 +1030,7 @@ class TestSerialization(RuleBuilderTestCase):
"rule": "HasGroupUnique",
"options": [],
"filtered_resolution": False,
"args": {"item_name_group": "g2", "count": 5},
"args": {"item_name_group": "g3", "count": 5},
},
],
}
@@ -964,9 +1062,15 @@ class TestExplain(RuleBuilderTestCase):
),
player=1,
),
HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
AtLeast.Resolved(
children=(
HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
),
count=2,
player=1,
),
HasFromListUnique.Resolved(("Item 13", "Item 14"), player=1),
HasGroup.Resolved("Group 1", ("Item 15", "Item 16", "Item 17"), player=1),
HasGroupUnique.Resolved("Group 2", ("Item 18", "Item 19"), count=2, player=1),
@@ -1031,6 +1135,9 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": "0/2"},
{"type": "text", "text": " of ("},
{"type": "text", "text": "Missing "},
{"type": "color", "color": "cyan", "text": "some"},
{"type": "text", "text": " of ("},
@@ -1041,7 +1148,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "salmon", "text": "Item 7"},
{"type": "text", "text": " x5"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Missing "},
{"type": "color", "color": "cyan", "text": "all"},
{"type": "text", "text": " of ("},
@@ -1052,7 +1159,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "salmon", "text": "Item 9"},
{"type": "text", "text": " x3"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "salmon", "text": "0/2"},
{"type": "text", "text": " items from ("},
@@ -1063,6 +1170,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ", "},
{"type": "color", "color": "salmon", "text": "Item 12"},
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "salmon", "text": "0/1"},
@@ -1129,6 +1237,9 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": "3/2"},
{"type": "text", "text": " of ("},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "all"},
{"type": "text", "text": " of ("},
@@ -1139,7 +1250,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "green", "text": "Item 7"},
{"type": "text", "text": " x5"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "some"},
{"type": "text", "text": " of ("},
@@ -1150,7 +1261,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "green", "text": "Item 9"},
{"type": "text", "text": " x3"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "green", "text": "30/2"},
{"type": "text", "text": " items from ("},
@@ -1161,6 +1272,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ", "},
{"type": "color", "color": "green", "text": "Item 12"},
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "green", "text": "2/1"},
@@ -1195,7 +1307,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "salmon", "text": "False"},
{"type": "text", "text": ")"},
]
assert self.resolved_rule.explain_json(self.state) == expected
self.assertEqual(self.resolved_rule.explain_json(self.state), expected)
def test_explain_json_without_state(self) -> None:
expected: list[JSONMessagePart] = [
@@ -1223,6 +1335,9 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": "2"},
{"type": "text", "text": " of ("},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "all"},
{"type": "text", "text": " of ("},
@@ -1232,7 +1347,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "item_name", "flags": 1, "text": "Item 7", "player": 1},
{"type": "text", "text": " x5"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "any"},
{"type": "text", "text": " of ("},
@@ -1242,7 +1357,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "item_name", "flags": 1, "text": "Item 9", "player": 1},
{"type": "text", "text": " x3"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "2"},
{"type": "text", "text": "x items from ("},
@@ -1252,6 +1367,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ", "},
{"type": "item_name", "flags": 1, "text": "Item 12", "player": 1},
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "1"},
@@ -1285,16 +1401,16 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "salmon", "text": "False"},
{"type": "text", "text": ")"},
]
assert self.resolved_rule.explain_json() == expected
self.assertEqual(self.resolved_rule.explain_json(), expected)
def test_explain_str_with_state_no_items(self) -> None:
expected = (
"((Missing 4x Item 1",
"| Missing some of (Missing: Item 2, Item 3)",
"| Missing all of (Missing: Item 4, Item 5))",
"& Missing some of (Missing: Item 6 x1, Item 7 x5)",
"& Missing all of (Missing: Item 8 x2, Item 9 x3)",
"& Has 0/2 items from (Missing: Item 10, Item 11, Item 12)",
"& At least 0/2 of (Missing some of (Missing: Item 6 x1, Item 7 x5),",
"Missing all of (Missing: Item 8 x2, Item 9 x3),",
"Has 0/2 items from (Missing: Item 10, Item 11, Item 12))",
"& Has 0/1 unique items from (Missing: Item 13, Item 14)",
"& Has 0/1 items from Group 1",
"& Has 0/2 unique items from Group 2",
@@ -1304,7 +1420,7 @@ class TestExplain(RuleBuilderTestCase):
"& True",
"& False)",
)
assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
def test_explain_str_with_state_all_items(self) -> None:
self._collect_all()
@@ -1313,9 +1429,9 @@ class TestExplain(RuleBuilderTestCase):
"((Has 4x Item 1",
"| Has all of (Found: Item 2, Item 3)",
"| Has some of (Found: Item 4, Item 5))",
"& Has all of (Found: Item 6 x1, Item 7 x5)",
"& Has some of (Found: Item 8 x2, Item 9 x3)",
"& Has 30/2 items from (Found: Item 10, Item 11, Item 12)",
"& At least 3/2 of (Has all of (Found: Item 6 x1, Item 7 x5),",
"Has some of (Found: Item 8 x2, Item 9 x3),",
"Has 30/2 items from (Found: Item 10, Item 11, Item 12))",
"& Has 2/1 unique items from (Found: Item 13, Item 14)",
"& Has 30/1 items from Group 1",
"& Has 2/2 unique items from Group 2",
@@ -1325,16 +1441,16 @@ class TestExplain(RuleBuilderTestCase):
"& True",
"& False)",
)
assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
def test_explain_str_without_state(self) -> None:
expected = (
"((Has 4x Item 1",
"| Has all of (Item 2, Item 3)",
"| Has any of (Item 4, Item 5))",
"& Has all of (Item 6 x1, Item 7 x5)",
"& Has any of (Item 8 x2, Item 9 x3)",
"& Has 2x items from (Item 10, Item 11, Item 12)",
"& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
"Has any of (Item 8 x2, Item 9 x3),",
"Has 2x items from (Item 10, Item 11, Item 12))",
"& Has a unique item from (Item 13, Item 14)",
"& Has an item from Group 1",
"& Has 2x unique items from Group 2",
@@ -1344,16 +1460,16 @@ class TestExplain(RuleBuilderTestCase):
"& True",
"& False)",
)
assert self.resolved_rule.explain_str() == " ".join(expected)
self.assertEqual(self.resolved_rule.explain_str(), " ".join(expected))
def test_str(self) -> None:
expected = (
"((Has 4x Item 1",
"| Has all of (Item 2, Item 3)",
"| Has any of (Item 4, Item 5))",
"& Has all of (Item 6 x1, Item 7 x5)",
"& Has any of (Item 8 x2, Item 9 x3)",
"& Has 2x items from (Item 10, Item 11, Item 12)",
"& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
"Has any of (Item 8 x2, Item 9 x3),",
"Has 2x items from (Item 10, Item 11, Item 12))",
"& Has a unique item from (Item 13, Item 14)",
"& Has an item from Group 1",
"& Has 2x unique items from Group 2",
@@ -1363,7 +1479,7 @@ class TestExplain(RuleBuilderTestCase):
"& True",
"& False)",
)
assert str(self.resolved_rule) == " ".join(expected)
self.assertEqual(str(self.resolved_rule), " ".join(expected))
@classvar_matrix(
+5
View File
@@ -33,4 +33,9 @@ class TestBase(unittest.TestCase):
cls.app = raw_app
def setUp(self) -> None:
from WebHostLib.models import db
from pony.orm import db_session
with db_session:
for entity in db.entities.values():
entity.select().delete(bulk=True)
self.client = self.app.test_client()
+107
View File
@@ -0,0 +1,107 @@
from datetime import timedelta
from uuid import UUID, uuid4
from pony.orm import db_session, commit
from Utils import utcnow
from WebHostLib.autolauncher import cleanup
from WebHostLib.models import Room, Seed, Slot
from . import TestBase
class TestCleanup(TestBase):
def test_cleanup_unowned(self) -> None:
with db_session:
s1 = Seed(id=uuid4(), multidata=b"", owner=UUID(int=0))
Room(id=uuid4(), owner=UUID(int=0), seed=s1)
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4()) # Owned
Room(id=uuid4(), owner=UUID(int=0), seed=s2) # Unowned room of owned seed
Seed(id=uuid4(), multidata=b"", owner=UUID(int=0)) # Unowned seed with no rooms
commit()
cleanup({"ROOM_AUTO_DELETE": 0})
with db_session:
self.assertEqual(Room.select().count(), 0) # Both rooms were unowned
self.assertEqual(Seed.select().count(), 1) # s2 is owned
self.assertIsNotNone(Seed.get(id=s2.id))
def test_cleanup_auto_delete(self) -> None:
now = utcnow()
old_time = now - timedelta(days=10)
recent_time = now - timedelta(days=2)
with db_session:
# Case 1: Old room, owned
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
# Case 2: Recent room, owned
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
r2 = Room(id=uuid4(), owner=uuid4(), seed=s2, last_activity=recent_time)
# Case 3: Old seed, no rooms, owned
s3 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
# Case 4: Recent seed, no rooms, owned
s4 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=recent_time)
# Case 5: Old seed with recent room (should not be deleted)
s5 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
r5 = Room(id=uuid4(), owner=uuid4(), seed=s5, last_activity=recent_time)
commit()
# Delete items older than 5 days
cleanup({"ROOM_AUTO_DELETE": 5})
with db_session:
self.assertIsNone(Room.get(id=r1.id), "Old room should be deleted")
self.assertIsNotNone(Room.get(id=r2.id), "Recent room should NOT be deleted")
self.assertIsNone(Seed.get(id=s3.id), "Old seed without rooms should be deleted")
self.assertIsNotNone(Seed.get(id=s4.id), "Recent seed without rooms should NOT be deleted")
self.assertIsNotNone(Seed.get(id=s5.id), "Old seed with recent room should NOT be deleted")
self.assertIsNotNone(Room.get(id=r5.id), "Recent room for old seed should NOT be deleted")
# Seeds are deleted if they have NO rooms AND are old.
# After r1 is deleted, s1 has no rooms. Since it's old, it should be deleted.
self.assertIsNone(Seed.get(id=s1.id), "Old seed whose only room was deleted should be deleted")
def test_cleanup_disabled(self) -> None:
now = utcnow()
old_time = now - timedelta(days=10)
with db_session:
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
commit()
cleanup({"ROOM_AUTO_DELETE": 0})
with db_session:
self.assertIsNotNone(Room.get(id=r1.id), "Room should NOT be deleted when auto-delete is 0")
self.assertIsNotNone(Seed.get(id=s1.id), "Seed should NOT be deleted when auto-delete is 0")
def test_cleanup_slots(self) -> None:
now = utcnow()
old_time = now - timedelta(days=10)
with db_session:
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
slot1 = Slot(player_id=1, player_name="P1", seed=s1, game="TestGame")
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=now)
slot2 = Slot(player_id=2, player_name="P2", seed=s2, game="TestGame")
commit()
# Delete items older than 5 days
cleanup({"ROOM_AUTO_DELETE": 5})
with db_session:
self.assertIsNone(Seed.get(id=s1.id), "Old seed should be deleted")
self.assertIsNone(Slot.get(id=slot1.id), "Slot of deleted seed should be deleted")
self.assertIsNotNone(Seed.get(id=s2.id), "Recent seed should NOT be deleted")
self.assertIsNotNone(Slot.get(id=slot2.id), "Slot of recent seed should NOT be deleted")
+89
View File
@@ -0,0 +1,89 @@
import os
import unittest
from socket import socket as Socket # noqa: N812
from Utils import is_macos
from WebHostLib.customserver import RandomPortSocketCreator
ci = bool(os.environ.get("CI"))
class TestPortAllocating(unittest.TestCase):
def test_parse_game_ports(self) -> None:
"""Ensure that game ports with ranges are parsed correctly"""
val = RandomPortSocketCreator._parse_game_ports(("1000-2000", "2000-5000", "1000-2000", 20, 40, "20", "0"))
self.assertCountEqual(val.valid_ports,
[*range(1000, 2001), *range(2000, 5001), *range(1000, 2001), 20, 40, 20],
"The parsed game ports are not the expected length")
self.assertTrue(val.ephemeral_allowed, "The ephemeral allowed flag is not set even though it was passed")
val = RandomPortSocketCreator._parse_game_ports(())
self.assertListEqual(val.valid_ports, [], "Empty list of game port returned something")
self.assertFalse(val.ephemeral_allowed, "Empty list returned that ephemeral is allowed")
val = RandomPortSocketCreator._parse_game_ports((0,))
self.assertListEqual(val.valid_ports, [], "Empty list of ranges returned something")
self.assertTrue(val.ephemeral_allowed, "List with just 0 is not allowing ephemeral ports")
val = RandomPortSocketCreator._parse_game_ports((1,))
self.assertListEqual(val.valid_ports, [1], "Valid ports doesn't contain the expected values")
self.assertFalse(val.ephemeral_allowed, "List with just single port returned that ephemeral is allowed")
def test_parse_game_port_errors(self) -> None:
"""Ensure that game ports with incorrect values raise the expected error"""
with self.assertRaises(ValueError, msg="Negative numbers didn't get interpreted as an invalid range"):
RandomPortSocketCreator._parse_game_ports(tuple("-50215"))
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number"):
RandomPortSocketCreator._parse_game_ports(tuple("dwafawg"))
with self.assertRaises(
ValueError,
msg="A range with an extra dash at the end didn't get interpreted as an invalid number because of it's end dash"
):
RandomPortSocketCreator._parse_game_ports(tuple("20-21215-"))
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number for the start of a range"):
RandomPortSocketCreator._parse_game_ports(tuple("f-21215"))
def test_random_port_socket_edge_cases(self) -> None:
"""Verify if edge cases on creation of random port socket is working fine"""
# Try giving an empty tuple and fail over it
creator = RandomPortSocketCreator(())
with self.assertRaises(OSError) as err:
creator.create("127.0.0.1")
self.assertEqual(err.exception.errno, 98, "Raised an unexpected error code")
self.assertEqual(err.exception.strerror, "No available ports", "Raised an unexpected error string")
# Try only having ephemeral ports enabled
creator = RandomPortSocketCreator(("0",))
try:
creator.create("127.0.0.1").close()
except OSError as err:
self.assertEqual(err.errno, 98, "Raised an unexpected error code")
# If it returns our error string that means something is wrong with our code
self.assertNotEqual(err.strerror, "No available ports",
"Raised an unexpected error string")
@unittest.skipUnless(ci, "can't guarantee free ports outside of CI")
def test_random_port_socket(self) -> None:
"""Verify if returned sockets use the correct port ranges"""
creator = RandomPortSocketCreator(("8080-8085",))
sockets: list[Socket] = []
for _ in range(6):
socket = creator.create("127.0.0.1")
sockets.append(socket)
_, port = socket.getsockname()
self.assertIn(port, range(8080, 8086), "Port of socket was not inside the expected range")
for s in sockets:
s.close()
sockets.clear()
creator = RandomPortSocketCreator(("30000-65535",))
length = 5_000 if is_macos else (30_000 - len(creator._get_used_ports()))
for _ in range(length):
socket = creator.create("127.0.0.1")
sockets.append(socket)
_, port = socket.getsockname()
self.assertIn(port, range(30_000, 65536), "Port of socket was not inside the expected range")
for s in sockets:
s.close()
+6 -12
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
import enum
import hashlib
import logging
import pathlib
@@ -21,15 +20,6 @@ if TYPE_CHECKING:
from NetUtils import GamesPackage, MultiData
from settings import Group
class FillerReason(enum.StrEnum):
undefined = enum.auto()
item_link = enum.auto()
panic_fill = enum.auto()
start_inventory_from_pool = enum.auto()
world = enum.auto()
perf_logger = logging.getLogger("performance")
@@ -363,6 +353,8 @@ class World(metaclass=AutoWorldRegister):
"""path it was loaded from"""
world_version: ClassVar[Version] = Version(0, 0, 0)
"""Optional world version loaded from archipelago.json"""
manifest: ClassVar[dict[str, Any]] = {}
"""Mapping of the world's archipelago.json manifest. Use game and world_version attrs instead for those values."""
def __init__(self, multiworld: "MultiWorld", player: int):
assert multiworld is not None
@@ -522,7 +514,9 @@ class World(metaclass=AutoWorldRegister):
def get_filler_item_name(self) -> str:
"""
Called when the item pool needs to be filled with additional items to match location count.
If core AP removes an item from your item pool, this method is called to choose a replacement item
so item count and location count remain equal.
For example: plando, item_links and start_inventory_from_pool are features that may cause this.
Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of.
For most worlds this will be one or more of your filler items, but the classification of these items
@@ -587,7 +581,7 @@ class World(metaclass=AutoWorldRegister):
pass
# following methods should not need to be overridden.
def create_filler(self, reason: FillerReason = FillerReason.undefined) -> "Item":
def create_filler(self) -> "Item":
return self.create_item(self.get_filler_item_name())
# convenience methods
+16 -5
View File
@@ -11,7 +11,7 @@ import json
from pathlib import Path
from types import ModuleType
from typing import List, Sequence
from zipfile import BadZipFile
from zipfile import ZipFile, BadZipFile
from NetUtils import DataPackage
from Utils import local_path, user_path, Version, version_tuple, tuplize_version, messagebox
@@ -33,7 +33,7 @@ __all__ = [
]
failed_world_loads: List[str] = []
failed_world_loads: dict[str, str] = {}
@dataclasses.dataclass(order=True)
@@ -68,8 +68,9 @@ class WorldSource:
print(f"Could not load world {self}:", file=file_like)
traceback.print_exc(file=file_like)
file_like.seek(0)
logging.exception(file_like.read())
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
reason = file_like.read()
logging.exception(reason)
failed_world_loads[os.path.basename(self.path).rsplit(".", 1)[0]] = reason
return False
@@ -118,6 +119,7 @@ for world_source in world_sources:
game = manifest.get("game")
if game in AutoWorldRegister.world_types:
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0"))
AutoWorldRegister.world_types[game].manifest = manifest
if apworlds:
# encapsulation for namespace / gc purposes
@@ -128,7 +130,7 @@ if apworlds:
def fail_world(game_name: str, reason: str, add_as_failed_to_load: bool = True) -> None:
if add_as_failed_to_load:
failed_world_loads.append(game_name)
failed_world_loads[game_name] = reason
logging.warning(reason)
for apworld_source in apworlds:
@@ -199,6 +201,15 @@ if apworlds:
# world could fail to load at this point
if apworld.world_version:
AutoWorldRegister.world_types[apworld.game].world_version = apworld.world_version
assert apworld.path
with ZipFile(apworld.path, "r") as zf:
manifest = apworld.read_contents(zf)
# version/compatible_version shouldn't be needed by world, makes it consistent with folder world
manifest.pop("version", None)
manifest.pop("compatible_version", None)
AutoWorldRegister.world_types[apworld.game].manifest = manifest
load_apworlds()
del load_apworlds
+8 -7
View File
@@ -50,16 +50,17 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t
## FAQ/Common Issues
### The game is not connecting when starting a new save!
For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
(rocket icon) in-game, and re-enable the mod.
### The game is crashing on startup repeatedly!
This is a common issue on older versions of the game, caused by the game failing to interface with the Steam Workshop.
To fix it you can try the following (from least to most effort required)
- Subscribe to any random workshop mod, then unsubscribe from it
- Restart Steam
- Restart your computer
- Delete the game's config directory from the files `steamapps/common/HatinTime/HatinTimeGame/Config` then verify the game files
- Reinstall the game
### Why do relics disappear from the stands in the Spaceship after they're completed?
This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that
a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed
after being completed to allow for the placement of more relics without being potentially locked out.
The level that the relic set unlocked will stay unlocked.
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
if you have too many save files. Delete them and it should fix the problem.
+478
View File
@@ -0,0 +1,478 @@
from __future__ import annotations
from dataclasses import dataclass, field
from functools import lru_cache
import hashlib
import random
from typing import TYPE_CHECKING, Optional
from Utils import pc_to_snes, snes_to_pc
from .enemizer_data.base_patch_data import ENEMIZER_BASE_PATCHES
from .enemizer_data.symbols import ENEMIZER_SYMBOLS
if TYPE_CHECKING:
from . import ALTTPWorld
from .Rom import LocalRom
@dataclass(frozen=True)
class BossPatchData:
pointer: tuple[int, int]
graphics: int
sprite_array: tuple[int, ...]
@dataclass(frozen=True)
class DungeonBossPatchData:
room_id: int
sprite_pointer_address: int
shell_x: int
shell_y: int
clear_layer2: bool = False
extra_sprites: tuple[int, ...] = ()
gt_sprite_write_address: Optional[int] = None
@dataclass
class RoomObjectTable:
header_byte_0: int
header_byte_1: int
layer_1_objects: list[bytes] = field(default_factory=list)
layer_1_doors: list[bytes] = field(default_factory=list)
layer_2_objects: list[bytes] = field(default_factory=list)
layer_2_doors: list[bytes] = field(default_factory=list)
layer_3_objects: list[bytes] = field(default_factory=list)
layer_3_doors: list[bytes] = field(default_factory=list)
@classmethod
def from_rom(cls, rom: "LocalRom", start_address: int) -> "RoomObjectTable":
table = cls(rom.read_byte(start_address), rom.read_byte(start_address + 1))
layers = (
(table.layer_1_objects, table.layer_1_doors),
(table.layer_2_objects, table.layer_2_doors),
(table.layer_3_objects, table.layer_3_doors),
)
index = start_address + 2
for objects, doors in layers:
is_door = False
while True:
if rom.read_bytes(index, 2) == bytearray((0xF0, 0xFF)):
is_door = True
index += 2
continue
if rom.read_bytes(index, 2) == bytearray((0xFF, 0xFF)):
index += 2
break
if is_door:
doors.append(bytes(rom.read_bytes(index, 2)))
index += 2
else:
objects.append(bytes(rom.read_bytes(index, 3)))
index += 3
return table
def add_shell(self, x: int, y: int, clear_layer_2: bool, shell_id: int) -> None:
self.header_byte_0 = 0xF0
if clear_layer_2:
self.layer_2_objects.clear()
self.layer_2_objects.append(_build_subtype_3_object(x, y, shell_id))
def remove_shell(self, shell_id: int) -> None:
self.layer_2_objects = [obj for obj in self.layer_2_objects if _object_id(obj) != shell_id]
def to_bytes(self) -> bytes:
output = bytearray((self.header_byte_0, self.header_byte_1))
output.extend(self._serialize_layer(self.layer_1_objects, self.layer_1_doors, is_last_layer=False))
output.extend(self._serialize_layer(self.layer_2_objects, self.layer_2_doors, is_last_layer=False))
output.extend(self._serialize_layer(self.layer_3_objects, self.layer_3_doors, is_last_layer=True))
return bytes(output)
@staticmethod
def _serialize_layer(objects: list[bytes], doors: list[bytes], is_last_layer: bool) -> bytes:
output = bytearray()
for obj in objects:
output.extend(obj)
if is_last_layer or doors:
output.extend((0xF0, 0xFF))
for door in doors:
output.extend(door)
output.extend((0xFF, 0xFF))
return bytes(output)
BOSS_PATCH_DATA: dict[str, BossPatchData] = {
"Armos": BossPatchData((0x87, 0xE8), 9, (0x05, 0x04, 0x53, 0x05, 0x07, 0x53, 0x05, 0x0A, 0x53,
0x08, 0x0A, 0x53, 0x08, 0x07, 0x53, 0x08, 0x04, 0x53,
0x08, 0xE7, 0x19)),
"Arrghus": BossPatchData((0x97, 0xD9), 20, (0x07, 0x07, 0x8C, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D)),
"Blind": BossPatchData((0x54, 0xE6), 32, (0x05, 0x09, 0xCE)),
"Helmasaur": BossPatchData((0x49, 0xE0), 21, (0x06, 0x07, 0x92)),
"Kholdstare": BossPatchData((0x01, 0xEA), 22, (0x05, 0x07, 0xA3, 0x05, 0x07, 0xA4, 0x05, 0x07, 0xA2)),
"Lanmola": BossPatchData((0xCB, 0xDC), 11, (0x07, 0x06, 0x54, 0x07, 0x09, 0x54, 0x09, 0x07, 0x54)),
"Moldorm": BossPatchData((0xC3, 0xD9), 12, (0x09, 0x09, 0x09)),
"Mothula": BossPatchData((0x31, 0xDC), 26, (0x06, 0x08, 0x88)),
"Trinexx": BossPatchData((0xBA, 0xE5), 23, (0x05, 0x07, 0xCB, 0x05, 0x07, 0xCC, 0x05, 0x07, 0xCD)),
"Vitreous": BossPatchData((0x57, 0xE4), 22, (0x05, 0x07, 0xBD)),
}
DUNGEON_BOSS_PATCH_DATA: dict[tuple[str, Optional[str]], DungeonBossPatchData] = {
("Eastern Palace", None): DungeonBossPatchData(200, 0x04D7BE, 0x2B, 0x28),
("Desert Palace", None): DungeonBossPatchData(51, 0x04D694, 0x0B, 0x28),
("Tower of Hera", None): DungeonBossPatchData(7, 0x04D63C, 0x18, 0x16),
("Palace of Darkness", None): DungeonBossPatchData(90, 0x04D6E2, 0x2B, 0x28),
("Swamp Palace", None): DungeonBossPatchData(6, 0x04D63A, 0x0B, 0x28),
("Skull Woods", None): DungeonBossPatchData(41, 0x04D680, 0x2B, 0x28),
("Thieves Town", None): DungeonBossPatchData(172, 0x04D786, 0x2B, 0x28, clear_layer2=True),
("Ice Palace", None): DungeonBossPatchData(222, 0x04D7EA, 0x2B, 0x08, clear_layer2=True),
("Misery Mire", None): DungeonBossPatchData(144, 0x04D74E, 0x0B, 0x28, clear_layer2=True),
("Turtle Rock", None): DungeonBossPatchData(164, 0x04D776, 0x0B, 0x28, clear_layer2=True),
("Ganons Tower", "bottom"): DungeonBossPatchData(
28, 0x04D666, 0x2B, 0x28, extra_sprites=(0x07, 0x07, 0xE3, 0x07, 0x08, 0xE3, 0x08, 0x07, 0xE3, 0x08, 0x08, 0xE3),
gt_sprite_write_address=0x04D87E,
),
("Ganons Tower", "middle"): DungeonBossPatchData(
108, 0x04D706, 0x0B, 0x28, extra_sprites=(0x18, 0x17, 0xD1, 0x1C, 0x03, 0xC5), gt_sprite_write_address=0x04D8B6,
),
("Ganons Tower", "top"): DungeonBossPatchData(77, 0x04D6C8, 0x18, 0x16),
}
TRINEXX_SHELL_OBJECT_ID = 0xFF2
KHOLDSTARE_SHELL_OBJECT_ID = 0xF95
TRINEXX_VANILLA_ROOM_ID = 164
KHOLDSTARE_VANILLA_ROOM_ID = 222
ENEMY_HP_TABLE_ADDRESS = 0x6B173
ENEMY_DAMAGE_TABLE_ADDRESS = 0x6B266
HIDDEN_ENEMY_CHANCE_POOL_ADDRESS = 0xD7BBB
DAMAGE_GROUP_TABLE_ADDRESS = 0x3742D
RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS = 0x301FC
RETRO_RUPEE_REPLACEMENT_SPRITE_ID = 0xDA
ARROW_REFILL_5_SPRITE_ID = 0xE1
THIEF_SPRITE_ID = 0xC4
THIEF_DEFAULT_HP = 4
VANILLA_HIDDEN_ENEMY_CHANCE_POOL = (
0x01, 0x01, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x12,
0x10, 0x01, 0x01, 0x01, 0x11, 0x01, 0x01, 0x03,
)
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL = (
0x01, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x12,
0x0F, 0x01, 0x0F, 0x0F, 0x11, 0x0F, 0x0F, 0x03,
)
EXCLUDED_ENEMY_TABLE_SPRITE_IDS = frozenset({
0x09, 0x53, 0x54, 0x70, 0x7A, 0x7B, 0x88, 0x89, 0x8C, 0x8D, 0x92,
0xA2, 0xA3, 0xA4, 0xBD, 0xBE, 0xBF, 0xCB, 0xCC, 0xCD, 0xCE, 0xD6, 0xD7,
})
ENEMY_HEALTH_RANGE_BY_KEY = {
"easy": (1, 4),
"normal": (2, 15),
"hard": (2, 25),
"expert": (4, 50),
}
_ENEMIZER_SYMBOLS: Optional[dict[str, int]] = None
BOSS_GFX_SHEET_INDEXES = {
"Agahnim1": 0x8D,
"Agahnim2": 0xB5,
"Agahnim3": 0xC8,
"Agahnim4": 0xB6,
"ArmosKnight1": 0x90,
"Ganon1": 0x94,
"Ganon2": 0xA6,
"Ganon3": 0xB4,
"Ganon4": 0xB8,
"Moldorm1": 0xA3,
"Lanmola1": 0xA4,
"Arrghus1": 0xAC,
"Mothula1": 0xAB,
"Helmasaure1": 0xAD,
"Helmasaure2": 0xB1,
"Blind1": 0xAE,
"Kholdstare1": 0xAF,
"Vitreous1": 0xB0,
"Trinexx1": 0xB2,
"Trinexx2": 0xB3,
}
BOSS_GFX_TABLE = {
"Agahnim1": (21, 190, 228),
"Agahnim2": (22, 255, 135),
"Agahnim3": (23, 220, 101),
"Agahnim4": (23, 132, 92),
"ArmosKnight1": (21, 206, 27),
"Ganon1": (21, 227, 160),
"Ganon2": (22, 186, 55),
"Ganon3": (22, 250, 199),
"Ganon4": (23, 142, 33),
"Moldorm1": (22, 175, 152),
"Lanmola1": (22, 180, 23),
"Arrghus1": (22, 214, 147),
"Mothula1": (22, 210, 84),
"Helmasaure1": (22, 219, 114),
"Helmasaure2": (22, 239, 177),
"Blind1": (22, 224, 90),
"Kholdstare1": (22, 230, 31),
"Vitreous1": (22, 235, 9),
"Trinexx1": (22, 243, 89),
"Trinexx2": (22, 246, 35),
}
TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS = 0x04B37E
TRINEXX_ICE_PROJECTILE_TILE_ADDRESS = 0xE7A5
TILE_TRAP_FLOOR_TILE_ADDRESS = 0xF3BED
def apply_enemizer_base_patch(rom: "LocalRom") -> None:
for address, patch_data in _load_enemizer_base_patches():
rom.write_bytes(address, patch_data)
_apply_trinexx_room_fixes(rom)
def patch_bosses(world: "ALTTPWorld", rom: "LocalRom") -> None:
dungeon_header_base = _get_enemizer_symbol("room_header_table")
moved_room_object_base = _get_enemizer_symbol("modified_room_object_table")
gt_dungeon_name = "Ganons Tower" if world.options.mode != "inverted" else "Inverted Ganons Tower"
gt_dungeon = world.dungeons[gt_dungeon_name]
placements = (
(world.dungeons["Eastern Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]),
(world.dungeons["Desert Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Desert Palace", None)]),
(world.dungeons["Tower of Hera"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Tower of Hera", None)]),
(world.dungeons["Palace of Darkness"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Palace of Darkness", None)]),
(world.dungeons["Swamp Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Swamp Palace", None)]),
(world.dungeons["Skull Woods"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Skull Woods", None)]),
(world.dungeons["Thieves Town"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Thieves Town", None)]),
(world.dungeons["Ice Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ice Palace", None)]),
(world.dungeons["Misery Mire"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Misery Mire", None)]),
(world.dungeons["Turtle Rock"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Turtle Rock", None)]),
(gt_dungeon.bosses["bottom"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "bottom")]),
(gt_dungeon.bosses["middle"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "middle")]),
(gt_dungeon.bosses["top"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "top")]),
)
modified_room_tables: dict[int, RoomObjectTable] = {}
for boss_name, dungeon_data in placements:
boss_data = BOSS_PATCH_DATA[boss_name]
rom.write_bytes(dungeon_data.sprite_pointer_address, boss_data.pointer)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 3, boss_data.graphics)
if boss_name == "Trinexx" and dungeon_data.room_id != TRINEXX_VANILLA_ROOM_ID:
room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
room_table.add_shell(
dungeon_data.shell_x,
dungeon_data.shell_y - 2,
dungeon_data.clear_layer2,
TRINEXX_SHELL_OBJECT_ID,
)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0x60)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x04)
if boss_name == "Kholdstare" and dungeon_data.room_id != KHOLDSTARE_VANILLA_ROOM_ID:
room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
room_table.add_shell(
dungeon_data.shell_x,
dungeon_data.shell_y,
dungeon_data.clear_layer2,
KHOLDSTARE_SHELL_OBJECT_ID,
)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0xE0)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x01)
if boss_name != "Trinexx" and dungeon_data.room_id == TRINEXX_VANILLA_ROOM_ID:
_get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(TRINEXX_SHELL_OBJECT_ID)
if boss_name != "Kholdstare" and dungeon_data.room_id == KHOLDSTARE_VANILLA_ROOM_ID:
_get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(KHOLDSTARE_SHELL_OBJECT_ID)
if dungeon_data.gt_sprite_write_address is not None:
_write_gt_boss_sprite_block(rom, dungeon_data, boss_data)
write_address = moved_room_object_base
for room_id in sorted(modified_room_tables):
table_bytes = modified_room_tables[room_id].to_bytes()
_write_room_object_pointer(rom, room_id, write_address)
rom.write_bytes(write_address, table_bytes)
write_address += len(table_bytes)
rom.write_byte(0x1B0101, 0x01)
rom.write_byte(0x04DE81, 0x00)
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
rom.write_byte(0x04DE81, 0x06)
rom.write_byte(0x1B0101, 0x00)
def _get_room_object_table(rom: "LocalRom", cache: dict[int, RoomObjectTable], room_id: int) -> RoomObjectTable:
room_table = cache.get(room_id)
if room_table is not None:
return room_table
pointer_address = 0xF8000 + (room_id * 3)
snes_address_bytes = rom.read_bytes(pointer_address, 3)
snes_address = (snes_address_bytes[2] << 16) | (snes_address_bytes[1] << 8) | snes_address_bytes[0]
room_table = RoomObjectTable.from_rom(rom, snes_to_pc(snes_address))
cache[room_id] = room_table
return room_table
def _write_gt_boss_sprite_block(rom: "LocalRom", dungeon_data: DungeonBossPatchData, boss_data: BossPatchData) -> None:
assert dungeon_data.gt_sprite_write_address is not None
rom.write_int16(dungeon_data.sprite_pointer_address, dungeon_data.gt_sprite_write_address)
sprite_block = bytearray((0x00,))
sprite_block.extend(boss_data.sprite_array)
if dungeon_data.room_id == 28 and boss_data.pointer == BOSS_PATCH_DATA["Arrghus"].pointer:
sprite_block.extend(dungeon_data.extra_sprites[:6])
else:
sprite_block.extend(dungeon_data.extra_sprites)
sprite_block.append(0xFF)
rom.write_bytes(dungeon_data.gt_sprite_write_address, sprite_block)
def _write_room_object_pointer(rom: "LocalRom", room_id: int, pc_address: int) -> None:
snes_address = pc_to_snes(pc_address)
pointer_address = 0xF8000 + (room_id * 3)
rom.write_bytes(pointer_address, (
snes_address & 0xFF,
(snes_address >> 8) & 0xFF,
(snes_address >> 16) & 0xFF,
))
def _build_subtype_3_object(x: int, y: int, object_id: int) -> bytes:
return bytes((
((x << 2) & 0xFC) | (object_id & 0x03),
((y << 2) & 0xFC) | ((object_id >> 2) & 0x03),
0xF0 | ((object_id >> 4) & 0x0F),
))
def _object_id(object_bytes: bytes) -> Optional[int]:
if len(object_bytes) != 3:
return None
if object_bytes[0] >= 0xFC:
return (object_bytes[2] & 0x3F) + 0x100
if object_bytes[2] >= 0xF8:
return 0xF00 | ((object_bytes[2] & 0x0F) << 4) | ((object_bytes[1] & 0x03) << 2) | (object_bytes[0] & 0x03)
return object_bytes[2]
def _set_enemizer_flag(rom: "LocalRom", symbol_name: str, enabled: bool) -> None:
rom.write_byte(_get_enemizer_symbol(symbol_name), 0x01 if enabled else 0x00)
def _apply_killable_thief(rom: "LocalRom") -> None:
rom.write_byte(_get_enemizer_symbol("notItemSprite_Mimic") + 4, THIEF_SPRITE_ID)
thief_hp_address = ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID
if rom.read_byte(thief_hp_address) != 0xFF:
rom.write_byte(thief_hp_address, THIEF_DEFAULT_HP)
def _randomize_enemy_health(rom: "LocalRom", rng: random.Random, enemy_health_key: str) -> None:
min_hp, max_hp = ENEMY_HEALTH_RANGE_BY_KEY[enemy_health_key]
for sprite_id in range(0xF3):
hp_address = ENEMY_HP_TABLE_ADDRESS + sprite_id
if rom.read_byte(hp_address) == 0xFF or sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
continue
rom.write_byte(hp_address, rng.randrange(min_hp, max_hp))
def _randomize_enemy_damage(rom: "LocalRom", rng: random.Random, allow_zero_damage: bool) -> None:
for sprite_id in range(0xF3):
if sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
continue
new_damage = rng.randrange(8)
if not allow_zero_damage and new_damage == 2:
continue
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + sprite_id, new_damage)
def _shuffle_damage_groups(
rom: "LocalRom",
rng: random.Random,
*,
chaos_mode: bool,
allow_zero_damage: bool,
) -> None:
min_damage = 0 if allow_zero_damage else 4
max_damage = 64 if chaos_mode else 32
for group_id in range(10):
green_mail_damage = rng.randrange(min_damage, max_damage)
if chaos_mode:
blue_mail_damage = rng.randrange(min_damage, max_damage)
red_mail_damage = rng.randrange(min_damage, max_damage)
else:
blue_mail_damage = green_mail_damage * 3 // 4
red_mail_damage = green_mail_damage * 3 // 8
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
rom.write_bytes(group_address, (green_mail_damage, blue_mail_damage, red_mail_damage))
def _update_hidden_enemy_item_table_for_retro_mode(rom: "LocalRom") -> None:
if rom.read_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS) != RETRO_RUPEE_REPLACEMENT_SPRITE_ID:
return
item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
for index in range(22):
if rom.read_byte(item_table_address + index) == ARROW_REFILL_5_SPRITE_ID:
rom.write_byte(item_table_address + index, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
def _apply_trinexx_room_fixes(rom: "LocalRom") -> None:
# Match original Enemizer's unconditional Trinexx ice-floor removal so
# blue-head projectiles do not create solid walls in non-vanilla rooms.
rom.write_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, (0xEA, 0xEA, 0xEA, 0xEA))
def _apply_randomized_tile_trap_floor_tile(rom: "LocalRom") -> None:
# Original Enemizer's RandomizeTileTrapFloorTile option changes the tile
# left behind by flying floor tile traps. AP does not currently expose or
# call this option, so keep the implementation isolated and unused.
rom.write_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, (0x88, 0x01))
rom.write_byte(TILE_TRAP_FLOOR_TILE_ADDRESS, 0x12)
def _make_native_enemizer_rng(world: "ALTTPWorld") -> random.Random:
seed_material = "|".join((
str(world.multiworld.seed),
world.multiworld.seed_name,
str(world.player),
_option_key(world.options.enemy_health),
_option_key(world.options.enemy_damage),
str(int(bool(world.options.enemy_shuffle))),
str(int(bool(world.options.bush_shuffle))),
str(int(bool(world.options.killable_thieves))),
))
seed = int.from_bytes(hashlib.sha256(seed_material.encode("utf-8")).digest()[:8], "big")
return random.Random(seed)
@lru_cache(maxsize=1)
def _load_enemizer_base_patches() -> tuple[tuple[int, bytes], ...]:
return tuple(
(entry.address, entry.patch_data)
for entry in ENEMIZER_BASE_PATCHES
)
def _option_key(option: object) -> str:
return str(getattr(option, "current_key", option))
def _get_enemizer_symbol(symbol_name: str) -> int:
global _ENEMIZER_SYMBOLS
if _ENEMIZER_SYMBOLS is None:
_ENEMIZER_SYMBOLS = _load_enemizer_symbols()
return _ENEMIZER_SYMBOLS[symbol_name]
def _load_enemizer_symbols() -> dict[str, int]:
return {
name: snes_to_pc(snes_address)
for name, snes_address in ENEMIZER_SYMBOLS.items()
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -9,6 +9,7 @@ from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
from .Bosses import place_bosses
from .Dungeons import get_dungeon_item_pool_player
from .EnemyShuffle import generate_enemy_shuffle_state
from .EntranceShuffle import connect_entrance
from .Items import item_factory, GetBeemizerItem, trap_replaceable, item_name_groups
from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_shuffle, TriforcePiecesMode, LTTPBosses
@@ -511,6 +512,8 @@ def generate_itempool(world: "ALTTPWorld"):
world.options.turtle_rock_medallion.current_key.title())
place_bosses(world)
if world.options.enemy_shuffle:
world.enemy_shuffle_state = generate_enemy_shuffle_state(world)
multiworld.itempool += items
+125
View File
@@ -0,0 +1,125 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from Utils import snes_to_pc
from .enemizer_data.pot_shuffle_data import POT_ROOMS
if TYPE_CHECKING:
from . import ALTTPWorld
from .Rom import LocalRom
POT_ITEM_POINTER_TABLE = 0xDB67
POT_KEY = 0x08
POT_ARROW = 0x09
POT_BLUE_RUPEE = 0x07
POT_SWITCH = 0x88
POT_HOLE = 0x80
@dataclass(frozen=True)
class PotData:
x: int
y: int
reserved: int
@dataclass(frozen=True)
class PotRoomData:
room_id: int
pots: tuple[PotData, ...]
items: tuple[int, ...]
@dataclass(frozen=True)
class FilledPot:
x: int
y: int
item: int
def generate_pot_shuffle(world: "ALTTPWorld") -> dict[int, tuple[FilledPot, ...]]:
room_data = _load_pot_room_data()
shuffled_pots: dict[int, tuple[FilledPot, ...]] = {}
for room in room_data:
room_items = [item for item in room.items if item != POT_HOLE]
if world.options.retro_bow:
room_items = [POT_BLUE_RUPEE if item == POT_ARROW else item for item in room_items]
empty_pots: list[PotData] = []
filled_pots: list[FilledPot] = []
for pot in room.pots:
if pot.reserved == 3:
filled_pots.append(FilledPot(pot.x, pot.y, POT_HOLE))
else:
empty_pots.append(pot)
while POT_KEY in room_items:
candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 1]
if not candidate_indices:
break
pot_index = world.random.choice(candidate_indices)
pot = empty_pots.pop(pot_index)
room_items.remove(POT_KEY)
filled_pots.append(FilledPot(pot.x, pot.y, POT_KEY))
while POT_SWITCH in room_items:
candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 2]
if not candidate_indices:
break
pot_index = world.random.choice(candidate_indices)
pot = empty_pots.pop(pot_index)
room_items.remove(POT_SWITCH)
filled_pots.append(FilledPot(pot.x, pot.y, POT_SWITCH))
while room_items and empty_pots:
pot_index = world.random.randrange(len(empty_pots))
item_index = world.random.randrange(len(room_items))
pot = empty_pots.pop(pot_index)
item = room_items.pop(item_index)
filled_pots.append(FilledPot(pot.x, pot.y, item))
shuffled_pots[room.room_id] = tuple(filled_pots)
return shuffled_pots
def apply_pot_shuffle(rom: "LocalRom", shuffled_pots: dict[int, tuple[FilledPot, ...]]) -> None:
for room_id, pots in shuffled_pots.items():
pointer_address = POT_ITEM_POINTER_TABLE + (room_id * 2)
snes_address = rom.read_byte(pointer_address) | (rom.read_byte(pointer_address + 1) << 8) | (0x01 << 16)
address = snes_to_pc(snes_address)
for index, pot in enumerate(pots):
rom.write_bytes(address + (index * 3), (pot.x, pot.y, pot.item))
def get_unique_pot_item_position(
shuffled_pots: dict[int, tuple[FilledPot, ...]],
room_id: int,
item: int,
) -> tuple[int, int]:
positions = [
(pot.x, pot.y)
for pot in shuffled_pots.get(room_id, ())
if pot.item == item
]
if len(positions) != 1:
raise ValueError(
f"Expected exactly one pot item {hex(item)} in room {hex(room_id)}, found {len(positions)}"
)
return positions[0]
def _load_pot_room_data() -> tuple[PotRoomData, ...]:
return tuple(
PotRoomData(
room_id=room.room_id,
pots=tuple(PotData(x=pot.x, y=pot.y, reserved=pot.reserved) for pot in room.pots),
items=room.items,
)
for room in POT_ROOMS
)
+132 -225
View File
@@ -15,7 +15,6 @@ import logging
import os
import random
import struct
import subprocess
import threading
import concurrent.futures
import bsdiff4
@@ -53,9 +52,6 @@ try:
except:
xxtea = None
enemizer_logger = logging.getLogger("Enemizer")
class LocalRom:
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
@@ -179,43 +175,6 @@ class LocalRom:
self.write_int32(startaddress + (i * 4), value)
check_lock = threading.Lock()
def check_enemizer(enemizercli):
if getattr(check_enemizer, "done", None):
return
if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"):
raise Exception(f"Enemizer not found at {enemizercli}, please install it. "
f"Such as https://github.com/Ijwu/Enemizer/releases")
with check_lock:
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
if getattr(check_enemizer, "done", None):
return
wanted_version = (7, 1, 0)
# version info is saved on the lib, for some reason
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
with open(library_info) as f:
info = json.load(f)
for lib in info["libraries"]:
if lib.startswith("EnemizerLibrary/"):
version = lib.split("/")[-1]
version = tuple(int(element) for element in version.split("."))
enemizer_logger.debug(f"Found Enemizer version {version}")
if version < wanted_version:
raise Exception(
f"Enemizer found at {enemizercli} is outdated ({version}) < ({wanted_version}), "
f"please update your Enemizer. "
f"Such as from https://github.com/Ijwu/Enemizer/releases")
break
else:
raise Exception(f"Could not find Enemizer library version information in {library_info}")
check_enemizer.done = True
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
userandomsprites = False
if sprite and not isinstance(sprite, Sprite):
@@ -282,174 +241,6 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
player = world.player
check_enemizer(enemizercli)
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc'))
# write options file for enemizer
options = {
'RandomizeEnemies': world.options.enemy_shuffle.value,
'RandomizeEnemiesType': 3,
'RandomizeBushEnemyChance': world.options.bush_shuffle.value,
'RandomizeEnemyHealthRange': world.options.enemy_health != 'default',
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
world.options.enemy_health.current_key],
'OHKO': False,
'RandomizeEnemyDamage': world.options.enemy_damage != 'default',
'AllowEnemyZeroDamage': True,
'ShuffleEnemyDamageGroups': world.options.enemy_damage != 'default',
'EnemyDamageChaosMode': world.options.enemy_damage == 'chaos',
'EasyModeEscape': world.options.mode == "standard",
'EnemiesAbsorbable': False,
'AbsorbableSpawnRate': 10,
'AbsorbableTypes': {
'FullMagic': True, 'SmallMagic': True, 'Bomb_1': True, 'BlueRupee': True, 'Heart': True, 'BigKey': True,
'Key': True,
'Fairy': True, 'Arrow_10': True, 'Arrow_5': True, 'Bomb_8': True, 'Bomb_4': True, 'GreenRupee': True,
'RedRupee': True
},
'BossMadness': False,
'RandomizeBosses': True,
'RandomizeBossesType': 0,
'RandomizeBossHealth': False,
'RandomizeBossHealthMinAmount': 0,
'RandomizeBossHealthMaxAmount': 300,
'RandomizeBossDamage': False,
'RandomizeBossDamageMinAmount': 0,
'RandomizeBossDamageMaxAmount': 200,
'RandomizeBossBehavior': False,
'RandomizeDungeonPalettes': False,
'SetBlackoutMode': False,
'RandomizeOverworldPalettes': False,
'RandomizeSpritePalettes': False,
'SetAdvancedSpritePalettes': False,
'PukeMode': False,
'NegativeMode': False,
'GrayscaleMode': False,
'GenerateSpoilers': False,
'RandomizeLinkSpritePalette': False,
'RandomizePots': world.options.pot_shuffle.value,
'ShuffleMusic': False,
'BootlegMagic': True,
'CustomBosses': False,
'AndyMode': False,
'HeartBeepSpeed': 0,
'AlternateGfx': False,
'ShieldGraphics': "shield_gfx/normal.gfx",
'SwordGraphics': "sword_gfx/normal.gfx",
'BeeMizer': False,
'BeesLevel': 0,
'RandomizeTileTrapPattern': False,
'RandomizeTileTrapFloorTile': False,
'AllowKillableThief': world.options.killable_thieves.value,
'RandomizeSpriteOnHit': False,
'DebugMode': False,
'DebugForceEnemy': False,
'DebugForceEnemyId': 0,
'DebugForceBoss': False,
'DebugForceBossId': 0,
'DebugOpenShutterDoors': False,
'DebugForceEnemyDamageZero': False,
'DebugShowRoomIdInRupeeCounter': False,
'UseManualBosses': True,
'ManualBosses': {
'EasternPalace': world.dungeons["Eastern Palace"].boss.enemizer_name,
'DesertPalace': world.dungeons["Desert Palace"].boss.enemizer_name,
'TowerOfHera': world.dungeons["Tower of Hera"].boss.enemizer_name,
'AgahnimsTower': 'Agahnim',
'PalaceOfDarkness': world.dungeons["Palace of Darkness"].boss.enemizer_name,
'SwampPalace': world.dungeons["Swamp Palace"].boss.enemizer_name,
'SkullWoods': world.dungeons["Skull Woods"].boss.enemizer_name,
'ThievesTown': world.dungeons["Thieves Town"].boss.enemizer_name,
'IcePalace': world.dungeons["Ice Palace"].boss.enemizer_name,
'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
'GanonsTower1':
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
'GanonsTower2':
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['middle'].enemizer_name,
'GanonsTower3':
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['top'].enemizer_name,
'GanonsTower4': 'Agahnim2',
'Ganon': 'Ganon',
}
}
rom.write_to_file(randopatch_path)
with open(options_path, 'w') as f:
json.dump(options, f)
max_enemizer_tries = 5
for i in range(max_enemizer_tries):
enemizer_seed = str(world.random.randint(0, 999999999))
enemizer_command = [os.path.abspath(enemizercli),
'--rom', randopatch_path,
'--seed', enemizer_seed,
'--binary',
'--enemizer', options_path,
'--output', enemizer_output_path]
p_open = subprocess.Popen(enemizer_command,
cwd=os.path.dirname(enemizercli),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
enemizer_logger.debug(
f"Enemizer attempt {i + 1} of {max_enemizer_tries} for player {player} using enemizer seed {enemizer_seed}")
for stdout_line in iter(p_open.stdout.readline, ""):
if i == max_enemizer_tries - 1:
enemizer_logger.warning(stdout_line.rstrip())
else:
enemizer_logger.debug(stdout_line.rstrip())
p_open.stdout.close()
return_code = p_open.wait()
if return_code:
if i == max_enemizer_tries - 1:
raise subprocess.CalledProcessError(return_code, enemizer_command)
continue
for j in range(i + 1, max_enemizer_tries):
world.random.randint(0, 999999999)
# Sacrifice all remaining random numbers that would have been used for unused enemizer tries.
# This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
break
rom.read_from_file(enemizer_output_path)
os.remove(enemizer_output_path)
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
rom.write_byte(0x04DE81, 6)
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry.
# Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on.
# Replace them with a Slime enemy if they are placed.
if world.options.key_drop_shuffle:
key_drop_enemies = {
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
}
for enemy in key_drop_enemies:
if rom.read_byte(enemy) == 0x12:
logging.debug(f"Moblin found and replaced at {enemy} in world {player}")
rom.write_byte(enemy, 0x8F)
for used in (randopatch_path, options_path):
try:
os.remove(used)
except OSError:
pass
tile_list_lock = threading.Lock()
_tile_collection_table = []
@@ -795,9 +586,13 @@ def get_nonnative_item_sprite(code: int) -> int:
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool):
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
local_random = multiworld.worlds[player].random
local_world = multiworld.worlds[player]
enemized = bool(local_world.options.boss_shuffle or local_world.options.enemy_shuffle
or local_world.options.enemy_health != 'default' or local_world.options.enemy_damage != 'default'
or local_world.options.pot_shuffle or local_world.options.bush_shuffle
or local_world.options.killable_thieves)
# patch items
@@ -1331,6 +1126,13 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
starting_max_arrows = 30
startingstate = CollectionState(multiworld)
has_blue_shield = False
has_red_shield = False
has_mirror_shield = False
progressive_shields = 0
has_blue_mail = False
has_red_mail = False
progressive_mail = 0
if startingstate.has('Silver Bow', player):
equip[0x340] = 1
@@ -1359,18 +1161,6 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
elif startingstate.has('Fighter Sword', player):
equip[0x359] = 1
if startingstate.has('Mirror Shield', player):
equip[0x35A] = 3
elif startingstate.has('Red Shield', player):
equip[0x35A] = 2
elif startingstate.has('Blue Shield', player):
equip[0x35A] = 1
if startingstate.has('Red Mail', player):
equip[0x35B] = 2
elif startingstate.has('Blue Mail', player):
equip[0x35B] = 1
if startingstate.has('Magic Upgrade (1/4)', player):
equip[0x37B] = 2
equip[0x36E] = 0x80
@@ -1383,8 +1173,6 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
'Titans Mitts', 'Power Glove', 'Progressive Glove',
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
'Red Mail', 'Blue Mail', 'Progressive Mail',
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}:
continue
@@ -1489,9 +1277,63 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
if item.name != 'Piece of Heart' or equip[0x36B] == 0:
equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0)
equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0)
elif item.name == 'Blue Shield':
has_blue_shield = True
continue
elif item.name == 'Red Shield':
has_red_shield = True
continue
elif item.name == 'Mirror Shield':
has_mirror_shield = True
continue
elif item.name == 'Progressive Shield':
progressive_shields += 1
continue
elif item.name == 'Blue Mail':
has_blue_mail = True
continue
elif item.name == 'Red Mail':
has_red_mail = True
continue
elif item.name == 'Progressive Mail':
progressive_mail += 1
continue
else:
raise RuntimeError(f'Unsupported item in starting equipment: {item.name}')
for _ in range(progressive_shields):
if has_mirror_shield:
continue
if has_red_shield and local_world.difficulty_requirements.progressive_shield_limit >= 3:
has_mirror_shield = True
continue
if has_blue_shield and local_world.difficulty_requirements.progressive_shield_limit >= 2:
has_red_shield = True
continue
if local_world.difficulty_requirements.progressive_shield_limit >= 1:
has_blue_shield = True
for _ in range(progressive_mail):
if has_red_mail:
continue
if has_blue_mail and local_world.difficulty_requirements.progressive_armor_limit >= 2:
has_red_mail = True
continue
if local_world.difficulty_requirements.progressive_armor_limit >= 1:
has_blue_mail = True
if has_mirror_shield:
equip[0x35A] = 3
elif has_red_shield:
equip[0x35A] = 2
elif has_blue_shield:
equip[0x35A] = 1
if has_red_mail:
equip[0x35B] = 2
elif has_blue_mail:
equip[0x35B] = 1
equip[0x343] = min(equip[0x343], starting_max_bombs)
rom.write_byte(0x180034, starting_max_bombs)
equip[0x377] = min(equip[0x377], starting_max_arrows)
@@ -1710,6 +1552,71 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
if encoded_players > ROM_PLAYER_LIMIT:
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
if enemized:
from . import EnemizerPatches as enemizer_patches
from .EnemyShuffle import apply_enemy_shuffle
from .PotShuffle import apply_pot_shuffle
enemizer_patches.apply_enemizer_base_patch(rom)
enemy_shuffle_enabled = bool(local_world.options.enemy_shuffle)
bush_shuffle_enabled = bool(local_world.options.bush_shuffle)
enemy_health_key = enemizer_patches._option_key(local_world.options.enemy_health)
enemy_damage_key = enemizer_patches._option_key(local_world.options.enemy_damage)
if enemy_shuffle_enabled or bush_shuffle_enabled:
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
hidden_enemy_chance_pool = (
enemizer_patches.RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL
if bush_shuffle_enabled
else enemizer_patches.VANILLA_HIDDEN_ENEMY_CHANCE_POOL
)
rom.write_bytes(enemizer_patches.HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
enemizer_patches._update_hidden_enemy_item_table_for_retro_mode(rom)
if enemy_shuffle_enabled:
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
rom.write_bytes(0x1F2D5, (0x54, 0x9C))
rom.write_byte(0x1F2E5, 0xB0)
rom.write_byte(0x1F2EB, 0xD0)
if local_world.options.killable_thieves:
enemizer_patches._apply_killable_thief(rom)
if enemy_health_key != "default" or enemy_damage_key != "default":
rng = enemizer_patches._make_native_enemizer_rng(local_world)
else:
rng = None
if enemy_health_key != "default":
assert rng is not None
enemizer_patches._randomize_enemy_health(rom, rng, enemy_health_key)
if enemy_damage_key != "default":
assert rng is not None
enemizer_patches._randomize_enemy_damage(rom, rng, allow_zero_damage=True)
enemizer_patches._shuffle_damage_groups(
rom,
rng,
chaos_mode=enemy_damage_key == "chaos",
allow_zero_damage=True,
)
enemy_shuffle_state = getattr(local_world, "enemy_shuffle_state", None)
if local_world.options.enemy_shuffle and enemy_shuffle_state is not None:
apply_enemy_shuffle(rom, enemy_shuffle_state)
if local_world.options.boss_shuffle:
# Boss shuffle must run after enemy shuffle so boss room sprite pointers
# and graphics block IDs are not restored to the enemy-shuffled room values.
enemizer_patches.patch_bosses(local_world, rom)
pot_shuffle_state = getattr(local_world, "pot_shuffle_state", None)
if local_world.options.pot_shuffle and pot_shuffle_state is not None:
apply_pot_shuffle(rom, pot_shuffle_state)
# Write title screen Code
hashint = int(rom.get_hash(), 16)
code = [
@@ -1860,7 +1767,7 @@ def apply_oof_sfx(rom: LocalRom, oof: str):
rom.write_bytes(0x12803A, oof_bytes)
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
# Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
# Preserve SPC $3188 instead of writing the unused "WHAT" sound effect there.
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
+8 -29
View File
@@ -14,9 +14,10 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import ALTTPOptions, small_key_shuffle
from .PotShuffle import generate_pot_shuffle
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
is_main_entrance, key_drop_data
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
from .Rom import LocalRom, patch_rom, patch_race_rom, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules
from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name
@@ -253,17 +254,6 @@ class ALTTPWorld(World):
create_items = generate_itempool
_enemizer_path: typing.ClassVar[typing.Optional[str]] = None
@property
def enemizer_path(self) -> str:
# TODO: directly use settings
cls = self.__class__
if cls._enemizer_path is None:
cls._enemizer_path = settings.get_settings().generator.enemizer_path
assert isinstance(cls._enemizer_path, str)
return cls._enemizer_path
# custom instance vars
dungeon_local_item_names: typing.Set[str]
dungeon_specific_item_names: typing.Set[str]
@@ -305,6 +295,8 @@ class ALTTPWorld(World):
self.required_medallions = ["Ether", "Quake"]
self.escape_assist = []
self.shops = []
self.enemy_shuffle_state = None
self.pot_shuffle_state = None
self.logical_heart_containers = 10
self.logical_heart_pieces = 24
super(ALTTPWorld, self).__init__(*args, **kwargs)
@@ -316,10 +308,6 @@ class ALTTPWorld(World):
raise FileNotFoundError(rom_file)
if multiworld.is_race:
import xxtea # noqa
for player in multiworld.get_game_players(cls.game):
if multiworld.worlds[player].use_enemizer:
check_enemizer(multiworld.worlds[player].enemizer_path)
break
def generate_early(self):
multiworld = self.multiworld
@@ -339,6 +327,9 @@ class ALTTPWorld(World):
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
if self.options.pot_shuffle:
self.pot_shuffle_state = generate_pot_shuffle(self)
if self.options.mode == 'standard':
if self.options.small_key_shuffle:
if (self.options.small_key_shuffle not in
@@ -564,13 +555,6 @@ class ALTTPWorld(World):
def stage_generate_output(cls, multiworld, output_directory):
push_shop_inventories(multiworld)
@property
def use_enemizer(self) -> bool:
return bool(self.options.boss_shuffle or self.options.enemy_shuffle
or self.options.enemy_health != 'default' or self.options.enemy_damage != 'default'
or self.options.pot_shuffle or self.options.bush_shuffle
or self.options.killable_thieves)
def generate_output(self, output_directory: str):
multiworld = self.multiworld
player = self.player
@@ -578,14 +562,9 @@ class ALTTPWorld(World):
self.pushed_shop_inventories.wait()
try:
use_enemizer = self.use_enemizer
rom = LocalRom(get_base_rom_path())
patch_rom(multiworld, rom, player, use_enemizer)
if use_enemizer:
patch_enemizer(self, rom, self.enemizer_path, output_directory)
patch_rom(multiworld, rom, player)
if multiworld.is_race:
patch_race_rom(rom, multiworld, player)
+27
View File
@@ -0,0 +1,27 @@
These modules are vendored/generated from the upstream Enemizer compiled release and source that were already present
locally in `/home/alchav/PycharmProjects/Archipelago/EnemizerCLI` and `/home/alchav/PycharmProjects/Archipelago/Enemizer`.
Source details:
- Upstream project: `Ijwu/Enemizer`
- Release family: `7.1`
- Library version from `EnemizerCLI/EnemizerCLI.Core.deps.json`: `EnemizerLibrary/7.1.0`
Vendored data modules:
- `base_patch_data.py`
- `symbols.py`
- `enemy_room_metadata.py`
- `enemy_sprite_requirements.py`
- `overworld_enemy_metadata.py`
- `dungeon_sprite_addresses.py`
- `pot_shuffle_data.py`
Purpose:
- `base_patch_data.py` contains the generated base patch Enemizer applies before feature-specific randomization.
- `symbols.py` contains the assembled symbol map consumed by Enemizer's runtime code for ROM addresses.
- `enemy_room_metadata.py` and `overworld_enemy_metadata.py` contain room and area grouping/randomization constraints.
- `enemy_sprite_requirements.py` contains the sprite metadata used by the native enemy shuffle implementation.
- `dungeon_sprite_addresses.py` contains dungeon sprite slot metadata derived from Enemizer's source tables and keyed-enemy address list.
- `pot_shuffle_data.py` contains the native pot shuffle room/item source data.
+1
View File
@@ -0,0 +1 @@
"""Native ALTTP Enemizer data modules."""
File diff suppressed because one or more lines are too long
@@ -0,0 +1,202 @@
from __future__ import annotations
from typing import NamedTuple
class DungeonSpriteAddressData(NamedTuple):
room_id: int
sprite_id_addresses: tuple[int, ...]
DUNGEON_SPRITE_ADDRESSES = (
DungeonSpriteAddressData(room_id=2, sprite_id_addresses=(317750, 317753, 317756, 317759, 317762, 317792, 317795)),
DungeonSpriteAddressData(room_id=4, sprite_id_addresses=(317803, 317806, 317809, 317812, 317827, 317839, 317842, 317845)),
DungeonSpriteAddressData(room_id=9, sprite_id_addresses=(317904, 317907, 317910)),
DungeonSpriteAddressData(room_id=10, sprite_id_addresses=(317915, 317918, 317921, 317924, 317930, 317933)),
DungeonSpriteAddressData(room_id=11, sprite_id_addresses=(317941, 317944, 317947, 317950, 317953, 317956, 317959, 317962, 317965)),
DungeonSpriteAddressData(room_id=14, sprite_id_addresses=(317978, 317981, 317984)),
DungeonSpriteAddressData(room_id=17, sprite_id_addresses=(317992, 317995, 317998, 318001, 318004, 318007, 318010, 318013)),
DungeonSpriteAddressData(room_id=19, sprite_id_addresses=(318029, 318032, 318035, 318038, 318044, 318056, 318053, 318041)),
DungeonSpriteAddressData(room_id=21, sprite_id_addresses=(318105, 318108, 318111, 318114, 318117, 318120)),
DungeonSpriteAddressData(room_id=22, sprite_id_addresses=(318125, 318128, 318131, 318134, 318137, 318140, 318143)),
DungeonSpriteAddressData(room_id=23, sprite_id_addresses=(318157, 318160, 318163, 318166, 318169, 318172)),
DungeonSpriteAddressData(room_id=25, sprite_id_addresses=(318177, 318180, 318183, 318186)),
DungeonSpriteAddressData(room_id=26, sprite_id_addresses=(318191, 318194, 318197, 318200, 318203, 318206, 318209, 318212, 318218)),
DungeonSpriteAddressData(room_id=27, sprite_id_addresses=(318232, 318235, 318238, 318241)),
DungeonSpriteAddressData(room_id=30, sprite_id_addresses=(318284, 318287, 318290, 318293, 318296, 318299)),
DungeonSpriteAddressData(room_id=31, sprite_id_addresses=(318304, 318307, 318310, 318313, 318316, 318319, 318322, 318325)),
DungeonSpriteAddressData(room_id=33, sprite_id_addresses=(318335, 318341, 318344, 318347, 318350, 318353, 318356, 318359, 318362, 318365, 318368)),
DungeonSpriteAddressData(room_id=34, sprite_id_addresses=(318373, 318376, 318379, 318382, 318385, 318388, 318391)),
DungeonSpriteAddressData(room_id=36, sprite_id_addresses=(318413, 318416, 318419, 318422, 318425, 318428, 318431)),
DungeonSpriteAddressData(room_id=38, sprite_id_addresses=(318471, 318438, 318441, 318444, 318447, 318450, 318453, 318459, 318462, 318465, 318468)),
DungeonSpriteAddressData(room_id=39, sprite_id_addresses=(318476, 318479, 318482, 318485, 318488, 318491, 318494)),
DungeonSpriteAddressData(room_id=40, sprite_id_addresses=(318511,)),
DungeonSpriteAddressData(room_id=42, sprite_id_addresses=(318530, 318533, 318536, 318539, 318542, 318545)),
DungeonSpriteAddressData(room_id=43, sprite_id_addresses=(318556, 318559, 318562, 318565, 318568, 318571)),
DungeonSpriteAddressData(room_id=46, sprite_id_addresses=(318590, 318593, 318596, 318599, 318602, 318605)),
DungeonSpriteAddressData(room_id=49, sprite_id_addresses=(318621, 318624, 318627, 318630, 318633, 318636, 318639, 318642, 318645, 318648)),
DungeonSpriteAddressData(room_id=50, sprite_id_addresses=(318653, 318656, 318659, 318662, 318665)),
DungeonSpriteAddressData(room_id=52, sprite_id_addresses=(318681, 318684, 318687, 318693, 318696, 318699, 318690)),
DungeonSpriteAddressData(room_id=53, sprite_id_addresses=(318710, 318713, 318716, 318719, 318722, 318728, 318731, 318734, 318725)),
DungeonSpriteAddressData(room_id=54, sprite_id_addresses=(318742, 318745, 318754, 318760, 318763)),
DungeonSpriteAddressData(room_id=55, sprite_id_addresses=(318777, 318780, 318783, 318786, 318792, 318795, 318798, 318801, 318789)),
DungeonSpriteAddressData(room_id=56, sprite_id_addresses=(318806, 318809, 318812, 318815, 318818, 318821, 318824)),
DungeonSpriteAddressData(room_id=57, sprite_id_addresses=(318829, 318835, 318841, 318844, 318847, 318850)),
DungeonSpriteAddressData(room_id=58, sprite_id_addresses=(318855, 318858, 318861, 318864, 318867, 318870)),
DungeonSpriteAddressData(room_id=59, sprite_id_addresses=(318875, 318878, 318881, 318884, 318887, 318890, 318893)),
DungeonSpriteAddressData(room_id=60, sprite_id_addresses=(318898, 318901, 318904)),
DungeonSpriteAddressData(room_id=61, sprite_id_addresses=(318915, 318921, 318924, 318927, 318930, 318933, 318939, 318942, 318945, 318948, 318951)),
DungeonSpriteAddressData(room_id=62, sprite_id_addresses=(318959, 318962, 318980, 318983, 318989, 318992)),
DungeonSpriteAddressData(room_id=63, sprite_id_addresses=(319000, 319006, 319009)),
DungeonSpriteAddressData(room_id=64, sprite_id_addresses=(319014, 319017, 319023, 319026, 319029)),
DungeonSpriteAddressData(room_id=65, sprite_id_addresses=(319036, 319039, 319042, 319045)),
DungeonSpriteAddressData(room_id=66, sprite_id_addresses=(319050, 319053, 319056, 319059, 319062, 319065)),
DungeonSpriteAddressData(room_id=67, sprite_id_addresses=(319070, 319073)),
DungeonSpriteAddressData(room_id=68, sprite_id_addresses=(319084, 319087, 319090, 319093, 319096, 319102)),
DungeonSpriteAddressData(room_id=69, sprite_id_addresses=(319110, 319116, 319119, 319131, 319134, 319137, 319113, 319122, 319125, 319128)),
DungeonSpriteAddressData(room_id=70, sprite_id_addresses=(319142, 319148, 319154)),
DungeonSpriteAddressData(room_id=73, sprite_id_addresses=(319161, 319164, 319167, 319170, 319173, 319176, 319182, 319185, 319188, 319191, 319194, 319197)),
DungeonSpriteAddressData(room_id=74, sprite_id_addresses=(319205, 319208)),
DungeonSpriteAddressData(room_id=75, sprite_id_addresses=(319213, 319216, 319219, 319222, 319225, 319228, 319231, 319234)),
DungeonSpriteAddressData(room_id=76, sprite_id_addresses=(319245, 319248, 319251, 319254, 319257, 319260)),
DungeonSpriteAddressData(room_id=78, sprite_id_addresses=(319270, 319273, 319276, 319279)),
DungeonSpriteAddressData(room_id=80, sprite_id_addresses=(319295, 319298, 319301)),
DungeonSpriteAddressData(room_id=81, sprite_id_addresses=(319309, 319312)),
DungeonSpriteAddressData(room_id=82, sprite_id_addresses=(319317, 319320, 319323)),
DungeonSpriteAddressData(room_id=83, sprite_id_addresses=(319328, 319331, 319334, 319337, 319340, 319343, 319346, 319349, 319352, 319355, 319358, 319361, 319364)),
DungeonSpriteAddressData(room_id=84, sprite_id_addresses=(319369, 319372, 319375, 319378, 319381, 319384, 319387, 319390)),
DungeonSpriteAddressData(room_id=85, sprite_id_addresses=(319398, 319401)),
DungeonSpriteAddressData(room_id=86, sprite_id_addresses=(319415, 319418, 319421, 319424, 319430, 319433, 319436, 319442, 319439)),
DungeonSpriteAddressData(room_id=87, sprite_id_addresses=(319447, 319450, 319453, 319456, 319459, 319462, 319468, 319471, 319474, 319477, 319480, 319483, 319486, 319489)),
DungeonSpriteAddressData(room_id=88, sprite_id_addresses=(319497, 319500, 319506, 319509, 319515, 319518, 319521)),
DungeonSpriteAddressData(room_id=89, sprite_id_addresses=(319526, 319529, 319538, 319544, 319547, 319550, 319553, 319556, 319559, 319541)),
DungeonSpriteAddressData(room_id=91, sprite_id_addresses=(319575, 319578, 319581, 319584)),
DungeonSpriteAddressData(room_id=93, sprite_id_addresses=(319615, 319618, 319621, 319624, 319627, 319633, 319636, 319639, 319651, 319642, 319645, 319648, 319630)),
DungeonSpriteAddressData(room_id=94, sprite_id_addresses=(319659, 319662, 319665, 319668)),
DungeonSpriteAddressData(room_id=95, sprite_id_addresses=(319673, 319676, 319679)),
DungeonSpriteAddressData(room_id=96, sprite_id_addresses=(319684,)),
DungeonSpriteAddressData(room_id=97, sprite_id_addresses=(319689, 319692, 319695)),
DungeonSpriteAddressData(room_id=98, sprite_id_addresses=(319700, 319703, 319706)),
DungeonSpriteAddressData(room_id=99, sprite_id_addresses=(319714, 319711)),
DungeonSpriteAddressData(room_id=100, sprite_id_addresses=(319719, 319725, 319728, 319731, 319734, 319737)),
DungeonSpriteAddressData(room_id=101, sprite_id_addresses=(319760, 319763, 319766, 319769, 319772)),
DungeonSpriteAddressData(room_id=102, sprite_id_addresses=(319777, 319783, 319786, 319795, 319798, 319801, 319804, 319810)),
DungeonSpriteAddressData(room_id=103, sprite_id_addresses=(319818, 319821, 319824, 319827, 319830, 319833, 319836, 319839, 319842)),
DungeonSpriteAddressData(room_id=104, sprite_id_addresses=(319859, 319865, 319868)),
DungeonSpriteAddressData(room_id=106, sprite_id_addresses=(319873, 319876, 319879, 319882, 319885, 319888)),
DungeonSpriteAddressData(room_id=107, sprite_id_addresses=(319899, 319902, 319905, 319911, 319914, 319917, 319920, 319923, 319926, 319929, 319932)),
DungeonSpriteAddressData(room_id=109, sprite_id_addresses=(319954, 319957, 319960, 319963, 319966, 319969, 319972, 319975, 319978)),
DungeonSpriteAddressData(room_id=110, sprite_id_addresses=(319983, 319986, 319989, 319992, 319995)),
DungeonSpriteAddressData(room_id=113, sprite_id_addresses=(320000, 320003)),
DungeonSpriteAddressData(room_id=114, sprite_id_addresses=(320011, 320017)),
DungeonSpriteAddressData(room_id=115, sprite_id_addresses=(320022, 320025, 320028, 320031, 320034, 320037)),
DungeonSpriteAddressData(room_id=116, sprite_id_addresses=(320045, 320048, 320051, 320054, 320057, 320060, 320063, 320066)),
DungeonSpriteAddressData(room_id=117, sprite_id_addresses=(320071, 320074, 320077, 320080, 320083, 320086, 320095, 320098)),
DungeonSpriteAddressData(room_id=118, sprite_id_addresses=(320106, 320109, 320112, 320115, 320121)),
DungeonSpriteAddressData(room_id=119, sprite_id_addresses=(320126, 320138, 320141)),
DungeonSpriteAddressData(room_id=123, sprite_id_addresses=(320146, 320149, 320152, 320155, 320158, 320161, 320167, 320170, 320173, 320176)),
DungeonSpriteAddressData(room_id=124, sprite_id_addresses=(320181, 320184, 320187, 320190, 320193, 320196)),
DungeonSpriteAddressData(room_id=125, sprite_id_addresses=(320216, 320219, 320225, 320228, 320234, 320222, 320231, 320204, 320207, 320210, 320213, 320222, 320231)),
DungeonSpriteAddressData(room_id=126, sprite_id_addresses=(320242, 320245, 320254, 320257)),
DungeonSpriteAddressData(room_id=128, sprite_id_addresses=(320291, 320294)),
DungeonSpriteAddressData(room_id=129, sprite_id_addresses=(320302, 320305)),
DungeonSpriteAddressData(room_id=130, sprite_id_addresses=(320310, 320313, 320316)),
DungeonSpriteAddressData(room_id=131, sprite_id_addresses=(320321, 320324, 320327, 320330, 320333, 320336, 320339, 320342, 320345, 320348)),
DungeonSpriteAddressData(room_id=132, sprite_id_addresses=(320353, 320356, 320359, 320362, 320365, 320368, 320371)),
DungeonSpriteAddressData(room_id=133, sprite_id_addresses=(320376, 320379, 320382, 320385, 320388, 320391, 320394, 320397, 320400, 320403)),
DungeonSpriteAddressData(room_id=135, sprite_id_addresses=(320410, 320413, 320416, 320419, 320434, 320437, 320440, 320446, 320422)),
DungeonSpriteAddressData(room_id=139, sprite_id_addresses=(320468, 320471, 320474, 320477, 320480)),
DungeonSpriteAddressData(room_id=140, sprite_id_addresses=(320503, 320506, 320509, 320512, 320518, 320521, 320527, 320524, 320515)),
DungeonSpriteAddressData(room_id=141, sprite_id_addresses=(320538, 320541, 320544, 320547, 320550, 320556, 320559, 320562, 320565, 320568, 320571, 320535)),
DungeonSpriteAddressData(room_id=142, sprite_id_addresses=(320579, 320582, 320585, 320588, 320591, 320594, 320597)),
DungeonSpriteAddressData(room_id=145, sprite_id_addresses=(320610, 320616, 320619, 320622, 320625, 320613)),
DungeonSpriteAddressData(room_id=146, sprite_id_addresses=(320636, 320639, 320642, 320645, 320648, 320654, 320657, 320660, 320663)),
DungeonSpriteAddressData(room_id=147, sprite_id_addresses=(320668, 320671, 320674, 320677, 320680, 320683, 320686, 320689)),
DungeonSpriteAddressData(room_id=149, sprite_id_addresses=(320694, 320697, 320700, 320703)),
DungeonSpriteAddressData(room_id=151, sprite_id_addresses=(320728,)),
DungeonSpriteAddressData(room_id=152, sprite_id_addresses=(320733, 320736, 320739, 320742, 320745)),
DungeonSpriteAddressData(room_id=153, sprite_id_addresses=(320750, 320753, 320756, 320759, 320765, 320768, 320771, 320774, 320777, 320780)),
DungeonSpriteAddressData(room_id=155, sprite_id_addresses=(320794, 320797, 320800, 320803, 320806, 320809, 320812, 320815, 320818, 320821)),
DungeonSpriteAddressData(room_id=156, sprite_id_addresses=(320826, 320829, 320832, 320835, 320838, 320841)),
DungeonSpriteAddressData(room_id=157, sprite_id_addresses=(320852, 320855, 320858, 320861, 320864, 320867, 320870, 320873)),
DungeonSpriteAddressData(room_id=158, sprite_id_addresses=(320878, 320881, 320884, 320887)),
DungeonSpriteAddressData(room_id=159, sprite_id_addresses=(320907, 320910)),
DungeonSpriteAddressData(room_id=160, sprite_id_addresses=(320915, 320918, 320921)),
DungeonSpriteAddressData(room_id=161, sprite_id_addresses=(320929, 320932, 320935, 320938, 320941, 320944, 320947, 320950)),
DungeonSpriteAddressData(room_id=165, sprite_id_addresses=(320968, 320971, 320974, 320977, 320980, 320983, 320986, 320989, 320998, 321001)),
DungeonSpriteAddressData(room_id=167, sprite_id_addresses=(321014, 321017)),
DungeonSpriteAddressData(room_id=168, sprite_id_addresses=(321022, 321025, 321028, 321031, 321034)),
DungeonSpriteAddressData(room_id=169, sprite_id_addresses=(321039, 321042, 321057, 321060, 321045, 321048, 321051, 321054)),
DungeonSpriteAddressData(room_id=170, sprite_id_addresses=(321065, 321068, 321071, 321074, 321077, 321080)),
DungeonSpriteAddressData(room_id=171, sprite_id_addresses=(321088, 321091, 321094, 321097, 321100, 321103, 321106)),
DungeonSpriteAddressData(room_id=174, sprite_id_addresses=(321116, 321119)),
DungeonSpriteAddressData(room_id=176, sprite_id_addresses=(321129, 321132, 321135, 321138, 321141, 321144, 321147, 321150, 321153, 321156, 321159, 321165, 321168)),
DungeonSpriteAddressData(room_id=177, sprite_id_addresses=(321173, 321176, 321179, 321182, 321185, 321188, 321191, 321194, 321197, 321200)),
DungeonSpriteAddressData(room_id=178, sprite_id_addresses=(321205, 321208, 321211, 321214, 321217, 321220, 321223, 321226, 321229, 321232, 321235, 321238, 321241, 321244)),
DungeonSpriteAddressData(room_id=179, sprite_id_addresses=(321249, 321252, 321255, 321258, 321261)),
DungeonSpriteAddressData(room_id=182, sprite_id_addresses=(321277, 321280, 321289, 321292, 321301, 321304)),
DungeonSpriteAddressData(room_id=183, sprite_id_addresses=(321309, 321312)),
DungeonSpriteAddressData(room_id=184, sprite_id_addresses=(321317, 321320, 321323, 321326, 321329, 321332)),
DungeonSpriteAddressData(room_id=186, sprite_id_addresses=(321342, 321345, 321348, 321351, 321354, 321357, 321360)),
DungeonSpriteAddressData(room_id=187, sprite_id_addresses=(321365, 321368, 321371, 321374, 321377, 321380, 321386, 321389, 321392, 321395, 321383)),
DungeonSpriteAddressData(room_id=188, sprite_id_addresses=(321403, 321406, 321409, 321412, 321418, 321421, 321424, 321433, 321400, 321415, 321427, 321430)),
DungeonSpriteAddressData(room_id=190, sprite_id_addresses=(321440, 321446, 321449, 321452, 321455, 321458)),
DungeonSpriteAddressData(room_id=192, sprite_id_addresses=(321471, 321474, 321477, 321480, 321486, 321489, 321492, 321495)),
DungeonSpriteAddressData(room_id=193, sprite_id_addresses=(321503, 321506, 321509, 321512, 321518, 321524, 321527, 321530, 321521, 321515, 321536)),
DungeonSpriteAddressData(room_id=194, sprite_id_addresses=(321547, 321550, 321553, 321556, 321562, 321559, 321544, 321541)),
DungeonSpriteAddressData(room_id=195, sprite_id_addresses=(321567, 321585, 321588)),
DungeonSpriteAddressData(room_id=196, sprite_id_addresses=(321605, 321608, 321611, 321614, 321617, 321620)),
DungeonSpriteAddressData(room_id=201, sprite_id_addresses=(321697, 321700, 321703)),
DungeonSpriteAddressData(room_id=203, sprite_id_addresses=(321708, 321717, 321720, 321723, 321726, 321729, 321732, 321735, 321738, 321741, 321714, 321711)),
DungeonSpriteAddressData(room_id=204, sprite_id_addresses=(321746, 321749, 321755, 321758, 321761, 321770, 321773, 321776, 321779, 321782, 321785, 321752, 321764, 321767)),
DungeonSpriteAddressData(room_id=206, sprite_id_addresses=(321790, 321793, 321799, 321802, 321805, 321808, 321811)),
DungeonSpriteAddressData(room_id=208, sprite_id_addresses=(321816, 321819, 321822, 321825, 321828, 321831, 321834, 321837, 321840, 321843, 321846)),
DungeonSpriteAddressData(room_id=209, sprite_id_addresses=(321851, 321854, 321857, 321860, 321863, 321866, 321869, 321872)),
DungeonSpriteAddressData(room_id=210, sprite_id_addresses=(321877, 321880, 321883, 321886, 321889, 321892, 321895, 321898, 321901, 321904)),
DungeonSpriteAddressData(room_id=216, sprite_id_addresses=(321937, 321940, 321943, 321946, 321949, 321952, 321955, 321958, 321961, 321964, 321967)),
DungeonSpriteAddressData(room_id=217, sprite_id_addresses=(321975, 321978, 321981, 321972)),
DungeonSpriteAddressData(room_id=218, sprite_id_addresses=(321986, 321989)),
DungeonSpriteAddressData(room_id=219, sprite_id_addresses=(321994, 321997, 322000, 322006, 322003, 322012, 322009)),
DungeonSpriteAddressData(room_id=220, sprite_id_addresses=(322020, 322023, 322026, 322029, 322032, 322035, 322047, 322017, 322038, 322041, 322044)),
DungeonSpriteAddressData(room_id=223, sprite_id_addresses=(322063, 322066)),
DungeonSpriteAddressData(room_id=224, sprite_id_addresses=(322071, 322074, 322077, 322080)),
DungeonSpriteAddressData(room_id=232, sprite_id_addresses=(322189, 322192, 322195, 322198)),
DungeonSpriteAddressData(room_id=238, sprite_id_addresses=(322213, 322216, 322219, 322222, 322225)),
DungeonSpriteAddressData(room_id=239, sprite_id_addresses=(322230, 322233, 322236)),
DungeonSpriteAddressData(room_id=249, sprite_id_addresses=(322323, 322326, 322329, 322332)),
DungeonSpriteAddressData(room_id=254, sprite_id_addresses=(322378, 322381, 322384, 322387, 322390)),
DungeonSpriteAddressData(room_id=263, sprite_id_addresses=(322444, 322447)),
DungeonSpriteAddressData(room_id=264, sprite_id_addresses=(322452, 322455, 322458, 322461)),
DungeonSpriteAddressData(room_id=267, sprite_id_addresses=(322494,)),
DungeonSpriteAddressData(room_id=269, sprite_id_addresses=(322525, 322528)),
DungeonSpriteAddressData(room_id=291, sprite_id_addresses=(322671, 322674, 322677, 322680)),
)
KEYED_SPRITE_ID_ADDRESSES = frozenset((317984,
318044,
318335,
318835,
318915,
318983,
320003,
320011,
320294,
320759,
321292,
321480,
321530,
320000,
321159,
321937,
321940,
321943,
321946,
321949,
321952,
321955,
321958,
321961,
321964,
321967,
321424,
321421,
321418))
@@ -0,0 +1,106 @@
from __future__ import annotations
from typing import NamedTuple, Optional
class RoomGroupRequirementData(NamedTuple):
group_id: Optional[int]
subgroup_0: Optional[int]
subgroup_1: Optional[int]
subgroup_2: Optional[int]
subgroup_3: Optional[int]
rooms: tuple[int, ...]
SHUTTER_ROOM_IDS = frozenset((184,
11,
27,
75,
4,
36,
182,
40,
14,
46,
62,
110,
49,
135,
68,
69,
83,
117,
133,
61,
93,
107,
109,
123,
125,
141,
150,
165,
113,
168,
216,
176,
192,
224,
178,
210,
239,
268,
291))
WATER_ROOM_IDS = frozenset((22, 40, 52, 54, 56, 70, 102))
DONT_RANDOMIZE_ROOM_IDS = frozenset((0, 1, 3, 13, 20, 32, 48, 127))
NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS = frozenset((1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 85, 96, 97, 98, 112, 113, 114, 128, 129, 130))
BOSS_ROOM_IDS = frozenset((200, 51, 108, 7, 77, 90, 6, 41, 172, 222, 144, 164, 32, 13, 0))
ROOM_GROUP_REQUIREMENTS = (
RoomGroupRequirementData(group_id=1, subgroup_0=70, subgroup_1=73, subgroup_2=28, subgroup_3=82, rooms=(228, 240)),
RoomGroupRequirementData(group_id=5, subgroup_0=75, subgroup_1=77, subgroup_2=74, subgroup_3=90, rooms=(243, 265, 270, 271, 272, 273, 282, 284, 290)),
RoomGroupRequirementData(group_id=None, subgroup_0=75, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(255, 274, 287)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, rooms=(289,)),
RoomGroupRequirementData(group_id=7, subgroup_0=75, subgroup_1=77, subgroup_2=57, subgroup_3=54, rooms=(8, 44, 276, 277)),
RoomGroupRequirementData(group_id=13, subgroup_0=81, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(85, 258, 260)),
RoomGroupRequirementData(group_id=14, subgroup_0=71, subgroup_1=73, subgroup_2=76, subgroup_3=80, rooms=(18, 261, 266)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=80, rooms=(264,)),
RoomGroupRequirementData(group_id=15, subgroup_0=79, subgroup_1=77, subgroup_2=74, subgroup_3=80, rooms=(244, 245, 257, 259, 262, 280, 281)),
RoomGroupRequirementData(group_id=18, subgroup_0=85, subgroup_1=61, subgroup_2=66, subgroup_3=67, rooms=(32, 48)),
RoomGroupRequirementData(group_id=24, subgroup_0=85, subgroup_1=26, subgroup_2=66, subgroup_3=67, rooms=(13,)),
RoomGroupRequirementData(group_id=34, subgroup_0=33, subgroup_1=65, subgroup_2=69, subgroup_3=51, rooms=(0,)),
RoomGroupRequirementData(group_id=40, subgroup_0=14, subgroup_1=None, subgroup_2=74, subgroup_3=80, rooms=(225, 256, 293, 292, 294)),
RoomGroupRequirementData(group_id=None, subgroup_0=14, subgroup_1=30, subgroup_2=None, subgroup_3=None, rooms=(291,)),
RoomGroupRequirementData(group_id=23, subgroup_0=64, subgroup_1=None, subgroup_2=None, subgroup_3=63, rooms=()),
RoomGroupRequirementData(group_id=9, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=29, rooms=(227,)),
RoomGroupRequirementData(group_id=11, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=61, rooms=()),
RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=49, rooms=()),
RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=60, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=21, subgroup_0=None, subgroup_1=None, subgroup_2=58, subgroup_3=62, rooms=()),
RoomGroupRequirementData(group_id=28, subgroup_0=None, subgroup_1=None, subgroup_2=38, subgroup_3=82, rooms=(14, 126, 142, 158, 190)),
RoomGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=48, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=26, subgroup_0=None, subgroup_1=None, subgroup_2=56, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=20, subgroup_0=None, subgroup_1=None, subgroup_2=57, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=32, subgroup_0=None, subgroup_1=44, subgroup_2=59, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=3, subgroup_0=93, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(81,)),
RoomGroupRequirementData(group_id=42, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(286,)),
RoomGroupRequirementData(group_id=10, subgroup_0=47, subgroup_1=None, subgroup_2=46, subgroup_3=None, rooms=(92, 117, 185, 217)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(54, 70, 102, 118)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=32, subgroup_2=None, subgroup_3=None, rooms=(62, 159)),
RoomGroupRequirementData(group_id=None, subgroup_0=31, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(127,)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=35, subgroup_3=None, rooms=(57, 73, 86, 87, 104, 141)),
RoomGroupRequirementData(group_id=37, subgroup_0=31, subgroup_1=None, subgroup_2=39, subgroup_3=82, rooms=(36, 180, 181, 198, 199, 214)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(23, 42, 68, 76, 86, 88, 89, 103, 104, 126, 139, 235, 251)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(23, 42, 76, 89, 103, 104, 126, 139, 235, 251)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(11, 19, 27, 30, 42, 43, 49, 61, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(11, 19, 27, 30, 42, 43, 49, 53, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(19, 35, 150, 165, 195, 197, 213)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(19, 35, 150, 165, 197, 213)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 38, 43, 64, 74, 87, 107, 123)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(38, 43, 64, 74, 87, 107, 123, 206)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(2, 88, 100, 140, 267)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 61, 68, 86, 94, 124, 149, 195)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(4, 63, 206)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(53, 55, 118)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(40,)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=37, subgroup_3=None, rooms=(151,)),
)
@@ -0,0 +1,295 @@
from __future__ import annotations
from typing import NamedTuple, Optional
class EnemySpriteRequirementData(NamedTuple):
sprite_name: str
sprite_id: int
boss: bool
overlord: bool
do_not_randomize: bool
killable: bool
npc: bool
never_use_dungeon: bool
never_use_overworld: bool
cannot_have_key: bool
is_object: bool
absorbable: bool
is_water_sprite: bool
is_enemy_sprite: bool
group_ids: tuple[int, ...]
subgroup_0: tuple[int, ...]
subgroup_1: tuple[int, ...]
subgroup_2: tuple[int, ...]
subgroup_3: tuple[int, ...]
parameters: Optional[int]
special_glitched: bool
excluded_rooms: tuple[int, ...]
dont_randomize_rooms: tuple[int, ...]
spawnable_rooms: tuple[int, ...]
ENEMY_SPRITE_REQUIREMENTS = (
EnemySpriteRequirementData(sprite_name='RavenSprite', sprite_id=0, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17, 25), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VultureSprite', sprite_id=1, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EmptySprite', sprite_id=3, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PullSwitch_GoodSprite', sprite_id=4, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PullSwitch_TrapSprite', sprite_id=6, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Octorok_OneWaySprite', sprite_id=8, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MoldormSprite', sprite_id=9, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(48,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Octorok_FourWaySprite', sprite_id=10, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ChickenSprite', sprite_id=11, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21, 80), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BuzzblobSprite', sprite_id=13, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SnapdragonSprite', sprite_id=14, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OctoballoonSprite', sprite_id=15, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OctoballoonHatchlingsSprite', sprite_id=16, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HinoxSprite', sprite_id=17, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MoblinSprite', sprite_id=18, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MiniHelmasaurSprite', sprite_id=19, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GargoylesDomainGateSprite', sprite_id=20, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AntifairySprite', sprite_id=21, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(64, 210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SahasrahlaAginahSprite', sprite_id=22, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BushHoarderSprite', sprite_id=23, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MiniMoldormSprite', sprite_id=24, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PoeSprite', sprite_id=25, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(14, 21), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DwarvesSprite', sprite_id=26, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrowInWall_MaybeSprite', sprite_id=27, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='StatueSprite', sprite_id=28, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268, 63), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WeathervaneSprite', sprite_id=29, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CrystalSwitchSprite', sprite_id=30, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BugCatchingKidSprite', sprite_id=31, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(81,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SluggulaSprite', sprite_id=32, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PushSwitchSprite', sprite_id=33, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(83,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RopaSprite', sprite_id=34, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedBariSprite', sprite_id=35, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueBariSprite', sprite_id=36, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TalkingTreeSprite', sprite_id=37, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HardhatBeetleSprite', sprite_id=38, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DeadrockSprite', sprite_id=39, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(127, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='StorytellersSprite', sprite_id=40, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlindHideoutAttendantSprite', sprite_id=41, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14, 79), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SweepingLadySprite', sprite_id=42, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MultipurposeSpriteSprite', sprite_id=43, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LumberjacksSprite', sprite_id=44, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TelepathicStones_NoIdeaWhatThisActuallyIsLikelyUnusedSprite', sprite_id=45, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FluteBoysNotesSprite', sprite_id=46, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RaceHPNPCsSprite', sprite_id=47, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Person_MaybeSprite', sprite_id=48, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FortuneTellerSprite', sprite_id=49, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AngryBrothersSprite', sprite_id=50, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PullForRupeesSpriteSprite', sprite_id=51, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ScaredGirl2Sprite', sprite_id=52, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='InnkeeperSprite', sprite_id=53, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WitchSprite', sprite_id=54, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WaterfallSprite', sprite_id=55, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrowTargetSprite', sprite_id=56, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AverageMiddleAgedManSprite', sprite_id=57, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HalfMagicBatSprite', sprite_id=58, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DashItemSprite', sprite_id=59, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VillageKidSprite', sprite_id=60, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Signs_ChickenLadyAlsoShowedUp_ScaredLadiesOutsideHousesSprite', sprite_id=61, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RockHoarderSprite', sprite_id=62, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TutorialSoldierSprite', sprite_id=63, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LightningLockSprite', sprite_id=64, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueSwordSoldier_DetectPlayerSprite', sprite_id=65, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenSwordSoldierSprite', sprite_id=66, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedSpearSoldierSprite', sprite_id=67, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AssaultSwordSoldierSprite', sprite_id=68, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenSpearSoldierSprite', sprite_id=69, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueArcherSprite', sprite_id=70, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenArcherSprite', sprite_id=71, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedJavelinSoldierSprite', sprite_id=72, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedJavelinSoldier2Sprite', sprite_id=73, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedBombSoldiersSprite', sprite_id=74, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenSoldierRecruits_HMKnightSprite', sprite_id=75, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(19,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GeldmanSprite', sprite_id=76, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RabbitSprite', sprite_id=77, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PopoSprite', sprite_id=78, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Popo2Sprite', sprite_id=79, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CannonBallsSprite', sprite_id=80, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArmosSprite', sprite_id=81, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GiantZoraSprite', sprite_id=82, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArmosKnightsSprite', sprite_id=83, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LanmolasSprite', sprite_id=84, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(49,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FireballZoraSprite', sprite_id=85, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WalkingZoraSprite', sprite_id=86, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DesertPalaceBarriersSprite', sprite_id=87, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CrabSprite', sprite_id=88, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BirdSprite', sprite_id=89, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SquirrelSprite', sprite_id=90, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Spark_LeftToRightSprite', sprite_id=91, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Spark_RightToLeftSprite', sprite_id=92, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Roller_VerticalMovingSprite', sprite_id=93, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Roller_VerticalMoving2Sprite', sprite_id=94, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RollerSprite', sprite_id=95, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Roller_HorizontalMovingSprite', sprite_id=96, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BeamosSprite', sprite_id=97, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MasterSwordSprite', sprite_id=98, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Devalant_NonShooterSprite', sprite_id=99, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Devalant_ShooterSprite', sprite_id=100, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ShootingGalleryProprietorSprite', sprite_id=101, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_RightSprite', sprite_id=102, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_LeftSprite', sprite_id=103, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_DownSprite', sprite_id=104, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_UpSprite', sprite_id=105, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BallNChainTrooperSprite', sprite_id=106, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CannonSoldierSprite', sprite_id=107, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MirrorPortalSprite', sprite_id=108, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RatSprite', sprite_id=109, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RopeSprite', sprite_id=110, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KeeseSprite', sprite_id=111, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LeeverSprite', sprite_id=113, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ActivatoForThePonds_WhereYouThrowInItemsSprite', sprite_id=114, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='UnclePriestSprite', sprite_id=115, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(71, 81), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RunningManSprite', sprite_id=116, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BottleSalesmanSprite', sprite_id=117, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PrincessZeldaSprite', sprite_id=118, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VillageElderSprite', sprite_id=120, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AgahnimSprite', sprite_id=122, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(26, 61), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AgahnimEnergyBallSprite', sprite_id=123, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FloatingStalfosHeadSprite', sprite_id=124, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BigSpikeTrapSprite', sprite_id=125, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GuruguruBar_ClockwiseSprite', sprite_id=126, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GuruguruBar_CounterClockwiseSprite', sprite_id=127, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WinderSprite', sprite_id=128, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WaterTektiteSprite', sprite_id=129, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(40,), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AntifairyCircleSprite', sprite_id=130, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenEyegoreSprite', sprite_id=131, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedEyegoreSprite', sprite_id=132, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KodongosSprite', sprite_id=134, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MothulaSprite', sprite_id=136, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MothulasBeamSprite', sprite_id=137, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SpikeTrapSprite', sprite_id=138, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(40, 11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GibdoSprite', sprite_id=139, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrghusSprite', sprite_id=140, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrghusSpawnSprite', sprite_id=141, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TerrorpinSprite', sprite_id=142, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SlimeSprite_JumpsOutOfTheFloor', sprite_id=143, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WallmasterSprite', sprite_id=144, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 96, 97, 98, 112, 113, 114, 128, 129, 130, 137, 153, 168, 169, 170, 184, 185, 186, 200, 201, 216, 217, 218, 51, 67, 83, 99, 115, 116, 117, 131, 132, 133, 7, 23, 39, 49, 119, 135, 167, 32, 48, 64, 176, 192, 208, 224, 9, 10, 11, 25, 26, 27, 42, 43, 58, 59, 74, 75, 90, 106, 6, 22, 38, 40, 52, 53, 54, 55, 56, 70, 84, 102, 118, 41, 57, 73, 86, 87, 88, 89, 103, 104, 68, 69, 100, 101, 171, 172, 187, 188, 203, 204, 219, 220, 14, 30, 31, 46, 62, 63, 78, 79, 94, 95, 110, 126, 127, 142, 158, 159, 174, 175, 190, 191, 206, 222, 144, 145, 146, 147, 151, 152, 160, 161, 162, 163, 177, 178, 179, 193, 194, 195, 209, 210, 4, 19, 20, 21, 35, 36, 164, 180, 181, 182, 183, 196, 197, 198, 199, 213, 214, 12, 13, 28, 29, 61, 76, 77, 91, 92, 93, 107, 108, 109, 123, 124, 125, 139, 140, 141, 149, 150, 155, 156, 157, 165, 166)),
EnemySpriteRequirementData(sprite_name='StalfosKnightSprite', sprite_id=145, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HelmasaurKingSprite', sprite_id=146, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(58,), subgroup_3=(62,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BumperSprite', sprite_id=147, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SwimmersEvilSprite', sprite_id=148, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EyeLaser_RightSprite', sprite_id=149, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EyeLaser_LeftSprite', sprite_id=150, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EyeLaser_DownSprite', sprite_id=151, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EyeLaser_UpSprite', sprite_id=152, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PengatorSprite', sprite_id=153, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KyameronWaterSplashSprite', sprite_id=154, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(40,), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WizzrobeSprite', sprite_id=155, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VerminHorizontalSprite', sprite_id=156, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VerminVerticalSprite', sprite_id=157, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Ostrich_HauntedGroveSprite', sprite_id=158, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FluteSprite', sprite_id=159, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Birds_HauntedGroveSprite', sprite_id=160, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FreezorSprite', sprite_id=161, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KholdstareSprite', sprite_id=162, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KholdstaresShellSprite', sprite_id=163, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FallingIceSprite', sprite_id=164, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueZazakSprite', sprite_id=165, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedZazakSprite', sprite_id=166, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='StalfosSprite', sprite_id=167, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworldSprite', sprite_id=168, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworld2Sprite', sprite_id=169, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PikitSprite', sprite_id=170, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MaidenSprite', sprite_id=171, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AppleSprite', sprite_id=172, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LostOldManSprite', sprite_id=173, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(28,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DownPipeSprite', sprite_id=174, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='UpPipeSprite', sprite_id=175, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RightPipeSprite', sprite_id=176, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LeftPipeSprite', sprite_id=177, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GoodBee_AgainMaybeSprite', sprite_id=178, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HylianInscriptionSprite', sprite_id=179, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ThiefsChestSprite', sprite_id=180, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BombSalesmanSprite', sprite_id=181, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KikiSprite', sprite_id=182, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MaidenInBlindDungeonSprite', sprite_id=183, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MimicSprite', sprite_id=184, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FeudingFriendsOnDeathMountainSprite', sprite_id=185, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WhirlpoolSprite', sprite_id=186, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(255, 274, 287)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(271, 272)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(272,)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(280,)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(293,)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(286,)),
EnemySpriteRequirementData(sprite_name='DrunkInTheInnSprite', sprite_id=188, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Vitreous_LargeEyeballSprite', sprite_id=189, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Vitreous_SmallEyeballSprite', sprite_id=190, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VitreousLightningSprite', sprite_id=191, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CatFish_QuakeMedallionSprite', sprite_id=192, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(24,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AgahnimTeleportingZeldaToDarkworldSprite', sprite_id=193, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(61,), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BouldersSprite', sprite_id=194, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Gibo_FloatingBlobSprite', sprite_id=195, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ThiefSprite', sprite_id=196, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(14, 21), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MedusaSprite', sprite_id=197, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FourWayFireballSpittersSprite', sprite_id=198, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HokkuBokkuSprite', sprite_id=199, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BigFairyWhoHealsYouSprite', sprite_id=200, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TektiteSprite', sprite_id=201, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ChainChompSprite', sprite_id=202, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TrinexxSprite', sprite_id=203, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AnotherPartOfTrinexxSprite', sprite_id=204, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='YetAnotherPartOfTrinexxSprite', sprite_id=205, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlindTheThiefSprite', sprite_id=206, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(59,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SwamolaSprite', sprite_id=207, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LynelSprite', sprite_id=208, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BunnyBeamSprite', sprite_id=209, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FloppingFishSprite', sprite_id=210, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='StalSprite', sprite_id=211, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LandmineSprite', sprite_id=212, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DiggingGameProprietorSprite', sprite_id=213, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(42,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GanonSprite', sprite_id=214, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(33,), subgroup_1=(65,), subgroup_2=(69,), subgroup_3=(51,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CopyOfGanon_ExceptInvincibleSprite', sprite_id=215, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HeartSprite', sprite_id=216, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenRupeeSprite', sprite_id=217, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueRupeeSprite', sprite_id=218, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedRupeeSprite', sprite_id=219, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BombRefill1Sprite', sprite_id=220, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BombRefill4Sprite', sprite_id=221, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BombRefill8Sprite', sprite_id=222, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SmallMagicRefillSprite', sprite_id=223, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FullMagicRefillSprite', sprite_id=224, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrowRefill5Sprite', sprite_id=225, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrowRefill10Sprite', sprite_id=226, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FairySprite', sprite_id=227, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KeySprite', sprite_id=228, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BigKeySprite', sprite_id=229, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ShieldEaterSprite', sprite_id=230, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MushroomSprite', sprite_id=231, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FakeMasterSwordSprite', sprite_id=232, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MagicShopDude_HisItemsIncludingTheMagicPowderSprite', sprite_id=233, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HeartContainerSprite', sprite_id=234, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HeartPieceSprite', sprite_id=235, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BushesSprite', sprite_id=236, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatformSprite', sprite_id=237, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MantleSprite', sprite_id=238, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(93,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused1Sprite', sprite_id=239, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused2Sprite', sprite_id=240, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused3Sprite', sprite_id=241, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MedallionTabletSprite', sprite_id=242, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OW_OL_FallingRocks', sprite_id=244, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EP4Walls', sprite_id=258, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EPEntrance', sprite_id=259, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_StalfosHeadTrap', sprite_id=261, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_BombDrop_RopeTrap', sprite_id=262, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_MovingFloor', sprite_id=263, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_SlimeDropper', sprite_id=264, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_Wallmaster', sprite_id=265, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Square', sprite_id=266, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Path', sprite_id=267, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_RightEvil_PirogusuSpawner', sprite_id=272, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_LeftEvil_PirogusuSpawner', sprite_id=273, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_DownEvil_PirogusuSpawner', sprite_id=274, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_UpEvil_PirogusuSpawner', sprite_id=275, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_FlyingFloorTileTrap', sprite_id=276, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_WizzrobeSpawner', sprite_id=277, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_BlackSpawn_Zoro_BombHole', sprite_id=278, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_4Skull_Trap_Pot', sprite_id=279, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_Stalfos_Spawn_Trap_EP', sprite_id=280, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_ArmosKnight_Trigger', sprite_id=281, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_BombDrop_BombTrap', sprite_id=282, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
)
@@ -0,0 +1,131 @@
from __future__ import annotations
from typing import NamedTuple, Optional
class OverworldGroupRequirementData(NamedTuple):
group_id: Optional[int]
subgroup_0: Optional[int]
subgroup_1: Optional[int]
subgroup_2: Optional[int]
subgroup_3: Optional[int]
areas: tuple[int, ...]
AREA_IDS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207)
DO_NOT_RANDOMIZE_AREA_IDS = frozenset((1,
4,
6,
8,
9,
11,
12,
13,
14,
25,
28,
31,
32,
33,
35,
36,
38,
39,
49,
54,
56,
57,
61,
62,
65,
68,
70,
72,
73,
75,
76,
77,
78,
89,
92,
95,
96,
97,
99,
100,
102,
103,
113,
118,
120,
121,
125,
126,
42,
106,
130,
131,
132,
133,
134,
135,
136,
137,
138,
139,
140,
141,
142,
143,
186,
250,
145,
148,
150,
152,
153,
155,
156,
158,
169,
172,
175,
176,
177,
179,
180,
182,
183,
193,
198,
200,
274,
275,
276,
277,
278,
279,
281,
288))
FORCED_GROUP_REQUIREMENTS = (
OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=74, subgroup_3=None, areas=(2,)),
OverworldGroupRequirementData(group_id=16, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=16, areas=(3, 147)),
OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=17, areas=(10, 154)),
OverworldGroupRequirementData(group_id=4, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(15, 159)),
OverworldGroupRequirementData(group_id=3, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=14, areas=(20, 164)),
OverworldGroupRequirementData(group_id=1, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=63, areas=(27, 171)),
OverworldGroupRequirementData(group_id=6, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(34, 40, 178, 184)),
OverworldGroupRequirementData(group_id=8, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=None, areas=(48, 192)),
OverworldGroupRequirementData(group_id=10, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(58, 202)),
OverworldGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=24, subgroup_3=None, areas=(79, 223)),
OverworldGroupRequirementData(group_id=21, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=21, areas=(98, 242)),
OverworldGroupRequirementData(group_id=27, subgroup_0=None, subgroup_1=42, subgroup_2=None, subgroup_3=None, areas=(104, 248)),
OverworldGroupRequirementData(group_id=13, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=None, areas=(22, 166)),
OverworldGroupRequirementData(group_id=29, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, areas=(105, 249)),
OverworldGroupRequirementData(group_id=15, subgroup_0=None, subgroup_1=None, subgroup_2=78, subgroup_3=None, areas=(42, 186)),
OverworldGroupRequirementData(group_id=17, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=76, areas=(106, 250)),
OverworldGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=55, subgroup_3=54, areas=(128, 272)),
OverworldGroupRequirementData(group_id=14, subgroup_0=None, subgroup_1=None, subgroup_2=12, subgroup_3=68, areas=(129, 273)),
OverworldGroupRequirementData(group_id=26, subgroup_0=15, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(146,)),
OverworldGroupRequirementData(group_id=23, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=25, areas=(94, 238)),
)
@@ -0,0 +1,107 @@
from __future__ import annotations
from typing import NamedTuple
class PotDataRecord(NamedTuple):
x: int
y: int
reserved: int
class PotRoomDataRecord(NamedTuple):
room_id: int
pots: tuple[PotDataRecord, ...]
items: tuple[int, ...]
POT_ROOMS = (
PotRoomDataRecord(room_id=4, pots=(PotDataRecord(x=162, y=25, reserved=0), PotDataRecord(x=152, y=25, reserved=0), PotDataRecord(x=152, y=22, reserved=0), PotDataRecord(x=162, y=22, reserved=0), PotDataRecord(x=240, y=19, reserved=0), PotDataRecord(x=204, y=19, reserved=0),), items=(10, 10)),
PotRoomDataRecord(room_id=9, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(1, 11, 136)),
PotRoomDataRecord(room_id=10, pots=(PotDataRecord(x=204, y=11, reserved=0), PotDataRecord(x=156, y=17, reserved=0), PotDataRecord(x=96, y=8, reserved=0), PotDataRecord(x=100, y=7, reserved=0), PotDataRecord(x=160, y=17, reserved=0), PotDataRecord(x=104, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0),), items=(11, 11, 136)),
PotRoomDataRecord(room_id=17, pots=(PotDataRecord(x=152, y=19, reserved=0), PotDataRecord(x=152, y=15, reserved=0), PotDataRecord(x=144, y=15, reserved=0), PotDataRecord(x=10, y=15, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=160, y=19, reserved=0),), items=(11, 11, 11, 11)),
PotRoomDataRecord(room_id=21, pots=(PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=100, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0), PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=12, y=6, reserved=0), PotDataRecord(x=16, y=6, reserved=0), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=70, y=11, reserved=0),), items=(1, 7, 9, 9, 10, 11, 12, 12, 13)),
PotRoomDataRecord(room_id=22, pots=(PotDataRecord(x=188, y=3, reserved=0), PotDataRecord(x=192, y=3, reserved=0), PotDataRecord(x=188, y=4, reserved=0), PotDataRecord(x=192, y=4, reserved=0), PotDataRecord(x=188, y=5, reserved=0), PotDataRecord(x=192, y=5, reserved=0), PotDataRecord(x=188, y=6, reserved=0), PotDataRecord(x=192, y=6, reserved=0), PotDataRecord(x=240, y=19, reserved=0),), items=(8, 9, 9, 10, 10, 11, 11, 12, 12)),
PotRoomDataRecord(room_id=26, pots=(PotDataRecord(x=232, y=19, reserved=0), PotDataRecord(x=212, y=19, reserved=0), PotDataRecord(x=28, y=5, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(10, 10, 10, 10)),
PotRoomDataRecord(room_id=33, pots=(PotDataRecord(x=100, y=28, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=48, y=28, reserved=0), PotDataRecord(x=82, y=28, reserved=0), PotDataRecord(x=160, y=20, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(11, 12, 12)),
PotRoomDataRecord(room_id=35, pots=(PotDataRecord(x=86, y=26, reserved=0), PotDataRecord(x=90, y=26, reserved=0), PotDataRecord(x=94, y=26, reserved=0), PotDataRecord(x=98, y=26, reserved=0), PotDataRecord(x=102, y=26, reserved=0),), items=(1, 10, 11)),
PotRoomDataRecord(room_id=36, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(1, 11, 12, 7)),
PotRoomDataRecord(room_id=38, pots=(PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=150, y=19, reserved=2), PotDataRecord(x=22, y=26, reserved=2), PotDataRecord(x=220, y=26, reserved=0),), items=(7, 9, 10, 12, 136)),
PotRoomDataRecord(room_id=39, pots=(PotDataRecord(x=214, y=19, reserved=0), PotDataRecord(x=214, y=20, reserved=0), PotDataRecord(x=166, y=20, reserved=0), PotDataRecord(x=214, y=21, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=102, y=17, reserved=0), PotDataRecord(x=98, y=17, reserved=0), PotDataRecord(x=106, y=17, reserved=0), PotDataRecord(x=166, y=21, reserved=0), PotDataRecord(x=166, y=19, reserved=0), PotDataRecord(x=92, y=12, reserved=0), PotDataRecord(x=160, y=12, reserved=0),), items=(1, 1, 10, 11, 7, 7)),
PotRoomDataRecord(room_id=43, pots=(PotDataRecord(x=16, y=5, reserved=2), PotDataRecord(x=44, y=5, reserved=2), PotDataRecord(x=16, y=6, reserved=2), PotDataRecord(x=44, y=6, reserved=2), PotDataRecord(x=16, y=7, reserved=2), PotDataRecord(x=44, y=7, reserved=2), PotDataRecord(x=146, y=21, reserved=0), PotDataRecord(x=170, y=21, reserved=0), PotDataRecord(x=146, y=22, reserved=0), PotDataRecord(x=170, y=22, reserved=0),), items=(9, 9, 10, 10, 10, 10, 11, 11, 11, 136)),
PotRoomDataRecord(room_id=47, pots=(PotDataRecord(x=28, y=7, reserved=0), PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=28, y=9, reserved=0), PotDataRecord(x=32, y=9, reserved=0), PotDataRecord(x=172, y=19, reserved=0), PotDataRecord(x=180, y=19, reserved=0), PotDataRecord(x=104, y=27, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(7, 7, 7, 7, 11, 11, 11, 11)),
PotRoomDataRecord(room_id=53, pots=(PotDataRecord(x=60, y=6, reserved=1), PotDataRecord(x=20, y=8, reserved=0), PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=28, y=8, reserved=0), PotDataRecord(x=32, y=8, reserved=0), PotDataRecord(x=36, y=8, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=76, y=23, reserved=1), PotDataRecord(x=88, y=23, reserved=1), PotDataRecord(x=100, y=27, reserved=1), PotDataRecord(x=242, y=28, reserved=1), PotDataRecord(x=240, y=22, reserved=1), PotDataRecord(x=76, y=28, reserved=1),), items=(7, 7, 7, 7, 7, 8, 11)),
PotRoomDataRecord(room_id=54, pots=(PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=10, y=16, reserved=0), PotDataRecord(x=114, y=16, reserved=0),), items=(8, 10, 11)),
PotRoomDataRecord(room_id=55, pots=(PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=60, y=6, reserved=0),), items=(8,)),
PotRoomDataRecord(room_id=56, pots=(PotDataRecord(x=164, y=12, reserved=0), PotDataRecord(x=164, y=13, reserved=0), PotDataRecord(x=164, y=18, reserved=0), PotDataRecord(x=164, y=19, reserved=0),), items=(8, 7, 10, 10)),
PotRoomDataRecord(room_id=57, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=100, y=22, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 9, 11, 12)),
PotRoomDataRecord(room_id=60, pots=(PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=64, y=12, reserved=0), PotDataRecord(x=20, y=14, reserved=0), PotDataRecord(x=68, y=18, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=64, y=20, reserved=0), PotDataRecord(x=64, y=26, reserved=0),), items=(1, 7, 7, 7, 7, 11, 12)),
PotRoomDataRecord(room_id=61, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=24, y=22, reserved=0), PotDataRecord(x=40, y=22, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0),), items=(9, 7, 10, 10, 11, 11, 13)),
PotRoomDataRecord(room_id=62, pots=(PotDataRecord(x=96, y=6, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=92, y=10, reserved=0),), items=(10, 11, 12, 12)),
PotRoomDataRecord(room_id=63, pots=(PotDataRecord(x=12, y=25, reserved=0), PotDataRecord(x=20, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=12, y=27, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=28, y=23, reserved=0),), items=(1, 1, 8, 10, 10, 11, 136)),
PotRoomDataRecord(room_id=65, pots=(PotDataRecord(x=100, y=10, reserved=0), PotDataRecord(x=52, y=15, reserved=0), PotDataRecord(x=52, y=16, reserved=0), PotDataRecord(x=148, y=22, reserved=0),), items=(1, 11, 12, 12)),
PotRoomDataRecord(room_id=67, pots=(PotDataRecord(x=112, y=28, reserved=1), PotDataRecord(x=76, y=28, reserved=1), PotDataRecord(x=76, y=20, reserved=1), PotDataRecord(x=66, y=4, reserved=0), PotDataRecord(x=78, y=4, reserved=0), PotDataRecord(x=66, y=9, reserved=0), PotDataRecord(x=78, y=9, reserved=0), PotDataRecord(x=112, y=20, reserved=1),), items=(8, 9, 11, 11, 12)),
PotRoomDataRecord(room_id=69, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=108, y=11, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=220, y=16, reserved=0), PotDataRecord(x=236, y=16, reserved=0),), items=(9, 9, 11, 11, 12)),
PotRoomDataRecord(room_id=73, pots=(PotDataRecord(x=156, y=27, reserved=0), PotDataRecord(x=172, y=24, reserved=0), PotDataRecord(x=172, y=23, reserved=0), PotDataRecord(x=144, y=20, reserved=0), PotDataRecord(x=104, y=15, reserved=0), PotDataRecord(x=104, y=16, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=172, y=20, reserved=0), PotDataRecord(x=144, y=27, reserved=0), PotDataRecord(x=172, y=28, reserved=0), PotDataRecord(x=160, y=27, reserved=0),), items=(11, 11, 12, 12, 12, 12)),
PotRoomDataRecord(room_id=78, pots=(PotDataRecord(x=48, y=10, reserved=2), PotDataRecord(x=140, y=11, reserved=2), PotDataRecord(x=28, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=0),), items=(136, 11, 12)),
PotRoomDataRecord(room_id=83, pots=(PotDataRecord(x=92, y=11, reserved=0), PotDataRecord(x=96, y=11, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=11, reserved=0),), items=(8, 11, 11, 12)),
PotRoomDataRecord(room_id=84, pots=(PotDataRecord(x=186, y=25, reserved=0), PotDataRecord(x=186, y=26, reserved=0), PotDataRecord(x=186, y=27, reserved=0), PotDataRecord(x=186, y=28, reserved=0),), items=(7, 11, 11, 11)),
PotRoomDataRecord(room_id=86, pots=(PotDataRecord(x=100, y=6, reserved=1), PotDataRecord(x=96, y=10, reserved=1), PotDataRecord(x=92, y=10, reserved=1), PotDataRecord(x=48, y=20, reserved=1), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=40, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=36, y=7, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=24, y=9, reserved=0), PotDataRecord(x=36, y=9, reserved=0), PotDataRecord(x=20, y=10, reserved=0), PotDataRecord(x=40, y=10, reserved=0), PotDataRecord(x=12, y=20, reserved=1),), items=(7, 7, 11, 11, 8, 12, 12, 12, 12, 12, 12)),
PotRoomDataRecord(room_id=87, pots=(PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=12, y=20, reserved=2), PotDataRecord(x=92, y=23, reserved=0), PotDataRecord(x=100, y=23, reserved=0), PotDataRecord(x=84, y=25, reserved=0), PotDataRecord(x=76, y=27, reserved=0), PotDataRecord(x=48, y=20, reserved=2), PotDataRecord(x=30, y=22, reserved=2),), items=(7, 10, 11, 12, 12, 12, 13, 136)),
PotRoomDataRecord(room_id=88, pots=(PotDataRecord(x=96, y=9, reserved=0), PotDataRecord(x=92, y=8, reserved=0), PotDataRecord(x=108, y=8, reserved=0), PotDataRecord(x=108, y=6, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=92, y=6, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=16, y=7, reserved=0), PotDataRecord(x=96, y=5, reserved=0), PotDataRecord(x=100, y=5, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=108, y=7, reserved=0), PotDataRecord(x=16, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0), PotDataRecord(x=104, y=9, reserved=0),), items=(10, 10, 11, 11, 12, 12, 12, 12)),
PotRoomDataRecord(room_id=91, pots=(PotDataRecord(x=218, y=37, reserved=0), PotDataRecord(x=222, y=37, reserved=0), PotDataRecord(x=226, y=37, reserved=0),), items=(136,)),
PotRoomDataRecord(room_id=92, pots=(PotDataRecord(x=228, y=25, reserved=0), PotDataRecord(x=104, y=24, reserved=0), PotDataRecord(x=228, y=22, reserved=0), PotDataRecord(x=216, y=25, reserved=0), PotDataRecord(x=84, y=24, reserved=0), PotDataRecord(x=216, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=94, y=26, reserved=0),), items=(10, 13)),
PotRoomDataRecord(room_id=93, pots=(PotDataRecord(x=16, y=5, reserved=0), PotDataRecord(x=44, y=5, reserved=0), PotDataRecord(x=16, y=11, reserved=0), PotDataRecord(x=44, y=11, reserved=0), PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(1, 7, 9, 9, 10, 10, 10, 12)),
PotRoomDataRecord(room_id=94, pots=(PotDataRecord(x=92, y=4, reserved=0), PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=112, y=8, reserved=0),), items=(11, 11, 12, 12)),
PotRoomDataRecord(room_id=99, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(8, 11)),
PotRoomDataRecord(room_id=100, pots=(PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=16, y=22, reserved=0), PotDataRecord(x=20, y=22, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(10, 10, 10, 10, 12, 12, 136)),
PotRoomDataRecord(room_id=102, pots=(PotDataRecord(x=48, y=37, reserved=0), PotDataRecord(x=52, y=37, reserved=0), PotDataRecord(x=56, y=37, reserved=0), PotDataRecord(x=84, y=5, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=48, y=38, reserved=0), PotDataRecord(x=52, y=38, reserved=0), PotDataRecord(x=56, y=38, reserved=0), PotDataRecord(x=84, y=6, reserved=0), PotDataRecord(x=104, y=6, reserved=0),), items=(7, 7, 9, 9, 9, 10, 10, 10, 11, 11)),
PotRoomDataRecord(room_id=103, pots=(PotDataRecord(x=22, y=26, reserved=0), PotDataRecord(x=18, y=22, reserved=0), PotDataRecord(x=92, y=9, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=48, y=7, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=74, y=20, reserved=0), PotDataRecord(x=18, y=23, reserved=0), PotDataRecord(x=18, y=26, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(9, 11, 11, 11, 12, 12, 12)),
PotRoomDataRecord(room_id=104, pots=(PotDataRecord(x=84, y=14, reserved=0), PotDataRecord(x=84, y=13, reserved=0), PotDataRecord(x=88, y=12, reserved=0), PotDataRecord(x=88, y=6, reserved=0), PotDataRecord(x=88, y=5, reserved=0), PotDataRecord(x=88, y=4, reserved=0), PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=64, y=15, reserved=0), PotDataRecord(x=64, y=7, reserved=0), PotDataRecord(x=88, y=7, reserved=0), PotDataRecord(x=64, y=16, reserved=0), PotDataRecord(x=64, y=24, reserved=0), PotDataRecord(x=64, y=25, reserved=0),), items=(11, 11, 11, 12, 12)),
PotRoomDataRecord(room_id=115, pots=(PotDataRecord(x=154, y=21, reserved=0), PotDataRecord(x=158, y=21, reserved=0), PotDataRecord(x=20, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=144, y=24, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0), PotDataRecord(x=154, y=27, reserved=0), PotDataRecord(x=158, y=27, reserved=0),), items=(1, 1, 11, 11, 7, 7, 9, 9, 12, 136)),
PotRoomDataRecord(room_id=116, pots=(PotDataRecord(x=30, y=5, reserved=0), PotDataRecord(x=62, y=5, reserved=0), PotDataRecord(x=94, y=5, reserved=0), PotDataRecord(x=14, y=11, reserved=0), PotDataRecord(x=46, y=11, reserved=0), PotDataRecord(x=78, y=11, reserved=0), PotDataRecord(x=110, y=11, reserved=0),), items=(9, 9, 11, 11, 12, 12, 136)),
PotRoomDataRecord(room_id=117, pots=(PotDataRecord(x=148, y=22, reserved=0), PotDataRecord(x=160, y=22, reserved=0), PotDataRecord(x=172, y=22, reserved=0),), items=(9, 11, 12)),
PotRoomDataRecord(room_id=123, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=76, y=7, reserved=0), PotDataRecord(x=60, y=4, reserved=0), PotDataRecord(x=64, y=4, reserved=0),), items=(11, 8)),
PotRoomDataRecord(room_id=124, pots=(PotDataRecord(x=36, y=21, reserved=0), PotDataRecord(x=24, y=11, reserved=0), PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=32, y=4, reserved=0),), items=(11, 11)),
PotRoomDataRecord(room_id=125, pots=(PotDataRecord(x=44, y=12, reserved=0), PotDataRecord(x=44, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=108, y=20, reserved=0), PotDataRecord(x=114, y=20, reserved=0), PotDataRecord(x=76, y=28, reserved=0),), items=(9, 10, 10, 11)),
PotRoomDataRecord(room_id=126, pots=(PotDataRecord(x=86, y=15, reserved=0), PotDataRecord(x=82, y=26, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=104, y=26, reserved=0),), items=(11, 12, 136)),
PotRoomDataRecord(room_id=130, pots=(PotDataRecord(x=50, y=5, reserved=0), PotDataRecord(x=50, y=10, reserved=0), PotDataRecord(x=76, y=50, reserved=0),), items=(11,)),
PotRoomDataRecord(room_id=131, pots=(PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=80, y=4, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0),), items=(1, 7, 9, 9)),
PotRoomDataRecord(room_id=132, pots=(PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=60, y=17, reserved=0), PotDataRecord(x=80, y=14, reserved=0), PotDataRecord(x=44, y=14, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=24, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=100, y=7, reserved=0),), items=(9, 9)),
PotRoomDataRecord(room_id=135, pots=(PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=112, y=20, reserved=0), PotDataRecord(x=16, y=12, reserved=0), PotDataRecord(x=40, y=12, reserved=0), PotDataRecord(x=32, y=12, reserved=0), PotDataRecord(x=24, y=12, reserved=0), PotDataRecord(x=16, y=11, reserved=0),), items=(12, 13)),
PotRoomDataRecord(room_id=139, pots=(PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=76, y=12, reserved=1), PotDataRecord(x=32, y=23, reserved=1), PotDataRecord(x=28, y=23, reserved=1), PotDataRecord(x=112, y=12, reserved=1), PotDataRecord(x=32, y=9, reserved=1), PotDataRecord(x=76, y=28, reserved=0),), items=(8, 11, 12)),
PotRoomDataRecord(room_id=140, pots=(PotDataRecord(x=76, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=2), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=92, y=20, reserved=0), PotDataRecord(x=100, y=21, reserved=0), PotDataRecord(x=104, y=26, reserved=0), PotDataRecord(x=88, y=27, reserved=0),), items=(9, 10, 10, 10, 10, 12, 136)),
PotRoomDataRecord(room_id=145, pots=(PotDataRecord(x=84, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0),), items=(11, 12)),
PotRoomDataRecord(room_id=150, pots=(PotDataRecord(x=14, y=18, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=32, y=17, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=76, y=21, reserved=0), PotDataRecord(x=112, y=21, reserved=0), PotDataRecord(x=14, y=24, reserved=0),), items=(11, 12, 12, 13)),
PotRoomDataRecord(room_id=155, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(8, 12)),
PotRoomDataRecord(room_id=157, pots=(PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=40, y=9, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=84, y=4, reserved=0),), items=(10, 12)),
PotRoomDataRecord(room_id=159, pots=(PotDataRecord(x=138, y=20, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=138, y=21, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=138, y=27, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=21, reserved=0), PotDataRecord(x=178, y=20, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=178, y=27, reserved=0), PotDataRecord(x=178, y=26, reserved=0), PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=138, y=26, reserved=0), PotDataRecord(x=20, y=21, reserved=0),), items=(8, 11, 11, 11, 11, 11, 136)),
PotRoomDataRecord(room_id=161, pots=(PotDataRecord(x=96, y=27, reserved=0), PotDataRecord(x=92, y=21, reserved=0), PotDataRecord(x=150, y=6, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=12, reserved=0), PotDataRecord(x=108, y=13, reserved=0), PotDataRecord(x=112, y=14, reserved=0), PotDataRecord(x=96, y=23, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(8, 11, 11, 11, 12, 12)),
PotRoomDataRecord(room_id=168, pots=(PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=30, y=24, reserved=0),), items=(1, 11)),
PotRoomDataRecord(room_id=169, pots=(PotDataRecord(x=12, y=19, reserved=0), PotDataRecord(x=112, y=19, reserved=0), PotDataRecord(x=144, y=43, reserved=0), PotDataRecord(x=236, y=43, reserved=0), PotDataRecord(x=144, y=44, reserved=0), PotDataRecord(x=236, y=44, reserved=0), PotDataRecord(x=16, y=20, reserved=0), PotDataRecord(x=108, y=20, reserved=0),), items=(11, 11, 11, 9, 9, 9)),
PotRoomDataRecord(room_id=170, pots=(PotDataRecord(x=212, y=10, reserved=2), PotDataRecord(x=232, y=10, reserved=2), PotDataRecord(x=232, y=5, reserved=2), PotDataRecord(x=212, y=5, reserved=2), PotDataRecord(x=94, y=8, reserved=2), PotDataRecord(x=108, y=55, reserved=0), PotDataRecord(x=108, y=56, reserved=0), PotDataRecord(x=108, y=57, reserved=0),), items=(11, 11, 11, 11, 136)),
PotRoomDataRecord(room_id=176, pots=(PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=24, y=24, reserved=0), PotDataRecord(x=44, y=25, reserved=0), PotDataRecord(x=20, y=21, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=16, y=23, reserved=0), PotDataRecord(x=44, y=23, reserved=0), PotDataRecord(x=36, y=24, reserved=0), PotDataRecord(x=16, y=25, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(1, 1, 7, 7, 9, 9, 10, 10, 11, 11)),
PotRoomDataRecord(room_id=179, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(8, 12, 136)),
PotRoomDataRecord(room_id=180, pots=(PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(11, 13)),
PotRoomDataRecord(room_id=181, pots=(PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=112, y=15, reserved=0), PotDataRecord(x=76, y=16, reserved=0), PotDataRecord(x=112, y=16, reserved=0), PotDataRecord(x=112, y=17, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 10, 11, 11, 13, 136)),
PotRoomDataRecord(room_id=184, pots=(PotDataRecord(x=96, y=13, reserved=0), PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=104, y=16, reserved=0),), items=(11, 11, 136)),
PotRoomDataRecord(room_id=185, pots=(PotDataRecord(x=92, y=18, reserved=0), PotDataRecord(x=96, y=18, reserved=0), PotDataRecord(x=104, y=18, reserved=0), PotDataRecord(x=108, y=18, reserved=0),), items=(1, 1, 7, 7)),
PotRoomDataRecord(room_id=186, pots=(PotDataRecord(x=100, y=8, reserved=0), PotDataRecord(x=88, y=8, reserved=0), PotDataRecord(x=94, y=4, reserved=0), PotDataRecord(x=76, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=76, y=10, reserved=0), PotDataRecord(x=112, y=10, reserved=0), PotDataRecord(x=94, y=12, reserved=0),), items=(1, 1, 8, 11, 11, 12)),
PotRoomDataRecord(room_id=188, pots=(PotDataRecord(x=138, y=3, reserved=2), PotDataRecord(x=178, y=3, reserved=2), PotDataRecord(x=86, y=4, reserved=1), PotDataRecord(x=102, y=4, reserved=1), PotDataRecord(x=138, y=12, reserved=2), PotDataRecord(x=178, y=12, reserved=2), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(7, 7, 7, 7, 8, 10, 10, 10, 10, 10, 11, 11, 136)),
PotRoomDataRecord(room_id=191, pots=(PotDataRecord(x=40, y=20, reserved=0), PotDataRecord(x=44, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 10, 11, 12, 12, 12)),
PotRoomDataRecord(room_id=192, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=12, y=14, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=28, y=27, reserved=0),), items=(1, 7, 10, 11)),
PotRoomDataRecord(room_id=194, pots=(PotDataRecord(x=180, y=7, reserved=0), PotDataRecord(x=100, y=46, reserved=0), PotDataRecord(x=68, y=48, reserved=0), PotDataRecord(x=64, y=52, reserved=0),), items=(1, 9, 12, 136)),
PotRoomDataRecord(room_id=196, pots=(PotDataRecord(x=84, y=9, reserved=0), PotDataRecord(x=24, y=14, reserved=0), PotDataRecord(x=56, y=17, reserved=0), PotDataRecord(x=84, y=17, reserved=0), PotDataRecord(x=12, y=21, reserved=0), PotDataRecord(x=76, y=23, reserved=0), PotDataRecord(x=48, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0),), items=(1, 9, 12, 7, 10, 10, 11, 11)),
PotRoomDataRecord(room_id=199, pots=(PotDataRecord(x=12, y=10, reserved=0), PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(9, 12, 11, 13)),
PotRoomDataRecord(room_id=201, pots=(PotDataRecord(x=30, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=60, y=22, reserved=0),), items=(1, 1, 136)),
PotRoomDataRecord(room_id=203, pots=(PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=88, y=28, reserved=0),), items=(7, 11)),
PotRoomDataRecord(room_id=204, pots=(PotDataRecord(x=36, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 11, 7, 10)),
PotRoomDataRecord(room_id=206, pots=(PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=80, y=8, reserved=0), PotDataRecord(x=108, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=204, y=11, reserved=3),), items=(9, 12, 12, 10, 128)),
PotRoomDataRecord(room_id=208, pots=(PotDataRecord(x=158, y=5, reserved=0), PotDataRecord(x=140, y=11, reserved=0), PotDataRecord(x=42, y=13, reserved=0), PotDataRecord(x=48, y=16, reserved=0), PotDataRecord(x=176, y=20, reserved=0), PotDataRecord(x=146, y=23, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(1, 1, 7, 11, 11, 12, 12)),
PotRoomDataRecord(room_id=209, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=168, y=7, reserved=0), PotDataRecord(x=112, y=12, reserved=0),), items=(9, 1, 1, 1, 13)),
PotRoomDataRecord(room_id=214, pots=(PotDataRecord(x=92, y=22, reserved=0), PotDataRecord(x=96, y=22, reserved=0),), items=(10, 13)),
PotRoomDataRecord(room_id=216, pots=(PotDataRecord(x=202, y=8, reserved=0), PotDataRecord(x=242, y=8, reserved=0), PotDataRecord(x=202, y=10, reserved=0), PotDataRecord(x=242, y=10, reserved=0), PotDataRecord(x=202, y=12, reserved=0), PotDataRecord(x=242, y=12, reserved=0), PotDataRecord(x=92, y=24, reserved=0), PotDataRecord(x=96, y=24, reserved=0),), items=(9, 9, 9, 9, 9, 11, 11, 11)),
PotRoomDataRecord(room_id=218, pots=(PotDataRecord(x=24, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=24, y=25, reserved=0), PotDataRecord(x=36, y=25, reserved=0),), items=(9, 9, 11, 136)),
PotRoomDataRecord(room_id=219, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=62, y=19, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=88, y=16, reserved=0),), items=(7, 11)),
PotRoomDataRecord(room_id=220, pots=(PotDataRecord(x=56, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=68, y=16, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(7, 9, 10, 11)),
PotRoomDataRecord(room_id=235, pots=(PotDataRecord(x=206, y=8, reserved=0), PotDataRecord(x=210, y=8, reserved=0), PotDataRecord(x=88, y=14, reserved=0), PotDataRecord(x=92, y=14, reserved=0), PotDataRecord(x=96, y=14, reserved=0),), items=(7, 7, 11, 12, 12)),
)
+171
View File
@@ -0,0 +1,171 @@
from __future__ import annotations
ENEMIZER_SYMBOLS = {
':pos_1_0': 0x36975E,
':pos_1_1': 0x36976D,
':pos_1_10': 0x369819,
':pos_1_11': 0x369828,
':pos_1_12': 0x369837,
':pos_1_13': 0x369845,
':pos_1_14': 0x36984B,
':pos_1_15': 0x369851,
':pos_1_16': 0x369873,
':pos_1_17': 0x369898,
':pos_1_18': 0x36989E,
':pos_1_19': 0x3698A4,
':pos_1_2': 0x369787,
':pos_1_20': 0x3698C6,
':pos_1_21': 0x3698EB,
':pos_1_22': 0x3698F1,
':pos_1_23': 0x3698F7,
':pos_1_24': 0x369919,
':pos_1_25': 0x36993E,
':pos_1_26': 0x369944,
':pos_1_27': 0x36994A,
':pos_1_28': 0x36996C,
':pos_1_29': 0x369AA5,
':pos_1_3': 0x369796,
':pos_1_30': 0x36B796,
':pos_1_4': 0x3697A5,
':pos_1_5': 0x3697B4,
':pos_1_6': 0x3697D5,
':pos_1_7': 0x3697E8,
':pos_1_8': 0x3697F7,
':pos_1_9': 0x369806,
'CheckIfLinkShouldDie': 0x3699B2,
'CheckIfLinkShouldDie_dead': 0x3699BB,
'CheckIfLinkShouldDie_done': 0x3699BD,
'Check_for_Blind_Fight': 0x1DA085,
'CopyShield': 0x36B713,
'CopyShield_loop_copy': 0x36B729,
'CopyShield_shield_positon_gfx': 0x36B74B,
'CopySword': 0x36B6D1,
'CopySword_loop_copy': 0x36B6E7,
'CopySword_sword_positon_gfx': 0x36B709,
'DMAKholdstare': 0x3695A5,
'DMATrinexx': 0x369617,
'Dungeon_ResetSprites': 0x09C114,
'EnemizerCodeStart': 0x3694F5,
'EnemizerFlags': 0x368100,
'EnemizerFlags_agahnim_fun_balls': 0x368104,
'EnemizerFlags_close_blind_door': 0x368101,
'EnemizerFlags_enable_mimic_override': 0x368105,
'EnemizerFlags_enable_terrorpin_ai_fix': 0x368106,
'EnemizerFlags_moldorm_eye_count': 0x368102,
'EnemizerFlags_randomize_bushes': 0x368100,
'EnemizerFlags_randomize_sprites': 0x368103,
'EnemizerTablesStart': 0x368000,
'Ext_OnBossDeath': 0x29803C,
'Ext_OnDungeonCompleted': 0x29804B,
'Ext_OnDungeonEnter': 0x298041,
'Ext_OnDungeonExit': 0x298046,
'Ext_OnFairyRevive': 0x298019,
'Ext_OnFileCreate': 0x298000,
'Ext_OnFileLoad': 0x298005,
'Ext_OnFileSave': 0x29800A,
'Ext_OnIemMenuOpen': 0x298023,
'Ext_OnItemChange': 0x29802D,
'Ext_OnItemMenuClose': 0x298028,
'Ext_OnMapUse': 0x298014,
'Ext_OnPlayerAttack': 0x298037,
'Ext_OnPlayerDamaged': 0x298032,
'Ext_OnPlayerDeath': 0x29800F,
'Ext_OnYItemUse': 0x29801E,
'Ext_OnZeldaRescued': 0x298050,
'FixTerrorpin': 0x36971A,
'FixTerrorpin_new': 0x369728,
'GFX_Kholdstare_Shell': 0x36C79A,
'GFX_Trinexx_Shell': 0x36D79A,
'GFX_Trinexx_Shell2': 0x36DF9A,
'GetRandomInt': 0x0DBA71,
'Initialize_Blind_Fight': 0x1DA090,
'Kholdstare_Draw': 0x0DD97F,
'LoadFile': 0x369A87,
'LoadNewSoundFx': 0x369A9E,
'LoadOverworldSprites': 0x36B753,
'Module_LoadFile_indoors': 0x028118,
'Moldorm_UpdateOamPosition': 0x3699E7,
'Moldorm_UpdateOamPosition_more_eyes': 0x3699ED,
'NMIHookAction': 0x36956C,
'NMIHookAction_loadKholdstare': 0x369584,
'NMIHookAction_loadTrinexx': 0x369590,
'NMIHookAction_return': 0x36959A,
'NMIHookReturn': 0x0080D5,
'NewLoadSoundBank': 0x369A98,
'NewLoadSoundBank_Intro': 0x369A92,
'OnInitFileSelect': 0x3696FA,
'OnInitFileSelect_continue': 0x36970F,
'Player_Main': 0x078000,
'Sound_LoadSongBank': 0x008888,
'Sound_SetSfx3PanLong': 0x0DBB8A,
'Sound_SetSfxPanWithPlayerCoords': 0x0DBB67,
'Spawn_Bees': 0x36B75D,
'Spawn_Bees_done': 0x36B779,
'SpritePrep_Eyegore': 0x1EC6FA,
'SpritePrep_EyegoreNew': 0x369A1A,
'SpritePrep_EyegoreNew_mimic': 0x369A31,
'SpritePrep_EyegoreNew_new': 0x369A25,
'Sprite_ResetAll': 0x09C44E,
'Sprite_SpawnDynamically': 0x1DF65D,
'VitreousKeyReset': 0x36B77A,
'boss_move': 0x369743,
'boss_move_loop_bottom_left': 0x369935,
'boss_move_loop_bottom_left2': 0x369963,
'boss_move_loop_bottom_right': 0x3698E2,
'boss_move_loop_bottom_right2': 0x369910,
'boss_move_loop_middle': 0x36983C,
'boss_move_loop_middle2': 0x36986A,
'boss_move_loop_top_right': 0x36988F,
'boss_move_loop_top_right2': 0x3698BD,
'boss_move_move_to_bottom_left': 0x369933,
'boss_move_move_to_bottom_right': 0x3698E0,
'boss_move_move_to_middle': 0x36983A,
'boss_move_move_to_top_right': 0x36988D,
'boss_move_no_blind_door': 0x3697D2,
'boss_move_no_change': 0x369863,
'boss_move_no_change2': 0x3698B6,
'boss_move_no_change3': 0x369909,
'boss_move_no_change4': 0x36995C,
'boss_move_no_change_ov': 0x369885,
'boss_move_no_change_ov2': 0x3698D8,
'boss_move_no_change_ov3': 0x36992B,
'boss_move_no_change_ov4': 0x36997E,
'boss_move_return': 0x369986,
'change_heartcontainer_position': 0x3699BE,
'change_heartcontainer_position_not_moldorm_room': 0x3699E1,
'check_blind_boss_room': 0x36B782,
'check_special_action': 0x369731,
'check_special_action_no_special_action': 0x36973E,
'enemizer_info_table': 0x368000,
'linkIsDead': 0x0780D5,
'linkNotDead': 0x0780F7,
'modified_room_object_table': 0x36B79A,
'moved_room_header_bank_value_address': 0x368374,
'newKodongoCollision': 0x369A02,
'newKodongoCollision_continue': 0x369A19,
'new_kholdstare_code': 0x369987,
'new_kholdstare_code_already_iced': 0x369997,
'new_trinexx_code': 0x36999C,
'new_trinexx_code_already_rocked': 0x3699AC,
'notItemSprite_Mimic': 0x369A66,
'notItemSprite_Mimic_changeSpriteId': 0x369A7A,
'notItemSprite_Mimic_continue': 0x369A82,
'notItemSprite_Mimic_reloadSpriteIdAndSkipMimic': 0x369A7F,
'resetSprite_Mimic': 0x369A4E,
'resetSprite_Mimic_notMimic': 0x369A60,
'room_header_table': 0x368375,
'shieldgfx': 0x36AAD1,
'sprite_bush_spawn': 0x3694F5,
'sprite_bush_spawn_continue': 0x36950F,
'sprite_bush_spawn_dontGoPhase2': 0x369565,
'sprite_bush_spawn_item_table': 0x369525,
'sprite_bush_spawn_newSpriteSpawn': 0x36954C,
'sprite_bush_spawn_not_random': 0x36953B,
'sprite_bush_spawn_not_random_old': 0x36950B,
'sprite_bush_spawn_return': 0x369568,
'sprite_bush_spawn_table': 0x368120,
'sprite_bush_spawn_table_dungeons': 0x368248,
'sprite_bush_spawn_table_overworld': 0x368120,
'sprite_bush_spawn_table_random_sprites': 0x368370,
'swordgfx': 0x369AD1,
}
+308
View File
@@ -0,0 +1,308 @@
import unittest
from types import SimpleNamespace
from worlds.alttp.EnemizerPatches import (
ARROW_REFILL_5_SPRITE_ID,
BOSS_GFX_SHEET_INDEXES,
BOSS_PATCH_DATA,
DAMAGE_GROUP_TABLE_ADDRESS,
DUNGEON_BOSS_PATCH_DATA,
ENEMY_DAMAGE_TABLE_ADDRESS,
ENEMY_HP_TABLE_ADDRESS,
EXCLUDED_ENEMY_TABLE_SPRITE_IDS,
HIDDEN_ENEMY_CHANCE_POOL_ADDRESS,
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS,
RETRO_RUPEE_REPLACEMENT_SPRITE_ID,
THIEF_DEFAULT_HP,
THIEF_SPRITE_ID,
TILE_TRAP_FLOOR_TILE_ADDRESS,
TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS,
TRINEXX_ICE_PROJECTILE_TILE_ADDRESS,
VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
_apply_killable_thief,
_apply_randomized_tile_trap_floor_tile,
_get_enemizer_symbol,
_make_native_enemizer_rng,
_option_key,
patch_bosses,
_randomize_enemy_damage,
_randomize_enemy_health,
_set_enemizer_flag,
_shuffle_damage_groups,
_update_hidden_enemy_item_table_for_retro_mode,
apply_enemizer_base_patch,
)
class FakeRom:
def __init__(self, size: int = 0x400000) -> None:
self.buffer = bytearray(size)
def read_byte(self, address: int) -> int:
return self.buffer[address]
def read_bytes(self, startaddress: int, length: int) -> bytearray:
return self.buffer[startaddress:startaddress + length]
def write_byte(self, address: int, value: int) -> None:
self.buffer[address] = value
def write_bytes(self, startaddress: int, values) -> None:
self.buffer[startaddress:startaddress + len(values)] = values
def write_int16(self, address: int, value: int) -> None:
self.write_bytes(address, (value & 0xFF, (value >> 8) & 0xFF))
class TestEnemizerPatches(unittest.TestCase):
def test_enemizer_base_patch_applies_mimic_hooks(self) -> None:
rom = FakeRom()
apply_enemizer_base_patch(rom)
self.assertEqual(tuple(rom.read_bytes(0x307CB, 2)), (0xB6, 0x91))
self.assertEqual(tuple(rom.read_bytes(0x311B6, 4)), (0x22, 0x1A, 0x9A, 0x36))
self.assertEqual(tuple(rom.read_bytes(0x36C08, 5)), (0x22, 0x4E, 0x9A, 0x36, 0xEA))
self.assertEqual(tuple(rom.read_bytes(0x36DA6, 4)), (0x22, 0x66, 0x9A, 0x36))
self.assertEqual(tuple(rom.read_bytes(0xF0BB1, 2)), (0x95, 0xC7))
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, 4)), (0xEA, 0xEA, 0xEA, 0xEA))
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x00, 0x00))
self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x00)
def test_randomized_tile_trap_floor_tile_patch_is_separate(self) -> None:
rom = FakeRom()
_apply_randomized_tile_trap_floor_tile(rom)
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x88, 0x01))
self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x12)
def test_enemy_shuffle_enables_hidden_enemy_and_mimic_support(self) -> None:
rom = FakeRom()
world = self._build_world(enemy_shuffle=True, bush_shuffle=False)
self._apply_native_enemizer_features(world, rom)
self.assertEqual(
tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(VANILLA_HIDDEN_ENEMY_CHANCE_POOL))),
VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x01)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x01)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x01)
self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x54, 0x9C))
self.assertEqual(rom.read_byte(0x1F2E5), 0xB0)
self.assertEqual(rom.read_byte(0x1F2EB), 0xD0)
def test_bush_shuffle_and_remaining_tables_are_patched_natively(self) -> None:
rom = FakeRom()
item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
not_item_sprite_address = _get_enemizer_symbol("notItemSprite_Mimic")
rom.write_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
rom.write_byte(item_table_address + 5, ARROW_REFILL_5_SPRITE_ID)
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
included_hp_sprite_id = 0x01
included_damage_sprite_id = 0x02
excluded_sprite_id = min(EXCLUDED_ENEMY_TABLE_SPRITE_IDS)
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id, 0x06)
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id, 0x07)
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id, 0x06)
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id, 0x05)
world = self._build_world(
bush_shuffle=True,
killable_thieves=True,
enemy_health="hard",
enemy_damage="chaos",
)
self._apply_native_enemizer_features(world, rom)
self.assertEqual(
tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL))),
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
)
self.assertEqual(rom.read_byte(item_table_address + 5), RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
self.assertEqual(rom.read_byte(not_item_sprite_address + 4), THIEF_SPRITE_ID)
self.assertNotEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 0x08)
self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 2)
self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 25)
self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 2)
self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 25)
self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id), 0x07)
self.assertIn(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id), range(8))
self.assertEqual(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id), 0x05)
for group_id in range(10):
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
self.assertIn(green_mail, range(64))
self.assertIn(blue_mail, range(64))
self.assertIn(red_mail, range(64))
def test_killable_thief_sets_default_hp_without_enemy_health_shuffle(self) -> None:
rom = FakeRom()
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
world = self._build_world(killable_thieves=True)
self._apply_native_enemizer_features(world, rom)
self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), THIEF_DEFAULT_HP)
def test_bush_shuffle_without_enemy_shuffle_does_not_enable_sprite_randomization_flags(self) -> None:
rom = FakeRom()
self._apply_native_enemizer_features(self._build_world(bush_shuffle=True), rom)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x00)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x00)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x00)
self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x00, 0x00))
self.assertEqual(rom.read_byte(0x1F2E5), 0x00)
self.assertEqual(rom.read_byte(0x1F2EB), 0x00)
def test_non_chaos_enemy_damage_uses_expected_mail_scaling(self) -> None:
rom = FakeRom()
self._apply_native_enemizer_features(self._build_world(enemy_damage="hard"), rom)
for group_id in range(10):
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
self.assertEqual(blue_mail, green_mail * 3 // 4)
self.assertEqual(red_mail, green_mail * 3 // 8)
def test_patch_bosses_overwrites_enemy_shuffle_boss_room_graphics(self) -> None:
rom = FakeRom()
dungeon_header_base = _get_enemizer_symbol("room_header_table")
eastern_dungeon_data = DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]
rom.write_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3, BOSS_PATCH_DATA["Armos"].graphics)
for table_index in BOSS_GFX_SHEET_INDEXES.values():
rom.write_byte(0x4FC0 + table_index, 0xAA)
rom.write_byte(0x509F + table_index, 0xBB)
rom.write_byte(0x517E + table_index, 0xCC)
patch_bosses(self._build_boss_world({"Eastern Palace": "Vitreous"}), rom)
eastern_boss_data = BOSS_PATCH_DATA["Vitreous"]
self.assertEqual(
tuple(rom.read_bytes(eastern_dungeon_data.sprite_pointer_address, 2)),
eastern_boss_data.pointer,
)
self.assertEqual(
rom.read_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3),
eastern_boss_data.graphics,
)
for table_index in BOSS_GFX_SHEET_INDEXES.values():
self.assertEqual(rom.read_byte(0x4FC0 + table_index), 0xAA)
self.assertEqual(rom.read_byte(0x509F + table_index), 0xBB)
self.assertEqual(rom.read_byte(0x517E + table_index), 0xCC)
def test_native_enemizer_rng_is_deterministic_for_same_world_settings(self) -> None:
world = self._build_world(enemy_health="hard", enemy_damage="chaos", bush_shuffle=True)
rng_a = _make_native_enemizer_rng(world)
rng_b = _make_native_enemizer_rng(world)
self.assertEqual([rng_a.randrange(256) for _ in range(8)], [rng_b.randrange(256) for _ in range(8)])
@staticmethod
def _apply_native_enemizer_features(world: SimpleNamespace, rom: FakeRom) -> None:
enemy_shuffle_enabled = bool(world.options.enemy_shuffle)
bush_shuffle_enabled = bool(world.options.bush_shuffle)
enemy_health_key = _option_key(world.options.enemy_health)
enemy_damage_key = _option_key(world.options.enemy_damage)
if enemy_shuffle_enabled or bush_shuffle_enabled:
_set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
hidden_enemy_chance_pool = (
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL if bush_shuffle_enabled else VANILLA_HIDDEN_ENEMY_CHANCE_POOL
)
rom.write_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
_update_hidden_enemy_item_table_for_retro_mode(rom)
if enemy_shuffle_enabled:
_set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
_set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
_set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
rom.write_bytes(0x1F2D5, (0x54, 0x9C))
rom.write_byte(0x1F2E5, 0xB0)
rom.write_byte(0x1F2EB, 0xD0)
if world.options.killable_thieves:
_apply_killable_thief(rom)
if enemy_health_key != "default" or enemy_damage_key != "default":
rng = _make_native_enemizer_rng(world)
else:
rng = None
if enemy_health_key != "default":
assert rng is not None
_randomize_enemy_health(rom, rng, enemy_health_key)
if enemy_damage_key != "default":
assert rng is not None
_randomize_enemy_damage(rom, rng, allow_zero_damage=True)
_shuffle_damage_groups(rom, rng, chaos_mode=enemy_damage_key == "chaos", allow_zero_damage=True)
@staticmethod
def _build_world(
*,
enemy_shuffle: bool = False,
bush_shuffle: bool = False,
killable_thieves: bool = False,
enemy_health: str = "default",
enemy_damage: str = "default",
) -> SimpleNamespace:
return SimpleNamespace(
player=1,
multiworld=SimpleNamespace(seed=12345, seed_name="native-enemizer-test"),
options=SimpleNamespace(
enemy_shuffle=enemy_shuffle,
bush_shuffle=bush_shuffle,
killable_thieves=killable_thieves,
enemy_health=SimpleNamespace(current_key=enemy_health),
enemy_damage=SimpleNamespace(current_key=enemy_damage),
),
)
@staticmethod
def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
boss_overrides = boss_overrides or {}
def boss(name: str) -> SimpleNamespace:
return SimpleNamespace(enemizer_name=name)
return SimpleNamespace(
options=SimpleNamespace(mode="open"),
dungeons={
"Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
"Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
"Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
"Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
"Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
"Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
"Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
"Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
"Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
"Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
"Ganons Tower": SimpleNamespace(
bosses={
"bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
"middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
"top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
}
),
},
)
if __name__ == "__main__":
unittest.main()
+834
View File
@@ -0,0 +1,834 @@
import unittest
from types import SimpleNamespace
import random
from worlds.alttp.EnemyShuffle import (
DungeonEnemyRoom,
DungeonEnemySprite,
DungeonSpriteGroup,
EnemyShuffleState,
EnemySpriteRequirement,
OverworldEnemyArea,
OverworldEnemySprite,
RandomizedDungeonEnemyRoom,
RandomizedDungeonEnemySprite,
RandomizedOverworldEnemyArea,
RandomizedOverworldEnemySprite,
WALLMASTER_SPRITE_ID,
_load_dungeon_sprite_metadata,
_read_room_sprites,
get_possible_dungeon_sprite_groups,
_get_requirements_for_usable_dungeon_enemies,
_get_requirements_for_usable_overworld_enemies,
_get_randomizable_sprites_in_room,
_apply_selected_boss_group_requirements,
_randomize_overworld_groups,
_randomize_room_sprites,
_setup_required_overworld_groups,
can_spawn_in_room,
validate_enemy_shuffle_state,
)
class TestEnemyShuffleValidation(unittest.TestCase):
def test_curated_room_sprite_addresses_exclude_hera_basement_key_slot(self) -> None:
room_id = 135
sprite_table_address = 0x4E397
rom_bytes = bytearray(0x4E3C0)
rom_bytes[sprite_table_address] = 0
room_135_sprite_records = (
(0x4E398, 0x05, 0x14, 0x18),
(0x4E39B, 0x07, 0x1A, 0x18),
(0x4E39E, 0x0B, 0x13, 0x18),
(0x4E3A1, 0x19, 0x06, 0x18),
(0x4E3A4, 0x08, 0xE7, 0x14),
(0x4E3A7, 0x04, 0x17, 0x1E),
(0x4E3AA, 0x0C, 0x03, 0x1E),
(0x4E3AD, 0x15, 0x04, 0x1E),
(0x4E3B0, 0x17, 0x0B, 0xA7),
(0x4E3B3, 0x18, 0x19, 0xA7),
(0x4E3B6, 0x19, 0x04, 0xA7),
(0x4E3B9, 0x1A, 0x08, 0xE4),
(0x4E3BC, 0x1C, 0x15, 0xA7),
)
for address, byte_0, byte_1, sprite_id in room_135_sprite_records:
rom_bytes[address] = byte_0
rom_bytes[address + 1] = byte_1
rom_bytes[address + 2] = sprite_id
rom_bytes[0x4E3BF] = 0xFF
sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, _load_dungeon_sprite_metadata())
sprite_addresses = {sprite.address for sprite in sprites}
self.assertNotIn(0x4E3B9, sprite_addresses)
self.assertIn(0x4E3B6, sprite_addresses)
self.assertFalse(any(sprite.has_key for sprite in sprites))
def test_curated_room_sprite_addresses_deduplicate_duplicate_slots(self) -> None:
room_id = 125
sprite_table_address = 0x4E2CA
metadata = _load_dungeon_sprite_metadata()
max_sprite_id_address = max(metadata["room_sprite_id_addresses"][room_id])
rom_bytes = bytearray(max_sprite_id_address + 2)
rom_bytes[sprite_table_address] = 0
for offset, sprite_id_address in enumerate(metadata["room_sprite_id_addresses"][room_id]):
address = sprite_id_address - 2
sprite_id = 0x80 if offset % 2 == 0 else 0x81
rom_bytes[address] = 0
rom_bytes[address + 1] = 0
rom_bytes[address + 2] = sprite_id
sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, metadata)
sprite_addresses = [sprite.address for sprite in sprites]
self.assertEqual(len(sprite_addresses), len(set(sprite_addresses)))
def test_rejects_non_killable_shutter_room(self) -> None:
room = DungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x10, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={1: room},
randomized_dungeon_rooms={
1: RandomizedDungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
original_graphics_block_id=1,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
RandomizedDungeonEnemySprite(
address=0x1000,
byte_0=0,
byte_1=0,
original_sprite_id=0x10,
sprite_id=0x11,
is_overlord=False,
has_key=False,
),
),
skipped_randomization=False,
)
},
sprite_requirements=(
self._requirement(0x10, killable=True, subgroup_0=(1,)),
self._requirement(0x11, killable=False, subgroup_0=(1,)),
),
)
with self.assertRaises(ValueError):
validate_enemy_shuffle_state(state, is_standard_mode=False)
def test_rejects_water_enemy_in_non_water_room(self) -> None:
room = DungeonEnemyRoom(
room_id=165,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={165: room},
randomized_dungeon_rooms={
165: RandomizedDungeonEnemyRoom(
room_id=165,
room_header_address=0,
sprite_table_address=0,
original_graphics_block_id=1,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
RandomizedDungeonEnemySprite(
address=0x1000,
byte_0=0,
byte_1=0,
original_sprite_id=0x20,
sprite_id=0x81,
is_overlord=False,
has_key=False,
),
),
skipped_randomization=False,
)
},
sprite_requirements=(
self._requirement(0x20, killable=True, subgroup_0=(1,)),
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
),
)
with self.assertRaisesRegex(ValueError, "water enemy"):
validate_enemy_shuffle_state(state, is_standard_mode=False)
def test_rejects_multiple_flopping_fish(self) -> None:
area = OverworldEnemyArea(
area_id=0x10,
sprite_table_address=0,
graphics_block_address=0,
graphics_block_id=1,
bush_sprite_id=0x20,
sprites=(
OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
),
do_not_randomize=False,
)
state = self._build_state(
overworld_areas={0x10: area},
randomized_overworld_areas={
0x10: RandomizedOverworldEnemyArea(
area_id=0x10,
sprite_table_address=0,
graphics_block_address=0,
original_graphics_block_id=1,
graphics_block_id=1,
original_bush_sprite_id=0x20,
bush_sprite_id=0xD2,
sprites=(
RandomizedOverworldEnemySprite(
address=0x2000,
y_coord=0,
x_coord=0,
original_sprite_id=0x20,
sprite_id=0xD2,
),
RandomizedOverworldEnemySprite(
address=0x2003,
y_coord=0,
x_coord=0,
original_sprite_id=0x21,
sprite_id=0xD2,
),
),
skipped_randomization=False,
)
},
sprite_requirements=(
self._requirement(0x20, group_ids=(1,)),
self._requirement(0x21, group_ids=(1,)),
self._requirement(0x22, group_ids=(1,)),
self._requirement(0xD2, group_ids=(1,)),
),
)
with self.assertRaises(ValueError):
validate_enemy_shuffle_state(state, is_standard_mode=False)
def test_allows_multiple_flopping_fish_when_no_other_sprite_is_possible(self) -> None:
area = OverworldEnemyArea(
area_id=0x10,
sprite_table_address=0,
graphics_block_address=0,
graphics_block_id=1,
bush_sprite_id=0x20,
sprites=(
OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
),
do_not_randomize=False,
)
state = self._build_state(
overworld_areas={0x10: area},
randomized_overworld_areas={
0x10: RandomizedOverworldEnemyArea(
area_id=0x10,
sprite_table_address=0,
graphics_block_address=0,
original_graphics_block_id=1,
graphics_block_id=1,
original_bush_sprite_id=0x20,
bush_sprite_id=0xD2,
sprites=(
RandomizedOverworldEnemySprite(
address=0x2000,
y_coord=0,
x_coord=0,
original_sprite_id=0x20,
sprite_id=0xD2,
),
RandomizedOverworldEnemySprite(
address=0x2003,
y_coord=0,
x_coord=0,
original_sprite_id=0x21,
sprite_id=0xD2,
),
),
skipped_randomization=False,
)
},
sprite_requirements=(
self._requirement(0x20, group_ids=(2,)),
self._requirement(0x21, group_ids=(2,)),
self._requirement(0xD2, group_ids=(1,)),
),
)
validate_enemy_shuffle_state(state, is_standard_mode=False)
def test_excludes_absorbables_from_usable_enemy_pools(self) -> None:
state = self._build_state(
sprite_requirements=(
self._requirement(0x10, subgroup_0=(1,)),
self._requirement(0xE3, subgroup_0=(1,), absorbable=True),
self._requirement(0x20, subgroup_0=(1,), never_use_dungeon=True),
self._requirement(0x21, subgroup_0=(1,), never_use_overworld=True),
),
)
self.assertEqual(
[requirement.sprite_id for requirement in _get_requirements_for_usable_dungeon_enemies(state)],
[0x10, 0x21],
)
self.assertEqual(
[requirement.sprite_id for requirement in _get_requirements_for_usable_overworld_enemies(state)],
[0x10, 0x20],
)
def test_key_enemy_replacements_exclude_moblins(self) -> None:
room = DungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x12, is_overlord=False, has_key=True),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={1: room},
sprite_requirements=(
self._requirement(0x12, killable=True, subgroup_0=(1,), cannot_have_key=True),
self._requirement(0x13, killable=True, subgroup_0=(1,)),
),
)
selected_group = state.sprite_groups[0x41]
randomized_room = _randomize_room_sprites(
SimpleNamespace(random=random.Random(0)),
state,
room,
selected_group,
False,
)
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x13)
def test_shutter_water_room_prefers_killable_water_enemy(self) -> None:
room = DungeonEnemyRoom(
room_id=40,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x8A, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=True,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={40: room},
sprite_requirements=(
self._requirement(0x8A, killable=False, subgroup_2=(34,)),
self._requirement(0x81, killable=True, subgroup_2=(34,), is_water_sprite=True),
self._requirement(0x9A, killable=False, subgroup_2=(34,), is_water_sprite=True),
),
)
selected_group = state.sprite_groups[0x41]
selected_group.subgroup_2 = 34
randomized_room = _randomize_room_sprites(
SimpleNamespace(random=random.Random(0)),
state,
room,
selected_group,
False,
)
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x81)
def test_non_water_shutter_room_replacements_exclude_water_enemies(self) -> None:
room = DungeonEnemyRoom(
room_id=165,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={165: room},
sprite_requirements=(
self._requirement(0x20, killable=False, subgroup_0=(1,)),
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
self._requirement(0x22, killable=True, subgroup_0=(1,)),
),
)
randomized_room = _randomize_room_sprites(
SimpleNamespace(random=random.Random(1)),
state,
room,
state.sprite_groups[0x41],
False,
)
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x22)
def test_non_water_shutter_group_selection_requires_non_water_killable_enemy(self) -> None:
room = DungeonEnemyRoom(
room_id=165,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={165: room},
sprite_requirements=(
self._requirement(0x20, killable=False, subgroup_0=(1,)),
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
),
)
self.assertEqual(get_possible_dungeon_sprite_groups(state, room), tuple())
def test_wallmaster_cannot_spawn_in_high_room_ids(self) -> None:
room = DungeonEnemyRoom(
room_id=0x100,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=tuple(),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
self.assertFalse(can_spawn_in_room(self._requirement(WALLMASTER_SPRITE_ID), room))
def test_room_specific_do_not_randomize_sprites_are_not_updated(self) -> None:
room = DungeonEnemyRoom(
room_id=7,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x30, is_overlord=False, has_key=False),
DungeonEnemySprite(address=0x1003, byte_0=0, byte_1=0, sprite_id=0x31, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={7: room},
sprite_requirements=(
self._requirement(0x30, subgroup_0=(1,), dont_randomize_rooms=(7,)),
self._requirement(0x31, subgroup_0=(1,)),
),
)
self.assertEqual(
[sprite.sprite_id for sprite in _get_randomizable_sprites_in_room(state, room)],
[0x31],
)
def test_water_rooms_only_use_water_enemies(self) -> None:
room = DungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=True,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={1: room},
sprite_requirements=(
self._requirement(0x20, subgroup_0=(1,)),
self._requirement(0x21, subgroup_0=(1,), is_water_sprite=True),
self._requirement(0x22, subgroup_0=(1,), is_water_sprite=True),
),
)
randomized_room = _randomize_room_sprites(
SimpleNamespace(random=random.Random(0)),
state,
room,
state.sprite_groups[0x41],
False,
)
self.assertIn(randomized_room.sprites[0].sprite_id, {0x21, 0x22})
def test_dungeon_group_selection_excludes_groups_without_enemy_requirements(self) -> None:
room = DungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={1: room},
sprite_requirements=(self._requirement(0x20, subgroup_0=(1,)),),
)
state.sprite_groups[0x42] = DungeonSpriteGroup(
group_id=0x42,
dungeon_group_id=2,
subgroup_0=0,
subgroup_1=0,
subgroup_2=0,
subgroup_3=0,
)
possible_groups = get_possible_dungeon_sprite_groups(state, room)
self.assertEqual([group.group_id for group in possible_groups], [0x41])
def test_key_room_group_selection_excludes_groups_without_room_spawnable_key_enemies(self) -> None:
room = DungeonEnemyRoom(
room_id=61,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=True),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={61: room},
sprite_requirements=(
self._requirement(0x20, subgroup_0=(1,)),
self._requirement(0x50, killable=True, subgroup_1=(32,), excluded_rooms=(61,)),
self._requirement(0x9C, killable=True, subgroup_1=(32,), cannot_have_key=True),
self._requirement(0x51, killable=True, subgroup_1=(33,)),
),
)
state.sprite_groups[0x41] = DungeonSpriteGroup(
group_id=0x41,
dungeon_group_id=1,
subgroup_0=1,
subgroup_1=32,
subgroup_2=1,
subgroup_3=1,
)
state.sprite_groups[0x42] = DungeonSpriteGroup(
group_id=0x42,
dungeon_group_id=2,
subgroup_0=1,
subgroup_1=33,
subgroup_2=1,
subgroup_3=1,
)
possible_groups = get_possible_dungeon_sprite_groups(state, room)
self.assertEqual([group.group_id for group in possible_groups], [0x42])
def test_overworld_group_randomization_preserves_forced_subgroups(self) -> None:
sprite_groups = {
7: DungeonSpriteGroup(group_id=7, dungeon_group_id=-57, subgroup_0=1, subgroup_1=2, subgroup_2=3, subgroup_3=4),
}
_setup_required_overworld_groups(
sprite_groups,
(
SimpleNamespace(
group_id=7,
subgroup_0=None,
subgroup_1=None,
subgroup_2=None,
subgroup_3=17,
areas=(0x02,),
),
),
)
_randomize_overworld_groups(SimpleNamespace(random=random.Random(0)), sprite_groups)
group = sprite_groups[7]
self.assertEqual(group.subgroup_3, 17)
self.assertIn(group.subgroup_0, {22, 31, 47, 14})
self.assertIn(group.subgroup_1, {44, 30, 32})
self.assertIn(group.subgroup_2, {12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42})
def test_selected_boss_group_requirements_override_shared_boss_graphics_group(self) -> None:
sprite_groups = {
0x56: DungeonSpriteGroup(
group_id=0x56,
dungeon_group_id=22,
subgroup_0=1,
subgroup_1=1,
subgroup_2=60,
subgroup_3=49,
),
}
sprite_requirements = (
self._requirement(162, subgroup_2=(60,)),
self._requirement(189, subgroup_3=(61,)),
)
_apply_selected_boss_group_requirements(
self._build_boss_world({"Eastern Palace": "Vitreous"}),
sprite_groups,
sprite_requirements,
)
group = sprite_groups[0x56]
self.assertEqual(group.subgroup_2, 60)
self.assertEqual(group.subgroup_3, 61)
self.assertTrue(group.preserve_subgroup_2)
self.assertTrue(group.preserve_subgroup_3)
@staticmethod
def _requirement(
sprite_id: int,
*,
killable: bool = False,
subgroup_0: tuple[int, ...] = tuple(),
subgroup_1: tuple[int, ...] = tuple(),
subgroup_2: tuple[int, ...] = tuple(),
subgroup_3: tuple[int, ...] = tuple(),
group_ids: tuple[int, ...] = tuple(),
absorbable: bool = False,
never_use_dungeon: bool = False,
never_use_overworld: bool = False,
cannot_have_key: bool = False,
is_water_sprite: bool = False,
excluded_rooms: tuple[int, ...] = tuple(),
dont_randomize_rooms: tuple[int, ...] = tuple(),
) -> EnemySpriteRequirement:
return EnemySpriteRequirement(
sprite_name=f"sprite_{sprite_id:02x}",
sprite_id=sprite_id,
boss=False,
overlord=False,
do_not_randomize=False,
killable=killable,
npc=False,
never_use_dungeon=never_use_dungeon,
never_use_overworld=never_use_overworld,
cannot_have_key=cannot_have_key,
is_object=False,
absorbable=absorbable,
is_water_sprite=is_water_sprite,
is_enemy_sprite=True,
group_ids=group_ids,
subgroup_0=subgroup_0,
subgroup_1=subgroup_1,
subgroup_2=subgroup_2,
subgroup_3=subgroup_3,
parameters=None,
special_glitched=False,
excluded_rooms=excluded_rooms,
dont_randomize_rooms=dont_randomize_rooms,
spawnable_rooms=tuple(),
)
@staticmethod
def _build_state(
*,
dungeon_rooms=None,
overworld_areas=None,
randomized_dungeon_rooms=None,
randomized_overworld_areas=None,
sprite_requirements=tuple(),
) -> EnemyShuffleState:
sprite_groups = {
1: DungeonSpriteGroup(group_id=1, dungeon_group_id=-63, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
0x41: DungeonSpriteGroup(group_id=0x41, dungeon_group_id=1, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
}
return EnemyShuffleState(
dungeon_rooms=dungeon_rooms or {},
overworld_areas=overworld_areas or {},
sprite_groups=sprite_groups,
sprite_requirements=sprite_requirements,
room_group_requirements=tuple(),
overworld_group_requirements=tuple(),
shutter_room_ids=frozenset(),
water_room_ids=frozenset(),
dont_randomize_room_ids=frozenset(),
no_special_enemies_standard_room_ids=frozenset(),
boss_room_ids=frozenset(),
dont_randomize_overworld_area_ids=frozenset(),
randomized_dungeon_rooms=randomized_dungeon_rooms or {},
randomized_overworld_areas=randomized_overworld_areas or {},
)
@staticmethod
def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
boss_overrides = boss_overrides or {}
def boss(name: str) -> SimpleNamespace:
return SimpleNamespace(enemizer_name=name)
return SimpleNamespace(
options=SimpleNamespace(mode="open"),
dungeons={
"Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
"Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
"Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
"Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
"Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
"Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
"Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
"Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
"Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
"Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
"Ganons Tower": SimpleNamespace(
bosses={
"bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
"middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
"top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
}
),
},
)
if __name__ == "__main__":
unittest.main()
+56
View File
@@ -0,0 +1,56 @@
import random
import unittest
from types import SimpleNamespace
from worlds.alttp.PotShuffle import (
POT_KEY,
POT_HOLE,
generate_pot_shuffle,
get_unique_pot_item_position,
)
class TestPotShuffle(unittest.TestCase):
def test_reserved_key_rooms_only_place_actual_keys(self) -> None:
for seed in range(10):
world = SimpleNamespace(
random=random.Random(seed),
options=SimpleNamespace(retro_bow=False),
)
shuffled_pots = generate_pot_shuffle(world)
conveyor_cross_keys = [
pot for pot in shuffled_pots[0x8B]
if pot.item == POT_KEY
]
self.assertEqual(len(conveyor_cross_keys), 1)
def test_get_unique_pot_item_position_returns_single_match(self) -> None:
world = SimpleNamespace(
random=random.Random(0),
options=SimpleNamespace(retro_bow=False),
)
shuffled_pots = generate_pot_shuffle(world)
self.assertEqual(
get_unique_pot_item_position(shuffled_pots, 0x36, POT_KEY),
(114, 16),
)
def test_reserved_hole_room_keeps_hole_fixed(self) -> None:
for seed in range(25):
world = SimpleNamespace(
random=random.Random(seed),
options=SimpleNamespace(retro_bow=False),
)
shuffled_pots = generate_pot_shuffle(world)
hole_positions = [
(pot.x, pot.y)
for pot in shuffled_pots[206]
if pot.item == POT_HOLE
]
self.assertEqual(hole_positions, [(204, 11)])
if __name__ == "__main__":
unittest.main()
+3
View File
@@ -739,6 +739,9 @@ group_table: Dict[str, Set[str]] = {
"Broken Left Eye of the Traitor"}
}
# Because each item is only in a single group, a reverse lookup table from each item to its group can be created.
group_table_reverse: Dict[str, str] = {item: group for group, items in group_table.items() for item in items}
tears_list: List[str] = [
"Tears of Atonement (500)",
"Tears of Atonement (625)",
+377 -262
View File
@@ -1,5 +1,6 @@
from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING
from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING, Mapping
from BaseClasses import CollectionState
from worlds.generic.Rules import CollectionRule
if TYPE_CHECKING:
from . import BlasphemousWorld
@@ -7,30 +8,145 @@ else:
BlasphemousWorld = object
# Depending on a player's options, some logic can either always be True, or always be False.
# When combining rules together in load_rule(), optimizations can be made by checking whether a rule being combined is
# _always or _never.
def _always(state: CollectionState):
return True
def _never(state: CollectionState):
return False
def _bool_rule(b) -> CollectionRule:
"""Small helper to return the appropriate rule function for a rule that can be pre-calculated"""
if b:
return _always
else:
return _never
# Player strengths required to logically beat bosses.
# Mapping is an immutable type, so type hints should warn if attempts are made to modify it.
BOSS_STRENGTHS: Mapping[str, float] = {
"warden": -0.10,
"ten-piedad": 0.05,
"charred-visage": 0.20,
"tres-angustias": 0.15,
"esdras": 0.25,
"melquiades": 0.25,
"exposito": 0.30,
"quirce": 0.35,
"crisanta": 0.50,
"isidora": 0.70,
"sierpes": 0.70,
"amanecida": 0.60,
"laudes": 0.60,
"perpetua": -0.05,
"legionary": 0.20
}
class BlasRules:
player: int
world: BlasphemousWorld
string_rules: Dict[str, Callable[[CollectionState], bool]]
upwarp_skips_allowed: bool
mourning_skip_allowed: bool
enemy_skips_allowed: bool
obscure_skips_allowed: bool
precise_skips_allowed: bool
can_enemy_bounce: bool
# Player strengths required to logically beat bosses, adjusted by the player's difficulty option.
boss_strengths: Mapping[str, float]
can_enemy_upslash: CollectionRule
can_air_stall: CollectionRule
can_dawn_jump: CollectionRule
can_dive_laser: CollectionRule
can_survive_poison_1: CollectionRule
can_survive_poison_2: CollectionRule
can_survive_poison_3: CollectionRule
def __init__(self, world: "BlasphemousWorld") -> None:
self.player = world.player
self.world = world
self.multiworld = world.multiworld
self.indirect_conditions: List[Tuple[str, str]] = []
difficulty = world.options.difficulty.value
# Rules that can be fully or partially pre-calculated based on world.options.
# Special Skips
self.upwarp_skips_allowed = difficulty >= 2
self.mourning_skip_allowed = difficulty >= 2
self.enemy_skips_allowed = difficulty >= 2 and not world.options.enemy_randomizer.value
self.obscure_skips_allowed = difficulty >= 2
self.precise_skips_allowed = difficulty >= 2
if difficulty >= 2:
# Beating bosses ends up in logic earlier.
self.boss_strengths = {boss: strength - 0.1 for boss, strength in BOSS_STRENGTHS.items()}
elif difficulty >= 1:
self.boss_strengths = BOSS_STRENGTHS
else:
# Beating bosses ends up in logic later.
self.boss_strengths = {boss: strength + 0.1 for boss, strength in BOSS_STRENGTHS.items()}
# Enemy tech
if self.enemy_skips_allowed:
self.can_enemy_bounce = True
self.can_enemy_upslash = lambda state: self.combo(state) >= 2
else:
self.can_enemy_bounce = False
self.can_enemy_upslash = _never
# Movement tech
if difficulty >= 1:
self.can_air_stall = self.ranged
self.can_dawn_jump = lambda state: self.dawn_heart(state) and self.dash(state)
else:
self.can_air_stall = _never
self.can_dawn_jump = _never
# Breakable tech
if difficulty >= 2:
self.can_dive_laser = lambda state: self.dive(state) >= 3
else:
self.can_dive_laser = _never
# Lung tech
if difficulty >= 2:
self.can_survive_poison_1 = _always
self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state)
self.can_survive_poison_3 = lambda state: self.lung(state) or (self.tiento(state)
and self.total_fervour(state) >= 120)
elif difficulty >= 1:
self.can_survive_poison_1 = lambda state: self.lung(state) or self.tiento(state)
self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state)
self.can_survive_poison_3 = self.lung
else:
self.can_survive_poison_1 = self.lung
self.can_survive_poison_2 = self.lung
self.can_survive_poison_3 = self.lung
# BrandenEK/Blasphemous.Randomizer/ItemRando/BlasphemousInventory.cs
self.string_rules = {
self.string_rules: dict[str, CollectionRule] = {
# Visibility flags
"DoubleJump": lambda state: bool(self.world.options.purified_hand),
"NormalLogic": lambda state: self.world.options.difficulty >= 1,
"NormalLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 1 \
and bool(self.world.options.purified_hand),
"HardLogic": lambda state: self.world.options.difficulty >= 2,
"HardLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 2 \
and bool(self.world.options.purified_hand),
"EnemySkips": self.enemy_skips_allowed,
"EnemySkipsAndDoubleJump": lambda state: self.enemy_skips_allowed(state) \
and bool(self.world.options.purified_hand),
"DoubleJump": _bool_rule(self.world.options.purified_hand.value),
"NormalLogic": _bool_rule(self.world.options.difficulty.value >= 1),
"NormalLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 1
and bool(self.world.options.purified_hand.value)),
"HardLogic": _bool_rule(self.world.options.difficulty.value >= 2),
"HardLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 2
and bool(self.world.options.purified_hand.value)),
"EnemySkips": _bool_rule(self.enemy_skips_allowed),
"EnemySkipsAndDoubleJump": _bool_rule(self.enemy_skips_allowed and self.world.options.purified_hand.value),
# Relics
"blood": self.blood,
@@ -52,20 +168,20 @@ class BlasRules:
"cherubs20": lambda state: self.cherubs(state) >= 20,
"cherubs38": lambda state: self.cherubs(state) >= 38,
"bones4": lambda state: self.bones(state) >= 4,
"bones8": lambda state: self.bones(state) >= 8,
"bones12": lambda state: self.bones(state) >= 12,
"bones16": lambda state: self.bones(state) >= 16,
"bones20": lambda state: self.bones(state) >= 20,
"bones24": lambda state: self.bones(state) >= 24,
"bones28": lambda state: self.bones(state) >= 28,
"bones30": lambda state: self.bones(state) >= 30,
"bones32": lambda state: self.bones(state) >= 32,
"bones36": lambda state: self.bones(state) >= 36,
"bones40": lambda state: self.bones(state) >= 40,
"bones44": lambda state: self.bones(state) >= 44,
"bones4": lambda state: self.bones(state, 4),
"bones8": lambda state: self.bones(state, 8),
"bones12": lambda state: self.bones(state, 12),
"bones16": lambda state: self.bones(state, 16),
"bones20": lambda state: self.bones(state, 20),
"bones24": lambda state: self.bones(state, 24),
"bones28": lambda state: self.bones(state, 28),
"bones30": lambda state: self.bones(state, 30),
"bones32": lambda state: self.bones(state, 32),
"bones36": lambda state: self.bones(state, 36),
"bones40": lambda state: self.bones(state, 40),
"bones44": lambda state: self.bones(state, 44),
"tears0": lambda state: True,
"tears0": _always,
# Special items
"dash": self.dash,
@@ -118,13 +234,13 @@ class BlasRules:
# skip "dive"
# skip "lunge"
"chargeBeam": self.charge_beam,
"rangedAttack": lambda state: self.ranged(state) > 0,
"rangedAttack": self.ranged,
# Main quest
"holyWounds3": lambda state: self.holy_wounds(state) >= 3,
"masks1": lambda state: self.masks(state) >= 1,
"masks2": lambda state: self.masks(state) >= 2,
"masks3": lambda state: self.masks(state) >= 3,
"holyWounds3": lambda state: self.holy_wounds(state, 3),
"masks1": lambda state: self.masks(state, 1),
"masks2": lambda state: self.masks(state, 2),
"masks3": lambda state: self.masks(state, 3),
"guiltBead": self.guilt_bead,
# LOTL quest
@@ -133,17 +249,17 @@ class BlasRules:
"hatchedEgg": self.hatched_egg,
# Tirso quest
"herbs1": lambda state: self.herbs(state) >= 1,
"herbs2": lambda state: self.herbs(state) >= 2,
"herbs3": lambda state: self.herbs(state) >= 3,
"herbs4": lambda state: self.herbs(state) >= 4,
"herbs5": lambda state: self.herbs(state) >= 5,
"herbs6": lambda state: self.herbs(state) >= 6,
"herbs1": lambda state: self.herbs(state, 1),
"herbs2": lambda state: self.herbs(state, 2),
"herbs3": lambda state: self.herbs(state, 3),
"herbs4": lambda state: self.herbs(state, 4),
"herbs5": lambda state: self.herbs(state, 5),
"herbs6": lambda state: self.herbs(state, 6),
# Tentudia quest
"tentudiaRemains1": lambda state: self.tentudia_remains(state) >= 1,
"tentudiaRemains2": lambda state: self.tentudia_remains(state) >= 2,
"tentudiaRemains3": lambda state: self.tentudia_remains(state) >= 3,
"tentudiaRemains1": lambda state: self.tentudia_remains(state, 1),
"tentudiaRemains2": lambda state: self.tentudia_remains(state, 2),
"tentudiaRemains3": lambda state: self.tentudia_remains(state, 3),
# Gemino quest
"emptyThimble": self.empty_thimble,
@@ -151,7 +267,7 @@ class BlasRules:
"driedFlowers": self.dried_flowers,
# Altasgracias quest
"ceremonyItems3": lambda state: self.ceremony_items(state) >= 3,
"ceremonyItems3": lambda state: self.ceremony_items(state, 3),
"egg": self.egg,
# Redento quest
@@ -159,13 +275,13 @@ class BlasRules:
# skip "knots", not actually used
# Cleofas quest
"marksOfRefuge3": lambda state: self.marks_of_refuge(state) >= 3,
"marksOfRefuge3": lambda state: self.marks_of_refuge(state, 3),
"cord": self.cord,
# Crisanta quest
"scapular": self.scapular,
"trueHeart": self.true_heart,
"traitorEyes2": lambda state: self.traitor_eyes(state) >= 2,
"traitorEyes2": lambda state: self.traitor_eyes(state, 2),
# Jibrael quest
"bell": self.bell,
@@ -190,32 +306,32 @@ class BlasRules:
"canSurvivePoison3": self.can_survive_poison_3,
# Enemy tech
"canEnemyBounce": self.can_enemy_bounce,
"canEnemyBounce": _bool_rule(self.can_enemy_bounce),
"canEnemyUpslash": self.can_enemy_upslash,
# Reaching rooms
"guiltRooms1": lambda state: self.guilt_rooms(state) >= 1,
"guiltRooms2": lambda state: self.guilt_rooms(state) >= 2,
"guiltRooms3": lambda state: self.guilt_rooms(state) >= 3,
"guiltRooms4": lambda state: self.guilt_rooms(state) >= 4,
"guiltRooms5": lambda state: self.guilt_rooms(state) >= 5,
"guiltRooms6": lambda state: self.guilt_rooms(state) >= 6,
"guiltRooms7": lambda state: self.guilt_rooms(state) >= 7,
"guiltRooms1": lambda state: self.guilt_rooms(state, 1),
"guiltRooms2": lambda state: self.guilt_rooms(state, 2),
"guiltRooms3": lambda state: self.guilt_rooms(state, 3),
"guiltRooms4": lambda state: self.guilt_rooms(state, 4),
"guiltRooms5": lambda state: self.guilt_rooms(state, 5),
"guiltRooms6": lambda state: self.guilt_rooms(state, 6),
"guiltRooms7": lambda state: self.guilt_rooms(state, 7),
"swordRooms1": lambda state: self.sword_rooms(state) >= 1,
"swordRooms2": lambda state: self.sword_rooms(state) >= 2,
"swordRooms3": lambda state: self.sword_rooms(state) >= 3,
"swordRooms4": lambda state: self.sword_rooms(state) >= 4,
"swordRooms5": lambda state: self.sword_rooms(state) >= 5,
"swordRooms6": lambda state: self.sword_rooms(state) >= 6,
"swordRooms7": lambda state: self.sword_rooms(state) >= 7,
"swordRooms1": lambda state: self.sword_rooms(state, 1),
"swordRooms2": lambda state: self.sword_rooms(state, 2),
"swordRooms3": lambda state: self.sword_rooms(state, 3),
"swordRooms4": lambda state: self.sword_rooms(state, 4),
"swordRooms5": lambda state: self.sword_rooms(state, 5),
"swordRooms6": lambda state: self.sword_rooms(state, 6),
"swordRooms7": lambda state: self.sword_rooms(state, 7),
"redentoRooms2": lambda state: self.redento_rooms(state) >= 2,
"redentoRooms3": lambda state: self.redento_rooms(state) >= 3,
"redentoRooms4": lambda state: self.redento_rooms(state) >= 4,
"redentoRooms5": lambda state: self.redento_rooms(state) >= 5,
"redentoRooms2": lambda state: self.redento_rooms(state, 2),
"redentoRooms3": lambda state: self.redento_rooms(state, 3),
"redentoRooms4": lambda state: self.redento_rooms(state, 4),
"redentoRooms5": lambda state: self.redento_rooms(state, 5),
"miriamRooms5": lambda state: self.miriam_rooms(state) >= 5,
"miriamRooms5": self.all_miriam_rooms,
"amanecidaRooms1": lambda state: self.amanecida_rooms(state) >= 1,
"amanecidaRooms2": lambda state: self.amanecida_rooms(state) >= 2,
@@ -254,11 +370,11 @@ class BlasRules:
"openedBotSSLadder": self.opened_botss_ladder,
# Special skips
"upwarpSkipsAllowed": self.upwarp_skips_allowed,
"mourningSkipAllowed": self.mourning_skip_allowed,
"enemySkipsAllowed": self.enemy_skips_allowed,
"obscureSkipsAllowed": self.obscure_skips_allowed,
"preciseSkipsAllowed": self.precise_skips_allowed,
"upwarpSkipsAllowed": _bool_rule(self.upwarp_skips_allowed),
"mourningSkipAllowed": _bool_rule(self.mourning_skip_allowed),
"enemySkipsAllowed": _bool_rule(self.enemy_skips_allowed),
"obscureSkipsAllowed": _bool_rule(self.obscure_skips_allowed),
"preciseSkipsAllowed": _bool_rule(self.precise_skips_allowed),
# Bosses
"canBeatBrotherhoodBoss": self.can_beat_brotherhood_boss,
@@ -498,30 +614,74 @@ class BlasRules:
def load_rule(self, obj_is_region: bool, name: str, obj: Dict[str, Any]) -> Callable[[CollectionState], bool]:
clauses = []
clauses_are_impossible_if_empty = False
rule_indirect_conditions = []
for clause in obj["logic"]:
reqs = []
clause_indirect_conditions = []
clause_is_impossible = False
for req in clause["item_requirements"]:
if self.req_is_region(req):
if obj_is_region:
# add to indirect conditions if object and requirement are doors
self.indirect_conditions.append((req, f"{name} -> {obj['target']}"))
clause_indirect_conditions.append((req, f"{name} -> {obj['target']}"))
reqs.append(lambda state, req=req: state.can_reach_region(req, self.player))
else:
string_rule = self.string_rules[req]
if string_rule is _never:
# This clause is not possible with the options this player has chosen.
clause_is_impossible = True
break
elif string_rule is _always:
# Don't need to add a rule that is always True with the options this player has chosen.
# Continue to the next requirement.
continue
if obj_is_region and req in self.indirect_regions:
# add to indirect conditions if object is door and requirement has list of regions
for region in self.indirect_regions[req]:
self.indirect_conditions.append((region, f"{name} -> {obj['target']}"))
clause_indirect_conditions.append((region, f"{name} -> {obj['target']}"))
reqs.append(self.string_rules[req])
if clause_is_impossible:
# At least one clause was impossible, so if all clauses were impossible, the entire rule is impossible.
clauses_are_impossible_if_empty = True
# Continue to the next clause.
continue
rule_indirect_conditions.extend(clause_indirect_conditions)
# Combine the requirements if there are multiple.
# Requirements are AND-ed together.
if len(reqs) == 1:
clauses.append(reqs[0])
else:
clauses.append(lambda state, reqs=reqs: all(req(state) for req in reqs))
def req_func(state, reqs=reqs):
for req in reqs:
if not req(state):
return False
return True
clauses.append(req_func)
# Combine the clauses if there are multiple.
# Clauses are OR-ed together.
if not clauses:
return lambda state: True
# There is no need to register the indirect conditions if it turns out the rule is impossible or always
# possible.
rule_indirect_conditions.clear()
if clauses_are_impossible_if_empty:
to_return = _never
else:
to_return = _always
elif len(clauses) == 1:
return clauses[0]
to_return = clauses[0]
else:
return lambda state: any(clause(state) for clause in clauses)
def clause_func(state, clauses=clauses):
for clause in clauses:
if clause(state):
return True
return False
to_return = clause_func
# Update the list of indirect conditions to add.
self.indirect_conditions.extend(rule_indirect_conditions)
return to_return
# Relics
def blood(self, state: CollectionState) -> bool:
@@ -565,8 +725,10 @@ class BlasRules:
def cherubs(self, state: CollectionState) -> int:
return state.count("Child of Moonlight", self.player)
def bones(self, state: CollectionState) -> int:
return state.count_group_unique("bones", self.player)
def bones(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "bones" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("bones", self.player, count)
# def tears():
@@ -594,7 +756,7 @@ class BlasRules:
# Health boosts
def flasks(self, state: CollectionState) -> int:
doors = {
doors = (
"D01Z05S05[SW]",
"D02Z02S04[W]",
"D03Z02S08[W]",
@@ -602,10 +764,11 @@ class BlasRules:
"D04Z02S13[W]",
"D05Z01S08[NW]",
"D20Z01S07[NE]"
}
return state.count("Empty Bile Vessel", self.player) \
if sum(state.can_reach_region(door, self.player) for door in doors) >= 1 else 0
)
for door in doors:
if state.can_reach_region(door, self.player):
return state.count("Empty Bile Vessel", self.player)
return 0
def quicksilver(self, state: CollectionState) -> int:
return state.count("Quicksilver", self.player) if state.can_reach_region("D01Z05S01[W]", self.player) else 0
@@ -613,7 +776,7 @@ class BlasRules:
# Puzzles
def red_wax(self, state: CollectionState) -> int:
return state.count("Bead of Red Wax", self.player)
def blue_wax(self, state: CollectionState) -> int:
return state.count("Bead of Blue Wax", self.player)
@@ -670,7 +833,7 @@ class BlasRules:
or self.cante(state)
or self.cantina(state)
or self.tiento(state)
or state.has_any({
or state.has_any((
"Campanillero to the Sons of the Aurora",
"Mirabras of the Return to Port",
"Romance to the Crimson Mist",
@@ -678,7 +841,7 @@ class BlasRules:
"Seguiriya to your Eyes like Stars",
"Verdiales of the Forsaken Hamlet",
"Zambra to the Resplendent Crown"
}, self.player)
), self.player)
)
def pillar(self, state: CollectionState) -> bool:
@@ -710,8 +873,8 @@ class BlasRules:
def charged(self, state: CollectionState) -> int:
return state.count("Charged Skill", self.player)
def ranged(self, state: CollectionState) -> int:
return state.count("Ranged Skill", self.player)
def ranged(self, state: CollectionState) -> bool:
return state.has("Ranged Skill", self.player)
def dive(self, state: CollectionState) -> int:
return state.count("Dive Skill", self.player)
@@ -723,11 +886,15 @@ class BlasRules:
return self.charged(state) >= 3
# Main quest
def holy_wounds(self, state: CollectionState) -> int:
return state.count_group_unique("wounds", self.player)
def holy_wounds(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "wounds" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("wounds", self.player, count)
def masks(self, state: CollectionState) -> int:
return state.count_group_unique("masks", self.player)
def masks(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "masks" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("masks", self.player, count)
def guilt_bead(self, state: CollectionState) -> bool:
return state.has("Weight of True Guilt", self.player)
@@ -743,12 +910,16 @@ class BlasRules:
return state.has("Hatched Egg of Deformity", self.player)
# Tirso quest
def herbs(self, state: CollectionState) -> int:
return state.count_group_unique("tirso", self.player)
def herbs(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "tirso" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("tirso", self.player, count)
# Tentudia quest
def tentudia_remains(self, state: CollectionState) -> int:
return state.count_group_unique("tentudia", self.player)
def tentudia_remains(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "tentudia" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("tentudia", self.player, count)
# Gemino quest
def empty_thimble(self, state: CollectionState) -> bool:
@@ -761,23 +932,29 @@ class BlasRules:
return state.has("Dried Flowers bathed in Tears", self.player)
# Altasgracias quest
def ceremony_items(self, state: CollectionState) -> int:
return state.count_group_unique("egg", self.player)
def ceremony_items(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "egg" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("egg", self.player, count)
def egg(self, state: CollectionState) -> bool:
return state.has("Egg of Deformity", self.player)
# Redento quest
def limestones(self, state: CollectionState) -> int:
return state.count_group_unique("toe", self.player)
def limestones(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "toe" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("toe", self.player, count)
def knots(self, state: CollectionState) -> int:
return state.count("Knot of Rosary Rope", self.player) if state.can_reach_region("D17Z01S07[NW]", self.player)\
else 0
# Cleofas quest
def marks_of_refuge(self, state: CollectionState) -> int:
return state.count_group_unique("marks", self.player)
def marks_of_refuge(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "marks" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("marks", self.player, count)
def cord(self, state: CollectionState) -> bool:
return state.has("Cord of the True Burying", self.player)
@@ -789,8 +966,10 @@ class BlasRules:
def true_heart(self, state: CollectionState) -> bool:
return state.has("Apodictic Heart of Mea Culpa", self.player)
def traitor_eyes(self, state: CollectionState) -> int:
return state.count_group_unique("eye", self.player)
def traitor_eyes(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "eye" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("eye", self.player, count)
# Jibrael quest
def bell(self, state: CollectionState) -> bool:
@@ -800,19 +979,6 @@ class BlasRules:
return state.count("Verses Spun from Gold", self.player)
# Movement tech
def can_air_stall(self, state: CollectionState) -> bool:
return (
self.ranged(state) > 0
and self.world.options.difficulty >= 1
)
def can_dawn_jump(self, state: CollectionState) -> bool:
return (
self.dawn_heart(state)
and self.dash(state)
and self.world.options.difficulty >= 1
)
def can_water_jump(self, state: CollectionState) -> bool:
return (
self.nail(state)
@@ -828,12 +994,6 @@ class BlasRules:
or self.can_use_any_prayer(state)
)
def can_dive_laser(self, state: CollectionState) -> bool:
return (
self.dive(state) >= 3
and self.world.options.difficulty >= 2
)
# Root tech
def can_walk_on_root(self, state: CollectionState) -> bool:
return self.root(state)
@@ -844,40 +1004,6 @@ class BlasRules:
and self.wall_climb(state)
)
# Lung tech
def can_survive_poison_1(self, state: CollectionState) -> bool:
return (
self.lung(state)
or self.world.options.difficulty >= 1
and self.tiento(state)
or self.world.options.difficulty >= 2
)
def can_survive_poison_2(self, state: CollectionState) -> bool:
return (
self.lung(state)
or self.world.options.difficulty >= 1
and self.tiento(state)
)
def can_survive_poison_3(self, state: CollectionState) -> bool:
return (
self.lung(state)
or self.world.options.difficulty >= 2
and self.tiento(state)
and self.total_fervour(state) >= 120
)
# Enemy tech
def can_enemy_bounce(self, state: CollectionState) -> bool:
return self.enemy_skips_allowed(state)
def can_enemy_upslash(self, state: CollectionState) -> bool:
return (
self.combo(state) >= 2
and self.enemy_skips_allowed(state)
)
# Crossing gaps
def can_cross_gap_1(self, state: CollectionState) -> bool:
return (
@@ -1021,7 +1147,7 @@ class BlasRules:
or state.can_reach_region("D03Z02S03[E]", self.player)
and (
self.can_cross_gap_5(state)
or self.can_enemy_bounce(state)
or self.can_enemy_bounce
and self.can_cross_gap_3(state)
)
)
@@ -1067,25 +1193,6 @@ class BlasRules:
or state.can_reach_region("D17BZ02S01[FrontR]", self.player)
)
# Special skips
def upwarp_skips_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
def mourning_skip_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
def enemy_skips_allowed(self, state: CollectionState) -> bool:
return (
self.world.options.difficulty >= 2
and not self.world.options.enemy_randomizer
)
def obscure_skips_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
def precise_skips_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
# Bosses
def can_beat_brotherhood_boss(self, state: CollectionState) -> bool:
return (
@@ -1183,18 +1290,18 @@ class BlasRules:
and state.can_reach_region("D20Z02S07[W]", self.player)
)
def can_beat_graveyard_boss(self, state: CollectionState) -> bool:
def can_beat_graveyard_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
self.has_boss_strength(state, "amanecida", player_strength)
and self.wall_climb(state)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and state.can_reach_region("D02Z03S18[NW]", self.player)
and state.can_reach_region("D02Z02S03[NE]", self.player)
)
def can_beat_jondo_boss(self, state: CollectionState) -> bool:
def can_beat_jondo_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
self.has_boss_strength(state, "amanecida", player_strength)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and (
state.can_reach_region("D20Z01S06[NE]", self.player)
@@ -1206,9 +1313,9 @@ class BlasRules:
)
)
def can_beat_patio_boss(self, state: CollectionState) -> bool:
def can_beat_patio_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
self.has_boss_strength(state, "amanecida", player_strength)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and state.can_reach_region("D06Z01S02[W]", self.player)
and (
@@ -1218,9 +1325,9 @@ class BlasRules:
)
)
def can_beat_wall_boss(self, state: CollectionState) -> bool:
def can_beat_wall_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
self.has_boss_strength(state, "amanecida", player_strength)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and state.can_reach_region("D09Z01S09[Cell24]", self.player)
and (
@@ -1244,8 +1351,7 @@ class BlasRules:
def can_beat_legionary(self, state: CollectionState) -> bool:
return self.has_boss_strength(state, "legionary")
def has_boss_strength(self, state: CollectionState, boss: str) -> bool:
def get_player_strength(self, state: CollectionState) -> float:
life: int = state.count("Life Upgrade", self.player)
sword: int = state.count("Mea Culpa Upgrade", self.player)
fervour: int = state.count("Fervour Upgrade", self.player)
@@ -1259,30 +1365,16 @@ class BlasRules:
+ min(8, flasks) * 0.15 / 8
+ min(5, quicksilver) * 0.15 / 5
)
return player_strength
bosses: Dict[str, float] = {
"warden": -0.10,
"ten-piedad": 0.05,
"charred-visage": 0.20,
"tres-angustias": 0.15,
"esdras": 0.25,
"melquiades": 0.25,
"exposito": 0.30,
"quirce": 0.35,
"crisanta": 0.50,
"isidora": 0.70,
"sierpes": 0.70,
"amanecida": 0.60,
"laudes": 0.60,
"perpetua": -0.05,
"legionary": 0.20
}
boss_strength: float = bosses[boss]
return player_strength >= (boss_strength - 0.10 if self.world.options.difficulty >= 2 else
(boss_strength if self.world.options.difficulty >= 1 else boss_strength + 0.10))
def has_boss_strength(self, state: CollectionState, boss: str, player_strength: float | None = None) -> bool:
if player_strength is None:
return self.get_player_strength(state) >= self.boss_strengths[boss]
else:
return player_strength >= self.boss_strengths[boss]
def guilt_rooms(self, state: CollectionState) -> int:
doors = [
def guilt_rooms(self, state: CollectionState, count: int) -> bool:
doors = (
"D01Z04S01[NE]",
"D02Z02S11[W]",
"D03Z03S02[NE]",
@@ -1290,20 +1382,25 @@ class BlasRules:
"D05Z01S05[NE]",
"D09Z01S05[W]",
"D17Z01S04[W]",
]
)
return sum(state.can_reach_region(door, self.player) for door in doors)
total: int = 0
for door in doors:
total += state.can_reach_region(door, self.player)
if total >= count:
return True
return False
def sword_rooms(self, state: CollectionState) -> int:
doors = [
["D01Z02S07[E]", "D01Z02S02[SW]"],
["D20Z01S04[E]", "D01Z05S23[W]"],
["D02Z03S02[NE]"],
["D04Z02S21[NE]"],
["D05Z01S21[NW]"],
["D06Z01S15[NE]"],
["D17Z01S07[SW]"]
]
def sword_rooms(self, state: CollectionState, count: int) -> bool:
doors = (
("D01Z02S07[E]", "D01Z02S02[SW]"),
("D20Z01S04[E]", "D01Z05S23[W]"),
("D02Z03S02[NE]",),
("D04Z02S21[NE]",),
("D05Z01S21[NW]",),
("D06Z01S15[NE]",),
("D17Z01S07[SW]",)
)
total: int = 0
for subdoors in doors:
@@ -1311,72 +1408,90 @@ class BlasRules:
if state.can_reach_region(door, self.player):
total += 1
break
if total >= count:
return True
return total
return False
def redento_rooms(self, state: CollectionState) -> int:
if (
state.can_reach_region("D03Z01S04[E]", self.player)
or state.can_reach_region("D03Z02S10[N]", self.player)
def redento_rooms(self, state: CollectionState, count: int) -> bool:
if not (
state.can_reach_region("D03Z01S04[E]", self.player)
or state.can_reach_region("D03Z02S10[N]", self.player)
):
if (
# Realistically, count should never be zero or negative.
return count < 1
if count == 1:
return True
if not (
state.can_reach_region("D17Z01S05[S]", self.player)
or state.can_reach_region("D17BZ02S01[FrontR]", self.player)
):
if (
state.can_reach_region("D01Z03S04[E]", self.player)
or state.can_reach_region("D08Z01S01[W]", self.player)
):
if (
state.can_reach_region("D04Z01S03[E]", self.player)
or state.can_reach_region("D04Z02S01[W]", self.player)
or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)
):
if (
self.knots(state) >= 1
and self.limestones(state) >= 3
and (
state.can_reach_region("D04Z02S08[E]", self.player)
or state.can_reach_region("D04BZ02S01[Redento]", self.player)
)
):
return 5
return 4
return 3
return 2
return 1
return 0
def miriam_rooms(self, state: CollectionState) -> int:
doors = [
):
return False
if count == 2:
return True
if not (state.can_reach_region("D01Z03S04[E]", self.player)
or state.can_reach_region("D08Z01S01[W]", self.player)):
return False
if count == 3:
return True
if not (state.can_reach_region("D04Z01S03[E]", self.player)
or state.can_reach_region("D04Z02S01[W]", self.player)
or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)):
return False
if count == 4:
return True
if not (
self.knots(state) >= 1
and self.limestones(state, 3)
and (state.can_reach_region("D04Z02S08[E]", self.player)
or state.can_reach_region("D04BZ02S01[Redento]", self.player))
):
return False
return count == 5
def all_miriam_rooms(self, state: CollectionState) -> bool:
doors = (
"D02Z03S07[NWW]",
"D03Z03S07[NW]",
"D04Z04S01[E]",
"D05Z01S06[W]",
"D06Z01S17[E]"
]
)
return sum(state.can_reach_region(door, self.player) for door in doors)
for door in doors:
if not state.can_reach_region(door, self.player):
return False
return True
def amanecida_rooms(self, state: CollectionState) -> int:
player_strength = self.get_player_strength(state)
total: int = 0
if self.can_beat_graveyard_boss(state):
if self.can_beat_graveyard_boss(state, player_strength):
total += 1
if self.can_beat_jondo_boss(state):
if self.can_beat_jondo_boss(state, player_strength):
total += 1
if self.can_beat_patio_boss(state):
if self.can_beat_patio_boss(state, player_strength):
total += 1
if self.can_beat_wall_boss(state):
if self.can_beat_wall_boss(state, player_strength):
total += 1
return total
def chalice_rooms(self, state: CollectionState) -> int:
doors = [
["D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"],
["D05Z01S11[SE]", "D05Z02S02[NW]"],
["D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]"]
]
doors = (
("D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"),
("D05Z01S11[SE]", "D05Z02S02[NW]"),
("D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]")
)
total: int = 0
for subdoors in doors:
+23 -2
View File
@@ -1,9 +1,9 @@
from typing import Dict, List, Set, Any
from collections import Counter
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, CollectionState
from Options import OptionError
from worlds.AutoWorld import World, WebWorld
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
from .Items import base_id, item_table, group_table, tears_list, reliquary_set, group_table_reverse
from .Locations import location_names
from .Rules import BlasRules
from worlds.generic.Rules import set_rule
@@ -216,6 +216,27 @@ class BlasphemousWorld(World):
for loc, item in option_dict.items():
self.get_location(loc).place_locked_item(self.create_item(item))
def collect(self, state: CollectionState, item: Item) -> bool:
changed = super().collect(state, item)
if changed:
name = item.name
if name in group_table_reverse and state.count(name, self.player) == 1:
# Count was 0 before super().collect().
group_name = group_table_reverse[name]
# Increase unique count for items in this group.
state.prog_items[self.player][group_name] += 1
return changed
def remove(self, state: CollectionState, item: Item) -> bool:
changed = super().remove(state, item)
if changed:
name = item.name
if name in group_table_reverse and state.count(name, self.player) == 0:
# Count was 1 before super().remove().
group_name = group_table_reverse[name]
# Decrease unique count for items in this group.
state.prog_items[self.player][group_name] -= 1
return changed
def create_regions(self) -> None:
multiworld = self.multiworld
+1 -3
View File
@@ -296,11 +296,9 @@ class ImpatientMimicsOption(Toggle):
class RandomEnemyPresetOption(OptionDict):
"""The YAML preset for the static enemy randomizer.
See the static randomizer documentation in `randomizer\\presets\\README.txt` for details.
See the online enemy randomization documentation for all available options.
Include this as nested YAML. For example:
.. code-block:: YAML
random_enemy_preset:
RemoveSource: Ancient Wyvern; Darkeater Midir
DontRandomize: Iudex Gundyr
+11 -1
View File
@@ -1,9 +1,11 @@
# Dark Souls III
Game Page | [Items] | [Locations]
Game Page | [Setup] | [Items] | [Locations] | [Enemy Randomization]
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## What do I need to do to randomize DS3?
@@ -138,6 +140,14 @@ Check out the [item guide], which explains the named groups available for items.
[item guide]: /tutorial/Dark%20Souls%20III/items/en
## How can I change what enemies get randomized?
The [enemy randomization guide] explains how to further customize enemy randomization
for challenge runs or convenience. You can target specific enemies or entire
categories and even remove annoying enemy types outright.
[enemy randomization guide]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## What's new from 2.x.x?
Version 3.0.0 of the Dark Souls III Archipelago client has a number of
@@ -0,0 +1,411 @@
# Dark Souls III Enemy Randomization
[Game Page] | [Setup] | [Items] | [Locations] | Enemy Randomization
[Game Page]: /games/Dark%20Souls%20III/info/en
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
If `randomize_enemies` in your Dark Souls 3 player config YAML is enabled, bosses, minibosses and basic enemies will
be shuffled with themselves respectively.
To further customize enemy randomization beyond that, there is a section called `random_enemy_preset`.
This tutorial will show all the ways how to configure that preset.
## Table of Contents
- [The Basics](#the-basics)
- [Individual Assignments](#individual-assignments)
- [Pools](#pools)
* [Pool Groups](#pool-groups)
+ [RandomByType](#randombytype)
* [Weights](#weights)
- [Settings](#settings)
* [Boss](#boss)
* [Miniboss](#miniboss)
* [Basic](#basic)
+ [BuffBasicEnemiesAsBosses](#buffbasicenemiesasbosses)
* [Enemies](#enemies)
* [DontRandomize](#dontrandomize)
* [RemoveSource](#removesource)
* [OopsAll](#oopsall)
- [Enemy Categories](#enemy-categories)
## The Basics
There are two main ways to assign an enemy to be randomized: [individual enemy assignments](#individual-assignments)
to target a singular enemy placement and setting up [Pools](#pools) to target a category of enemies.
Custom pools are recommended unless you specifically want to single out one enemy placement.
All bosses also have their own category, so individual assignment is not necessary in those cases.
Be aware of correct indentation of your YAML file. Every example in this document will need to be nested under the
`random_enemy_preset:` section.
Disable the preset by leaving just empty brackets `{}`. Like usual with YAML, you can add comments by using `#`.
For further examples, check out the "presets" folder of the standalone randomizer.
## Individual Assignments
Individual enemy assignment allows you to target individual enemies, rather than a category as under pools.
This overrides pools and any other configuration, will usually ignore progression, and can possibly cause you to have to
fight Yhorm the Giant without Storm Ruler.
You use it in the [`Enemies`](#enemies) section by selecting a specific enemy using its unique
ID, or its specific name followed by its ID.
See the '/randomizer/preset/Template.txt' file of the static randomizer for all available IDs.
There are also some special target names available for individual assignments:
- `any`: This is the default and allows any enemy in the pool to appear there.
- `norandom`: Assigns an enemy to itself. This has the same effect as adding the enemy name to [`DontRandomize`](#dontrandomize).
## Pools
A pool is a collection of enemies. A pool can both be a randomization target and an eligible group of random enemies to
be drawn from for randomization. See [Enemy Categories](#enemy-categories) for all available pools.
Pool assignment generally respects progression, like requiring Storm Ruler to be accessible before Yhorm the Giant.
By default, using a boss as another boss, or a miniboss as another miniboss, takes the source enemy out of the default
pool for that category, so each enemy will still be used once if possible. However, the enemy can still appear more
than once if used in a custom pool.
### Pool Groups
Pools can be joined into a pool group by joining several names, separated by a semicolon.
```yaml
# All basic enemies are just different hollows now
Basic:
- Weight: 100
Pool: Hollow Soldiers; Large Hollow Soldiers
```
#### RandomByType
By default, selection will be random across all eligible enemies. In our example above it would select from:
- Hollow Soldier
- Road of Sacrifices Hollow Soldier
- Cathedral Hollow Soldier
- Lothric Castle Hollow Soldier
- Grand Archives Hollow Soldier
and
- Large Hollow Soldier
- Cathedral Large Hollow Soldier
- Lothric Castle Large Hollow Soldier
However, this would make it more likely to select a regular soldier instead of a large one (5 out of 8), just because
there are fewer entries in the latter category.
You can specify `RandomByType: true` to select randomly from the list itself (Hollow Soldiers, Large Hollow Soldiers)
and make our previous example a true 50/50 split.
```yaml
# All basic enemies are just different hollows now
Basic:
- Weight: 100
Pool: Hollow Soldiers; Large Hollow Soldiers
RandomByType: true # To make it truly 50/50 between the categories
```
### Weights
Weights can be used to select multiple different outcomes within a pool, weighted to give different probabilities each.
Weights don't necessarily have to add up to 100, but doing it that way makes estimating probabilities very intuitive.
```yaml
Boss:
- Weight: 79 # 79% of bosses will still be bosses
Pool: default
- Weight: 20 # Replace 20% of all bosses with minibosses
Pool: Miniboss
- Weight: 1 # Replace 1% of all bosses with regular enemies. It's always funny
Pool: Basic
```
Be aware that weights will not work in the [`Enemies`](#enemies) section.
## Settings
### Boss
This setting indicates which enemies can be used as replacements for bosses.
By default, this is the pool of all 29 bosses.
```yaml
Boss:
- Weight: 80
Pool: default
- Weight: 20 # Replace 20% of all bosses with minibosses
Pool: Miniboss
```
### Miniboss
This setting indicates which enemies can be used as replacements for minibosses.
By default, this is the pool of all 32 minibosses (including duplicates).
```yaml
Miniboss:
- Weight: 80
Pool: default
- Weight: 20 # Replace 20% of all minibosses with bosses
Pool: Boss
```
### Basic
This setting indicates which enemies can be used as replacements for all other enemies, so non-bosses and non-minibosses.
By default, this is the pool of all ~2000 basic enemies (including duplicates).
```yaml
Basic:
- Weight: 94
Pool: default
- Weight: 5 # Replace 5% of all basic enemies with minibosses
Pool: Miniboss
- Weight: 1 # Replace 1% of all basic enemies with bosses
Pool: Boss
```
#### BuffBasicEnemiesAsBosses
If enabled, this causes basic enemies to become a lot stronger when randomized into the slot of a boss.
```yaml
Boss:
- Weight: 100 # All bosses are just basic enemies...
Pool: Basic
BuffBasicEnemiesAsBosses: true # ...but they are strong
```
### Enemies
Under the `Enemies:` setting you can add more nuanced replacements of random enemies.
There are two ways you can adjust enemies:
- Assign to a group of enemies using their category pool (see [Enemy Categories](#enemy-categories))
- Assign to one specifc enemy by using its number (see [Individual Assignments](#individual-assignments))
```yaml
Enemies:
# Replace only the very first Ravenous Crystal Lizard with the final boss
Ravenous Crystal Lizard 4000380: Lords of Cinder
# Replace all regular soldiers with skeletons or small crabs
Hollow Soldiers: Skeletons; Lesser Crab
# Knights remain knights, but variants (i.e. weapons) are still shuffled within the category
High Wall Lothric Knight: High Wall Lothric Knight
```
### DontRandomize
A semicolon-separated list of enemies or enemy types to not randomize (assign to themselves).
It is taken out of its default pool and also custom pools in this case, but it can still be assigned to
[individual enemies](#individual-assignments).
```yaml
DontRandomize: Iudex Gundyr # Iudex Gundyr will be at his vanilla location
Boss:
- Weight: 100
Pool: default # Boss slots other than Iudex Gundyr will never become him
```
### RemoveSource
A semicolon-separated list of enemies or enemy types to remove from all pools.
It can still be assigned to individual enemies.
This is overridden by [`DontRandomize`](#dontrandomize) directives.
```yaml
# Remove the most annoying enemies from all pools
RemoveSource: Bridge Darkeater Midir; Ancient Wyvern Mob; Curse-rotted Greatwood; High Lord Wolnir; Carthus Sandworm
```
### OopsAll
Assigning an enemy or a pool to `OopsAll` sets all pools to that specific enemy or category of enemy. This can still be
overridden using [individual enemy assginments](#individual-assignments), but otherwise every enemy is replaced by
this setting.
```yaml
# This run suddenly got very spooky
OopsAll: Skeletons
```
---
## Enemy Categories
The following enemy category pools are available:
- Any
- Bosses
- Minibosses
- Bosses and Minibosses
- Basic
- Abyss Watchers
- Aldrich, Devourer of Gods
- Ancient Wyvern
- Ancient Wyvern Mob
- Angel Pilgrim
- Basilisk
- Black Knight
- Blackflame Friede
- Boreal Outrider Knight
- Bridge Darkeater Midir
- Cage Spider
- Carthus Sandworm
- Cathedral Evangelist
- Cathedral Knight
- Cemetery Hollow
- Champion Gundyr
- Champion's Gravetender and Gravetender Greatwolf
- Consumed King Oceiros
- Corpse-grub
- Corvian
- Corvian Knight
- Corvian Settler
- Crabs
- Lesser Crab
- Greater Crab
- Ariandel Greater Crab
- Crystal Lizard
- Crystal Sage
- Crystal Sage in Archives
- Curse-rotted Greatwood
- Dancer of the Boreal Valley
- Darkeater Midir
- Darkwraith
- Deacon
- Cathedral Deacon
- Wide Deacon
- Irirthyll Deacon
- Irirthyll Tall Deacon
- Deacons of the Deep
- Deep Accursed
- Demon
- Demon Cleric
- Demon Prince
- Demonic Statue
- Dragonslayer Armour
- Dreg Heap Thrall
- Elder Ghru
- Father Ariandel
- Farron Follower
- Fire Witch
- Gargoyles
- Profaned Capital Gargoyle
- Archives Gargoyle
- Ghru Grunt
- Giant Fly
- Giant Slave
- Grand Archives Scholar
- Grave Warden
- Halflight, Spear of the Church
- Harald Legion Knight
- High Lord Wolnir
- Hobbled Cleric
- Hollow Manservant
- Hollow Soldiers
- Hollow Soldier
- Road of Sacrifices Hollow Soldier
- Cathedral Hollow Soldier
- Lothric Castle Hollow Soldier
- Grand Archives Hollow Soldier
- Hound Rat
- Infested Corpse
- Irirthyll Dungeon Peasant Hollow
- Irithyll Giant Slave
- Irithyll Starved Hound
- Irithyllian Slave
- Iudex Gundyr
- Jailer
- Judicator
- King of the Storm
- Large Hollow Soldiers
- Large Hollow Soldier
- Cathedral Large Hollow Soldier
- Lothric Castle Large Hollow Soldier
- Large Hound Rat
- Large Serpent-Man
- Large Starved Hound
- Locust Preacher
- Lords of Cinder (actually called "Soul of Cinder" ingame)
- Lorian, Elder Prince
- Lothric Knights
- High Wall Lothric Knight
- Lothric Castle Lothric Knight
- Dreg Heap Lothric Knight
- Red-Eyed Lothric Knight
- Lothric Priest
- Lothric, Younger Prince
- Lycanthrope
- Lycanthrope Hunter
- Maggot Belly Starved Hound
- Millwood Knight
- Mimic Chest
- Monstrosity of Sin
- Murkman
- Murkman Summoner
- Nameless King
- Old Demon King
- Passive Locust Preacher
- Peasant Hollow
- Poisonhorn Bug
- Pontiff Knight
- Pontiff Sulyvahn
- Pus of Man
- Ravenous Crystal Lizard
- Reanimated Corpse
- Ringed City Cleric
- Ringed Knight
- Road of Sacrifices Sorcerer
- Rock Lizard
- Rotten Slug
- Serpent-Man
- Serpent-Man Summoner
- Sewer Centipede
- Silver Knight
- Sister Friede
- Skeletons
- Skeleton
- Bonewheel Skeleton
- Carthus Curved Sword Skeleton
- Carthus Shotel Skeleton
- Ringed City Skeleton
- Slave Knight Gael
- Slave Knight Gael 1
- Slave Knight Gael 2
- Small Locust Preacher
- Smouldering Ghru Grunt
- Starved Hound
- Stray Demon
- Sulyvahn's Beast
- Thrall
- Tree Woman
- Vordt of the Boreal Valley
- Winged Knight
- Wolves
- Smaller Wolf
- Larger Wolf
- Greatwolf
- Wretch
- Writhing Flesh
- Catacombs Writhing Flesh
- Smouldering Writhing Flesh
- Anor Londo Writhing Flesh
- Yhorm the Giant
+3 -1
View File
@@ -1,9 +1,11 @@
# Dark Souls III Items
[Game Page] | Items | [Locations]
[Game Page] | [Setup] | Items | [Locations] | [Enemy Randomization]
[Game Page]: /games/Dark%20Souls%20III/info/en
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## Item Groups
+3 -1
View File
@@ -1,9 +1,11 @@
# Dark Souls III Locations
[Game Page] | [Items] | Locations
[Game Page] | [Setup] | [Items] | Locations | [Enemy Randomization]
[Game Page]: /games/Dark%20Souls%20III/info/en
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## Table of Contents
+7
View File
@@ -1,5 +1,12 @@
# Dark Souls III Randomizer Setup Guide
[Game Page] | Setup | [Items] | [Locations] | [Enemy Randomization]
[Game Page]: /games/Dark%20Souls%20III/info/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
Executable → Regular
+19 -7
View File
@@ -19,7 +19,7 @@ import factorio_rcon
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
from MultiServer import mark_raw
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between
from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between, user_path
from .settings import FactorioSettings
from settings import get_settings
@@ -63,7 +63,7 @@ class FactorioCommandProcessor(ClientCommandProcessor):
def _cmd_toggle_chat(self):
"""Toggle sending of chat messages from players on the Factorio server to Archipelago."""
self.ctx.toggle_bridge_chat_out()
def _cmd_rcon_reconnect(self) -> bool:
"""Reconnect the RCON client if its disconnected."""
try:
@@ -88,7 +88,7 @@ class FactorioContext(CommonContext):
def __init__(self, server_address, password, filter_connection_changes: bool, filter_item_sends: bool, bridge_chat_out: bool,
rcon_port: int, rcon_password: str, server_settings_path: str | None,
factorio_server_args: tuple[str, ...]):
config_file: str, factorio_server_args: tuple[str, ...] | list[str]):
super(FactorioContext, self).__init__(server_address, password)
self.send_index: int = 0
self.rcon_client = None
@@ -105,6 +105,7 @@ class FactorioContext(CommonContext):
self.rcon_port: int = rcon_port
self.rcon_password: str = rcon_password
self.server_settings_path: str = server_settings_path
self.config_file: str = config_file
self.additional_factorio_server_args = factorio_server_args
@property
@@ -158,9 +159,11 @@ class FactorioContext(CommonContext):
"--rcon-port", str(self.rcon_port),
"--rcon-password", self.rcon_password,
"--server-settings", self.server_settings_path,
"--config", self.config_file,
*self.additional_factorio_server_args)
else:
return ("--rcon-port", str(self.rcon_port), "--rcon-password", self.rcon_password,
"--config", self.config_file,
*self.additional_factorio_server_args)
@property
@@ -364,7 +367,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name, "--preset", "archipelago"
executable, "--create", savegame_name, "--preset", "archipelago", "--config", ctx.config_file
))
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
*ctx.server_args),
@@ -474,11 +477,11 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
savegame_name = os.path.abspath("Archipelago.zip")
savegame_name = user_path("factorio", "saves", "Archipelago.zip")
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name
executable, "--create", savegame_name, "--config", ctx.config_file
))
factorio_process = subprocess.Popen(
(executable, "--start-server", savegame_name, *ctx.server_args),
@@ -609,6 +612,9 @@ def launch(*new_args: str):
if not os.path.exists(os.path.dirname(executable)):
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
if os.path.isdir(executable) and os.path.exists(os.path.join(executable, "Contents", "MacOS", "factorio")):
# user entered the .App bundle, let's find the executable
executable = os.path.join(executable, "Contents", "MacOS", "factorio")
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
executable = os.path.join(executable, "factorio")
if not os.path.isfile(executable):
@@ -617,9 +623,15 @@ def launch(*new_args: str):
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
config_file = user_path('factorio', 'config', 'apconfig.ini')
if not os.path.exists(config_file):
os.makedirs(os.path.dirname(config_file), exist_ok=True)
with open(config_file, 'w') as f:
f.write(f"[path]\nread-data=__PATH__system-read-data__\nwrite-data={user_path('factorio')}")
asyncio.run(main(lambda: FactorioContext(
args.connect, args.password,
initial_filter_connection_changes, initial_filter_item_sends, initial_bridge_chat_out,
rcon_port, rcon_password, server_settings, rest
rcon_port, rcon_password, server_settings, config_file, rest
)))
colorama.deinit()
+2 -6
View File
@@ -23,7 +23,6 @@ template_env: Optional[jinja2.Environment] = None
data_template: Optional[jinja2.Template] = None
data_final_template: Optional[jinja2.Template] = None
locale_template: Optional[jinja2.Template] = None
control_template: Optional[jinja2.Template] = None
settings_template: Optional[jinja2.Template] = None
@@ -94,7 +93,7 @@ def generate_mod(world: "Factorio", output_directory: str):
multiworld = world.multiworld
random = world.random
global data_final_template, locale_template, control_template, data_template, settings_template
global data_final_template, control_template, data_template, settings_template
with template_load_lock:
if not data_final_template:
def load_template(name: str):
@@ -107,7 +106,6 @@ def generate_mod(world: "Factorio", output_directory: str):
data_template = template_env.get_template("data.lua")
data_final_template = template_env.get_template("data-final-fixes.lua")
locale_template = template_env.get_template(r"locale/en/locale.cfg")
control_template = template_env.get_template("control.lua")
settings_template = template_env.get_template("settings.lua")
# get data for templates
@@ -195,9 +193,7 @@ def generate_mod(world: "Factorio", output_directory: str):
control_template.render(**template_data)))
mod.writing_tasks.append(lambda: (versioned_mod_name + "/settings.lua",
settings_template.render(**template_data)))
mod.writing_tasks.append(lambda: (versioned_mod_name + "/locale/en/locale.cfg",
locale_template.render(**template_data)))
info = base_info.copy()
info["name"] = mod_name
mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json",
@@ -0,0 +1,36 @@
[map-gen-preset-name]
archipelago=Archipelago
[entity-name]
ap-energy-bridge=Archipelago EnergyLink Bridge
[map-gen-preset-description]
archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
[technology-name]
ap-technology-full=__1__'s __2__ (__3__)
ap-technology-hidden=__1__
[technology-description]
ap-technology-full=Researching this technology sends __1__ to __2____3__.
ap-technology-item-advancement=, which is considered a logical advancement
ap-technology-item-useful=, which is considered useful
ap-technology-item-trap=, which is considered fun
ap-technology-hidden=Researching this technology sends something to someone__1__.
[mod-setting-name]
archipelago-death-link=Death Link
[mod-setting-description]
archipelago-death-link=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
[archipelago]
receive-ap-item=Received __1__ from __2__.
receive-ap-catchup=Received __1__ as it is already checked.
receive-sample-item=Received __1__x __2__
sample-inventory-full=Additional items will be sent when inventory space is available.
sample-error=Unable to receive __1__x [item=__2__] as this item does not exist.
fail-to-place=Failed to place __1__ in __2__
[traps]
new-evolution-factor=New evolution factor: __1__
+10 -10
View File
@@ -420,12 +420,12 @@ function update_player(index)
sent = 0
end
if sent > 0 then
player.print("Received " .. sent .. "x [item=" .. name .. ",quality={{ free_sample_quality_name }}]")
player.print({"archipelago.receive-sample-item", sent, "[item=" .. name .. ",quality="..stack.quality.."]"})
data.suppress_full_inventory_message = false
end
if sent ~= count then -- Couldn't full send.
if not data.suppress_full_inventory_message then
player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
player.print({"archipelago.sample-inventory-full"}, {r=1, g=1, b=0.25})
end
data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
samples[name] = count - sent -- Buffer the remaining items
@@ -434,7 +434,7 @@ function update_player(index)
samples[name] = nil -- Remove from the list
end
else
player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.")
player.print({"archipelago.sample-inventory-full", count, name})
samples[name] = nil
end
end
@@ -665,7 +665,7 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores)
end
end
if new_entity == nil then
force.print("Failed to place " .. args.name .. " in " .. serpent.line({x = x, y = y, radius = radius}))
force.print({"archipelago.fail-to-place", args.name, serpent.line({x = x, y = y, radius = radius})})
end
end
@@ -725,7 +725,7 @@ end,
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})
game.print({"traps.new-evolution-factor", new_factor})
end,
["Teleport Trap"] = function()
for _, player in ipairs(game.forces["player"].players) do
@@ -777,10 +777,10 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
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
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."})
game.print({"archipelago.receive-ap-catchup", "[technology=" .. tech.name .. "]"})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
@@ -792,7 +792,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
for _, item_name in ipairs(tech_stack) do
tech = force.technologies[item_name]
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source})
game.play_sound({path="utility/research_completed"})
tech.researched = true
return
@@ -804,7 +804,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
if tech ~= nil then
storage.index_sync[index] = tech
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
@@ -812,7 +812,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
elseif TRAP_TABLE[item_name] ~= nil then
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})
game.print({"archipelago.receive-ap-item", item_name, source})
TRAP_TABLE[item_name]()
end
else
@@ -154,6 +154,13 @@ technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
{#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #}
{%- if location.revealed %}
new_tree_copy.localised_name = {"technology-name.ap-technology-full", "{{ player_names[item.player] }}", "{{ item.name }}", "{{ location.name }}"}
new_tree_copy.localised_description = {"technology-description.ap-technology-full", "{{ item.name }}", "{{ player_names[item.player] }}", {% if item.advancement %}{"technology-description.ap-technology-item-advancement"}{% elif item.useful %}{"technology-description.ap-technology-item-useful"}{% elif item.trap %}{"technology-description.ap-technology-item-trap"}{% else %}""{% endif %}}
{%- else %}
new_tree_copy.localised_name = {"technology-name.ap-technology-hidden", "{{location.name}}"}
new_tree_copy.localised_description = {"technology-description.ap-technology-hidden", {% if tech_tree_information == 1 and item.advancement %}{"technology-description.ap-technology-item-advancement"}{% else %}""{% endif %}}
{% endif -%}
{% if location.crafted_item is not none %}
new_tree_copy.research_trigger = {
type = "{{ 'craft-fluid' if location.crafted_item in liquids else 'craft-item' }}",
@@ -13,7 +13,6 @@ end
local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"])
energy_bridge.name = "ap-energy-bridge"
energy_bridge.minable.result = "ap-energy-bridge"
energy_bridge.localised_name = "Archipelago EnergyLink Bridge"
energy_bridge.energy_source.buffer_capacity = "50MJ"
energy_bridge.energy_source.input_flow_limit = "10MW"
energy_bridge.energy_source.output_flow_limit = "10MW"
@@ -25,7 +24,6 @@ data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge
local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"])
energy_bridge_item.name = "ap-energy-bridge"
energy_bridge_item.localised_name = "Archipelago EnergyLink Bridge"
energy_bridge_item.place_result = energy_bridge.name
tint_icon(energy_bridge_item, energy_bridge_tint())
data.raw["item"]["ap-energy-bridge"] = energy_bridge_item
@@ -35,7 +33,6 @@ energy_bridge_recipe.name = "ap-energy-bridge"
energy_bridge_recipe.results = { {type = "item", name = energy_bridge_item.name, amount = 1} }
energy_bridge_recipe.energy_required = 1
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
data.raw["map-gen-presets"].default["archipelago"] = {{ dict_to_lua({"default": False, "order": "a", "basic_settings": world_gen["basic"], "advanced_settings": world_gen["advanced"]}) }}
@@ -1,31 +0,0 @@
[map-gen-preset-name]
archipelago=Archipelago
[map-gen-preset-description]
archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
[technology-name]
{% for location, item in locations %}
{%- if location.revealed %}
ap-{{ location.address }}-={{ player_names[item.player] }}'s {{ item.name }} ({{ location.name }})
{%- else %}
ap-{{ location.address }}-= {{location.name}}
{%- endif -%}
{% endfor %}
[technology-description]
{% for location, item in locations %}
{%- if location.revealed %}
ap-{{ location.address }}-=Researching this technology sends {{ item.name }} to {{ player_names[item.player] }}{% if item.advancement %}, which is considered a logical advancement{% elif item.useful %}, which is considered useful{% elif item.trap %}, which is considered fun{% endif %}.
{%- elif tech_tree_information == 1 and item.advancement %}
ap-{{ location.address }}-=Researching this technology sends something to someone, which is considered a logical advancement.
{%- else %}
ap-{{ location.address }}-=Researching this technology sends something to someone.
{%- endif -%}
{% endfor %}
[mod-setting-name]
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Death Link
[mod-setting-description]
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
@@ -21,6 +21,8 @@ data:extend({
type = "bool-setting",
name = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}",
setting_type = "runtime-global",
localised_name = {"mod-setting-name.archipelago-death-link"},
localised_description = {"mod-setting-description.archipelago-death-link"},
{% if death_link %}
default_value = true
{% else %}
+4 -1
View File
@@ -60,7 +60,7 @@ adding more randomness and "mystery" to your options. Every configurable setting
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
options or in some cases nested within other nested options. The only options that should exist in root
are `description`, `name`, `game`, `requires`, and the name of the games you want options for.
are `description`, `name`, `game`, `quantity`, `requires`, and the name of the games you want options for.
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files
using this to detail the intention of the file.
@@ -78,6 +78,9 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan
* `game` is where either your chosen game goes or, if you would like, can be filled with multiple games each with
different weights.
* `quantity` is the amount of times this yaml should be used when generating. This option is optional, the default value is 1.
To ensure that the name is unique with a value of at least two, the keywords from above must be used.
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version,
options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
+1 -1
View File
@@ -99,7 +99,7 @@ case-sensitive. You can also use item groups and location groups that are define
## Item Plando Examples
```yaml
plando_items:
plando_items:
# Example block - Pokémon Red and Blue
- items:
Potion: 3
+12
View File
@@ -232,6 +232,9 @@ class JakAndDaxterWorld(World):
power_cell_thresholds_minus_one: list[int]
trap_weights: tuple[list[str], list[int]]
# UT Yaml-less flag
ut_can_gen_without_yaml = True
# Store these dictionaries for speed improvements.
level_to_regions: dict[str, list[JakAndDaxterRegion]] # Contains all levels and regions.
level_to_orb_regions: dict[str, list[JakAndDaxterRegion]] # Contains only regions which contain orbs.
@@ -243,6 +246,15 @@ class JakAndDaxterWorld(World):
self.level_to_regions = defaultdict(list)
self.level_to_orb_regions = defaultdict(list)
# Implement Universal Tracker support - reset all options to those from UT's gen if applicable.
if hasattr(self.multiworld, "re_gen_passthrough"):
if jak1_name in self.multiworld.re_gen_passthrough:
for key, val in self.multiworld.re_gen_passthrough[jak1_name].items():
try:
getattr(self.options, key).value = val
except AttributeError:
pass
# Cache the power cell threshold values for quicker reference.
self.power_cell_thresholds = [
self.options.fire_canyon_cell_count.value,
-1
View File
@@ -579,7 +579,6 @@ visit_locking_dict = {
ItemName.IceCream,
ItemName.WaytotheDawn,
ItemName.IdentityDisk,
ItemName.IceCream,
ItemName.NamineSketches
],
"AllVisitLocking": {
+2
View File
@@ -184,6 +184,8 @@ class KH2World(World):
if self.visitlocking_dict[item] == 0:
self.visitlocking_dict.pop(item)
self.multiworld.push_precollected(self.create_item(item))
# tt is 3 visits so 2nd visit locking unlocks only the third visit
self.multiworld.push_precollected(self.create_item(ItemName.IceCream))
for _ in range(self.options.RandomVisitLockingItem.value):
if sum(self.visitlocking_dict.values()) <= 0:
+2 -2
View File
@@ -14,7 +14,7 @@ Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.
2. Lua Backend from the OpenKH Mod Manager
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
- Needed for Archipelago
1. [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases)
1. [Archipelago Launcher](https://github.com/ArchipelagoMW/Archipelago/releases)
2. Install the Archipelago Companion mod from `JaredWeakStrike/APCompanion` using OpenKH Mod Manager
3. Install the mod from `TopazTK/KH2-ArchipelagoEnablers` using OpenKH Mod manager
1. Do Note that if you have `KH2FM-Mods-equations19/auto-save` OR `KH2FM-Mods-equations19/soft-reset` you should download `TopazTK/KH2-ArchipelagoEnablersLITE` instead
@@ -58,7 +58,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot
## Using the KH2 Client
Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoLauncher.exe](https://github.com/ArchipelagoMW/Archipelago/releases) and select the KH2 Client.<br>
When you successfully connect to the server the client will automatically hook into the game to send/receive checks. <br>
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.<br>
+4 -3
View File
@@ -80,9 +80,10 @@ class LinksAwakeningSettings(settings.Group):
class GfxModFile(settings.FilePath):
"""
Gfxmod file, get it from upstream: https://github.com/daid/LADXR/tree/master/gfx
Only .bin or .bdiff files
The same directory will be checked for a matching text modification file
Gfxmod file, select one from the `Archipelago/data/sprites/ladx/` folder,
or make your own. You can generate a template here: https://ladx-gfx.3and3.dev/
Only .bin or .bdiff files.
Extended spritesheets from the upstream randomizer are not supported.
"""
def browse(self, filetypes=None, **kwargs):
filetypes = [("Binary / Patch files", [".bin", ".bdiff"])]
+2 -2
View File
@@ -12,8 +12,8 @@
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Links Awakening DX ROM file. This only needs to be done once..
2. The first time you patch your game, you will be asked to locate your base ROM file.
This is your Links Awakening DX ROM file. This only needs to be done once.
3. You should assign your emulator as your default program for launching ROM
files.
+1 -1
View File
@@ -48,7 +48,7 @@ class L2ACWorld(World):
"""
The Ancient Cave is a roguelike dungeon crawling game built into
the RGP Lufia II. Face 99 floors of ever harder to beat monsters,
random items and find new companions on the way to face the Royal
random items and find new companions on the way to face the Master
Jelly in the end. Can you beat it?
"""
game: ClassVar[str] = "Lufia II Ancient Cave"
@@ -9,8 +9,8 @@ config file.
As you may or may not know, randomization was already a core feature of the Ancient Cave in Lufia II, basically being a
whole game within a game. The Ancient Cave has 99 floors with increasingly hard enemies, red chests and blue chests. At
the end of the Ancient Cave you get to fight the Royal Jelly... if you make it that far. You cannot lose the Royal
Jelly fight as it kills itself after giving you three rounds to try and kill it (or manage to vanquish your own party,
the end of the Ancient Cave you get to fight the Master Jelly... if you make it that far. The Master
Jelly gives you three rounds to try and kill it (or manage to vanquish your own party,
whichever one you can manage).
The Randomizer allows you to set different goals and modify the game in several other ways
-1
View File
@@ -56,7 +56,6 @@ If you would like to validate your config file to make sure it works, you may do
4. You will be presented with a server page, from which you can download your patch file.
5. Double-click on your patch file, and SNIClient will launch automatically, create your ROM from the patch file, and
open your emulator for you.
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
## Joining a MultiWorld Game
+79 -36
View File
@@ -56,6 +56,9 @@ class MarioLand2World(World):
web = MarioLand2WebWorld()
ut_can_gen_without_yaml = True
glitches_item_name = "ut_glitch"
item_name_groups = {
"Level Progression": {
item_name for item_name in items if item_name.endswith(("Progression", "Secret", "Secret 1", "Secret 2"))
@@ -94,42 +97,51 @@ class MarioLand2World(World):
self.max_coin_locations = {}
self.sprite_data = {}
self.coin_fragments_required = 0
self.ut = False
def generate_early(self):
self.sprite_data = deepcopy(level_sprites)
if self.options.randomize_enemies:
randomize_enemies(self.sprite_data, self.random)
if self.options.randomize_platforms:
randomize_platforms(self.sprite_data, self.random)
if self.options.marios_castle_midway_bell:
self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell"
if self.options.auto_scroll_chances == "vanilla":
self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)]
if hasattr(self.multiworld, "re_gen_passthrough") and self.game in self.multiworld.re_gen_passthrough:
self.ut = True
for key, value in self.multiworld.re_gen_passthrough[self.game].items():
if hasattr(self.options, key):
getattr(self.options, key).value = value
else:
setattr(self, key, value)
else:
self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances)
for _ in range(32)]
self.sprite_data = deepcopy(level_sprites)
self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0
unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"]
if not self.options.shuffle_midway_bells:
unbeatable_scroll_levels.append("Pumpkin Zone 1")
for level, i in enumerate(self.auto_scroll_levels):
if i == 1:
if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"):
self.auto_scroll_levels[level] = 2
elif self.options.auto_scroll_mode == "chaos":
if (self.options.accessibility == "full"
and level_id_to_name[level] in unbeatable_scroll_levels):
if self.options.marios_castle_midway_bell:
self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell"
if self.options.randomize_enemies:
randomize_enemies(self.sprite_data, self.random)
if self.options.randomize_platforms:
randomize_platforms(self.sprite_data, self.random)
if self.options.auto_scroll_chances == "vanilla":
self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)]
else:
self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances)
for _ in range(32)]
self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0
unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"]
if not self.options.shuffle_midway_bells:
unbeatable_scroll_levels.append("Pumpkin Zone 1")
for level, i in enumerate(self.auto_scroll_levels):
if i == 1:
if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"):
self.auto_scroll_levels[level] = 2
else:
self.auto_scroll_levels[level] = self.random.randint(1, 3)
elif (self.options.accessibility == "full"
and level_id_to_name[level] in unbeatable_scroll_levels):
self.auto_scroll_levels[level] = 0
if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key:
self.auto_scroll_levels[level] = 3
elif self.options.auto_scroll_mode == "chaos":
if (self.options.accessibility == "full"
and level_id_to_name[level] in unbeatable_scroll_levels):
self.auto_scroll_levels[level] = 2
else:
self.auto_scroll_levels[level] = self.random.randint(1, 3)
elif (self.options.accessibility == "full"
and level_id_to_name[level] in unbeatable_scroll_levels):
self.auto_scroll_levels[level] = 0
if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key:
self.auto_scroll_levels[level] = 3
def create_regions(self):
menu_region = Region("Menu", self.player, self.multiworld)
@@ -178,7 +190,10 @@ class MarioLand2World(World):
wario.place_locked_item(MarioLand2Item("Wario Defeated", ItemClassification.progression, None, self.player))
if self.options.coinsanity:
coinsanity_checks = self.options.coinsanity_checks.value
if hasattr(self.multiworld, "generation_is_fake"):
coinsanity_checks = self.options.coinsanity_checks.range_end
else:
coinsanity_checks = self.options.coinsanity_checks.value
self.num_coin_locations = [[region, 1] for region in created_regions if region != "Mario's Castle"]
self.max_coin_locations = {region: len(coins_coords[region]) for region in created_regions
if region != "Mario's Castle"}
@@ -423,12 +438,40 @@ class MarioLand2World(World):
self.multiworld.itempool += [self.create_item(item_name) for _ in range(count)]
def fill_slot_data(self):
return {
"energy_link": self.options.energy_link.value
}
# Expose settings for accurate tracker logic
shark_count: int = [
self.multiworld.worlds[self.player].sprite_data["Turtle Zone 1"][i]["sprite"]
for i in (27, 28)
].count("Shark")
crane_count: int = [
self.multiworld.worlds[self.player].sprite_data["Mario Zone 3"][i]["sprite"]
for i in (17, 18, 25)
].count("Claw Grabber")
options_dict = self.options.as_dict(
"energy_link",
"shuffle_golden_coins",
"required_golden_coins",
"coinsanity",
"shuffle_midway_bells",
"marios_castle_midway_bell",
"shuffle_pipe_traversal",
"auto_scroll_mode"
)
options_dict.update({
"auto_scroll_levels": self.auto_scroll_levels,
"turtle_zone_1_shark_count": shark_count,
"mario_zone_3_crane_count": crane_count,
"coin_fragments_required": self.coin_fragments_required
})
return options_dict
@staticmethod
def interpret_slot_data(slot_data):
return slot_data
def create_item(self, name: str) -> Item:
return MarioLand2Item(name, items[name], self.item_name_to_id[name], self.player)
return MarioLand2Item(name, items[name] if name in items else ItemClassification.progression,
self.item_name_to_id[name] if name in self.item_name_to_id else None, self.player)
def get_filler_item_name(self):
return "1 Coin"
+13 -4
View File
@@ -5,6 +5,8 @@ def is_auto_scroll(state, player, level):
level_id = level_name_to_id[level]
if state.has_any(["Cancel Auto Scroll", f"Cancel Auto Scroll - {level}"], player):
return False
if state.has("ut_glitch", player) and state.multiworld.worlds[player].auto_scroll_levels[level_id] == 3:
return state.has(f"Auto Scroll - {level}", player)
return state.multiworld.worlds[player].auto_scroll_levels[level_id] > 0
@@ -287,8 +289,12 @@ def mario_zone_3_coins(state, player, coins):
if state.has("Carrot", player):
reachable_spike_coins = 15
else:
sprites = state.multiworld.worlds[player].sprite_data["Mario Zone 3"]
reachable_spike_coins = min(3, len({sprites[i]["sprite"] == "Claw Grabber" for i in (17, 18, 25)})
if state.multiworld.worlds[player].ut:
claw_grabbers = state.multiworld.worlds[player].mario_zone_3_crane_count
else:
sprites = state.multiworld.worlds[player].sprite_data["Mario Zone 3"]
claw_grabbers = len({sprites[i]["sprite"] == "Claw Grabber" for i in (17, 18, 25)})
reachable_spike_coins = min(3, claw_grabbers
+ state.has("Mushroom", player) + state.has("Fire Flower", player)) * 5
reachable_coins += reachable_spike_coins
if not auto_scroll:
@@ -309,8 +315,11 @@ def mario_zone_4_coins(state, player, coins):
def not_blocked_by_sharks(state, player):
sharks = [state.multiworld.worlds[player].sprite_data["Turtle Zone 1"][i]["sprite"]
for i in (27, 28)].count("Shark")
if state.multiworld.worlds[player].ut:
sharks = state.multiworld.worlds[player].turtle_zone_1_shark_count
else:
sharks = [state.multiworld.worlds[player].sprite_data["Turtle Zone 1"][i]["sprite"]
for i in (27, 28)].count("Shark")
if state.has("Carrot", player) or not sharks:
return True
if sharks == 2:
+4 -1
View File
@@ -316,7 +316,7 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
"Searing Mega Shard Shop": [
"Searing Crags - Falling Rocks Shop",
"Searing Crags - Before Final Climb Shop",
"Searing Crags - Key of Strength Shop",
"Searing Crags - Key of Strength Room",
],
"Before Final Climb Shop": [
"Searing Crags - Raining Rocks Checkpoint",
@@ -330,6 +330,9 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
"Searing Crags - Top",
],
"Key of Strength Shop": [
"Searing Crags - Key of Strength Room",
],
"Key of Strength Room": [
"Searing Crags - Searing Mega Shard Shop",
],
"Triple Ball Spinner Checkpoint": [
+1 -5
View File
@@ -7,7 +7,6 @@ from Options import PlandoConnection
if TYPE_CHECKING:
from . import MessengerWorld
PORTALS: list[str] = [
"Autumn Hills",
"Riviere Turquoise",
@@ -17,7 +16,6 @@ PORTALS: list[str] = [
"Glacial Peak",
]
SHOP_POINTS: dict[str, list[str]] = {
"Autumn Hills": [
"Climbing Claws",
@@ -112,7 +110,6 @@ SHOP_POINTS: dict[str, list[str]] = {
]
}
CHECKPOINTS: dict[str, list[str]] = {
"Autumn Hills": [
"Hope Latch",
@@ -185,7 +182,6 @@ CHECKPOINTS: dict[str, list[str]] = {
]
}
REGION_ORDER: list[str] = [
"Autumn Hills",
"Forlorn Temple",
@@ -306,4 +302,4 @@ def add_closed_portal_reqs(world: "MessengerWorld") -> None:
closed_portals = [entrance for entrance in PORTALS if f"{entrance} Portal" not in world.starting_portals]
for portal in closed_portals:
tower_exit = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player)
tower_exit.access_rule = lambda state, portal_item=portal: state.has(portal_item, world.player)
tower_exit.access_rule = lambda state, portal_item=portal: state.has(f"{portal_item} Portal", world.player)
+1 -1
View File
@@ -96,7 +96,7 @@ LOCATIONS: dict[str, list[str]] = {
"Searing Crags - Power Thistle",
"Searing Crags - Astral Tea Leaves",
],
"Searing Crags - Key of Strength Shop": [
"Searing Crags - Key of Strength Room": [
"Searing Crags - Key of Strength",
],
"Searing Crags - Portal": [
+21 -8
View File
@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from BaseClasses import CollectionState, CollectionRule
from BaseClasses import CollectionState, CollectionRule, Region
from worlds.generic.Rules import add_rule, allow_self_locking_items
from .constants import NOTES, PHOBEKINS
from .options import MessengerAccessibility
@@ -13,6 +13,7 @@ class MessengerRules:
player: int
world: "MessengerWorld"
connection_rules: dict[str, CollectionRule]
indirect_conditions: dict[str, list[Region]]
region_rules: dict[str, CollectionRule]
location_rules: dict[str, CollectionRule]
maximum_price: int
@@ -108,17 +109,18 @@ class MessengerRules:
"Searing Crags - Right -> Searing Crags - Portal":
lambda state: self.has_tabi(state) and self.has_wingsuit(state),
"Searing Crags - Colossuses Shop -> Searing Crags - Key of Strength Shop":
lambda state: state.has("Power Thistle", self.player)
and (self.has_dart(state)
or (self.has_wingsuit(state)
and self.can_destroy_projectiles(state))),
lambda state: state.has("Power Thistle", self.player),
"Searing Crags - Key of Strength Shop -> Searing Crags - Key of Strength Room":
lambda state: self.has_dart(state)
or (self.has_wingsuit(state)
and self.can_destroy_projectiles(state)),
"Searing Crags - Falling Rocks Shop -> Searing Crags - Searing Mega Shard Shop":
self.has_dart,
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Before Final Climb Shop":
lambda state: self.has_dart(state) or self.can_destroy_projectiles(state),
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Falling Rocks Shop":
self.has_dart,
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop":
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Room":
self.false,
"Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop":
self.has_dart,
@@ -219,6 +221,16 @@ class MessengerRules:
lambda state: self.can_dboost(state) or self.has_dart(state),
}
# dict of connection names and the regions checked in the requirements to traverse the exit
self.indirect_conditions = {
"Howling Grotto - Breezy Crushers Checkpoint -> Howling Grotto - Crushing Pits Shop": [
self.world.get_region("Howling Grotto - Emerald Golem Shop")
],
"Glacial Peak - Left -> Elemental Skylands - Air Shmup": [
self.world.get_location("Quillshroom Marsh - Queen of Quills").parent_region
],
}
self.location_rules = {
# hq
"Money Wrench": self.can_shop,
@@ -364,6 +376,8 @@ class MessengerRules:
for entrance_name, rule in self.connection_rules.items():
entrance = multiworld.get_entrance(entrance_name, self.player)
entrance.access_rule = rule
for region in self.indirect_conditions.get(entrance_name, ()):
multiworld.register_indirect_condition(region, entrance)
for loc in multiworld.get_locations(self.player):
if loc.name in self.location_rules:
loc.access_rule = self.location_rules[loc.name]
@@ -406,7 +420,7 @@ class MessengerHardRules(MessengerRules):
lambda state: self.has_dart(state) or
(self.can_destroy_projectiles(state) and
(self.has_wingsuit(state) or self.can_dboost(state))),
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop":
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Room":
lambda state: self.can_leash(state) or self.has_windmill(state),
"Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop":
self.true,
@@ -512,7 +526,6 @@ class MessengerOOBRules(MessengerRules):
self.location_rules = {
"Bamboo Creek - Claustro": self.has_wingsuit,
"Searing Crags - Key of Strength": self.has_wingsuit,
"Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
"Searing Crags - Pyro": self.has_tabi,
"Underworld - Key of Chaos": self.has_tabi,
+26
View File
@@ -35,3 +35,29 @@ class PortalTestBase(MessengerTestBase):
test_state.collect(item)
self.assertTrue(entrance.can_reach(test_state), grouping)
entrance.access_rule = lambda state: True
class PortalUnlockTest(MessengerTestBase):
options = {
"available_portals": 3,
}
def test_unlocking_portal(self) -> None:
"""Validate that unlocking the portal event actually unlock the portal in HQ"""
print(self.world.starting_portals)
for portal in PORTALS:
name = f"{portal} Portal"
if name in self.world.starting_portals:
continue
entrance_name = f"ToTHQ {name}"
with self.subTest(portal=name, entrance_name=entrance_name):
hq_portal = self.multiworld.get_entrance(entrance_name, self.player)
test_state = CollectionState(self.multiworld)
self.assertFalse(hq_portal.can_reach(test_state), "reachable with nothing")
event = self.multiworld.get_location(name, self.player)
test_state.collect(event.item)
self.assertTrue(hq_portal.can_reach(test_state))
+1 -1
View File
@@ -341,7 +341,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if CHECKSUM_BLUE != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match US GBA Blue Version.'
raise Exception('Supplied Base Rom does not match US GBA Blue Version. '
'Please provide the correct ROM version')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
+14
View File
@@ -712,4 +712,18 @@ SONG_DATA: Dict[str, SongData] = {
"Chasing Daylight": SongData(2900836, "96-0", "Wuthering Waves Pioneer Podcast", False, 3, 5, 8),
"CATCH ME IF YOU CAN": SongData(2900837, "96-1", "Wuthering Waves Pioneer Podcast", False, 4, 6, 9),
"RUNNING FOR YOUR LIFE": SongData(2900838, "96-2", "Wuthering Waves Pioneer Podcast", False, 2, 5, 8),
"Denpa-tic Imaginary Girl": SongData(2900839, "43-73", "MD Plus Project", False, 4, 6, 9),
"Don't Fight The Music": SongData(2900840, "97-0", "DASH AND SHOOT!", False, 6, 8, 10),
"Viyella's Scream": SongData(2900841, "97-1", "DASH AND SHOOT!", False, 7, 9, 11),
"Final Flash Flight": SongData(2900842, "97-2", "DASH AND SHOOT!", False, 5, 7, 10),
"YURUSHITE": SongData(2900843, "97-3", "DASH AND SHOOT!", False, 6, 8, 11),
"girls.exe": SongData(2900844, "97-4", "DASH AND SHOOT!", False, 6, 8, 11),
"Baqeela": SongData(2900845, "97-5", "DASH AND SHOOT!", False, 6, 8, 10),
"Help me, ERINNNNNN!!": SongData(2900846, "43-74", "MD Plus Project", False, 4, 8, 10),
"Utakata, Ai no Mahoroba": SongData(2900847, "98-0", "Touhou Mugakudan -V-", False, 3, 5, 7),
"dynamite": SongData(2900848, "98-1", "Touhou Mugakudan -V-", False, 5, 7, 9),
"Matsuyoi Nightbug": SongData(2900849, "98-2", "Touhou Mugakudan -V-", False, 5, 7, 10),
"Coooonsultant!": SongData(2900850, "98-3", "Touhou Mugakudan -V-", False, 6, 8, 10),
"Stop at the affected part and melt quickly - Madness Udine Quarter": SongData(2900851, "98-4", "Touhou Mugakudan -V-", False, 4, 6, 10),
"Ultimate taste": SongData(2900852, "98-5", "Touhou Mugakudan -V-", False, 6, 8, 11),
}
+15 -5
View File
@@ -3,6 +3,7 @@ from BaseClasses import Region, Item, ItemClassification, Tutorial
from typing import List, ClassVar, Type, Set
from math import floor
from Options import PerGameCommonOptions, OptionError
from rule_builder.rules import Has
from .Options import MuseDashOptions, md_option_groups
from .Items import MuseDashSongItem, MuseDashFixedItem
@@ -33,7 +34,16 @@ class MuseDashWebWorld(WebWorld):
["Shiny"]
)
tutorials = [setup_en, setup_es]
setup_it = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Italiano",
"setup_it.md",
"setup/it",
["UCSA"]
)
tutorials = [setup_en, setup_es, setup_it]
options_presets = MuseDashPresets
option_groups = md_option_groups
@@ -284,17 +294,17 @@ class MuseDashWorld(World):
# Adds 2 item locations per song/album to the menu region.
for i in range(0, len(all_selected_locations)):
name = all_selected_locations[i]
rule = Has(name)
loc1 = MuseDashLocation(self.player, name + "-0", self.md_collection.song_locations[name + "-0"], menu_region)
loc1.access_rule = lambda state, place=name: state.has(place, self.player)
self.set_rule(loc1, rule)
menu_region.locations.append(loc1)
loc2 = MuseDashLocation(self.player, name + "-1", self.md_collection.song_locations[name + "-1"], menu_region)
loc2.access_rule = lambda state, place=name: state.has(place, self.player)
self.set_rule(loc2, rule)
menu_region.locations.append(loc2)
def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: \
state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count())
self.set_completion_rule(Has(self.md_collection.MUSIC_SHEET_NAME, self.get_music_sheet_win_count()))
def get_available_traps(self) -> List[str]:
full_trap_list = self.md_collection.trap_items.keys()

Some files were not shown because too many files have changed in this diff Show More