Compare commits

..

1 Commits

Author SHA1 Message Date
NewSoupVi
5e8af01cca Update options.py 2024-11-24 21:39:53 +01:00
450 changed files with 7104 additions and 24595 deletions

1
.gitattributes vendored
View File

@@ -1,2 +1 @@
worlds/blasphemous/region_data.py linguist-generated=true
worlds/yachtdice/YachtWeights.py linguist-generated=true

View File

@@ -16,7 +16,7 @@
"reportMissingImports": true,
"reportMissingTypeStubs": true,
"pythonVersion": "3.10",
"pythonVersion": "3.8",
"pythonPlatform": "Windows",
"executionEnvironments": [

View File

@@ -53,7 +53,7 @@ jobs:
- uses: actions/setup-python@v5
if: env.diff != ''
with:
python-version: '3.10'
python-version: 3.8
- name: "Install dependencies"
if: env.diff != ''

View File

@@ -24,15 +24,14 @@ env:
jobs:
# build-release-macos: # LF volunteer
build-win: # RCs will still be built and signed by hand
build-win-py38: # RCs will still be built and signed by hand
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install python
uses: actions/setup-python@v5
with:
python-version: '~3.12.7'
check-latest: true
python-version: '3.8'
- name: Download run-time dependencies
run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
@@ -112,11 +111,10 @@ jobs:
- name: Get a recent python
uses: actions/setup-python@v5
with:
python-version: '~3.12.7'
check-latest: true
python-version: '3.11'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.12" >> $GITHUB_ENV
echo "PYTHON=python3.11" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract

View File

@@ -44,11 +44,10 @@ jobs:
- name: Get a recent python
uses: actions/setup-python@v5
with:
python-version: '~3.12.7'
check-latest: true
python-version: '3.11'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.12" >> $GITHUB_ENV
echo "PYTHON=python3.11" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract

View File

@@ -40,10 +40,10 @@ jobs:
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x ./llvm.sh
sudo ./llvm.sh 19
sudo ./llvm.sh 17
- name: Install scan-build command
run: |
sudo apt install clang-tools-19
sudo apt install clang-tools-17
- name: Get a recent python
uses: actions/setup-python@v5
with:
@@ -56,7 +56,7 @@ jobs:
- name: scan-build
run: |
source venv/bin/activate
scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
- name: Store report
if: failure()
uses: actions/upload-artifact@v4

View File

@@ -33,11 +33,13 @@ jobs:
matrix:
os: [ubuntu-latest]
python:
- {version: '3.8'}
- {version: '3.9'}
- {version: '3.10'}
- {version: '3.11'}
- {version: '3.12'}
include:
- python: {version: '3.10'} # old compat
- python: {version: '3.8'} # win7 compat
os: windows-latest
- python: {version: '3.12'} # current
os: windows-latest

View File

@@ -1,16 +1,18 @@
from __future__ import annotations
import collections
import itertools
import functools
import logging
import random
import secrets
import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
Optional, Protocol, Set, Tuple, Union, Type)
from typing_extensions import NotRequired, TypedDict
@@ -18,8 +20,7 @@ import NetUtils
import Options
import Utils
if TYPE_CHECKING:
from entrance_rando import ERPlacementState
if typing.TYPE_CHECKING:
from worlds import AutoWorld
@@ -230,7 +231,7 @@ class MultiWorld():
for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints})
@@ -427,12 +428,12 @@ class MultiWorld():
def get_location(self, location_name: str, player: int) -> Location:
return self.regions.location_cache[player][location_name]
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False) -> CollectionState:
def get_all_state(self, use_cache: bool) -> CollectionState:
cached = getattr(self, "_all_state", None)
if use_cache and cached:
return cached.copy()
ret = CollectionState(self, allow_partial_entrances)
ret = CollectionState(self)
for item in self.itempool:
self.worlds[item.player].collect(ret, item)
@@ -605,49 +606,6 @@ class MultiWorld():
state.collect(location.item, True, location)
locations -= sphere
def get_sendable_spheres(self) -> Iterator[Set[Location]]:
"""
yields a set of multiserver sendable locations (location.item.code: int) for each logical sphere
If there are unreachable locations, the last sphere of reachable locations is followed by an empty set,
and then a set of all of the unreachable locations.
"""
state = CollectionState(self)
locations: Set[Location] = set()
events: Set[Location] = set()
for location in self.get_filled_locations():
if type(location.item.code) is int:
locations.add(location)
else:
events.add(location)
while locations:
sphere: Set[Location] = set()
# cull events out
done_events: Set[Union[Location, None]] = {None}
while done_events:
done_events = set()
for event in events:
if event.can_reach(state):
state.collect(event.item, True, event)
done_events.add(event)
events -= done_events
for location in locations:
if location.can_reach(state):
sphere.add(location)
yield sphere
if not sphere:
if locations:
yield locations # unreachable locations
break
for location in sphere:
state.collect(location.item, True, location)
locations -= sphere
def fulfills_accessibility(self, state: Optional[CollectionState] = None):
"""Check if accessibility rules are fulfilled with current or supplied state."""
if not state:
@@ -718,11 +676,10 @@ class CollectionState():
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
stale: Dict[int, bool]
allow_partial_entrances: bool
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = []
def __init__(self, parent: MultiWorld, allow_partial_entrances: bool = False):
def __init__(self, parent: MultiWorld):
self.prog_items = {player: Counter() for player in parent.get_all_ids()}
self.multiworld = parent
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
@@ -731,7 +688,6 @@ class CollectionState():
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in parent.get_all_ids()}
self.allow_partial_entrances = allow_partial_entrances
for function in self.additional_init_functions:
function(self, parent)
for items in parent.precollected_items.values():
@@ -766,8 +722,6 @@ class CollectionState():
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
if self.allow_partial_entrances and not new_region:
continue
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
@@ -793,9 +747,7 @@ class CollectionState():
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
if self.allow_partial_entrances and not new_region:
continue
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
@@ -815,7 +767,6 @@ class CollectionState():
ret.advancements = self.advancements.copy()
ret.path = self.path.copy()
ret.locations_checked = self.locations_checked.copy()
ret.allow_partial_entrances = self.allow_partial_entrances
for function in self.additional_copy_functions:
ret = function(self, ret)
return ret
@@ -980,11 +931,6 @@ class CollectionState():
self.stale[item.player] = True
class EntranceType(IntEnum):
ONE_WAY = 1
TWO_WAY = 2
class Entrance:
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
hide_path: bool = False
@@ -992,24 +938,19 @@ class Entrance:
name: str
parent_region: Optional[Region]
connected_region: Optional[Region] = None
randomization_group: int
randomization_type: EntranceType
# LttP specific, TODO: should make a LttPEntrance
addresses = None
target = None
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None,
randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None:
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
self.name = name
self.parent_region = parent
self.player = player
self.randomization_group = randomization_group
self.randomization_type = randomization_type
def can_reach(self, state: CollectionState) -> bool:
assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and self not in state.path:
if not self.hide_path and not self in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
return True
@@ -1021,32 +962,6 @@ class Entrance:
self.addresses = addresses
region.entrances.append(self)
def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool:
"""
Determines whether this is a valid source transition, that is, whether the entrance
randomizer is allowed to pair it to place any other regions. By default, this is the
same as a reachability check, but can be modified by Entrance implementations to add
other restrictions based on the placement state.
:param er_state: The current (partial) state of the ongoing entrance randomization
"""
return self.can_reach(er_state.collection_state)
def can_connect_to(self, other: Entrance, dead_end: bool, er_state: "ERPlacementState") -> bool:
"""
Determines whether a given Entrance is a valid target transition, that is, whether
the entrance randomizer is allowed to pair this Entrance to that Entrance. By default,
only allows connection between entrances of the same type (one ways only go to one ways,
two ways always go to two ways) and prevents connecting an exit to itself in coupled mode.
:param other: The proposed Entrance to connect to
:param dead_end: Whether the other entrance considered a dead end by Entrance randomization
:param er_state: The current (partial) state of the ongoing entrance randomization
"""
# the implementation of coupled causes issues for self-loops since the reverse entrance will be the
# same as the forward entrance. In uncoupled they are ok.
return self.randomization_type == other.randomization_type and (not er_state.coupled or self.name != other.name)
def __repr__(self):
multiworld = self.parent_region.multiworld if self.parent_region else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
@@ -1060,7 +975,7 @@ class Region:
entrances: List[Entrance]
exits: List[Entrance]
locations: List[Location]
entrance_type: ClassVar[type[Entrance]] = Entrance
entrance_type: ClassVar[Type[Entrance]] = Entrance
class Register(MutableSequence):
region_manager: MultiWorld.RegionManager
@@ -1160,7 +1075,7 @@ class Region:
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def add_locations(self, locations: Dict[str, Optional[int]],
location_type: Optional[type[Location]] = None) -> None:
location_type: Optional[Type[Location]] = None) -> None:
"""
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address.
@@ -1196,18 +1111,8 @@ class Region:
self.exits.append(exit_)
return exit_
def create_er_target(self, name: str) -> Entrance:
"""
Creates and returns an Entrance object as an entrance to this region
:param name: name of the Entrance being created
"""
entrance = self.entrance_type(self.player, name)
entrance.connect(self)
return entrance
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
"""
Connects current region to regions in exit dictionary. Passed region names must exist first.
@@ -1217,14 +1122,10 @@ class Region:
"""
if not isinstance(exits, Dict):
exits = dict.fromkeys(exits)
return [
self.connect(
self.multiworld.get_region(connecting_region, self.player),
name,
rules[connecting_region] if rules and connecting_region in rules else None,
)
for connecting_region, name in exits.items()
]
for connecting_region, name in exits.items():
self.connect(self.multiworld.get_region(connecting_region, self.player),
name,
rules[connecting_region] if rules and connecting_region in rules else None)
def __repr__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
@@ -1308,26 +1209,13 @@ class Location:
class ItemClassification(IntFlag):
filler = 0b0000
""" aka trash, as in filler items like ammo, currency etc """
progression = 0b0001
""" Item that is logically relevant.
Protects this item from being placed on excluded or unreachable locations. """
useful = 0b0010
""" Item that is especially useful.
Protects this item from being placed on excluded or unreachable locations.
When combined with another flag like "progression", it means "an especially useful progression item". """
trap = 0b0100
""" Item that is detrimental in some way. """
skip_balancing = 0b1000
""" should technically never occur on its own
Item that is logically relevant, but progression balancing should not touch.
Typically currency or other counted items. """
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
progression = 0b0001 # Item that is logically relevant
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
trap = 0b0100 # detrimental item
skip_balancing = 0b1000 # should technically never occur on its own
# Item that is logically relevant, but progression balancing should not touch.
# Typically currency or other counted items.
progression_skip_balancing = 0b1001 # only progression gets balanced
def as_flag(self) -> int:
@@ -1376,10 +1264,6 @@ class Item:
def trap(self) -> bool:
return ItemClassification.trap in self.classification
@property
def filler(self) -> bool:
return not (self.advancement or self.useful or self.trap)
@property
def excludable(self) -> bool:
return not (self.advancement or self.useful)
@@ -1502,21 +1386,14 @@ class Spoiler:
# second phase, sphere 0
removed_precollected: List[Item] = []
for precollected_items in multiworld.precollected_items.values():
# The list of items is mutated by removing one item at a time to determine if each item is required to beat
# the game, and re-adding that item if it was required, so a copy needs to be made before iterating.
for item in precollected_items.copy():
if not item.advancement:
continue
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
precollected_items.remove(item)
multiworld.state.remove(item)
if not multiworld.can_beat_game():
# Add the item back into `precollected_items` and collect it into `multiworld.state`.
multiworld.push_precollected(item)
else:
removed_precollected.append(item)
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
multiworld.precollected_items[item.player].remove(item)
multiworld.state.remove(item)
if not multiworld.can_beat_game():
multiworld.push_precollected(item)
else:
removed_precollected.append(item)
# we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others
@@ -1655,7 +1532,7 @@ class Spoiler:
[f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else
[f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
outfile.write('\n\nUnreachable Progression Items:\n\n')
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write(
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))

View File

@@ -23,7 +23,7 @@ if __name__ == "__main__":
from MultiServer import CommandProcessor
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType)
from Utils import Version, stream_input, async_start
from worlds import network_data_package, AutoWorldRegister
import os
@@ -412,7 +412,6 @@ class CommonContext:
await self.server.socket.close()
if self.server_task is not None:
await self.server_task
self.ui.update_hints()
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
""" `msgs` JSON serializable """
@@ -552,14 +551,7 @@ class CommonContext:
await self.ui_task
if self.input_task:
self.input_task.cancel()
# Hints
def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None:
msg = {"cmd": "UpdateHint", "location": location, "player": finding_player}
if status is not None:
msg["status"] = status
async_start(self.send_msgs([msg]), name="update_hint")
# DataPackage
async def prepare_data_package(self, relevant_games: typing.Set[str],
remote_date_package_versions: typing.Dict[str, int],

82
Fill.py
View File

@@ -36,8 +36,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True,
name: str = "Unknown") -> None:
allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None:
"""
:param multiworld: Multiworld to be filled.
:param base_state: State assumed before fill.
@@ -64,22 +63,14 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
placed = 0
while any(reachable_items.values()) and locations:
if one_item_per_player:
# grab one item per player
items_to_place = [items.pop()
for items in reachable_items.values() if items]
else:
next_player = multiworld.random.choice([player for player, items in reachable_items.items() if items])
items_to_place = []
if item_pool:
items_to_place.append(reachable_items[next_player].pop())
# grab one item per player
items_to_place = [items.pop()
for items in reachable_items.values() if items]
for item in items_to_place:
for p, pool_item in enumerate(item_pool):
if pool_item is item:
item_pool.pop(p)
break
maximum_exploration_state = sweep_from_pool(
base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player)
if single_player_placement else None)
@@ -235,30 +226,18 @@ def remaining_fill(multiworld: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item],
name: str = "Remaining",
move_unplaceable_to_start_inventory: bool = False,
check_location_can_fill: bool = False) -> None:
move_unplaceable_to_start_inventory: bool = False) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
total = min(len(itempool), len(locations))
placed = 0
# Optimisation: Decide whether to do full location.can_fill check (respect excluded), or only check the item rule
if check_location_can_fill:
state = CollectionState(multiworld)
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
return location_to_fill.can_fill(state, item_to_fill, check_access=False)
else:
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
return location_to_fill.item_rule(item_to_fill)
while locations and itempool:
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None
for i, location in enumerate(locations):
if location_can_fill_item(location, item_to_place):
if location.item_rule(item_to_place):
# popping by index is faster than removing by content,
spot_to_fill = locations.pop(i)
# skipping a scan for the element
@@ -279,7 +258,7 @@ def remaining_fill(multiworld: MultiWorld,
location.item = None
placed_item.location = None
if location_can_fill_item(location, item_to_place):
if location.item_rule(item_to_place):
# Add this item to the existing placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
@@ -501,8 +480,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if prioritylocations:
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority", one_item_per_player=False)
single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations
@@ -531,8 +509,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if progitempool:
raise FillError(
f"Not enough locations for progression items. "
f"There are {len(progitempool)} more progression items than there are available locations.\n"
f"Unfilled locations:\n{multiworld.get_unfilled_locations()}.",
f"There are {len(progitempool)} more progression items than there are available locations.",
multiworld=multiworld,
)
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
@@ -550,7 +527,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. "
f"There are {len(excludedlocations)} more excluded locations than excludable items.",
f"There are {len(excludedlocations)} more excluded locations than filler or trap items.",
multiworld=multiworld,
)
@@ -1001,32 +978,15 @@ def distribute_planned(multiworld: MultiWorld) -> None:
multiworld.random.shuffle(items)
count = 0
err: typing.List[str] = []
successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = []
claimed_indices: typing.Set[typing.Optional[int]] = set()
successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
for item_name in items:
index_to_delete: typing.Optional[int] = None
if from_pool:
try:
# If from_pool, try to find an existing item with this name & player in the itempool and use it
index_to_delete, item = next(
(i, item) for i, item in enumerate(multiworld.itempool)
if item.player == player and item.name == item_name and i not in claimed_indices
)
except StopIteration:
warn(
f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.",
placement['force'])
item = multiworld.worlds[player].create_item(item_name)
else:
item = multiworld.worlds[player].create_item(item_name)
item = multiworld.worlds[player].create_item(item_name)
for location in reversed(candidates):
if (location.address is None) == (item.code is None): # either both None or both not None
if not location.item:
if location.item_rule(item):
if location.can_fill(multiworld.state, item, False):
successful_pairs.append((index_to_delete, item, location))
claimed_indices.add(index_to_delete)
successful_pairs.append((item, location))
candidates.remove(location)
count = count + 1
break
@@ -1038,7 +998,6 @@ def distribute_planned(multiworld: MultiWorld) -> None:
err.append(f"Cannot place {item_name} into already filled location {location}.")
else:
err.append(f"Mismatch between {item_name} and {location}, only one is an event.")
if count == maxcount:
break
if count < placement['count']['min']:
@@ -1046,16 +1005,17 @@ def distribute_planned(multiworld: MultiWorld) -> None:
failed(
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
placement['force'])
# Sort indices in reverse so we can remove them one by one
successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True)
for (index, item, location) in successful_pairs:
for (item, location) in successful_pairs:
multiworld.push_item(location, item, collect=False)
location.locked = True
logging.debug(f"Plando placed {item} at {location}")
if index is not None: # If this item is from_pool and was found in the pool, remove it.
multiworld.itempool.pop(index)
if from_pool:
try:
multiworld.itempool.remove(item)
except ValueError:
warn(
f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.",
placement['force'])
except Exception as e:
raise Exception(

View File

@@ -114,14 +114,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
path = os.path.join(args.player_files_path, fname)
try:
weights_for_file = []
for doc_idx, yaml in enumerate(read_weights_yamls(path)):
if yaml is None:
logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}")
else:
weights_for_file.append(yaml)
weights_cache[fname] = tuple(weights_for_file)
weights_cache[fname] = read_weights_yamls(path)
except Exception as e:
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
@@ -500,8 +493,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
for option_key in game_weights:
if option_key in {"triggers", *valid_keys}:
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers "
f"for player {ret.name}.")
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
if PlandoOptions.items in plando_options:
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":

View File

@@ -1,7 +1,7 @@
MIT License
Copyright (c) 2017 LLCoolDave
Copyright (c) 2025 Berserker66
Copyright (c) 2022 Berserker66
Copyright (c) 2022 CaitSith2
Copyright (c) 2021 LegendaryLinux

View File

@@ -126,13 +126,12 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
elif component.display_name == "Text Client":
text_client_component = component
if client_component is None:
run_component(text_client_component, *launch_args)
return
from kvui import App, Button, BoxLayout, Label, Window
from kvui import App, Button, BoxLayout, Label, Clock, Window
class Popup(App):
timer_label: Label
remaining_time: Optional[int]
def __init__(self):
self.title = "Connect to Multiworld"
self.icon = r"data/icon.png"
@@ -140,25 +139,48 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
def build(self):
layout = BoxLayout(orientation="vertical")
layout.add_widget(Label(text="Select client to open and connect with."))
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
text_client_button = Button(
text=text_client_component.display_name,
on_release=lambda *args: run_component(text_client_component, *launch_args)
)
button_row.add_widget(text_client_button)
if client_component is None:
self.remaining_time = 7
label_text = (f"A game client able to parse URIs was not detected for {game}.\n"
f"Launching Text Client in 7 seconds...")
self.timer_label = Label(text=label_text)
layout.add_widget(self.timer_label)
Clock.schedule_interval(self.update_label, 1)
else:
layout.add_widget(Label(text="Select client to open and connect with."))
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
game_client_button = Button(
text=client_component.display_name,
on_release=lambda *args: run_component(client_component, *launch_args)
)
button_row.add_widget(game_client_button)
text_client_button = Button(
text=text_client_component.display_name,
on_release=lambda *args: run_component(text_client_component, *launch_args)
)
button_row.add_widget(text_client_button)
layout.add_widget(button_row)
game_client_button = Button(
text=client_component.display_name,
on_release=lambda *args: run_component(client_component, *launch_args)
)
button_row.add_widget(game_client_button)
layout.add_widget(button_row)
return layout
def update_label(self, dt):
if self.remaining_time > 1:
# countdown the timer and string replace the number
self.remaining_time -= 1
self.timer_label.text = self.timer_label.text.replace(
str(self.remaining_time + 1), str(self.remaining_time)
)
else:
# our timer is finished so launch text client and close down
run_component(text_client_component, *launch_args)
Clock.unschedule(self.update_label)
App.get_running_app().stop()
Window.close()
def _stop(self, *largs):
# see run_gui Launcher _stop comment for details
self.root_window.close()
@@ -224,8 +246,9 @@ refresh_components: Optional[Callable[[], None]] = None
def run_gui():
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
from kivy.core.window import Window
from kivy.uix.image import AsyncImage
from kivy.uix.relativelayout import RelativeLayout
class Launcher(App):
@@ -258,8 +281,8 @@ def run_gui():
button.component = component
button.bind(on_release=self.component_action)
if component.icon != "icon":
image = ApAsyncImage(source=icon_paths[component.icon],
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
image = AsyncImage(source=icon_paths[component.icon],
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
box_layout = RelativeLayout(size_hint_y=None, height=40)
box_layout.add_widget(button)
box_layout.add_widget(image)

View File

@@ -235,7 +235,7 @@ class RAGameboy():
def check_command_response(self, command: str, response: bytes):
if command == "VERSION":
ok = re.match(r"\d+\.\d+\.\d+", response.decode('ascii')) is not None
ok = re.match("\d+\.\d+\.\d+", response.decode('ascii')) is not None
else:
ok = response.startswith(command.encode())
if not ok:

84
Main.py
View File

@@ -153,38 +153,45 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible.
fallback_inventory = StartInventoryPool({})
depletion_pool: Dict[int, Dict[str, int]] = {
player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy()
for player in multiworld.player_ids
}
target_per_player = {
player: sum(target_items.values()) for player, target_items in depletion_pool.items() if target_items
}
if target_per_player:
new_itempool: List[Item] = []
# Make new itempool with start_inventory_from_pool items removed
for item in multiworld.itempool:
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
new_items: List[Item] = []
old_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: getattr(multiworld.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.copy()
for player in multiworld.player_ids
}
for player, items in depletion_pool.items():
player_world: AutoWorld.World = multiworld.worlds[player]
for count in items.values():
for _ in range(count):
new_items.append(player_world.create_filler())
target: int = sum(sum(items.values()) for items in depletion_pool.values())
for i, item in enumerate(multiworld.itempool):
if depletion_pool[item.player].get(item.name, 0):
target -= 1
depletion_pool[item.player][item.name] -= 1
# quick abort if we have found all items
if not target:
old_items.extend(multiworld.itempool[i+1:])
break
else:
new_itempool.append(item)
old_items.append(item)
# Create filler in place of the removed items, warn if any items couldn't be found in the multiworld itempool
for player, target in target_per_player.items():
unfound_items = {item: count for item, count in depletion_pool[player].items() if count}
if unfound_items:
player_name = multiworld.get_player_name(player)
logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}")
needed_items = target_per_player[player] - sum(unfound_items.values())
new_itempool += [multiworld.worlds[player].create_filler() for _ in range(needed_items)]
assert len(multiworld.itempool) == len(new_itempool), "Item Pool amounts should not change."
multiworld.itempool[:] = new_itempool
# leftovers?
if target:
for player, remaining_items in depletion_pool.items():
remaining_items = {name: count for name, count in remaining_items.items() if count}
if remaining_items:
logger.warning(f"{multiworld.get_player_name(player)}"
f" is trying to remove items from their pool that don't exist: {remaining_items}")
# find all filler we generated for the current player and remove until it matches
removables = [item for item in new_items if item.player == player]
for _ in range(sum(remaining_items.values())):
new_items.remove(removables.pop())
assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change."
multiworld.itempool[:] = new_items + old_items
multiworld.link_items()
@@ -242,7 +249,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
def write_multidata():
import NetUtils
from NetUtils import HintStatus
slot_data = {}
client_versions = {}
games = {}
@@ -267,10 +273,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for slot in multiworld.player_ids:
slot_data[slot] = multiworld.worlds[slot].fill_slot_data()
def precollect_hint(location: Location, auto_status: HintStatus):
def precollect_hint(location):
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False, entrance, location.item.flags, auto_status)
location.item.code, False, entrance, location.item.flags)
precollected_hints[location.player].add(hint)
if location.item.player not in multiworld.groups:
precollected_hints[location.item.player].add(hint)
@@ -283,22 +289,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if type(location.address) == int:
assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None. Location: " \
f" {location}, Item: {location.item}"
f" {location}"
assert location.address not in locations_data[location.player], (
f"Locations with duplicate address. {location} and "
f"{locations_data[location.player][location.address]}")
locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags
auto_status = HintStatus.HINT_AVOID if location.item.trap else HintStatus.HINT_PRIORITY
if location.name in multiworld.worlds[location.player].options.start_location_hints:
if not location.item.trap: # Unspecified status for location hints, except traps
auto_status = HintStatus.HINT_UNSPECIFIED
precollect_hint(location, auto_status)
precollect_hint(location)
elif location.item.name in multiworld.worlds[location.item.player].options.start_hints:
precollect_hint(location, auto_status)
precollect_hint(location)
elif any([location.item.name in multiworld.worlds[player].options.start_hints
for player in multiworld.groups.get(location.item.player, {}).get("players", [])]):
precollect_hint(location, auto_status)
precollect_hint(location)
# embedded data package
data_package = {
@@ -310,10 +313,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# get spheres -> filter address==None -> skip empty
spheres: List[Dict[int, Set[int]]] = []
for sphere in multiworld.get_sendable_spheres():
for sphere in multiworld.get_spheres():
current_sphere: Dict[int, Set[int]] = collections.defaultdict(set)
for sphere_location in sphere:
current_sphere[sphere_location.player].add(sphere_location.address)
if type(sphere_location.address) is int:
current_sphere[sphere_location.player].add(sphere_location.address)
if current_sphere:
spheres.append(dict(current_sphere))

View File

@@ -5,15 +5,8 @@ import multiprocessing
import warnings
if sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 11):
# Official micro version updates. This should match the number in docs/running from source.md.
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.10.15+ is supported.")
elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 15):
# There are known security issues, but no easy way to install fixed versions on Windows for testing.
warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.")
elif sys.version_info < (3, 10, 1):
# Other platforms may get security backports instead of micro updates, so the number is unreliable.
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.1+ is supported.")
if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())

View File

