forked from mirror/Archipelago
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f110a7efbc | |||
| f9c5868f7b | |||
| fde71f66f3 | |||
| e6e0bc3042 | |||
| 5ccef9802a | |||
| 09aba95c03 | |||
| 2a8ebe2399 | |||
| ad0af18f51 | |||
| 65277a50ce | |||
| d241829585 | |||
| b24b3d10b6 | |||
| 787ddc400c | |||
| 545171f3f4 | |||
| d0abfeb88b | |||
| b575983599 | |||
| d8d148ac13 | |||
| 24f75ba072 | |||
| 38e77e1b46 | |||
| 799e0b7b0f | |||
| 4ef1fb7630 | |||
| 0cd81ff500 | |||
| 55a1b12cb7 | |||
| 3fcd337f65 | |||
| 7a5acfeceb | |||
| 06dc1b897c | |||
| 4a28888a66 | |||
| d0407a74d6 | |||
| e0810022f8 | |||
| 29a6f40c2b | |||
| 2bd572c23d | |||
| b59e52a103 | |||
| 6d9d340c71 | |||
| 4486510fbc | |||
| a68109f5a7 | |||
| 234b0581c9 | |||
| 798eeaad91 | |||
| 1875062099 | |||
| 9da3c29990 | |||
| 90b72c0fa5 | |||
| fd8f3fcd8b | |||
| d36ba62243 | |||
| 8ac335f92d | |||
| 74a0cd6022 | |||
| 31f5a6c4ea | |||
| be9c7b1728 | |||
| a0236b2d74 | |||
| af39d7926a | |||
| a48bf0195f | |||
| 73856f63c8 | |||
| d5cc201138 | |||
| 4346191c55 | |||
| 0601494e39 | |||
| 78937054de | |||
| 59674926ed | |||
| b716096a4b | |||
| c851eff521 | |||
| 01033940d6 | |||
| 15561e1e2d | |||
| 9c78edc764 | |||
| d2395de9fa | |||
| d41cec6494 | |||
| dc911399fb | |||
| 0b38065123 | |||
| a6c1347102 | |||
| 0a742b6c98 | |||
| 66712bbd87 | |||
| 5f9e38b783 | |||
| bdde2140b3 | |||
| 42d24b4869 | |||
| 14aa6571de | |||
| 37d32a10b1 | |||
| 0ef2ed5b81 |
@@ -46,7 +46,6 @@ dist
|
||||
/prof/
|
||||
README.html
|
||||
.vs/
|
||||
EnemizerCLI/
|
||||
/Players/
|
||||
/SNI/
|
||||
/sni-*/
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"../test/programs/test_multi_server.py",
|
||||
"../test/utils/__init__.py",
|
||||
"../test/webhost/test_descriptions.py",
|
||||
"../test/webhost/test_suuid.py",
|
||||
"../worlds/AutoSNIClient.py",
|
||||
"type_check.py"
|
||||
],
|
||||
|
||||
@@ -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
-1
@@ -514,7 +514,7 @@ class MultiWorld():
|
||||
state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic."""
|
||||
self.indirect_connections.setdefault(region, set()).add(entrance)
|
||||
|
||||
def get_locations(self, player: Optional[int] = None) -> Iterable[Location]:
|
||||
def get_locations(self, player: int | None = None) -> Collection[Location]:
|
||||
if player is not None:
|
||||
return self.regions.location_cache[player].values()
|
||||
return Utils.RepeatableChain(tuple(self.regions.location_cache[player].values()
|
||||
@@ -1038,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:
|
||||
@@ -1049,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:
|
||||
@@ -1077,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]:
|
||||
@@ -1089,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
@@ -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
@@ -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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import collections
|
||||
from collections import Counter, defaultdict, deque
|
||||
from collections.abc import Iterable, Sequence
|
||||
import itertools
|
||||
import logging
|
||||
import typing
|
||||
from collections import Counter, deque
|
||||
from typing import Callable, Literal
|
||||
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, PlandoItemBlock
|
||||
from Options import Accessibility
|
||||
@@ -12,10 +12,11 @@ from worlds.generic.Rules import add_item_rule
|
||||
|
||||
|
||||
class FillError(RuntimeError):
|
||||
def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None:
|
||||
if "multiworld" in kwargs and isinstance(args[0], str):
|
||||
placements = (args[0] + f"\nAll Placements:\n" +
|
||||
f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}")
|
||||
def __init__(self, *args: str | object, **kwargs: object) -> None:
|
||||
multiworld = kwargs.get("multiworld")
|
||||
if isinstance(multiworld, MultiWorld) and isinstance(args[0], str):
|
||||
placements = (args[0] + "\nAll Placements:\n" +
|
||||
f"{[(loc, loc.item) for loc in multiworld.get_filled_locations()]}")
|
||||
args = (placements, *args[1:])
|
||||
super().__init__(*args)
|
||||
|
||||
@@ -24,8 +25,9 @@ def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
|
||||
logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.")
|
||||
|
||||
|
||||
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple(),
|
||||
locations: typing.Optional[typing.List[Location]] = None) -> CollectionState:
|
||||
def sweep_from_pool(base_state: CollectionState,
|
||||
itempool: Sequence[Item] = (),
|
||||
locations: Iterable[Location] | None = None) -> CollectionState:
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
new_state.collect(item, True)
|
||||
@@ -33,9 +35,9 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
return new_state
|
||||
|
||||
|
||||
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
|
||||
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: list[Location],
|
||||
item_pool: list[Item], single_player_placement: bool = False, lock: bool = False,
|
||||
swap: bool = True, on_place: Callable[[Location], None] | None = None,
|
||||
allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True,
|
||||
name: str = "Unknown") -> None:
|
||||
"""
|
||||
@@ -51,11 +53,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
:param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations
|
||||
:param name: name of this fill step for progress logging purposes
|
||||
"""
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
unplaced_items: list[Item] = []
|
||||
placements: list[Location] = []
|
||||
cleanup_required = False
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str, bool]] = Counter()
|
||||
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
|
||||
swapped_items: Counter[tuple[int, str, bool]] = Counter()
|
||||
reachable_items: dict[int, deque[Item]] = {}
|
||||
for item in item_pool:
|
||||
reachable_items.setdefault(item.player, deque()).append(item)
|
||||
|
||||
@@ -64,6 +66,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
placed = 0
|
||||
|
||||
while any(reachable_items.values()) and locations:
|
||||
items_to_place: list[Item]
|
||||
if one_item_per_player:
|
||||
# grab one item per player
|
||||
items_to_place = [items.pop()
|
||||
@@ -95,13 +98,15 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
break
|
||||
item_to_place = items_to_place.pop(0)
|
||||
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
spot_to_fill: Location | None = None
|
||||
|
||||
# if minimal accessibility, only check whether location is reachable if game not beatable
|
||||
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
||||
perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state,
|
||||
item_to_place.player) \
|
||||
if single_player_placement else not has_beaten_game
|
||||
perform_access_check = (
|
||||
(not multiworld.has_beaten_game(maximum_exploration_state, item_to_place.player))
|
||||
if single_player_placement
|
||||
else not has_beaten_game
|
||||
)
|
||||
else:
|
||||
perform_access_check = True
|
||||
|
||||
@@ -118,7 +123,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
if swap:
|
||||
# Keep a cache of previous safe swap states that might be usable to sweep from to produce the next
|
||||
# swap state, instead of sweeping from `base_state` each time.
|
||||
previous_safe_swap_state_cache: typing.Deque[CollectionState] = deque()
|
||||
previous_safe_swap_state_cache: deque[CollectionState] = deque()
|
||||
# Almost never are more than 2 states needed. The rare cases that do are usually highly restrictive
|
||||
# single_player_placement=True pre-fills which can go through more than 10 states in some seeds.
|
||||
max_swap_base_state_cache_length = 3
|
||||
@@ -215,7 +220,10 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
base_state, [], multiworld.get_filled_locations(item.player)
|
||||
if single_player_placement else None)
|
||||
for placement in placements:
|
||||
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
|
||||
if (
|
||||
multiworld.worlds[placement.item.player].options.accessibility != "minimal" and
|
||||
not placement.can_reach(state)
|
||||
):
|
||||
placement.item.location = None
|
||||
unplaced_items.append(placement.item)
|
||||
placement.item = None
|
||||
@@ -255,14 +263,14 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
|
||||
|
||||
def remaining_fill(multiworld: MultiWorld,
|
||||
locations: typing.List[Location],
|
||||
itempool: typing.List[Item],
|
||||
name: str = "Remaining",
|
||||
locations: list[Location],
|
||||
itempool: list[Item],
|
||||
name: str = "Remaining",
|
||||
move_unplaceable_to_start_inventory: bool = False,
|
||||
check_location_can_fill: bool = False) -> None:
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
unplaced_items: list[Item] = []
|
||||
placements: list[Location] = []
|
||||
swapped_items: Counter[tuple[int, str]] = Counter()
|
||||
total = min(len(itempool), len(locations))
|
||||
placed = 0
|
||||
|
||||
@@ -270,15 +278,15 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
if check_location_can_fill:
|
||||
state = CollectionState(multiworld)
|
||||
|
||||
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
|
||||
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item) -> bool:
|
||||
return location_to_fill.can_fill(state, item_to_fill, check_access=False)
|
||||
else:
|
||||
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
|
||||
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item) -> bool:
|
||||
return location_to_fill.item_rule(item_to_fill)
|
||||
|
||||
while locations and itempool:
|
||||
item_to_place = itempool.pop()
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
spot_to_fill: Location | None = None
|
||||
|
||||
# going through locations in the same order as the provided `locations` argument
|
||||
for i, location in enumerate(locations):
|
||||
@@ -336,7 +344,7 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
if unplaced_items and locations:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
if move_unplaceable_to_start_inventory:
|
||||
last_batch = []
|
||||
last_batch: list[Item] = []
|
||||
for item in unplaced_items:
|
||||
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
||||
multiworld.push_precollected(item)
|
||||
@@ -355,8 +363,8 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
|
||||
|
||||
def fast_fill(multiworld: MultiWorld,
|
||||
item_pool: typing.List[Item],
|
||||
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
|
||||
item_pool: list[Item],
|
||||
fill_locations: list[Location]) -> tuple[list[Item], list[Location]]:
|
||||
placing = min(len(item_pool), len(fill_locations))
|
||||
for item, location in zip(item_pool, fill_locations):
|
||||
multiworld.push_item(location, item, False)
|
||||
@@ -370,11 +378,14 @@ def accessibility_corrections(multiworld: MultiWorld,
|
||||
if pool is None:
|
||||
pool = []
|
||||
maximum_exploration_state = sweep_from_pool(state, pool)
|
||||
minimal_players = {player for player in multiworld.player_ids if
|
||||
multiworld.worlds[player].options.accessibility == "minimal"}
|
||||
unreachable_locations = [location for location in multiworld.get_locations() if
|
||||
location.player in minimal_players and
|
||||
not location.can_reach(maximum_exploration_state)]
|
||||
minimal_players = {player
|
||||
for player in multiworld.player_ids
|
||||
if multiworld.worlds[player].options.accessibility == "minimal"}
|
||||
unreachable_locations = [
|
||||
location
|
||||
for location in multiworld.get_locations()
|
||||
if location.player in minimal_players and not location.can_reach(maximum_exploration_state)
|
||||
]
|
||||
for location in unreachable_locations:
|
||||
if (location.item is not None and location.item.advancement and location.address is not None and not
|
||||
location.locked and location.item.player not in minimal_players):
|
||||
@@ -389,33 +400,36 @@ def accessibility_corrections(multiworld: MultiWorld,
|
||||
fill_restrictive(multiworld, state, locations, pool, name="Accessibility Corrections")
|
||||
|
||||
|
||||
def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations):
|
||||
def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations: Iterable[Location]) -> None:
|
||||
maximum_exploration_state = sweep_from_pool(state)
|
||||
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
|
||||
if unreachable_locations:
|
||||
def forbid_important_item_rule(item: Item):
|
||||
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != "minimal")
|
||||
def forbid_important_item_rule(item: Item) -> bool:
|
||||
return not ((item.classification & 0b0011) and
|
||||
multiworld.worlds[item.player].options.accessibility != "minimal")
|
||||
|
||||
for location in unreachable_locations:
|
||||
add_item_rule(location, forbid_important_item_rule)
|
||||
|
||||
|
||||
def distribute_early_items(multiworld: MultiWorld,
|
||||
fill_locations: typing.List[Location],
|
||||
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
|
||||
fill_locations: list[Location],
|
||||
itempool: list[Item]) -> tuple[list[Location], list[Item]]:
|
||||
""" returns new fill_locations and itempool """
|
||||
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
|
||||
early_items_count: dict[tuple[str, int], list[int]] = {}
|
||||
for player in multiworld.player_ids:
|
||||
items = itertools.chain(multiworld.early_items[player], multiworld.local_early_items[player])
|
||||
for item in items:
|
||||
early_items_count[item, player] = [multiworld.early_items[player].get(item, 0),
|
||||
multiworld.local_early_items[player].get(item, 0)]
|
||||
if early_items_count:
|
||||
early_locations: typing.List[Location] = []
|
||||
early_priority_locations: typing.List[Location] = []
|
||||
loc_indexes_to_remove: typing.Set[int] = set()
|
||||
early_locations: list[Location] = []
|
||||
early_priority_locations: list[Location] = []
|
||||
loc_indexes_to_remove: set[int] = set()
|
||||
base_state = multiworld.state.copy()
|
||||
base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
||||
base_state.sweep_for_advancements(locations=(loc
|
||||
for loc in multiworld.get_filled_locations()
|
||||
if loc.address is None))
|
||||
for i, loc in enumerate(fill_locations):
|
||||
if loc.can_reach(base_state):
|
||||
if loc.progress_type == LocationProgressType.PRIORITY:
|
||||
@@ -425,11 +439,11 @@ def distribute_early_items(multiworld: MultiWorld,
|
||||
loc_indexes_to_remove.add(i)
|
||||
fill_locations = [loc for i, loc in enumerate(fill_locations) if i not in loc_indexes_to_remove]
|
||||
|
||||
early_prog_items: typing.List[Item] = []
|
||||
early_rest_items: typing.List[Item] = []
|
||||
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
|
||||
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
|
||||
item_indexes_to_remove: typing.Set[int] = set()
|
||||
early_prog_items: list[Item] = []
|
||||
early_rest_items: list[Item] = []
|
||||
early_local_prog_items: dict[int, list[Item]] = {player: [] for player in multiworld.player_ids}
|
||||
early_local_rest_items: dict[int, list[Item]] = {player: [] for player in multiworld.player_ids}
|
||||
item_indexes_to_remove: set[int] = set()
|
||||
for i, item in enumerate(itempool):
|
||||
if (item.name, item.player) in early_items_count:
|
||||
if item.advancement:
|
||||
@@ -487,7 +501,7 @@ def distribute_early_items(multiworld: MultiWorld,
|
||||
|
||||
|
||||
def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None:
|
||||
panic_method: Literal["swap", "raise", "start_inventory"] = "swap") -> None:
|
||||
assert all(item.location is None for item in multiworld.itempool), (
|
||||
"At the start of distribute_items_restrictive, "
|
||||
"there are items in the multiworld itempool that are already placed on locations:\n"
|
||||
@@ -502,9 +516,9 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
|
||||
fill_locations, itempool = distribute_early_items(multiworld, fill_locations, itempool)
|
||||
|
||||
progitempool: typing.List[Item] = []
|
||||
usefulitempool: typing.List[Item] = []
|
||||
filleritempool: typing.List[Item] = []
|
||||
progitempool: list[Item] = []
|
||||
usefulitempool: list[Item] = []
|
||||
filleritempool: list[Item] = []
|
||||
|
||||
for item in itempool:
|
||||
if item.advancement:
|
||||
@@ -516,7 +530,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
|
||||
call_all(multiworld, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
|
||||
|
||||
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
|
||||
locations: dict[LocationProgressType, list[Location]] = {
|
||||
loc_type: [] for loc_type in LocationProgressType}
|
||||
|
||||
for loc in fill_locations:
|
||||
@@ -527,17 +541,17 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
excludedlocations = locations[LocationProgressType.EXCLUDED]
|
||||
|
||||
# can't lock due to accessibility corrections touching things, so we remember which ones got placed and lock later
|
||||
lock_later = []
|
||||
lock_later: list[Location] = []
|
||||
|
||||
def mark_for_locking(location: Location):
|
||||
def mark_for_locking(location: Location) -> None:
|
||||
nonlocal lock_later
|
||||
lock_later.append(location)
|
||||
|
||||
single_player = multiworld.players == 1 and not multiworld.groups
|
||||
|
||||
if prioritylocations:
|
||||
regular_progression = []
|
||||
deprioritized_progression = []
|
||||
regular_progression: list[Item] = []
|
||||
deprioritized_progression: list[Item] = []
|
||||
for item in progitempool:
|
||||
if item.deprioritized:
|
||||
deprioritized_progression.append(item)
|
||||
@@ -623,7 +637,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 +649,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
|
||||
@@ -654,18 +668,19 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
for player in multiworld.player_ids:
|
||||
if more_locations[player]:
|
||||
logging.error(
|
||||
f"Player {multiworld.get_player_name(player)} had {more_locations[player]} more locations than items.")
|
||||
f"Player {multiworld.get_player_name(player)} had "
|
||||
f"{more_locations[player]} more locations than items.")
|
||||
elif more_items[player]:
|
||||
logging.warning(
|
||||
f"Player {multiworld.get_player_name(player)} had {more_items[player]} more items than locations.")
|
||||
if unfilled:
|
||||
raise FillError(
|
||||
f"Unable to fill all locations.\n" +
|
||||
"Unable to fill all locations.\n"
|
||||
f"Unfilled locations({len(unfilled)}): {unfilled}"
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Unable to place all items.\n" +
|
||||
"Unable to place all items.\n"
|
||||
f"Unplaced items({len(unplaced)}): {unplaced}"
|
||||
)
|
||||
|
||||
@@ -715,7 +730,7 @@ def flood_items(multiworld: MultiWorld) -> None:
|
||||
if candidate_item_to_place is not None:
|
||||
item_to_place = candidate_item_to_place
|
||||
else:
|
||||
raise FillError('No more progress items left to place.', multiworld=multiworld)
|
||||
raise FillError("No more progress items left to place.", multiworld=multiworld)
|
||||
|
||||
# find item to replace with progress item
|
||||
location_list = multiworld.get_reachable_locations()
|
||||
@@ -738,7 +753,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
# Define a threshold value based on the player with the most available locations.
|
||||
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
|
||||
# which gives more locations available by this sphere.
|
||||
balanceable_players: typing.Dict[int, float] = {
|
||||
balanceable_players: dict[int, float] = {
|
||||
player: multiworld.worlds[player].options.progression_balancing / 100
|
||||
for player in multiworld.player_ids
|
||||
if multiworld.worlds[player].options.progression_balancing > 0
|
||||
@@ -749,15 +764,15 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
logging.info(f"Balancing multiworld progression for {len(balanceable_players)} Players.")
|
||||
logging.debug(balanceable_players)
|
||||
state: CollectionState = CollectionState(multiworld)
|
||||
checked_locations: typing.Set[Location] = set()
|
||||
unchecked_locations: typing.Set[Location] = set(multiworld.get_locations())
|
||||
checked_locations: set[Location] = set()
|
||||
unchecked_locations: set[Location] = set(multiworld.get_locations())
|
||||
|
||||
total_locations_count: typing.Counter[int] = Counter(
|
||||
total_locations_count: Counter[int] = Counter(
|
||||
location.player
|
||||
for location in multiworld.get_locations()
|
||||
if not location.locked
|
||||
)
|
||||
reachable_locations_count: typing.Dict[int, int] = {
|
||||
reachable_locations_count: dict[int, int] = {
|
||||
player: 0
|
||||
for player in multiworld.player_ids
|
||||
if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0
|
||||
@@ -771,7 +786,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
moved_item_count: int = 0
|
||||
|
||||
def get_sphere_locations(sphere_state: CollectionState,
|
||||
locations: typing.Set[Location]) -> typing.Set[Location]:
|
||||
locations: set[Location]) -> set[Location]:
|
||||
return {loc for loc in locations if sphere_state.can_reach(loc)}
|
||||
|
||||
def item_percentage(player: int, num: int) -> float:
|
||||
@@ -819,7 +834,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
balancing_unchecked_locations = unchecked_locations.copy()
|
||||
balancing_reachables = reachable_locations_count.copy()
|
||||
balancing_sphere = sphere_locations.copy()
|
||||
candidate_items: typing.Dict[int, typing.Set[Location]] = collections.defaultdict(set)
|
||||
candidate_items: dict[int, set[Location]] = defaultdict(set)
|
||||
while True:
|
||||
# Check locations in the current sphere and gather progression items to swap earlier
|
||||
for location in balancing_sphere:
|
||||
@@ -846,11 +861,11 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
elif not balancing_sphere:
|
||||
raise RuntimeError("Not all required items reachable. Something went terribly wrong here.")
|
||||
# Gather a set of locations which we can swap items into
|
||||
unlocked_locations: typing.Dict[int, typing.Set[Location]] = collections.defaultdict(set)
|
||||
unlocked_locations: dict[int, set[Location]] = defaultdict(set)
|
||||
for l in unchecked_locations:
|
||||
if l not in balancing_unchecked_locations:
|
||||
unlocked_locations[l.player].add(l)
|
||||
items_to_replace: typing.List[Location] = []
|
||||
items_to_replace: list[Location] = []
|
||||
for player in balancing_players:
|
||||
locations_to_test = unlocked_locations[player]
|
||||
items_to_test = list(candidate_items[player])
|
||||
@@ -956,6 +971,12 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
for block in multiworld.worlds[player].options.plando_items:
|
||||
new_block: PlandoItemBlock = PlandoItemBlock(player, block.from_pool, block.force)
|
||||
target_world = block.world
|
||||
# TODO: This doesn't handle the plando API correctly
|
||||
# It says `world` can be other containers other than list,
|
||||
# but this is checking specifically for list.
|
||||
# (But it's not simple to just change the check to Iterable,
|
||||
# because that will catch the str case too early.)
|
||||
# (And it will be broken without failing unit tests. So also TODO: unit test this.)
|
||||
if target_world is False or multiworld.players == 1: # target own world
|
||||
worlds: set[int] = {player}
|
||||
elif target_world is True: # target any worlds besides own
|
||||
@@ -985,7 +1006,7 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
worlds = {world_name_lookup[target_world]}
|
||||
new_block.worlds = worlds
|
||||
|
||||
items: list[str] | dict[str, typing.Any] = block.items
|
||||
items = block.items
|
||||
if isinstance(items, dict):
|
||||
item_list: list[str] = []
|
||||
for key, value in items.items():
|
||||
@@ -1046,8 +1067,8 @@ def resolve_early_locations_for_planned(multiworld: MultiWorld):
|
||||
swept_state = multiworld.state.copy()
|
||||
swept_state.sweep_for_advancements()
|
||||
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
||||
early_locations: dict[int, list[Location]] = collections.defaultdict(list)
|
||||
non_early_locations: dict[int, list[Location]] = collections.defaultdict(list)
|
||||
early_locations: dict[int, list[Location]] = defaultdict(list)
|
||||
non_early_locations: dict[int, list[Location]] = defaultdict(list)
|
||||
for loc in multiworld.get_unfilled_locations():
|
||||
if loc in reachable:
|
||||
early_locations[loc.player].append(loc)
|
||||
@@ -1055,7 +1076,7 @@ def resolve_early_locations_for_planned(multiworld: MultiWorld):
|
||||
non_early_locations[loc.player].append(loc)
|
||||
|
||||
for player in multiworld.plando_item_blocks:
|
||||
removed = []
|
||||
removed: list[PlandoItemBlock] = []
|
||||
for block in multiworld.plando_item_blocks[player]:
|
||||
locations = block.locations
|
||||
resolved_locations = block.resolved_locations
|
||||
@@ -1079,8 +1100,7 @@ def resolve_early_locations_for_planned(multiworld: MultiWorld):
|
||||
block.count["max"] = len(block.resolved_locations)
|
||||
if block.count["min"] > len(block.resolved_locations):
|
||||
block.count["min"] = len(block.resolved_locations)
|
||||
block.count["target"] = multiworld.random.randint(block.count["min"],
|
||||
block.count["max"])
|
||||
block.count["target"] = multiworld.random.randint(block.count["min"], block.count["max"])
|
||||
|
||||
if not block.count["target"]:
|
||||
removed.append(block)
|
||||
@@ -1118,7 +1138,7 @@ def distribute_planned_blocks(multiworld: MultiWorld, plando_blocks: list[Plando
|
||||
maxcount = placement.count["target"]
|
||||
from_pool = placement.from_pool
|
||||
|
||||
item_candidates = []
|
||||
item_candidates: list[Item] = []
|
||||
if from_pool:
|
||||
instances = [item for item in multiworld.itempool if item.player == player and item.name in items]
|
||||
for item in multiworld.random.sample(items, maxcount):
|
||||
@@ -1144,8 +1164,9 @@ def distribute_planned_blocks(multiworld: MultiWorld, plando_blocks: list[Plando
|
||||
continue
|
||||
else:
|
||||
is_real = item_candidates[0].code is not None
|
||||
candidates = [candidate for candidate in locations if candidate.item is None
|
||||
and bool(candidate.address) == is_real]
|
||||
candidates = [candidate
|
||||
for candidate in locations
|
||||
if candidate.item is None and bool(candidate.address) == is_real]
|
||||
multiworld.random.shuffle(candidates)
|
||||
allstate = multiworld.get_all_state(False)
|
||||
mincount = placement.count["min"]
|
||||
|
||||
+13
-2
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
+4
-3
@@ -38,7 +38,8 @@ class RequirementsSet(set):
|
||||
|
||||
|
||||
local_dir = os.path.dirname(__file__)
|
||||
requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),))
|
||||
core_constraints = os.path.join(local_dir, 'requirements.txt')
|
||||
requirements_files = RequirementsSet((core_constraints,))
|
||||
|
||||
if not update_ran:
|
||||
for entry in os.scandir(os.path.join(local_dir, "worlds")):
|
||||
@@ -69,7 +70,7 @@ def confirm(msg: str):
|
||||
def update_command():
|
||||
check_pip()
|
||||
for file in requirements_files:
|
||||
subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--upgrade"])
|
||||
subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--constraint", core_constraints])
|
||||
|
||||
|
||||
def install_pkg_resources(yes=False):
|
||||
@@ -79,7 +80,7 @@ def install_pkg_resources(yes=False):
|
||||
check_pip()
|
||||
if not yes:
|
||||
confirm("pkg_resources not found, press enter to install it")
|
||||
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools>=75,<81"])
|
||||
subprocess.call([sys.executable, "-m", "pip", "install", "setuptools>=75,<81"])
|
||||
|
||||
|
||||
def update(yes: bool = False, force: bool = False) -> None:
|
||||
|
||||
+2
-2
@@ -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')
|
||||
|
||||
@@ -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.")
|
||||
|
||||
+56
-30
@@ -212,6 +212,13 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
else:
|
||||
return cls.name_lookup[value]
|
||||
|
||||
def __eq__(self, other: typing.Any) -> bool:
|
||||
if isinstance(other, self.__class__):
|
||||
return self.value == other.value
|
||||
if isinstance(other, Option):
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
return self.value == other
|
||||
|
||||
def __int__(self) -> T:
|
||||
return self.value
|
||||
|
||||
@@ -930,13 +937,34 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
|
||||
class OptionCounter(OptionDict):
|
||||
min: int | None = None
|
||||
max: int | None = None
|
||||
cull_zeroes: bool = False
|
||||
|
||||
def __init__(self, value: dict[str, int]) -> None:
|
||||
super(OptionCounter, self).__init__(collections.Counter(value))
|
||||
cleaned_dict = {}
|
||||
|
||||
invalid_value_errors = []
|
||||
for key, value in value.items():
|
||||
if not isinstance(value, (int, float)) or int(value) != value:
|
||||
invalid_value_errors += [f"Invalid value {value} for key {key}, must be an integer."]
|
||||
continue
|
||||
|
||||
if self.cull_zeroes and value == 0:
|
||||
continue
|
||||
|
||||
cleaned_dict[key] = int(value)
|
||||
|
||||
if invalid_value_errors:
|
||||
type_errors = [f"For option {self.__class__.__name__}:"] + invalid_value_errors
|
||||
raise TypeError("\n".join(invalid_value_errors))
|
||||
|
||||
super(OptionCounter, self).__init__(collections.Counter(cleaned_dict))
|
||||
|
||||
def verify(self, world: type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||
super(OptionCounter, self).verify(world, player_name, plando_options)
|
||||
|
||||
self.verify_values()
|
||||
|
||||
def verify_values(self):
|
||||
range_errors = []
|
||||
|
||||
if self.max is not None:
|
||||
@@ -959,13 +987,8 @@ class OptionCounter(OptionDict):
|
||||
class ItemDict(OptionCounter):
|
||||
verify_item_name = True
|
||||
|
||||
min = 0
|
||||
|
||||
def __init__(self, value: dict[str, int]) -> None:
|
||||
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
|
||||
value = {item_name: amount for item_name, amount in value.items() if amount != 0}
|
||||
|
||||
super(ItemDict, self).__init__(value)
|
||||
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
|
||||
cull_zeroes = True
|
||||
|
||||
|
||||
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
@@ -1446,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
|
||||
@@ -1454,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.
|
||||
"""
|
||||
@@ -1833,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:
|
||||
|
||||
@@ -85,6 +85,7 @@ Currently, the following games are supported:
|
||||
* Satisfactory
|
||||
* EarthBound
|
||||
* Mega Man 3
|
||||
* Gauntlet Legends
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -18,11 +18,12 @@ import logging
|
||||
import warnings
|
||||
|
||||
from argparse import Namespace
|
||||
from collections.abc import Collection, Iterable
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from settings import Settings, get_settings
|
||||
from time import sleep
|
||||
from typing import BinaryIO, Coroutine, Mapping, Optional, Set, Dict, Any, Union, TypeGuard
|
||||
from typing import BinaryIO, Coroutine, Generic, Mapping, Optional, Set, Dict, Any, TypeVar, Union, TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
from pathspec import PathSpec, GitIgnoreSpec
|
||||
from typing_extensions import deprecated
|
||||
@@ -52,7 +53,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")
|
||||
@@ -450,13 +451,10 @@ safe_builtins = frozenset((
|
||||
|
||||
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
generic_properties_module: Optional[object]
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||
self.options_module = importlib.import_module("Options")
|
||||
self.net_utils_module = importlib.import_module("NetUtils")
|
||||
self.generic_properties_module = None
|
||||
|
||||
def find_class(self, module: str, name: str) -> type:
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
@@ -470,10 +468,6 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
"SlotType", "NetworkSlot", "HintStatus"}:
|
||||
return getattr(self.net_utils_module, name)
|
||||
# Options and Plando are unpickled by WebHost -> Generate
|
||||
if module == "worlds.generic" and name == "PlandoItem":
|
||||
if not self.generic_properties_module:
|
||||
self.generic_properties_module = importlib.import_module("worlds.generic")
|
||||
return getattr(self.generic_properties_module, name)
|
||||
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
|
||||
if module.lower().endswith("options"):
|
||||
if module == "Options":
|
||||
@@ -1277,8 +1271,11 @@ def visualize_regions(
|
||||
f.write("\n".join(uml))
|
||||
|
||||
|
||||
class RepeatableChain:
|
||||
def __init__(self, iterable: typing.Iterable):
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
|
||||
|
||||
class RepeatableChain(Generic[_T_co]):
|
||||
def __init__(self, iterable: Iterable[Collection[_T_co]]):
|
||||
self.iterable = iterable
|
||||
|
||||
def __iter__(self):
|
||||
@@ -1290,6 +1287,9 @@ class RepeatableChain:
|
||||
def __len__(self):
|
||||
return sum(len(iterable) for iterable in self.iterable)
|
||||
|
||||
def __contains__(self, o: object) -> bool:
|
||||
return any(o in sub_iterable for sub_iterable in self.iterable)
|
||||
|
||||
|
||||
def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]]:
|
||||
""" `str` is `Iterable`, but that's not what we want """
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -71,7 +74,9 @@ CLI(app)
|
||||
|
||||
|
||||
def to_python(value: str) -> uuid.UUID:
|
||||
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=='))
|
||||
if "=" in value or any(c.isspace() for c in value):
|
||||
raise ValueError("Invalid UUID format")
|
||||
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=' * (-len(value) % 4)))
|
||||
|
||||
|
||||
def to_url(value: uuid.UUID) -> str:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -80,6 +80,9 @@
|
||||
# Final Fantasy Mystic Quest
|
||||
/worlds/ffmq/ @Alchav @wildham0
|
||||
|
||||
# Gauntlet Legends
|
||||
/worlds/gl/ @jamesbrq
|
||||
|
||||
# Heretic
|
||||
/worlds/heretic/ @Daivuk @KScl
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ Unless you configured PyCharm to use pytest as a test runner, you may get import
|
||||
edit the run configuration, and set the working directory to the Archipelago directory which contains all the project files.
|
||||
|
||||
If you only want to run your world's defined tests, repeat the steps for the test directory within your world.
|
||||
Your working directory should be the directory of your world in the worlds directory and the script should be the
|
||||
Your working directory should be the root Archipelago directory and the script should be the
|
||||
tests folder within your world.
|
||||
|
||||
You can also find the 'Archipelago Unittests' as an option in the dropdown at the top of the window
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+7
-3
@@ -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]
|
||||
@@ -208,11 +206,17 @@ Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archi
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
|
||||
Root: HKCR; Subkey: ".apmm3"; ValueData: "{#MyAppName}mm3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm3patch"; ValueData: "Archipelago Mega Man 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm3patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apgl"; ValueData: "{#MyAppName}glpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}glpatch"; ValueData: "Archipelago Gauntlet Legends Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}glpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}glpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Verify that NetUtils' enums work correctly with all supported Python versions."""
|
||||
|
||||
import pickle
|
||||
import unittest
|
||||
from enum import Enum
|
||||
from typing import Type
|
||||
|
||||
from NetUtils import ClientStatus, HintStatus, SlotType
|
||||
from Utils import restricted_loads
|
||||
|
||||
|
||||
class Base:
|
||||
class DataEnumTest(unittest.TestCase):
|
||||
type: Type[Enum]
|
||||
value: Enum
|
||||
|
||||
def test_unpickle(self) -> None:
|
||||
"""Tests that enums used in multidata or multisave can be pickled and unpickled."""
|
||||
pickled = pickle.dumps(self.value)
|
||||
unpickled = restricted_loads(pickled)
|
||||
self.assertEqual(unpickled, self.value)
|
||||
self.assertIsInstance(unpickled, self.type)
|
||||
|
||||
|
||||
class HintStatusTest(Base.DataEnumTest):
|
||||
type = HintStatus
|
||||
value = HintStatus.HINT_AVOID
|
||||
|
||||
|
||||
class ClientStatusTest(Base.DataEnumTest):
|
||||
type = ClientStatus
|
||||
value = ClientStatus.CLIENT_GOAL
|
||||
|
||||
|
||||
class SlotTypeTest(Base.DataEnumTest):
|
||||
type = SlotType
|
||||
value = SlotType.player
|
||||
@@ -1,6 +1,8 @@
|
||||
import unittest
|
||||
|
||||
from Options import Choice, DefaultOnToggle, Toggle
|
||||
from collections import Counter
|
||||
|
||||
from Options import Choice, DefaultOnToggle, Toggle, OptionDict, OptionError, OptionSet, OptionList, OptionCounter
|
||||
|
||||
|
||||
class TestNumericOptions(unittest.TestCase):
|
||||
@@ -74,3 +76,97 @@ class TestNumericOptions(unittest.TestCase):
|
||||
self.assertTrue(toggle_string)
|
||||
self.assertTrue(toggle_int)
|
||||
self.assertTrue(toggle_alias)
|
||||
|
||||
|
||||
class TestContainerOptions(unittest.TestCase):
|
||||
def test_option_dict(self):
|
||||
class TestOptionDict(OptionDict):
|
||||
valid_keys = frozenset({"A", "B", "C"})
|
||||
|
||||
unknown_key_init_dict = {"D": "Foo"}
|
||||
test_option_dict = TestOptionDict(unknown_key_init_dict)
|
||||
self.assertRaises(OptionError, test_option_dict.verify_keys)
|
||||
|
||||
init_dict = {"A": "foo", "B": "bar"}
|
||||
test_option_dict = TestOptionDict(init_dict)
|
||||
|
||||
self.assertEqual(test_option_dict, init_dict) # Implicit value comparison
|
||||
self.assertEqual(test_option_dict["A"], "foo")
|
||||
self.assertIn("B", test_option_dict)
|
||||
self.assertNotIn("C", test_option_dict)
|
||||
self.assertRaises(KeyError, lambda: test_option_dict["C"])
|
||||
|
||||
def test_option_set(self):
|
||||
class TestOptionSet(OptionSet):
|
||||
valid_keys = frozenset({"A", "B", "C"})
|
||||
|
||||
unknown_key_init_set = {"D"}
|
||||
test_option_set = TestOptionSet(unknown_key_init_set)
|
||||
self.assertRaises(OptionError, test_option_set.verify_keys)
|
||||
|
||||
init_set = {"A", "B"}
|
||||
test_option_set = TestOptionSet(init_set)
|
||||
|
||||
self.assertEqual(test_option_set, init_set) # Implicit value comparison
|
||||
self.assertIn("B", test_option_set)
|
||||
self.assertNotIn("C", test_option_set)
|
||||
|
||||
def test_option_list(self):
|
||||
class TestOptionList(OptionList):
|
||||
valid_keys = frozenset({"A", "B", "C"})
|
||||
|
||||
unknown_key_init_list = ["D"]
|
||||
test_option_list = TestOptionList(unknown_key_init_list)
|
||||
self.assertRaises(OptionError, test_option_list.verify_keys)
|
||||
|
||||
init_list = ["A", "B"]
|
||||
test_option_list = TestOptionList(init_list)
|
||||
|
||||
self.assertEqual(test_option_list, init_list)
|
||||
self.assertIn("B", test_option_list)
|
||||
self.assertNotIn("C", test_option_list)
|
||||
|
||||
|
||||
def test_option_counter(self):
|
||||
class TestOptionCounter(OptionCounter):
|
||||
valid_keys = frozenset({"A", "B", "C"})
|
||||
|
||||
max = 10
|
||||
min = 0
|
||||
|
||||
unknown_key_init_dict = {"D": 5}
|
||||
test_option_counter = TestOptionCounter(unknown_key_init_dict)
|
||||
self.assertRaises(OptionError, test_option_counter.verify_keys)
|
||||
|
||||
wrong_value_type_init_dict = {"A": "B"}
|
||||
self.assertRaises(TypeError, TestOptionCounter, wrong_value_type_init_dict)
|
||||
|
||||
violates_max_init_dict = {"A": 5, "B": 11}
|
||||
test_option_counter = TestOptionCounter(violates_max_init_dict)
|
||||
self.assertRaises(OptionError, test_option_counter.verify_values)
|
||||
|
||||
violates_min_init_dict = {"A": -1, "B": 5}
|
||||
test_option_counter = TestOptionCounter(violates_min_init_dict)
|
||||
self.assertRaises(OptionError, test_option_counter.verify_values)
|
||||
|
||||
init_dict = {"A": 0, "B": 10}
|
||||
test_option_counter = TestOptionCounter(init_dict)
|
||||
self.assertEqual(test_option_counter, Counter(init_dict))
|
||||
self.assertIn("A", test_option_counter)
|
||||
self.assertNotIn("C", test_option_counter)
|
||||
self.assertEqual(test_option_counter["A"], 0)
|
||||
self.assertEqual(test_option_counter["B"], 10)
|
||||
self.assertEqual(test_option_counter["C"], 0)
|
||||
|
||||
def test_culling_option_counter(self):
|
||||
class TestCullingCounter(OptionCounter):
|
||||
valid_keys = frozenset({"A", "B", "C"})
|
||||
cull_zeroes = True
|
||||
|
||||
init_dict = {"A": 0, "B": 10}
|
||||
test_option_counter = TestCullingCounter(init_dict)
|
||||
self.assertNotIn("A", test_option_counter)
|
||||
self.assertIn("B", test_option_counter)
|
||||
self.assertNotIn("C", test_option_counter)
|
||||
self.assertEqual(test_option_counter["A"], 0) # It's still a Counter! cull_zeroes is about "in" checks.
|
||||
self.assertEqual(test_option_counter, Counter({"B": 10}))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -0,0 +1,76 @@
|
||||
import math
|
||||
from typing import Any, Callable
|
||||
from typing_extensions import override
|
||||
from uuid import uuid4
|
||||
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
from . import TestBase
|
||||
|
||||
|
||||
class TestSUUID(TestBase):
|
||||
converter: BaseConverter
|
||||
filter: Callable[[Any], str]
|
||||
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
from werkzeug.routing import Map
|
||||
|
||||
super().setUp()
|
||||
self.converter = self.app.url_map.converters["suuid"](Map())
|
||||
self.filter = self.app.jinja_env.filters["suuid"] # type: ignore # defines how we use it, not what it can be
|
||||
|
||||
def test_is_reversible(self) -> None:
|
||||
u = uuid4()
|
||||
self.assertEqual(u, self.converter.to_python(self.converter.to_url(u)))
|
||||
s = "A" * 22 # uuid with all zeros
|
||||
self.assertEqual(s, self.converter.to_url(self.converter.to_python(s)))
|
||||
|
||||
def test_uuid_length(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
self.converter.to_python("AAAA")
|
||||
|
||||
def test_padding(self) -> None:
|
||||
self.converter.to_python("A" * 22) # check that the correct value works
|
||||
with self.assertRaises(ValueError):
|
||||
self.converter.to_python("A" * 22 + "==") # converter should not allow padding
|
||||
|
||||
def test_empty(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
self.converter.to_python("")
|
||||
|
||||
def test_stray_equal_signs(self) -> None:
|
||||
self.converter.to_python("A" * 22) # check that the correct value works
|
||||
with self.assertRaises(ValueError):
|
||||
self.converter.to_python("A" * 22 + "==" + "AA") # the "==AA" should not be ignored, but error out
|
||||
with self.assertRaises(ValueError):
|
||||
self.converter.to_python("A" * 20 + "==" + "AA") # the final "A"s should not be appended to the first "A"s
|
||||
|
||||
def test_stray_whitespace(self) -> None:
|
||||
s = "A" * 22
|
||||
self.converter.to_python(s) # check that the correct value works
|
||||
for char in " \t\r\n\v":
|
||||
for pos in (0, 11, 22):
|
||||
with self.subTest(char=char, pos=pos):
|
||||
s_with_whitespace = s[0:pos] + char * 4 + s[pos:] # insert 4 to make padding correct
|
||||
# check that the constructed s_with_whitespace is correct
|
||||
self.assertEqual(len(s_with_whitespace), len(s) + 4)
|
||||
self.assertEqual(s_with_whitespace[pos], char)
|
||||
# s_with_whitespace should be invalid as SUUID
|
||||
with self.assertRaises(ValueError):
|
||||
self.converter.to_python(s_with_whitespace)
|
||||
|
||||
def test_filter_returns_valid_string(self) -> None:
|
||||
u = uuid4()
|
||||
s = self.filter(u)
|
||||
self.assertIsInstance(s, str)
|
||||
self.assertNotIn("=", s)
|
||||
self.assertEqual(len(s), math.ceil(len(u.bytes) * 4 / 3))
|
||||
|
||||
def test_filter_is_same_as_converter(self) -> None:
|
||||
u = uuid4()
|
||||
self.assertEqual(self.filter(u), self.converter.to_url(u))
|
||||
|
||||
def test_filter_bad_type(self) -> None:
|
||||
with self.assertRaises(Exception): # currently the type is not checked directly, so any exception is valid
|
||||
self.filter(None)
|
||||
+5
-1
@@ -353,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
|
||||
@@ -512,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
|
||||
|
||||
+16
-5
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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])
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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)),
|
||||
)
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"game": "APQuest",
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "1.0.1",
|
||||
"minimum_ap_version": "0.6.7",
|
||||
"world_version": "2.0.0",
|
||||
"authors": ["NewSoupVi"]
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ class APQuestContext(CommonContext):
|
||||
if self.ap_quest_game is None:
|
||||
raise RuntimeError("Tried to render before self.ap_quest_game was initialized.")
|
||||
|
||||
self.ui.render(self.ap_quest_game, self.player_sprite)
|
||||
self.ui.render(self.ap_quest_game, self.player_sprite, self.hard_mode)
|
||||
self.handle_game_events()
|
||||
|
||||
def location_checked_side_effects(self, location: int) -> None:
|
||||
|
||||
@@ -88,23 +88,23 @@ class APQuestManager(GameManager):
|
||||
self.game_view.force_focus()
|
||||
self.sound_manager.game_started = True
|
||||
|
||||
def render(self, game: Game, player_sprite: PlayerSprite) -> None:
|
||||
def render(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None:
|
||||
self.setup_game_grid_if_not_setup(game)
|
||||
|
||||
# This calls game.render(), which needs to happen to update the state of math traps
|
||||
self.render_gameboard(game, player_sprite)
|
||||
self.render_gameboard(game, player_sprite, hard_mode)
|
||||
# Only now can we check whether a math problem is active
|
||||
self.render_background_game_grid(game.gameboard.size, game.active_math_problem is None)
|
||||
self.sound_manager.math_trap_active = game.active_math_problem is not None
|
||||
|
||||
self.render_item_column(game)
|
||||
|
||||
def render_gameboard(self, game: Game, player_sprite: PlayerSprite) -> None:
|
||||
def render_gameboard(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None:
|
||||
rendered_gameboard = game.render()
|
||||
|
||||
for gameboard_row, image_row in zip(rendered_gameboard, self.top_image_grid, strict=False):
|
||||
for graphic, image in zip(gameboard_row, image_row[:11], strict=False):
|
||||
texture = get_texture(graphic, player_sprite)
|
||||
texture = get_texture(graphic, player_sprite, hard_mode)
|
||||
|
||||
if texture is None:
|
||||
image.opacity = 0
|
||||
|
||||
@@ -29,6 +29,7 @@ class RelatedTexture(NamedTuple):
|
||||
|
||||
|
||||
IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
|
||||
# Inanimates
|
||||
Graphic.WALL: RelatedTexture("inanimates.png", 16, 32, 16, 16),
|
||||
Graphic.BREAKABLE_BLOCK: RelatedTexture("inanimates.png", 32, 32, 16, 16),
|
||||
Graphic.CHEST: RelatedTexture("inanimates.png", 0, 16, 16, 16),
|
||||
@@ -37,29 +38,25 @@ IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
|
||||
Graphic.BUTTON_NOT_ACTIVATED: RelatedTexture("inanimates.png", 0, 0, 16, 16),
|
||||
Graphic.BUTTON_ACTIVATED: RelatedTexture("inanimates.png", 16, 0, 16, 16),
|
||||
Graphic.BUTTON_DOOR: RelatedTexture("inanimates.png", 32, 0, 16, 16),
|
||||
|
||||
# Enemies
|
||||
Graphic.NORMAL_ENEMY_1_HEALTH: RelatedTexture("normal_enemy.png", 0, 0, 16, 16),
|
||||
Graphic.NORMAL_ENEMY_2_HEALTH: RelatedTexture("normal_enemy.png", 16, 0, 16, 16),
|
||||
|
||||
Graphic.BOSS_5_HEALTH: RelatedTexture("boss.png", 16, 16, 16, 16),
|
||||
Graphic.BOSS_4_HEALTH: RelatedTexture("boss.png", 0, 16, 16, 16),
|
||||
Graphic.BOSS_3_HEALTH: RelatedTexture("boss.png", 32, 32, 16, 16),
|
||||
Graphic.BOSS_2_HEALTH: RelatedTexture("boss.png", 16, 32, 16, 16),
|
||||
Graphic.BOSS_1_HEALTH: RelatedTexture("boss.png", 0, 32, 16, 16),
|
||||
|
||||
# Items
|
||||
Graphic.EMPTY_HEART: RelatedTexture("hearts.png", 0, 0, 16, 16),
|
||||
Graphic.HEART: RelatedTexture("hearts.png", 16, 0, 16, 16),
|
||||
Graphic.HALF_HEART: RelatedTexture("hearts.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.REMOTE_ITEM: RelatedTexture("items.png", 0, 16, 16, 16),
|
||||
Graphic.CONFETTI_CANNON: RelatedTexture("items.png", 16, 16, 16, 16),
|
||||
Graphic.HAMMER: RelatedTexture("items.png", 32, 16, 16, 16),
|
||||
Graphic.KEY: RelatedTexture("items.png", 0, 0, 16, 16),
|
||||
Graphic.SHIELD: RelatedTexture("items.png", 16, 0, 16, 16),
|
||||
Graphic.SWORD: RelatedTexture("items.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.ITEMS_TEXT: "items_text.png",
|
||||
|
||||
# Numbers
|
||||
Graphic.ZERO: RelatedTexture("numbers.png", 0, 16, 16, 16),
|
||||
Graphic.ONE: RelatedTexture("numbers.png", 16, 16, 16, 16),
|
||||
Graphic.TWO: RelatedTexture("numbers.png", 32, 16, 16, 16),
|
||||
@@ -70,26 +67,29 @@ IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
|
||||
Graphic.SEVEN: RelatedTexture("numbers.png", 32, 0, 16, 16),
|
||||
Graphic.EIGHT: RelatedTexture("numbers.png", 48, 0, 16, 16),
|
||||
Graphic.NINE: RelatedTexture("numbers.png", 64, 0, 16, 16),
|
||||
|
||||
# Letters
|
||||
Graphic.LETTER_A: RelatedTexture("letters.png", 0, 16, 16, 16),
|
||||
Graphic.LETTER_E: RelatedTexture("letters.png", 16, 16, 16, 16),
|
||||
Graphic.LETTER_H: RelatedTexture("letters.png", 32, 16, 16, 16),
|
||||
Graphic.LETTER_I: RelatedTexture("letters.png", 0, 0, 16, 16),
|
||||
Graphic.LETTER_M: RelatedTexture("letters.png", 16, 0, 16, 16),
|
||||
Graphic.LETTER_T: RelatedTexture("letters.png", 32, 0, 16, 16),
|
||||
|
||||
# Mathematical symbols
|
||||
Graphic.DIVIDE: RelatedTexture("symbols.png", 0, 16, 16, 16),
|
||||
Graphic.EQUALS: RelatedTexture("symbols.png", 16, 16, 16, 16),
|
||||
Graphic.MINUS: RelatedTexture("symbols.png", 32, 16, 16, 16),
|
||||
Graphic.PLUS: RelatedTexture("symbols.png", 0, 0, 16, 16),
|
||||
Graphic.TIMES: RelatedTexture("symbols.png", 16, 0, 16, 16),
|
||||
# Other visual-only elements
|
||||
Graphic.ITEMS_TEXT: "items_text.png",
|
||||
Graphic.NO: RelatedTexture("symbols.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.UNKNOWN: RelatedTexture("symbols.png", 32, 0, 16, 16), # Same as "No"
|
||||
}
|
||||
|
||||
BACKGROUND_TILE = RelatedTexture("inanimates.png", 0, 32, 16, 16)
|
||||
|
||||
EASY_MODE_BOSS_2_HEALTH = RelatedTexture("boss.png", 16, 0, 16, 16)
|
||||
|
||||
|
||||
class PlayerSprite(Enum):
|
||||
HUMAN = 0
|
||||
@@ -160,13 +160,18 @@ def get_texture_by_identifier(texture_identifier: str | RelatedTexture) -> Textu
|
||||
return sub_texture
|
||||
|
||||
|
||||
def get_texture(graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None) -> Texture | None:
|
||||
def get_texture(
|
||||
graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None, hard_mode: bool = False
|
||||
) -> Texture | None:
|
||||
if graphic == Graphic.EMPTY:
|
||||
return None
|
||||
|
||||
if graphic == "Grass":
|
||||
return get_texture_by_identifier(BACKGROUND_TILE)
|
||||
|
||||
if graphic == Graphic.BOSS_2_HEALTH and not hard_mode:
|
||||
return get_texture_by_identifier(EASY_MODE_BOSS_2_HEALTH)
|
||||
|
||||
if graphic in IMAGE_GRAPHICS:
|
||||
return get_texture_by_identifier(IMAGE_GRAPHICS[graphic])
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ def create_gameboard(hard_mode: bool, hammer_exists: bool, extra_chest: bool) ->
|
||||
breakable_block = BreakableBlock() if hammer_exists else Empty()
|
||||
|
||||
normal_enemy = EnemyWithLoot(2 if hard_mode else 1, Location.ENEMY_DROP)
|
||||
boss = FinalBoss(5 if hard_mode else 3)
|
||||
boss = FinalBoss(5 if hard_mode else 2)
|
||||
|
||||
gameboard = (
|
||||
(Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty()),
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 754 B |
+92
-71
@@ -2,12 +2,16 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from rule_builder.options import OptionFilter
|
||||
from rule_builder.rules import Has, HasAll, Rule
|
||||
|
||||
from .options import HardMode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .world import APQuestWorld
|
||||
|
||||
HAS_KEY = Has("Key") # Hmm, what could this be? A little foreshadowing perhaps? :) You'll find out if you keep reading!
|
||||
|
||||
|
||||
def set_all_rules(world: APQuestWorld) -> None:
|
||||
# In order for AP to generate an item layout that is actually possible for the player to complete,
|
||||
@@ -26,36 +30,46 @@ def set_all_entrance_rules(world: APQuestWorld) -> None:
|
||||
overworld_to_top_left_room = world.get_entrance("Overworld to Top Left Room")
|
||||
right_room_to_final_boss_room = world.get_entrance("Right Room to Final Boss Room")
|
||||
|
||||
# An access rule is a function. We can define this function like any other function.
|
||||
# This function must accept exactly one parameter: A "CollectionState".
|
||||
# A CollectionState describes the current progress of the players in the multiworld, i.e. what items they have,
|
||||
# which regions they've reached, etc.
|
||||
# In an access rule, we can ask whether the player has a collected a certain item.
|
||||
# We can do this via the state.has(...) function.
|
||||
# This function takes an item name, a player number, and an optional count parameter (more on that below)
|
||||
# Since a rule only takes a CollectionState parameter, but we also need the player number in the state.has call,
|
||||
# our function needs to be locally defined so that it has access to the player number from the outer scope.
|
||||
# In our case, we are inside a function that has access to the "world" parameter, so we can use world.player.
|
||||
def can_destroy_bush(state: CollectionState) -> bool:
|
||||
return state.has("Sword", world.player)
|
||||
# Now, let's make some rules!
|
||||
# First, let's handle the transition from the overworld to the bottom right room,
|
||||
# which requires slashing a bush with the Sword.
|
||||
# For this, we need a rule that says "player has a Sword".
|
||||
# We can use a "Has"-type rule from the rule_builder module for this.
|
||||
can_destroy_bush = Has("Sword")
|
||||
|
||||
# Now we can set our "can_destroy_bush" rule to our entrance which requires slashing a bush to clear the path.
|
||||
# One way to set rules is via the set_rule() function, which works on both Entrances and Locations.
|
||||
set_rule(overworld_to_bottom_right_room, can_destroy_bush)
|
||||
# Now we can set our "can_destroy_bush" rule to the entrance which requires slashing a bush to clear the path.
|
||||
# The easiest way to do this is by calling world.set_rule, which works for both Locations and Entrances.
|
||||
world.set_rule(overworld_to_bottom_right_room, can_destroy_bush)
|
||||
|
||||
# Because the function has to be defined locally, most worlds prefer the lambda syntax.
|
||||
set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
|
||||
|
||||
# Conditions can depend on event items.
|
||||
set_rule(right_room_to_final_boss_room, lambda state: state.has("Top Left Room Button Pressed", world.player))
|
||||
# Conditions can also depend on event items.
|
||||
button_pressed = Has("Top Left Room Button Pressed")
|
||||
world.set_rule(right_room_to_final_boss_room, button_pressed)
|
||||
|
||||
# Some entrance rules may only apply if the player enabled certain options.
|
||||
# In our case, if the hammer option is enabled, we need to add the Hammer requirement to the Entrance from
|
||||
# Overworld to the Top Middle Room.
|
||||
if world.options.hammer:
|
||||
overworld_to_top_middle_room = world.get_entrance("Overworld to Top Middle Room")
|
||||
set_rule(overworld_to_top_middle_room, lambda state: state.has("Hammer", world.player))
|
||||
can_smash_brick = Has("Hammer")
|
||||
world.set_rule(overworld_to_top_middle_room, can_smash_brick)
|
||||
|
||||
# So far, we've been using "Has" from the Rule Builder to make our rules.
|
||||
# There is another way to make rules that you will see in a lot of older worlds.
|
||||
# A rule can just be a function that takes a "state" argument and returns a bool.
|
||||
# As a demonstration of what that looks like, let's do it with our final Entrance rule:
|
||||
world.set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
|
||||
# This style is not really recommended anymore, though.
|
||||
# Notice how you have to explicitly capture world.player here so that the rule applies to the correct player?
|
||||
# Well, Rule Builder does this part for you, inside of world.set_rule.
|
||||
# This doesn't just result in shorter code, it also means you can define rules statically (at the module level).
|
||||
# APQuest opts to create its Rule objects locally, but just to show what this would look like,
|
||||
# we'll re-set the "Overworld to Top Left Room" rule to a constant defined at the top of this file:
|
||||
world.set_rule(overworld_to_top_left_room, HAS_KEY)
|
||||
|
||||
# Beyond these structural advantages,
|
||||
# Rule Builder also allows the core AP code to do a lot of under-the-hood optimizations.
|
||||
# Rule Builder is quite comprehensive, and even if you have really esoteric rules,
|
||||
# you can make custom rules by subclassing CustomRule.
|
||||
|
||||
def set_all_location_rules(world: APQuestWorld) -> None:
|
||||
# Location rules work no differently from Entrance rules.
|
||||
@@ -67,65 +81,72 @@ def set_all_location_rules(world: APQuestWorld) -> None:
|
||||
# So, we need to set requirements on the Locations themselves.
|
||||
# Since combat is a bit more complicated, we'll use this chance to cover some advanced access rule concepts.
|
||||
|
||||
# Sometimes, you may want to have different rules depending on the player's chosen options.
|
||||
# There is a wrong way to do this, and a right way to do this. Let's do the wrong way first.
|
||||
# In "set_all_entrance_rules", we had a rule for a location that doesn't always exist.
|
||||
# In this case, we had to check for its existence (by checking the player's chosen options) before setting the rule.
|
||||
# Other times, you may have a situation where a location can have two different rules depending on the options.
|
||||
# In our case, the enemy in the right room has more health if hard mode is selected,
|
||||
# so ontop of the Sword, the player will either need one more health or a Shield in hard mode.
|
||||
# First, let's make our sword condition.
|
||||
can_defeat_basic_enemy: Rule = Has("Sword")
|
||||
|
||||
# Next, we'll check whether hard mode has been chosen in the player options.
|
||||
if world.options.hard_mode:
|
||||
# We'll make the condition for "Has a Shield or a Health Upgrade".
|
||||
# We can chain two "Has" conditions together with the | operator to make "Has Shield or has Health Upgrade".
|
||||
can_withstand_a_hit = Has("Shield") | Has("Health Upgrade")
|
||||
|
||||
# Now, we chain this rule to our Sword rule.
|
||||
# Since we want both conditions to be true, in this case, we have to chain them in an "and" way.
|
||||
# For this, we can use the & operator.
|
||||
can_defeat_basic_enemy = can_defeat_basic_enemy & can_withstand_a_hit
|
||||
|
||||
# Finally, we set our rule onto the Right Room Eney Drop location.
|
||||
right_room_enemy = world.get_location("Right Room Enemy Drop")
|
||||
world.set_rule(right_room_enemy, can_defeat_basic_enemy)
|
||||
|
||||
# DON'T DO THIS!!!!
|
||||
set_rule(
|
||||
right_room_enemy,
|
||||
lambda state: (
|
||||
state.has("Sword", world.player)
|
||||
and (not world.options.hard_mode or state.has_any(("Shield", "Health Upgrade"), world.player))
|
||||
),
|
||||
)
|
||||
# DON'T DO THIS!!!!
|
||||
# For the final boss, we also need to chain multiple conditions.
|
||||
# First of all, you always need a Sword and a Shield.
|
||||
# So far, we used the | and & operators to chain "Has" rules.
|
||||
# Instead, we can also use HasAny for an or-chain of items, or HasAll for an and-chain of items.
|
||||
has_sword_and_shield: Rule = HasAll("Sword", "Shield")
|
||||
|
||||
# Now, what's actually wrong with this? It works perfectly fine, right?
|
||||
# If hard mode disabled, Sword is enough. If hard mode is enabled, we also need a Shield or a Health Upgrade.
|
||||
# The access rule we just wrote does this correctly, so what's the problem?
|
||||
# The problem is performance.
|
||||
# Most of your world code doesn't need to be perfectly performant, since it just runs once per slot.
|
||||
# However, access rules in particular are by far the hottest code path in Archipelago.
|
||||
# An access rule will potentially be called thousands or even millions of times over the course of one generation.
|
||||
# As a result, access rules are the one place where it's really worth putting in some effort to optimize.
|
||||
# What's the performance problem here?
|
||||
# Every time our access rule is called, it has to evaluate whether world.options.hard_mode is True or False.
|
||||
# Wouldn't it be better if in easy mode, the access rule only checked for Sword to begin with?
|
||||
# Wouldn't it also be better if in hard mode, it already knew it had to check Shield and Health Upgrade as well?
|
||||
# Well, we can achieve this by doing the "if world.options.hard_mode" check outside the set_rule call,
|
||||
# and instead having two *different* set_rule calls depending on which case we're in.
|
||||
# In hard mode, the player also needs both Health Upgrades to survive long enough to defeat the boss.
|
||||
# For this, we can use the optional "count" parameter for "Has".
|
||||
has_both_health_upgrades = Has("Health Upgrade", count=2)
|
||||
|
||||
if world.options.hard_mode:
|
||||
# If you have multiple conditions, you can obviously chain them via "or" or "and".
|
||||
# However, there are also the nice helper functions "state.has_any" and "state.has_all".
|
||||
set_rule(
|
||||
right_room_enemy,
|
||||
lambda state: (
|
||||
state.has("Sword", world.player) and state.has_any(("Shield", "Health Upgrade"), world.player)
|
||||
),
|
||||
)
|
||||
else:
|
||||
set_rule(right_room_enemy, lambda state: state.has("Sword", world.player))
|
||||
# Previously, we used an "if world.options.hard_mode" condition to check if we should apply the extra requirement.
|
||||
# However, if you're comfortable with boolean logic, there is another way.
|
||||
# OptionFilter is a rule component which isn't a "Rule" on its own, but when used in a boolean expression with
|
||||
# rules, it acts like True if the option has the specified value, and acts like False otherwise.
|
||||
hard_mode_is_off = OptionFilter(HardMode, False)
|
||||
|
||||
# Another way to chain multiple conditions is via the add_rule function.
|
||||
# This makes the access rules a bit slower though, so it should only be used if your structure justifies it.
|
||||
# In our case, it's pretty useful because hard mode and easy mode have different requirements.
|
||||
# So with this option-checking rule component in hand, we can write our boss condition like this:
|
||||
can_defeat_final_boss = has_sword_and_shield & (hard_mode_is_off | has_both_health_upgrades)
|
||||
# If you're not as comfortable with boolean logic, it might be somewhat confusing why this is correct.
|
||||
# There is nothing wrong with using "if" conditions to check for options, if you find that easier to understand.
|
||||
|
||||
# Finally, we apply the rule to our "Final Boss Defeated" event location.
|
||||
final_boss = world.get_location("Final Boss Defeated")
|
||||
|
||||
# For the "known" requirements, it's still better to chain them using a normal "and" condition.
|
||||
add_rule(final_boss, lambda state: state.has_all(("Sword", "Shield"), world.player))
|
||||
|
||||
if world.options.hard_mode:
|
||||
# You can check for multiple copies of an item by using the optional count parameter of state.has().
|
||||
add_rule(final_boss, lambda state: state.has("Health Upgrade", world.player, 2))
|
||||
world.set_rule(final_boss, can_defeat_final_boss)
|
||||
|
||||
|
||||
def set_completion_condition(world: APQuestWorld) -> None:
|
||||
# Finally, we need to set a completion condition for our world, defining what the player needs to win the game.
|
||||
# For this, we can use world.set_completion_rule.
|
||||
# You can just set a completion condition directly like any other condition, referencing items the player receives:
|
||||
world.multiworld.completion_condition[world.player] = lambda state: state.has_all(("Sword", "Shield"), world.player)
|
||||
world.set_completion_rule(HasAll("Sword", "Shield"))
|
||||
|
||||
# In our case, we went for the Victory event design pattern (see create_events() in locations.py).
|
||||
# So lets undo what we just did, and instead set the completion condition to:
|
||||
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
|
||||
world.set_completion_rule(Has("Victory"))
|
||||
|
||||
|
||||
# One final comment about rules:
|
||||
# If your world exclusively uses Rule Builder rules (like APQuest), it's worth trying CachedRuleBuilderWorld.
|
||||
# CachedRuleBuilderWorld is a subclass of World that has a bunch of caching magic to make rules faster.
|
||||
# Just have your world class subclass CachedRuleBuilderWorld instead of World:
|
||||
# class APQuestWorld(CachedRuleBuilderWorld): ...
|
||||
# This may speed up your world, or it may make it slower.
|
||||
# The exact factors are complex and not well understood, but there is no harm in trying it.
|
||||
# Generate a few seeds and see if there is a noticeable difference!
|
||||
# If you're wondering, author has checked: APQuest is too simple to see any benefits, so we'll stick with "World".
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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__
|
||||
@@ -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 %}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
- Factorio: [Factorio Official Website](https://factorio.com)
|
||||
- Needed by Players and Hosts
|
||||
- Players will need to download an additional copy of Factorio for Archipelago to use. If you already own the game, you can download another copy for free by logging into the website.
|
||||
|
||||
##### Server Hosts
|
||||
|
||||
@@ -38,9 +39,13 @@ Validator page: [Yaml Validation Page](/check)
|
||||
Connecting to someone else's game is the simplest way to play Factorio with Archipelago. It allows multiple people to
|
||||
play in a single world, all contributing to the completion of the seed.
|
||||
|
||||
1. Acquire the Archipelago mod for this seed. It should be named `AP_*.zip`, where `*` is the seed number.
|
||||
2. Copy the mod file into your Factorio `mods` folder, which by default is located at:
|
||||
`C:\Users\<YourUserName>\AppData\Roaming\Factorio\mods`
|
||||
**Note:** If nobody else on the multiworld is joining your Factorio game, then you are
|
||||
hosting your own. This includes cases where more than one person is playing Factorio, but
|
||||
all on separate worlds.
|
||||
|
||||
1. Acquire the Archipelago mod for this seed. Once you create the multiworld on the Archipelago website, there will be a link next to your name in the "Download Link" column for your Archipelago room. It will be called `AP-<seed>-<playernumber>-<playername>_<archipelago_version>.zip`.
|
||||
2. Copy the mod file into your Factorio `mods` folder, which by default is located at:
|
||||
`C:\Users\<YourUserName>\AppData\Roaming\Factorio\mods`. If you're on Linux, it's located at `~/.factorio/mods`.
|
||||
3. Get the server address from the person hosting the game you are joining.
|
||||
4. Launch Factorio
|
||||
5. Click on "Multiplayer" in the main menu
|
||||
@@ -66,8 +71,8 @@ This guide uses the following terms to refer to the software:
|
||||
### What a Playable State Looks Like
|
||||
|
||||
- An Archipelago Server
|
||||
- The generated Factorio Mod, created as a result of running `ArchipelagoGenerate.exe`
|
||||
- One running instance of `ArchipelagoFactorioClient.exe` (the Archipelago Client) per Factorio world
|
||||
- The generated Factorio Mod, created as a result of running `ArchipelagoGenerate.exe` or downloaded from
|
||||
- One running instance of the Archipelago Client per Factorio world
|
||||
- A running modded Factorio Server, which should have been started by the Archipelago Client automatically
|
||||
- A running modded Factorio Client
|
||||
|
||||
@@ -75,7 +80,7 @@ This guide uses the following terms to refer to the software:
|
||||
|
||||
To play Factorio with Archipelago, a dedicated server setup is required. This dedicated Factorio Server must be
|
||||
installed separately from your main Factorio Client installation. The recommended way to install two instances of
|
||||
Factorio on your computer is to download the Factorio installer file directly from
|
||||
Factorio on your computer is to download the standalone Factorio file directly from
|
||||
factorio.com: [Factorio Official Website Download Page](https://factorio.com/download).
|
||||
|
||||
#### If you purchased Factorio on Steam, GOG, etc.
|
||||
@@ -88,7 +93,7 @@ Factorio product code. This will allow you to download the game directly from th
|
||||
|
||||
It is recommended to download the standalone version of Factorio for use as a dedicated server. Doing so prevents any
|
||||
potential conflicts with your currently-installed version of Factorio. Download the file by clicking on the button
|
||||
appropriate to your operating system, and extract the folder to a convenient location. The best place to do this for
|
||||
appropriate to your operating system, and extract the folder to a convenient location. The best place to do this for
|
||||
Archipelago is to place the extracted game folder into the `Archipelago` directory and rename it to just be "Factorio".
|
||||
|
||||
|
||||
@@ -100,9 +105,9 @@ have logged in, you may close the game.
|
||||
|
||||
#### Configure your Archipelago Installation
|
||||
|
||||
If you did not place the Factorio standalone in your Archipelago installation, you must modify your `host.yaml` file
|
||||
inside your Archipelago installation directory so that it points to your standalone Factorio executable. Here is an
|
||||
example of the appropriate setup, note the double `\\` are required:
|
||||
If you did not place the Factorio standalone in your Archipelago installation, you must modify your `host.yaml` file
|
||||
inside your Archipelago installation directory so that it points to your standalone Factorio executable. Here is an
|
||||
example of the appropriate setup, note the double `\\` are required if you are on Windows:
|
||||
|
||||
```yaml
|
||||
factorio_options:
|
||||
@@ -115,12 +120,11 @@ This allows you to host your own Factorio game.
|
||||
|
||||
1. Obtain the Factorio mod for this Archipelago seed. It should be named `AP_*.zip`, where `*` is the seed number.
|
||||
2. Install the mod into your Factorio Server by copying the zip file into the `mods` folder.
|
||||
3. Install the mod into your Factorio Client by copying the zip file into the `mods` folder, which is likely located
|
||||
at `C:\Users\YourName\AppData\Roaming\Factorio\mods`.
|
||||
4. Obtain the Archipelago Server address from the website's host room, or from the server host.
|
||||
5. Run your Archipelago Client, which is named `ArchipelagoFactorioClient.exe`. This was installed along with
|
||||
Archipelago if you chose to include it during the installation process.
|
||||
6. Enter `/connect [server-address]` into the input box at the bottom of the Archipelago Client and press "Enter"
|
||||
3. Disable Space Age on your server by editing `mods-list.json` in the server's `mods` folder, going to the Space Age section, and changing `enabled: true` to `enabled: false`.
|
||||
4. Install the mod into your Factorio Client by copying the zip file into the `mods` folder, which is likely located
|
||||
at `C:\Users\YourName\AppData\Roaming\Factorio\mods`. If you're on Linux, it will be located at `~/.factorio/mods`.
|
||||
5. Obtain the Archipelago Server address from the website's host room, or from the server host.
|
||||
6. Open your Archipelago client. Type `/connect <server-address>` into the input box at the bottom of the client and press enter.
|
||||
|
||||

|
||||
|
||||
@@ -138,9 +142,10 @@ For more information about the commands you can use, see the [Commands Guide](/t
|
||||
## Allowing Other People to Join Your Game
|
||||
|
||||
1. Ensure your Archipelago Client is running.
|
||||
2. Ensure port `34197` is forwarded to the computer running the Archipelago Client.
|
||||
3. Obtain your IP address by visiting whatismyip.com: [WhatIsMyIP Website](https://whatismyip.com/).
|
||||
4. Provide your IP address to anyone you want to join your game, and have them follow the steps for
|
||||
2. Ensure port `34197` is open on your computer's firewall.
|
||||
3. Ensure port `34197` is forwarded to the computer running the Archipelago Client. This can be done in your router's settings.
|
||||
4. Obtain your IP address by visiting whatismyip.com: [WhatIsMyIP Website](https://whatismyip.com/).
|
||||
5. Provide your IP address to anyone you want to join your game, and have them follow the steps for
|
||||
"Connecting to Someone Else's Factorio Game" above.
|
||||
|
||||
## Enabling Peaceful Mode
|
||||
@@ -210,8 +215,8 @@ contents of this file may help you troubleshoot an issue on your own and is vita
|
||||
in Archipelago.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Alternate Tutorial by
|
||||
Umenen: [Factorio (Steam) Archipelago Setup Guide for Windows](https://docs.google.com/document/d/1yZPAaXB-QcetD8FJsmsFrenAHO5V6Y2ctMAyIoT9jS4)
|
||||
- Factorio Speedrun Guide: [Factorio Speedrun Guide by Nefrums](https://www.youtube.com/watch?v=ExLrmK1c7tA)
|
||||
- Factorio Wiki: [Factorio Official Wiki](https://wiki.factorio.com/)
|
||||
- [Archipelago Factorio Cheat Sheet](https://docs.google.com/spreadsheets/d/1317PZU957IVWCG9jUUklyHAl82BhrKIriynSZJ0AGNg/edit?gid=0#gid=0)
|
||||
|
||||
@@ -52,24 +52,3 @@ class GenericWorld(World):
|
||||
if name == "Nothing":
|
||||
return Item(name, ItemClassification.filler, -1, self.player)
|
||||
raise InvalidItemError(name)
|
||||
|
||||
@deprecated("worlds.generic.PlandoItem is deprecated and will be removed in the next version. "
|
||||
"Use Options.PlandoItem(s) instead.")
|
||||
class PlandoItem(NamedTuple):
|
||||
item: str
|
||||
location: str
|
||||
world: Union[bool, str] = False # False -> own world, True -> not own world
|
||||
from_pool: bool = True # if item should be removed from item pool
|
||||
force: str = 'silent' # false -> warns if item not successfully placed. true -> errors out on failure to place item.
|
||||
|
||||
def warn(self, warning: str):
|
||||
if self.force in ['true', 'fail', 'failure', 'none', 'false', 'warn', 'warning']:
|
||||
logging.warning(f'{warning}')
|
||||
else:
|
||||
logging.debug(f'{warning}')
|
||||
|
||||
def failed(self, warning: str, exception=Exception):
|
||||
if self.force in ['true', 'fail', 'failure']:
|
||||
raise exception(warning)
|
||||
else:
|
||||
self.warn(warning)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
from collections.abc import Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from .Locations import LocationData, get_locations_by_tags
|
||||
|
||||
# Item name to ram value conversion
|
||||
item_ids: Final[Mapping[str, int]] = MappingProxyType({
|
||||
"Strength": 0x000002,
|
||||
"Speed": 0x000003,
|
||||
"Magic": 0x000004,
|
||||
"Armour": 0x000005,
|
||||
"Gold": 0x000006,
|
||||
"Key": 0x000007,
|
||||
"XP": 0x000009,
|
||||
"Level": 0x00000A,
|
||||
"Class": 0x00000B,
|
||||
"Max": 0x00000D,
|
||||
"Compass": 0x000270,
|
||||
"Lightning Potion": 0x001008,
|
||||
"Light Potion": 0x002008,
|
||||
"Acid Potion": 0x003008,
|
||||
"Fire Potion": 0x004008,
|
||||
"Acid Breath": 0x800480,
|
||||
"Lightning Breath": 0x8004F0,
|
||||
"Fire Breath": 0x800460,
|
||||
"Light Amulet": 0x400870,
|
||||
"Acid Amulet": 0x400880,
|
||||
"Lightning Amulet": 0x4008F0,
|
||||
"Fire Amulet": 0x400860,
|
||||
"Lightning Shield": 0x400CF0,
|
||||
"Fire Shield": 0x400C60,
|
||||
"Invisibility": 0x4000E0,
|
||||
"Levitate": 0x400030,
|
||||
"Speed Boots": 0x400173,
|
||||
"3-Way Shot": 0x400020,
|
||||
"5-Way Shot": 0x400160,
|
||||
"Rapid Fire": 0x400050,
|
||||
"Reflective Shot": 0x400840,
|
||||
"Reflective Shield": 0x400C40,
|
||||
"Super Shot": 0x8008B0,
|
||||
"Timestop": 0x4000D0,
|
||||
"Phoenix Familiar": 0x400110,
|
||||
"Growth": 0x400120,
|
||||
"Shrink": 0x400130,
|
||||
"Thunder Hammer": 0x800940,
|
||||
"Anti-Death Halo": 0x7000A0,
|
||||
"Invulnerability": 0x7000C0,
|
||||
"Health": 0x000001,
|
||||
"Runestone": 0x008210,
|
||||
"Mirror Shard": 0x008260,
|
||||
"Ice Axe of Untar": 0x8201C0,
|
||||
"Flame of Tarkana": 0x8201D0,
|
||||
"Scimitar of Decapitation": 0x8201E0,
|
||||
"Marker's Javelin": 0x8201F0,
|
||||
"Soul Savior": 0x820200,
|
||||
"Mountain": 0x00804A,
|
||||
"Castle": 0x00801A,
|
||||
"Hell": 0x00806A,
|
||||
"Ice": 0x00807A,
|
||||
"Town": 0x00808A,
|
||||
"Temple": 0x00809A,
|
||||
"Battlefield": 0x0080DA,
|
||||
"Skorne": 0x0080EA,
|
||||
"Secret": 0x0080FA,
|
||||
"Obelisk": 0x00800C,
|
||||
"Minotaur": 0x0001A1,
|
||||
"Falconess": 0x0001A2,
|
||||
"Jackal": 0x0001A3,
|
||||
"Tigress": 0x0001A4,
|
||||
"Sumner": 0x0001A5,
|
||||
"Skorne's Mask": 0x840620,
|
||||
"Skorne's Horns": 0x840630,
|
||||
"Skorne's Right Gauntlet": 0x840A40,
|
||||
"Skorne's Left Gauntlet": 0x840A50,
|
||||
"Death": 0x0000F0,
|
||||
"Crossbow Shooter": 0x0000F1,
|
||||
"Bomb Thrower": 0x0000F2,
|
||||
"Bomb Runner": 0x0000F3,
|
||||
"Golem": 0x0000F4,
|
||||
})
|
||||
|
||||
# Character names used for slot data
|
||||
characters: Final[tuple[str, ...]] = ("Minotaur", "Falconess", "Tigress", "Jackal", "Sumner")
|
||||
|
||||
# Base item charge count per pickup
|
||||
# Some items are bitwise
|
||||
base_count: Final[Mapping[str, int]] = MappingProxyType({
|
||||
"Key": 1,
|
||||
"Lightning Potion": 1,
|
||||
"Light Potion": 1,
|
||||
"Acid Potion": 1,
|
||||
"Fire Potion": 1,
|
||||
"Acid Breath": 5,
|
||||
"Lightning Breath": 5,
|
||||
"Fire Breath": 5,
|
||||
"Light Amulet": 30,
|
||||
"Acid Amulet": 30,
|
||||
"Lightning Amulet": 30,
|
||||
"Fire Amulet": 30,
|
||||
"Lightning Shield": 10,
|
||||
"Fire Shield": 10,
|
||||
"Invisibility": 30,
|
||||
"Levitate": 30,
|
||||
"Speed Boots": 30,
|
||||
"3-Way Shot": 30,
|
||||
"5-Way Shot": 30,
|
||||
"Rapid Fire": 30,
|
||||
"Reflective Shot": 30,
|
||||
"Reflective Shield": 30,
|
||||
"Super Shot": 3,
|
||||
"Timestop": 15,
|
||||
"Phoenix Familiar": 30,
|
||||
"Growth": 30,
|
||||
"Shrink": 30,
|
||||
"Thunder Hammer": 3,
|
||||
"Anti-Death Halo": 30,
|
||||
"Invulnerability": 30,
|
||||
"Fruit": 50,
|
||||
"Meat": 100,
|
||||
"Gold": 100,
|
||||
"Runestone 1": 1,
|
||||
"Runestone 2": 2,
|
||||
"Runestone 3": 3,
|
||||
"Runestone 4": 4,
|
||||
"Runestone 5": 5,
|
||||
"Runestone 6": 6,
|
||||
"Runestone 7": 7,
|
||||
"Runestone 8": 8,
|
||||
"Runestone 9": 9,
|
||||
"Runestone 10": 10,
|
||||
"Runestone 11": 11,
|
||||
"Runestone 12": 12,
|
||||
"Runestone 13": 13,
|
||||
"Dragon Mirror Shard": 1,
|
||||
"Chimera Mirror Shard": 3,
|
||||
"Plague Fiend Mirror Shard": 4,
|
||||
"Yeti Mirror Shard": 2,
|
||||
"Valley of Fire Obelisk": 1,
|
||||
"Dagger Peak Obelisk": 2,
|
||||
"Cliffs of Desolation Obelisk": 3,
|
||||
"Poisoned Fields Obelisk": 4,
|
||||
"Haunted Cemetery Obelisk": 5,
|
||||
"Castle Courtyard Obelisk": 6,
|
||||
"Dungeon of Torment Obelisk": 7,
|
||||
"Ice Axe of Untar": 1,
|
||||
"Flame of Tarkana": 1,
|
||||
"Scimitar of Decapitation": 1,
|
||||
"Marker's Javelin": 1,
|
||||
"Soul Savior": 1,
|
||||
"Minotaur": 1,
|
||||
"Falconess": 1,
|
||||
"Tigress": 1,
|
||||
"Jackal": 1,
|
||||
"Sumner": 1,
|
||||
"Poison Fruit": -50,
|
||||
"Skorne's Mask": 50,
|
||||
"Skorne's Horns": 50,
|
||||
"Skorne's Left Gauntlet": 100,
|
||||
"Skorne's Right Gauntlet": 100,
|
||||
"Portal to Dagger Peak": 1,
|
||||
"Portal to Cliffs of Desolation": 2,
|
||||
"Portal to Lost Cave": 3,
|
||||
"Portal to Volcanic Caverns": 4,
|
||||
"Portal to Dragon's Lair": 5,
|
||||
"Portal to Dungeon of Torment": 1,
|
||||
"Portal to Tower Armory": 2,
|
||||
"Portal to Castle Treasury": 3,
|
||||
"Portal to Chimera's Keep": 4,
|
||||
"Portal to Haunted Cemetery": 1,
|
||||
"Portal to Venomous Spire": 2,
|
||||
"Portal to Toxic Air Ship": 3,
|
||||
"Portal to Vat of the Plague Fiend": 4,
|
||||
"Portal to Frozen Camp": 1,
|
||||
"Portal to Crystal Mine": 2,
|
||||
"Portal to Erupting Fissure": 3,
|
||||
"Portal to Yeti's Cavern": 4,
|
||||
"Portal to Fortified Towers": 1,
|
||||
"Portal to Infernal Fortress": 2,
|
||||
"Death": 1,
|
||||
"Crossbow Shooter": 1,
|
||||
"Bomb Thrower": 1,
|
||||
"Bomb Runner": 1,
|
||||
"Golem": 1
|
||||
})
|
||||
|
||||
# (zone_config << 4) + room_config -> location list
|
||||
level_locations: Final[Mapping[int, Sequence[LocationData]]] = MappingProxyType({
|
||||
# Zone 0x00 - Castle
|
||||
0x00: get_locations_by_tags("castle_courtyard"),
|
||||
0x01: get_locations_by_tags("dungeon_of_torment"),
|
||||
0x02: get_locations_by_tags("tower_armory"),
|
||||
0x03: get_locations_by_tags("castle_treasury"),
|
||||
0x08: get_locations_by_tags("chimeras_keep"),
|
||||
|
||||
# Zone 0x03 - Mountain
|
||||
0x30: get_locations_by_tags("valley_of_fire"),
|
||||
0x31: get_locations_by_tags("dagger_peak"),
|
||||
0x32: get_locations_by_tags("cliffs_of_desolation"),
|
||||
0x33: get_locations_by_tags("lost_cave"),
|
||||
0x34: get_locations_by_tags("volcanic_cavern"),
|
||||
0x38: get_locations_by_tags("dragons_lair"),
|
||||
|
||||
# Zone 0x07 - Town
|
||||
0x70: get_locations_by_tags("poisoned_fields"),
|
||||
0x71: get_locations_by_tags("haunted_cemetery"),
|
||||
0x72: get_locations_by_tags("venomous_spire"),
|
||||
0x73: get_locations_by_tags("toxic_air_ship"),
|
||||
0x78: get_locations_by_tags("plague_fiend"),
|
||||
|
||||
# Zone 0x05 - Underworld
|
||||
0x50: get_locations_by_tags("gates_of_the_underworld"),
|
||||
|
||||
# Zone 0x06 - Ice
|
||||
0x60: get_locations_by_tags("arctic_docks"),
|
||||
0x61: get_locations_by_tags("frozen_camp"),
|
||||
0x62: get_locations_by_tags("crystal_mine"),
|
||||
0x63: get_locations_by_tags("erupting_fissure"),
|
||||
0x68: get_locations_by_tags("yeti"),
|
||||
|
||||
# Zone 0x0D - Temple
|
||||
0xD0: get_locations_by_tags("desecrated_temple"),
|
||||
0xD8: get_locations_by_tags("altar_of_skorne"),
|
||||
|
||||
# Zone 0x0C - Battlefield
|
||||
0xC0: get_locations_by_tags("battle_trenches"),
|
||||
0xC1: get_locations_by_tags("fortified_towers"),
|
||||
0xC2: get_locations_by_tags("infernal_fortress"),
|
||||
})
|
||||
|
||||
# Compressed level size in ROM
|
||||
level_size: Final[tuple[int, ...]] = (
|
||||
0x9E0,
|
||||
0x5E0,
|
||||
0x740,
|
||||
0x8A0,
|
||||
0x90,
|
||||
0x3B0,
|
||||
0x5A0,
|
||||
0x890,
|
||||
0x670,
|
||||
0x7D0,
|
||||
0x90,
|
||||
0xCE0,
|
||||
0xA50,
|
||||
0xA30,
|
||||
0x8E0,
|
||||
0x20,
|
||||
0x760,
|
||||
0xE90,
|
||||
0xE40,
|
||||
0xE00,
|
||||
0xCD0,
|
||||
0x20,
|
||||
0x3F0,
|
||||
0x0,
|
||||
0xB00,
|
||||
0xA30,
|
||||
0xB30
|
||||
)
|
||||
|
||||
# Level address in ROM
|
||||
level_address: Final[tuple[int, ...]] = (
|
||||
0xF939B0,
|
||||
0xF958B0,
|
||||
0xF945B0,
|
||||
0xF94EE0,
|
||||
0xF84CC0,
|
||||
0xF910B0,
|
||||
0xF915B0,
|
||||
0xF91D00,
|
||||
0xF92710,
|
||||
0xF92F40,
|
||||
0xF84B70,
|
||||
0xF84FA0,
|
||||
0xF85EB0,
|
||||
0xF86B60,
|
||||
0xF877C0,
|
||||
0xF880E0,
|
||||
0xF901C0,
|
||||
0xF884E0,
|
||||
0xF89370,
|
||||
0xF8A5F0,
|
||||
0xF8B760,
|
||||
0xF8C7C0,
|
||||
0xF90B50,
|
||||
0x0,
|
||||
0xF8D960,
|
||||
0xF8E6E0,
|
||||
0xF8F110
|
||||
)
|
||||
|
||||
# Level header address in ROM
|
||||
level_header: Final[tuple[int, ...]] = (
|
||||
0xF9DD9C,
|
||||
0xF9E07C,
|
||||
0xF9DE54,
|
||||
0xF9DF0C,
|
||||
0xF9DFC4,
|
||||
0xF9E6E0,
|
||||
0xF9E798,
|
||||
0xF9E850,
|
||||
0xF9E908,
|
||||
0xF9E9C0,
|
||||
0xF9EA78,
|
||||
0xF9F004,
|
||||
0xF9F0BC,
|
||||
0xF9F174,
|
||||
0xF9F22C,
|
||||
0xF9F2E4,
|
||||
0xF9E1C0,
|
||||
0xF9E330,
|
||||
0xF9E3E4,
|
||||
0xF9E498,
|
||||
0xF9E54C,
|
||||
0xF9E600,
|
||||
0xF9DC2C,
|
||||
0x0,
|
||||
0xF9DA04,
|
||||
0xF9DABC,
|
||||
0xF9DB74
|
||||
)
|
||||
|
||||
# Runestones required to access difficulties
|
||||
# Used in Rules.py for access calculation
|
||||
difficulty_lambda: Final[Mapping[int, Sequence[int]]] = MappingProxyType({
|
||||
0x3: (0, 2, 3, 4),
|
||||
0x0: (0, 5, 6, 7),
|
||||
0x7: (0, 8, 9, 10),
|
||||
0x6: (0, 11, 12, 13),
|
||||
0xD: (0, 14, 15, 16),
|
||||
0xC: (0, 17, 18, 19),
|
||||
0x5: (0, 20, 21, 22)
|
||||
})
|
||||
|
||||
# Runestones required to access difficulties
|
||||
# Used in Rules.py for access calculation
|
||||
difficulty_lambda_no_portal: Final[Mapping[int, Sequence[int]]] = MappingProxyType({
|
||||
0x3: (0, 0, 1, 2),
|
||||
0x0: (0, 2, 3, 4),
|
||||
0x7: (0, 5, 6, 7),
|
||||
0x6: (0, 8, 9, 10),
|
||||
0xD: (0, 11, 12, 13),
|
||||
0xC: (0, 14, 15, 16),
|
||||
0x5: (0, 17, 18, 19)
|
||||
})
|
||||
|
||||
# ID's for names said by announcer
|
||||
sounds: Final[Mapping[int, int]] = MappingProxyType({
|
||||
0: 0x9D,
|
||||
1: 0x9E,
|
||||
2: 0xA7,
|
||||
3: 0xA5,
|
||||
5: 0x9F,
|
||||
6: 0xA1,
|
||||
7: 0xA0,
|
||||
8: 0xA2,
|
||||
9: 0xA7
|
||||
})
|
||||
|
||||
# ID's for colors said by announcer
|
||||
colors: Final[Mapping[int, int]] = MappingProxyType({
|
||||
0: 0xA3,
|
||||
1: 0xA6,
|
||||
2: 0x9C,
|
||||
3: 0xA4
|
||||
})
|
||||
|
||||
obelisks: Final[tuple[str, ...]] = (
|
||||
"Valley of Fire Obelisk",
|
||||
"Dagger Peak Obelisk",
|
||||
"Cliffs of Desolation Obelisk",
|
||||
"Poisoned Fields Obelisk",
|
||||
"Haunted Cemetery Obelisk",
|
||||
"Castle Courtyard Obelisk",
|
||||
"Dungeon of Torment Obelisk"
|
||||
)
|
||||
|
||||
mirror_shards: Final[tuple[str, ...]] = (
|
||||
"Dragon Mirror Shard",
|
||||
"Chimera Mirror Shard",
|
||||
"Yeti Mirror Shard",
|
||||
"Plague Fiend Mirror Shard"
|
||||
)
|
||||
|
||||
portals: Final[Mapping[str, str]] = MappingProxyType({
|
||||
"Portal to Dagger Peak": "Mountain",
|
||||
"Portal to Cliffs of Desolation": "Mountain",
|
||||
"Portal to Lost Cave": "Mountain",
|
||||
"Portal to Volcanic Caverns": "Mountain",
|
||||
"Portal to Dragon's Lair": "Mountain",
|
||||
"Portal to Dungeon of Torment": "Castle",
|
||||
"Portal to Tower Armory": "Castle",
|
||||
"Portal to Castle Treasury": "Castle",
|
||||
"Portal to Chimera's Keep": "Castle",
|
||||
"Portal to Frozen Camp": "Ice",
|
||||
"Portal to Crystal Mine": "Ice",
|
||||
"Portal to Erupting Fissure": "Ice",
|
||||
"Portal to Yeti's Cavern": "Ice",
|
||||
"Portal to Haunted Cemetery": "Town",
|
||||
"Portal to Venomous Spire": "Town",
|
||||
"Portal to Toxic Air Ship": "Town",
|
||||
"Portal to Vat of the Plague Fiend": "Town",
|
||||
"Portal to Fortified Towers": "Battlefield",
|
||||
"Portal to Infernal Fortress": "Battlefield"
|
||||
})
|
||||
|
||||
excluded_portals: Final[Mapping[str, list[str]]] = MappingProxyType({
|
||||
"Castle" : ["Portal to Dungeon of Torment", "Portal to Tower Armory", "Portal to Castle Treasury", "Portal to Chimera's Keep"],
|
||||
"Town" : ["Portal to Haunted Cemetery", "Portal to Venomous Spire", "Portal to Toxic Air Ship", "Portal to Vat of the Plague Fiend"],
|
||||
"Ice" : ["Portal to Frozen Camp", "Portal to Crystal Mine", "Portal to Erupting Fissure", "Portal to Yeti's Cavern"],
|
||||
"Battlefield" : ["Portal to Fortified Towers", "Portal to Infernal Fortress"]
|
||||
})
|
||||
|
||||
excluded_levels: Final[Mapping[str, list[str]]] = MappingProxyType({
|
||||
"Mountain" : ["Valley of Fire", "Dagger Peak", "Cliffs of Desolation", "Lost Cave", "Volcanic Caverns", "Dragon's Lair"],
|
||||
"Castle" : ["Castle Courtyard" ,"Dungeon of Torment", "Tower Armory", "Castle Treasury", "Chimera's Keep"],
|
||||
"Ice" : ["Arctic Docks", "Frozen Camp", "Crystal Mine", "Erupting Fissure", "Yeti's Cavern"],
|
||||
"Town" : ["Poisoned Fields", "Haunted Cemetery", "Venomous Spire", "Toxic Air Ship", "Vat of the Plague Fiend"],
|
||||
"Battlefield" : ["Battle Trenches", "Fortified Towers", "Infernal Fortress"]
|
||||
})
|
||||
|
||||
excluded_obelisks: Final[Mapping[str, list[str]]] = MappingProxyType({
|
||||
"Castle" : ["Valley of Fire Obelisk", "Dagger Peak Obelisk", "Cliffs of Desolation Obelisk"],
|
||||
"Town" : ["Castle Courtyard Obelisk", "Dungeon of Torment Obelisk"],
|
||||
"Ice" : ["Poisoned Fields Obelisk", "Haunted Cemetery Obelisk"],
|
||||
})
|
||||
|
||||
# Map location names to offsets in decompressed boss.bin
|
||||
boss_location_offsets: Final[Mapping[str, int]] = MappingProxyType({
|
||||
"Dragon's Lair - Dragon Mirror Shard": 0x9C08,
|
||||
"Yeti's Cavern - Yeti Mirror Shard": 0x9C18,
|
||||
"Chimera's Keep - Chimera Mirror Shard": 0x9C28,
|
||||
"Vat of the Plague Fiend - Plague Fiend Mirror Shard": 0x9C38,
|
||||
"Altar of Skorne - Skorne's Mask": 0x9C48,
|
||||
"Altar of Skorne - Skorne's Horns": 0x9C58,
|
||||
"Altar of Skorne - Skorne's Left Gauntlet": 0x9C68,
|
||||
"Altar of Skorne - Skorne's Right Gauntlet": 0x9C78
|
||||
})
|
||||
|
||||
boss_regions: Final[tuple[str, ...]] = (
|
||||
"Dragon's Lair",
|
||||
"Yeti's Cavern",
|
||||
"Chimera's Keep",
|
||||
"Vat of the Plague Fiend",
|
||||
"Altar of Skorne",
|
||||
"Gates of the Underworld",
|
||||
)
|
||||
|
||||
|
||||
# Item IDs for spawner traps (77780087-77780090)
|
||||
spawner_trap_ids: Final[tuple[int, ...]] = (77780087, 77780088, 77780089, 77780090)
|
||||
|
||||
player_compass_index: Final[Mapping[int, int]] = MappingProxyType({
|
||||
1: 1,
|
||||
2: 0,
|
||||
3: 3,
|
||||
4: 2
|
||||
})
|
||||
@@ -0,0 +1,728 @@
|
||||
import asyncio
|
||||
import Utils
|
||||
import settings
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import traceback
|
||||
import subprocess
|
||||
import Patch
|
||||
|
||||
from typing import Optional
|
||||
from BaseClasses import ItemClassification
|
||||
from CommonClient import ClientCommandProcessor, CommonContext, get_base_parser, gui_enabled, logger, server_loop
|
||||
from NetUtils import ClientStatus, NetworkItem
|
||||
|
||||
from .Data import (
|
||||
base_count,
|
||||
item_ids,
|
||||
level_locations,
|
||||
sounds,
|
||||
colors,
|
||||
portals,
|
||||
spawner_trap_ids,
|
||||
player_compass_index
|
||||
)
|
||||
from .Items import ItemData, items_by_id
|
||||
|
||||
READ = "READ_CORE_RAM"
|
||||
WRITE = "WRITE_CORE_RAM"
|
||||
PLAYER_CLASS = 0xFD30F
|
||||
PLAYER_COLOR = 0xFD30E
|
||||
SOUND_ADDRESS = 0xAE740
|
||||
SOUND_START = 0xEEFC
|
||||
PLAYER_KILL = 0xFD300
|
||||
PLAYER_MENU = 0x9D3C
|
||||
BOSS_GOAL = 0x45D34
|
||||
BOSS_GOAL_BACKUP = 0x45D3C
|
||||
LOCATIONS_BASE_ADDRESS = 0x64A68
|
||||
ZONE_ID = 0x6CA58
|
||||
LEVEL_ID = 0x6CA5C
|
||||
|
||||
MOD_ITEM_ID = 0x3FC800
|
||||
MOD_QUANTITY = 0x3FC804
|
||||
MOD_PLAYER_ID = 0x3FC808
|
||||
MOD_OBELISK_QUANTITY = 0x3FC7E4
|
||||
MOD_BOSS_GOAL = 0x3FC7E5
|
||||
MOD_PLAYERS_LIST = 0x3FC7D0
|
||||
MOD_COMPASS_COUNT = 0x3FC7D4
|
||||
|
||||
GOAL_POLLS = 5
|
||||
|
||||
|
||||
class RetroSocket:
|
||||
def __init__(self):
|
||||
self.host = "localhost"
|
||||
self.port = 55355
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.socket.setblocking(False)
|
||||
|
||||
def send(self, message: str):
|
||||
try:
|
||||
self.socket.sendto(message.encode(), (self.host, self.port))
|
||||
except Exception as e:
|
||||
raise Exception("An error occurred while sending a message.")
|
||||
|
||||
async def read(self, message: str) -> Optional[bytes]:
|
||||
self.send(message)
|
||||
try:
|
||||
response = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(self.socket, 30000), 1.0)
|
||||
data = response.decode().strip("\n").split(" ")
|
||||
b = b""
|
||||
for s in data[2:]:
|
||||
if "-1" in s:
|
||||
raise Exception("Client tried to read from an invalid address or ROM is not open...")
|
||||
b += bytes.fromhex(s)
|
||||
return b
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Timeout while waiting for socket response...")
|
||||
await asyncio.sleep(2)
|
||||
return None
|
||||
except ConnectionResetError:
|
||||
logger.error("The connection was reset...")
|
||||
await asyncio.sleep(2)
|
||||
return None
|
||||
except OSError as e:
|
||||
logger.error(f"Socket error during read: {e}")
|
||||
await asyncio.sleep(2)
|
||||
return None
|
||||
|
||||
async def status(self) -> str:
|
||||
message = "GET_STATUS"
|
||||
self.send(message)
|
||||
try:
|
||||
data = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(self.socket, 4096), 1.0)
|
||||
return data.decode()
|
||||
except (asyncio.TimeoutError, ConnectionResetError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
def message_format(arg: str, params: str) -> str:
|
||||
return f"{arg} {params}"
|
||||
|
||||
|
||||
def param_format(adr: int, arr: bytes) -> str:
|
||||
return " ".join([hex(adr)] + [f"0x{byte:02X}" for byte in arr])
|
||||
|
||||
|
||||
class GauntletLegendsCommandProcessor(ClientCommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
super().__init__(ctx)
|
||||
|
||||
def _cmd_deathlink_toggle(self):
|
||||
"""Toggle Deathlink on or off"""
|
||||
self.ctx.deathlink_enabled = not self.ctx.deathlink_enabled
|
||||
self.ctx.update_death_link(self.ctx.deathlink_enabled)
|
||||
logger.info(f"Deathlink {('Enabled.' if self.ctx.deathlink_enabled else 'Disabled.')}")
|
||||
|
||||
|
||||
class GauntletLegendsContext(CommonContext):
|
||||
command_processor = GauntletLegendsCommandProcessor
|
||||
game = "Gauntlet Legends"
|
||||
items_handling = 0b101
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.useful: list[NetworkItem] = []
|
||||
self.deathlink_pending: bool = False
|
||||
self.deathlink_enabled: bool = False
|
||||
self.deathlink_triggered: bool = False
|
||||
self.gl_sync_task = None
|
||||
self.glslotdata = None
|
||||
self.socket = RetroSocket()
|
||||
self.rom_loaded: bool = False
|
||||
self.locations_checked: list[int] = []
|
||||
self.retro_connected: bool = False
|
||||
self.scouted: bool = False
|
||||
self.obelisks: list[NetworkItem] = []
|
||||
self.item_locations: list[int] = []
|
||||
self.obelisk_locations: list[int] = []
|
||||
self.chest_locations: list[int] = []
|
||||
self.spawner_locations: list[int] = []
|
||||
self.item_address: int = 0
|
||||
self.chest_address: int = 0
|
||||
self.spawner_address: int = 0
|
||||
self.vanilla_spawner_count: int = 0
|
||||
self.zone: int = 0
|
||||
self.level: int = 0
|
||||
self.current_zone: int = -1
|
||||
self.current_level: int = -1
|
||||
self.level_id: int = 0
|
||||
self.location_scouts: list[NetworkItem] = []
|
||||
self.players: list[int] = []
|
||||
self.queued_traps: list[tuple[str, int, bool]] = []
|
||||
self.spawned_traps: int = 0
|
||||
self.item_ram_indices: dict[int, int] = {}
|
||||
self.chest_ram_indices: dict[int, int] = {}
|
||||
self.goal_streak: int = 0
|
||||
|
||||
def on_deathlink(self, data: dict):
|
||||
super().on_deathlink(data)
|
||||
self.deathlink_pending = True
|
||||
|
||||
async def update_stage(self):
|
||||
self.zone = await self._read_ram_int(ZONE_ID, 1)
|
||||
self.level = await self._read_ram_int(LEVEL_ID, 1)
|
||||
|
||||
async def check_goal(self) -> bool:
|
||||
if self.glslotdata is None:
|
||||
return False
|
||||
if self.glslotdata["goal"] == 1:
|
||||
goal = await self._read_ram_int(BOSS_GOAL, 4)
|
||||
backup = await self._read_ram_int(BOSS_GOAL_BACKUP, 4)
|
||||
return goal == 0xA or backup == 0xA
|
||||
elif self.glslotdata["goal"] == 2:
|
||||
goal = await self._read_ram_int(MOD_BOSS_GOAL, 1, True)
|
||||
if goal >= self.glslotdata["boss_goal_count"]:
|
||||
self.goal_streak += 1
|
||||
else:
|
||||
self.goal_streak = 0
|
||||
return self.goal_streak >= GOAL_POLLS
|
||||
|
||||
def _normalize_item_name(self, name: str) -> str:
|
||||
if "Runestone" in name:
|
||||
return "Runestone"
|
||||
if "Fruit" in name or "Meat" in name:
|
||||
return "Health"
|
||||
if "Obelisk" in name:
|
||||
return "Obelisk"
|
||||
if "Mirror" in name:
|
||||
return "Mirror Shard"
|
||||
if portals.get(name, False):
|
||||
return portals[name]
|
||||
return name
|
||||
|
||||
async def update_item(self, name: str, count: int, player: int = None, infinite_count: bool = False):
|
||||
name = self._normalize_item_name(name)
|
||||
await self._write_ram(MOD_ITEM_ID,
|
||||
int.to_bytes((item_ids[name] if not infinite_count else item_ids[name] & 0xFFFF), 4,
|
||||
"little"))
|
||||
await self._write_ram(MOD_QUANTITY, int.to_bytes(count, 4, "little", signed=True))
|
||||
await self._write_ram(MOD_PLAYER_ID, int.to_bytes(player, 4, "little"))
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(GauntletLegendsContext, self).server_auth(password_requested)
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"Connected"}:
|
||||
self.glslotdata = args["slot_data"]
|
||||
self.deathlink_enabled = bool(self.glslotdata["death_link"])
|
||||
if self.deathlink_enabled:
|
||||
Utils.async_start(self.update_death_link(self.deathlink_enabled))
|
||||
elif cmd == "LocationInfo":
|
||||
self.location_scouts = args["locations"]
|
||||
elif cmd == "RoomInfo":
|
||||
self.seed_name = args["seed_name"]
|
||||
|
||||
# Update inventory based on items received from server
|
||||
# Also adds starting items based on a few yaml options
|
||||
async def handle_items(self):
|
||||
self.players = list(await self._read_ram(MOD_PLAYERS_LIST, 4))
|
||||
self.players = [player for player in self.players if player != 0]
|
||||
if not self.players:
|
||||
return
|
||||
for player in self.players:
|
||||
active = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (player - 1)), 1)
|
||||
if active != 0x4:
|
||||
continue
|
||||
compass = await self._read_ram_int(MOD_COMPASS_COUNT + (2 * player_compass_index[player]), 2)
|
||||
if compass - 1 < len(self.items_received):
|
||||
for index in range(compass - 1, len(self.items_received)):
|
||||
active = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (player - 1)), 1)
|
||||
if active != 0x4:
|
||||
break
|
||||
item = self.items_received[index].item
|
||||
if player != self.players[0] and item in spawner_trap_ids:
|
||||
continue
|
||||
item_name = items_by_id[item].item_name
|
||||
if self.current_zone in (0x8, 0xE) and item in spawner_trap_ids:
|
||||
if len([trap for trap in self.queued_traps if trap[1] == index]) < 1:
|
||||
self.queued_traps.append((item_name, index, False))
|
||||
await self.give_item(item_name, player)
|
||||
await self.update_item("Compass", 1, player)
|
||||
|
||||
async def give_item(self, item_name: str, player: int):
|
||||
await self.wait_for_mod_clear()
|
||||
await asyncio.sleep(0.02)
|
||||
await self.update_item(item_name, base_count[item_name], player)
|
||||
await self.wait_for_mod_clear()
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
async def wait_for_mod_clear(self, poll_interval: float = 0.05):
|
||||
while True:
|
||||
mod_data = await self.socket.read(message_format(READ, f"0x{MOD_ITEM_ID:x} 12"))
|
||||
if mod_data and all(byte == 0 for byte in mod_data):
|
||||
return True
|
||||
await asyncio.sleep(poll_interval)
|
||||
|
||||
async def _read_ram(self, address: int, size: int) -> bytes:
|
||||
return await self.socket.read(message_format(READ, f"0x{address:x} {size}"))
|
||||
|
||||
async def _read_ram_int(self, address: int, size: int, signed: bool = False) -> int:
|
||||
data = await self._read_ram(address, size)
|
||||
return int.from_bytes(data, "little", signed=signed) if data else 0
|
||||
|
||||
async def _write_ram(self, address: int, data: bytes):
|
||||
self.socket.send(message_format(WRITE, param_format(address, data)))
|
||||
|
||||
async def dead(self) -> bool:
|
||||
val = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (self.players[0] - 1)), 1)
|
||||
return (val & 0xF) == 0x8
|
||||
|
||||
async def dead_or_menu(self) -> bool:
|
||||
val = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (self.players[0] - 1)), 1)
|
||||
return (val & 0xF) == 0x8 or (val & 0xF) == 0x1
|
||||
|
||||
async def get_seed_name(self) -> str:
|
||||
seed_name = await self._read_ram(0x3FC7F0, 0x10)
|
||||
return seed_name.decode("utf-8").strip()
|
||||
|
||||
async def scout_locations(self, ctx: "GauntletLegendsContext") -> None:
|
||||
try:
|
||||
self.location_scouts = []
|
||||
self.obelisk_locations = []
|
||||
self.item_locations = []
|
||||
self.chest_locations = []
|
||||
self.spawner_locations = []
|
||||
self.useful = []
|
||||
self.obelisks = []
|
||||
self.vanilla_spawner_count = 0
|
||||
self.item_ram_indices = {}
|
||||
self.chest_ram_indices = {}
|
||||
|
||||
raw_locations = [location for location in level_locations.get(self.level_id, []) if
|
||||
"Mirror" not in location.name and "Skorne" not in location.name]
|
||||
scoutable_location_ids = [location.id for location in raw_locations if
|
||||
location.id in ctx.checked_locations or location.id in self.missing_locations]
|
||||
|
||||
# Scout locations if any exist
|
||||
if raw_locations:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "LocationScouts",
|
||||
"locations": scoutable_location_ids,
|
||||
"create_as_hint": 0,
|
||||
}])
|
||||
while not self.location_scouts:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Build lookup for scouted items by location
|
||||
scouted_by_location = {item.location: item for item in self.location_scouts}
|
||||
|
||||
item_slots: list[int] = []
|
||||
chest_slots: list[int] = []
|
||||
appended_items: list[int] = []
|
||||
|
||||
for loc in raw_locations:
|
||||
is_obelisk_loc = "Obelisk" in loc.name
|
||||
is_chest_loc = (not is_obelisk_loc) and (
|
||||
"Chest" in loc.name
|
||||
or ("Barrel" in loc.name and "Barrel of Gold" not in loc.name)
|
||||
)
|
||||
|
||||
scouted = scouted_by_location.get(loc.id)
|
||||
|
||||
if not scouted:
|
||||
if is_obelisk_loc:
|
||||
pass
|
||||
elif is_chest_loc:
|
||||
chest_slots.append(loc.id)
|
||||
else:
|
||||
item_slots.append(loc.id)
|
||||
continue
|
||||
|
||||
item_id = scouted.item
|
||||
item_player = scouted.player
|
||||
item_data = items_by_id.get(item_id, ItemData())
|
||||
|
||||
if is_obelisk_loc:
|
||||
if "Obelisk" in item_data.item_name and item_player == self.slot:
|
||||
|
||||
pass
|
||||
else:
|
||||
|
||||
appended_items.append(loc.id)
|
||||
continue
|
||||
|
||||
if item_player == self.slot and item_id in spawner_trap_ids:
|
||||
continue
|
||||
|
||||
if item_player != self.slot:
|
||||
|
||||
if is_chest_loc:
|
||||
chest_slots.append(loc.id)
|
||||
else:
|
||||
item_slots.append(loc.id)
|
||||
continue
|
||||
|
||||
if "Obelisk" in item_data.item_name:
|
||||
continue
|
||||
|
||||
if (item_data.progression in (ItemClassification.useful, ItemClassification.progression)
|
||||
and is_chest_loc):
|
||||
appended_items.append(loc.id)
|
||||
continue
|
||||
|
||||
if is_chest_loc:
|
||||
chest_slots.append(loc.id)
|
||||
else:
|
||||
item_slots.append(loc.id)
|
||||
|
||||
final_item_order = item_slots + appended_items
|
||||
|
||||
self.item_ram_indices = {
|
||||
loc_id: idx for idx, loc_id in enumerate(final_item_order)
|
||||
}
|
||||
self.chest_ram_indices = {
|
||||
loc_id: idx for idx, loc_id in enumerate(chest_slots)
|
||||
}
|
||||
|
||||
# Categorize scouted locations - mirror ROM's patch_items logic exactly
|
||||
for loc in raw_locations:
|
||||
scouted_item = scouted_by_location.get(loc.id)
|
||||
if not scouted_item:
|
||||
continue
|
||||
|
||||
item_id = scouted_item.item
|
||||
item_player = scouted_item.player
|
||||
item_data = items_by_id.get(item_id, ItemData())
|
||||
is_chest = "Chest" in loc.name or ("Barrel" in loc.name and "Barrel of Gold" not in loc.name)
|
||||
|
||||
# Check obelisk locations
|
||||
if "Obelisk" in loc.name:
|
||||
if "Obelisk" in item_data.item_name and item_player == self.slot:
|
||||
self.obelisk_locations.append(loc.id)
|
||||
self.obelisks.append(scouted_item)
|
||||
else:
|
||||
self.item_locations.append(loc.id)
|
||||
continue
|
||||
|
||||
# Check spawner (ROM: item[1] == player and item[0] in SPAWNER_TRAP_IDS)
|
||||
if item_player == self.slot and item_id in spawner_trap_ids:
|
||||
self.spawner_locations.append(loc.id)
|
||||
continue
|
||||
|
||||
# Check non-local player (ROM: item[1] != player) - stays as item/chest
|
||||
if item_player != self.slot:
|
||||
if is_chest:
|
||||
self.chest_locations.append(loc.id)
|
||||
else:
|
||||
self.item_locations.append(loc.id)
|
||||
continue
|
||||
|
||||
# Check obelisk item at non-obelisk location (ROM: "Obelisk" in item_name)
|
||||
if "Obelisk" in item_data.item_name:
|
||||
self.obelisk_locations.append(loc.id)
|
||||
self.obelisks.append(scouted_item)
|
||||
continue
|
||||
|
||||
# Check useful/progression chest -> item conversion
|
||||
if item_data.progression in (ItemClassification.useful, ItemClassification.progression) and is_chest:
|
||||
self.item_locations.append(loc.id)
|
||||
self.useful.append(scouted_item)
|
||||
continue
|
||||
|
||||
# Regular item/chest
|
||||
if is_chest:
|
||||
self.chest_locations.append(loc.id)
|
||||
else:
|
||||
self.item_locations.append(loc.id)
|
||||
|
||||
self.scouted = True
|
||||
except Exception:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def location_loop(self) -> list[int]:
|
||||
if self.current_zone == -1:
|
||||
self.current_zone = self.zone
|
||||
self.current_level = self.level
|
||||
self.level_id = (self.current_zone << 4) + self.current_level
|
||||
|
||||
zone_or_level_changed = self.zone != self.current_zone or self.level != self.current_level
|
||||
if zone_or_level_changed:
|
||||
if self.current_level & 0x8 == 0x8 and self.level_id != 0x58 and not await self.dead_or_menu():
|
||||
await self.check_locations([loc.id for loc in level_locations[self.level_id]
|
||||
if "Mirror Shard" in loc.name or "Skorne" in loc.name])
|
||||
self.current_zone = self.zone
|
||||
self.current_level = self.level
|
||||
self.level_id = (self.current_zone << 4) + self.current_level
|
||||
self.scouted = False
|
||||
self.spawned_traps = 0
|
||||
self.goal_streak = 0
|
||||
await asyncio.sleep(2)
|
||||
|
||||
if self.current_zone in (0x8, 0xE):
|
||||
return []
|
||||
|
||||
if not self.scouted:
|
||||
await self.scout_locations(self)
|
||||
|
||||
active = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (self.players[0] - 1)), 1)
|
||||
if active != 0x4:
|
||||
return []
|
||||
|
||||
if len(self.queued_traps) > 0 and self.spawned_traps == 0:
|
||||
for i, trap in enumerate(self.queued_traps):
|
||||
trap_name, index, given = trap
|
||||
if not given:
|
||||
self.spawned_traps += 1
|
||||
await self.give_item(trap_name, self.players[0])
|
||||
self.queued_traps[i] = (trap_name, index, True)
|
||||
if self.spawned_traps >= 5:
|
||||
break
|
||||
|
||||
locations_address = await self._read_ram_int(LOCATIONS_BASE_ADDRESS, 4) & 0xFFFFFF
|
||||
self.item_address = await self._read_ram_int(locations_address + 0x14, 4)
|
||||
self.spawner_address = await self._read_ram_int(locations_address + 0x1C, 4)
|
||||
self.chest_address = await self._read_ram_int(locations_address + 0x30, 4)
|
||||
|
||||
# Read vanilla spawner count from level header (2 bytes at offset 0x28)
|
||||
spawner_count_data = await self._read_ram(locations_address + 0x6, 2)
|
||||
if spawner_count_data and not self.vanilla_spawner_count:
|
||||
total_spawner_count = int.from_bytes(spawner_count_data, "little")
|
||||
# Vanilla count is total minus the ones we added
|
||||
self.vanilla_spawner_count = total_spawner_count - len(self.spawner_locations)
|
||||
|
||||
if 0x7FFF0BAD in (self.item_address, self.chest_address, self.spawner_address):
|
||||
return []
|
||||
|
||||
self.item_address &= 0xFFFFFF
|
||||
self.spawner_address &= 0xFFFFFF
|
||||
self.chest_address &= 0xFFFFFF
|
||||
|
||||
acquired = []
|
||||
|
||||
max_item_idx = max((self.item_ram_indices[loc_id] for loc_id in self.item_locations), default=-1)
|
||||
item_section = await self._read_ram(self.item_address, (max_item_idx + 1) * 0x18) if max_item_idx >= 0 else b""
|
||||
for loc_id in self.item_locations:
|
||||
offset = self.item_ram_indices[loc_id] * 0x18
|
||||
active_byte, state = item_section[offset + 0x2], item_section[offset + 0x3]
|
||||
if state < 0x7F and active_byte == 1 and state == 0:
|
||||
acquired.append(loc_id)
|
||||
|
||||
obelisk = await self._read_ram_int(MOD_OBELISK_QUANTITY, 1)
|
||||
for j, loc_id in enumerate(self.obelisk_locations):
|
||||
bit = base_count[items_by_id[self.obelisks[j].item].item_name] - 1
|
||||
if obelisk & (1 << bit):
|
||||
acquired.append(loc_id)
|
||||
|
||||
max_chest_idx = max((self.chest_ram_indices[loc_id] for loc_id in self.chest_locations), default=-1)
|
||||
chest_section = await self._read_ram(self.chest_address, (max_chest_idx + 1) * 0x18) if max_chest_idx >= 0 else b""
|
||||
for loc_id in self.chest_locations:
|
||||
offset = self.chest_ram_indices[loc_id] * 0x18
|
||||
active_byte, state = chest_section[offset + 0x2], chest_section[offset + 0x3]
|
||||
if state < 0x7F and active_byte == 1 and state != 1:
|
||||
acquired.append(loc_id)
|
||||
|
||||
# Check spawner locations - these are added after vanilla spawners
|
||||
# Read spawners starting after vanilla_spawner_count
|
||||
if self.spawner_locations:
|
||||
spawner_start = self.spawner_address + (self.vanilla_spawner_count * 0x1C)
|
||||
spawner_section = await self._read_ram(spawner_start, len(self.spawner_locations) * 0x1C)
|
||||
for i, loc_id in enumerate(self.spawner_locations):
|
||||
offset = i * 0x1C
|
||||
active_byte, hit = spawner_section[offset + 0x2], spawner_section[offset + 0x1A]
|
||||
if active_byte == 1 and hit == 1:
|
||||
acquired.append(loc_id)
|
||||
|
||||
await self.update_stage()
|
||||
if self.zone != self.current_zone or self.level != self.current_level:
|
||||
return []
|
||||
return [loc_id for loc_id in acquired if loc_id not in self.checked_locations]
|
||||
|
||||
async def die(self):
|
||||
"""Trigger deathlink death with character-specific death sound."""
|
||||
self.deathlink_triggered = True
|
||||
for player in self.players:
|
||||
char = await self._read_ram_int(PLAYER_CLASS + (0x1F0 * (player - 1)), 1)
|
||||
color = await self._read_ram_int(PLAYER_COLOR + (0x1F0 * (player - 1)), 1)
|
||||
|
||||
# Play death sound
|
||||
sound_data = (int.to_bytes(colors[color], 4, "little") +
|
||||
int.to_bytes(sounds[char], 4, "little") +
|
||||
int.to_bytes(0xBB, 4, "little"))
|
||||
await self._write_ram(SOUND_ADDRESS, sound_data)
|
||||
await self._write_ram(SOUND_START, int.to_bytes(0xE00AE718, 4, "little"))
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Stop sound and kill player
|
||||
await self._write_ram(SOUND_START, int.to_bytes(0x0, 4, "little"))
|
||||
await self._write_ram(PLAYER_KILL + (0x1F0 * (player - 1)), int.to_bytes(0x7, 1, "little"))
|
||||
|
||||
def make_gui(self):
|
||||
ui = super().make_gui()
|
||||
ui.base_title = "Archipelago Gauntlet Legends Client"
|
||||
return ui
|
||||
|
||||
|
||||
async def gl_sync_task(ctx: GauntletLegendsContext):
|
||||
logger.info("Starting N64 connector...")
|
||||
while not ctx.exit_event.is_set():
|
||||
try:
|
||||
if not ctx.retro_connected:
|
||||
logger.info("Attempting to connect to Retroarch...")
|
||||
status = await ctx.socket.status()
|
||||
ctx.retro_connected = True
|
||||
ctx.rom_loaded = "CONTENTLESS" not in status
|
||||
logger.info("Connected to Retroarch")
|
||||
continue
|
||||
|
||||
if not ctx.rom_loaded:
|
||||
status = await ctx.socket.status()
|
||||
if "CONTENTLESS" in status:
|
||||
logger.info("No ROM loaded, waiting...")
|
||||
await asyncio.sleep(3)
|
||||
continue
|
||||
logger.info("ROM Loaded")
|
||||
ctx.rom_loaded = True
|
||||
|
||||
if not ctx.auth:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
seed_name = await ctx.get_seed_name()
|
||||
if seed_name != ctx.seed_name[0:16]:
|
||||
logger.info(f"ROM seed does not match room seed ({seed_name} != {ctx.seed_name}), "
|
||||
f"please load the correct ROM.")
|
||||
await ctx.disconnect()
|
||||
continue
|
||||
|
||||
await ctx.update_stage()
|
||||
if ctx.zone == 0x10:
|
||||
continue
|
||||
|
||||
await ctx.handle_items()
|
||||
checking = await ctx.location_loop()
|
||||
|
||||
menu = await ctx._read_ram_int(PLAYER_MENU, 4)
|
||||
alive = False
|
||||
if ctx.deathlink_pending and ctx.deathlink_enabled and menu == 1:
|
||||
ctx.deathlink_pending = False
|
||||
ctx.deathlink_triggered = True
|
||||
await ctx.die()
|
||||
elif len(ctx.players) > 0:
|
||||
player_state = await ctx._read_ram_int(PLAYER_KILL + (0x1F0 * (ctx.players[0] - 1)), 1)
|
||||
dead = (player_state & 0xF) == 0x8
|
||||
alive = (player_state & 0xF) == 0x4
|
||||
if dead and ctx.deathlink_enabled and not ctx.deathlink_triggered:
|
||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} ran out of food.")
|
||||
ctx.deathlink_triggered = True
|
||||
|
||||
if alive:
|
||||
ctx.deathlink_triggered = False
|
||||
|
||||
if checking:
|
||||
ctx.locations_checked += checking
|
||||
await ctx.check_locations(checking)
|
||||
|
||||
# Only trust the goal check when the player is alive
|
||||
if alive:
|
||||
goal = await ctx.check_goal()
|
||||
if not ctx.finished_game and goal:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
else:
|
||||
ctx.goal_streak = 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}\n{traceback.format_exc()}")
|
||||
ctx.socket = RetroSocket()
|
||||
ctx.retro_connected = False
|
||||
await asyncio.sleep(2)
|
||||
|
||||
|
||||
_original_opt_content: dict[str, str | None] = {}
|
||||
|
||||
|
||||
async def _patch_opt():
|
||||
"""Create RetroArch core options override for CountPerOp=1."""
|
||||
retroarch_path = settings.get_settings().gl_options.retroarch_path
|
||||
override_dir = os.path.join(retroarch_path, "config", "Mupen64Plus-Next")
|
||||
os.makedirs(override_dir, exist_ok=True)
|
||||
override_path = os.path.join(override_dir, "Mupen64Plus-Next.opt")
|
||||
target_setting = 'mupen64plus-CountPerOp = "1"'
|
||||
|
||||
if override_path not in _original_opt_content:
|
||||
_original_opt_content[override_path] = open(override_path).read() if os.path.exists(override_path) else None
|
||||
|
||||
content = _original_opt_content[override_path] or ""
|
||||
if target_setting in content:
|
||||
return
|
||||
|
||||
if "mupen64plus-CountPerOp" in content:
|
||||
content = re.sub(r'mupen64plus-CountPerOp\s*=\s*"[^"]*"', target_setting, content)
|
||||
else:
|
||||
content = content.rstrip("\n") + f"\n{target_setting}\n" if content else f"{target_setting}\n"
|
||||
|
||||
with open(override_path, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def _restore_opt_files():
|
||||
for path, original in _original_opt_content.items():
|
||||
try:
|
||||
if original is None and os.path.exists(path):
|
||||
os.remove(path)
|
||||
elif original is not None:
|
||||
with open(path, "w") as f:
|
||||
f.write(original)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to restore {path}: {e}")
|
||||
_original_opt_content.clear()
|
||||
|
||||
|
||||
async def _launch_retroarch(rom_path: str):
|
||||
retroarch_path = settings.get_settings().gl_options.retroarch_path
|
||||
retroarch_exe = os.path.join(retroarch_path, "retroarch.exe")
|
||||
core_path = os.path.join(retroarch_path, "cores", "mupen64plus_next_libretro.dll")
|
||||
|
||||
if not os.path.exists(retroarch_exe):
|
||||
logger.error(f"RetroArch not found at: {retroarch_exe}")
|
||||
return
|
||||
|
||||
if not os.path.exists(core_path):
|
||||
logger.error(f"Mupen64Plus core not found at: {core_path}")
|
||||
return
|
||||
|
||||
subprocess.Popen([retroarch_exe, "-L", core_path, rom_path])
|
||||
logger.info(f"Launched RetroArch with ROM: {rom_path}")
|
||||
|
||||
# Wait for RetroArch to start up
|
||||
await asyncio.sleep(2)
|
||||
|
||||
|
||||
async def _patch_and_launch_game(patch_file: str):
|
||||
metadata, output_file = Patch.create_rom_file(patch_file)
|
||||
await _patch_opt()
|
||||
await _launch_retroarch(output_file)
|
||||
|
||||
|
||||
def launch(*args):
|
||||
async def main(args):
|
||||
if args.patch_file:
|
||||
await asyncio.create_task(_patch_and_launch_game(args.patch_file))
|
||||
ctx = GauntletLegendsContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
ctx.gl_sync_task = asyncio.create_task(gl_sync_task(ctx), name="Gauntlet Legends Sync Task")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
_restore_opt_files()
|
||||
|
||||
parser = get_base_parser()
|
||||
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an APGL file")
|
||||
args = parser.parse_args(args)
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
@@ -0,0 +1,43 @@
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .Data import portals, obelisks, mirror_shards
|
||||
|
||||
|
||||
class ItemData:
|
||||
id: int
|
||||
item_name: str
|
||||
progression: ItemClassification
|
||||
rom_id: int
|
||||
frequency: int
|
||||
|
||||
def __init__(self, id: int | None = 0, item_name: str = "", progression: int = 0, rom_id: str = "0x0", frequency: int = 1):
|
||||
self.id = id
|
||||
self.item_name = item_name
|
||||
self.progression = ItemClassification(progression)
|
||||
self.rom_id = int(rom_id, 16)
|
||||
self.frequency = frequency
|
||||
|
||||
|
||||
|
||||
class GLItem(Item):
|
||||
game: str = "Gauntlet Legends"
|
||||
|
||||
def import_items() -> tuple[ItemData, ...]:
|
||||
import orjson
|
||||
import pkgutil
|
||||
|
||||
return tuple(ItemData(**item) for item in orjson.loads(pkgutil.get_data(__name__, "json/items.json").decode("utf-8")))
|
||||
|
||||
item_list: Final[tuple[ItemData, ...]] = import_items()
|
||||
item_table: Final[Mapping[str, ItemData]] = MappingProxyType({item.item_name: item for item in item_list})
|
||||
items_by_id: Final[Mapping[int, ItemData]] = MappingProxyType({item.id: item for item in item_list})
|
||||
|
||||
gauntlet_item_name_groups = {
|
||||
"Runestone": [f"Runestone {i}" for i in range(1, 14)],
|
||||
"Portal": portals.keys(),
|
||||
"Obelisk": obelisks,
|
||||
"Mirror Shard": mirror_shards
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user