@@ -41,8 +41,7 @@ import NetUtils
import Utils
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
SlotType, LocationStore, Hint, HintStatus
from BaseClasses import ItemClassification
SlotType, LocationStore
min_client_version = Version(0, 1, 6)
colorama.init()
@@ -229,7 +228,7 @@ class Context:
self.hint_cost = hint_cost
self.location_check_points = location_check_points
self.hints_used = collections.defaultdict(int)
self.hints: typing.Dict[team_slot, typing.Set[Hint]] = collections.defaultdict(set)
self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set)
self.release_mode: str = release_mode
self.remaining_mode: str = remaining_mode
self.collect_mode: str = collect_mode
@@ -444,7 +443,7 @@ class Context:
self.slot_info = decoded_obj["slot_info"]
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
if slot_info.type == SlotType.group}
self.clients = {0: {}}
@@ -657,29 +656,13 @@ class Context:
return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot])))
return 0
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None,
changed: typing.Optional[typing.Set[team_slot]] = None) -> None:
"""Refreshes the hints for the specified team/slot. Providing 'None' for either team or slot
will refresh all teams or all slots respectively. If a set is passed for 'changed', each (team,slot)
pair that has at least one hint modified will be added to the set.
"""
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None):
for hint_team, hint_slot in self.hints:
if team != hint_team and team is not None:
continue # Check specified team only, all if team is None
if slot != hint_slot and slot is not None:
continue # Check specified slot only, all if slot is None
new_hints: typing.Set[Hint] = set()
for hint in self.hints[hint_team, hint_slot]:
new_hint = hint.re_check(self, hint_team)
new_hints.add(new_hint)
if hint == new_hint:
continue
for player in self.slot_set(hint.receiving_player) | {hint.finding_player}:
if changed is not None:
changed.add((hint_team,player))
if slot is not None and slot != player:
self.replace_hint(hint_team, player, hint, new_hint)
self.hints[hint_team, hint_slot] = new_hints
if (team is None or team == hint_team) and (slot is None or slot == hint_slot):
self.hints[hint_team, hint_slot] = {
hint.re_check(self, hint_team) for hint in
self.hints[hint_team, hint_slot]
}
def get_rechecked_hints(self, team: int, slot: int):
self.recheck_hints(team, slot)
@@ -728,7 +711,7 @@ class Context:
else:
return self.player_names[team, slot]
def notify_hints(self, team: int, hints: typing.List[Hint], only_new: bool = False,
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False,
recipients: typing.Sequence[int] = None):
"""Send and remember hints."""
if only_new:
@@ -766,17 +749,6 @@ class Context:
for client in clients:
async_start(self.send_msgs(client, client_hints))
def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]:
for hint in self.hints[team, finding_player]:
if hint.location == seeked_location:
return hint
return None
def replace_hint(self, team: int, slot: int, old_hint: Hint, new_hint: Hint) -> None:
if old_hint in self.hints[team, slot]:
self.hints[team, slot].remove(old_hint)
self.hints[team, slot].add(new_hint)
# "events"
def on_goal_achieved(self, client: Client):
@@ -975,13 +947,9 @@ def get_status_string(ctx: Context, team: int, tag: str):
tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags])
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else ""
status_text = (
" and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else
" and is ready." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_READY else
"."
)
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
f"{tag_text}{status_text} {completion_text}"
f"{tag_text}{goal_text} {completion_text}"
return text
@@ -1082,15 +1050,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
"hint_points": get_slot_points(ctx, team, slot),
"checked_locations": new_locations, # send back new checks only
}])
updated_slots: typing.Set[tuple[int, int]] = set()
ctx.recheck_hints(team, slot, updated_slots)
for hint_team, hint_slot in updated_slots:
ctx.on_changed_hints(hint_team, hint_slot)
old_hints = ctx.hints[team, slot].copy()
ctx.recheck_hints(team, slot)
if old_hints != ctx.hints[team, slot]:
ctx.on_changed_hints(team, slot)
ctx.save()
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str], auto_status: HintStatus) \
-> typing.List[Hint]:
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]:
hints = []
slots: typing.Set[int] = {slot}
for group_id, group in ctx.groups.items():
@@ -1100,58 +1067,31 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
for finding_player, location_id, item_id, receiving_player, item_flags \
in ctx.locations.find_item(slots, seeked_item_id):
prev_hint = ctx.get_hint(team, slot, location_id)
if prev_hint:
hints.append(prev_hint)
else:
found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
new_status = auto_status
if found:
new_status = HintStatus.HINT_FOUND
elif item_flags & ItemClassification.trap:
new_status = HintStatus.HINT_AVOID
hints.append(Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
item_flags, new_status))
found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
item_flags))
return hints
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str, auto_status: HintStatus) \
-> typing.List[Hint]:
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
return collect_hint_location_id(ctx, team, slot, seeked_location, auto_status)
return collect_hint_location_id(ctx, team, slot, seeked_location)
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int, auto_status: HintStatus) \
-> typing.List[Hint]:
prev_hint = ctx.get_hint(team, slot, seeked_location)
if prev_hint:
return [prev_hint]
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]:
result = ctx.locations[slot].get(seeked_location, (None, None, None))
if any(result):
item_id, receiving_player, item_flags = result
found = seeked_location in ctx.location_checks[team, slot]
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
new_status = auto_status
if found:
new_status = HintStatus.HINT_FOUND
elif item_flags & ItemClassification.trap:
new_status = HintStatus.HINT_AVOID
return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags,
new_status)]
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)]
return []
status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "(found)",
HintStatus.HINT_UNSPECIFIED: "(unspecified)",
HintStatus.HINT_NO_PRIORITY: "(no priority)",
HintStatus.HINT_AVOID: "(avoid)",
HintStatus.HINT_PRIORITY: "(priority)",
}
def format_hint(ctx: Context, team: int, hint: Hint) -> str:
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \
f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \
@@ -1159,8 +1099,7 @@ def format_hint(ctx: Context, team: int, hint: Hint) -> str:
if hint.entrance:
text += f" at {hint.entrance}"
return text + ". " + status_names.get(hint.status, "(unknown)")
return text + (". (found)" if hint.found else ".")
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
@@ -1564,7 +1503,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
def get_hints(self, input_text: str, for_location: bool = False) -> bool:
points_available = get_client_points(self.ctx, self.client)
cost = self.ctx.get_hint_cost(self.client.slot)
auto_status = HintStatus.HINT_UNSPECIFIED if for_location else HintStatus.HINT_PRIORITY
if not input_text:
hints = {hint.re_check(self.ctx, self.client.team) for hint in
self.ctx.hints[self.client.team, self.client.slot]}
@@ -1590,9 +1529,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
hints = []
elif not for_location:
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id)
else:
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id)
else:
game = self.ctx.games[self.client.slot]
@@ -1612,16 +1551,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints = []
for item_name in self.ctx.item_name_groups[game][hint_name]:
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name, auto_status))
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
elif hint_name in self.ctx.location_name_groups[game]: # location group name
hints = []
for loc_name in self.ctx.location_name_groups[game][hint_name]:
if loc_name in self.ctx.location_names_for_game(game):
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name, auto_status))
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name))
else: # location name
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
else:
self.output(response)
@@ -1893,56 +1832,13 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
target_item, target_player, flags = ctx.locations[client.slot][location]
if create_as_hint:
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location,
HintStatus.HINT_UNSPECIFIED))
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location))
locs.append(NetworkItem(target_item, location, target_player, flags))
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2)
if locs and create_as_hint:
ctx.save()
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
elif cmd == 'UpdateHint':
location = args["location"]
player = args["player"]
status = args["status"]
if not isinstance(player, int) or not isinstance(location, int) \
or (status is not None and not isinstance(status, int)):
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint',
"original_cmd": cmd}])
return
hint = ctx.get_hint(client.team, player, location)
if not hint:
return # Ignored safely
if client.slot not in ctx.slot_set(hint.receiving_player):
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission',
"original_cmd": cmd}])
return
new_hint = hint
if status is None:
return
try:
status = HintStatus(status)
except ValueError:
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments",
"text": 'UpdateHint: Invalid Status', "original_cmd": cmd}])
return
if status == HintStatus.HINT_FOUND:
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments",
"text": 'UpdateHint: Cannot manually update status to "HINT_FOUND"', "original_cmd": cmd}])
return
new_hint = new_hint.re_prioritize(ctx, status)
if hint == new_hint:
return
ctx.replace_hint(client.team, hint.finding_player, hint, new_hint)
ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint)
ctx.save()
ctx.on_changed_hints(client.team, hint.finding_player)
ctx.on_changed_hints(client.team, hint.receiving_player)
elif cmd == 'StatusUpdate':
update_client_status(ctx, client, args["status"])
@@ -2247,9 +2143,9 @@ class ServerCommandProcessor(CommonCommandProcessor):
hints = []
for item_name_from_group in self.ctx.item_name_groups[game][item]:
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group, HintStatus.HINT_PRIORITY))
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
else: # item name or id
hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY)
hints = collect_hints(self.ctx, team, slot, item)
if hints:
self.ctx.notify_hints(team, hints)
@@ -2283,17 +2179,14 @@ class ServerCommandProcessor(CommonCommandProcessor):
if usable:
if isinstance(location, int):
hints = collect_hint_location_id(self.ctx, team, slot, location,
HintStatus.HINT_UNSPECIFIED)
hints = collect_hint_location_id(self.ctx, team, slot, location)
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
hints = []
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
if loc_name_from_group in self.ctx.location_names_for_game(game):
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group,
HintStatus.HINT_UNSPECIFIED))
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group))
else:
hints = collect_hint_location_name(self.ctx, team, slot, location,
HintStatus.HINT_UNSPECIFIED)
hints = collect_hint_location_name(self.ctx, team, slot, location)
if hints:
self.ctx.notify_hints(team, hints)
else:
@@ -2383,8 +2276,6 @@ def parse_args() -> argparse.Namespace:
parser.add_argument('--cert_key', help="Path to SSL Certificate Key file")
parser.add_argument('--loglevel', default=defaults["loglevel"],
choices=['debug', 'info', 'warning', 'error', 'critical'])
parser.add_argument('--logtime', help="Add timestamps to STDOUT",
default=defaults["logtime"], action='store_true')
parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int)
parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int)
parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true')
@@ -2465,9 +2356,7 @@ def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLConte
async def main(args: argparse.Namespace):
Utils.init_logging(name="Server",
loglevel=args.loglevel.lower(),
add_timestamp=args.logtime)
Utils.init_logging("Server", loglevel=args.loglevel.lower())
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode,

View File

@@ -10,14 +10,6 @@ import websockets
from Utils import ByValue, Version
class HintStatus(ByValue, enum.IntEnum):
HINT_FOUND = 0
HINT_UNSPECIFIED = 1
HINT_NO_PRIORITY = 10
HINT_AVOID = 20
HINT_PRIORITY = 30
class JSONMessagePart(typing.TypedDict, total=False):
text: str
# optional
@@ -27,8 +19,6 @@ class JSONMessagePart(typing.TypedDict, total=False):
player: int
# if type == item indicates item flags
flags: int
# if type == hint_status
hint_status: HintStatus
class ClientStatus(ByValue, enum.IntEnum):
@@ -194,7 +184,6 @@ class JSONTypes(str, enum.Enum):
location_name = "location_name"
location_id = "location_id"
entrance_name = "entrance_name"
hint_status = "hint_status"
class JSONtoTextParser(metaclass=HandlerMeta):
@@ -235,7 +224,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
def _handle_player_id(self, node: JSONMessagePart):
player = int(node["text"])
node["color"] = 'magenta' if self.ctx.slot_concerns_self(player) else 'yellow'
node["color"] = 'magenta' if player == self.ctx.slot else 'yellow'
node["text"] = self.ctx.player_names[player]
return self._handle_color(node)
@@ -276,10 +265,6 @@ class JSONtoTextParser(metaclass=HandlerMeta):
node["color"] = 'blue'
return self._handle_color(node)
def _handle_hint_status(self, node: JSONMessagePart):
node["color"] = status_colors.get(node["hint_status"], "red")
return self._handle_color(node)
class RawJSONtoTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
@@ -312,27 +297,6 @@ def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs)
parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs})
status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "(found)",
HintStatus.HINT_UNSPECIFIED: "(unspecified)",
HintStatus.HINT_NO_PRIORITY: "(no priority)",
HintStatus.HINT_AVOID: "(avoid)",
HintStatus.HINT_PRIORITY: "(priority)",
}
status_colors: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "green",
HintStatus.HINT_UNSPECIFIED: "white",
HintStatus.HINT_NO_PRIORITY: "slateblue",
HintStatus.HINT_AVOID: "salmon",
HintStatus.HINT_PRIORITY: "plum",
}
def add_json_hint_status(parts: list, hint_status: HintStatus, text: typing.Optional[str] = None, **kwargs):
parts.append({"text": text if text != None else status_names.get(hint_status, "(unknown)"),
"hint_status": hint_status, "type": JSONTypes.hint_status, **kwargs})
class Hint(typing.NamedTuple):
receiving_player: int
finding_player: int
@@ -341,21 +305,14 @@ class Hint(typing.NamedTuple):
found: bool
entrance: str = ""
item_flags: int = 0
status: HintStatus = HintStatus.HINT_UNSPECIFIED
def re_check(self, ctx, team) -> Hint:
if self.found and self.status == HintStatus.HINT_FOUND:
if self.found:
return self
found = self.location in ctx.location_checks[team, self.finding_player]
if found:
return self._replace(found=found, status=HintStatus.HINT_FOUND)
return self
def re_prioritize(self, ctx, status: HintStatus) -> Hint:
if self.found and status != HintStatus.HINT_FOUND:
status = HintStatus.HINT_FOUND
if status != self.status:
return self._replace(status=status)
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance,
self.item_flags)
return self
def __hash__(self):
@@ -377,7 +334,10 @@ class Hint(typing.NamedTuple):
else:
add_json_text(parts, "'s World")
add_json_text(parts, ". ")
add_json_hint_status(parts, self.status)
if self.found:
add_json_text(parts, "(found)", type="color", color="green")
else:
add_json_text(parts, "(not found)", type="color", color="red")
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,
@@ -423,8 +383,6 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
checked = state[team, slot]
if not checked:
# This optimizes the case where everyone connects to a fresh game at the same time.
if slot not in self:
raise KeyError(slot)
return []
return [location_id for
location_id in self[slot] if

View File

@@ -496,7 +496,7 @@ class TextChoice(Choice):
def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \
f"'{value}' is not a valid option for '{self.__class__.__name__}'"
f"{value} is not a valid option for {self.__class__.__name__}"
self.value = value
@property
@@ -617,17 +617,17 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
used_locations.append(location)
used_bosses.append(boss)
if not cls.valid_boss_name(boss):
raise ValueError(f"'{boss.title()}' is not a valid boss name.")
raise ValueError(f"{boss.title()} is not a valid boss name.")
if not cls.valid_location_name(location):
raise ValueError(f"'{location.title()}' is not a valid boss location name.")
raise ValueError(f"{location.title()} is not a valid boss location name.")
if not cls.can_place_boss(boss, location):
raise ValueError(f"'{location.title()}' is not a valid location for {boss.title()} to be placed.")
raise ValueError(f"{location.title()} is not a valid location for {boss.title()} to be placed.")
else:
if cls.duplicate_bosses:
if not cls.valid_boss_name(option):
raise ValueError(f"'{option}' is not a valid boss name.")
raise ValueError(f"{option} is not a valid boss name.")
else:
raise ValueError(f"'{option.title()}' is not formatted correctly.")
raise ValueError(f"{option.title()} is not formatted correctly.")
@classmethod
def can_place_boss(cls, boss: str, location: str) -> bool:
@@ -754,7 +754,7 @@ class NamedRange(Range):
elif value > self.range_end and value not in self.special_range_names.values():
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " +
f"and is also not one of the supported named special values: {self.special_range_names}")
# See docstring
for key in self.special_range_names:
if key != key.lower():
@@ -817,21 +817,18 @@ class VerifyKeys(metaclass=FreezeValidKeys):
for item_name in self.value:
if item_name not in world.item_names:
picks = get_fuzzy_results(item_name, world.item_names, limit=1)
raise Exception(f"Item '{item_name}' from option '{self}' "
f"is not a valid item name from '{world.game}'. "
raise Exception(f"Item {item_name} from option {self} "
f"is not a valid item name from {world.game}. "
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
elif self.verify_location_name:
for location_name in self.value:
if location_name not in world.location_names:
picks = get_fuzzy_results(location_name, world.location_names, limit=1)
raise Exception(f"Location '{location_name}' from option '{self}' "
f"is not a valid location name from '{world.game}'. "
raise Exception(f"Location {location_name} from option {self} "
f"is not a valid location name from {world.game}. "
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
def __iter__(self) -> typing.Iterator[typing.Any]:
return self.value.__iter__()
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
default = {}
supports_weighting = False
@@ -863,8 +860,6 @@ class ItemDict(OptionDict):
verify_item_name = True
def __init__(self, value: typing.Dict[str, int]):
if any(item_count is None for item_count in value.values()):
raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .")
if any(item_count < 1 for item_count in value.values()):
raise Exception("Cannot have non-positive item counts.")
super(ItemDict, self).__init__(value)
@@ -1111,11 +1106,11 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
used_entrances.append(entrance)
used_exits.append(exit)
if not cls.validate_entrance_name(entrance):
raise ValueError(f"'{entrance.title()}' is not a valid entrance.")
raise ValueError(f"{entrance.title()} is not a valid entrance.")
if not cls.validate_exit_name(exit):
raise ValueError(f"'{exit.title()}' is not a valid exit.")
raise ValueError(f"{exit.title()} is not a valid exit.")
if not cls.can_connect(entrance, exit):
raise ValueError(f"Connection between '{entrance.title()}' and '{exit.title()}' is invalid.")
raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.")
@classmethod
def from_any(cls, data: PlandoConFromAnyType) -> Self:
@@ -1180,7 +1175,7 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
class Accessibility(Choice):
"""
Set rules for reachability of your items/locations.
**Full:** ensure everything can be reached and acquired.
**Minimal:** ensure what is needed to reach your goal can be acquired.
@@ -1198,7 +1193,7 @@ class Accessibility(Choice):
class ItemsAccessibility(Accessibility):
"""
Set rules for reachability of your items/locations.
**Full:** ensure everything can be reached and acquired.
**Minimal:** ensure what is needed to reach your goal can be acquired.
@@ -1249,16 +1244,12 @@ class CommonOptions(metaclass=OptionsMetaProperty):
progression_balancing: ProgressionBalancing
accessibility: Accessibility
def as_dict(self,
*option_names: str,
casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake",
toggles_as_bools: bool = False) -> typing.Dict[str, typing.Any]:
def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]:
"""
Returns a dictionary of [str, Option.value]
:param option_names: names of the options to return
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
:param toggles_as_bools: whether toggle options should be output as bools instead of strings
"""
assert option_names, "options.as_dict() was used without any option names."
option_results = {}
@@ -1280,8 +1271,6 @@ class CommonOptions(metaclass=OptionsMetaProperty):
value = getattr(self, option_name).value
if isinstance(value, set):
value = sorted(value)
elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle):
value = bool(value)
option_results[display_name] = value
else:
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
@@ -1379,8 +1368,8 @@ class ItemLinks(OptionList):
picks_group = get_fuzzy_results(item_name, world.item_name_groups.keys(), limit=1)
picks_group = f" or '{picks_group[0][0]}' ({picks_group[0][1]}% sure)" if allow_item_groups else ""
raise Exception(f"Item '{item_name}' from item link '{item_link}' "
f"is not a valid item from '{world.game}' for '{pool_name}'. "
raise Exception(f"Item {item_name} from item link {item_link} "
f"is not a valid item from {world.game} for {pool_name}. "
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure){picks_group}")
if allow_item_groups:
pool |= world.item_name_groups.get(item_name, {item_name})
@@ -1471,26 +1460,22 @@ it.
def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[
str, typing.Dict[str, typing.Type[Option[typing.Any]]]]:
"""Generates and returns a dictionary for the option groups of a specified world."""
option_to_name = {option: option_name for option_name, option in world.options_dataclass.type_hints.items()}
ordered_groups = {group.name: group.options for group in world.web.option_groups}
option_groups = {option: option_group.name
for option_group in world.web.option_groups
for option in option_group.options}
# add a default option group for uncategorized options to get thrown into
if "Game Options" not in ordered_groups:
grouped_options = set(option for group in ordered_groups.values() for option in group)
ungrouped_options = [option for option in option_to_name if option not in grouped_options]
# only add the game options group if we have ungrouped options
if ungrouped_options:
ordered_groups = {**{"Game Options": ungrouped_options}, **ordered_groups}
ordered_groups = ["Game Options"]
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
grouped_options = {group: {} for group in ordered_groups}
for option_name, option in world.options_dataclass.type_hints.items():
if visibility_level & option.visibility:
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
return {
group: {
option_to_name[option]: option
for option in group_options
if (visibility_level in option.visibility and option in option_to_name)
}
for group, group_options in ordered_groups.items()
}
# if the world doesn't have any ungrouped options, this group will be empty so just remove it
if not grouped_options["Game Options"]:
del grouped_options["Game Options"]
return grouped_options
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None:

View File

@@ -76,10 +76,6 @@ Currently, the following games are supported:
* Kingdom Hearts 1
* Mega Man 2
* Yacht Dice
* Faxanadu
* Saving Princess
* Castlevania: Circle of the Moon
* Inscryption
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -243,9 +243,6 @@ class SNIContext(CommonContext):
# Once the games handled by SNIClient gets made to be remote items,
# this will no longer be needed.
async_start(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}]))
if self.client_handler is not None:
self.client_handler.on_package(self, cmd, args)
def run_gui(self) -> None:
from kvui import GameManager

View File

@@ -19,7 +19,8 @@ import warnings
from argparse import Namespace
from settings import Settings, get_settings
from time import sleep
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from typing_extensions import TypeGuard
from yaml import load, load_all, dump
try:
@@ -47,7 +48,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.6.0"
__version__ = "0.5.1"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -152,15 +153,8 @@ def home_path(*path: str) -> str:
if hasattr(home_path, 'cached_path'):
pass
elif sys.platform.startswith('linux'):
xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
home_path.cached_path = xdg_data_home + '/Archipelago'
if not os.path.isdir(home_path.cached_path):
legacy_home_path = os.path.expanduser('~/Archipelago')
if os.path.isdir(legacy_home_path):
os.renames(legacy_home_path, home_path.cached_path)
os.symlink(home_path.cached_path, legacy_home_path)
else:
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
home_path.cached_path = os.path.expanduser('~/Archipelago')
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
else:
# not implemented
home_path.cached_path = local_path() # this will generate the same exceptions we got previously
@@ -428,8 +422,7 @@ class RestrictedUnpickler(pickle.Unpickler):
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# used by MultiServer -> savegame/multidata
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
"SlotType", "NetworkSlot", "HintStatus"}:
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name == "PlandoItem":
@@ -492,9 +485,9 @@ def get_text_after(text: str, start: str) -> str:
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
write_mode: str = "w", log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
add_timestamp: bool = False, exception_logger: typing.Optional[str] = None):
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
exception_logger: typing.Optional[str] = None):
import datetime
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
log_folder = user_path("logs")
@@ -522,14 +515,10 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
return self.condition(record)
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.msg))
root_logger.addHandler(file_handler)
if sys.stdout:
formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.addFilter(Filter("NoFile", lambda record: not getattr(record, "NoStream", False)))
if add_timestamp:
stream_handler.setFormatter(formatter)
root_logger.addHandler(stream_handler)
# Relay unhandled exceptions to logger.
@@ -541,8 +530,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logging.getLogger(exception_logger).exception("Uncaught exception",
exc_info=(exc_type, exc_value, exc_traceback),
extra={"NoStream": exception_logger is None})
exc_info=(exc_type, exc_value, exc_traceback))
return orig_hook(exc_type, exc_value, exc_traceback)
handle_exception._wrapped = True
@@ -565,7 +553,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
import platform
logging.info(
f"Archipelago ({__version__}) logging initialized"
f" on {platform.platform()} process {os.getpid()}"
f" on {platform.platform()}"
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
f"{' (frozen)' if is_frozen() else ''}"
)
@@ -867,10 +855,11 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
task.add_done_callback(_faf_tasks.discard)
def deprecate(message: str, add_stacklevels: int = 0):
def deprecate(message: str):
if __debug__:
raise Exception(message)
warnings.warn(message, stacklevel=2 + add_stacklevels)
import warnings
warnings.warn(message)
class DeprecateDict(dict):
@@ -884,9 +873,10 @@ class DeprecateDict(dict):
def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message, add_stacklevels=1)
deprecate(self.log_message)
elif __debug__:
warnings.warn(self.log_message, stacklevel=2)
import warnings
warnings.warn(self.log_message)
return super().__getitem__(item)

View File

@@ -17,7 +17,7 @@ from Utils import get_file_safe_name
if typing.TYPE_CHECKING:
from flask import Flask
Utils.local_path.cached_path = os.path.dirname(__file__)
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
settings.no_gui = True
configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home
@@ -34,7 +34,7 @@ def get_app() -> "Flask":
app.config.from_file(configpath, yaml.safe_load)
logging.info(f"Updated config from {configpath}")
# inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it.
parser = argparse.ArgumentParser(allow_abbrev=False)
parser = argparse.ArgumentParser()
parser.add_argument('--config_override', default=None,
help="Path to yaml config file that overrules config.yaml.")
args = parser.parse_known_args()[0]

View File

@@ -39,8 +39,6 @@ app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
app.config["JOB_THRESHOLD"] = 1
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600
# memory limit for generator processes in bytes
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
app.config['SESSION_PERMANENT'] = True
# waitress uses one thread for I/O, these are for processing of views that then get sent
@@ -87,6 +85,6 @@ def register():
from WebHostLib.customserver import run_server_process
# to trigger app routing picking up on it
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options, session
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options
app.register_blueprint(api.api_endpoints)

View File

@@ -6,7 +6,6 @@ import multiprocessing
import typing
from datetime import timedelta, datetime
from threading import Event, Thread
from typing import Any
from uuid import UUID
from pony.orm import db_session, select, commit
@@ -54,21 +53,7 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
generation.state = STATE_STARTED
def init_generator(config: dict[str, Any]) -> None:
try:
import resource
except ModuleNotFoundError:
pass # unix only module
else:
# set soft limit for memory to from config (default 4GiB)
soft_limit = config["GENERATOR_MEMORY_LIMIT"]
old_limit, hard_limit = resource.getrlimit(resource.RLIMIT_AS)
if soft_limit != old_limit:
resource.setrlimit(resource.RLIMIT_AS, (soft_limit, hard_limit))
logging.debug(f"Changed AS mem limit {old_limit} -> {soft_limit}")
del resource, soft_limit, hard_limit
pony_config = config["PONY"]
def init_db(pony_config: dict):
db.bind(**pony_config)
db.generate_mapping()
@@ -120,8 +105,8 @@ def autogen(config: dict):
try:
with Locker("autogen"):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator,
initargs=(config,), maxtasksperchild=10) as generator_pool:
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool:
with db_session:
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)

View File

@@ -105,9 +105,8 @@ def roll_options(options: Dict[str, Union[dict, str]],
plando_options=plando_options)
else:
for i, yaml_data in enumerate(yaml_datas):
if yaml_data is not None:
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
plando_options=plando_options)
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
plando_options=plando_options)
except Exception as e:
if e.__cause__:
results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}"

View File

@@ -31,11 +31,11 @@ def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[s
server_options = {
"hint_cost": int(options_source.get("hint_cost", ServerOptions.hint_cost)),
"release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)),
"remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)),
"collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)),
"release_mode": options_source.get("release_mode", ServerOptions.release_mode),
"remaining_mode": options_source.get("remaining_mode", ServerOptions.remaining_mode),
"collect_mode": options_source.get("collect_mode", ServerOptions.collect_mode),
"item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))),
"server_password": str(options_source.get("server_password", None)),
"server_password": options_source.get("server_password", None),
}
generator_options = {
"spoiler": int(options_source.get("spoiler", GeneratorOptions.spoiler)),

View File

@@ -18,6 +18,13 @@ def get_world_theme(game_name: str):
return 'grass'
@app.before_request
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):

View File

@@ -5,7 +5,9 @@ waitress>=3.0.0
Flask-Caching>=2.3.0
Flask-Compress>=1.15
Flask-Limiter>=3.8.0
bokeh>=3.5.2
bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.4.3; python_version == '3.9'
bokeh>=3.5.2; python_version >= '3.10'
markupsafe>=2.1.5
Markdown>=3.7
mdx-breakless-lists>=1.0.1

View File

@@ -1,31 +0,0 @@
from uuid import uuid4, UUID
from flask import session, render_template
from WebHostLib import app
@app.before_request
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.route('/session')
def show_session():
return render_template(
"session.html",
)
@app.route('/session/<string:_id>')
def set_session(_id: str):
new_id: UUID = UUID(_id, version=4)
old_id: UUID = session["_id"]
if old_id != new_id:
session["_id"] = new_id
return render_template(
"session.html",
old_id=old_id,
)

View File

@@ -178,15 +178,8 @@
})
.then(text => new DOMParser().parseFromString(text, 'text/html'))
.then(newDocument => {
["host-room-info", "slots-table"].forEach(function(id) {
const newEl = newDocument.getElementById(id);
const oldEl = document.getElementById(id);
if (oldEl && newEl) {
oldEl.innerHTML = newEl.innerHTML;
} else if (newEl) {
console.warn(`Did not find element to replace for ${id}`)
}
});
let el = newDocument.getElementById("host-room-info");
document.getElementById("host-room-info").innerHTML = el.innerHTML;
});
}

View File

@@ -1,6 +1,6 @@
{% block footer %}
<footer id="island-footer">
<div id="copyright-notice">Copyright 2025 Archipelago</div>
<div id="copyright-notice">Copyright 2024 Archipelago</div>
<div id="links">
<a href="/sitemap">Site Map</a>
-

View File

@@ -8,7 +8,7 @@
{%- endmacro %}
{% macro list_patches_room(room) %}
{% if room.seed.slots %}
<table id="slots-table">
<table>
<thead>
<tr>
<th>Id</th>

View File

@@ -1,30 +0,0 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/stoneHeader.html' %}
<title>Session</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
{% endblock %}
{% block body %}
<div class="markdown">
{% if old_id is defined %}
<p>Your old code was:</p>
<code>{{ old_id }}</code>
<br>
{% endif %}
<p>The following code is your unique identifier, it binds your uploaded content, such as rooms and seeds to you.
Treat it like a combined login name and password.
You should save this securely if you ever need to restore access.
You can also paste it into another device to access your content from multiple devices / browsers.
Some browsers, such as Brave, will delete your identifier cookie on a timer.</p>
<code>{{ session["_id"] }}</code>
<br>
<p>
The following link can be used to set the identifier. Do not share the code or link with others. <br>
<a href="{{ url_for('set_session', _id=session['_id']) }}">
{{ url_for('set_session', _id=session['_id'], _external=True) }}
</a>
</p>
</div>
{% endblock %}

View File

@@ -26,7 +26,6 @@
<li><a href="/user-content">User Content</a></li>
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
<li><a href="/glossary/en">Glossary</a></li>
<li><a href="{{url_for("show_session")}}">Session / Login</a></li>
</ul>
<h2>Tutorials</h2>

View File

@@ -4,6 +4,9 @@
{% include 'header/grassHeader.html' %}
<title>Option Templates (YAML)</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
crossorigin="anonymous"></script>
{% endblock %}
{% block body %}

View File

@@ -53,7 +53,7 @@
<table class="range-rows" data-option="{{ option_name }}">
<tbody>
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
{% if option.default is number and option.range_start < option.default < option.range_end %}
{% if option.range_start < option.default < option.range_end %}
{{ RangeRow(option_name, option, option.default, option.default, True) }}
{% endif %}
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}

View File

@@ -69,14 +69,6 @@ cdef struct IndexEntry:
size_t count
if TYPE_CHECKING:
State = Dict[Tuple[int, int], Set[int]]
else:
State = Union[Tuple[int, int], Set[int], defaultdict]
T = TypeVar('T')
@cython.auto_pickle(False)
cdef class LocationStore:
"""Compact store for locations and their items in a MultiServer"""
@@ -145,16 +137,10 @@ cdef class LocationStore:
warnings.warn("Game has no locations")
# allocate the arrays and invalidate index (0xff...)
if count:
# leaving entries as NULL if there are none, makes potential memory errors more visible
self.entries = <LocationEntry*>self._mem.alloc(count, sizeof(LocationEntry))
self.entries = <LocationEntry*>self._mem.alloc(count, sizeof(LocationEntry))
self.sender_index = <IndexEntry*>self._mem.alloc(max_sender + 1, sizeof(IndexEntry))
self._raw_proxies = <PyObject**>self._mem.alloc(max_sender + 1, sizeof(PyObject*))
assert (not self.entries) == (not count)
assert self.sender_index
assert self._raw_proxies
# build entries and index
cdef size_t i = 0
for sender, locations in sorted(locations_dict.items()):
@@ -204,6 +190,8 @@ cdef class LocationStore:
raise KeyError(key)
return <object>self._raw_proxies[key]
T = TypeVar('T')
def get(self, key: int, default: T) -> Union[PlayerLocationProxy, T]:
# calling into self.__getitem__ here is slow, but this is not used in MultiServer
try:
@@ -258,11 +246,12 @@ cdef class LocationStore:
all_locations[sender].add(entry.location)
return all_locations
def get_checked(self, state: State, team: int, slot: int) -> List[int]:
cdef ap_player_t sender = slot
if sender < 0 or sender >= self.sender_index_size:
raise KeyError(slot)
if TYPE_CHECKING:
State = Dict[Tuple[int, int], Set[int]]
else:
State = Union[Tuple[int, int], Set[int], defaultdict]
def get_checked(self, state: State, team: int, slot: int) -> List[int]:
# This used to validate checks actually exist. A remnant from the past.
# If the order of locations becomes relevant at some point, we could not do sorted(set), so leaving it.
cdef set checked = state[team, slot]
@@ -274,6 +263,7 @@ cdef class LocationStore:
# Unless the set is close to empty, it's cheaper to use the python set directly, so we do that.
cdef LocationEntry* entry
cdef ap_player_t sender = slot
cdef size_t start = self.sender_index[sender].start
cdef size_t count = self.sender_index[sender].count
return [entry.location for
@@ -283,11 +273,9 @@ cdef class LocationStore:
def get_missing(self, state: State, team: int, slot: int) -> List[int]:
cdef LocationEntry* entry
cdef ap_player_t sender = slot
if sender < 0 or sender >= self.sender_index_size:
raise KeyError(slot)
cdef set checked = state[team, slot]
cdef size_t start = self.sender_index[sender].start
cdef size_t count = self.sender_index[sender].count
cdef set checked = state[team, slot]
if not len(checked):
# Skip `in` if none have been checked.
# This optimizes the case where everyone connects to a fresh game at the same time.
@@ -302,11 +290,9 @@ cdef class LocationStore:
def get_remaining(self, state: State, team: int, slot: int) -> List[Tuple[int, int]]:
cdef LocationEntry* entry
cdef ap_player_t sender = slot
if sender < 0 or sender >= self.sender_index_size:
raise KeyError(slot)
cdef set checked = state[team, slot]
cdef size_t start = self.sender_index[sender].start
cdef size_t count = self.sender_index[sender].count
cdef set checked = state[team, slot]
return sorted([(entry.receiver, entry.item) for
entry in self.entries[start:start+count] if
entry.location not in checked])
@@ -342,8 +328,7 @@ cdef class PlayerLocationProxy:
cdef LocationEntry* entry = NULL
# binary search
cdef size_t l = self._store.sender_index[self._player].start
cdef size_t e = l + self._store.sender_index[self._player].count
cdef size_t r = e
cdef size_t r = l + self._store.sender_index[self._player].count
cdef size_t m
while l < r:
m = (l + r) // 2
@@ -352,7 +337,7 @@ cdef class PlayerLocationProxy:
l = m + 1
else:
r = m
if l < e:
if entry: # count != 0
entry = self._store.entries + l
if entry.location == loc:
return entry
@@ -364,6 +349,8 @@ cdef class PlayerLocationProxy:
return entry.item, entry.receiver, entry.flags
raise KeyError(f"No location {key} for player {self._player}")
T = TypeVar('T')
def get(self, key: int, default: T) -> Union[Tuple[int, int, int], T]:
cdef LocationEntry* entry = self._get(key)
if entry:

View File

@@ -3,16 +3,8 @@ import os
def make_ext(modname, pyxfilename):
from distutils.extension import Extension
return Extension(
name=modname,
sources=[pyxfilename],
depends=["intset.h"],
include_dirs=[os.getcwd()],
language="c",
# to enable ASAN and debug build:
# extra_compile_args=["-fsanitize=address", "-UNDEBUG", "-Og", "-g"],
# extra_objects=["-fsanitize=address"],
# NOTE: we can not put -lasan at the front of link args, so needs to be run with
# LD_PRELOAD=/usr/lib/libasan.so ASAN_OPTIONS=detect_leaks=0 path/to/exe
# NOTE: this can't find everything unless libpython and cymem are also built with ASAN
)
return Extension(name=modname,
sources=[pyxfilename],
depends=["intset.h"],
include_dirs=[os.getcwd()],
language="c")

View File

@@ -59,7 +59,7 @@
finding_text: "Finding Player"
location_text: "Location"
entrance_text: "Entrance"
status_text: "Status"
found_text: "Found?"
TooltipLabel:
id: receiving
sort_key: 'receiving'
@@ -96,9 +96,9 @@
valign: 'center'
pos_hint: {"center_y": 0.5}
TooltipLabel:
id: status
sort_key: 'status'
text: root.status_text
id: found
sort_key: 'found'
text: root.found_text
halign: 'center'
valign: 'center'
pos_hint: {"center_y": 0.5}
@@ -147,8 +147,3 @@
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
<ServerToolTip>:
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
<AutocompleteHintInput>
size_hint_y: None
height: dp(30)
multiline: False
write_tab: False

View File

@@ -36,9 +36,6 @@
# Castlevania 64
/worlds/cv64/ @LiquidCat64
# Castlevania: Circle of the Moon
/worlds/cvcotm/ @LiquidCat64
# Celeste 64
/worlds/celeste64/ @PoryGone
@@ -58,22 +55,19 @@
/worlds/dlcquest/ @axe-y @agilbert1412
# DOOM 1993
/worlds/doom_1993/ @Daivuk @KScl
/worlds/doom_1993/ @Daivuk
# DOOM II
/worlds/doom_ii/ @Daivuk @KScl
/worlds/doom_ii/ @Daivuk
# Factorio
/worlds/factorio/ @Berserker66
# Faxanadu
/worlds/faxanadu/ @Daivuk
# Final Fantasy Mystic Quest
/worlds/ffmq/ @Alchav @wildham0
# Heretic
/worlds/heretic/ @Daivuk @KScl
/worlds/heretic/ @Daivuk
# Hollow Knight
/worlds/hk/ @BadMagic100 @qwint
@@ -81,9 +75,6 @@
# Hylics 2
/worlds/hylics2/ @TRPG0
# Inscryption
/worlds/inscryption/ @DrBibop @Glowbuzz
# Kirby's Dream Land 3
/worlds/kdl3/ @Silvris
@@ -148,11 +139,8 @@
# Risk of Rain 2
/worlds/ror2/ @kindasneaki
# Saving Princess
/worlds/saving_princess/ @LeonarthCG
# Shivers
/worlds/shivers/ @GodlFire @korydondzila
/worlds/shivers/ @GodlFire
# A Short Hike
/worlds/shorthike/ @chandler05 @BrandenEK

View File

@@ -43,26 +43,3 @@ A faster alternative to the `for` loop would be to use a [list comprehension](ht
```py
item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
```
---
### I learned about indirect conditions in the world API document, but I want to know more. What are they and why are they necessary?
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and **when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is quite complicated.
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph. It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to the queue until there is nothing more to check.
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region access, then the following may happen:
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been reached yet during the graph search.
2. Then, the region in its access_rule is determined to be reachable.
This entrance *would* be in logic if it were rechecked, but it won't be rechecked this cycle.
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new regions are reached.
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep if a specific region is reached during it.
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness), using them is significantly faster than just "rechecking each entrance until nothing new is found".
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they call `region.can_reach` on their respective parent/source region.
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance &rarr; region dependencies, making indirect conditions preferred because they are much faster.

View File

@@ -16,7 +16,7 @@ game contributions:
* **Do not introduce unit test failures/regressions.**
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
your changes. Currently, the oldest supported version
is [Python 3.10](https://www.python.org/downloads/release/python-31015/).
is [Python 3.8](https://www.python.org/downloads/release/python-380/).
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
pushing.
You can turn them on here:

View File

@@ -1,430 +0,0 @@
# Entrance Randomization
This document discusses the API and underlying implementation of the generic entrance randomization algorithm
exposed in [entrance_rando.py](/entrance_rando.py). Throughout the doc, entrance randomization is frequently abbreviated
as "ER."
This doc assumes familiarity with Archipelago's graph logic model. If you don't have a solid understanding of how
regions work, you should start there.
## Entrance randomization concepts
### Terminology
Some important terminology to understand when reading this doc and working with ER is listed below.
* Entrance rando - sometimes called "room rando," "transition rando," "door rando," or similar,
this is a game mode in which the game map itself is randomized.
In Archipelago, these things are often represented as `Entrance`s in the region graph, so we call it Entrance rando.
* Entrances and exits - entrances are ways into your region, exits are ways out of the region. In code, they are both
represented as `Entrance` objects. In this doc, the terms "entrances" and "exits" will be used in this sense; the
`Entrance` class will always be referenced in a code block with an uppercase E.
* Dead end - a connected group of regions which can never help ER progress. This means that it:
* Is not in any indirect conditions/access rules.
* Has no plando'd or otherwise preplaced progression items, including events.
* Has no randomized exits.
* One way transition - a transition that, in the game, is not safe to reverse through (for example, in Hollow Knight,
some transitions are inaccessible backwards in vanilla and would put you out of bounds). One way transitions are
paired together during randomization to prevent such unsafe game states. Most transitions are not one way.
### Basic randomization strategy
The Generic ER algorithm works by using the logic structures you are already familiar with. To give a basic example,
let's assume a toy world is defined with the vanilla region graph modeled below. In this diagram, the smaller boxes
represent regions while the larger boxes represent scenes. Scenes are not an Archipelago concept, the grouping is
purely illustrative.
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph startingRoom [Starting Room]
S[Starting Room Right Door]
end
subgraph sceneB [Scene B]
BR1[Scene B Right Door]
end
subgraph sceneA [Scene A]
AL1[Scene A Lower Left Door] <--> AR1[Scene A Right Door]
AL2[Scene A Upper Left Door] <--> AR1
end
subgraph sceneC [Scene C]
CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
CL1 <--> CR2[Scene C Lower Right Door]
end
subgraph sceneD [Scene D]
DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
end
subgraph endingRoom [Ending Room]
EL1[Ending Room Upper Left Door] <--> Victory
EL2[Ending Room Lower Left Door] <--> Victory
end
Menu --> S
S <--> AL2
BR1 <--> AL1
AR1 <--> CL1
CR1 <--> DL1
DR1 <--> EL1
CR2 <--> EL2
classDef hidden display:none;
```
First, the world begins by splitting the `Entrance`s which should be randomized. This is essentially all that has to be
done on the world side; calling the `randomize_entrances` function will do the rest, using your region definitions and
logic to generate a valid world layout by connecting the partially connected edges you've defined. After you have done
that, your region graph might look something like the following diagram. Note how each randomizable entrance/exit pair
(represented as a bidirectional arrow) is disconnected on one end.
> [!NOTE]
> It is required to use explicit indirect conditions when using Generic ER. Without this restriction,
> Generic ER would have no way to correctly determine that a region may be required in logic,
> leading to significantly higher failure rates due to mis-categorized regions.
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph startingRoom [Starting Room]
S[Starting Room Right Door]
end
subgraph sceneA [Scene A]
AL1[Scene A Upper Left Door] <--> AR1[Scene A Right Door]
AL2[Scene A Lower Left Door] <--> AR1
end
subgraph sceneB [Scene B]
BR1[Scene B Right Door]
end
subgraph sceneC [Scene C]
CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
CL1 <--> CR2[Scene C Lower Right Door]
end
subgraph sceneD [Scene D]
DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
end
subgraph endingRoom [Ending Room]
EL1[Ending Room Upper Left Door] <--> Victory
EL2[Ending Room Lower Left Door] <--> Victory
end
Menu --> S
S <--> T1:::hidden
T2:::hidden <--> AL1
T3:::hidden <--> AL2
AR1 <--> T5:::hidden
BR1 <--> T4:::hidden
T6:::hidden <--> CL1
CR1 <--> T7:::hidden
CR2 <--> T11:::hidden
T8:::hidden <--> DL1
DR1 <--> T9:::hidden
T10:::hidden <--> EL1
T12:::hidden <--> EL2
classDef hidden display:none;
```
From here, you can call the `randomize_entrances` function and Archipelago takes over. Starting from the Menu region,
the algorithm will sweep out to find eligible region exits to randomize. It will then select an eligible target entrance
and connect them, prioritizing giving access to unvisited regions first until all regions are placed. Once the exit has
been connected to the new region, placeholder entrances are deleted. This process is visualized in the diagram below
with the newly connected edge highlighted in red.
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph startingRoom [Starting Room]
S[Starting Room Right Door]
end
subgraph sceneA [Scene A]
AL1[Scene A Upper Left Door] <--> AR1[Scene A Right Door]
AL2[Scene A Lower Left Door] <--> AR1
end
subgraph sceneB [Scene B]
BR1[Scene B Right Door]
end
subgraph sceneC [Scene C]
CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
CL1 <--> CR2[Scene C Lower Right Door]
end
subgraph sceneD [Scene D]
DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
end
subgraph endingRoom [Ending Room]
EL1[Ending Room Upper Left Door] <--> Victory
EL2[Ending Room Lower Left Door] <--> Victory
end
Menu --> S
S <--> CL1
T2:::hidden <--> AL1
T3:::hidden <--> AL2
AR1 <--> T5:::hidden
BR1 <--> T4:::hidden
CR1 <--> T7:::hidden
CR2 <--> T11:::hidden
T8:::hidden <--> DL1
DR1 <--> T9:::hidden
T10:::hidden <--> EL1
T12:::hidden <--> EL2
classDef hidden display:none;
linkStyle 8 stroke:red,stroke-width:5px;
```
This process is then repeated until all disconnected `Entrance`s have been connected or deleted, eventually resulting
in a randomized region layout.
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph startingRoom [Starting Room]
S[Starting Room Right Door]
end
subgraph sceneA [Scene A]
AL1[Scene A Upper Left Door] <--> AR1[Scene A Right Door]
AL2[Scene A Lower Left Door] <--> AR1
end
subgraph sceneB [Scene B]
BR1[Scene B Right Door]
end
subgraph sceneC [Scene C]
CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
CL1 <--> CR2[Scene C Lower Right Door]
end
subgraph sceneD [Scene D]
DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
end
subgraph endingRoom [Ending Room]
EL1[Ending Room Upper Left Door] <--> Victory
EL2[Ending Room Lower Left Door] <--> Victory
end
Menu --> S
S <--> CL1
AR1 <--> DL1
BR1 <--> EL2
CR1 <--> EL1
CR2 <--> AL1
DR1 <--> AL2
classDef hidden display:none;
```
#### ER and minimal accessibility
In general, even on minimal accessibility, ER will prefer to provide access to as many regions as possible. This is for
2 reasons:
1. Generally, having items spread across the world is going to be a more fun/engaging experience for players than
severely restricting their map. Imagine an ER arrangement with just the start region, the goal region, and exactly
enough locations in between them to get the goal - this may be the intuitive behavior of minimal, or even the desired
behavior in some cases, but it is not a particularly interesting randomizer.
2. Giving access to more of the world will give item fill a higher chance to succeed.
However, ER will cull unreachable regions and exits if necessary to save the generation of a beaten minimal.
## Usage
### Defining entrances to be randomized
The first step to using generic ER is defining entrances to be randomized. In order to do this, you will need to
leave partially disconnected exits without a `target_region` and partially disconnected entrances without a
`parent_region`. You can do this either by hand using `region.create_exit` and `region.create_er_target`, or you can
create your vanilla region graph and then use `disconnect_entrance_for_randomization` to split the desired edges.
If you're not sure which to use, prefer the latter approach as it will automatically satisfy the requirements for
coupled randomization (discussed in more depth later).
> [!TIP]
> It's recommended to give your `Entrance`s non-default names when creating them. The default naming scheme is
> `f"{parent_region} -> {target_region}"` which is generally not helpful in an entrance rando context - after all,
> the target region will not be the same as vanilla and regions are often not user-facing anyway. Instead consider names
> that describe the location of the exit, such as "Starting Room Right Door."
When creating your `Entrance`s you should also set the randomization type and group. One-way `Entrance`s represent
transitions which are impossible to traverse in reverse. All other transitions are two-ways. To ensure that all
transitions can be accessed in the game, one-ways are only randomized with other one-ways and two-ways are only
randomized with other two-ways. You can set whether an `Entrance` is one-way or two-way using the `randomization_type`
attribute.
`Entrance`s can also set the `randomization_group` attribute to allow for grouping during randomization. This can be
any integer you define and may be based on player options. Some possible use cases for grouping include:
* Directional matching - only match leftward-facing transitions to rightward-facing ones
* Terrain matching - only match water transitions to water transitions and land transitions to land transitions
* Dungeon shuffle - only shuffle entrances within a dungeon/area with each other
* Combinations of the above
By default, all `Entrance`s are placed in the group 0. An entrance can only be a member of one group, but a given group
may connect to many other groups.
### Calling generic ER
Once you have defined all your entrances and exits and connected the Menu region to your region graph, you can call
`randomize_entrances` to perform randomization.
#### Coupled and uncoupled modes
In coupled randomization, an entrance placed from A to B guarantees that the reverse placement B to A also exists
(assuming that A and B are both two-way doors). Uncoupled randomization does not make this guarantee.
When using coupled mode, there are some requirements for how placeholder ER targets for two-ways are named.
`disconnect_entrance_for_randomization` will handle this for you. However, if you opt to create your ER targets and
exits by hand, you will need to ensure that ER targets into a region are named the same as the exit they correspond to.
This allows the randomizer to find and connect the reverse pairing after the first pairing is completed. See the diagram
below for an example of incorrect and correct naming.
Incorrect target naming:
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph a [" "]
direction TB
target1
target2
end
subgraph b [" "]
direction TB
Region
end
Region["Room1"] -->|Room1 Right Door| target1:::hidden
Region --- target2:::hidden -->|Room2 Left Door| Region
linkStyle 1 stroke:none;
classDef hidden display:none;
style a display:none;
style b display:none;
```
Correct target naming:
```mermaid
%%{init: {"graph": {"defaultRenderer": "elk"}} }%%
graph LR
subgraph a [" "]
direction TB
target1
target2
end
subgraph b [" "]
direction TB
Region
end
Region["Room1"] -->|Room1 Right Door| target1:::hidden
Region --- target2:::hidden -->|Room1 Right Door| Region
linkStyle 1 stroke:none;
classDef hidden display:none;
style a display:none;
style b display:none;
```
#### Implementing grouping
When you created your entrances, you defined the group each entrance belongs to. Now you will have to define how groups
should connect with each other. This is done with the `target_group_lookup` and `preserve_group_order` parameters.
There is also a convenience function `bake_target_group_lookup` which can help to prepare group lookups when more
complex group mapping logic is needed. Some recipes for `target_group_lookup` are presented here.
For the recipes below, assume the following groups (if the syntax used here is unfamiliar to you, "bit masking" and
"bitwise operators" would be the terms to search for):
```python
class Groups(IntEnum):
# Directions
LEFT = 1
RIGHT = 2
TOP = 3
BOTTOM = 4
DOOR = 5
# Areas
FIELD = 1 << 3
CAVE = 2 << 3
MOUNTAIN = 3 << 3
# Bitmasks
DIRECTION_MASK = FIELD - 1
AREA_MASK = ~0 << 3
```
Directional matching:
```python
direction_matching_group_lookup = {
# with preserve_group_order = False, pair a left transition to either a right transition or door randomly
# with preserve_group_order = True, pair a left transition to a right transition, or else a door if no
# viable right transitions remain
Groups.LEFT: [Groups.RIGHT, Groups.DOOR],
# ...
}
```
Terrain matching or dungeon shuffle:
```python
def randomize_within_same_group(group: int) -> List[int]:
return [group]
identity_group_lookup = bake_target_group_lookup(world, randomize_within_same_group)
```
Directional + area shuffle:
```python
def get_target_groups(group: int) -> List[int]:
# example group: LEFT | CAVE
# example result: [RIGHT | CAVE, DOOR | CAVE]
direction = group & Groups.DIRECTION_MASK
area = group & Groups.AREA_MASK
return [pair_direction | area for pair_direction in direction_matching_group_lookup[direction]]
target_group_lookup = bake_target_group_lookup(world, get_target_groups)
```
#### When to call `randomize_entrances`
The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading.
ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures.
This means 2 things about when you can call ER:
1. You must supply your item pool before calling ER, or call ER before setting any rules which require items.
2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules
and create your events before you call ER if you want to guarantee a correct output.
If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also
a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER
in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or
generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as
well.
#### Informing your client about randomized entrances
`randomize_entrances` returns the completed `ERPlacementState`. The `pairings` attribute contains a list of the
created placements by name which can be used to populate slot data.
### Imposing custom constraints on randomization
Generic ER is, as the name implies, generic! That means that your world may have some use case which is not covered by
the ER implementation. To solve this, you can create a custom `Entrance` class which provides custom implementations
for `is_valid_source_transition` and `can_connect_to`. These allow arbitrary constraints to be implemented on
randomization, for instance helping to prevent restrictive sphere 1s or ensuring a maximum distance from a "hub" region.
> [!IMPORTANT]
> When implementing these functions, make sure to use `super().is_valid_source_transition` and `super().can_connect_to`
> as part of your implementation. Otherwise ER may behave unexpectedly.
## Implementation details
This section is a medium-level explainer of the implementation of ER for those who don't want to decipher the code.
However, a basic understanding of the mechanics of `fill_restrictive` will be helpful as many of the underlying
algorithms are shared
ER uses a forward fill approach to create the region layout. First, ER collects `all_state` and performs a region sweep
from Menu, similar to fill. ER then proceeds in stages to complete the randomization:
1. Attempt to connect all non-dead-end regions, prioritizing access to unseen regions so there will always be new exits
to pair off.
2. Attempt to connect all dead-end regions, so that all regions will be placed
3. Connect all remaining dangling edges now that all regions are placed.
1. Connect any other dead end entrances (e.g. second entrances to the same dead end regions).
2. Connect all remaining non-dead-ends amongst each other.
The process for each connection will do the following:
1. Select a randomizable exit of a reachable region which is a valid source transition.
2. Get its group and check `target_group_lookup` to determine which groups are valid targets.
3. Look up ER targets from those groups and find one which is valid according to `can_connect_to`
4. Connect the source exit to the target's target_region and delete the target.
* In stage 1, before placing the last valid source transition, an additional speculative sweep is performed to ensure
that there will be an available exit after the placement so randomization can continue.
5. If it's coupled mode, find the reverse exit and target by name and connect them as well.
6. Sweep to update reachable regions.
7. Call the `on_connect` callback.
This process repeats until the stage is complete, no valid source transition is found, or no valid target transition is
found for any source transition. Unlike fill, there is no attempt made to save a failed randomization.

View File

@@ -272,7 +272,6 @@ These packets are sent purely from client to server. They are not accepted by cl
* [Sync](#Sync)
* [LocationChecks](#LocationChecks)
* [LocationScouts](#LocationScouts)
* [UpdateHint](#UpdateHint)
* [StatusUpdate](#StatusUpdate)
* [Say](#Say)
* [GetDataPackage](#GetDataPackage)
@@ -343,33 +342,6 @@ This is useful in cases where an item appears in the game world, such as 'ledge
| locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. |
| create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
### UpdateHint
Sent to the server to update the status of a Hint. The client must be the 'receiving_player' of the Hint, or the update fails.
### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| player | int | The ID of the player whose location is being hinted for. |
| location | int | The ID of the location to update the hint for. If no hint exists for this location, the packet is ignored. |
| status | [HintStatus](#HintStatus) | Optional. If included, sets the status of the hint to this status. Cannot set `HINT_FOUND`, or change the status from `HINT_FOUND`. |
#### HintStatus
An enumeration containing the possible hint states.
```python
import enum
class HintStatus(enum.IntEnum):
HINT_FOUND = 0 # The location has been collected. Status cannot be changed once found.
HINT_UNSPECIFIED = 1 # The receiving player has not specified any status
HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded
HINT_AVOID = 20 # The receiving player has specified that the item is detrimental
HINT_PRIORITY = 30 # The receiving player has specified that the item is needed
```
- Hints for items with `ItemClassification.trap` default to `HINT_AVOID`.
- Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`.
- Hints created with `!hint` or similar (hinting an item for yourself) default to `HINT_PRIORITY`.
- Once a hint is collected, its' status is updated to `HINT_FOUND` automatically, and can no longer be changed.
### StatusUpdate
Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past)
@@ -540,7 +512,7 @@ In JSON this may look like:
| ----- | ----- |
| 0 | Nothing special about this item |
| 0b001 | If set, indicates the item can unlock logical advancement |
| 0b010 | If set, indicates the item is especially useful |
| 0b010 | If set, indicates the item is important but not in a way that unlocks advancement |
| 0b100 | If set, indicates the item is a trap |
### JSONMessagePart
@@ -554,7 +526,6 @@ class JSONMessagePart(TypedDict):
color: Optional[str] # only available if type is a color
flags: Optional[int] # only available if type is an item_id or item_name
player: Optional[int] # only available if type is either item or location
hint_status: Optional[HintStatus] # only available if type is hint_status
```
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
@@ -570,7 +541,6 @@ Possible values for `type` include:
| location_id | Location ID, should be resolved to Location Name |
| location_name | Location Name, not currently used over network, but supported by reference Clients. |
| entrance_name | Entrance Name. No ID mapping exists. |
| hint_status | The [HintStatus](#HintStatus) of the hint. Both `text` and `hint_status` are given. |
| color | Regular text that should be colored. Only `type` that will contain `color` data. |
@@ -674,7 +644,6 @@ class Hint(typing.NamedTuple):
found: bool
entrance: str = ""
item_flags: int = 0
status: HintStatus = HintStatus.HINT_UNSPECIFIED
```
### Data Package Contents

View File

@@ -7,9 +7,7 @@ use that version. These steps are for developers or platforms without compiled r
## General
What you'll need:
* [Python 3.10.11 or newer](https://www.python.org/downloads/), not the Windows Store version
* On Windows, please consider only using the latest supported version in production environments since security
updates for older versions are not easily available.
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
* Python 3.12.x is currently the newest supported version
* pip: included in downloads from python.org, separate in many Linux distributions
* Matching C compiler
@@ -43,9 +41,9 @@ Recommended steps
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
* Run ModuleUpdate.py which will prompt installation of missing modules, press enter to confirm
* In PyCharm: right-click ModuleUpdate.py and select `Run 'ModuleUpdate'`
* Without PyCharm: open a command prompt in the source folder and type `py ModuleUpdate.py`
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
* In PyCharm: right-click Generate.py and select `Run 'Generate'`
* Without PyCharm: open a command prompt in the source folder and type `py Generate.py`
## macOS

View File

@@ -27,14 +27,8 @@
# If you wish to deploy, uncomment the following line and set it to something not easily guessable.
# SECRET_KEY: "Your secret key here"
# Slot limit to post a generation to Generator process pool instead of rolling directly in WebHost process
#JOB_THRESHOLD: 1
# After what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
#JOB_TIME: 600
# Memory limit for Generator processes in bytes, -1 for unlimited. Currently only works on Linux.
#GENERATOR_MEMORY_LIMIT: 4294967296
# TODO
#JOB_THRESHOLD: 2
# waitress uses one thread for I/O, these are for processing of view that get sent
#WAITRESS_THREADS: 10

View File

@@ -248,8 +248,7 @@ will all have the same ID. Name must not be numeric (must contain at least 1 let
Other classifications include:
* `filler`: a regular item or trash item
* `useful`: item that is especially useful. Cannot be placed on excluded or unreachable locations. When combined with
another flag like "progression", it means "an especially useful progression item".
* `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations
* `trap`: negative impact on the player
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
combined with `progression`; see below)
@@ -289,8 +288,8 @@ like entrance randomization in logic.
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295-L296)),
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
There must be one special region, "Menu", from which the logic unfolds. AP assumes that a player will always be able to
return to the "Menu" region by resetting the game ("Save and quit").
### Entrances
@@ -329,9 +328,6 @@ Even doing `state.can_reach_location` or `state.can_reach_entrance` is problemat
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L301),
avoiding the need for indirect conditions at the expense of performance.
### Item Rules
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
@@ -467,7 +463,7 @@ The world has to provide the following things for generation:
* the properties mentioned above
* additions to the item pool
* additions to the regions list: at least one named after the world class's origin_region_name ("Menu" by default)
* additions to the regions list: at least one called "Menu"
* locations placed inside those regions
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
* applying `self.multiworld.push_precollected` for world-defined start inventory
@@ -520,7 +516,7 @@ def generate_early(self) -> None:
```python
def create_regions(self) -> None:
# Add regions to the multiworld. One of them must use the origin_region_name as its name ("Menu" by default).
# Add regions to the multiworld. "Menu" is the required starting point.
# Arguments to Region() are name, player, multiworld, and optionally hint_text
menu_region = Region("Menu", self.player, self.multiworld)
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
@@ -700,92 +696,9 @@ When importing a file that defines a class that inherits from `worlds.AutoWorld.
is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing
world since the namespace is shared with all other logic mixins.
LogicMixin is handy when your logic is more complex than one-to-one location-item relationships.
A game in which "The red key opens the red door" can just express this relationship through a one-line access rule.
But now, consider a game with a heavy focus on combat, where the main logical consideration is which enemies you can
defeat with your current items.
There could be dozens of weapons, armor pieces, or consumables that each improve your ability to defeat
specific enemies to varying degrees. It would be useful to be able to keep track of "defeatable enemies" as a state variable,
and have this variable be recalculated as necessary based on newly collected/removed items.
This is the capability of LogicMixin: Adding custom variables to state that get recalculated as necessary.
In general, a LogicMixin class should have at least one mutable variable that is tracking some custom state per player,
as well as `init_mixin` and `copy_mixin` functions so that this variable gets initialized and copied correctly when
`CollectionState()` and `CollectionState.copy()` are called respectively.
```python
from BaseClasses import CollectionState, MultiWorld
from worlds.AutoWorld import LogicMixin
class MyGameState(LogicMixin):
mygame_defeatable_enemies: Dict[int, Set[str]] # per player
def init_mixin(self, multiworld: MultiWorld) -> None:
# Initialize per player with the corresponding "nothing" value, such as 0 or an empty set.
# You can also use something like Collections.defaultdict
self.mygame_defeatable_enemies = {
player: set() for player in multiworld.get_game_players("My Game")
}
def copy_mixin(self, new_state: CollectionState) -> CollectionState:
# Be careful to make a "deep enough" copy here!
new_state.mygame_defeatable_enemies = {
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
}
```
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
Usually, doing this coincides with an override of `World.collect` and `World.remove`, where the custom state variable
gets recalculated when a relevant item is collected or removed.
```python
# __init__.py
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
if change and item in COMBAT_ITEMS:
state.mygame_defeatable_enemies[self.player] |= get_newly_unlocked_enemies(state)
return change
def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if change and item in COMBAT_ITEMS:
state.mygame_defeatable_enemies[self.player] -= get_newly_locked_enemies(state)
return change
```
Using LogicMixin can greatly slow down your code if you don't use it intelligently. This is because `collect`
and `remove` are called very frequently during fill. If your `collect` & `remove` cause a heavy calculation
every time, your code might end up being *slower* than just doing calculations in your access rules.
One way to optimise recalculations is to make use of the fact that `collect` should only unlock things,
and `remove` should only lock things.
In our example, we have two different functions: `get_newly_unlocked_enemies` and `get_newly_locked_enemies`.
`get_newly_unlocked_enemies` should only consider enemies that are *not already in the set*
and check whether they were **unlocked**.
`get_newly_locked_enemies` should only consider enemies that are *already in the set*
and check whether they **became locked**.
Another impactful way to optimise LogicMixin is to use caching.
Your custom state variables don't actually need to be recalculated on every `collect` / `remove`, because there are
often multiple calls to `collect` / `remove` between access rule calls. Thus, it would be much more efficient to hold
off on recaculating until the an actual access rule call happens.
A common way to realize this is to define a `mygame_state_is_stale` variable that is set to True in `collect`, `remove`,
and `init_mixin`. The calls to the actual recalculating functions are then moved to the start of the relevant
access rules like this:
```python
def can_defeat_enemy(state: CollectionState, player: int, enemy: str) -> bool:
if state.mygame_state_is_stale[player]:
state.mygame_defeatable_enemies[player] = recalculate_defeatable_enemies(state)
state.mygame_state_is_stale[player] = False
return enemy in state.mygame_defeatable_enemies[player]
```
Only use LogicMixin if necessary. There are often other ways to achieve what it does, like making clever use of
`state.prog_items`, using event items, pseudo-regions, etc.
Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified
with the state.
Please do this with caution and only when necessary.
#### pre_fill

View File

@@ -1,447 +0,0 @@
import itertools
import logging
import random
import time
from collections import deque
from collections.abc import Callable, Iterable
from BaseClasses import CollectionState, Entrance, Region, EntranceType
from Options import Accessibility
from worlds.AutoWorld import World
class EntranceRandomizationError(RuntimeError):
pass
class EntranceLookup:
class GroupLookup:
_lookup: dict[int, list[Entrance]]
def __init__(self):
self._lookup = {}
def __len__(self):
return sum(map(len, self._lookup.values()))
def __bool__(self):
return bool(self._lookup)
def __getitem__(self, item: int) -> list[Entrance]:
return self._lookup.get(item, [])
def __iter__(self):
return itertools.chain.from_iterable(self._lookup.values())
def __repr__(self):
return str(self._lookup)
def add(self, entrance: Entrance) -> None:
self._lookup.setdefault(entrance.randomization_group, []).append(entrance)
def remove(self, entrance: Entrance) -> None:
group = self._lookup[entrance.randomization_group]
group.remove(entrance)
if not group:
del self._lookup[entrance.randomization_group]
dead_ends: GroupLookup
others: GroupLookup
_random: random.Random
_expands_graph_cache: dict[Entrance, bool]
_coupled: bool
def __init__(self, rng: random.Random, coupled: bool):
self.dead_ends = EntranceLookup.GroupLookup()
self.others = EntranceLookup.GroupLookup()
self._random = rng
self._expands_graph_cache = {}
self._coupled = coupled
def _can_expand_graph(self, entrance: Entrance) -> bool:
"""
Checks whether an entrance is able to expand the region graph, either by
providing access to randomizable exits or by granting access to items or
regions used in logic conditions.
:param entrance: A randomizable (no parent) region entrance
"""
# we've seen this, return cached result
if entrance in self._expands_graph_cache:
return self._expands_graph_cache[entrance]
visited = set()
q: deque[Region] = deque()
q.append(entrance.connected_region)
while q:
region = q.popleft()
visited.add(region)
# check if the region itself is progression
if region in region.multiworld.indirect_connections:
self._expands_graph_cache[entrance] = True
return True
# check if any placed locations are progression
for loc in region.locations:
if loc.advancement:
self._expands_graph_cache[entrance] = True
return True
# check if there is a randomized exit out (expands the graph directly) or else search any connected
# regions to see if they are/have progression
for exit_ in region.exits:
# randomizable exits which are not reverse of the incoming entrance.
# uncoupled mode is an exception because in this case going back in the door you just came in could
# actually lead somewhere new
if not exit_.connected_region and (not self._coupled or exit_.name != entrance.name):
self._expands_graph_cache[entrance] = True
return True
elif exit_.connected_region and exit_.connected_region not in visited:
q.append(exit_.connected_region)
self._expands_graph_cache[entrance] = False
return False
def add(self, entrance: Entrance) -> None:
lookup = self.others if self._can_expand_graph(entrance) else self.dead_ends
lookup.add(entrance)
def remove(self, entrance: Entrance) -> None:
lookup = self.others if self._can_expand_graph(entrance) else self.dead_ends
lookup.remove(entrance)
def get_targets(
self,
groups: Iterable[int],
dead_end: bool,
preserve_group_order: bool
) -> Iterable[Entrance]:
lookup = self.dead_ends if dead_end else self.others
if preserve_group_order:
for group in groups:
self._random.shuffle(lookup[group])
ret = [entrance for group in groups for entrance in lookup[group]]
else:
ret = [entrance for group in groups for entrance in lookup[group]]
self._random.shuffle(ret)
return ret
def __len__(self):
return len(self.dead_ends) + len(self.others)
class ERPlacementState:
"""The state of an ongoing or completed entrance randomization"""
placements: list[Entrance]
"""The list of randomized Entrance objects which have been connected successfully"""
pairings: list[tuple[str, str]]
"""A list of pairings of connected entrance names, of the form (source_exit, target_entrance)"""
world: World
"""The world which is having its entrances randomized"""
collection_state: CollectionState
"""The CollectionState backing the entrance randomization logic"""
coupled: bool
"""Whether entrance randomization is operating in coupled mode"""
def __init__(self, world: World, coupled: bool):
self.placements = []
self.pairings = []
self.world = world
self.coupled = coupled
self.collection_state = world.multiworld.get_all_state(False, True)
@property
def placed_regions(self) -> set[Region]:
return self.collection_state.reachable_regions[self.world.player]
def find_placeable_exits(self, check_validity: bool) -> list[Entrance]:
if check_validity:
blocked_connections = self.collection_state.blocked_connections[self.world.player]
blocked_connections = sorted(blocked_connections, key=lambda x: x.name)
placeable_randomized_exits = [connection for connection in blocked_connections
if not connection.connected_region
and connection.is_valid_source_transition(self)]
else:
# this is on a beaten minimal attempt, so any exit anywhere is fair game
placeable_randomized_exits = [ex for region in self.world.multiworld.get_regions(self.world.player)
for ex in region.exits if not ex.connected_region]
self.world.random.shuffle(placeable_randomized_exits)
return placeable_randomized_exits
def _connect_one_way(self, source_exit: Entrance, target_entrance: Entrance) -> None:
target_region = target_entrance.connected_region
target_region.entrances.remove(target_entrance)
source_exit.connect(target_region)
self.collection_state.stale[self.world.player] = True
self.placements.append(source_exit)
self.pairings.append((source_exit.name, target_entrance.name))
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance) -> bool:
copied_state = self.collection_state.copy()
# simulated connection. A real connection is unsafe because the region graph is shallow-copied and would
# propagate back to the real multiworld.
copied_state.reachable_regions[self.world.player].add(target_entrance.connected_region)
copied_state.blocked_connections[self.world.player].remove(source_exit)
copied_state.blocked_connections[self.world.player].update(target_entrance.connected_region.exits)
copied_state.update_reachable_regions(self.world.player)
copied_state.sweep_for_advancements()
# test that at there are newly reachable randomized exits that are ACTUALLY reachable
available_randomized_exits = copied_state.blocked_connections[self.world.player]
for _exit in available_randomized_exits:
if _exit.connected_region:
continue
# ignore the source exit, and, if coupled, the reverse exit. They're not actually new
if _exit.name == source_exit.name or (self.coupled and _exit.name == target_entrance.name):
continue
# technically this should be is_valid_source_transition, but that may rely on side effects from
# on_connect, which have not happened here (because we didn't do a real connection, and if we did, we would
# not want them to persist). can_reach is a close enough approximation most of the time.
if _exit.can_reach(copied_state):
return True
return False
def connect(
self,
source_exit: Entrance,
target_entrance: Entrance
) -> tuple[list[Entrance], list[Entrance]]:
"""
Connects a source exit to a target entrance in the graph, accounting for coupling
:returns: The newly placed exits and the dummy entrance(s) which were removed from the graph
"""
source_region = source_exit.parent_region
target_region = target_entrance.connected_region
self._connect_one_way(source_exit, target_entrance)
# if we're doing coupled randomization place the reverse transition as well.
if self.coupled and source_exit.randomization_type == EntranceType.TWO_WAY:
for reverse_entrance in source_region.entrances:
if reverse_entrance.name == source_exit.name:
if reverse_entrance.parent_region:
raise EntranceRandomizationError(
f"Could not perform coupling on {source_exit.name} -> {target_entrance.name} "
f"because the reverse entrance is already parented to "
f"{reverse_entrance.parent_region.name}.")
break
else:
raise EntranceRandomizationError(f"Two way exit {source_exit.name} had no corresponding entrance in "
f"{source_exit.parent_region.name}")
for reverse_exit in target_region.exits:
if reverse_exit.name == target_entrance.name:
if reverse_exit.connected_region:
raise EntranceRandomizationError(
f"Could not perform coupling on {source_exit.name} -> {target_entrance.name} "
f"because the reverse exit is already connected to "
f"{reverse_exit.connected_region.name}.")
break
else:
raise EntranceRandomizationError(f"Two way entrance {target_entrance.name} had no corresponding exit "
f"in {target_region.name}.")
self._connect_one_way(reverse_exit, reverse_entrance)
return [source_exit, reverse_exit], [target_entrance, reverse_entrance]
return [source_exit], [target_entrance]
def bake_target_group_lookup(world: World, get_target_groups: Callable[[int], list[int]]) \
-> dict[int, list[int]]:
"""
Applies a transformation to all known entrance groups on randomizable exists to build a group lookup table.
:param world: Your World instance
:param get_target_groups: Function to call that returns the groups that a specific group type is allowed to
connect to
"""
unique_groups = { entrance.randomization_group for entrance in world.multiworld.get_entrances(world.player)
if entrance.parent_region and not entrance.connected_region }
return { group: get_target_groups(group) for group in unique_groups }
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None) -> None:
"""
Given an entrance in a "vanilla" region graph, splits that entrance to prepare it for randomization
in randomize_entrances. This should be done after setting the type and group of the entrance.
:param entrance: The entrance which will be disconnected in preparation for randomization.
:param target_group: The group to assign to the created ER target. If not specified, the group from
the original entrance will be copied.
"""
child_region = entrance.connected_region
parent_region = entrance.parent_region
# disconnect the edge
child_region.entrances.remove(entrance)
entrance.connected_region = None
# create the needed ER target
if entrance.randomization_type == EntranceType.TWO_WAY:
# for 2-ways, create a target in the parent region with a matching name to support coupling.
# targets in the child region will be created when the other direction edge is disconnected
target = parent_region.create_er_target(entrance.name)
else:
# for 1-ways, the child region needs a target and coupling/naming is not a concern
target = child_region.create_er_target(child_region.name)
target.randomization_type = entrance.randomization_type
target.randomization_group = target_group or entrance.randomization_group
def randomize_entrances(
world: World,
coupled: bool,
target_group_lookup: dict[int, list[int]],
preserve_group_order: bool = False,
er_targets: list[Entrance] | None = None,
exits: list[Entrance] | None = None,
on_connect: Callable[[ERPlacementState, list[Entrance]], None] | None = None
) -> ERPlacementState:
"""
Randomizes Entrances for a single world in the multiworld.
:param world: Your World instance
:param coupled: Whether connected entrances should be coupled to go in both directions
:param target_group_lookup: Map from each group to a list of the groups that it can be connect to. Every group
used on an exit must be provided and must map to at least one other group. The default
group is 0.
:param preserve_group_order: Whether the order of groupings should be preserved for the returned target_groups
:param er_targets: The list of ER targets (Entrance objects with no parent region) to use for randomization.
Remember to be deterministic! If not provided, automatically discovers all valid targets
in your world.
:param exits: The list of exits (Entrance objects with no target region) to use for randomization.
Remember to be deterministic! If not provided, automatically discovers all valid exits in your world.
:param on_connect: A callback function which allows specifying side effects after a placement is completed
successfully and the underlying collection state has been updated.
"""
if not world.explicit_indirect_conditions:
raise EntranceRandomizationError("Entrance randomization requires explicit indirect conditions in order "
+ "to correctly analyze whether dead end regions can be required in logic.")
start_time = time.perf_counter()
er_state = ERPlacementState(world, coupled)
entrance_lookup = EntranceLookup(world.random, coupled)
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
perform_validity_check = True
def do_placement(source_exit: Entrance, target_entrance: Entrance) -> None:
placed_exits, removed_entrances = er_state.connect(source_exit, target_entrance)
# remove the placed targets from consideration
for entrance in removed_entrances:
entrance_lookup.remove(entrance)
# propagate new connections
er_state.collection_state.update_reachable_regions(world.player)
er_state.collection_state.sweep_for_advancements()
if on_connect:
on_connect(er_state, placed_exits)
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
nonlocal perform_validity_check
placeable_exits = er_state.find_placeable_exits(perform_validity_check)
for source_exit in placeable_exits:
target_groups = target_group_lookup[source_exit.randomization_group]
for target_entrance in entrance_lookup.get_targets(target_groups, dead_end, preserve_group_order):
# when requiring new exits, ideally we would like to make it so that every placement increases
# (or keeps the same number of) reachable exits. The goal is to continue to expand the search space
# so that we do not crash. In the interest of performance and bias reduction, generally, just checking
# that we are going to a new region is a good approximation. however, we should take extra care on the
# very last exit and check whatever exits we open up are functionally accessible.
# this requirement can be ignored on a beaten minimal, islands are no issue there.
exit_requirement_satisfied = (not perform_validity_check or not require_new_exits
or target_entrance.connected_region not in er_state.placed_regions)
needs_speculative_sweep = (not dead_end and require_new_exits and perform_validity_check
and len(placeable_exits) == 1)
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
if (needs_speculative_sweep
and not er_state.test_speculative_connection(source_exit, target_entrance)):
continue
do_placement(source_exit, target_entrance)
return True
else:
# no source exits had any valid target so this stage is deadlocked. retries may be implemented if early
# deadlocking is a frequent issue.
lookup = entrance_lookup.dead_ends if dead_end else entrance_lookup.others
# if we're in a stage where we're trying to get to new regions, we could also enter this
# branch in a success state (when all regions of the preferred type have been placed, but there are still
# additional unplaced entrances into those regions)
if require_new_exits:
if all(e.connected_region in er_state.placed_regions for e in lookup):
return False
# if we're on minimal accessibility and can guarantee the game is beatable,
# we can prevent a failure by bypassing future validity checks. this check may be
# expensive; fortunately we only have to do it once
if perform_validity_check and world.options.accessibility == Accessibility.option_minimal \
and world.multiworld.has_beaten_game(er_state.collection_state, world.player):
# ensure that we have enough locations to place our progression
accessible_location_count = 0
prog_item_count = sum(er_state.collection_state.prog_items[world.player].values())
# short-circuit location checking in this case
if prog_item_count == 0:
return True
for region in er_state.placed_regions:
for loc in region.locations:
if loc.can_reach(er_state.collection_state):
accessible_location_count += 1
if accessible_location_count >= prog_item_count:
perform_validity_check = False
# pretend that this was successful to retry the current stage
return True
unplaced_entrances = [entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region]
unplaced_exits = [exit_ for region in world.multiworld.get_regions(world.player)
for exit_ in region.exits if not exit_.connected_region]
entrance_kind = "dead ends" if dead_end else "non-dead ends"
region_access_requirement = "requires" if require_new_exits else "does not require"
raise EntranceRandomizationError(
f"None of the available entrances are valid targets for the available exits.\n"
f"Randomization stage is placing {entrance_kind} and {region_access_requirement} "
f"new region/exit access by default\n"
f"Placeable entrances: {lookup}\n"
f"Placeable exits: {placeable_exits}\n"
f"All unplaced entrances: {unplaced_entrances}\n"
f"All unplaced exits: {unplaced_exits}")
if not er_targets:
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
if not exits:
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
if len(er_targets) != len(exits):
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
for entrance in er_targets:
entrance_lookup.add(entrance)
# place the menu region and connected start region(s)
er_state.collection_state.update_reachable_regions(world.player)
# stage 1 - try to place all the non-dead-end entrances
while entrance_lookup.others:
if not find_pairing(dead_end=False, require_new_exits=True):
break
# stage 2 - try to place all the dead-end entrances
while entrance_lookup.dead_ends:
if not find_pairing(dead_end=True, require_new_exits=True):
break
# stage 3 - all the regions should be placed at this point. We now need to connect dangling edges
# stage 3a - get the rest of the dead ends (e.g. second entrances into already-visited regions)
# doing this before the non-dead-ends is important to ensure there are enough connections to
# go around
while entrance_lookup.dead_ends:
find_pairing(dead_end=True, require_new_exits=False)
# stage 3b - tie all the other loose ends connecting visited regions to each other
while entrance_lookup.others:
find_pairing(dead_end=False, require_new_exits=False)
running_time = time.perf_counter() - start_time
if running_time > 1.0:
logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player},"
f"named {world.multiworld.player_name[world.player]}")
return er_state

View File

@@ -186,11 +186,6 @@ Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Arc
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apcvcotm"; ValueData: "{#MyAppName}cvcotmpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cvcotmpatch"; ValueData: "Archipelago Castlevania Circle of the Moon Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cvcotmpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cvcotmpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apmm2"; ValueData: "{#MyAppName}mm2patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm2patch"; ValueData: "Archipelago Mega Man 2 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm2patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";

202
kvui.py
View File

@@ -3,8 +3,6 @@ import logging
import sys
import typing
import re
import io
import pkgutil
from collections import deque
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
@@ -14,7 +12,10 @@ if sys.platform == "win32":
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
ctypes.windll.shcore.SetProcessDpiAwareness(0)
try:
ctypes.windll.shcore.SetProcessDpiAwareness(0)
except FileNotFoundError: # shcore may not be found on <= Windows 7
pass # TODO: remove silent except when Python 3.8 is phased out.
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
@@ -36,11 +37,10 @@ from kivy.app import App
from kivy.core.window import Window
from kivy.core.clipboard import Clipboard
from kivy.core.text.markup import MarkupLabel
from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
from kivy.base import ExceptionHandler, ExceptionManager
from kivy.clock import Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.metrics import dp
from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget
@@ -55,7 +55,6 @@ from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar
from kivy.uix.dropdown import DropDown
from kivy.utils import escape_markup
from kivy.lang import Builder
from kivy.uix.recycleview.views import RecycleDataViewBehavior
@@ -64,12 +63,10 @@ from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.animation import Animation
from kivy.uix.popup import Popup
from kivy.uix.dropdown import DropDown
from kivy.uix.image import AsyncImage
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType, HintStatus
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType
from Utils import async_start, get_input_text_from_response
if typing.TYPE_CHECKING:
@@ -306,55 +303,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Respond to the selection of items in the view. """
self.selected = is_selected
class AutocompleteHintInput(TextInput):
min_chars = NumericProperty(3)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dropdown = DropDown()
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
self.bind(on_text_validate=self.on_message)
def on_message(self, instance):
App.get_running_app().commandprocessor("!hint "+instance.text)
def on_text(self, instance, value):
if len(value) >= self.min_chars:
self.dropdown.clear_widgets()
ctx: context_type = App.get_running_app().ctx
if not ctx.game:
return
item_names = ctx.item_names._game_store[ctx.game].values()
def on_press(button: Button):
split_text = MarkupLabel(text=button.text).markup
return self.dropdown.select("".join(text_frag for text_frag in split_text
if not text_frag.startswith("[")))
lowered = value.lower()
for item_name in item_names:
try:
index = item_name.lower().index(lowered)
except ValueError:
pass # substring not found
else:
text = escape_markup(item_name)
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
btn.bind(on_release=on_press)
self.dropdown.add_widget(btn)
if not self.dropdown.attach_to:
self.dropdown.open(self)
else:
self.dropdown.dismiss()
class HintLabel(RecycleDataViewBehavior, BoxLayout):
selected = BooleanProperty(False)
striped = BooleanProperty(False)
index = None
dropdown: DropDown
def __init__(self):
super(HintLabel, self).__init__()
@@ -363,32 +316,10 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.finding_text = ""
self.location_text = ""
self.entrance_text = ""
self.status_text = ""
self.hint = {}
self.found_text = ""
for child in self.children:
child.bind(texture_size=self.set_height)
ctx = App.get_running_app().ctx
self.dropdown = DropDown()
def set_value(button):
self.dropdown.select(button.status)
def select(instance, data):
ctx.update_hint(self.hint["location"],
self.hint["finding_player"],
data)
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
name = status_names[status]
status_button = Button(text=name, size_hint_y=None, height=dp(50))
status_button.status = status
status_button.bind(on_release=set_value)
self.dropdown.add_widget(status_button)
self.dropdown.bind(on_select=select)
def set_height(self, instance, value):
self.height = max([child.texture_size[1] for child in self.children])
@@ -400,8 +331,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.finding_text = data["finding"]["text"]
self.location_text = data["location"]["text"]
self.entrance_text = data["entrance"]["text"]
self.status_text = data["status"]["text"]
self.hint = data["status"]["hint"]
self.found_text = data["found"]["text"]
self.height = self.minimum_height
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
@@ -411,21 +341,13 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
return True
if self.index: # skip header
if self.collide_point(*touch.pos):
status_label = self.ids["status"]
if status_label.collide_point(*touch.pos):
if self.hint["status"] == HintStatus.HINT_FOUND:
return
ctx = App.get_running_app().ctx
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
# open a dropdown
self.dropdown.open(self.ids["status"])
elif self.selected:
if self.selected:
self.parent.clear_selection()
else:
text = "".join((self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
self.finding_text, "\'s World", (" at " + self.entrance_text)
if self.entrance_text != "Vanilla"
else "", ". (", self.status_text.lower(), ")"))
else "", ". (", self.found_text.lower(), ")"))
temp = MarkupLabel(text).markup
text = "".join(
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
@@ -439,16 +361,18 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
for child in self.children:
if child.collide_point(*touch.pos):
key = child.sort_key
if key == "status":
parent.hint_sorter = lambda element: element["status"]["hint"]["status"]
else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
if key == parent.sort_key:
# second click reverses order
parent.reversed = not parent.reversed
else:
parent.sort_key = key
parent.reversed = False
App.get_running_app().update_hints()
break
else:
logging.warning("Did not find clicked header for sorting.")
App.get_running_app().update_hints()
def apply_selection(self, rv, index, is_selected):
""" Respond to the selection of items in the view. """
@@ -615,10 +539,8 @@ class GameManager(App):
# show Archipelago tab if other logging is present
self.tabs.add_widget(panel)
hint_panel = self.add_client_tab("Hints", HintLayout())
self.hint_log = HintLog(self.json_to_kivy_parser)
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
self.log_panels["Hints"] = hint_panel.content
hint_panel.content.add_widget(self.hint_log)
if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago"
@@ -744,8 +666,8 @@ class GameManager(App):
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
def update_hints(self):
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
self.hint_log.refresh_hints(hints)
hints = self.ctx.stored_data[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"]
self.log_panels["Hints"].refresh_hints(hints)
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
@@ -800,34 +722,6 @@ class UILog(RecycleView):
element.height = element.texture_size[1]
class HintLayout(BoxLayout):
orientation = "vertical"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
boxlayout.add_widget(AutocompleteHintInput())
self.add_widget(boxlayout)
status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "Found",
HintStatus.HINT_UNSPECIFIED: "Unspecified",
HintStatus.HINT_NO_PRIORITY: "No Priority",
HintStatus.HINT_AVOID: "Avoid",
HintStatus.HINT_PRIORITY: "Priority",
}
status_colors: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "green",
HintStatus.HINT_UNSPECIFIED: "white",
HintStatus.HINT_NO_PRIORITY: "cyan",
HintStatus.HINT_AVOID: "salmon",
HintStatus.HINT_PRIORITY: "plum",
}
class HintLog(RecycleView):
header = {
"receiving": {"text": "[u]Receiving Player[/u]"},
@@ -835,13 +729,12 @@ class HintLog(RecycleView):
"finding": {"text": "[u]Finding Player[/u]"},
"location": {"text": "[u]Location[/u]"},
"entrance": {"text": "[u]Entrance[/u]"},
"status": {"text": "[u]Status[/u]",
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
"found": {"text": "[u]Status[/u]"},
"striped": True,
}
sort_key: str = ""
reversed: bool = True
reversed: bool = False
def __init__(self, parser):
super(HintLog, self).__init__()
@@ -849,18 +742,8 @@ class HintLog(RecycleView):
self.parser = parser
def refresh_hints(self, hints):
if not hints: # Fix the scrolling looking visually wrong in some edge cases
self.scroll_y = 1.0
data = []
ctx = App.get_running_app().ctx
for hint in hints:
if not hint.get("status"): # Allows connecting to old servers
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
hint_status_node = self.parser.handle_node({"type": "color",
"color": status_colors.get(hint["status"], "red"),
"text": status_names.get(hint["status"], "Unknown")})
if hint["status"] != HintStatus.HINT_FOUND and ctx.slot_concerns_self(hint["receiving_player"]):
hint_status_node = f"[u]{hint_status_node}[/u]"
data.append({
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
"item": {"text": self.parser.handle_node({
@@ -878,10 +761,9 @@ class HintLog(RecycleView):
"entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
"color": "blue", "text": hint["entrance"]
if hint["entrance"] else "Vanilla"})},
"status": {
"text": hint_status_node,
"hint": hint,
},
"found": {
"text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
"text": "Found" if hint["found"] else "Not Found"})},
})
data.sort(key=self.hint_sorter, reverse=self.reversed)
@@ -892,7 +774,7 @@ class HintLog(RecycleView):
@staticmethod
def hint_sorter(element: dict) -> str:
return element["status"]["hint"]["status"] # By status by default
return ""
def fix_heights(self):
"""Workaround fix for divergent texture and layout heights"""
@@ -901,40 +783,6 @@ class HintLog(RecycleView):
element.height = max_height
class ApAsyncImage(AsyncImage):
def is_uri(self, filename: str) -> bool:
if filename.startswith("ap:"):
return True
else:
return super().is_uri(filename)
class ImageLoaderPkgutil(ImageLoaderBase):
def load(self, filename: str) -> typing.List[ImageData]:
# take off the "ap:" prefix
module, path = filename[3:].split("/", 1)
data = pkgutil.get_data(module, path)
return self._bytes_to_data(data)
def _bytes_to_data(self, data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
return loader.load(loader, io.BytesIO(data))
# grab the default loader method so we can override it but use it as a fallback
_original_image_loader_load = ImageLoader.load
def load_override(filename: str, default_load=_original_image_loader_load, **kwargs):
if filename.startswith("ap:"):
return ImageLoaderPkgutil(filename)
else:
return default_load(filename, **kwargs)
ImageLoader.load = load_override
class E(ExceptionHandler):
logger = logging.getLogger("Client")

View File

@@ -7,7 +7,7 @@ schema>=0.7.7
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.2.2
certifi>=2024.12.14
certifi>=2024.8.30
cython>=3.0.11
cymem>=2.0.8
orjson>=3.10.7

View File

@@ -7,7 +7,6 @@ import os
import os.path
import shutil
import sys
import types
import typing
import warnings
from enum import IntEnum
@@ -163,13 +162,8 @@ class Group:
else:
# assign value, try to upcast to type hint
annotation = self.get_type_hints().get(k, None)
candidates = (
[] if annotation is None else (
typing.get_args(annotation)
if typing.get_origin(annotation) in (Union, types.UnionType)
else [annotation]
)
)
candidates = [] if annotation is None else \
typing.get_args(annotation) if typing.get_origin(annotation) is Union else [annotation]
none_type = type(None)
for cls in candidates:
assert isinstance(cls, type), f"{self.__class__.__name__}.{k}: type {cls} not supported in settings"
@@ -599,7 +593,6 @@ class ServerOptions(Group):
savefile: Optional[str] = None
disable_save: bool = False
loglevel: str = "info"
logtime: bool = False
server_password: Optional[ServerPassword] = None
disable_item_cheat: Union[DisableItemCheat, bool] = False
location_check_points: LocationCheckPoints = LocationCheckPoints(1)

View File

@@ -321,7 +321,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
f"{ex}\nPlease close all AP instances and delete manually.")
# regular cx build
self.buildtime = datetime.datetime.now(datetime.timezone.utc)
self.buildtime = datetime.datetime.utcnow()
super().run()
# manually copy built modules to lib folder. cx_Freeze does not know they exist.
@@ -634,7 +634,7 @@ cx_Freeze.setup(
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas", "zstandard"],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["worlds", "sc2"],
"zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
"include_msvcr": False,
"replace_paths": ["*."],

View File

@@ -1,387 +0,0 @@
import unittest
from enum import IntEnum
from BaseClasses import Region, EntranceType, MultiWorld, Entrance
from entrance_rando import disconnect_entrance_for_randomization, randomize_entrances, EntranceRandomizationError, \
ERPlacementState, EntranceLookup, bake_target_group_lookup
from Options import Accessibility
from test.general import generate_test_multiworld, generate_locations, generate_items
from worlds.generic.Rules import set_rule
class ERTestGroups(IntEnum):
LEFT = 1
RIGHT = 2
TOP = 3
BOTTOM = 4
directionally_matched_group_lookup = {
ERTestGroups.LEFT: [ERTestGroups.RIGHT],
ERTestGroups.RIGHT: [ERTestGroups.LEFT],
ERTestGroups.TOP: [ERTestGroups.BOTTOM],
ERTestGroups.BOTTOM: [ERTestGroups.TOP]
}
def generate_entrance_pair(region: Region, name_suffix: str, group: int):
lx = region.create_exit(region.name + name_suffix)
lx.randomization_group = group
lx.randomization_type = EntranceType.TWO_WAY
le = region.create_er_target(region.name + name_suffix)
le.randomization_group = group
le.randomization_type = EntranceType.TWO_WAY
def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length: int, region_size: int = 0,
region_type: type[Region] = Region):
"""
Generates a grid-like region structure for ER testing, where menu is connected to the top-left region, and each
region "in vanilla" has 2 2-way exits going either down or to the right, until reaching the goal region in the
bottom right
"""
for row in range(grid_side_length):
for col in range(grid_side_length):
index = row * grid_side_length + col
name = f"region{index}"
region = region_type(name, 1, multiworld)
multiworld.regions.append(region)
generate_locations(region_size, 1, region=region, tag=f"_{name}")
if row == 0 and col == 0:
multiworld.get_region("Menu", 1).connect(region)
if col != 0:
generate_entrance_pair(region, "_left", ERTestGroups.LEFT)
if col != grid_side_length - 1:
generate_entrance_pair(region, "_right", ERTestGroups.RIGHT)
if row != 0:
generate_entrance_pair(region, "_top", ERTestGroups.TOP)
if row != grid_side_length - 1:
generate_entrance_pair(region, "_bottom", ERTestGroups.BOTTOM)
class TestEntranceLookup(unittest.TestCase):
def test_shuffled_targets(self):
"""tests that get_targets shuffles targets between groups when requested"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region]
for entrance in er_targets:
lookup.add(entrance)
retrieved_targets = lookup.get_targets([ERTestGroups.TOP, ERTestGroups.BOTTOM],
False, False)
prev = None
group_order = [prev := group.randomization_group for group in retrieved_targets
if prev != group.randomization_group]
# technically possible that group order may not be shuffled, by some small chance, on some seeds. but generally
# a shuffled list should alternate more frequently which is the desired behavior here
self.assertGreater(len(group_order), 2)
def test_ordered_targets(self):
"""tests that get_targets does not shuffle targets between groups when requested"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region]
for entrance in er_targets:
lookup.add(entrance)
retrieved_targets = lookup.get_targets([ERTestGroups.TOP, ERTestGroups.BOTTOM],
False, True)
prev = None
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
class TestBakeTargetGroupLookup(unittest.TestCase):
def test_lookup_generation(self):
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
world = multiworld.worlds[1]
expected = {
ERTestGroups.LEFT: [-ERTestGroups.LEFT],
ERTestGroups.RIGHT: [-ERTestGroups.RIGHT],
ERTestGroups.TOP: [-ERTestGroups.TOP],
ERTestGroups.BOTTOM: [-ERTestGroups.BOTTOM]
}
actual = bake_target_group_lookup(world, lambda g: [-g])
self.assertEqual(expected, actual)
class TestDisconnectForRandomization(unittest.TestCase):
def test_disconnect_default_2way(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
r2 = Region("r2", 1, multiworld)
e = r1.create_exit("e")
e.randomization_type = EntranceType.TWO_WAY
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e)
self.assertIsNone(e.connected_region)
self.assertEqual([], r2.entrances)
self.assertEqual(1, len(r1.exits))
self.assertEqual(e, r1.exits[0])
self.assertEqual(1, len(r1.entrances))
self.assertIsNone(r1.entrances[0].parent_region)
self.assertEqual("e", r1.entrances[0].name)
self.assertEqual(EntranceType.TWO_WAY, r1.entrances[0].randomization_type)
self.assertEqual(1, r1.entrances[0].randomization_group)
def test_disconnect_default_1way(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
r2 = Region("r2", 1, multiworld)
e = r1.create_exit("e")
e.randomization_type = EntranceType.ONE_WAY
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e)
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
self.assertEqual(1, len(r1.exits))
self.assertEqual(e, r1.exits[0])
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(1, r2.entrances[0].randomization_group)
def test_disconnect_uses_alternate_group(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
r2 = Region("r2", 1, multiworld)
e = r1.create_exit("e")
e.randomization_type = EntranceType.ONE_WAY
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e, 2)
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
self.assertEqual(1, len(r1.exits))
self.assertEqual(e, r1.exits[0])
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(2, r2.entrances[0].randomization_group)
class TestRandomizeEntrances(unittest.TestCase):
def test_determinism(self):
"""tests that the same output is produced for the same input"""
multiworld1 = generate_test_multiworld()
generate_disconnected_region_grid(multiworld1, 5)
multiworld2 = generate_test_multiworld()
generate_disconnected_region_grid(multiworld2, 5)
result1 = randomize_entrances(multiworld1.worlds[1], False, directionally_matched_group_lookup)
result2 = randomize_entrances(multiworld2.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual(result1.pairings, result2.pairings)
for e1, e2 in zip(result1.placements, result2.placements):
self.assertEqual(e1.name, e2.name)
self.assertEqual(e1.parent_region.name, e1.parent_region.name)
self.assertEqual(e1.connected_region.name, e2.connected_region.name)
def test_all_entrances_placed(self):
"""tests that all entrances and exits were placed, all regions are connected, and no dangling edges exist"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual([], [entrance for region in multiworld.get_regions()
for entrance in region.entrances if not entrance.parent_region])
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
# 5x5 grid + menu
self.assertEqual(26, len(result.placed_regions))
self.assertEqual(80, len(result.pairings))
self.assertEqual(80, len(result.placements))
def test_coupling(self):
"""tests that in coupled mode, all 2 way transitions have an inverse"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
seen_placement_count = 0
def verify_coupled(_: ERPlacementState, placed_entrances: list[Entrance]):
nonlocal seen_placement_count
seen_placement_count += len(placed_entrances)
self.assertEqual(2, len(placed_entrances))
self.assertEqual(placed_entrances[0].parent_region, placed_entrances[1].connected_region)
self.assertEqual(placed_entrances[1].parent_region, placed_entrances[0].connected_region)
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup,
on_connect=verify_coupled)
# if we didn't visit every placement the verification on_connect doesn't really mean much
self.assertEqual(len(result.placements), seen_placement_count)
def test_uncoupled(self):
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
seen_placement_count = 0
def verify_uncoupled(state: ERPlacementState, placed_entrances: list[Entrance]):
nonlocal seen_placement_count
seen_placement_count += len(placed_entrances)
self.assertEqual(1, len(placed_entrances))
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup,
on_connect=verify_uncoupled)
# if we didn't visit every placement the verification on_connect doesn't really mean much
self.assertEqual(len(result.placements), seen_placement_count)
def test_oneway_twoway_pairing(self):
"""tests that 1 ways are only paired to 1 ways and 2 ways are only paired to 2 ways"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
region26 = Region("region26", 1, multiworld)
multiworld.regions.append(region26)
for index, region in enumerate(["region4", "region20", "region24"]):
x = multiworld.get_region(region, 1).create_exit(f"{region}_bottom_1way")
x.randomization_type = EntranceType.ONE_WAY
x.randomization_group = ERTestGroups.BOTTOM
e = region26.create_er_target(f"region26_top_1way{index}")
e.randomization_type = EntranceType.ONE_WAY
e.randomization_group = ERTestGroups.TOP
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
for exit_name, entrance_name in result.pairings:
# we have labeled our entrances in such a way that all the 1 way entrances have 1way in the name,
# so test for that since the ER target will have been discarded
if "1way" in exit_name:
self.assertIn("1way", entrance_name)
def test_group_constraints_satisfied(self):
"""tests that all grouping constraints are satisfied"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
for exit_name, entrance_name in result.pairings:
# we have labeled our entrances in such a way that all the entrances contain their group in the name
# so test for that since the ER target will have been discarded
if "top" in exit_name:
self.assertIn("bottom", entrance_name)
if "bottom" in exit_name:
self.assertIn("top", entrance_name)
if "left" in exit_name:
self.assertIn("right", entrance_name)
if "right" in exit_name:
self.assertIn("left", entrance_name)
def test_minimal_entrance_rando(self):
"""tests that entrance randomization can complete with minimal accessibility and unreachable exits"""
multiworld = generate_test_multiworld()
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
generate_disconnected_region_grid(multiworld, 5, 1)
prog_items = generate_items(10, 1, True)
multiworld.itempool += prog_items
filler_items = generate_items(15, 1, False)
multiworld.itempool += filler_items
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual([], [entrance for region in multiworld.get_regions()
for entrance in region.entrances if not entrance.parent_region])
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
def test_restrictive_region_requirement_does_not_fail(self):
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 2, 1)
region = Region("region4", 1, multiworld)
multiworld.regions.append(region)
generate_entrance_pair(multiworld.get_region("region0", 1), "_right2", ERTestGroups.RIGHT)
generate_entrance_pair(region, "_left", ERTestGroups.LEFT)
blocked_exits = ["region1_left", "region1_bottom",
"region2_top", "region2_right",
"region3_left", "region3_top"]
for exit_name in blocked_exits:
blocked_exit = multiworld.get_entrance(exit_name, 1)
blocked_exit.access_rule = lambda state: state.can_reach_region("region4", 1)
multiworld.register_indirect_condition(region, blocked_exit)
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup)
# verifying that we did in fact place region3 adjacent to region0 to unblock all the other connections
# (and implicitly, that ER didn't fail)
self.assertTrue(("region0_right", "region4_left") in result.pairings
or ("region0_right2", "region4_left") in result.pairings)
def test_fails_when_mismatched_entrance_and_exit_count(self):
"""tests that entrance randomization fast-fails if the input exit and entrance count do not match"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
multiworld.get_region("region1", 1).create_exit("extra")
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup)
def test_fails_when_some_unreachable_exit(self):
"""tests that entrance randomization fails if an exit is never reachable (non-minimal accessibility)"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup)
def test_fails_when_some_unconnectable_exit(self):
"""tests that entrance randomization fails if an exit can't be made into a valid placement (non-minimal)"""
class CustomEntrance(Entrance):
def can_connect_to(self, other: Entrance, dead_end: bool, er_state: "ERPlacementState") -> bool:
if other.name == "region1_right":
return False
class CustomRegion(Region):
entrance_type = CustomEntrance
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5, region_type=CustomRegion)
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup)
def test_minimal_er_fails_when_not_enough_locations_to_fit_progression(self):
"""
tests that entrance randomization fails in minimal accessibility if there are not enough locations
available to place all progression items locally
"""
multiworld = generate_test_multiworld()
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
generate_disconnected_region_grid(multiworld, 5, 1)
prog_items = generate_items(30, 1, True)
multiworld.itempool += prog_items
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup)

View File

@@ -52,68 +52,3 @@ 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}")
def test_explicit_indirect_conditions_spheres(self):
"""Tests that worlds using explicit indirect conditions produce identical spheres as when using implicit
indirect conditions"""
# Because the iteration order of blocked_connections in CollectionState.update_reachable_regions() is
# nondeterministic, this test may sometimes pass with the same seed even when there are missing indirect
# conditions.
for game_name, world_type in AutoWorldRegister.world_types.items():
multiworld = setup_solo_multiworld(world_type)
world = multiworld.get_game_worlds(game_name)[0]
if not world.explicit_indirect_conditions:
# The world does not use explicit indirect conditions, so it can be skipped.
continue
# The world may override explicit_indirect_conditions as a property that cannot be set, so try modifying it.
try:
world.explicit_indirect_conditions = False
world.explicit_indirect_conditions = True
except Exception:
# Could not modify the attribute, so skip this world.
with self.subTest(game=game_name, skipped="world.explicit_indirect_conditions could not be set"):
continue
with self.subTest(game=game_name, seed=multiworld.seed):
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
# Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked
# is nondeterministic and may vary between runs with the same seed.
explicit_spheres = list(multiworld.get_spheres())
# Disable explicit indirect conditions and produce a second list of spheres.
world.explicit_indirect_conditions = False
implicit_spheres = list(multiworld.get_spheres())
# Both lists should be identical.
if explicit_spheres == implicit_spheres:
# Test passed.
continue
# Find the first sphere that was different and provide a useful failure message.
zipped = zip(explicit_spheres, implicit_spheres)
for sphere_num, (sphere_explicit, sphere_implicit) in enumerate(zipped, start=1):
# Each sphere created with explicit indirect conditions should be identical to the sphere created
# with implicit indirect conditions.
if sphere_explicit != sphere_implicit:
reachable_only_with_implicit = sorted(sphere_implicit - sphere_explicit)
if reachable_only_with_implicit:
locations_and_parents = [(loc, loc.parent_region) for loc in reachable_only_with_implicit]
self.fail(f"Sphere {sphere_num} created with explicit indirect conditions did not contain"
f" the same locations as sphere {sphere_num} created with implicit indirect"
f" conditions. There may be missing indirect conditions for connections to the"
f" locations' parent regions or connections from other regions which connect to"
f" these regions."
f"\nLocations that should have been reachable in sphere {sphere_num} and their"
f" parent regions:"
f"\n{locations_and_parents}")
else:
# Some locations were only present in the sphere created with explicit indirect conditions.
# This should not happen because missing indirect conditions should only reduce
# accessibility, not increase accessibility.
reachable_only_with_explicit = sorted(sphere_explicit - sphere_implicit)
self.fail(f"Sphere {sphere_num} created with explicit indirect conditions contained more"
f" locations than sphere {sphere_num} created with implicit indirect conditions."
f" This should not happen."
f"\nUnexpectedly reachable locations in sphere {sphere_num}:"
f"\n{reachable_only_with_explicit}")
self.fail("Unreachable")

View File

@@ -80,21 +80,3 @@ class TestBase(unittest.TestCase):
call_all(multiworld, step)
self.assertEqual(created_items, multiworld.itempool,
f"{game_name} modified the itempool during {step}")
def test_locality_not_modified(self):
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "generate_basic", "pre_fill")
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
local_items = multiworld.worlds[1].options.local_items.value.copy()
non_local_items = multiworld.worlds[1].options.non_local_items.value.copy()
for step in additional_steps:
with self.subTest("step", step=step):
call_all(multiworld, step)
self.assertEqual(local_items, multiworld.worlds[1].options.local_items.value,
f"{game_name} modified local_items during {step}")
self.assertEqual(non_local_items, multiworld.worlds[1].options.non_local_items.value,
f"{game_name} modified non_local_items during {step}")

View File

@@ -1,16 +0,0 @@
from unittest import TestCase
from settings import Group
from worlds.AutoWorld import AutoWorldRegister
class TestSettings(TestCase):
def test_settings_can_update(self) -> None:
"""
Test that world settings can update.
"""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=game_name):
if world_type.settings is not None:
assert isinstance(world_type.settings, Group)
world_type.settings.update({}) # a previous bug had a crash in this call to update

View File

@@ -115,7 +115,6 @@ class Base:
def test_get_for_player(self) -> None:
self.assertEqual(self.store.get_for_player(3), {4: {9}})
self.assertEqual(self.store.get_for_player(1), {1: {13}, 2: {22, 23}})
self.assertEqual(self.store.get_for_player(9999), {})
def test_get_checked(self) -> None:
self.assertEqual(self.store.get_checked(full_state, 0, 1), [11, 12, 13])
@@ -123,48 +122,18 @@ class Base:
self.assertEqual(self.store.get_checked(empty_state, 0, 1), [])
self.assertEqual(self.store.get_checked(full_state, 0, 3), [9])
def test_get_checked_exception(self) -> None:
with self.assertRaises(KeyError):
self.store.get_checked(empty_state, 0, 9999)
bad_state = {(0, 6): {1}}
with self.assertRaises(KeyError):
self.store.get_checked(bad_state, 0, 6)
bad_state = {(0, 9999): set()}
with self.assertRaises(KeyError):
self.store.get_checked(bad_state, 0, 9999)
def test_get_missing(self) -> None:
self.assertEqual(self.store.get_missing(full_state, 0, 1), [])
self.assertEqual(self.store.get_missing(one_state, 0, 1), [11, 13])
self.assertEqual(self.store.get_missing(empty_state, 0, 1), [11, 12, 13])
self.assertEqual(self.store.get_missing(empty_state, 0, 3), [9])
def test_get_missing_exception(self) -> None:
with self.assertRaises(KeyError):
self.store.get_missing(empty_state, 0, 9999)
bad_state = {(0, 6): {1}}
with self.assertRaises(KeyError):
self.store.get_missing(bad_state, 0, 6)
bad_state = {(0, 9999): set()}
with self.assertRaises(KeyError):
self.store.get_missing(bad_state, 0, 9999)
def test_get_remaining(self) -> None:
self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [(1, 13), (2, 21)])
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [(1, 13), (2, 21), (2, 22)])
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [(4, 99)])
def test_get_remaining_exception(self) -> None:
with self.assertRaises(KeyError):
self.store.get_remaining(empty_state, 0, 9999)
bad_state = {(0, 6): {1}}
with self.assertRaises(KeyError):
self.store.get_missing(bad_state, 0, 6)
bad_state = {(0, 9999): set()}
with self.assertRaises(KeyError):
self.store.get_remaining(bad_state, 0, 9999)
def test_location_set_intersection(self) -> None:
locations = {10, 11, 12}
locations.intersection_update(self.store[1])
@@ -212,16 +181,6 @@ class Base:
})
self.assertEqual(len(store), 1)
self.assertEqual(len(store[1]), 0)
self.assertEqual(sorted(store.find_item(set(), 1)), [])
self.assertEqual(sorted(store.find_item({1}, 1)), [])
self.assertEqual(sorted(store.find_item({1, 2}, 1)), [])
self.assertEqual(store.get_for_player(1), {})
self.assertEqual(store.get_checked(empty_state, 0, 1), [])
self.assertEqual(store.get_checked(full_state, 0, 1), [])
self.assertEqual(store.get_missing(empty_state, 0, 1), [])
self.assertEqual(store.get_missing(full_state, 0, 1), [])
self.assertEqual(store.get_remaining(empty_state, 0, 1), [])
self.assertEqual(store.get_remaining(full_state, 0, 1), [])
def test_no_locations_for_1(self) -> None:
store = self.type({

View File

@@ -2,7 +2,9 @@
from __future__ import annotations
import abc
import logging
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union
from typing_extensions import TypeGuard
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
@@ -86,7 +88,3 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
async def deathlink_kill_player(self, ctx: SNIContext) -> None:
""" override this with implementation to kill player """
pass
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
""" override this with code to handle packages from the server """
pass

View File

@@ -7,7 +7,7 @@ import sys
import time
from random import Random
from dataclasses import make_dataclass
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Mapping, Optional, Set, TextIO, Tuple,
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple,
TYPE_CHECKING, Type, Union)
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
@@ -33,10 +33,7 @@ class AutoWorldRegister(type):
# lazy loading + caching to minimize runtime cost
if cls.__settings is None:
from settings import get_settings
try:
cls.__settings = get_settings()[cls.settings_key]
except AttributeError:
return None
cls.__settings = get_settings()[cls.settings_key]
return cls.__settings
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
@@ -534,24 +531,12 @@ class World(metaclass=AutoWorldRegister):
def get_location(self, location_name: str) -> "Location":
return self.multiworld.get_location(location_name, self.player)
def get_locations(self) -> "Iterable[Location]":
return self.multiworld.get_locations(self.player)
def get_entrance(self, entrance_name: str) -> "Entrance":
return self.multiworld.get_entrance(entrance_name, self.player)
def get_entrances(self) -> "Iterable[Entrance]":
return self.multiworld.get_entrances(self.player)
def get_region(self, region_name: str) -> "Region":
return self.multiworld.get_region(region_name, self.player)
def get_regions(self) -> "Iterable[Region]":
return self.multiworld.get_regions(self.player)
def push_precollected(self, item: Item) -> None:
self.multiworld.push_precollected(item)
@property
def player_name(self) -> str:
return self.multiworld.get_player_name(self.player)

View File

@@ -18,42 +18,16 @@ class Type(Enum):
class Component:
"""
A Component represents a process launchable by Archipelago Launcher, either by a User action in the GUI,
by resolving an archipelago://user:pass@host:port link from the WebHost, by resolving a patch file's metadata,
or by using a component name arg while running the Launcher in CLI i.e. `ArchipelagoLauncher.exe "Text Client"`
Expected to be appended to LauncherComponents.component list to be used.
"""
display_name: str
"""Used as the GUI button label and the component name in the CLI args"""
type: Type
"""
Enum "Type" classification of component intent, for filtering in the Launcher GUI
If not set in the constructor, it will be inferred by display_name
"""
script_name: Optional[str]
"""Recommended to use func instead; Name of file to run when the component is called"""
frozen_name: Optional[str]
"""Recommended to use func instead; Name of the frozen executable file for this component"""
icon: str # just the name, no suffix
"""Lookup ID for the icon path in LauncherComponents.icon_paths"""
cli: bool
"""Bool to control if the component gets launched in an appropriate Terminal for the OS"""
func: Optional[Callable]
"""
Function that gets called when the component gets launched
Any arg besides the component name arg is passed into the func as well, so handling *args is suggested
"""
file_identifier: Optional[Callable[[str], bool]]
"""
Function that is run against patch file arg to identify which component is appropriate to launch
If the function is an Instance of SuffixIdentifier the suffixes will also be valid for the Open Patch component
"""
game_name: Optional[str]
"""Game name to identify component when handling launch links from WebHost"""
supports_uri: Optional[bool]
"""Bool to identify if a component supports being launched by launch links from WebHost"""
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
@@ -129,7 +103,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
try:
import zipfile
zip = zipfile.ZipFile(apworld_path)
directories = [f.name for f in zipfile.Path(zip).iterdir() if f.is_dir()]
directories = [f.filename.strip('/') for f in zip.filelist if f.CRC == 0 and f.file_size == 0 and f.filename.count('/') == 1]
if len(directories) == 1 and directories[0] in apworld_path.stem:
module_name = directories[0]
apworld_name = module_name + ".apworld"
@@ -233,7 +207,6 @@ components: List[Component] = [
]
# if registering an icon from within an apworld, the format "ap:module.name/path/to/file.png" can be used
icon_paths = {
'icon': local_path('data', 'icon.png'),
'mcicon': local_path('data', 'mcicon.png'),

View File

@@ -66,12 +66,19 @@ class WorldSource:
start = time.perf_counter()
if self.is_zip:
importer = zipimport.zipimporter(self.resolved_path)
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
assert spec, f"{self.path} is not a loadable module"
mod = importlib.util.module_from_spec(spec)
mod.__package__ = f"worlds.{mod.__package__}"
if hasattr(importer, "find_spec"): # new in Python 3.10
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
assert spec, f"{self.path} is not a loadable module"
mod = importlib.util.module_from_spec(spec)
else: # TODO: remove with 3.8 support
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
if mod.__package__ is not None:
mod.__package__ = f"worlds.{mod.__package__}"
else:
# load_module does not populate package, we'll have to assume mod.__name__ is correct here
# probably safe to remove with 3.8 support
mod.__package__ = f"worlds.{mod.__name__}"
mod.__name__ = f"worlds.{mod.__name__}"
sys.modules[mod.__name__] = mod
with warnings.catch_warnings():

View File

@@ -47,6 +47,8 @@ class LocationData:
self.local_item: int = None
def get_random_position(self, random):
x: int = None
y: int = None
if self.world_positions is None or len(self.world_positions) == 0:
if self.room_id is None:
return None

View File

@@ -76,9 +76,10 @@ def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player
multiworld.regions.append(credits_room_far_side)
dragon_slay_check = options.dragon_slay_check.value
priority_locations = determine_priority_locations()
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
for name, location_data in location_table.items():
require_sword = False
if location_data.region == "Varies":
if location_data.name == "Slay Yorgle":
if not dragon_slay_check:
@@ -153,7 +154,6 @@ def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player
# Placeholder for adding sets of priority locations at generation, possibly as an option in the future
# def determine_priority_locations(multiworld: MultiWorld, dragon_slay_check: bool) -> {}:
def determine_priority_locations() -> {}:
def determine_priority_locations(world: MultiWorld, dragon_slay_check: bool) -> {}:
priority_locations = {}
return priority_locations

View File

@@ -86,7 +86,9 @@ class AdventureDeltaPatch(APPatch, metaclass=AutoPatchRegister):
# locations: [], autocollect: [], seed_name: bytes,
def __init__(self, *args: Any, **kwargs: Any) -> None:
patch_only = True
if "autocollect" in kwargs:
patch_only = False
self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y)
for loc in kwargs["locations"]]

View File

@@ -446,7 +446,7 @@ class AdventureWorld(World):
# end of ordered Main.py calls
def create_item(self, name: str) -> Item:
item_data: ItemData = item_table[name]
item_data: ItemData = item_table.get(name)
return AdventureItem(name, item_data.classification, item_data.id, self.player)
def create_event(self, name: str, classification: ItemClassification) -> Item:

View File

@@ -1,7 +1,7 @@
from dataclasses import dataclass
import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, PerGameCommonOptions, \
from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, Option, \
PlandoBosses, PlandoConnections, PlandoTexts, Removed, StartInventoryPool, Toggle
from .EntranceShuffle import default_connections, default_dungeon_connections, \
inverted_default_connections, inverted_default_dungeon_connections
@@ -742,86 +742,86 @@ class ALttPPlandoTexts(PlandoTexts):
valid_keys = TextTable.valid_keys
@dataclass
class ALTTPOptions(PerGameCommonOptions):
accessibility: ItemsAccessibility
plando_connections: ALttPPlandoConnections
plando_texts: ALttPPlandoTexts
start_inventory_from_pool: StartInventoryPool
goal: Goal
mode: Mode
glitches_required: GlitchesRequired
dark_room_logic: DarkRoomLogic
open_pyramid: OpenPyramid
crystals_needed_for_gt: CrystalsTower
crystals_needed_for_ganon: CrystalsGanon
triforce_pieces_mode: TriforcePiecesMode
triforce_pieces_percentage: TriforcePiecesPercentage
triforce_pieces_required: TriforcePiecesRequired
triforce_pieces_available: TriforcePiecesAvailable
triforce_pieces_extra: TriforcePiecesExtra
entrance_shuffle: EntranceShuffle
entrance_shuffle_seed: EntranceShuffleSeed
big_key_shuffle: big_key_shuffle
small_key_shuffle: small_key_shuffle
key_drop_shuffle: key_drop_shuffle
compass_shuffle: compass_shuffle
map_shuffle: map_shuffle
restrict_dungeon_item_on_boss: RestrictBossItem
item_pool: ItemPool
item_functionality: ItemFunctionality
enemy_health: EnemyHealth
enemy_damage: EnemyDamage
progressive: Progressive
swordless: Swordless
dungeon_counters: DungeonCounters
retro_bow: RetroBow
retro_caves: RetroCaves
hints: Hints
scams: Scams
boss_shuffle: LTTPBosses
pot_shuffle: PotShuffle
enemy_shuffle: EnemyShuffle
killable_thieves: KillableThieves
bush_shuffle: BushShuffle
shop_item_slots: ShopItemSlots
randomize_shop_inventories: RandomizeShopInventories
shuffle_shop_inventories: ShuffleShopInventories
include_witch_hut: IncludeWitchHut
randomize_shop_prices: RandomizeShopPrices
randomize_cost_types: RandomizeCostTypes
shop_price_modifier: ShopPriceModifier
shuffle_capacity_upgrades: ShuffleCapacityUpgrades
bombless_start: BomblessStart
shuffle_prizes: ShufflePrizes
tile_shuffle: TileShuffle
misery_mire_medallion: MiseryMireMedallion
turtle_rock_medallion: TurtleRockMedallion
glitch_boots: GlitchBoots
beemizer_total_chance: BeemizerTotalChance
beemizer_trap_chance: BeemizerTrapChance
timer: Timer
countdown_start_time: CountdownStartTime
red_clock_time: RedClockTime
blue_clock_time: BlueClockTime
green_clock_time: GreenClockTime
death_link: DeathLink
allow_collect: AllowCollect
ow_palettes: OWPalette
uw_palettes: UWPalette
hud_palettes: HUDPalette
sword_palettes: SwordPalette
shield_palettes: ShieldPalette
# link_palettes: LinkPalette
heartbeep: HeartBeep
heartcolor: HeartColor
quickswap: QuickSwap
menuspeed: MenuSpeed
music: Music
reduceflashing: ReduceFlashing
triforcehud: TriforceHud
alttp_options: typing.Dict[str, type(Option)] = {
"accessibility": ItemsAccessibility,
"plando_connections": ALttPPlandoConnections,
"plando_texts": ALttPPlandoTexts,
"start_inventory_from_pool": StartInventoryPool,
"goal": Goal,
"mode": Mode,
"glitches_required": GlitchesRequired,
"dark_room_logic": DarkRoomLogic,
"open_pyramid": OpenPyramid,
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,
"triforce_pieces_mode": TriforcePiecesMode,
"triforce_pieces_percentage": TriforcePiecesPercentage,
"triforce_pieces_required": TriforcePiecesRequired,
"triforce_pieces_available": TriforcePiecesAvailable,
"triforce_pieces_extra": TriforcePiecesExtra,
"entrance_shuffle": EntranceShuffle,
"entrance_shuffle_seed": EntranceShuffleSeed,
"big_key_shuffle": big_key_shuffle,
"small_key_shuffle": small_key_shuffle,
"key_drop_shuffle": key_drop_shuffle,
"compass_shuffle": compass_shuffle,
"map_shuffle": map_shuffle,
"restrict_dungeon_item_on_boss": RestrictBossItem,
"item_pool": ItemPool,
"item_functionality": ItemFunctionality,
"enemy_health": EnemyHealth,
"enemy_damage": EnemyDamage,
"progressive": Progressive,
"swordless": Swordless,
"dungeon_counters": DungeonCounters,
"retro_bow": RetroBow,
"retro_caves": RetroCaves,
"hints": Hints,
"scams": Scams,
"boss_shuffle": LTTPBosses,
"pot_shuffle": PotShuffle,
"enemy_shuffle": EnemyShuffle,
"killable_thieves": KillableThieves,
"bush_shuffle": BushShuffle,
"shop_item_slots": ShopItemSlots,
"randomize_shop_inventories": RandomizeShopInventories,
"shuffle_shop_inventories": ShuffleShopInventories,
"include_witch_hut": IncludeWitchHut,
"randomize_shop_prices": RandomizeShopPrices,
"randomize_cost_types": RandomizeCostTypes,
"shop_price_modifier": ShopPriceModifier,
"shuffle_capacity_upgrades": ShuffleCapacityUpgrades,
"bombless_start": BomblessStart,
"shuffle_prizes": ShufflePrizes,
"tile_shuffle": TileShuffle,
"misery_mire_medallion": MiseryMireMedallion,
"turtle_rock_medallion": TurtleRockMedallion,
"glitch_boots": GlitchBoots,
"beemizer_total_chance": BeemizerTotalChance,
"beemizer_trap_chance": BeemizerTrapChance,
"timer": Timer,
"countdown_start_time": CountdownStartTime,
"red_clock_time": RedClockTime,
"blue_clock_time": BlueClockTime,
"green_clock_time": GreenClockTime,
"death_link": DeathLink,
"allow_collect": AllowCollect,
"ow_palettes": OWPalette,
"uw_palettes": UWPalette,
"hud_palettes": HUDPalette,
"sword_palettes": SwordPalette,
"shield_palettes": ShieldPalette,
# "link_palettes": LinkPalette,
"heartbeep": HeartBeep,
"heartcolor": HeartColor,
"quickswap": QuickSwap,
"menuspeed": MenuSpeed,
"music": Music,
"reduceflashing": ReduceFlashing,
"triforcehud": TriforceHud,
# removed:
goals: Removed
smallkey_shuffle: Removed
bigkey_shuffle: Removed
"goals": Removed,
"smallkey_shuffle": Removed,
"bigkey_shuffle": Removed,
}

View File

@@ -782,8 +782,8 @@ def get_nonnative_item_sprite(code: int) -> int:
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
local_random = world.per_slot_randoms[player]
local_world = world.worlds[player]
local_random = local_world.random
# patch items
@@ -1867,7 +1867,7 @@ def apply_oof_sfx(rom, oof: str):
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
local_random = random if not world else world.worlds[player].random
local_random = random if not world else world.per_slot_randoms[player]
disable_music: bool = not music
# enable instant item menu
if menuspeed == 'instant':
@@ -2197,9 +2197,8 @@ def write_string_to_rom(rom, target, string):
def write_strings(rom, world, player):
from . import ALTTPWorld
local_random = world.per_slot_randoms[player]
w: ALTTPWorld = world.worlds[player]
local_random = w.random
tt = TextTable()
tt.removeUnwantedText()
@@ -2426,7 +2425,7 @@ def write_strings(rom, world, player):
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
world.swordless[player] or world.glitches_required[player] == 'no_glitches')):
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
local_random.shuffle(prog_bow_locs)
world.per_slot_randoms[player].shuffle(prog_bow_locs)
found_bow = False
found_bow_alt = False
while prog_bow_locs and not (found_bow and found_bow_alt):

View File

@@ -592,9 +592,9 @@ def global_rules(multiworld: MultiWorld, player: int):
lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
set_rule(multiworld.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
set_rule(multiworld.get_location('Ganons Tower - Pre-Moldorm Chest', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and can_use_bombs(state, player))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) and can_use_bombs(state, player))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Gap', player),
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))

View File

@@ -1,27 +1,28 @@
import logging
import os
import random
import settings
import threading
import typing
import settings
import Utils
from BaseClasses import Item, CollectionState, Tutorial, MultiWorld
from worlds.AutoWorld import World, WebWorld, LogicMixin
from .Client import ALTTPSNIClient
from .Dungeons import create_dungeons, Dungeon
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import ALTTPOptions, small_key_shuffle
from .Options import alttp_options, small_key_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 .Client import ALTTPSNIClient
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, 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
from .StateHelpers import can_buy_unlimited
from .SubClasses import ALttPItem, LTTPRegionType
from worlds.AutoWorld import World, WebWorld, LogicMixin
from .StateHelpers import can_buy_unlimited
lttp_logger = logging.getLogger("A Link to the Past")
@@ -131,8 +132,7 @@ class ALTTPWorld(World):
Ganon!
"""
game = "A Link to the Past"
options_dataclass = ALTTPOptions
options: ALTTPOptions
option_definitions = alttp_options
settings_key = "lttp_options"
settings: typing.ClassVar[ALTTPSettings]
topology_present = True
@@ -286,22 +286,13 @@ class ALTTPWorld(World):
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
if multiworld.is_race:
import xxtea # noqa
import xxtea
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):
# write old options
import dataclasses
is_first = self.player == min(self.multiworld.get_game_players(self.game))
for field in dataclasses.fields(self.options_dataclass):
if is_first:
setattr(self.multiworld, field.name, {})
getattr(self.multiworld, field.name)[self.player] = getattr(self.options, field.name)
# end of old options re-establisher
player = self.player
multiworld = self.multiworld
@@ -545,10 +536,12 @@ class ALTTPWorld(World):
@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)
world = self.multiworld
player = self.player
return bool(world.boss_shuffle[player] or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.pot_shuffle[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
def generate_output(self, output_directory: str):
multiworld = self.multiworld

View File

@@ -1,32 +0,0 @@
# A Link to the Past
## Où se trouve la page des paramètres ?
La [page des paramètres du joueur pour ce jeu](../player-options) contient tous les paramètres dont vous avez besoin
pour configurer et exporter le fichier.
## Quel est l'effet de la randomisation sur ce jeu ?
Les objets que le joueur devrait normalement obtenir au cours du jeu ont été déplacés. Il y a tout de même une logique
pour que le jeu puisse être terminé, mais dû au mélange des objets, le joueur peut avoir besoin d'accéder à certaines
zones plus tôt que dans le jeu original.
## Quels sont les objets et endroits mélangés ?
Tous les objets principaux, les collectibles et munitions peuvent être mélangés, et tous les endroits qui
pourraient contenir un de ces objets peuvent avoir leur contenu modifié.
## Quels objets peuvent être dans le monde d'un autre joueur ?
Un objet pouvant être mélangé peut être aussi placé dans le monde d'un autre joueur. Il est possible de limiter certains
objets à votre propre monde.
## À quoi ressemble un objet d'un autre monde dans LttP ?
Les objets appartenant à d'autres mondes sont représentés par une Étoile de Super Mario World.
## Quand le joueur reçoit un objet, que ce passe-t-il ?
Quand le joueur reçoit un objet, Link montrera l'objet au monde en le mettant au-dessus de sa tête. C'est bon pour
les affaires !

View File

@@ -1,28 +1,41 @@
# Guide d'installation du MultiWorld de A Link to the Past Randomizer
<div id="tutorial-video-container">
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/mJKEHaiyR_Y" frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
</iframe>
</div>
## Logiciels requis
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [SNI](https://github.com/alttpo/sni/releases). Inclus avec l'installation d'Archipelago ci-dessus.
- SNI n'est pas compatible avec (Q)Usb2Snes.
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
- Un émulateur capable de se connecter à SNI
[snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), ([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
[BSNES-plus](https://github.com/black-sliver/bsnes-plus),
[BizHawk](https://tasvideos.org/BizHawk), ou
[RetroArch](https://retroarch.com?page=platforms) 1.10.1 ou plus récent). Ou,
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle compatible. **À noter:
les SNES minis ne sont pas encore supportés par SNI. Certains utilisateurs rapportent avoir du succès avec QUsb2Snes pour ce système,
mais ce n'est pas supporté.**
- Le fichier ROM de la v1.0 japonaise, habituellement nommé `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
- Un émulateur capable d'éxécuter des scripts Lua
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
[BizHawk](https://tasvideos.org/BizHawk))
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle
compatible
- Le fichier ROM de la v1.0 japonaise, sûrement nommé `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
## Procédure d'installation
1. Téléchargez et installez [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). **L'installateur se situe dans la section "assets" en bas des informations de version**.
2. Si c'est la première fois que vous faites une génération locale ou un patch, il vous sera demandé votre fichier ROM de base. Il s'agit de votre fichier ROM Link to the Past japonais. Cet étape n'a besoin d'être faite qu'une seule fois.
### Installation sur Windows
3. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme
1. Téléchargez et installez les utilitaires du MultiWorld à l'aide du lien au-dessus, faites attention à bien installer
la version la plus récente.
**Le fichier se situe dans la section "assets" en bas des informations de version**. Si vous voulez jouer des parties
classiques de multiworld, téléchargez `Setup.BerserkerMultiWorld.exe`
- Si vous voulez jouer à la version alternative avec le mélangeur de portes dans les donjons, vous téléchargez le
fichier
`Setup.BerserkerMultiWorld.Doors.exe`.
- Durant le processus d'installation, il vous sera demandé de localiser votre ROM v1.0 japonaise. Si vous avez déjà
installé le logiciel auparavant et qu'il s'agit simplement d'une mise à jour, la localisation de la ROM originale
ne sera pas requise.
- Il vous sera peut-être également demandé d'installer Microsoft Visual C++. Si vous le possédez déjà (possiblement
parce qu'un jeu Steam l'a déjà installé), l'installateur ne reproposera pas de l'installer.
2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme
programme par défaut pour ouvrir vos ROMs.
1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez.
2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...**
@@ -31,6 +44,58 @@
5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre émulateur et choisissez **Ouvrir**. Ce fichier
devrait se trouver dans le dossier que vous avez extrait à la première étape.
### Installation sur Mac
- Des volontaires sont recherchés pour remplir cette section ! Contactez **Farrak Kilhn** sur Discord si vous voulez
aider.
## Configurer son fichier YAML
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur
comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra fournir son propre fichier YAML. Cela permet
à chaque joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld
peuvent avoir différentes options.
### Où est-ce que j'obtiens un fichier YAML ?
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options) vous permet de configurer vos
paramètres personnels et de les exporter vers un fichier YAML.
### Configuration avancée du fichier YAML
Une version plus avancée du fichier YAML peut être créée en utilisant la page
des [paramètres de pondération](/games/A Link to the Past/weighted-options), qui vous permet de configurer jusqu'à
trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs
glissants. Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux
autres disponibles dans une même catégorie.
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
Dans cet exemple, il y a soixante morceaux de papier dans le seau : vingt pour "On" et quarante pour "Off". Quand le
générateur décide s'il doit oui ou non activer le mélange des cartes pour votre partie, , il tire aléatoirement un
papier dans le seau. Dans cet exemple, il y a de plus grandes chances d'avoir le mélange de cartes désactivé.
S'il y a une option dont vous ne voulez jamais, mettez simplement sa valeur à zéro. N'oubliez pas qu'il faut que pour
chaque paramètre il faut au moins une option qui soit paramétrée sur un nombre strictement positif.
### Vérifier son fichier YAML
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
[Validateur de YAML](/check).
## Générer une partie pour un joueur
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options), configurez vos options,
et cliquez sur le bouton "Generate Game".
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
## Rejoindre un MultiWorld
### Obtenir son patch et créer sa ROM
Quand vous rejoignez un multiworld, il vous sera demandé de fournir votre fichier YAML à celui qui héberge la partie ou
@@ -44,58 +109,35 @@ automatiquement le client, et devrait créer la ROM dans le même dossier que vo
#### Avec un émulateur
Quand le client se lance automatiquement, SNI devrait se lancer automatiquement également en arrière-plan. Si
Quand le client se lance automatiquement, QUsb2Snes devrait se lancer automatiquement également en arrière-plan. Si
c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer à travers le pare-feu
Windows.
#### snes9x-nwa
1. Cliquez sur 'Network Menu' et cochez **Enable Emu Network Control**
2. Chargez votre ROM si ce n'est pas déjà fait.
##### snes9x-rr
1. Chargez votre ROM si ce n'est pas déjà fait.
2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting**
3. Cliquez alors sur **New Lua Script Window...**
4. Dans la nouvelle fenêtre, sélectionnez **Browse...**
5. Sélectionnez le fichier lua connecteur inclus avec votre client
- Recherchez `/SNI/lua/` dans votre fichier Archipelago.
6. Si vous avez une erreur en chargeant le script indiquant `socket.dll missing` ou similaire, naviguez vers le fichier du
lua que vous utilisez dans votre explorateur de fichiers et copiez le `socket.dll` à la base de votre installation snes9x.
#### BSNES-Plus
1. Chargez votre ROM si ce n'est pas déjà fait.
2. L'émulateur devrait automatiquement se connecter lorsque SNI se lancera.
5. Dirigez vous vers le dossier où vous avez extrait snes9x-rr, allez dans le dossier `lua`, puis
choisissez `multibridge.lua`
6. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom
dans le coin en haut à gauche.
##### BizHawk
1. Assurez vous d'avoir le cœur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant
1. Assurez vous d'avoir le coeur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant
ces options de menu :
- (≤ 2.8) `Config``Cores``SNES``BSNES`
- (≥ 2.9) `Config``Preferred Cores``SNES``BSNESv115+`
Une fois le cœur changé, rechargez le avec Ctrl+R (par défaut).
`Config --> Cores --> SNES --> BSNES`
Une fois le coeur changé, vous devez redémarrer BizHawk.
2. Chargez votre ROM si ce n'est pas déjà fait.
3. Glissez et déposez le fichier `Connector.lua` que vous avez téléchargé ci-dessus sur la fenêtre principale EmuHawk.
- Recherchez `/SNI/lua/` dans votre fichier Archipelago.
- Vous pouvez aussi ouvrir la console Lua manuellement, cliquez sur `Script``Open Script`, et naviguez sur `Connecteur.lua`
avec le sélecteur de fichiers.
##### RetroArch 1.10.1 ou plus récent
Vous n'avez qu'à faire ces étapes qu'une fois.
1. Entrez dans le menu principal RetroArch
2. Allez dans Réglages --> Interface utilisateur. Mettez "Afficher les réglages avancés" sur ON.
3. Allez dans Réglages --> Réseau. Mettez "Commandes Réseau" sur ON. (trouvé sous Request Device 16.) Laissez le
Port des commandes réseau à 555355.
![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-fr.png)
4. Allez dans Menu Principal --> Mise à jour en ligne --> Téléchargement de cœurs. Descendez jusqu'a"Nintendo - SNES / SFC (bsnes-mercury Performance)" et
sélectionnez le.
Quand vous chargez une ROM, veillez a sélectionner un cœur **bsnes-mercury**. Ce sont les seuls cœurs qui autorisent les outils externs à lire les données d'une ROM.
3. Cliquez sur le menu "Tools" et cliquez sur **Lua Console**
4. Cliquez sur le bouton pour ouvrir un nouveau script Lua.
5. Dirigez vous vers le dossier d'installation des utilitaires du MultiWorld, puis dans les dossiers suivants :
`QUsb2Snes/Qusb2Snes/LuaBridge`
6. Sélectionnez `luabridge.lua` et cliquez sur "Open".
7. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom
dans le coin en haut à gauche.
#### Avec une solution matérielle
@@ -105,7 +147,10 @@ le maintenant. Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger
[sur cette page](http://usb2snes.com/#supported-platforms).
1. Fermez votre émulateur, qui s'est potentiellement lancé automatiquement.
2. Lancez votre console et chargez la ROM.
2. Fermez QUsb2Snes, qui s'est lancé automatiquement avec le client.
3. Lancez la version appropriée de QUsb2Snes (v0.7.16).
4. Lancer votre console et chargez la ROM.
5. Remarquez que l'interface Web affiche "SNES Device: Connected", avec le nom de votre appareil.
### Se connecter au MultiServer
@@ -120,6 +165,47 @@ l'interface Web.
### Jouer au jeu
Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations,
vous venez de rejoindre un multiworld ! Vous pouvez exécuter différentes commandes dans votre client. Pour plus d'informations
sur ces commandes, vous pouvez utiliser `/help` pour les commandes locales et `!help` pour les commandes serveur.
Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations
pour avoir rejoint un multiworld !
## Héberger un MultiWorld
La méthode recommandée pour héberger une partie est d'utiliser le service d'hébergement fourni par
[le site](https://berserkermulti.world/generate). Le processus est relativement simple :
1. Récupérez les fichiers YAML des joueurs.
2. Créez une archive zip contenant ces fichiers YAML.
3. Téléversez l'archive zip sur le lien ci-dessus.
4. Attendez un moment que les seed soient générées.
5. Lorsque les seeds sont générées, vous serez redirigé vers une page d'informations "Seed Info".
6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres
joueurs afin qu'ils puissent récupérer leurs patchs.
**Note:** Les patchs fournis sur cette page permettront aux joueurs de se connecteur automatiquement au serveur,
tandis que ceux de la page "Seed Info" non.
7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également
fournir ce lien aux joueurs pour qu'ils puissent suivre la progression de la partie. N'importe quel personne voulant
observer devrait avoir accès à ce lien.
8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer.
## Auto-tracking
Si vous voulez utiliser l'auto-tracking, plusieurs logiciels offrent cette possibilité.
Le logiciel recommandé pour l'auto-tracking actuellement est
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
### Installation
1. Téléchargez le fichier d'installation approprié pour votre ordinateur (Les utilisateurs Windows voudront le
fichier `.msi`).
2. Durant le processus d'installation, il vous sera peut-être demandé d'installer les outils "Microsoft Visual Studio
Build Tools". Un lien est fourni durant l'installation d'OpenTracker, et celle des outils doit se faire manuellement.
### Activer l'auto-tracking
1. Une fois OpenTracker démarré, cliquez sur le menu "Tracking" en haut de la fenêtre, puis choisissez **
AutoTracker...**
2. Appuyez sur le bouton **Get Devices**
3. Sélectionnez votre appareil SNES dans la liste déroulante.
4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking**
5. Cliquez sur le bouton **Start Autotracking**
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -130,21 +130,19 @@ class TestGanonsTower(TestDungeon):
["Ganons Tower - Pre-Moldorm Chest", False, []],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, []],
["Ganons Tower - Validation Chest", False, [], ['Hookshot']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Validation Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
])

View File

@@ -59,316 +59,156 @@ class ItemData:
type: ItemType
group: ItemGroup
def __init__(self, aId: int, count: int, aType: ItemType, group: ItemGroup):
def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup):
"""
Initialisation of the item data
@param aId: The item ID
@param id: The item ID
@param count: the number of items in the pool
@param aType: the importance type of the item
@param type: the importance type of the item
@param group: the usage of the item in the game
"""
self.id = aId
self.id = id
self.count = count
self.type = aType
self.type = type
self.group = group
class ItemNames:
"""
Constants used to represent the mane of every items.
"""
# Normal items
ANEMONE = "Anemone"
ARNASSI_STATUE = "Arnassi Statue"
BIG_SEED = "Big Seed"
GLOWING_SEED = "Glowing Seed"
BLACK_PEARL = "Black Pearl"
BABY_BLASTER = "Baby Blaster"
CRAB_ARMOR = "Crab Armor"
BABY_DUMBO = "Baby Dumbo"
TOOTH = "Tooth"
ENERGY_STATUE = "Energy Statue"
KROTITE_ARMOR = "Krotite Armor"
GOLDEN_STARFISH = "Golden Starfish"
GOLDEN_GEAR = "Golden Gear"
JELLY_BEACON = "Jelly Beacon"
JELLY_COSTUME = "Jelly Costume"
JELLY_PLANT = "Jelly Plant"
MITHALAS_DOLL = "Mithalas Doll"
MITHALAN_DRESS = "Mithalan Dress"
MITHALAS_BANNER = "Mithalas Banner"
MITHALAS_POT = "Mithalas Pot"
MUTANT_COSTUME = "Mutant Costume"
BABY_NAUTILUS = "Baby Nautilus"
BABY_PIRANHA = "Baby Piranha"
ARNASSI_ARMOR = "Arnassi Armor"
SEED_BAG = "Seed Bag"
KING_S_SKULL = "King's Skull"
SONG_PLANT_SPORE = "Song Plant Spore"
STONE_HEAD = "Stone Head"
SUN_KEY = "Sun Key"
GIRL_COSTUME = "Girl Costume"
ODD_CONTAINER = "Odd Container"
TRIDENT = "Trident"
TURTLE_EGG = "Turtle Egg"
JELLY_EGG = "Jelly Egg"
URCHIN_COSTUME = "Urchin Costume"
BABY_WALKER = "Baby Walker"
VEDHA_S_CURE_ALL = "Vedha's Cure-All"
ZUUNA_S_PEROGI = "Zuuna's Perogi"
ARCANE_POULTICE = "Arcane Poultice"
BERRY_ICE_CREAM = "Berry Ice Cream"
BUTTERY_SEA_LOAF = "Buttery Sea Loaf"
COLD_BORSCHT = "Cold Borscht"
COLD_SOUP = "Cold Soup"
CRAB_CAKE = "Crab Cake"
DIVINE_SOUP = "Divine Soup"
DUMBO_ICE_CREAM = "Dumbo Ice Cream"
FISH_OIL = "Fish Oil"
GLOWING_EGG = "Glowing Egg"
HAND_ROLL = "Hand Roll"
HEALING_POULTICE = "Healing Poultice"
HEARTY_SOUP = "Hearty Soup"
HOT_BORSCHT = "Hot Borscht"
HOT_SOUP = "Hot Soup"
ICE_CREAM = "Ice Cream"
LEADERSHIP_ROLL = "Leadership Roll"
LEAF_POULTICE = "Leaf Poultice"
LEECHING_POULTICE = "Leeching Poultice"
LEGENDARY_CAKE = "Legendary Cake"
LOAF_OF_LIFE = "Loaf of Life"
LONG_LIFE_SOUP = "Long Life Soup"
MAGIC_SOUP = "Magic Soup"
MUSHROOM_X_2 = "Mushroom x 2"
PEROGI = "Perogi"
PLANT_LEAF = "Plant Leaf"
PLUMP_PEROGI = "Plump Perogi"
POISON_LOAF = "Poison Loaf"
POISON_SOUP = "Poison Soup"
RAINBOW_MUSHROOM = "Rainbow Mushroom"
RAINBOW_SOUP = "Rainbow Soup"
RED_BERRY = "Red Berry"
RED_BULB_X_2 = "Red Bulb x 2"
ROTTEN_CAKE = "Rotten Cake"
ROTTEN_LOAF_X_8 = "Rotten Loaf x 8"
ROTTEN_MEAT = "Rotten Meat"
ROYAL_SOUP = "Royal Soup"
SEA_CAKE = "Sea Cake"
SEA_LOAF = "Sea Loaf"
SHARK_FIN_SOUP = "Shark Fin Soup"
SIGHT_POULTICE = "Sight Poultice"
SMALL_BONE_X_2 = "Small Bone x 2"
SMALL_EGG = "Small Egg"
SMALL_TENTACLE_X_2 = "Small Tentacle x 2"
SPECIAL_BULB = "Special Bulb"
SPECIAL_CAKE = "Special Cake"
SPICY_MEAT_X_2 = "Spicy Meat x 2"
SPICY_ROLL = "Spicy Roll"
SPICY_SOUP = "Spicy Soup"
SPIDER_ROLL = "Spider Roll"
SWAMP_CAKE = "Swamp Cake"
TASTY_CAKE = "Tasty Cake"
TASTY_ROLL = "Tasty Roll"
TOUGH_CAKE = "Tough Cake"
TURTLE_SOUP = "Turtle Soup"
VEDHA_SEA_CRISP = "Vedha Sea Crisp"
VEGGIE_CAKE = "Veggie Cake"
VEGGIE_ICE_CREAM = "Veggie Ice Cream"
VEGGIE_SOUP = "Veggie Soup"
VOLCANO_ROLL = "Volcano Roll"
HEALTH_UPGRADE = "Health Upgrade"
WOK = "Wok"
EEL_OIL_X_2 = "Eel Oil x 2"
FISH_MEAT_X_2 = "Fish Meat x 2"
FISH_OIL_X_3 = "Fish Oil x 3"
GLOWING_EGG_X_2 = "Glowing Egg x 2"
HEALING_POULTICE_X_2 = "Healing Poultice x 2"
HOT_SOUP_X_2 = "Hot Soup x 2"
LEADERSHIP_ROLL_X_2 = "Leadership Roll x 2"
LEAF_POULTICE_X_3 = "Leaf Poultice x 3"
PLANT_LEAF_X_2 = "Plant Leaf x 2"
PLANT_LEAF_X_3 = "Plant Leaf x 3"
ROTTEN_MEAT_X_2 = "Rotten Meat x 2"
ROTTEN_MEAT_X_8 = "Rotten Meat x 8"
SEA_LOAF_X_2 = "Sea Loaf x 2"
SMALL_BONE_X_3 = "Small Bone x 3"
SMALL_EGG_X_2 = "Small Egg x 2"
LI_AND_LI_SONG = "Li and Li Song"
SHIELD_SONG = "Shield Song"
BEAST_FORM = "Beast Form"
SUN_FORM = "Sun Form"
NATURE_FORM = "Nature Form"
ENERGY_FORM = "Energy Form"
BIND_SONG = "Bind Song"
FISH_FORM = "Fish Form"
SPIRIT_FORM = "Spirit Form"
DUAL_FORM = "Dual Form"
TRANSTURTLE_VEIL_TOP_LEFT = "Transturtle Veil top left"
TRANSTURTLE_VEIL_TOP_RIGHT = "Transturtle Veil top right"
TRANSTURTLE_OPEN_WATERS = "Transturtle Open Waters top right"
TRANSTURTLE_KELP_FOREST = "Transturtle Kelp Forest bottom left"
TRANSTURTLE_HOME_WATERS = "Transturtle Home Waters"
TRANSTURTLE_ABYSS = "Transturtle Abyss right"
TRANSTURTLE_BODY = "Transturtle Final Boss"
TRANSTURTLE_SIMON_SAYS = "Transturtle Simon Says"
TRANSTURTLE_ARNASSI_RUINS = "Transturtle Arnassi Ruins"
# Events name
BODY_TONGUE_CLEARED = "Body Tongue cleared"
HAS_SUN_CRYSTAL = "Has Sun Crystal"
FALLEN_GOD_BEATED = "Fallen God beated"
MITHALAN_GOD_BEATED = "Mithalan God beated"
DRUNIAN_GOD_BEATED = "Drunian God beated"
LUMEREAN_GOD_BEATED = "Lumerean God beated"
THE_GOLEM_BEATED = "The Golem beated"
NAUTILUS_PRIME_BEATED = "Nautilus Prime beated"
BLASTER_PEG_PRIME_BEATED = "Blaster Peg Prime beated"
MERGOG_BEATED = "Mergog beated"
MITHALAN_PRIESTS_BEATED = "Mithalan priests beated"
OCTOPUS_PRIME_BEATED = "Octopus Prime beated"
CRABBIUS_MAXIMUS_BEATED = "Crabbius Maximus beated"
MANTIS_SHRIMP_PRIME_BEATED = "Mantis Shrimp Prime beated"
KING_JELLYFISH_GOD_PRIME_BEATED = "King Jellyfish God Prime beated"
VICTORY = "Victory"
FIRST_SECRET_OBTAINED = "First Secret obtained"
SECOND_SECRET_OBTAINED = "Second Secret obtained"
THIRD_SECRET_OBTAINED = "Third Secret obtained"
"""Information data for every (not event) item."""
item_table = {
# name: ID, Nb, Item Type, Item Group
ItemNames.ANEMONE: ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone
ItemNames.ARNASSI_STATUE: ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue
ItemNames.BIG_SEED: ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed
ItemNames.GLOWING_SEED: ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed
ItemNames.BLACK_PEARL: ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl
ItemNames.BABY_BLASTER: ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster
ItemNames.CRAB_ARMOR: ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume
ItemNames.BABY_DUMBO: ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo
ItemNames.TOOTH: ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss
ItemNames.ENERGY_STATUE: ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue
ItemNames.KROTITE_ARMOR: ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple
ItemNames.GOLDEN_STARFISH: ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star
ItemNames.GOLDEN_GEAR: ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear
ItemNames.JELLY_BEACON: ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon
ItemNames.JELLY_COSTUME: ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume
ItemNames.JELLY_PLANT: ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant
ItemNames.MITHALAS_DOLL: ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll
ItemNames.MITHALAN_DRESS: ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume
ItemNames.MITHALAS_BANNER: ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner
ItemNames.MITHALAS_POT: ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot
ItemNames.MUTANT_COSTUME: ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
ItemNames.BABY_NAUTILUS: ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
ItemNames.BABY_PIRANHA: ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
ItemNames.ARNASSI_ARMOR: ItemData(698023, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_seahorse_costume
ItemNames.SEED_BAG: ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
ItemNames.KING_S_SKULL: ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
ItemNames.SONG_PLANT_SPORE: ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
ItemNames.STONE_HEAD: ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head
ItemNames.SUN_KEY: ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key
ItemNames.GIRL_COSTUME: ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume
ItemNames.ODD_CONTAINER: ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest
ItemNames.TRIDENT: ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
ItemNames.TURTLE_EGG: ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
ItemNames.JELLY_EGG: ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
ItemNames.URCHIN_COSTUME: ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
ItemNames.BABY_WALKER: ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
ItemNames.VEDHA_S_CURE_ALL: ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
ItemNames.ZUUNA_S_PEROGI: ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
ItemNames.ARCANE_POULTICE: ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice
ItemNames.BERRY_ICE_CREAM: ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream
ItemNames.BUTTERY_SEA_LOAF: ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf
ItemNames.COLD_BORSCHT: ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht
ItemNames.COLD_SOUP: ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup
ItemNames.CRAB_CAKE: ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake
ItemNames.DIVINE_SOUP: ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup
ItemNames.DUMBO_ICE_CREAM: ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream
ItemNames.FISH_OIL: ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
ItemNames.GLOWING_EGG: ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
ItemNames.HAND_ROLL: ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll
ItemNames.HEALING_POULTICE: ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
ItemNames.HEARTY_SOUP: ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup
ItemNames.HOT_BORSCHT: ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht
ItemNames.HOT_SOUP: ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
ItemNames.ICE_CREAM: ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream
ItemNames.LEADERSHIP_ROLL: ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
ItemNames.LEAF_POULTICE: ItemData(698055, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leafpoultice
ItemNames.LEECHING_POULTICE: ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice
ItemNames.LEGENDARY_CAKE: ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake
ItemNames.LOAF_OF_LIFE: ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife
ItemNames.LONG_LIFE_SOUP: ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup
ItemNames.MAGIC_SOUP: ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup
ItemNames.MUSHROOM_X_2: ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom
ItemNames.PEROGI: ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi
ItemNames.PLANT_LEAF: ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
ItemNames.PLUMP_PEROGI: ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi
ItemNames.POISON_LOAF: ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf
ItemNames.POISON_SOUP: ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup
ItemNames.RAINBOW_MUSHROOM: ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom
ItemNames.RAINBOW_SOUP: ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup
ItemNames.RED_BERRY: ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry
ItemNames.RED_BULB_X_2: ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb
ItemNames.ROTTEN_CAKE: ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake
ItemNames.ROTTEN_LOAF_X_8: ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf
ItemNames.ROTTEN_MEAT: ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
ItemNames.ROYAL_SOUP: ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup
ItemNames.SEA_CAKE: ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake
ItemNames.SEA_LOAF: ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
ItemNames.SHARK_FIN_SOUP: ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup
ItemNames.SIGHT_POULTICE: ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice
ItemNames.SMALL_BONE_X_2: ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
ItemNames.SMALL_EGG: ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
ItemNames.SMALL_TENTACLE_X_2: ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle
ItemNames.SPECIAL_BULB: ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb
ItemNames.SPECIAL_CAKE: ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake
ItemNames.SPICY_MEAT_X_2: ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat
ItemNames.SPICY_ROLL: ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll
ItemNames.SPICY_SOUP: ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup
ItemNames.SPIDER_ROLL: ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll
ItemNames.SWAMP_CAKE: ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake
ItemNames.TASTY_CAKE: ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake
ItemNames.TASTY_ROLL: ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll
ItemNames.TOUGH_CAKE: ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake
ItemNames.TURTLE_SOUP: ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup
ItemNames.VEDHA_SEA_CRISP: ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp
ItemNames.VEGGIE_CAKE: ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake
ItemNames.VEGGIE_ICE_CREAM: ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream
ItemNames.VEGGIE_SOUP: ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup
ItemNames.VOLCANO_ROLL: ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll
ItemNames.HEALTH_UPGRADE: ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_?
ItemNames.WOK: ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok
ItemNames.EEL_OIL_X_2: ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil
ItemNames.FISH_MEAT_X_2: ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat
ItemNames.FISH_OIL_X_3: ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
ItemNames.GLOWING_EGG_X_2: ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
ItemNames.HEALING_POULTICE_X_2: ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
ItemNames.HOT_SOUP_X_2: ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
ItemNames.LEADERSHIP_ROLL_X_2: ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
ItemNames.LEAF_POULTICE_X_3: ItemData(698107, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leafpoultice
ItemNames.PLANT_LEAF_X_2: ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
ItemNames.PLANT_LEAF_X_3: ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
ItemNames.ROTTEN_MEAT_X_2: ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
ItemNames.ROTTEN_MEAT_X_8: ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
ItemNames.SEA_LOAF_X_2: ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
ItemNames.SMALL_BONE_X_3: ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
ItemNames.SMALL_EGG_X_2: ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
ItemNames.LI_AND_LI_SONG: ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li
ItemNames.SHIELD_SONG: ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield
ItemNames.BEAST_FORM: ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast
ItemNames.SUN_FORM: ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun
ItemNames.NATURE_FORM: ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature
ItemNames.ENERGY_FORM: ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy
ItemNames.BIND_SONG: ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind
ItemNames.FISH_FORM: ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish
ItemNames.SPIRIT_FORM: ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit
ItemNames.DUAL_FORM: ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual
ItemNames.TRANSTURTLE_VEIL_TOP_LEFT: ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01
ItemNames.TRANSTURTLE_VEIL_TOP_RIGHT: ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02
ItemNames.TRANSTURTLE_OPEN_WATERS: ItemData(698127, 1, ItemType.PROGRESSION,
ItemGroup.TURTLE), # transport_openwater03
ItemNames.TRANSTURTLE_KELP_FOREST: ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE),
# transport_forest04
ItemNames.TRANSTURTLE_HOME_WATERS: ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea
ItemNames.TRANSTURTLE_ABYSS: ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03
ItemNames.TRANSTURTLE_BODY: ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss
ItemNames.TRANSTURTLE_SIMON_SAYS: ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
ItemNames.TRANSTURTLE_ARNASSI_RUINS: ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
"Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone
"Arnassi Statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue
"Big Seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed
"Glowing Seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed
"Black Pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl
"Baby Blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster
"Crab Armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume
"Baby Dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo
"Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss
"Energy Statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue
"Krotite Armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple
"Golden Starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star
"Golden Gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear
"Jelly Beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon
"Jelly Costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume
"Jelly Plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant
"Mithalas Doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll
"Mithalan Dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume
"Mithalas Banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner
"Mithalas Pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot
"Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
"Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
"Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
"Arnassi Armor": ItemData(698023, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_seahorse_costume
"Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
"King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
"Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
"Stone Head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head
"Sun Key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key
"Girl Costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume
"Odd Container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest
"Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
"Turtle Egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
"Jelly Egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
"Urchin Costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
"Baby Walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
"Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
"Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
"Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice
"Berry ice cream": ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream
"Buttery sea loaf": ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf
"Cold borscht": ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht
"Cold soup": ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup
"Crab cake": ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake
"Divine soup": ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup
"Dumbo ice cream": ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream
"Fish oil": ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
"Glowing egg": ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
"Hand roll": ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll
"Healing poultice": ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
"Hearty soup": ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup
"Hot borscht": ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht
"Hot soup": ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
"Ice cream": ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream
"Leadership roll": ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
"Leaf poultice": ItemData(698055, 5, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
"Leeching poultice": ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice
"Legendary cake": ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake
"Loaf of life": ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife
"Long life soup": ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup
"Magic soup": ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup
"Mushroom x 2": ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom
"Perogi": ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi
"Plant leaf": ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
"Plump perogi": ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi
"Poison loaf": ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf
"Poison soup": ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup
"Rainbow mushroom": ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom
"Rainbow soup": ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup
"Red berry": ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry
"Red bulb x 2": ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb
"Rotten cake": ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake
"Rotten loaf x 8": ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf
"Rotten meat": ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
"Royal soup": ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup
"Sea cake": ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake
"Sea loaf": ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
"Shark fin soup": ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup
"Sight poultice": ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice
"Small bone x 2": ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
"Small egg": ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
"Small tentacle x 2": ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle
"Special bulb": ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb
"Special cake": ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake
"Spicy meat x 2": ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat
"Spicy roll": ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll
"Spicy soup": ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup
"Spider roll": ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll
"Swamp cake": ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake
"Tasty cake": ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake
"Tasty roll": ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll
"Tough cake": ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake
"Turtle soup": ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup
"Vedha sea crisp": ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp
"Veggie cake": ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake
"Veggie ice cream": ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream
"Veggie soup": ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup
"Volcano roll": ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll
"Health upgrade": ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_?
"Wok": ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok
"Eel oil x 2": ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil
"Fish meat x 2": ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat
"Fish oil x 3": ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
"Glowing egg x 2": ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
"Healing poultice x 2": ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
"Hot soup x 2": ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
"Leadership roll x 2": ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
"Leaf poultice x 3": ItemData(698107, 2, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
"Plant leaf x 2": ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
"Plant leaf x 3": ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
"Rotten meat x 2": ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
"Rotten meat x 8": ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
"Sea loaf x 2": ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
"Small bone x 3": ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
"Small egg x 2": ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
"Li and Li song": ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li
"Shield song": ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield
"Beast form": ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast
"Sun form": ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun
"Nature form": ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature
"Energy form": ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy
"Bind song": ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind
"Fish form": ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish
"Spirit form": ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit
"Dual form": ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual
"Transturtle Veil top left": ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01
"Transturtle Veil top right": ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02
"Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION,
ItemGroup.TURTLE), # transport_openwater03
"Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04
"Transturtle Home Water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea
"Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03
"Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss
"Transturtle Simon Says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
"Transturtle Arnassi Ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,7 @@ class IngredientRandomizer(Choice):
"""
display_name = "Randomize Ingredients"
option_off = 0
alias_false = 0
option_common_ingredients = 1
alias_on = 1
alias_true = 1
option_all_ingredients = 2
default = 0
@@ -32,43 +29,14 @@ class TurtleRandomizer(Choice):
"""Randomize the transportation turtle."""
display_name = "Turtle Randomizer"
option_none = 0
alias_off = 0
alias_false = 0
option_all = 1
option_all_except_final = 2
alias_on = 2
alias_true = 2
default = 2
class EarlyBindSong(Choice):
"""
Force the Bind song to be in a location early in the multiworld (or directly in your world if Early and Local is
selected).
"""
display_name = "Early Bind song"
option_off = 0
alias_false = 0
option_early = 1
alias_on = 1
alias_true = 1
option_early_and_local = 2
default = 1
class EarlyEnergyForm(Choice):
"""
Force the Energy form to be in a location early in the multiworld (or directly in your world if Early and Local is
selected).
"""
display_name = "Early Energy form"
option_off = 0
alias_false = 0
option_early = 1
alias_on = 1
alias_true = 1
option_early_and_local = 2
default = 1
class EarlyEnergyForm(DefaultOnToggle):
""" Force the Energy Form to be in a location early in the game """
display_name = "Early Energy Form"
class AquarianTranslation(Toggle):
@@ -79,7 +47,7 @@ class AquarianTranslation(Toggle):
class BigBossesToBeat(Range):
"""
The number of big bosses to beat before having access to the creator (the final boss). The big bosses are
"Fallen God", "Mithalan God", "Drunian God", "Lumerean God" and "The Golem".
"Fallen God", "Mithalan God", "Drunian God", "Sun God" and "The Golem".
"""
display_name = "Big bosses to beat"
range_start = 0
@@ -136,7 +104,7 @@ class LightNeededToGetToDarkPlaces(DefaultOnToggle):
display_name = "Light needed to get to dark places"
class BindSongNeededToGetUnderRockBulb(DefaultOnToggle):
class BindSongNeededToGetUnderRockBulb(Toggle):
"""
Make sure that the bind song can be acquired before having to obtain sing bulbs under rocks.
"""
@@ -153,18 +121,13 @@ class BlindGoal(Toggle):
class UnconfineHomeWater(Choice):
"""
Open the way out of the Home Waters area so that Naija can go to open water and beyond without the bind song.
Note that if you turn this option off, it is recommended to turn on the Early Energy form and Early Bind Song
options.
Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song.
"""
display_name = "Unconfine Home Waters Area"
display_name = "Unconfine Home Water Area"
option_off = 0
alias_false = 0
option_via_energy_door = 1
option_via_transturtle = 2
option_via_both = 3
alias_on = 3
alias_true = 3
default = 0
@@ -179,7 +142,6 @@ class AquariaOptions(PerGameCommonOptions):
big_bosses_to_beat: BigBossesToBeat
turtle_randomizer: TurtleRandomizer
early_energy_form: EarlyEnergyForm
early_bind_song: EarlyBindSong
light_needed_to_get_to_dark_places: LightNeededToGetToDarkPlaces
bind_song_needed_to_get_under_rock_bulb: BindSongNeededToGetUnderRockBulb
unconfine_home_water: UnconfineHomeWater

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,9 @@ Description: Main module for Aquaria game multiworld randomizer
from typing import List, Dict, ClassVar, Any
from worlds.AutoWorld import World, WebWorld
from BaseClasses import Tutorial, MultiWorld, ItemClassification
from .Items import item_table, AquariaItem, ItemType, ItemGroup, ItemNames
from .Locations import location_table, AquariaLocationNames
from .Options import (AquariaOptions, IngredientRandomizer, TurtleRandomizer, EarlyBindSong, EarlyEnergyForm,
UnconfineHomeWater, Objective)
from .Items import item_table, AquariaItem, ItemType, ItemGroup
from .Locations import location_table
from .Options import AquariaOptions
from .Regions import AquariaRegions
@@ -66,15 +65,15 @@ class AquariaWorld(World):
web: WebWorld = AquariaWeb()
"The web page generation informations"
item_name_to_id: ClassVar[Dict[str, int]] = \
item_name_to_id: ClassVar[Dict[str, int]] =\
{name: data.id for name, data in item_table.items()}
"The name and associated ID of each item of the world"
item_name_groups = {
"Damage": {ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
ItemNames.BABY_BLASTER},
"Light": {ItemNames.SUN_FORM, ItemNames.BABY_DUMBO}
"Damage": {"Energy form", "Nature form", "Beast form",
"Li and Li song", "Baby Nautilus", "Baby Piranha",
"Baby Blaster"},
"Light": {"Sun form", "Baby Dumbo"}
}
"""Grouping item make it easier to find them"""
@@ -149,32 +148,23 @@ class AquariaWorld(World):
def create_items(self) -> None:
"""Create every item in the world"""
precollected = [item.name for item in self.multiworld.precollected_items[self.player]]
if self.options.turtle_randomizer.value != TurtleRandomizer.option_none:
if self.options.turtle_randomizer.value == TurtleRandomizer.option_all_except_final:
self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
precollected)
if self.options.turtle_randomizer.value > 0:
if self.options.turtle_randomizer.value == 2:
self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected)
else:
self.__pre_fill_item(ItemNames.TRANSTURTLE_VEIL_TOP_LEFT,
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE, precollected)
self.__pre_fill_item(ItemNames.TRANSTURTLE_VEIL_TOP_RIGHT,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE, precollected)
self.__pre_fill_item(ItemNames.TRANSTURTLE_OPEN_WATERS,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE,
self.__pre_fill_item("Transturtle Veil top left", "The Veil top left area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Veil top right", "The Veil top right area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Open Water top right", "Open Water top right area, Transturtle",
precollected)
self.__pre_fill_item(ItemNames.TRANSTURTLE_KELP_FOREST,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE,
precollected)
self.__pre_fill_item(ItemNames.TRANSTURTLE_HOME_WATERS, AquariaLocationNames.HOME_WATERS_TRANSTURTLE,
precollected)
self.__pre_fill_item(ItemNames.TRANSTURTLE_ABYSS, AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE,
precollected)
self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle",
precollected)
self.__pre_fill_item("Transturtle Home Water", "Home Water, Transturtle", precollected)
self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected)
# The last two are inverted because in the original game, they are special turtle that communicate directly
self.__pre_fill_item(ItemNames.TRANSTURTLE_SIMON_SAYS, AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE,
precollected, ItemClassification.progression)
self.__pre_fill_item(ItemNames.TRANSTURTLE_ARNASSI_RUINS, AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE,
precollected)
self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected,
ItemClassification.progression)
self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected)
for name, data in item_table.items():
if name not in self.exclude:
for i in range(data.count):
@@ -185,17 +175,10 @@ class AquariaWorld(World):
"""
Launched when the Multiworld generator is ready to generate rules
"""
if self.options.early_energy_form == EarlyEnergyForm.option_early:
self.multiworld.early_items[self.player][ItemNames.ENERGY_FORM] = 1
elif self.options.early_energy_form == EarlyEnergyForm.option_early_and_local:
self.multiworld.local_early_items[self.player][ItemNames.ENERGY_FORM] = 1
if self.options.early_bind_song == EarlyBindSong.option_early:
self.multiworld.early_items[self.player][ItemNames.BIND_SONG] = 1
elif self.options.early_bind_song == EarlyBindSong.option_early_and_local:
self.multiworld.local_early_items[self.player][ItemNames.BIND_SONG] = 1
self.regions.adjusting_rules(self.options)
self.multiworld.completion_condition[self.player] = lambda \
state: state.has(ItemNames.VICTORY, self.player)
state: state.has("Victory", self.player)
def generate_basic(self) -> None:
"""
@@ -203,13 +186,13 @@ class AquariaWorld(World):
Used to fill then `ingredients_substitution` list
"""
simple_ingredients_substitution = [i for i in range(27)]
if self.options.ingredient_randomizer.value > IngredientRandomizer.option_off:
if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients:
if self.options.ingredient_randomizer.value > 0:
if self.options.ingredient_randomizer.value == 1:
simple_ingredients_substitution.pop(-1)
simple_ingredients_substitution.pop(-1)
simple_ingredients_substitution.pop(-1)
self.random.shuffle(simple_ingredients_substitution)
if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients:
if self.options.ingredient_randomizer.value == 1:
simple_ingredients_substitution.extend([24, 25, 26])
dishes_substitution = [i for i in range(27, 76)]
if self.options.dish_randomizer:
@@ -222,19 +205,14 @@ class AquariaWorld(World):
return {"ingredientReplacement": self.ingredients_substitution,
"aquarian_translate": bool(self.options.aquarian_translation.value),
"blind_goal": bool(self.options.blind_goal.value),
"secret_needed":
self.options.objective.value == Objective.option_obtain_secrets_and_kill_the_creator,
"secret_needed": self.options.objective.value > 0,
"minibosses_to_kill": self.options.mini_bosses_to_beat.value,
"bigbosses_to_kill": self.options.big_bosses_to_beat.value,
"skip_first_vision": bool(self.options.skip_first_vision.value),
"unconfine_home_water_energy_door":
self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_energy_door
or self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_both,
"unconfine_home_water_transturtle":
self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_transturtle
or self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_both,
"unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3],
"unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3],
"bind_song_needed_to_get_under_rock_bulb": bool(self.options.bind_song_needed_to_get_under_rock_bulb),
"no_progression_hard_or_hidden_locations": bool(self.options.no_progression_hard_or_hidden_locations),
"light_needed_to_get_to_dark_places": bool(self.options.light_needed_to_get_to_dark_places),
"turtle_randomizer": self.options.turtle_randomizer.value
"turtle_randomizer": self.options.turtle_randomizer.value,
}

View File

@@ -24,7 +24,7 @@ The locations in the randomizer are:
* Beating Mithalan God boss
* Fish Cave puzzle
* Beating Drunian God boss
* Beating Lumerean God boss
* Beating Sun God boss
* Breaking Li cage in the body
Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates,

View File

@@ -3,11 +3,11 @@
## Required Software
- The original Aquaria Game (purchasable from most online game stores)
- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases/latest)
- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
## Optional Software
- For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), for use with
[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)

View File

@@ -3,11 +3,12 @@
## Logiciels nécessaires
- Une copie du jeu Aquaria non-modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
- Le client du Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases/latest)
- Le client du Randomizer d'Aquaria [Aquaria randomizer]
(https://github.com/tioui/Aquaria_Randomizer/releases)
## Logiciels optionnels
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), pour utiliser avec [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)
## Procédures d'installation et d'exécution

View File

@@ -6,212 +6,211 @@ Description: Base class for the Aquaria randomizer unit tests
from test.bases import WorldTestBase
from ..Locations import AquariaLocationNames
# Every location accessible after the home water.
after_home_water_locations = [
AquariaLocationNames.SUN_CRYSTAL,
AquariaLocationNames.HOME_WATERS_TRANSTURTLE,
AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH,
AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH,
AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_TO_THE_RIGHT_OF_THE_SAVE_CRYSTAL,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_SMALL_PATH_BEFORE_MITHALAS,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_PATH_FROM_THE_LEFT_ENTRANCE,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_CLEARING_CLOSE_TO_THE_BOTTOM_EXIT,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_TO_THE_TOP_EXIT,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_FIRST_URN_IN_THE_MITHALAS_EXIT,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_SECOND_URN_IN_THE_MITHALAS_EXIT,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_THIRD_URN_IN_THE_MITHALAS_EXIT,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_TURTLE_ROOM,
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE,
AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_BEHIND_THE_CHOMPER_FISH,
AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_INSIDE_THE_LOWEST_FISH_PASS,
AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_BULB_CLOSE_TO_THE_RIGHT_EXIT,
AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_BULB_BEHIND_THE_CHOMPER_FISH,
AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_KING_SKULL,
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_RIGHT_PART,
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_LEFT_PART,
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_CENTER_PART,
AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE,
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_STATUE,
AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE,
AquariaLocationNames.ARNASSI_RUINS_CRAB_ARMOR,
AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE,
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_THE_LEFT_CITY_PART,
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_THE_LEFT_CITY_PART,
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_RIGHT_PART,
AquariaLocationNames.MITHALAS_CITY_BULB_AT_THE_TOP_OF_THE_CITY,
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_A_BROKEN_HOME,
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_A_BROKEN_HOME,
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_BOTTOM_LEFT_PART,
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_ONE_OF_THE_HOMES,
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_ONE_OF_THE_HOMES,
AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_ONE_OF_THE_HOMES,
AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_ONE_OF_THE_HOMES,
AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_THE_CITY_RESERVE,
AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_THE_CITY_RESERVE,
AquariaLocationNames.MITHALAS_CITY_THIRD_URN_IN_THE_CITY_RESERVE,
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_AT_THE_END_OF_THE_TOP_PATH,
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_AT_THE_END_OF_THE_TOP_PATH,
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_TOP_PATH,
AquariaLocationNames.MITHALAS_CITY_MITHALAS_POT,
AquariaLocationNames.MITHALAS_CITY_URN_IN_THE_CASTLE_FLOWER_TUBE_ENTRANCE,
AquariaLocationNames.MITHALAS_CITY_DOLL,
AquariaLocationNames.MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS,
AquariaLocationNames.MITHALAS_CITY_CASTLE_BULB_IN_THE_FLESH_HOLE,
AquariaLocationNames.MITHALAS_CITY_CASTLE_BLUE_BANNER,
AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BEDROOM,
AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_OF_THE_SINGLE_LAMP_PATH,
AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_OF_THE_SINGLE_LAMP_PATH,
AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BOTTOM_ROOM,
AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_ON_THE_ENTRANCE_PATH,
AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_ON_THE_ENTRANCE_PATH,
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
AquariaLocationNames.MITHALAS_CITY_CASTLE_TRIDENT_HEAD,
AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_TOP_RIGHT_ROOM,
AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_TOP_RIGHT_ROOM,
AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_TOP_RIGHT_ROOM,
AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_BOTTOM_RIGHT_PATH,
AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_BOTTOM_RIGHT_PATH,
AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BEHIND_THE_FLESH_VEIN,
AquariaLocationNames.MITHALAS_CATHEDRAL_URN_IN_THE_TOP_LEFT_EYES_BOSS_ROOM,
AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN,
AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN,
AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN,
AquariaLocationNames.MITHALAS_CATHEDRAL_FOURTH_URN_IN_THE_TOP_RIGHT_ROOM,
AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS,
AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BELOW_THE_LEFT_ENTRANCE,
AquariaLocationNames.MITHALAS_CATHEDRAL_BULB_IN_THE_FLESH_ROOM_WITH_FLEAS,
AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_CENTER_PART,
AquariaLocationNames.CATHEDRAL_UNDERGROUND_FIRST_BULB_IN_THE_TOP_LEFT_PART,
AquariaLocationNames.CATHEDRAL_UNDERGROUND_SECOND_BULB_IN_THE_TOP_LEFT_PART,
AquariaLocationNames.CATHEDRAL_UNDERGROUND_THIRD_BULB_IN_THE_TOP_LEFT_PART,
AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_BOTTOM_RIGHT_PATH,
AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_BOTTOM_LEFT_CLEARING,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_PATH_DOWN_FROM_THE_TOP_LEFT_CLEARING,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_TOP_LEFT_CLEARING,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH,
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_LEFT_OF_THE_CENTER_CLEARING,
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_BIG_ROOM,
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_SMALL_ROOM,
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_CENTER_CLEARING,
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL,
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE,
AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER,
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
AquariaLocationNames.KELP_FOREST_BOSS_ROOM_BULB_AT_THE_BOTTOM_OF_THE_AREA,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_FISH_CAVE_PUZZLE,
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_INSIDE_THE_FISH_PASS,
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM,
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_SEED_BAG,
AquariaLocationNames.MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE,
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_IN_LI_S_CAVE,
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH,
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_HIDDEN_BEHIND_THE_BLOCKING_ROCK,
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE,
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS,
AquariaLocationNames.TURTLE_CAVE_TURTLE_EGG,
AquariaLocationNames.TURTLE_CAVE_BULB_IN_BUBBLE_CLIFF,
AquariaLocationNames.TURTLE_CAVE_URCHIN_COSTUME,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_GOLDEN_STARFISH,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE,
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_LEFT_PATH,
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH,
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_VERSE_EGG,
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_STONE_HEAD,
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
AquariaLocationNames.OCTOPUS_CAVE_BULB_IN_THE_PATH_BELOW_THE_OCTOPUS_CAVE_PATH,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_LEFT_PART,
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_RIGHT_PART,
AquariaLocationNames.SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM,
AquariaLocationNames.SUN_TEMPLE_GOLDEN_GEAR,
AquariaLocationNames.SUN_TEMPLE_FIRST_BULB_OF_THE_TEMPLE,
AquariaLocationNames.SUN_TEMPLE_BULB_ON_THE_RIGHT_PART,
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART,
AquariaLocationNames.SUN_TEMPLE_SUN_KEY,
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB,
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB,
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB,
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_HIDDEN_PATH_ROOM,
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_RIGHT_PART,
AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_SEED,
AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_PLANT,
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_MIDDLE_PATH,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_LEFT_GREEN_ROOM,
AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE,
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT,
AquariaLocationNames.ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM,
AquariaLocationNames.ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM,
AquariaLocationNames.ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM,
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_LEFT_ROOM,
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
AquariaLocationNames.THE_WHALE_VERSE_EGG,
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE,
AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE,
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
AquariaLocationNames.FINAL_BOSS_AREA_FIRST_BULB_IN_THE_TURTLE_ROOM,
AquariaLocationNames.FINAL_BOSS_AREA_SECOND_BULB_IN_THE_TURTLE_ROOM,
AquariaLocationNames.FINAL_BOSS_AREA_THIRD_BULB_IN_THE_TURTLE_ROOM,
AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.SIMON_SAYS_AREA_BEATING_SIMON_SAYS,
AquariaLocationNames.BEATING_FALLEN_GOD,
AquariaLocationNames.BEATING_MITHALAN_GOD,
AquariaLocationNames.BEATING_DRUNIAN_GOD,
AquariaLocationNames.BEATING_LUMEREAN_GOD,
AquariaLocationNames.BEATING_THE_GOLEM,
AquariaLocationNames.BEATING_NAUTILUS_PRIME,
AquariaLocationNames.BEATING_BLASTER_PEG_PRIME,
AquariaLocationNames.BEATING_MERGOG,
AquariaLocationNames.BEATING_MITHALAN_PRIESTS,
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
AquariaLocationNames.BEATING_CRABBIUS_MAXIMUS,
AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME,
AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME,
AquariaLocationNames.FIRST_SECRET,
AquariaLocationNames.SECOND_SECRET,
AquariaLocationNames.THIRD_SECRET,
AquariaLocationNames.SUNKEN_CITY_CLEARED,
AquariaLocationNames.OBJECTIVE_COMPLETE,
"Sun Crystal",
"Home Water, Transturtle",
"Open Water top left area, bulb under the rock in the right path",
"Open Water top left area, bulb under the rock in the left path",
"Open Water top left area, bulb to the right of the save crystal",
"Open Water top right area, bulb in the small path before Mithalas",
"Open Water top right area, bulb in the path from the left entrance",
"Open Water top right area, bulb in the clearing close to the bottom exit",
"Open Water top right area, bulb in the big clearing close to the save crystal",
"Open Water top right area, bulb in the big clearing to the top exit",
"Open Water top right area, first urn in the Mithalas exit",
"Open Water top right area, second urn in the Mithalas exit",
"Open Water top right area, third urn in the Mithalas exit",
"Open Water top right area, bulb in the turtle room",
"Open Water top right area, Transturtle",
"Open Water bottom left area, bulb behind the chomper fish",
"Open Water bottom left area, bulb inside the lowest fish pass",
"Open Water skeleton path, bulb close to the right exit",
"Open Water skeleton path, bulb behind the chomper fish",
"Open Water skeleton path, King Skull",
"Arnassi Ruins, bulb in the right part",
"Arnassi Ruins, bulb in the left part",
"Arnassi Ruins, bulb in the center part",
"Arnassi Ruins, Song Plant Spore",
"Arnassi Ruins, Arnassi Armor",
"Arnassi Ruins, Arnassi Statue",
"Arnassi Ruins, Transturtle",
"Arnassi Ruins, Crab Armor",
"Simon Says area, Transturtle",
"Mithalas City, first bulb in the left city part",
"Mithalas City, second bulb in the left city part",
"Mithalas City, bulb in the right part",
"Mithalas City, bulb at the top of the city",
"Mithalas City, first bulb in a broken home",
"Mithalas City, second bulb in a broken home",
"Mithalas City, bulb in the bottom left part",
"Mithalas City, first bulb in one of the homes",
"Mithalas City, second bulb in one of the homes",
"Mithalas City, first urn in one of the homes",
"Mithalas City, second urn in one of the homes",
"Mithalas City, first urn in the city reserve",
"Mithalas City, second urn in the city reserve",
"Mithalas City, third urn in the city reserve",
"Mithalas City, first bulb at the end of the top path",
"Mithalas City, second bulb at the end of the top path",
"Mithalas City, bulb in the top path",
"Mithalas City, Mithalas Pot",
"Mithalas City, urn in the Castle flower tube entrance",
"Mithalas City, Doll",
"Mithalas City, urn inside a home fish pass",
"Mithalas City Castle, bulb in the flesh hole",
"Mithalas City Castle, Blue Banner",
"Mithalas City Castle, urn in the bedroom",
"Mithalas City Castle, first urn of the single lamp path",
"Mithalas City Castle, second urn of the single lamp path",
"Mithalas City Castle, urn in the bottom room",
"Mithalas City Castle, first urn on the entrance path",
"Mithalas City Castle, second urn on the entrance path",
"Mithalas City Castle, beating the Priests",
"Mithalas City Castle, Trident Head",
"Mithalas Cathedral, first urn in the top right room",
"Mithalas Cathedral, second urn in the top right room",
"Mithalas Cathedral, third urn in the top right room",
"Mithalas Cathedral, urn in the flesh room with fleas",
"Mithalas Cathedral, first urn in the bottom right path",
"Mithalas Cathedral, second urn in the bottom right path",
"Mithalas Cathedral, urn behind the flesh vein",
"Mithalas Cathedral, urn in the top left eyes boss room",
"Mithalas Cathedral, first urn in the path behind the flesh vein",
"Mithalas Cathedral, second urn in the path behind the flesh vein",
"Mithalas Cathedral, third urn in the path behind the flesh vein",
"Mithalas Cathedral, fourth urn in the top right room",
"Mithalas Cathedral, Mithalan Dress",
"Mithalas Cathedral, urn below the left entrance",
"Cathedral Underground, bulb in the center part",
"Cathedral Underground, first bulb in the top left part",
"Cathedral Underground, second bulb in the top left part",
"Cathedral Underground, third bulb in the top left part",
"Cathedral Underground, bulb close to the save crystal",
"Cathedral Underground, bulb in the bottom right path",
"Mithalas boss area, beating Mithalan God",
"Kelp Forest top left area, bulb in the bottom left clearing",
"Kelp Forest top left area, bulb in the path down from the top left clearing",
"Kelp Forest top left area, bulb in the top left clearing",
"Kelp Forest top left area, Jelly Egg",
"Kelp Forest top left area, bulb close to the Verse Egg",
"Kelp Forest top left area, Verse Egg",
"Kelp Forest top right area, bulb under the rock in the right path",
"Kelp Forest top right area, bulb at the left of the center clearing",
"Kelp Forest top right area, bulb in the left path's big room",
"Kelp Forest top right area, bulb in the left path's small room",
"Kelp Forest top right area, bulb at the top of the center clearing",
"Kelp Forest top right area, Black Pearl",
"Kelp Forest top right area, bulb in the top fish pass",
"Kelp Forest bottom left area, bulb close to the spirit crystals",
"Kelp Forest bottom left area, Walker Baby",
"Kelp Forest bottom left area, Transturtle",
"Kelp Forest bottom right area, Odd Container",
"Kelp Forest boss area, beating Drunian God",
"Kelp Forest boss room, bulb at the bottom of the area",
"Kelp Forest bottom left area, Fish Cave puzzle",
"Kelp Forest sprite cave, bulb inside the fish pass",
"Kelp Forest sprite cave, bulb in the second room",
"Kelp Forest sprite cave, Seed Bag",
"Mermog cave, bulb in the left part of the cave",
"Mermog cave, Piranha Egg",
"The Veil top left area, In Li's cave",
"The Veil top left area, bulb under the rock in the top right path",
"The Veil top left area, bulb hidden behind the blocking rock",
"The Veil top left area, Transturtle",
"The Veil top left area, bulb inside the fish pass",
"Turtle cave, Turtle Egg",
"Turtle cave, bulb in Bubble Cliff",
"Turtle cave, Urchin Costume",
"The Veil top right area, bulb in the middle of the wall jump cliff",
"The Veil top right area, Golden Starfish",
"The Veil top right area, bulb at the top of the waterfall",
"The Veil top right area, Transturtle",
"The Veil bottom area, bulb in the left path",
"The Veil bottom area, bulb in the spirit path",
"The Veil bottom area, Verse Egg",
"The Veil bottom area, Stone Head",
"Octopus Cave, Dumbo Egg",
"Octopus Cave, bulb in the path below the Octopus Cave path",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
"Sun Temple, bulb in the top left part",
"Sun Temple, bulb in the top right part",
"Sun Temple, bulb at the top of the high dark room",
"Sun Temple, Golden Gear",
"Sun Temple, first bulb of the temple",
"Sun Temple, bulb on the right part",
"Sun Temple, bulb in the hidden room of the right part",
"Sun Temple, Sun Key",
"Sun Worm path, first path bulb",
"Sun Worm path, second path bulb",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"Sun Temple boss area, beating Sun God",
"Abyss left area, bulb in hidden path room",
"Abyss left area, bulb in the right part",
"Abyss left area, Glowing Seed",
"Abyss left area, Glowing Plant",
"Abyss left area, bulb in the bottom fish pass",
"Abyss right area, bulb behind the rock in the whale room",
"Abyss right area, bulb in the middle path",
"Abyss right area, bulb behind the rock in the middle path",
"Abyss right area, bulb in the left green room",
"Abyss right area, Transturtle",
"Ice Cave, bulb in the room to the right",
"Ice Cave, first bulb in the top exit room",
"Ice Cave, second bulb in the top exit room",
"Ice Cave, third bulb in the top exit room",
"Ice Cave, bulb in the left room",
"King Jellyfish Cave, bulb in the right path from King Jelly",
"King Jellyfish Cave, Jellyfish Costume",
"The Whale, Verse Egg",
"Sunken City right area, crate close to the save crystal",
"Sunken City right area, crate in the left bottom room",
"Sunken City left area, crate in the little pipe room",
"Sunken City left area, crate close to the save crystal",
"Sunken City left area, crate before the bedroom",
"Sunken City left area, Girl Costume",
"Sunken City, bulb on top of the boss area",
"The Body center area, breaking Li's cage",
"The Body center area, bulb on the main path blocking tube",
"The Body left area, first bulb in the top face room",
"The Body left area, second bulb in the top face room",
"The Body left area, bulb below the water stream",
"The Body left area, bulb in the top path to the top face room",
"The Body left area, bulb in the bottom face room",
"The Body right area, bulb in the top face room",
"The Body right area, bulb in the top path to the bottom face room",
"The Body right area, bulb in the bottom face room",
"The Body bottom area, bulb in the Jelly Zap room",
"The Body bottom area, bulb in the nautilus room",
"The Body bottom area, Mutant Costume",
"Final Boss area, first bulb in the turtle room",
"Final Boss area, second bulb in the turtle room",
"Final Boss area, third bulb in the turtle room",
"Final Boss area, Transturtle",
"Final Boss area, bulb in the boss third form room",
"Simon Says area, beating Simon Says",
"Beating Fallen God",
"Beating Mithalan God",
"Beating Drunian God",
"Beating Sun God",
"Beating the Golem",
"Beating Nautilus Prime",
"Beating Blaster Peg Prime",
"Beating Mergog",
"Beating Mithalan priests",
"Beating Octopus Prime",
"Beating Crabbius Maximus",
"Beating Mantis Shrimp Prime",
"Beating King Jellyfish God Prime",
"First secret",
"Second secret",
"Third secret",
"Sunken City cleared",
"Objective complete",
]
class AquariaTestBase(WorldTestBase):

View File

@@ -5,8 +5,6 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
class BeastFormAccessTest(AquariaTestBase):
@@ -15,16 +13,16 @@ class BeastFormAccessTest(AquariaTestBase):
def test_beast_form_location(self) -> None:
"""Test locations that require beast form"""
locations = [
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG,
AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
AquariaLocationNames.BEATING_THE_GOLEM,
AquariaLocationNames.BEATING_MERGOG,
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
AquariaLocationNames.SUNKEN_CITY_CLEARED,
"Mermog cave, Piranha Egg",
"Kelp Forest top left area, Jelly Egg",
"Mithalas Cathedral, Mithalan Dress",
"The Veil top right area, bulb at the top of the waterfall",
"Sunken City, bulb on top of the boss area",
"Octopus Cave, Dumbo Egg",
"Beating the Golem",
"Beating Mergog",
"Beating Octopus Prime",
"Sunken City cleared",
]
items = [[ItemNames.BEAST_FORM]]
items = [["Beast form"]]
self.assertAccessDependency(locations, items)

View File

@@ -5,8 +5,6 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
class BeastForArnassiArmormAccessTest(AquariaTestBase):
@@ -15,27 +13,27 @@ class BeastForArnassiArmormAccessTest(AquariaTestBase):
def test_beast_form_arnassi_armor_location(self) -> None:
"""Test locations that require beast form or arnassi armor"""
locations = [
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
AquariaLocationNames.ARNASSI_RUINS_CRAB_ARMOR,
AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE,
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_AT_THE_END_OF_THE_TOP_PATH,
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_AT_THE_END_OF_THE_TOP_PATH,
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_TOP_PATH,
AquariaLocationNames.MITHALAS_CITY_MITHALAS_POT,
AquariaLocationNames.MITHALAS_CITY_URN_IN_THE_CASTLE_FLOWER_TUBE_ENTRANCE,
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
AquariaLocationNames.BEATING_THE_GOLEM,
AquariaLocationNames.BEATING_MERGOG,
AquariaLocationNames.BEATING_CRABBIUS_MAXIMUS,
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
AquariaLocationNames.BEATING_MITHALAN_PRIESTS,
AquariaLocationNames.SUNKEN_CITY_CLEARED
"Mithalas City Castle, beating the Priests",
"Arnassi Ruins, Crab Armor",
"Arnassi Ruins, Song Plant Spore",
"Mithalas City, first bulb at the end of the top path",
"Mithalas City, second bulb at the end of the top path",
"Mithalas City, bulb in the top path",
"Mithalas City, Mithalas Pot",
"Mithalas City, urn in the Castle flower tube entrance",
"Mermog cave, Piranha Egg",
"Mithalas Cathedral, Mithalan Dress",
"Kelp Forest top left area, Jelly Egg",
"The Veil top right area, bulb in the middle of the wall jump cliff",
"The Veil top right area, bulb at the top of the waterfall",
"Sunken City, bulb on top of the boss area",
"Octopus Cave, Dumbo Egg",
"Beating the Golem",
"Beating Mergog",
"Beating Crabbius Maximus",
"Beating Octopus Prime",
"Beating Mithalan priests",
"Sunken City cleared"
]
items = [[ItemNames.BEAST_FORM, ItemNames.ARNASSI_ARMOR]]
items = [["Beast form", "Arnassi Armor"]]
self.assertAccessDependency(locations, items)

View File

@@ -6,36 +6,31 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase, after_home_water_locations
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
from ..Options import UnconfineHomeWater, EarlyBindSong
class BindSongAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the bind song"""
options = {
"bind_song_needed_to_get_under_rock_bulb": False,
"unconfine_home_water": UnconfineHomeWater.option_off,
"early_bind_song": EarlyBindSong.option_off
}
def test_bind_song_location(self) -> None:
"""Test locations that require Bind song"""
locations = [
AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BIG_SEED,
AquariaLocationNames.HOME_WATERS_BULB_IN_THE_PATH_BELOW_NAUTILUS_PRIME,
AquariaLocationNames.HOME_WATERS_BULB_IN_THE_BOTTOM_LEFT_ROOM,
AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
AquariaLocationNames.SONG_CAVE_VERSE_EGG,
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BEATING_THE_ENERGY_STATUE,
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BULB_IN_THE_BOTTOM_ROOM_BLOCKED_BY_A_ROCK,
AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL,
AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK,
AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR,
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
"Verse Cave right area, Big Seed",
"Home Water, bulb in the path below Nautilus Prime",
"Home Water, bulb in the bottom left room",
"Home Water, Nautilus Egg",
"Song Cave, Verse Egg",
"Energy Temple first area, beating the Energy Statue",
"Energy Temple first area, bulb in the bottom room blocked by a rock",
"Energy Temple first area, Energy Idol",
"Energy Temple second area, bulb under the rock",
"Energy Temple bottom entrance, Krotite Armor",
"Energy Temple third area, bulb in the bottom path",
"Energy Temple boss area, Fallen God Tooth",
"Energy Temple blaster room, Blaster Egg",
*after_home_water_locations
]
items = [[ItemNames.BIND_SONG]]
items = [["Bind song"]]
self.assertAccessDependency(locations, items)

View File

@@ -7,8 +7,6 @@ Description: Unit test used to test accessibility of locations with and without
from . import AquariaTestBase
from .test_bind_song_access import after_home_water_locations
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
class BindSongOptionAccessTest(AquariaTestBase):
@@ -20,25 +18,25 @@ class BindSongOptionAccessTest(AquariaTestBase):
def test_bind_song_location(self) -> None:
"""Test locations that require Bind song with the bind song needed option activated"""
locations = [
AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BIG_SEED,
AquariaLocationNames.VERSE_CAVE_LEFT_AREA_BULB_UNDER_THE_ROCK_AT_THE_END_OF_THE_PATH,
AquariaLocationNames.HOME_WATERS_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH_FROM_THE_VERSE_CAVE,
AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_CLOSE_TO_THE_SONG_DOOR,
AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_IN_THE_PATH_TO_THE_SINGING_STATUES,
AquariaLocationNames.NAIJA_S_HOME_BULB_UNDER_THE_ROCK_AT_THE_RIGHT_OF_THE_MAIN_PATH,
AquariaLocationNames.HOME_WATERS_BULB_IN_THE_PATH_BELOW_NAUTILUS_PRIME,
AquariaLocationNames.HOME_WATERS_BULB_IN_THE_BOTTOM_LEFT_ROOM,
AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
AquariaLocationNames.SONG_CAVE_VERSE_EGG,
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BEATING_THE_ENERGY_STATUE,
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BULB_IN_THE_BOTTOM_ROOM_BLOCKED_BY_A_ROCK,
AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL,
AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK,
AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR,
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
"Verse Cave right area, Big Seed",
"Verse Cave left area, bulb under the rock at the end of the path",
"Home Water, bulb under the rock in the left path from the Verse Cave",
"Song Cave, bulb under the rock close to the song door",
"Song Cave, bulb under the rock in the path to the singing statues",
"Naija's Home, bulb under the rock at the right of the main path",
"Home Water, bulb in the path below Nautilus Prime",
"Home Water, bulb in the bottom left room",
"Home Water, Nautilus Egg",
"Song Cave, Verse Egg",
"Energy Temple first area, beating the Energy Statue",
"Energy Temple first area, bulb in the bottom room blocked by a rock",
"Energy Temple first area, Energy Idol",
"Energy Temple second area, bulb under the rock",
"Energy Temple bottom entrance, Krotite Armor",
"Energy Temple third area, bulb in the bottom path",
"Energy Temple boss area, Fallen God Tooth",
"Energy Temple blaster room, Blaster Egg",
*after_home_water_locations
]
items = [[ItemNames.BIND_SONG]]
items = [["Bind song"]]
self.assertAccessDependency(locations, items)

View File

@@ -5,17 +5,16 @@ Description: Unit test used to test accessibility of region with the home water
"""
from . import AquariaTestBase
from ..Options import UnconfineHomeWater, EarlyEnergyForm
class ConfinedHomeWaterAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of region with the unconfine home water option disabled"""
options = {
"unconfine_home_water": UnconfineHomeWater.option_off,
"early_energy_form": EarlyEnergyForm.option_off
"unconfine_home_water": 0,
"early_energy_form": False
}
def test_confine_home_water_location(self) -> None:
"""Test region accessible with confined home water"""
self.assertFalse(self.can_reach_region("Open Waters top left area"), "Can reach Open Waters top left area")
self.assertFalse(self.can_reach_region("Home Waters, turtle room"), "Can reach Home Waters, turtle room")
self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area")
self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")

View File

@@ -5,25 +5,22 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
from ..Options import TurtleRandomizer
class LiAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the dual song"""
options = {
"turtle_randomizer": TurtleRandomizer.option_all,
"turtle_randomizer": 1,
}
def test_li_song_location(self) -> None:
"""Test locations that require the dual song"""
locations = [
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.OBJECTIVE_COMPLETE
"The Body bottom area, bulb in the Jelly Zap room",
"The Body bottom area, bulb in the nautilus room",
"The Body bottom area, Mutant Costume",
"Final Boss area, bulb in the boss third form room",
"Objective complete"
]
items = [[ItemNames.DUAL_FORM]]
items = [["Dual form"]]
self.assertAccessDependency(locations, items)

View File

@@ -6,31 +6,28 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
from ..Options import EarlyEnergyForm
class EnergyFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the energy form"""
options = {
"early_energy_form": EarlyEnergyForm.option_off
"early_energy_form": False,
}
def test_energy_form_location(self) -> None:
"""Test locations that require Energy form"""
locations = [
AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK,
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.OBJECTIVE_COMPLETE,
"Energy Temple second area, bulb under the rock",
"Energy Temple third area, bulb in the bottom path",
"The Body left area, first bulb in the top face room",
"The Body left area, second bulb in the top face room",
"The Body left area, bulb below the water stream",
"The Body left area, bulb in the top path to the top face room",
"The Body left area, bulb in the bottom face room",
"The Body right area, bulb in the top path to the bottom face room",
"The Body right area, bulb in the bottom face room",
"Final Boss area, bulb in the boss third form room",
"Objective complete",
]
items = [[ItemNames.ENERGY_FORM]]
items = [["Energy form"]]
self.assertAccessDependency(locations, items)

View File

@@ -5,74 +5,88 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
from ..Options import EarlyEnergyForm, TurtleRandomizer
class EnergyFormDualFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the energy form and dual form (and Li)"""
options = {
"early_energy_form": EarlyEnergyForm.option_off,
"turtle_randomizer": TurtleRandomizer.option_all
"early_energy_form": False,
}
def test_energy_form_or_dual_form_location(self) -> None:
"""Test locations that require Energy form or dual form"""
locations = [
AquariaLocationNames.NAIJA_S_HOME_BULB_AFTER_THE_ENERGY_DOOR,
AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK,
AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR,
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE,
AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE,
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.BEATING_FALLEN_GOD,
AquariaLocationNames.BEATING_BLASTER_PEG_PRIME,
AquariaLocationNames.BEATING_MITHALAN_GOD,
AquariaLocationNames.BEATING_DRUNIAN_GOD,
AquariaLocationNames.BEATING_LUMEREAN_GOD,
AquariaLocationNames.BEATING_THE_GOLEM,
AquariaLocationNames.BEATING_NAUTILUS_PRIME,
AquariaLocationNames.BEATING_MERGOG,
AquariaLocationNames.BEATING_MITHALAN_PRIESTS,
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME,
AquariaLocationNames.BEATING_THE_GOLEM,
AquariaLocationNames.SUNKEN_CITY_CLEARED,
AquariaLocationNames.FIRST_SECRET,
AquariaLocationNames.OBJECTIVE_COMPLETE
"Naija's Home, bulb after the energy door",
"Home Water, Nautilus Egg",
"Energy Temple second area, bulb under the rock",
"Energy Temple bottom entrance, Krotite Armor",
"Energy Temple third area, bulb in the bottom path",
"Energy Temple blaster room, Blaster Egg",
"Energy Temple boss area, Fallen God Tooth",
"Mithalas City Castle, beating the Priests",
"Mithalas boss area, beating Mithalan God",
"Mithalas Cathedral, first urn in the top right room",
"Mithalas Cathedral, second urn in the top right room",
"Mithalas Cathedral, third urn in the top right room",
"Mithalas Cathedral, urn in the flesh room with fleas",
"Mithalas Cathedral, first urn in the bottom right path",
"Mithalas Cathedral, second urn in the bottom right path",
"Mithalas Cathedral, urn behind the flesh vein",
"Mithalas Cathedral, urn in the top left eyes boss room",
"Mithalas Cathedral, first urn in the path behind the flesh vein",
"Mithalas Cathedral, second urn in the path behind the flesh vein",
"Mithalas Cathedral, third urn in the path behind the flesh vein",
"Mithalas Cathedral, fourth urn in the top right room",
"Mithalas Cathedral, Mithalan Dress",
"Mithalas Cathedral, urn below the left entrance",
"Kelp Forest top left area, bulb close to the Verse Egg",
"Kelp Forest top left area, Verse Egg",
"Kelp Forest boss area, beating Drunian God",
"Mermog cave, Piranha Egg",
"Octopus Cave, Dumbo Egg",
"Sun Temple boss area, beating Sun God",
"King Jellyfish Cave, bulb in the right path from King Jelly",
"King Jellyfish Cave, Jellyfish Costume",
"Sunken City right area, crate close to the save crystal",
"Sunken City right area, crate in the left bottom room",
"Sunken City left area, crate in the little pipe room",
"Sunken City left area, crate close to the save crystal",
"Sunken City left area, crate before the bedroom",
"Sunken City left area, Girl Costume",
"Sunken City, bulb on top of the boss area",
"The Body center area, breaking Li's cage",
"The Body center area, bulb on the main path blocking tube",
"The Body left area, first bulb in the top face room",
"The Body left area, second bulb in the top face room",
"The Body left area, bulb below the water stream",
"The Body left area, bulb in the top path to the top face room",
"The Body left area, bulb in the bottom face room",
"The Body right area, bulb in the top face room",
"The Body right area, bulb in the top path to the bottom face room",
"The Body right area, bulb in the bottom face room",
"The Body bottom area, bulb in the Jelly Zap room",
"The Body bottom area, bulb in the nautilus room",
"The Body bottom area, Mutant Costume",
"Final Boss area, bulb in the boss third form room",
"Final Boss area, first bulb in the turtle room",
"Final Boss area, second bulb in the turtle room",
"Final Boss area, third bulb in the turtle room",
"Final Boss area, Transturtle",
"Beating Fallen God",
"Beating Blaster Peg Prime",
"Beating Mithalan God",
"Beating Drunian God",
"Beating Sun God",
"Beating the Golem",
"Beating Nautilus Prime",
"Beating Mergog",
"Beating Mithalan priests",
"Beating Octopus Prime",
"Beating King Jellyfish God Prime",
"Beating the Golem",
"Sunken City cleared",
"First secret",
"Objective complete"
]
items = [[ItemNames.ENERGY_FORM, ItemNames.DUAL_FORM, ItemNames.LI_AND_LI_SONG, ItemNames.BODY_TONGUE_CLEARED]]
items = [["Energy form", "Dual form", "Li and Li song", "Body tongue cleared"]]
self.assertAccessDependency(locations, items)

View File

@@ -5,36 +5,33 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
from ..Options import TurtleRandomizer
class FishFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the fish form"""
options = {
"turtle_randomizer": TurtleRandomizer.option_all,
"turtle_randomizer": 1,
}
def test_fish_form_location(self) -> None:
"""Test locations that require fish form"""
locations = [
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS,
AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL,
AquariaLocationNames.MITHALAS_CITY_DOLL,
AquariaLocationNames.MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS,
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS,
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_VERSE_EGG,
AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_INSIDE_THE_LOWEST_FISH_PASS,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
AquariaLocationNames.MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE,
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
AquariaLocationNames.BEATING_MERGOG,
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
AquariaLocationNames.OCTOPUS_CAVE_BULB_IN_THE_PATH_BELOW_THE_OCTOPUS_CAVE_PATH,
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS
"The Veil top left area, bulb inside the fish pass",
"Energy Temple first area, Energy Idol",
"Mithalas City, Doll",
"Mithalas City, urn inside a home fish pass",
"Kelp Forest top right area, bulb in the top fish pass",
"The Veil bottom area, Verse Egg",
"Open Water bottom left area, bulb inside the lowest fish pass",
"Kelp Forest top left area, bulb close to the Verse Egg",
"Kelp Forest top left area, Verse Egg",
"Mermog cave, bulb in the left part of the cave",
"Mermog cave, Piranha Egg",
"Beating Mergog",
"Octopus Cave, Dumbo Egg",
"Octopus Cave, bulb in the path below the Octopus Cave path",
"Beating Octopus Prime",
"Abyss left area, bulb in the bottom fish pass"
]
items = [[ItemNames.FISH_FORM]]
items = [["Fish form"]]
self.assertAccessDependency(locations, items)

View File

@@ -5,44 +5,41 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
from ..Options import TurtleRandomizer
class LiAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without Li"""
options = {
"turtle_randomizer": TurtleRandomizer.option_all,
"turtle_randomizer": 1,
}
def test_li_song_location(self) -> None:
"""Test locations that require Li"""
locations = [
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE,
AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE,
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.BEATING_THE_GOLEM,
AquariaLocationNames.SUNKEN_CITY_CLEARED,
AquariaLocationNames.OBJECTIVE_COMPLETE
"Sunken City right area, crate close to the save crystal",
"Sunken City right area, crate in the left bottom room",
"Sunken City left area, crate in the little pipe room",
"Sunken City left area, crate close to the save crystal",
"Sunken City left area, crate before the bedroom",
"Sunken City left area, Girl Costume",
"Sunken City, bulb on top of the boss area",
"The Body center area, breaking Li's cage",
"The Body center area, bulb on the main path blocking tube",
"The Body left area, first bulb in the top face room",
"The Body left area, second bulb in the top face room",
"The Body left area, bulb below the water stream",
"The Body left area, bulb in the top path to the top face room",
"The Body left area, bulb in the bottom face room",
"The Body right area, bulb in the top face room",
"The Body right area, bulb in the top path to the bottom face room",
"The Body right area, bulb in the bottom face room",
"The Body bottom area, bulb in the Jelly Zap room",
"The Body bottom area, bulb in the nautilus room",
"The Body bottom area, Mutant Costume",
"Final Boss area, bulb in the boss third form room",
"Beating the Golem",
"Sunken City cleared",
"Objective complete"
]
items = [[ItemNames.LI_AND_LI_SONG, ItemNames.BODY_TONGUE_CLEARED]]
items = [["Li and Li song", "Body tongue cleared"]]
self.assertAccessDependency(locations, items)

View File

@@ -5,15 +5,12 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
from ..Options import TurtleRandomizer
class LightAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without light"""
options = {
"turtle_randomizer": TurtleRandomizer.option_all,
"turtle_randomizer": 1,
"light_needed_to_get_to_dark_places": True,
}
@@ -22,52 +19,52 @@ class LightAccessTest(AquariaTestBase):
locations = [
# Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be
# tested.
# AquariaLocationNames.THIRD_SECRET,
# AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_LEFT_PART,
# AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_RIGHT_PART,
# AquariaLocationNames.SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM,
# AquariaLocationNames.SUN_TEMPLE_GOLDEN_GEAR,
# AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB,
# AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB,
# AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER,
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL,
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_HIDDEN_PATH_ROOM,
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_RIGHT_PART,
AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_SEED,
AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_PLANT,
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_MIDDLE_PATH,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_LEFT_GREEN_ROOM,
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT,
AquariaLocationNames.ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM,
AquariaLocationNames.ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM,
AquariaLocationNames.ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM,
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_LEFT_ROOM,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME,
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME,
AquariaLocationNames.THE_WHALE_VERSE_EGG,
AquariaLocationNames.FIRST_SECRET,
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.SUNKEN_CITY_CLEARED,
AquariaLocationNames.BEATING_THE_GOLEM,
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.OBJECTIVE_COMPLETE,
# "Third secret",
# "Sun Temple, bulb in the top left part",
# "Sun Temple, bulb in the top right part",
# "Sun Temple, bulb at the top of the high dark room",
# "Sun Temple, Golden Gear",
# "Sun Worm path, first path bulb",
# "Sun Worm path, second path bulb",
# "Sun Worm path, first cliff bulb",
"Octopus Cave, Dumbo Egg",
"Kelp Forest bottom right area, Odd Container",
"Kelp Forest top right area, Black Pearl",
"Abyss left area, bulb in hidden path room",
"Abyss left area, bulb in the right part",
"Abyss left area, Glowing Seed",
"Abyss left area, Glowing Plant",
"Abyss left area, bulb in the bottom fish pass",
"Abyss right area, bulb behind the rock in the whale room",
"Abyss right area, bulb in the middle path",
"Abyss right area, bulb behind the rock in the middle path",
"Abyss right area, bulb in the left green room",
"Ice Cave, bulb in the room to the right",
"Ice Cave, first bulb in the top exit room",
"Ice Cave, second bulb in the top exit room",
"Ice Cave, third bulb in the top exit room",
"Ice Cave, bulb in the left room",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
"Beating Mantis Shrimp Prime",
"King Jellyfish Cave, bulb in the right path from King Jelly",
"King Jellyfish Cave, Jellyfish Costume",
"Beating King Jellyfish God Prime",
"The Whale, Verse Egg",
"First secret",
"Sunken City right area, crate close to the save crystal",
"Sunken City right area, crate in the left bottom room",
"Sunken City left area, crate in the little pipe room",
"Sunken City left area, crate close to the save crystal",
"Sunken City left area, crate before the bedroom",
"Sunken City left area, Girl Costume",
"Sunken City, bulb on top of the boss area",
"Sunken City cleared",
"Beating the Golem",
"Beating Octopus Prime",
"Final Boss area, bulb in the boss third form room",
"Objective complete",
]
items = [[ItemNames.SUN_FORM, ItemNames.BABY_DUMBO, ItemNames.HAS_SUN_CRYSTAL]]
items = [["Sun form", "Baby Dumbo", "Has sun crystal"]]
self.assertAccessDependency(locations, items)

View File

@@ -5,56 +5,53 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
from ..Options import TurtleRandomizer
class NatureFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the nature form"""
options = {
"turtle_randomizer": TurtleRandomizer.option_all,
"turtle_randomizer": 1,
}
def test_nature_form_location(self) -> None:
"""Test locations that require nature form"""
locations = [
AquariaLocationNames.SONG_CAVE_ANEMONE_SEED,
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
AquariaLocationNames.BEATING_BLASTER_PEG_PRIME,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM,
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_SEED_BAG,
AquariaLocationNames.BEATING_MITHALAN_PRIESTS,
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS,
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME,
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.BEATING_THE_GOLEM,
AquariaLocationNames.SUNKEN_CITY_CLEARED,
AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE,
AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE,
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.OBJECTIVE_COMPLETE
"Song Cave, Anemone Seed",
"Energy Temple blaster room, Blaster Egg",
"Beating Blaster Peg Prime",
"Kelp Forest top left area, Verse Egg",
"Kelp Forest top left area, bulb close to the Verse Egg",
"Mithalas City Castle, beating the Priests",
"Kelp Forest sprite cave, bulb in the second room",
"Kelp Forest sprite cave, Seed Bag",
"Beating Mithalan priests",
"Abyss left area, bulb in the bottom fish pass",
"Bubble Cave, Verse Egg",
"Beating Mantis Shrimp Prime",
"Sunken City right area, crate close to the save crystal",
"Sunken City right area, crate in the left bottom room",
"Sunken City left area, crate in the little pipe room",
"Sunken City left area, crate close to the save crystal",
"Sunken City left area, crate before the bedroom",
"Sunken City left area, Girl Costume",
"Sunken City, bulb on top of the boss area",
"Beating the Golem",
"Sunken City cleared",
"The Body center area, breaking Li's cage",
"The Body center area, bulb on the main path blocking tube",
"The Body left area, first bulb in the top face room",
"The Body left area, second bulb in the top face room",
"The Body left area, bulb below the water stream",
"The Body left area, bulb in the top path to the top face room",
"The Body left area, bulb in the bottom face room",
"The Body right area, bulb in the top face room",
"The Body right area, bulb in the top path to the bottom face room",
"The Body right area, bulb in the bottom face room",
"The Body bottom area, bulb in the Jelly Zap room",
"The Body bottom area, bulb in the nautilus room",
"The Body bottom area, Mutant Costume",
"Final Boss area, bulb in the boss third form room",
"Objective complete"
]
items = [[ItemNames.NATURE_FORM]]
items = [["Nature form"]]
self.assertAccessDependency(locations, items)

View File

@@ -6,7 +6,6 @@ Description: Unit test used to test that no progression items can be put in hard
from . import AquariaTestBase
from BaseClasses import ItemClassification
from ..Locations import AquariaLocationNames
class UNoProgressionHardHiddenTest(AquariaTestBase):
@@ -16,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
}
unfillable_locations = [
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
AquariaLocationNames.SUN_TEMPLE_SUN_KEY,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART,
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
"Energy Temple boss area, Fallen God Tooth",
"Mithalas boss area, beating Mithalan God",
"Kelp Forest boss area, beating Drunian God",
"Sun Temple boss area, beating Sun God",
"Sunken City, bulb on top of the boss area",
"Home Water, Nautilus Egg",
"Energy Temple blaster room, Blaster Egg",
"Mithalas City Castle, beating the Priests",
"Mermog cave, Piranha Egg",
"Octopus Cave, Dumbo Egg",
"King Jellyfish Cave, bulb in the right path from King Jelly",
"King Jellyfish Cave, Jellyfish Costume",
"Final Boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The Veil top right area, bulb at the top of the waterfall",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
"Kelp Forest bottom left area, bulb close to the spirit crystals",
"Kelp Forest bottom left area, Walker Baby",
"Sun Temple, Sun Key",
"The Body bottom area, Mutant Costume",
"Sun Temple, bulb in the hidden room of the right part",
"Arnassi Ruins, Arnassi Armor",
]
def test_unconfine_home_water_both_location_fillable(self) -> None:

View File

@@ -5,7 +5,6 @@ Description: Unit test used to test that progression items can be put in hard or
"""
from . import AquariaTestBase
from ..Locations import AquariaLocationNames
class UNoProgressionHardHiddenTest(AquariaTestBase):
@@ -15,31 +14,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
}
unfillable_locations = [
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB,
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
AquariaLocationNames.SUN_TEMPLE_SUN_KEY,
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART,
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
"Energy Temple boss area, Fallen God Tooth",
"Mithalas boss area, beating Mithalan God",
"Kelp Forest boss area, beating Drunian God",
"Sun Temple boss area, beating Sun God",
"Sunken City, bulb on top of the boss area",
"Home Water, Nautilus Egg",
"Energy Temple blaster room, Blaster Egg",
"Mithalas City Castle, beating the Priests",
"Mermog cave, Piranha Egg",
"Octopus Cave, Dumbo Egg",
"King Jellyfish Cave, bulb in the right path from King Jelly",
"King Jellyfish Cave, Jellyfish Costume",
"Final Boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The Veil top right area, bulb at the top of the waterfall",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
"Kelp Forest bottom left area, bulb close to the spirit crystals",
"Kelp Forest bottom left area, Walker Baby",
"Sun Temple, Sun Key",
"The Body bottom area, Mutant Costume",
"Sun Temple, bulb in the hidden room of the right part",
"Arnassi Ruins, Arnassi Armor",
]
def test_unconfine_home_water_both_location_fillable(self) -> None:

View File

@@ -5,8 +5,6 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
class SpiritFormAccessTest(AquariaTestBase):
@@ -15,23 +13,23 @@ class SpiritFormAccessTest(AquariaTestBase):
def test_spirit_form_location(self) -> None:
"""Test locations that require spirit form"""
locations = [
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH,
AquariaLocationNames.MITHALAS_CITY_CASTLE_TRIDENT_HEAD,
AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_KING_SKULL,
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
AquariaLocationNames.THE_WHALE_VERSE_EGG,
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT,
AquariaLocationNames.ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM,
AquariaLocationNames.ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM,
AquariaLocationNames.ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM,
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_LEFT_ROOM,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME,
AquariaLocationNames.FIRST_SECRET,
"The Veil bottom area, bulb in the spirit path",
"Mithalas City Castle, Trident Head",
"Open Water skeleton path, King Skull",
"Kelp Forest bottom left area, Walker Baby",
"Abyss right area, bulb behind the rock in the whale room",
"The Whale, Verse Egg",
"Ice Cave, bulb in the room to the right",
"Ice Cave, first bulb in the top exit room",
"Ice Cave, second bulb in the top exit room",
"Ice Cave, third bulb in the top exit room",
"Ice Cave, bulb in the left room",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
"Sunken City left area, Girl Costume",
"Beating Mantis Shrimp Prime",
"First secret",
]
items = [[ItemNames.SPIRIT_FORM]]
items = [["Spirit form"]]
self.assertAccessDependency(locations, items)

View File

@@ -5,8 +5,6 @@ Description: Unit test used to test accessibility of locations with and without
"""
from . import AquariaTestBase
from ..Items import ItemNames
from ..Locations import AquariaLocationNames
class SunFormAccessTest(AquariaTestBase):
@@ -15,16 +13,16 @@ class SunFormAccessTest(AquariaTestBase):
def test_sun_form_location(self) -> None:
"""Test locations that require sun form"""
locations = [
AquariaLocationNames.FIRST_SECRET,
AquariaLocationNames.THE_WHALE_VERSE_EGG,
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
AquariaLocationNames.BEATING_THE_GOLEM,
AquariaLocationNames.SUNKEN_CITY_CLEARED,
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
AquariaLocationNames.OBJECTIVE_COMPLETE
"First secret",
"The Whale, Verse Egg",
"Abyss right area, bulb behind the rock in the whale room",
"Octopus Cave, Dumbo Egg",
"Beating Octopus Prime",
"Sunken City, bulb on top of the boss area",
"Beating the Golem",
"Sunken City cleared",
"Final Boss area, bulb in the boss third form room",
"Objective complete"
]
items = [[ItemNames.SUN_FORM]]
items = [["Sun form"]]
self.assertAccessDependency(locations, items)

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