mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 01:23:48 -07:00
Compare commits
157 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9e79854a8 | ||
|
|
144d612c52 | ||
|
|
3acbe9ece1 | ||
|
|
7d0b701a2d | ||
|
|
f91537fb48 | ||
|
|
3c5ec49dbe | ||
|
|
9a37a136a1 | ||
|
|
54a0a5ac00 | ||
|
|
704f14ffcd | ||
|
|
925fb967d3 | ||
|
|
5dd19fccd0 | ||
|
|
781100a571 | ||
|
|
3fb0b57d19 | ||
|
|
f79657b41a | ||
|
|
4a5ba756b6 | ||
|
|
0b3d34ab24 | ||
|
|
2e81774a1f | ||
|
|
aa22b62b41 | ||
|
|
51c4fe8f67 | ||
|
|
26f9720e69 | ||
|
|
1f712d9a87 | ||
|
|
5b4d7c7526 | ||
|
|
a948697f3a | ||
|
|
e3b5451672 | ||
|
|
6c69f590cf | ||
|
|
c9625e1b35 | ||
|
|
ced93022b6 | ||
|
|
f4b926ebbe | ||
|
|
203d89d1d3 | ||
|
|
4d42814f5d | ||
|
|
d80069385d | ||
|
|
85a0d59f73 | ||
|
|
58f2205304 | ||
|
|
769fbc55a9 | ||
|
|
f43fa612d5 | ||
|
|
5b0de6b6c7 | ||
|
|
ac8a206d46 | ||
|
|
6896d631db | ||
|
|
6f2e1c2a7e | ||
|
|
ffe0221deb | ||
|
|
18e8d50768 | ||
|
|
81b9a53a37 | ||
|
|
b6ab91fe4b | ||
|
|
f26cda07db | ||
|
|
409c915375 | ||
|
|
ecc3094c70 | ||
|
|
17b3ee6eaf | ||
|
|
284e7797c5 | ||
|
|
62ce42440b | ||
|
|
7b755408fa | ||
|
|
ed721dd0c1 | ||
|
|
1a5d22ca78 | ||
|
|
21dbfd2472 | ||
|
|
472d2d5406 | ||
|
|
3af2b1dc66 | ||
|
|
6cfc3a4667 | ||
|
|
992657750c | ||
|
|
a67688749f | ||
|
|
f735416bda | ||
|
|
e5374eb8b8 | ||
|
|
b83b48629d | ||
|
|
ca6792a8a7 | ||
|
|
7cbd50a2e6 | ||
|
|
d6da3bc899 | ||
|
|
9eaca95277 | ||
|
|
c1b27f79ac | ||
|
|
0705f6e6c0 | ||
|
|
a537d8eb65 | ||
|
|
845a604955 | ||
|
|
7adb673a80 | ||
|
|
72e88bb493 | ||
|
|
089b3f17a7 | ||
|
|
ad30e3264a | ||
|
|
e262c8be9c | ||
|
|
492e3a355e | ||
|
|
1487d323cd | ||
|
|
dd88b2c658 | ||
|
|
46dfc4d4fc | ||
|
|
b0a61be9df | ||
|
|
7c00c9a49d | ||
|
|
1365bd7a0a | ||
|
|
6e5adc7abd | ||
|
|
c97e4866dd | ||
|
|
8444ffa0c7 | ||
|
|
2fb59d39c9 | ||
|
|
b5343a36ff | ||
|
|
d7a0f4cb4c | ||
|
|
77d35b95e2 | ||
|
|
b605fb1032 | ||
|
|
a5231a27cc | ||
|
|
1454bacfdd | ||
|
|
ed4e44b994 | ||
|
|
d36c983461 | ||
|
|
05aa96a335 | ||
|
|
6f2464d4ad | ||
|
|
91185f4f7c | ||
|
|
1371c63a8d | ||
|
|
30b414429f | ||
|
|
ce210cd4ee | ||
|
|
8923b06a49 | ||
|
|
b783eab1e8 | ||
|
|
b972e8c071 | ||
|
|
faeb54224e | ||
|
|
1ba7700283 | ||
|
|
710cf4ebba | ||
|
|
82260d728f | ||
|
|
62e4285924 | ||
|
|
ce78c75999 | ||
|
|
c022c742b5 | ||
|
|
3cb5219e09 | ||
|
|
5d30d16e09 | ||
|
|
4780fd9974 | ||
|
|
3ba0576cf6 | ||
|
|
283d1ab7e8 | ||
|
|
78bc7b8156 | ||
|
|
a07ddb4371 | ||
|
|
4395c608e8 | ||
|
|
f4322242a1 | ||
|
|
a3711eb463 | ||
|
|
6656528d78 | ||
|
|
e1f16c6721 | ||
|
|
334781e976 | ||
|
|
6c939d2d59 | ||
|
|
e882c68277 | ||
|
|
dbf284d4b2 | ||
|
|
75624042f7 | ||
|
|
0dade05133 | ||
|
|
fcaba14b62 | ||
|
|
6073d5e37e | ||
|
|
41a7d7eeee | ||
|
|
d3a3c29bc9 | ||
|
|
0ad5b0ade8 | ||
|
|
e6e31a27e6 | ||
|
|
a650e90b57 | ||
|
|
36f17111bf | ||
|
|
03b90cf39b | ||
|
|
5729b78504 | ||
|
|
ba50c947ba | ||
|
|
2424b79626 | ||
|
|
d4b1351c99 | ||
|
|
859ae87ec9 | ||
|
|
124ce13da7 | ||
|
|
48ea274655 | ||
|
|
85a713771b | ||
|
|
3ae8992fb6 | ||
|
|
01c6037562 | ||
|
|
4b80b786e2 | ||
|
|
bd5c8ec172 | ||
|
|
baf291d7a2 | ||
|
|
9c102da901 | ||
|
|
75e18e3cc9 | ||
|
|
a3d6036939 | ||
|
|
7eb12174b7 | ||
|
|
73146ef30c | ||
|
|
66314de965 | ||
|
|
5141f36e95 | ||
|
|
9ba613277e |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
worlds/blasphemous/region_data.py linguist-generated=true
|
||||
worlds/yachtdice/YachtWeights.py linguist-generated=true
|
||||
|
||||
2
.github/pyright-config.json
vendored
2
.github/pyright-config.json
vendored
@@ -16,7 +16,7 @@
|
||||
"reportMissingImports": true,
|
||||
"reportMissingTypeStubs": true,
|
||||
|
||||
"pythonVersion": "3.8",
|
||||
"pythonVersion": "3.10",
|
||||
"pythonPlatform": "Windows",
|
||||
|
||||
"executionEnvironments": [
|
||||
|
||||
2
.github/workflows/analyze-modified-files.yml
vendored
2
.github/workflows/analyze-modified-files.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
if: env.diff != ''
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: "Install dependencies"
|
||||
if: env.diff != ''
|
||||
|
||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -24,14 +24,15 @@ env:
|
||||
jobs:
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
build-win-py38: # RCs will still be built and signed by hand
|
||||
build-win: # RCs will still be built and signed by hand
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.8'
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
||||
@@ -111,10 +112,11 @@ jobs:
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.11" >> $GITHUB_ENV
|
||||
echo "PYTHON=python3.12" >> $GITHUB_ENV
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -44,10 +44,11 @@ jobs:
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.11" >> $GITHUB_ENV
|
||||
echo "PYTHON=python3.12" >> $GITHUB_ENV
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
|
||||
6
.github/workflows/scan-build.yml
vendored
6
.github/workflows/scan-build.yml
vendored
@@ -40,10 +40,10 @@ jobs:
|
||||
run: |
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
chmod +x ./llvm.sh
|
||||
sudo ./llvm.sh 17
|
||||
sudo ./llvm.sh 19
|
||||
- name: Install scan-build command
|
||||
run: |
|
||||
sudo apt install clang-tools-17
|
||||
sudo apt install clang-tools-19
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: scan-build
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
|
||||
scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
|
||||
- name: Store report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
4
.github/workflows/unittests.yml
vendored
4
.github/workflows/unittests.yml
vendored
@@ -33,13 +33,11 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python:
|
||||
- {version: '3.8'}
|
||||
- {version: '3.9'}
|
||||
- {version: '3.10'}
|
||||
- {version: '3.11'}
|
||||
- {version: '3.12'}
|
||||
include:
|
||||
- python: {version: '3.8'} # win7 compat
|
||||
- python: {version: '3.10'} # old compat
|
||||
os: windows-latest
|
||||
- python: {version: '3.12'} # current
|
||||
os: windows-latest
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
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)
|
||||
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
@@ -20,7 +18,7 @@ import NetUtils
|
||||
import Options
|
||||
import Utils
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
@@ -231,7 +229,7 @@ class MultiWorld():
|
||||
for player in self.player_ids:
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||
self.worlds[player] = world_type(self, player)
|
||||
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
|
||||
options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass
|
||||
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
|
||||
for option_key in options_dataclass.type_hints})
|
||||
|
||||
@@ -606,6 +604,49 @@ class MultiWorld():
|
||||
state.collect(location.item, True, location)
|
||||
locations -= sphere
|
||||
|
||||
def get_sendable_spheres(self) -> Iterator[Set[Location]]:
|
||||
"""
|
||||
yields a set of multiserver sendable locations (location.item.code: int) for each logical sphere
|
||||
|
||||
If there are unreachable locations, the last sphere of reachable locations is followed by an empty set,
|
||||
and then a set of all of the unreachable locations.
|
||||
"""
|
||||
state = CollectionState(self)
|
||||
locations: Set[Location] = set()
|
||||
events: Set[Location] = set()
|
||||
for location in self.get_filled_locations():
|
||||
if type(location.item.code) is int:
|
||||
locations.add(location)
|
||||
else:
|
||||
events.add(location)
|
||||
|
||||
while locations:
|
||||
sphere: Set[Location] = set()
|
||||
|
||||
# cull events out
|
||||
done_events: Set[Union[Location, None]] = {None}
|
||||
while done_events:
|
||||
done_events = set()
|
||||
for event in events:
|
||||
if event.can_reach(state):
|
||||
state.collect(event.item, True, event)
|
||||
done_events.add(event)
|
||||
events -= done_events
|
||||
|
||||
for location in locations:
|
||||
if location.can_reach(state):
|
||||
sphere.add(location)
|
||||
|
||||
yield sphere
|
||||
if not sphere:
|
||||
if locations:
|
||||
yield locations # unreachable locations
|
||||
break
|
||||
|
||||
for location in sphere:
|
||||
state.collect(location.item, True, location)
|
||||
locations -= sphere
|
||||
|
||||
def fulfills_accessibility(self, state: Optional[CollectionState] = None):
|
||||
"""Check if accessibility rules are fulfilled with current or supplied state."""
|
||||
if not state:
|
||||
@@ -975,7 +1016,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
|
||||
@@ -1075,7 +1116,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.
|
||||
@@ -1112,7 +1153,7 @@ class Region:
|
||||
return exit_
|
||||
|
||||
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
|
||||
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
|
||||
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
|
||||
"""
|
||||
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||
|
||||
@@ -1122,10 +1163,14 @@ class Region:
|
||||
"""
|
||||
if not isinstance(exits, Dict):
|
||||
exits = dict.fromkeys(exits)
|
||||
for connecting_region, name in exits.items():
|
||||
self.connect(self.multiworld.get_region(connecting_region, self.player),
|
||||
name,
|
||||
rules[connecting_region] if rules and connecting_region in rules else None)
|
||||
return [
|
||||
self.connect(
|
||||
self.multiworld.get_region(connecting_region, self.player),
|
||||
name,
|
||||
rules[connecting_region] if rules and connecting_region in rules else None,
|
||||
)
|
||||
for connecting_region, name in exits.items()
|
||||
]
|
||||
|
||||
def __repr__(self):
|
||||
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
||||
@@ -1264,6 +1309,10 @@ 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)
|
||||
@@ -1386,14 +1435,21 @@ class Spoiler:
|
||||
|
||||
# second phase, sphere 0
|
||||
removed_precollected: List[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)
|
||||
|
||||
for precollected_items in multiworld.precollected_items.values():
|
||||
# The list of items is mutated by removing one item at a time to determine if each item is required to beat
|
||||
# the game, and re-adding that item if it was required, so a copy needs to be made before iterating.
|
||||
for item in precollected_items.copy():
|
||||
if not item.advancement:
|
||||
continue
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
precollected_items.remove(item)
|
||||
multiworld.state.remove(item)
|
||||
if not multiworld.can_beat_game():
|
||||
# Add the item back into `precollected_items` and collect it into `multiworld.state`.
|
||||
multiworld.push_precollected(item)
|
||||
else:
|
||||
removed_precollected.append(item)
|
||||
|
||||
# we are now down to just the required progress items in collection_spheres. Unfortunately
|
||||
# the previous pruning stage could potentially have made certain items dependant on others
|
||||
@@ -1532,7 +1588,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 Items:\n\n')
|
||||
outfile.write('\n\nUnreachable Progression Items:\n\n')
|
||||
outfile.write(
|
||||
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
||||
|
||||
|
||||
@@ -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, SlotType)
|
||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
|
||||
from Utils import Version, stream_input, async_start
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
@@ -412,6 +412,7 @@ class CommonContext:
|
||||
await self.server.socket.close()
|
||||
if self.server_task is not None:
|
||||
await self.server_task
|
||||
self.ui.update_hints()
|
||||
|
||||
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
|
||||
""" `msgs` JSON serializable """
|
||||
@@ -551,7 +552,14 @@ class CommonContext:
|
||||
await self.ui_task
|
||||
if self.input_task:
|
||||
self.input_task.cancel()
|
||||
|
||||
|
||||
# Hints
|
||||
def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None:
|
||||
msg = {"cmd": "UpdateHint", "location": location, "player": finding_player}
|
||||
if status is not None:
|
||||
msg["status"] = status
|
||||
async_start(self.send_msgs([msg]), name="update_hint")
|
||||
|
||||
# DataPackage
|
||||
async def prepare_data_package(self, relevant_games: typing.Set[str],
|
||||
remote_date_package_versions: typing.Dict[str, int],
|
||||
@@ -710,6 +718,11 @@ class CommonContext:
|
||||
|
||||
def run_cli(self):
|
||||
if sys.stdin:
|
||||
if sys.stdin.fileno() != 0:
|
||||
from multiprocessing import parent_process
|
||||
if parent_process():
|
||||
return # ignore MultiProcessing pipe
|
||||
|
||||
# steam overlay breaks when starting console_loop
|
||||
if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''):
|
||||
logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.")
|
||||
|
||||
59
Fill.py
59
Fill.py
@@ -36,7 +36,8 @@ 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, name: str = "Unknown") -> None:
|
||||
allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True,
|
||||
name: str = "Unknown") -> None:
|
||||
"""
|
||||
:param multiworld: Multiworld to be filled.
|
||||
:param base_state: State assumed before fill.
|
||||
@@ -63,14 +64,22 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
placed = 0
|
||||
|
||||
while any(reachable_items.values()) and locations:
|
||||
# grab one item per player
|
||||
items_to_place = [items.pop()
|
||||
for items in reachable_items.values() if items]
|
||||
if one_item_per_player:
|
||||
# grab one item per player
|
||||
items_to_place = [items.pop()
|
||||
for items in reachable_items.values() if items]
|
||||
else:
|
||||
next_player = multiworld.random.choice([player for player, items in reachable_items.items() if items])
|
||||
items_to_place = []
|
||||
if item_pool:
|
||||
items_to_place.append(reachable_items[next_player].pop())
|
||||
|
||||
for item in items_to_place:
|
||||
for p, pool_item in enumerate(item_pool):
|
||||
if pool_item is item:
|
||||
item_pool.pop(p)
|
||||
break
|
||||
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player)
|
||||
if single_player_placement else None)
|
||||
@@ -480,7 +489,8 @@ 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")
|
||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||
name="Priority", one_item_per_player=False)
|
||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||
defaultlocations = prioritylocations + defaultlocations
|
||||
|
||||
@@ -978,15 +988,32 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
multiworld.random.shuffle(items)
|
||||
count = 0
|
||||
err: typing.List[str] = []
|
||||
successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
|
||||
successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = []
|
||||
claimed_indices: typing.Set[typing.Optional[int]] = set()
|
||||
for item_name in items:
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
index_to_delete: typing.Optional[int] = None
|
||||
if from_pool:
|
||||
try:
|
||||
# If from_pool, try to find an existing item with this name & player in the itempool and use it
|
||||
index_to_delete, item = next(
|
||||
(i, item) for i, item in enumerate(multiworld.itempool)
|
||||
if item.player == player and item.name == item_name and i not in claimed_indices
|
||||
)
|
||||
except StopIteration:
|
||||
warn(
|
||||
f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.",
|
||||
placement['force'])
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
else:
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
|
||||
for location in reversed(candidates):
|
||||
if (location.address is None) == (item.code is None): # either both None or both not None
|
||||
if not location.item:
|
||||
if location.item_rule(item):
|
||||
if location.can_fill(multiworld.state, item, False):
|
||||
successful_pairs.append((item, location))
|
||||
successful_pairs.append((index_to_delete, item, location))
|
||||
claimed_indices.add(index_to_delete)
|
||||
candidates.remove(location)
|
||||
count = count + 1
|
||||
break
|
||||
@@ -998,6 +1025,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
err.append(f"Cannot place {item_name} into already filled location {location}.")
|
||||
else:
|
||||
err.append(f"Mismatch between {item_name} and {location}, only one is an event.")
|
||||
|
||||
if count == maxcount:
|
||||
break
|
||||
if count < placement['count']['min']:
|
||||
@@ -1005,17 +1033,16 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
failed(
|
||||
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
|
||||
placement['force'])
|
||||
for (item, location) in successful_pairs:
|
||||
|
||||
# Sort indices in reverse so we can remove them one by one
|
||||
successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True)
|
||||
|
||||
for (index, item, location) in successful_pairs:
|
||||
multiworld.push_item(location, item, collect=False)
|
||||
location.locked = True
|
||||
logging.debug(f"Plando placed {item} at {location}")
|
||||
if from_pool:
|
||||
try:
|
||||
multiworld.itempool.remove(item)
|
||||
except ValueError:
|
||||
warn(
|
||||
f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.",
|
||||
placement['force'])
|
||||
if index is not None: # If this item is from_pool and was found in the pool, remove it.
|
||||
multiworld.itempool.pop(index)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
|
||||
13
Generate.py
13
Generate.py
@@ -114,7 +114,14 @@ 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_cache[fname] = read_weights_yamls(path)
|
||||
weights_for_file = []
|
||||
for doc_idx, yaml in enumerate(read_weights_yamls(path)):
|
||||
if yaml is None:
|
||||
logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}")
|
||||
else:
|
||||
weights_for_file.append(yaml)
|
||||
weights_cache[fname] = tuple(weights_for_file)
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
|
||||
|
||||
@@ -453,6 +460,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
||||
|
||||
ret.game = get_choice("game", weights)
|
||||
if not isinstance(ret.game, str):
|
||||
if ret.game is None:
|
||||
raise Exception('"game" not specified')
|
||||
raise Exception(f"Invalid game: {ret.game}")
|
||||
if ret.game not in AutoWorldRegister.world_types:
|
||||
from worlds import failed_world_loads
|
||||
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
|
||||
|
||||
79
Launcher.py
79
Launcher.py
@@ -22,16 +22,15 @@ from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||
|
||||
import Utils
|
||||
import settings
|
||||
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
|
||||
is_windows, is_macos, is_linux
|
||||
import settings
|
||||
import Utils
|
||||
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
||||
user_path)
|
||||
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
||||
|
||||
|
||||
def open_host_yaml():
|
||||
@@ -127,12 +126,13 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
elif component.display_name == "Text Client":
|
||||
text_client_component = component
|
||||
|
||||
from kvui import App, Button, BoxLayout, Label, Clock, Window
|
||||
if client_component is None:
|
||||
run_component(text_client_component, *launch_args)
|
||||
return
|
||||
|
||||
from kvui import App, Button, BoxLayout, Label, 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,47 +140,29 @@ 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))
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
text_client_button = Button(
|
||||
text=text_client_component.display_name,
|
||||
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(text_client_button)
|
||||
game_client_button = Button(
|
||||
text=client_component.display_name,
|
||||
on_release=lambda *args: run_component(client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(game_client_button)
|
||||
|
||||
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)
|
||||
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()
|
||||
super()._stop(*largs)
|
||||
|
||||
Popup().run()
|
||||
|
||||
@@ -242,9 +224,8 @@ refresh_components: Optional[Callable[[], None]] = None
|
||||
|
||||
|
||||
def run_gui():
|
||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
|
||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
|
||||
from kivy.core.window import Window
|
||||
from kivy.uix.image import AsyncImage
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
|
||||
class Launcher(App):
|
||||
@@ -277,8 +258,8 @@ def run_gui():
|
||||
button.component = component
|
||||
button.bind(on_release=self.component_action)
|
||||
if component.icon != "icon":
|
||||
image = AsyncImage(source=icon_paths[component.icon],
|
||||
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
||||
image = ApAsyncImage(source=icon_paths[component.icon],
|
||||
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
||||
box_layout = RelativeLayout(size_hint_y=None, height=40)
|
||||
box_layout.add_widget(button)
|
||||
box_layout.add_widget(image)
|
||||
|
||||
90
Main.py
90
Main.py
@@ -153,45 +153,38 @@ 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.
|
||||
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:
|
||||
old_items.append(item)
|
||||
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
|
||||
}
|
||||
|
||||
# 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
|
||||
if target_per_player:
|
||||
new_itempool: List[Item] = []
|
||||
|
||||
# Make new itempool with start_inventory_from_pool items removed
|
||||
for item in multiworld.itempool:
|
||||
if depletion_pool[item.player].get(item.name, 0):
|
||||
depletion_pool[item.player][item.name] -= 1
|
||||
else:
|
||||
new_itempool.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
|
||||
|
||||
multiworld.link_items()
|
||||
|
||||
@@ -249,6 +242,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
from NetUtils import HintStatus
|
||||
slot_data = {}
|
||||
client_versions = {}
|
||||
games = {}
|
||||
@@ -273,10 +267,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for slot in multiworld.player_ids:
|
||||
slot_data[slot] = multiworld.worlds[slot].fill_slot_data()
|
||||
|
||||
def precollect_hint(location):
|
||||
def precollect_hint(location: Location, auto_status: HintStatus):
|
||||
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False, entrance, location.item.flags)
|
||||
location.item.code, False, entrance, location.item.flags, auto_status)
|
||||
precollected_hints[location.player].add(hint)
|
||||
if location.item.player not in multiworld.groups:
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
@@ -289,19 +283,22 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
if type(location.address) == int:
|
||||
assert location.item.code is not None, "item code None should be event, " \
|
||||
"location.address should then also be None. Location: " \
|
||||
f" {location}"
|
||||
f" {location}, Item: {location.item}"
|
||||
assert location.address not in locations_data[location.player], (
|
||||
f"Locations with duplicate address. {location} and "
|
||||
f"{locations_data[location.player][location.address]}")
|
||||
locations_data[location.player][location.address] = \
|
||||
location.item.code, location.item.player, location.item.flags
|
||||
auto_status = HintStatus.HINT_AVOID if location.item.trap else HintStatus.HINT_PRIORITY
|
||||
if location.name in multiworld.worlds[location.player].options.start_location_hints:
|
||||
precollect_hint(location)
|
||||
if not location.item.trap: # Unspecified status for location hints, except traps
|
||||
auto_status = HintStatus.HINT_UNSPECIFIED
|
||||
precollect_hint(location, auto_status)
|
||||
elif location.item.name in multiworld.worlds[location.item.player].options.start_hints:
|
||||
precollect_hint(location)
|
||||
precollect_hint(location, auto_status)
|
||||
elif any([location.item.name in multiworld.worlds[player].options.start_hints
|
||||
for player in multiworld.groups.get(location.item.player, {}).get("players", [])]):
|
||||
precollect_hint(location)
|
||||
precollect_hint(location, auto_status)
|
||||
|
||||
# embedded data package
|
||||
data_package = {
|
||||
@@ -313,11 +310,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
# get spheres -> filter address==None -> skip empty
|
||||
spheres: List[Dict[int, Set[int]]] = []
|
||||
for sphere in multiworld.get_spheres():
|
||||
for sphere in multiworld.get_sendable_spheres():
|
||||
current_sphere: Dict[int, Set[int]] = collections.defaultdict(set)
|
||||
for sphere_location in sphere:
|
||||
if type(sphere_location.address) is int:
|
||||
current_sphere[sphere_location.player].add(sphere_location.address)
|
||||
current_sphere[sphere_location.player].add(sphere_location.address)
|
||||
|
||||
if current_sphere:
|
||||
spheres.append(dict(current_sphere))
|
||||
|
||||
@@ -5,8 +5,15 @@ import multiprocessing
|
||||
import warnings
|
||||
|
||||
|
||||
if sys.version_info < (3, 8, 6):
|
||||
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
||||
if sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 11):
|
||||
# Official micro version updates. This should match the number in docs/running from source.md.
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.10.15+ is supported.")
|
||||
elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 15):
|
||||
# There are known security issues, but no easy way to install fixed versions on Windows for testing.
|
||||
warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.")
|
||||
elif sys.version_info < (3, 10, 1):
|
||||
# Other platforms may get security backports instead of micro updates, so the number is unreliable.
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.1+ is supported.")
|
||||
|
||||
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
||||
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
|
||||
|
||||
211
MultiServer.py
211
MultiServer.py
@@ -41,7 +41,8 @@ import NetUtils
|
||||
import Utils
|
||||
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||
SlotType, LocationStore
|
||||
SlotType, LocationStore, Hint, HintStatus
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
min_client_version = Version(0, 1, 6)
|
||||
colorama.init()
|
||||
@@ -228,7 +229,7 @@ class Context:
|
||||
self.hint_cost = hint_cost
|
||||
self.location_check_points = location_check_points
|
||||
self.hints_used = collections.defaultdict(int)
|
||||
self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set)
|
||||
self.hints: typing.Dict[team_slot, typing.Set[Hint]] = collections.defaultdict(set)
|
||||
self.release_mode: str = release_mode
|
||||
self.remaining_mode: str = remaining_mode
|
||||
self.collect_mode: str = collect_mode
|
||||
@@ -656,13 +657,29 @@ class Context:
|
||||
return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||
return 0
|
||||
|
||||
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None):
|
||||
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None,
|
||||
changed: typing.Optional[typing.Set[team_slot]] = None) -> None:
|
||||
"""Refreshes the hints for the specified team/slot. Providing 'None' for either team or slot
|
||||
will refresh all teams or all slots respectively. If a set is passed for 'changed', each (team,slot)
|
||||
pair that has at least one hint modified will be added to the set.
|
||||
"""
|
||||
for hint_team, hint_slot in self.hints:
|
||||
if (team is None or team == hint_team) and (slot is None or slot == hint_slot):
|
||||
self.hints[hint_team, hint_slot] = {
|
||||
hint.re_check(self, hint_team) for hint in
|
||||
self.hints[hint_team, hint_slot]
|
||||
}
|
||||
if team != hint_team and team is not None:
|
||||
continue # Check specified team only, all if team is None
|
||||
if slot != hint_slot and slot is not None:
|
||||
continue # Check specified slot only, all if slot is None
|
||||
new_hints: typing.Set[Hint] = set()
|
||||
for hint in self.hints[hint_team, hint_slot]:
|
||||
new_hint = hint.re_check(self, hint_team)
|
||||
new_hints.add(new_hint)
|
||||
if hint == new_hint:
|
||||
continue
|
||||
for player in self.slot_set(hint.receiving_player) | {hint.finding_player}:
|
||||
if changed is not None:
|
||||
changed.add((hint_team,player))
|
||||
if slot is not None and slot != player:
|
||||
self.replace_hint(hint_team, player, hint, new_hint)
|
||||
self.hints[hint_team, hint_slot] = new_hints
|
||||
|
||||
def get_rechecked_hints(self, team: int, slot: int):
|
||||
self.recheck_hints(team, slot)
|
||||
@@ -711,7 +728,7 @@ class Context:
|
||||
else:
|
||||
return self.player_names[team, slot]
|
||||
|
||||
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False,
|
||||
def notify_hints(self, team: int, hints: typing.List[Hint], only_new: bool = False,
|
||||
recipients: typing.Sequence[int] = None):
|
||||
"""Send and remember hints."""
|
||||
if only_new:
|
||||
@@ -727,15 +744,15 @@ class Context:
|
||||
if not hint.local and data not in concerns[hint.finding_player]:
|
||||
concerns[hint.finding_player].append(data)
|
||||
# remember hints in all cases
|
||||
if not hint.found:
|
||||
# since hints are bidirectional, finding player and receiving player,
|
||||
# we can check once if hint already exists
|
||||
if hint not in self.hints[team, hint.finding_player]:
|
||||
self.hints[team, hint.finding_player].add(hint)
|
||||
new_hint_events.add(hint.finding_player)
|
||||
for player in self.slot_set(hint.receiving_player):
|
||||
self.hints[team, player].add(hint)
|
||||
new_hint_events.add(player)
|
||||
|
||||
# since hints are bidirectional, finding player and receiving player,
|
||||
# we can check once if hint already exists
|
||||
if hint not in self.hints[team, hint.finding_player]:
|
||||
self.hints[team, hint.finding_player].add(hint)
|
||||
new_hint_events.add(hint.finding_player)
|
||||
for player in self.slot_set(hint.receiving_player):
|
||||
self.hints[team, player].add(hint)
|
||||
new_hint_events.add(player)
|
||||
|
||||
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
||||
for slot in new_hint_events:
|
||||
@@ -749,6 +766,17 @@ 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):
|
||||
@@ -947,9 +975,13 @@ def get_status_string(ctx: Context, team: int, tag: str):
|
||||
tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags])
|
||||
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
|
||||
tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else ""
|
||||
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
|
||||
status_text = (
|
||||
" and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else
|
||||
" and is ready." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_READY else
|
||||
"."
|
||||
)
|
||||
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
|
||||
f"{tag_text}{goal_text} {completion_text}"
|
||||
f"{tag_text}{status_text} {completion_text}"
|
||||
return text
|
||||
|
||||
|
||||
@@ -1050,14 +1082,15 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
"hint_points": get_slot_points(ctx, team, slot),
|
||||
"checked_locations": new_locations, # send back new checks only
|
||||
}])
|
||||
old_hints = ctx.hints[team, slot].copy()
|
||||
ctx.recheck_hints(team, slot)
|
||||
if old_hints != ctx.hints[team, slot]:
|
||||
ctx.on_changed_hints(team, slot)
|
||||
updated_slots: typing.Set[tuple[int, int]] = set()
|
||||
ctx.recheck_hints(team, slot, updated_slots)
|
||||
for hint_team, hint_slot in updated_slots:
|
||||
ctx.on_changed_hints(hint_team, hint_slot)
|
||||
ctx.save()
|
||||
|
||||
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str], auto_status: HintStatus) \
|
||||
-> typing.List[Hint]:
|
||||
hints = []
|
||||
slots: typing.Set[int] = {slot}
|
||||
for group_id, group in ctx.groups.items():
|
||||
@@ -1067,31 +1100,58 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
|
||||
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
|
||||
for finding_player, location_id, item_id, receiving_player, item_flags \
|
||||
in ctx.locations.find_item(slots, seeked_item_id):
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
||||
item_flags))
|
||||
prev_hint = ctx.get_hint(team, slot, location_id)
|
||||
if prev_hint:
|
||||
hints.append(prev_hint)
|
||||
else:
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||
new_status = auto_status
|
||||
if found:
|
||||
new_status = HintStatus.HINT_FOUND
|
||||
elif item_flags & ItemClassification.trap:
|
||||
new_status = HintStatus.HINT_AVOID
|
||||
hints.append(Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
||||
item_flags, new_status))
|
||||
|
||||
return hints
|
||||
|
||||
|
||||
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str, auto_status: HintStatus) \
|
||||
-> typing.List[Hint]:
|
||||
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
|
||||
return collect_hint_location_id(ctx, team, slot, seeked_location)
|
||||
return collect_hint_location_id(ctx, team, slot, seeked_location, auto_status)
|
||||
|
||||
|
||||
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int, auto_status: HintStatus) \
|
||||
-> typing.List[Hint]:
|
||||
prev_hint = ctx.get_hint(team, slot, seeked_location)
|
||||
if prev_hint:
|
||||
return [prev_hint]
|
||||
result = ctx.locations[slot].get(seeked_location, (None, None, None))
|
||||
if any(result):
|
||||
item_id, receiving_player, item_flags = result
|
||||
|
||||
found = seeked_location in ctx.location_checks[team, slot]
|
||||
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
|
||||
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)]
|
||||
new_status = auto_status
|
||||
if found:
|
||||
new_status = HintStatus.HINT_FOUND
|
||||
elif item_flags & ItemClassification.trap:
|
||||
new_status = HintStatus.HINT_AVOID
|
||||
return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags,
|
||||
new_status)]
|
||||
return []
|
||||
|
||||
|
||||
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
status_names: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "(found)",
|
||||
HintStatus.HINT_UNSPECIFIED: "(unspecified)",
|
||||
HintStatus.HINT_NO_PRIORITY: "(no priority)",
|
||||
HintStatus.HINT_AVOID: "(avoid)",
|
||||
HintStatus.HINT_PRIORITY: "(priority)",
|
||||
}
|
||||
def format_hint(ctx: Context, team: int, hint: Hint) -> str:
|
||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||
f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \
|
||||
f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \
|
||||
@@ -1099,7 +1159,8 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
|
||||
if hint.entrance:
|
||||
text += f" at {hint.entrance}"
|
||||
return text + (". (found)" if hint.found else ".")
|
||||
|
||||
return text + ". " + status_names.get(hint.status, "(unknown)")
|
||||
|
||||
|
||||
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||
@@ -1503,7 +1564,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]}
|
||||
@@ -1529,9 +1590,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||
hints = []
|
||||
elif not for_location:
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id)
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
|
||||
else:
|
||||
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id)
|
||||
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
|
||||
|
||||
else:
|
||||
game = self.ctx.games[self.client.slot]
|
||||
@@ -1551,16 +1612,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
hints = []
|
||||
for item_name in self.ctx.item_name_groups[game][hint_name]:
|
||||
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name, auto_status))
|
||||
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
|
||||
elif hint_name in self.ctx.location_name_groups[game]: # location group name
|
||||
hints = []
|
||||
for loc_name in self.ctx.location_name_groups[game][hint_name]:
|
||||
if loc_name in self.ctx.location_names_for_game(game):
|
||||
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name))
|
||||
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name, auto_status))
|
||||
else: # location name
|
||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
|
||||
|
||||
else:
|
||||
self.output(response)
|
||||
@@ -1832,13 +1893,56 @@ 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))
|
||||
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location,
|
||||
HintStatus.HINT_UNSPECIFIED))
|
||||
locs.append(NetworkItem(target_item, location, target_player, flags))
|
||||
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2)
|
||||
if locs and create_as_hint:
|
||||
ctx.save()
|
||||
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||
|
||||
|
||||
elif cmd == 'UpdateHint':
|
||||
location = args["location"]
|
||||
player = args["player"]
|
||||
status = args["status"]
|
||||
if not isinstance(player, int) or not isinstance(location, int) \
|
||||
or (status is not None and not isinstance(status, int)):
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
hint = ctx.get_hint(client.team, player, location)
|
||||
if not hint:
|
||||
return # Ignored safely
|
||||
if client.slot not in ctx.slot_set(hint.receiving_player):
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
new_hint = hint
|
||||
if status is None:
|
||||
return
|
||||
try:
|
||||
status = HintStatus(status)
|
||||
except ValueError:
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments",
|
||||
"text": 'UpdateHint: Invalid Status', "original_cmd": cmd}])
|
||||
return
|
||||
if status == HintStatus.HINT_FOUND:
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments",
|
||||
"text": 'UpdateHint: Cannot manually update status to "HINT_FOUND"', "original_cmd": cmd}])
|
||||
return
|
||||
new_hint = new_hint.re_prioritize(ctx, status)
|
||||
if hint == new_hint:
|
||||
return
|
||||
ctx.replace_hint(client.team, hint.finding_player, hint, new_hint)
|
||||
ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint)
|
||||
ctx.save()
|
||||
ctx.on_changed_hints(client.team, hint.finding_player)
|
||||
ctx.on_changed_hints(client.team, hint.receiving_player)
|
||||
|
||||
elif cmd == 'StatusUpdate':
|
||||
update_client_status(ctx, client, args["status"])
|
||||
|
||||
@@ -2143,9 +2247,9 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
hints = []
|
||||
for item_name_from_group in self.ctx.item_name_groups[game][item]:
|
||||
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group, HintStatus.HINT_PRIORITY))
|
||||
else: # item name or id
|
||||
hints = collect_hints(self.ctx, team, slot, item)
|
||||
hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY)
|
||||
|
||||
if hints:
|
||||
self.ctx.notify_hints(team, hints)
|
||||
@@ -2179,14 +2283,17 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
|
||||
if usable:
|
||||
if isinstance(location, int):
|
||||
hints = collect_hint_location_id(self.ctx, team, slot, location)
|
||||
hints = collect_hint_location_id(self.ctx, team, slot, location,
|
||||
HintStatus.HINT_UNSPECIFIED)
|
||||
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
|
||||
hints = []
|
||||
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
|
||||
if loc_name_from_group in self.ctx.location_names_for_game(game):
|
||||
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group))
|
||||
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group,
|
||||
HintStatus.HINT_UNSPECIFIED))
|
||||
else:
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location)
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location,
|
||||
HintStatus.HINT_UNSPECIFIED)
|
||||
if hints:
|
||||
self.ctx.notify_hints(team, hints)
|
||||
else:
|
||||
@@ -2276,6 +2383,8 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument('--cert_key', help="Path to SSL Certificate Key file")
|
||||
parser.add_argument('--loglevel', default=defaults["loglevel"],
|
||||
choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--logtime', help="Add timestamps to STDOUT",
|
||||
default=defaults["logtime"], action='store_true')
|
||||
parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int)
|
||||
parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int)
|
||||
parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true')
|
||||
@@ -2356,7 +2465,9 @@ def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLConte
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace):
|
||||
Utils.init_logging("Server", loglevel=args.loglevel.lower())
|
||||
Utils.init_logging(name="Server",
|
||||
loglevel=args.loglevel.lower(),
|
||||
add_timestamp=args.logtime)
|
||||
|
||||
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||
args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode,
|
||||
|
||||
45
NetUtils.py
45
NetUtils.py
@@ -29,6 +29,14 @@ class ClientStatus(ByValue, enum.IntEnum):
|
||||
CLIENT_GOAL = 30
|
||||
|
||||
|
||||
class HintStatus(enum.IntEnum):
|
||||
HINT_FOUND = 0
|
||||
HINT_UNSPECIFIED = 1
|
||||
HINT_NO_PRIORITY = 10
|
||||
HINT_AVOID = 20
|
||||
HINT_PRIORITY = 30
|
||||
|
||||
|
||||
class SlotType(ByValue, enum.IntFlag):
|
||||
spectator = 0b00
|
||||
player = 0b01
|
||||
@@ -224,7 +232,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
|
||||
def _handle_player_id(self, node: JSONMessagePart):
|
||||
player = int(node["text"])
|
||||
node["color"] = 'magenta' if player == self.ctx.slot else 'yellow'
|
||||
node["color"] = 'magenta' if self.ctx.slot_concerns_self(player) else 'yellow'
|
||||
node["text"] = self.ctx.player_names[player]
|
||||
return self._handle_color(node)
|
||||
|
||||
@@ -297,6 +305,20 @@ 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",
|
||||
}
|
||||
class Hint(typing.NamedTuple):
|
||||
receiving_player: int
|
||||
finding_player: int
|
||||
@@ -305,14 +327,21 @@ class Hint(typing.NamedTuple):
|
||||
found: bool
|
||||
entrance: str = ""
|
||||
item_flags: int = 0
|
||||
status: HintStatus = HintStatus.HINT_UNSPECIFIED
|
||||
|
||||
def re_check(self, ctx, team) -> Hint:
|
||||
if self.found:
|
||||
if self.found and self.status == HintStatus.HINT_FOUND:
|
||||
return self
|
||||
found = self.location in ctx.location_checks[team, self.finding_player]
|
||||
if found:
|
||||
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance,
|
||||
self.item_flags)
|
||||
return self._replace(found=found, status=HintStatus.HINT_FOUND)
|
||||
return self
|
||||
|
||||
def re_prioritize(self, ctx, status: HintStatus) -> Hint:
|
||||
if self.found and status != HintStatus.HINT_FOUND:
|
||||
status = HintStatus.HINT_FOUND
|
||||
if status != self.status:
|
||||
return self._replace(status=status)
|
||||
return self
|
||||
|
||||
def __hash__(self):
|
||||
@@ -334,10 +363,8 @@ class Hint(typing.NamedTuple):
|
||||
else:
|
||||
add_json_text(parts, "'s World")
|
||||
add_json_text(parts, ". ")
|
||||
if self.found:
|
||||
add_json_text(parts, "(found)", type="color", color="green")
|
||||
else:
|
||||
add_json_text(parts, "(not found)", type="color", color="red")
|
||||
add_json_text(parts, status_names.get(self.status, "(unknown)"), type="color",
|
||||
color=status_colors.get(self.status, "red"))
|
||||
|
||||
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
|
||||
"receiving": self.receiving_player,
|
||||
@@ -383,6 +410,8 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
|
||||
checked = state[team, slot]
|
||||
if not checked:
|
||||
# This optimizes the case where everyone connects to a fresh game at the same time.
|
||||
if slot not in self:
|
||||
raise KeyError(slot)
|
||||
return []
|
||||
return [location_id for
|
||||
location_id in self[slot] if
|
||||
|
||||
51
Options.py
51
Options.py
@@ -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():
|
||||
@@ -828,7 +828,10 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
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
|
||||
@@ -860,6 +863,8 @@ class ItemDict(OptionDict):
|
||||
verify_item_name = True
|
||||
|
||||
def __init__(self, value: typing.Dict[str, int]):
|
||||
if any(item_count is None for item_count in value.values()):
|
||||
raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .")
|
||||
if any(item_count < 1 for item_count in value.values()):
|
||||
raise Exception("Cannot have non-positive item counts.")
|
||||
super(ItemDict, self).__init__(value)
|
||||
@@ -1175,7 +1180,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.
|
||||
@@ -1193,7 +1198,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.
|
||||
@@ -1244,12 +1249,16 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
||||
progression_balancing: ProgressionBalancing
|
||||
accessibility: Accessibility
|
||||
|
||||
def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]:
|
||||
def as_dict(self,
|
||||
*option_names: str,
|
||||
casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake",
|
||||
toggles_as_bools: bool = False) -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
Returns a dictionary of [str, Option.value]
|
||||
|
||||
:param option_names: names of the options to return
|
||||
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
|
||||
:param toggles_as_bools: whether toggle options should be output as bools instead of strings
|
||||
"""
|
||||
assert option_names, "options.as_dict() was used without any option names."
|
||||
option_results = {}
|
||||
@@ -1271,6 +1280,8 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
||||
value = getattr(self, option_name).value
|
||||
if isinstance(value, set):
|
||||
value = sorted(value)
|
||||
elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle):
|
||||
value = bool(value)
|
||||
option_results[display_name] = value
|
||||
else:
|
||||
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
|
||||
@@ -1460,22 +1471,26 @@ it.
|
||||
def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[
|
||||
str, typing.Dict[str, typing.Type[Option[typing.Any]]]]:
|
||||
"""Generates and returns a dictionary for the option groups of a specified world."""
|
||||
option_groups = {option: option_group.name
|
||||
for option_group in world.web.option_groups
|
||||
for option in option_group.options}
|
||||
option_to_name = {option: option_name for option_name, option in world.options_dataclass.type_hints.items()}
|
||||
|
||||
ordered_groups = {group.name: group.options for group in world.web.option_groups}
|
||||
|
||||
# add a default option group for uncategorized options to get thrown into
|
||||
ordered_groups = ["Game Options"]
|
||||
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
|
||||
grouped_options = {group: {} for group in ordered_groups}
|
||||
for option_name, option in world.options_dataclass.type_hints.items():
|
||||
if visibility_level & option.visibility:
|
||||
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
||||
if "Game Options" not in ordered_groups:
|
||||
grouped_options = set(option for group in ordered_groups.values() for option in group)
|
||||
ungrouped_options = [option for option in option_to_name if option not in grouped_options]
|
||||
# only add the game options group if we have ungrouped options
|
||||
if ungrouped_options:
|
||||
ordered_groups = {**{"Game Options": ungrouped_options}, **ordered_groups}
|
||||
|
||||
# if the world doesn't have any ungrouped options, this group will be empty so just remove it
|
||||
if not grouped_options["Game Options"]:
|
||||
del grouped_options["Game Options"]
|
||||
|
||||
return grouped_options
|
||||
return {
|
||||
group: {
|
||||
option_to_name[option]: option
|
||||
for option in group_options
|
||||
if (visibility_level in option.visibility and option in option_to_name)
|
||||
}
|
||||
for group, group_options in ordered_groups.items()
|
||||
}
|
||||
|
||||
|
||||
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None:
|
||||
|
||||
@@ -76,6 +76,9 @@ Currently, the following games are supported:
|
||||
* Kingdom Hearts 1
|
||||
* Mega Man 2
|
||||
* Yacht Dice
|
||||
* Faxanadu
|
||||
* Saving Princess
|
||||
* Castlevania: Circle of the Moon
|
||||
|
||||
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
|
||||
|
||||
35
Utils.py
35
Utils.py
@@ -18,8 +18,8 @@ import warnings
|
||||
|
||||
from argparse import Namespace
|
||||
from settings import Settings, get_settings
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
||||
from typing_extensions import TypeGuard
|
||||
from time import sleep
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
|
||||
try:
|
||||
@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.5.1"
|
||||
__version__ = "0.6.0"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -421,7 +421,8 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
# used by MultiServer -> savegame/multidata
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
|
||||
"SlotType", "NetworkSlot", "HintStatus"}:
|
||||
return getattr(self.net_utils_module, name)
|
||||
# Options and Plando are unpickled by WebHost -> Generate
|
||||
if module == "worlds.generic" and name == "PlandoItem":
|
||||
@@ -484,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",
|
||||
exception_logger: typing.Optional[str] = None):
|
||||
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
|
||||
write_mode: str = "w", log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
|
||||
add_timestamp: bool = False, exception_logger: typing.Optional[str] = None):
|
||||
import datetime
|
||||
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
|
||||
log_folder = user_path("logs")
|
||||
@@ -513,11 +514,15 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
return self.condition(record)
|
||||
|
||||
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
|
||||
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
|
||||
file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.getMessage()))
|
||||
root_logger.addHandler(file_handler)
|
||||
if sys.stdout:
|
||||
formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
||||
stream_handler = logging.StreamHandler(sys.stdout)
|
||||
stream_handler.addFilter(Filter("NoFile", lambda record: not getattr(record, "NoStream", False)))
|
||||
if add_timestamp:
|
||||
stream_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(stream_handler)
|
||||
|
||||
# Relay unhandled exceptions to logger.
|
||||
@@ -552,7 +557,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||
import platform
|
||||
logging.info(
|
||||
f"Archipelago ({__version__}) logging initialized"
|
||||
f" on {platform.platform()}"
|
||||
f" on {platform.platform()} process {os.getpid()}"
|
||||
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
f"{' (frozen)' if is_frozen() else ''}"
|
||||
)
|
||||
@@ -568,6 +573,8 @@ def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"):
|
||||
else:
|
||||
if text:
|
||||
queue.put_nowait(text)
|
||||
else:
|
||||
sleep(0.01) # non-blocking stream
|
||||
|
||||
from threading import Thread
|
||||
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
|
||||
@@ -852,11 +859,10 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
|
||||
task.add_done_callback(_faf_tasks.discard)
|
||||
|
||||
|
||||
def deprecate(message: str):
|
||||
def deprecate(message: str, add_stacklevels: int = 0):
|
||||
if __debug__:
|
||||
raise Exception(message)
|
||||
import warnings
|
||||
warnings.warn(message)
|
||||
warnings.warn(message, stacklevel=2 + add_stacklevels)
|
||||
|
||||
|
||||
class DeprecateDict(dict):
|
||||
@@ -870,10 +876,9 @@ class DeprecateDict(dict):
|
||||
|
||||
def __getitem__(self, item: Any) -> Any:
|
||||
if self.should_error:
|
||||
deprecate(self.log_message)
|
||||
deprecate(self.log_message, add_stacklevels=1)
|
||||
elif __debug__:
|
||||
import warnings
|
||||
warnings.warn(self.log_message)
|
||||
warnings.warn(self.log_message, stacklevel=2)
|
||||
return super().__getitem__(item)
|
||||
|
||||
|
||||
|
||||
@@ -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__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
||||
settings.no_gui = True
|
||||
configpath = os.path.abspath("config.yaml")
|
||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
@@ -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()
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument('--config_override', default=None,
|
||||
help="Path to yaml config file that overrules config.yaml.")
|
||||
args = parser.parse_known_args()[0]
|
||||
|
||||
@@ -39,6 +39,8 @@ app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
app.config["JOB_THRESHOLD"] = 1
|
||||
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
||||
app.config["JOB_TIME"] = 600
|
||||
# memory limit for generator processes in bytes
|
||||
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
||||
app.config['SESSION_PERMANENT'] = True
|
||||
|
||||
# waitress uses one thread for I/O, these are for processing of views that then get sent
|
||||
@@ -85,6 +87,6 @@ def register():
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
# to trigger app routing picking up on it
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options, session
|
||||
|
||||
app.register_blueprint(api.api_endpoints)
|
||||
|
||||
@@ -6,6 +6,7 @@ import multiprocessing
|
||||
import typing
|
||||
from datetime import timedelta, datetime
|
||||
from threading import Event, Thread
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
@@ -53,7 +54,21 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
generation.state = STATE_STARTED
|
||||
|
||||
|
||||
def init_db(pony_config: dict):
|
||||
def init_generator(config: dict[str, Any]) -> None:
|
||||
try:
|
||||
import resource
|
||||
except ModuleNotFoundError:
|
||||
pass # unix only module
|
||||
else:
|
||||
# set soft limit for memory to from config (default 4GiB)
|
||||
soft_limit = config["GENERATOR_MEMORY_LIMIT"]
|
||||
old_limit, hard_limit = resource.getrlimit(resource.RLIMIT_AS)
|
||||
if soft_limit != old_limit:
|
||||
resource.setrlimit(resource.RLIMIT_AS, (soft_limit, hard_limit))
|
||||
logging.debug(f"Changed AS mem limit {old_limit} -> {soft_limit}")
|
||||
del resource, soft_limit, hard_limit
|
||||
|
||||
pony_config = config["PONY"]
|
||||
db.bind(**pony_config)
|
||||
db.generate_mapping()
|
||||
|
||||
@@ -105,8 +120,8 @@ def autogen(config: dict):
|
||||
try:
|
||||
with Locker("autogen"):
|
||||
|
||||
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
|
||||
initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool:
|
||||
with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator,
|
||||
initargs=(config,), maxtasksperchild=10) as generator_pool:
|
||||
with db_session:
|
||||
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
|
||||
|
||||
|
||||
@@ -105,8 +105,9 @@ def roll_options(options: Dict[str, Union[dict, str]],
|
||||
plando_options=plando_options)
|
||||
else:
|
||||
for i, yaml_data in enumerate(yaml_datas):
|
||||
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
||||
plando_options=plando_options)
|
||||
if yaml_data is not None:
|
||||
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
||||
plando_options=plando_options)
|
||||
except Exception as e:
|
||||
if e.__cause__:
|
||||
results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}"
|
||||
|
||||
@@ -18,13 +18,6 @@ def get_world_theme(game_name: str):
|
||||
return 'grass'
|
||||
|
||||
|
||||
@app.before_request
|
||||
def register_session():
|
||||
session.permanent = True # technically 31 days after the last visit
|
||||
if not session.get("_id", None):
|
||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
|
||||
def page_not_found(err):
|
||||
|
||||
@@ -5,9 +5,7 @@ waitress>=3.0.0
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress>=1.15
|
||||
Flask-Limiter>=3.8.0
|
||||
bokeh>=3.1.1; python_version <= '3.8'
|
||||
bokeh>=3.4.3; python_version == '3.9'
|
||||
bokeh>=3.5.2; python_version >= '3.10'
|
||||
bokeh>=3.5.2
|
||||
markupsafe>=2.1.5
|
||||
Markdown>=3.7
|
||||
mdx-breakless-lists>=1.0.1
|
||||
|
||||
31
WebHostLib/session.py
Normal file
31
WebHostLib/session.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
from flask import session, render_template
|
||||
|
||||
from WebHostLib import app
|
||||
|
||||
|
||||
@app.before_request
|
||||
def register_session():
|
||||
session.permanent = True # technically 31 days after the last visit
|
||||
if not session.get("_id", None):
|
||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
||||
|
||||
|
||||
@app.route('/session')
|
||||
def show_session():
|
||||
return render_template(
|
||||
"session.html",
|
||||
)
|
||||
|
||||
|
||||
@app.route('/session/<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,
|
||||
)
|
||||
@@ -98,6 +98,8 @@
|
||||
<td>
|
||||
{% if hint.finding_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
|
||||
{% elif get_slot_info(team, hint.finding_player).type == 2 %}
|
||||
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
|
||||
{% else %}
|
||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
|
||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||
@@ -107,6 +109,8 @@
|
||||
<td>
|
||||
{% if hint.receiving_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
|
||||
{% elif get_slot_info(team, hint.receiving_player).type == 2 %}
|
||||
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
|
||||
{% else %}
|
||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">
|
||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||
|
||||
@@ -178,8 +178,15 @@
|
||||
})
|
||||
.then(text => new DOMParser().parseFromString(text, 'text/html'))
|
||||
.then(newDocument => {
|
||||
let el = newDocument.getElementById("host-room-info");
|
||||
document.getElementById("host-room-info").innerHTML = el.innerHTML;
|
||||
["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}`)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{%- endmacro %}
|
||||
{% macro list_patches_room(room) %}
|
||||
{% if room.seed.slots %}
|
||||
<table>
|
||||
<table id="slots-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
|
||||
@@ -21,8 +21,20 @@
|
||||
)
|
||||
-%}
|
||||
<tr>
|
||||
<td>{{ player_names_with_alias[(team, hint.finding_player)] }}</td>
|
||||
<td>{{ player_names_with_alias[(team, hint.receiving_player)] }}</td>
|
||||
<td>
|
||||
{% if get_slot_info(team, hint.finding_player).type == 2 %}
|
||||
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
|
||||
{% else %}
|
||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if get_slot_info(team, hint.receiving_player).type == 2 %}
|
||||
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
|
||||
{% else %}
|
||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
||||
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
|
||||
<td>{{ games[(team, hint.finding_player)] }}</td>
|
||||
|
||||
30
WebHostLib/templates/session.html
Normal file
30
WebHostLib/templates/session.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% 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 %}
|
||||
@@ -26,6 +26,7 @@
|
||||
<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>
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
{% 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 %}
|
||||
|
||||
@@ -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.range_start < option.default < option.range_end %}
|
||||
{% if option.default is number and option.range_start < option.default < option.range_end %}
|
||||
{{ RangeRow(option_name, option, option.default, option.default, True) }}
|
||||
{% endif %}
|
||||
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}
|
||||
|
||||
@@ -423,6 +423,7 @@ def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) ->
|
||||
template_name_or_list="genericTracker.html",
|
||||
game_specific_tracker=game in _player_trackers,
|
||||
room=tracker_data.room,
|
||||
get_slot_info=tracker_data.get_slot_info,
|
||||
team=team,
|
||||
player=player,
|
||||
player_name=tracker_data.get_room_long_player_names()[team, player],
|
||||
@@ -446,6 +447,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="Generic",
|
||||
room=tracker_data.room,
|
||||
get_slot_info=tracker_data.get_slot_info,
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_players(),
|
||||
locations=tracker_data.get_room_locations(),
|
||||
@@ -497,7 +499,7 @@ if "Factorio" in network_data_package["games"]:
|
||||
(team, player): collections.Counter({
|
||||
tracker_data.item_id_to_name["Factorio"][item_id]: count
|
||||
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
|
||||
}) for team, players in tracker_data.get_all_slots().items() for player in players
|
||||
}) for team, players in tracker_data.get_all_players().items() for player in players
|
||||
if tracker_data.get_player_game(team, player) == "Factorio"
|
||||
}
|
||||
|
||||
@@ -506,6 +508,7 @@ if "Factorio" in network_data_package["games"]:
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="Factorio",
|
||||
room=tracker_data.room,
|
||||
get_slot_info=tracker_data.get_slot_info,
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_players(),
|
||||
locations=tracker_data.get_room_locations(),
|
||||
@@ -638,6 +641,7 @@ if "A Link to the Past" in network_data_package["games"]:
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="A Link to the Past",
|
||||
room=tracker_data.room,
|
||||
get_slot_info=tracker_data.get_slot_info,
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_players(),
|
||||
locations=tracker_data.get_room_locations(),
|
||||
|
||||
@@ -69,6 +69,14 @@ 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"""
|
||||
@@ -137,10 +145,16 @@ cdef class LocationStore:
|
||||
warnings.warn("Game has no locations")
|
||||
|
||||
# allocate the arrays and invalidate index (0xff...)
|
||||
self.entries = <LocationEntry*>self._mem.alloc(count, sizeof(LocationEntry))
|
||||
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.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()):
|
||||
@@ -190,8 +204,6 @@ 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:
|
||||
@@ -246,12 +258,11 @@ cdef class LocationStore:
|
||||
all_locations[sender].add(entry.location)
|
||||
return all_locations
|
||||
|
||||
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]:
|
||||
cdef ap_player_t sender = slot
|
||||
if sender < 0 or sender >= self.sender_index_size:
|
||||
raise KeyError(slot)
|
||||
|
||||
# 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]
|
||||
@@ -263,7 +274,6 @@ 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
|
||||
@@ -273,9 +283,11 @@ 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.
|
||||
@@ -290,9 +302,11 @@ 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])
|
||||
@@ -328,7 +342,8 @@ cdef class PlayerLocationProxy:
|
||||
cdef LocationEntry* entry = NULL
|
||||
# binary search
|
||||
cdef size_t l = self._store.sender_index[self._player].start
|
||||
cdef size_t r = l + self._store.sender_index[self._player].count
|
||||
cdef size_t e = l + self._store.sender_index[self._player].count
|
||||
cdef size_t r = e
|
||||
cdef size_t m
|
||||
while l < r:
|
||||
m = (l + r) // 2
|
||||
@@ -337,7 +352,7 @@ cdef class PlayerLocationProxy:
|
||||
l = m + 1
|
||||
else:
|
||||
r = m
|
||||
if entry: # count != 0
|
||||
if l < e:
|
||||
entry = self._store.entries + l
|
||||
if entry.location == loc:
|
||||
return entry
|
||||
@@ -349,8 +364,6 @@ 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:
|
||||
|
||||
@@ -3,8 +3,16 @@ 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")
|
||||
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
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
finding_text: "Finding Player"
|
||||
location_text: "Location"
|
||||
entrance_text: "Entrance"
|
||||
found_text: "Found?"
|
||||
status_text: "Status"
|
||||
TooltipLabel:
|
||||
id: receiving
|
||||
sort_key: 'receiving'
|
||||
@@ -96,9 +96,9 @@
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: found
|
||||
sort_key: 'found'
|
||||
text: root.found_text
|
||||
id: status
|
||||
sort_key: 'status'
|
||||
text: root.status_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
# Castlevania 64
|
||||
/worlds/cv64/ @LiquidCat64
|
||||
|
||||
# Castlevania: Circle of the Moon
|
||||
/worlds/cvcotm/ @LiquidCat64
|
||||
|
||||
# Celeste 64
|
||||
/worlds/celeste64/ @PoryGone
|
||||
|
||||
@@ -55,19 +58,22 @@
|
||||
/worlds/dlcquest/ @axe-y @agilbert1412
|
||||
|
||||
# DOOM 1993
|
||||
/worlds/doom_1993/ @Daivuk
|
||||
/worlds/doom_1993/ @Daivuk @KScl
|
||||
|
||||
# DOOM II
|
||||
/worlds/doom_ii/ @Daivuk
|
||||
/worlds/doom_ii/ @Daivuk @KScl
|
||||
|
||||
# Factorio
|
||||
/worlds/factorio/ @Berserker66
|
||||
|
||||
# Faxanadu
|
||||
/worlds/faxanadu/ @Daivuk
|
||||
|
||||
# Final Fantasy Mystic Quest
|
||||
/worlds/ffmq/ @Alchav @wildham0
|
||||
|
||||
# Heretic
|
||||
/worlds/heretic/ @Daivuk
|
||||
/worlds/heretic/ @Daivuk @KScl
|
||||
|
||||
# Hollow Knight
|
||||
/worlds/hk/ @BadMagic100 @qwint
|
||||
@@ -139,6 +145,9 @@
|
||||
# Risk of Rain 2
|
||||
/worlds/ror2/ @kindasneaki
|
||||
|
||||
# Saving Princess
|
||||
/worlds/saving_princess/ @LeonarthCG
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire
|
||||
|
||||
|
||||
@@ -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.8](https://www.python.org/downloads/release/python-380/).
|
||||
is [Python 3.10](https://www.python.org/downloads/release/python-31015/).
|
||||
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:
|
||||
|
||||
@@ -272,6 +272,7 @@ 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)
|
||||
@@ -342,6 +343,33 @@ 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)
|
||||
|
||||
@@ -644,6 +672,7 @@ class Hint(typing.NamedTuple):
|
||||
found: bool
|
||||
entrance: str = ""
|
||||
item_flags: int = 0
|
||||
status: HintStatus = HintStatus.HINT_UNSPECIFIED
|
||||
```
|
||||
|
||||
### Data Package Contents
|
||||
|
||||
@@ -7,7 +7,9 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
## General
|
||||
|
||||
What you'll need:
|
||||
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* [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.12.x is currently the newest supported version
|
||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||
* Matching C compiler
|
||||
|
||||
@@ -27,8 +27,14 @@
|
||||
# If you wish to deploy, uncomment the following line and set it to something not easily guessable.
|
||||
# SECRET_KEY: "Your secret key here"
|
||||
|
||||
# TODO
|
||||
#JOB_THRESHOLD: 2
|
||||
# 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
|
||||
|
||||
# waitress uses one thread for I/O, these are for processing of view that get sent
|
||||
#WAITRESS_THREADS: 10
|
||||
|
||||
@@ -288,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, "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").
|
||||
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").
|
||||
|
||||
### Entrances
|
||||
|
||||
@@ -328,6 +328,9 @@ 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
|
||||
@@ -463,7 +466,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 called "Menu"
|
||||
* additions to the regions list: at least one named after the world class's origin_region_name ("Menu" by default)
|
||||
* 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
|
||||
@@ -516,7 +519,7 @@ def generate_early(self) -> None:
|
||||
|
||||
```python
|
||||
def create_regions(self) -> None:
|
||||
# Add regions to the multiworld. "Menu" is the required starting point.
|
||||
# Add regions to the multiworld. One of them must use the origin_region_name as its name ("Menu" by default).
|
||||
# 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...]
|
||||
|
||||
@@ -186,6 +186,11 @@ 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: "";
|
||||
|
||||
139
kvui.py
139
kvui.py
@@ -3,6 +3,8 @@ 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"
|
||||
@@ -12,10 +14,7 @@ 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
|
||||
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.
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(0)
|
||||
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
@@ -37,6 +36,7 @@ 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
|
||||
@@ -55,6 +55,7 @@ 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
|
||||
@@ -63,10 +64,11 @@ 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.image import AsyncImage
|
||||
|
||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||
|
||||
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType
|
||||
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType, HintStatus
|
||||
from Utils import async_start, get_input_text_from_response
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -303,11 +305,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
""" Respond to the selection of items in the view. """
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
selected = BooleanProperty(False)
|
||||
striped = BooleanProperty(False)
|
||||
index = None
|
||||
dropdown: DropDown
|
||||
|
||||
def __init__(self):
|
||||
super(HintLabel, self).__init__()
|
||||
@@ -316,10 +318,32 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.finding_text = ""
|
||||
self.location_text = ""
|
||||
self.entrance_text = ""
|
||||
self.found_text = ""
|
||||
self.status_text = ""
|
||||
self.hint = {}
|
||||
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])
|
||||
|
||||
@@ -331,7 +355,8 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.finding_text = data["finding"]["text"]
|
||||
self.location_text = data["location"]["text"]
|
||||
self.entrance_text = data["entrance"]["text"]
|
||||
self.found_text = data["found"]["text"]
|
||||
self.status_text = data["status"]["text"]
|
||||
self.hint = data["status"]["hint"]
|
||||
self.height = self.minimum_height
|
||||
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
||||
|
||||
@@ -341,13 +366,21 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
return True
|
||||
if self.index: # skip header
|
||||
if self.collide_point(*touch.pos):
|
||||
if self.selected:
|
||||
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:
|
||||
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.found_text.lower(), ")"))
|
||||
else "", ". (", self.status_text.lower(), ")"))
|
||||
temp = MarkupLabel(text).markup
|
||||
text = "".join(
|
||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
@@ -361,18 +394,16 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
for child in self.children:
|
||||
if child.collide_point(*touch.pos):
|
||||
key = child.sort_key
|
||||
parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
|
||||
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()
|
||||
if key == parent.sort_key:
|
||||
# second click reverses order
|
||||
parent.reversed = not parent.reversed
|
||||
else:
|
||||
parent.sort_key = key
|
||||
parent.reversed = False
|
||||
break
|
||||
else:
|
||||
logging.warning("Did not find clicked header for sorting.")
|
||||
|
||||
App.get_running_app().update_hints()
|
||||
App.get_running_app().update_hints()
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
""" Respond to the selection of items in the view. """
|
||||
@@ -666,7 +697,7 @@ 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[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"]
|
||||
hints = self.ctx.stored_data.get(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
|
||||
@@ -722,6 +753,22 @@ class UILog(RecycleView):
|
||||
element.height = element.texture_size[1]
|
||||
|
||||
|
||||
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]"},
|
||||
@@ -729,12 +776,13 @@ class HintLog(RecycleView):
|
||||
"finding": {"text": "[u]Finding Player[/u]"},
|
||||
"location": {"text": "[u]Location[/u]"},
|
||||
"entrance": {"text": "[u]Entrance[/u]"},
|
||||
"found": {"text": "[u]Status[/u]"},
|
||||
"status": {"text": "[u]Status[/u]",
|
||||
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
||||
"striped": True,
|
||||
}
|
||||
|
||||
sort_key: str = ""
|
||||
reversed: bool = False
|
||||
reversed: bool = True
|
||||
|
||||
def __init__(self, parser):
|
||||
super(HintLog, self).__init__()
|
||||
@@ -742,8 +790,18 @@ 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({
|
||||
@@ -761,9 +819,10 @@ 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"})},
|
||||
"found": {
|
||||
"text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
|
||||
"text": "Found" if hint["found"] else "Not Found"})},
|
||||
"status": {
|
||||
"text": hint_status_node,
|
||||
"hint": hint,
|
||||
},
|
||||
})
|
||||
|
||||
data.sort(key=self.hint_sorter, reverse=self.reversed)
|
||||
@@ -774,7 +833,7 @@ class HintLog(RecycleView):
|
||||
|
||||
@staticmethod
|
||||
def hint_sorter(element: dict) -> str:
|
||||
return ""
|
||||
return element["status"]["hint"]["status"] # By status by default
|
||||
|
||||
def fix_heights(self):
|
||||
"""Workaround fix for divergent texture and layout heights"""
|
||||
@@ -783,6 +842,40 @@ 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")
|
||||
|
||||
|
||||
11
settings.py
11
settings.py
@@ -7,6 +7,7 @@ import os
|
||||
import os.path
|
||||
import shutil
|
||||
import sys
|
||||
import types
|
||||
import typing
|
||||
import warnings
|
||||
from enum import IntEnum
|
||||
@@ -162,8 +163,13 @@ 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) is Union else [annotation]
|
||||
candidates = (
|
||||
[] if annotation is None else (
|
||||
typing.get_args(annotation)
|
||||
if typing.get_origin(annotation) in (Union, types.UnionType)
|
||||
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"
|
||||
@@ -593,6 +599,7 @@ 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)
|
||||
|
||||
4
setup.py
4
setup.py
@@ -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.utcnow()
|
||||
self.buildtime = datetime.datetime.now(datetime.timezone.utc)
|
||||
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", "orjson"], # TODO: remove orjson here once we drop py3.8 support
|
||||
"zip_exclude_packages": ["worlds", "sc2"],
|
||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||
"include_msvcr": False,
|
||||
"replace_paths": ["*."],
|
||||
|
||||
@@ -80,3 +80,21 @@ 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}")
|
||||
|
||||
@@ -78,4 +78,4 @@ class TestOptions(unittest.TestCase):
|
||||
if not world_type.hidden:
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
with self.subTest(game=gamename, option=option_key):
|
||||
pickle.dumps(option(option.default))
|
||||
pickle.dumps(option.from_any(option.default))
|
||||
|
||||
16
test/general/test_settings.py
Normal file
16
test/general/test_settings.py
Normal file
@@ -0,0 +1,16 @@
|
||||
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
|
||||
@@ -115,6 +115,7 @@ 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])
|
||||
@@ -122,18 +123,48 @@ 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])
|
||||
@@ -181,6 +212,16 @@ 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({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import PlandoOptions
|
||||
from worlds import AutoWorldRegister
|
||||
from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet
|
||||
|
||||
@@ -14,6 +15,10 @@ class TestOptionPresets(unittest.TestCase):
|
||||
with self.subTest(game=game_name, preset=preset_name, option=option_name):
|
||||
try:
|
||||
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
# some options may need verification to ensure the provided option is actually valid
|
||||
# pass in all plando options in case a preset wants to require certain plando options
|
||||
# for some reason
|
||||
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
|
||||
supported_types = [NumericOption, OptionSet, OptionList, ItemDict]
|
||||
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
||||
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union
|
||||
|
||||
from typing_extensions import TypeGuard
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
||||
|
||||
|
||||
@@ -33,7 +33,10 @@ class AutoWorldRegister(type):
|
||||
# lazy loading + caching to minimize runtime cost
|
||||
if cls.__settings is None:
|
||||
from settings import get_settings
|
||||
cls.__settings = get_settings()[cls.settings_key]
|
||||
try:
|
||||
cls.__settings = get_settings()[cls.settings_key]
|
||||
except AttributeError:
|
||||
return None
|
||||
return cls.__settings
|
||||
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
||||
|
||||
@@ -103,7 +103,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
|
||||
try:
|
||||
import zipfile
|
||||
zip = zipfile.ZipFile(apworld_path)
|
||||
directories = [f.filename.strip('/') for f in zip.filelist if f.CRC == 0 and f.file_size == 0 and f.filename.count('/') == 1]
|
||||
directories = [f.name for f in zipfile.Path(zip).iterdir() if f.is_dir()]
|
||||
if len(directories) == 1 and directories[0] in apworld_path.stem:
|
||||
module_name = directories[0]
|
||||
apworld_name = module_name + ".apworld"
|
||||
@@ -207,6 +207,7 @@ 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'),
|
||||
|
||||
@@ -66,19 +66,12 @@ class WorldSource:
|
||||
start = time.perf_counter()
|
||||
if self.is_zip:
|
||||
importer = zipimport.zipimporter(self.resolved_path)
|
||||
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])
|
||||
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 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():
|
||||
|
||||
@@ -47,8 +47,6 @@ 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
|
||||
|
||||
@@ -76,10 +76,9 @@ 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(multiworld, dragon_slay_check)
|
||||
priority_locations = determine_priority_locations()
|
||||
|
||||
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:
|
||||
@@ -154,6 +153,7 @@ 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(world: MultiWorld, dragon_slay_check: bool) -> {}:
|
||||
# def determine_priority_locations(multiworld: MultiWorld, dragon_slay_check: bool) -> {}:
|
||||
def determine_priority_locations() -> {}:
|
||||
priority_locations = {}
|
||||
return priority_locations
|
||||
|
||||
@@ -86,9 +86,7 @@ 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"]]
|
||||
|
||||
|
||||
@@ -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.get(name)
|
||||
item_data: ItemData = item_table[name]
|
||||
return AdventureItem(name, item_data.classification, item_data.id, self.player)
|
||||
|
||||
def create_event(self, name: str, classification: ItemClassification) -> Item:
|
||||
|
||||
@@ -740,17 +740,20 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
|
||||
|
||||
|
||||
def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region):
|
||||
i = 1
|
||||
while i <= len(rift_access_regions[time_rift.name]):
|
||||
for i, access_region in enumerate(rift_access_regions[time_rift.name], start=1):
|
||||
# Matches the naming convention and iteration order in `create_rift_connections()`.
|
||||
name = f"{time_rift.name} Portal - Entrance {i}"
|
||||
entrance: Entrance
|
||||
try:
|
||||
entrance = world.multiworld.get_entrance(name, world.player)
|
||||
entrance = world.get_entrance(name)
|
||||
# Reconnect the rift access region to the new exit region.
|
||||
reconnect_regions(entrance, entrance.parent_region, exit_region)
|
||||
except KeyError:
|
||||
time_rift.connect(exit_region, name)
|
||||
|
||||
i += 1
|
||||
# The original entrance to the time rift has been deleted by already reconnecting a telescope act to the
|
||||
# time rift, so create a new entrance from the original rift access region to the new exit region.
|
||||
# Normally, acts and time rifts are sorted such that time rifts are reconnected to acts/rifts first, but
|
||||
# starting acts/rifts and act-plando can reconnect acts to time rifts before this happens.
|
||||
world.get_region(access_region).connect(exit_region, name)
|
||||
|
||||
|
||||
def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, Option, \
|
||||
from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, PerGameCommonOptions, \
|
||||
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
|
||||
|
||||
|
||||
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,
|
||||
@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
|
||||
|
||||
# removed:
|
||||
"goals": Removed,
|
||||
"smallkey_shuffle": Removed,
|
||||
"bigkey_shuffle": Removed,
|
||||
}
|
||||
goals: Removed
|
||||
smallkey_shuffle: Removed
|
||||
bigkey_shuffle: Removed
|
||||
|
||||
@@ -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.per_slot_randoms[player]
|
||||
local_random = random if not world else world.worlds[player].random
|
||||
disable_music: bool = not music
|
||||
# enable instant item menu
|
||||
if menuspeed == 'instant':
|
||||
@@ -2197,8 +2197,9 @@ 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()
|
||||
@@ -2425,7 +2426,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)
|
||||
world.per_slot_randoms[player].shuffle(prog_bow_locs)
|
||||
local_random.shuffle(prog_bow_locs)
|
||||
found_bow = False
|
||||
found_bow_alt = False
|
||||
while prog_bow_locs and not (found_bow and found_bow_alt):
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import settings
|
||||
import threading
|
||||
import typing
|
||||
|
||||
import Utils
|
||||
import settings
|
||||
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 alttp_options, small_key_shuffle
|
||||
from .Options import ALTTPOptions, 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 .SubClasses import ALttPItem, LTTPRegionType
|
||||
from worlds.AutoWorld import World, WebWorld, LogicMixin
|
||||
from .StateHelpers import can_buy_unlimited
|
||||
from .SubClasses import ALttPItem, LTTPRegionType
|
||||
|
||||
lttp_logger = logging.getLogger("A Link to the Past")
|
||||
|
||||
@@ -132,7 +131,8 @@ class ALTTPWorld(World):
|
||||
Ganon!
|
||||
"""
|
||||
game = "A Link to the Past"
|
||||
option_definitions = alttp_options
|
||||
options_dataclass = ALTTPOptions
|
||||
options: ALTTPOptions
|
||||
settings_key = "lttp_options"
|
||||
settings: typing.ClassVar[ALTTPSettings]
|
||||
topology_present = True
|
||||
@@ -286,13 +286,22 @@ class ALTTPWorld(World):
|
||||
if not os.path.exists(rom_file):
|
||||
raise FileNotFoundError(rom_file)
|
||||
if multiworld.is_race:
|
||||
import xxtea
|
||||
import xxtea # noqa
|
||||
for player in multiworld.get_game_players(cls.game):
|
||||
if multiworld.worlds[player].use_enemizer:
|
||||
check_enemizer(multiworld.worlds[player].enemizer_path)
|
||||
break
|
||||
|
||||
def generate_early(self):
|
||||
# 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
|
||||
@@ -536,12 +545,10 @@ class ALTTPWorld(World):
|
||||
|
||||
@property
|
||||
def use_enemizer(self) -> bool:
|
||||
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])
|
||||
return bool(self.options.boss_shuffle or self.options.enemy_shuffle
|
||||
or self.options.enemy_health != 'default' or self.options.enemy_damage != 'default'
|
||||
or self.options.pot_shuffle or self.options.bush_shuffle
|
||||
or self.options.killable_thieves)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
multiworld = self.multiworld
|
||||
|
||||
32
worlds/alttp/docs/fr_A Link to the Past.md
Normal file
32
worlds/alttp/docs/fr_A Link to the Past.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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 !
|
||||
|
||||
@@ -1,41 +1,28 @@
|
||||
# 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)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
|
||||
- [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.
|
||||
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
|
||||
- Un émulateur capable d'éxécuter des scripts Lua
|
||||
([snes9x rr](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`
|
||||
- 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`
|
||||
|
||||
## Procédure d'installation
|
||||
|
||||
### Installation sur Windows
|
||||
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.
|
||||
|
||||
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
|
||||
3. 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...**
|
||||
@@ -44,58 +31,6 @@
|
||||
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
|
||||
@@ -109,35 +44,58 @@ 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, QUsb2Snes devrait se lancer automatiquement également en arrière-plan. Si
|
||||
Quand le client se lance automatiquement, SNI 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. 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.
|
||||
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.
|
||||
|
||||
##### BizHawk
|
||||
|
||||
1. Assurez vous d'avoir le coeur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant
|
||||
1. Assurez vous d'avoir le cœur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant
|
||||
ces options de menu :
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Une fois le coeur changé, vous devez redémarrer BizHawk.
|
||||
- (≤ 2.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).
|
||||
2. Chargez votre ROM si ce n'est pas déjà fait.
|
||||
3. Cliquez sur le menu "Tools" et cliquez sur **Lua Console**
|
||||
4. Cliquez sur le bouton pour ouvrir un nouveau script Lua.
|
||||
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.
|
||||
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.
|
||||
|
||||

|
||||
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.
|
||||
|
||||
#### Avec une solution matérielle
|
||||
|
||||
@@ -147,10 +105,7 @@ 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. 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.
|
||||
2. Lancez votre console et chargez la ROM.
|
||||
|
||||
### Se connecter au MultiServer
|
||||
|
||||
@@ -165,47 +120,6 @@ l'interface Web.
|
||||
|
||||
### Jouer au jeu
|
||||
|
||||
Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations
|
||||
pour avoir rejoint un multiworld !
|
||||
|
||||
## Héberger un MultiWorld
|
||||
|
||||
La méthode recommandée pour héberger une partie est d'utiliser le service d'hébergement fourni par
|
||||
[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
|
||||
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.
|
||||
|
||||
BIN
worlds/alttp/docs/retroarch-network-commands-fr.png
Normal file
BIN
worlds/alttp/docs/retroarch-network-commands-fr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -59,156 +59,316 @@ class ItemData:
|
||||
type: ItemType
|
||||
group: ItemGroup
|
||||
|
||||
def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup):
|
||||
def __init__(self, aId: int, count: int, aType: ItemType, group: ItemGroup):
|
||||
"""
|
||||
Initialisation of the item data
|
||||
@param id: The item ID
|
||||
@param aId: The item ID
|
||||
@param count: the number of items in the pool
|
||||
@param type: the importance type of the item
|
||||
@param aType: the importance type of the item
|
||||
@param group: the usage of the item in the game
|
||||
"""
|
||||
self.id = id
|
||||
self.id = aId
|
||||
self.count = count
|
||||
self.type = type
|
||||
self.type = aType
|
||||
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
|
||||
"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
|
||||
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
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,10 @@ 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
|
||||
|
||||
@@ -29,14 +32,43 @@ 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 EarlyEnergyForm(DefaultOnToggle):
|
||||
""" Force the Energy Form to be in a location early in the game """
|
||||
display_name = "Early Energy Form"
|
||||
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 AquarianTranslation(Toggle):
|
||||
@@ -47,7 +79,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", "Sun God" and "The Golem".
|
||||
"Fallen God", "Mithalan God", "Drunian God", "Lumerean God" and "The Golem".
|
||||
"""
|
||||
display_name = "Big bosses to beat"
|
||||
range_start = 0
|
||||
@@ -104,7 +136,7 @@ class LightNeededToGetToDarkPlaces(DefaultOnToggle):
|
||||
display_name = "Light needed to get to dark places"
|
||||
|
||||
|
||||
class BindSongNeededToGetUnderRockBulb(Toggle):
|
||||
class BindSongNeededToGetUnderRockBulb(DefaultOnToggle):
|
||||
"""
|
||||
Make sure that the bind song can be acquired before having to obtain sing bulbs under rocks.
|
||||
"""
|
||||
@@ -121,13 +153,18 @@ class BlindGoal(Toggle):
|
||||
|
||||
class UnconfineHomeWater(Choice):
|
||||
"""
|
||||
Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song.
|
||||
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.
|
||||
"""
|
||||
display_name = "Unconfine Home Water Area"
|
||||
display_name = "Unconfine Home Waters 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
|
||||
|
||||
|
||||
@@ -142,6 +179,7 @@ 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
@@ -7,9 +7,10 @@ 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
|
||||
from .Locations import location_table
|
||||
from .Options import AquariaOptions
|
||||
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 .Regions import AquariaRegions
|
||||
|
||||
|
||||
@@ -65,15 +66,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": {"Energy form", "Nature form", "Beast form",
|
||||
"Li and Li song", "Baby Nautilus", "Baby Piranha",
|
||||
"Baby Blaster"},
|
||||
"Light": {"Sun form", "Baby Dumbo"}
|
||||
"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}
|
||||
}
|
||||
"""Grouping item make it easier to find them"""
|
||||
|
||||
@@ -117,16 +118,13 @@ class AquariaWorld(World):
|
||||
Create an AquariaItem using 'name' as item name.
|
||||
"""
|
||||
result: AquariaItem
|
||||
try:
|
||||
data = item_table[name]
|
||||
classification: ItemClassification = ItemClassification.useful
|
||||
if data.type == ItemType.JUNK:
|
||||
classification = ItemClassification.filler
|
||||
elif data.type == ItemType.PROGRESSION:
|
||||
classification = ItemClassification.progression
|
||||
result = AquariaItem(name, classification, data.id, self.player)
|
||||
except BaseException:
|
||||
raise Exception('The item ' + name + ' is not valid.')
|
||||
data = item_table[name]
|
||||
classification: ItemClassification = ItemClassification.useful
|
||||
if data.type == ItemType.JUNK:
|
||||
classification = ItemClassification.filler
|
||||
elif data.type == ItemType.PROGRESSION:
|
||||
classification = ItemClassification.progression
|
||||
result = AquariaItem(name, classification, data.id, self.player)
|
||||
|
||||
return result
|
||||
|
||||
@@ -151,23 +149,32 @@ 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 > 0:
|
||||
if self.options.turtle_randomizer.value == 2:
|
||||
self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected)
|
||||
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)
|
||||
else:
|
||||
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",
|
||||
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,
|
||||
precollected)
|
||||
self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle",
|
||||
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,
|
||||
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("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected,
|
||||
ItemClassification.progression)
|
||||
self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected)
|
||||
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)
|
||||
for name, data in item_table.items():
|
||||
if name not in self.exclude:
|
||||
for i in range(data.count):
|
||||
@@ -178,10 +185,17 @@ 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("Victory", self.player)
|
||||
state: state.has(ItemNames.VICTORY, self.player)
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
"""
|
||||
@@ -189,13 +203,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 > 0:
|
||||
if self.options.ingredient_randomizer.value == 1:
|
||||
if self.options.ingredient_randomizer.value > IngredientRandomizer.option_off:
|
||||
if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients:
|
||||
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 == 1:
|
||||
if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients:
|
||||
simple_ingredients_substitution.extend([24, 25, 26])
|
||||
dishes_substitution = [i for i in range(27, 76)]
|
||||
if self.options.dish_randomizer:
|
||||
@@ -208,14 +222,19 @@ 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 > 0,
|
||||
"secret_needed":
|
||||
self.options.objective.value == Objective.option_obtain_secrets_and_kill_the_creator,
|
||||
"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 in [1, 3],
|
||||
"unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3],
|
||||
"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,
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ The locations in the randomizer are:
|
||||
* Beating Mithalan God boss
|
||||
* Fish Cave puzzle
|
||||
* Beating Drunian God boss
|
||||
* Beating Sun God boss
|
||||
* Beating Lumerean God boss
|
||||
* Breaking Li cage in the body
|
||||
|
||||
Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates,
|
||||
|
||||
@@ -6,211 +6,212 @@ 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 = [
|
||||
"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",
|
||||
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,
|
||||
]
|
||||
|
||||
class AquariaTestBase(WorldTestBase):
|
||||
|
||||
@@ -5,6 +5,8 @@ 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):
|
||||
@@ -13,16 +15,16 @@ class BeastFormAccessTest(AquariaTestBase):
|
||||
def test_beast_form_location(self) -> None:
|
||||
"""Test locations that require beast form"""
|
||||
locations = [
|
||||
"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",
|
||||
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,
|
||||
]
|
||||
items = [["Beast form"]]
|
||||
items = [[ItemNames.BEAST_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,6 +5,8 @@ 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):
|
||||
@@ -13,27 +15,27 @@ class BeastForArnassiArmormAccessTest(AquariaTestBase):
|
||||
def test_beast_form_arnassi_armor_location(self) -> None:
|
||||
"""Test locations that require beast form or arnassi armor"""
|
||||
locations = [
|
||||
"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"
|
||||
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
|
||||
]
|
||||
items = [["Beast form", "Arnassi Armor"]]
|
||||
items = [[ItemNames.BEAST_FORM, ItemNames.ARNASSI_ARMOR]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -6,31 +6,36 @@ 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 = [
|
||||
"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",
|
||||
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,
|
||||
*after_home_water_locations
|
||||
]
|
||||
items = [["Bind song"]]
|
||||
items = [[ItemNames.BIND_SONG]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -7,6 +7,8 @@ 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):
|
||||
@@ -18,25 +20,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 = [
|
||||
"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",
|
||||
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,
|
||||
*after_home_water_locations
|
||||
]
|
||||
items = [["Bind song"]]
|
||||
items = [[ItemNames.BIND_SONG]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,16 +5,17 @@ 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": 0,
|
||||
"early_energy_form": False
|
||||
"unconfine_home_water": UnconfineHomeWater.option_off,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_confine_home_water_location(self) -> None:
|
||||
"""Test region accessible with confined home water"""
|
||||
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")
|
||||
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")
|
||||
|
||||
@@ -5,22 +5,25 @@ 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": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
}
|
||||
|
||||
def test_li_song_location(self) -> None:
|
||||
"""Test locations that require the dual song"""
|
||||
locations = [
|
||||
"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"
|
||||
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
|
||||
]
|
||||
items = [["Dual form"]]
|
||||
items = [[ItemNames.DUAL_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -6,28 +6,31 @@ 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": False,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_energy_form_location(self) -> None:
|
||||
"""Test locations that require Energy form"""
|
||||
locations = [
|
||||
"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",
|
||||
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,
|
||||
]
|
||||
items = [["Energy form"]]
|
||||
items = [[ItemNames.ENERGY_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,88 +5,74 @@ 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": False,
|
||||
"early_energy_form": EarlyEnergyForm.option_off,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all
|
||||
}
|
||||
|
||||
def test_energy_form_or_dual_form_location(self) -> None:
|
||||
"""Test locations that require Energy form or dual form"""
|
||||
locations = [
|
||||
"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"
|
||||
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
|
||||
]
|
||||
items = [["Energy form", "Dual form", "Li and Li song", "Body tongue cleared"]]
|
||||
items = [[ItemNames.ENERGY_FORM, ItemNames.DUAL_FORM, ItemNames.LI_AND_LI_SONG, ItemNames.BODY_TONGUE_CLEARED]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,33 +5,36 @@ 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": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
}
|
||||
|
||||
def test_fish_form_location(self) -> None:
|
||||
"""Test locations that require fish form"""
|
||||
locations = [
|
||||
"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"
|
||||
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
|
||||
]
|
||||
items = [["Fish form"]]
|
||||
items = [[ItemNames.FISH_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,41 +5,44 @@ 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": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
}
|
||||
|
||||
def test_li_song_location(self) -> None:
|
||||
"""Test locations that require Li"""
|
||||
locations = [
|
||||
"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"
|
||||
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
|
||||
]
|
||||
items = [["Li and Li song", "Body tongue cleared"]]
|
||||
items = [[ItemNames.LI_AND_LI_SONG, ItemNames.BODY_TONGUE_CLEARED]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,12 +5,15 @@ 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": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
"light_needed_to_get_to_dark_places": True,
|
||||
}
|
||||
|
||||
@@ -19,52 +22,52 @@ class LightAccessTest(AquariaTestBase):
|
||||
locations = [
|
||||
# Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be
|
||||
# tested.
|
||||
# "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",
|
||||
# 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,
|
||||
]
|
||||
items = [["Sun form", "Baby Dumbo", "Has sun crystal"]]
|
||||
items = [[ItemNames.SUN_FORM, ItemNames.BABY_DUMBO, ItemNames.HAS_SUN_CRYSTAL]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,53 +5,56 @@ 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": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
}
|
||||
|
||||
def test_nature_form_location(self) -> None:
|
||||
"""Test locations that require nature form"""
|
||||
locations = [
|
||||
"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"
|
||||
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
|
||||
]
|
||||
items = [["Nature form"]]
|
||||
items = [[ItemNames.NATURE_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -6,6 +6,7 @@ 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):
|
||||
@@ -15,31 +16,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
"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",
|
||||
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,
|
||||
]
|
||||
|
||||
def test_unconfine_home_water_both_location_fillable(self) -> None:
|
||||
@@ -49,7 +50,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
if item.classification == ItemClassification.progression:
|
||||
if item.advancement:
|
||||
self.assertFalse(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
||||
|
||||
@@ -5,6 +5,7 @@ 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):
|
||||
@@ -14,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
"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",
|
||||
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,
|
||||
]
|
||||
|
||||
def test_unconfine_home_water_both_location_fillable(self) -> None:
|
||||
|
||||
@@ -5,6 +5,8 @@ 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):
|
||||
@@ -13,23 +15,23 @@ class SpiritFormAccessTest(AquariaTestBase):
|
||||
def test_spirit_form_location(self) -> None:
|
||||
"""Test locations that require spirit form"""
|
||||
locations = [
|
||||
"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",
|
||||
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,
|
||||
]
|
||||
items = [["Spirit form"]]
|
||||
items = [[ItemNames.SPIRIT_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,6 +5,8 @@ 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):
|
||||
@@ -13,16 +15,16 @@ class SunFormAccessTest(AquariaTestBase):
|
||||
def test_sun_form_location(self) -> None:
|
||||
"""Test locations that require sun form"""
|
||||
locations = [
|
||||
"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"
|
||||
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
|
||||
]
|
||||
items = [["Sun form"]]
|
||||
items = [[ItemNames.SUN_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -6,16 +6,17 @@ Description: Unit test used to test accessibility of region with the unconfined
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Options import UnconfineHomeWater, EarlyEnergyForm
|
||||
|
||||
|
||||
class UnconfineHomeWaterBothAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
|
||||
options = {
|
||||
"unconfine_home_water": 3,
|
||||
"early_energy_form": False
|
||||
"unconfine_home_water": UnconfineHomeWater.option_via_both,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_unconfine_home_water_both_location(self) -> None:
|
||||
"""Test locations accessible with unconfined home water via energy door and transportation turtle"""
|
||||
self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area")
|
||||
self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")
|
||||
self.assertTrue(self.can_reach_region("Open Waters top left area"), "Cannot reach Open Waters top left area")
|
||||
self.assertTrue(self.can_reach_region("Home Waters, turtle room"), "Cannot reach Home Waters, turtle room")
|
||||
|
||||
@@ -5,16 +5,17 @@ Description: Unit test used to test accessibility of region with the unconfined
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Options import UnconfineHomeWater, EarlyEnergyForm
|
||||
|
||||
|
||||
class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
|
||||
options = {
|
||||
"unconfine_home_water": 1,
|
||||
"early_energy_form": False
|
||||
"unconfine_home_water": UnconfineHomeWater.option_via_energy_door,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_unconfine_home_water_energy_door_location(self) -> None:
|
||||
"""Test locations accessible with unconfined home water via energy door"""
|
||||
self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area")
|
||||
self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")
|
||||
self.assertTrue(self.can_reach_region("Open Waters top left area"), "Cannot reach Open Waters top left area")
|
||||
self.assertFalse(self.can_reach_region("Home Waters, turtle room"), "Can reach Home Waters, turtle room")
|
||||
|
||||
@@ -5,16 +5,17 @@ Description: Unit test used to test accessibility of region with the unconfined
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Options import UnconfineHomeWater, EarlyEnergyForm
|
||||
|
||||
|
||||
class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
|
||||
options = {
|
||||
"unconfine_home_water": 2,
|
||||
"early_energy_form": False
|
||||
"unconfine_home_water": UnconfineHomeWater.option_via_transturtle,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_unconfine_home_water_transturtle_location(self) -> None:
|
||||
"""Test locations accessible with unconfined home water via transportation turtle"""
|
||||
self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")
|
||||
self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area")
|
||||
self.assertTrue(self.can_reach_region("Home Waters, turtle room"), "Cannot reach Home Waters, turtle room")
|
||||
self.assertFalse(self.can_reach_region("Open Waters top left area"), "Can reach Open Waters top left area")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool, OptionGroup
|
||||
import random
|
||||
|
||||
|
||||
@@ -213,6 +213,7 @@ class BlasphemousDeathLink(DeathLink):
|
||||
|
||||
@dataclass
|
||||
class BlasphemousOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
prie_dieu_warp: PrieDieuWarp
|
||||
skip_cutscenes: SkipCutscenes
|
||||
corpse_hints: CorpseHints
|
||||
|
||||
@@ -137,12 +137,6 @@ class BlasphemousWorld(World):
|
||||
]
|
||||
|
||||
skipped_items = []
|
||||
junk: int = 0
|
||||
|
||||
for item, count in self.options.start_inventory.value.items():
|
||||
for _ in range(count):
|
||||
skipped_items.append(item)
|
||||
junk += 1
|
||||
|
||||
skipped_items.extend(unrandomized_dict.values())
|
||||
|
||||
@@ -194,9 +188,6 @@ class BlasphemousWorld(World):
|
||||
for _ in range(count):
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
for _ in range(junk):
|
||||
pool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
self.multiworld.itempool += pool
|
||||
|
||||
self.place_items_from_dict(unrandomized_dict)
|
||||
|
||||
@@ -684,38 +684,37 @@ class CV64PatchExtensions(APPatchExtension):
|
||||
|
||||
# Disable the 3HBs checking and setting flags when breaking them and enable their individual items checking and
|
||||
# setting flags instead.
|
||||
if options["multi_hit_breakables"]:
|
||||
rom_data.write_int32(0xE87F8, 0x00000000) # NOP
|
||||
rom_data.write_int16(0xE836C, 0x1000)
|
||||
rom_data.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34
|
||||
rom_data.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter)
|
||||
# Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one)
|
||||
rom_data.write_int32(0xE7D54, 0x00000000) # NOP
|
||||
rom_data.write_int16(0xE7908, 0x1000)
|
||||
rom_data.write_byte(0xE7A5C, 0x10)
|
||||
rom_data.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C
|
||||
rom_data.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter)
|
||||
rom_data.write_int32(0xE87F8, 0x00000000) # NOP
|
||||
rom_data.write_int16(0xE836C, 0x1000)
|
||||
rom_data.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34
|
||||
rom_data.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter)
|
||||
# Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one)
|
||||
rom_data.write_int32(0xE7D54, 0x00000000) # NOP
|
||||
rom_data.write_int16(0xE7908, 0x1000)
|
||||
rom_data.write_byte(0xE7A5C, 0x10)
|
||||
rom_data.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C
|
||||
rom_data.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter)
|
||||
|
||||
# New flag values to put in each 3HB vanilla flag's spot
|
||||
rom_data.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock
|
||||
rom_data.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock
|
||||
rom_data.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub
|
||||
rom_data.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab
|
||||
rom_data.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab
|
||||
rom_data.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock
|
||||
rom_data.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge
|
||||
rom_data.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge
|
||||
rom_data.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate
|
||||
rom_data.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal
|
||||
rom_data.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab
|
||||
rom_data.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge
|
||||
rom_data.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate
|
||||
rom_data.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab
|
||||
rom_data.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab
|
||||
rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
|
||||
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
|
||||
rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
|
||||
rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
|
||||
# New flag values to put in each 3HB vanilla flag's spot
|
||||
rom_data.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock
|
||||
rom_data.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock
|
||||
rom_data.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub
|
||||
rom_data.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab
|
||||
rom_data.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab
|
||||
rom_data.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock
|
||||
rom_data.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge
|
||||
rom_data.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge
|
||||
rom_data.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate
|
||||
rom_data.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal
|
||||
rom_data.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab
|
||||
rom_data.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge
|
||||
rom_data.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate
|
||||
rom_data.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab
|
||||
rom_data.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab
|
||||
rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
|
||||
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
|
||||
rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
|
||||
rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
|
||||
|
||||
# Once-per-frame gameplay checks
|
||||
rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
|
||||
|
||||
248
worlds/cvcotm/LICENSES.txt
Normal file
248
worlds/cvcotm/LICENSES.txt
Normal file
@@ -0,0 +1,248 @@
|
||||
|
||||
Regarding the sprite data specifically for the Archipelago logo found in data > patches.py:
|
||||
|
||||
The Archipelago Logo is © 2022 by Krista Corkos and Christopher Wilson and licensed under Attribution-NonCommercial 4.0
|
||||
International. Logo modified by Liquid Cat to fit artstyle and uses within this mod. To view a copy of this license,
|
||||
visit http://creativecommons.org/licenses/by-nc/4.0/
|
||||
|
||||
The other custom sprites that I made, as long as you don't lie by claiming you were the one who drew them, I am fine
|
||||
with you using and distributing them however you want to. -Liquid Cat
|
||||
|
||||
========================================================================================================================
|
||||
|
||||
For the lz10.py and cvcotm_text.py modules specifically the MIT license applies. Its terms are as follows:
|
||||
|
||||
MIT License
|
||||
|
||||
cvcotm_text.py Copyright (c) 2024 Liquid Cat
|
||||
(Please consider the associated pixel data for the ASCII characters missing from CotM in data > patches.py
|
||||
in the public domain, if there was any thought that that could even be copyrighted. -Liquid Cat)
|
||||
|
||||
lz10.py Copyright (c) 2024 lilDavid, NoiseCrush
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
========================================================================================================================
|
||||
|
||||
Everything else in this world package not mentioned above can be assumed covered by standalone CotMR's Apache license
|
||||
being a piece of a direct derivative of it. The terms are as follows:
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021-2024 DevAnj, fusecavator, spooky, Malaert64
|
||||
|
||||
Archipelago version by Liquid Cat
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
4
worlds/cvcotm/NOTICE.txt
Normal file
4
worlds/cvcotm/NOTICE.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Circle of the Moon Randomizer
|
||||
Copyright 2021-2024 DevAnj, fusecavator, spooky, Malaert64
|
||||
|
||||
Archipelago version by Liquid Cat
|
||||
221
worlds/cvcotm/__init__.py
Normal file
221
worlds/cvcotm/__init__.py
Normal file
@@ -0,0 +1,221 @@
|
||||
import os
|
||||
import typing
|
||||
import settings
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from BaseClasses import Item, Region, Tutorial, ItemClassification
|
||||
from .items import CVCotMItem, FILLER_ITEM_NAMES, ACTION_CARDS, ATTRIBUTE_CARDS, cvcotm_item_info, \
|
||||
get_item_names_to_ids, get_item_counts
|
||||
from .locations import CVCotMLocation, get_location_names_to_ids, BASE_ID, get_named_locations_data, \
|
||||
get_location_name_groups
|
||||
from .options import cvcotm_option_groups, CVCotMOptions, SubWeaponShuffle, IronMaidenBehavior, RequiredSkirmishes, \
|
||||
CompletionGoal, EarlyEscapeItem
|
||||
from .regions import get_region_info, get_all_region_names
|
||||
from .rules import CVCotMRules
|
||||
from .data import iname, lname
|
||||
from .presets import cvcotm_options_presets
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \
|
||||
get_start_inventory_data
|
||||
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, \
|
||||
CVCOTM_VC_US_HASH
|
||||
from .client import CastlevaniaCotMClient
|
||||
|
||||
|
||||
class CVCotMSettings(settings.Group):
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""File name of the Castlevania CotM US rom"""
|
||||
copy_to = "Castlevania - Circle of the Moon (USA).gba"
|
||||
description = "Castlevania CotM (US) ROM File"
|
||||
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
|
||||
class CVCotMWeb(WebWorld):
|
||||
theme = "stone"
|
||||
options_presets = cvcotm_options_presets
|
||||
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Archipleago Castlevania: Circle of the Moon randomizer on your computer and "
|
||||
"connecting it to a multiworld.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Liquid Cat"]
|
||||
)]
|
||||
|
||||
option_groups = cvcotm_option_groups
|
||||
|
||||
|
||||
class CVCotMWorld(World):
|
||||
"""
|
||||
Castlevania: Circle of the Moon is a launch title for the Game Boy Advance and the first of three Castlevania games
|
||||
released for the handheld in the "Metroidvania" format. As Nathan Graves, wielding the Hunter Whip and utilizing the
|
||||
Dual Set-Up System for new possibilities, you must battle your way through Camilla's castle and rescue your master
|
||||
from a demonic ritual to restore the Count's power...
|
||||
"""
|
||||
game = "Castlevania - Circle of the Moon"
|
||||
item_name_groups = {
|
||||
"DSS": ACTION_CARDS.union(ATTRIBUTE_CARDS),
|
||||
"Card": ACTION_CARDS.union(ATTRIBUTE_CARDS),
|
||||
"Action": ACTION_CARDS,
|
||||
"Action Card": ACTION_CARDS,
|
||||
"Attribute": ATTRIBUTE_CARDS,
|
||||
"Attribute Card": ATTRIBUTE_CARDS,
|
||||
"Freeze": {iname.serpent, iname.cockatrice, iname.mercury, iname.mars},
|
||||
"Freeze Action": {iname.mercury, iname.mars},
|
||||
"Freeze Attribute": {iname.serpent, iname.cockatrice}
|
||||
}
|
||||
location_name_groups = get_location_name_groups()
|
||||
options_dataclass = CVCotMOptions
|
||||
options: CVCotMOptions
|
||||
settings: typing.ClassVar[CVCotMSettings]
|
||||
origin_region_name = "Catacomb"
|
||||
hint_blacklist = frozenset({lname.ba24}) # The Battle Arena reward, if it's put in, will always be a Last Key.
|
||||
|
||||
item_name_to_id = {name: cvcotm_item_info[name].code + BASE_ID for name in cvcotm_item_info
|
||||
if cvcotm_item_info[name].code is not None}
|
||||
location_name_to_id = get_location_names_to_ids()
|
||||
|
||||
# Default values to possibly be updated in generate_early
|
||||
total_last_keys: int = 0
|
||||
required_last_keys: int = 0
|
||||
|
||||
auth: bytearray
|
||||
|
||||
web = CVCotMWeb()
|
||||
|
||||
def generate_early(self) -> None:
|
||||
# Generate the player's unique authentication
|
||||
self.auth = bytearray(self.random.getrandbits(8) for _ in range(16))
|
||||
|
||||
# If Required Skirmishes are on, force the Required and Available Last Keys to 8 or 9 depending on which option
|
||||
# was chosen.
|
||||
if self.options.required_skirmishes == RequiredSkirmishes.option_all_bosses:
|
||||
self.options.required_last_keys.value = 8
|
||||
self.options.available_last_keys.value = 8
|
||||
elif self.options.required_skirmishes == RequiredSkirmishes.option_all_bosses_and_arena:
|
||||
self.options.required_last_keys.value = 9
|
||||
self.options.available_last_keys.value = 9
|
||||
|
||||
self.total_last_keys = self.options.available_last_keys.value
|
||||
self.required_last_keys = self.options.required_last_keys.value
|
||||
|
||||
# If there are more Last Keys required than there are Last Keys in total, drop the required Last Keys to
|
||||
# the total Last Keys.
|
||||
if self.required_last_keys > self.total_last_keys:
|
||||
self.required_last_keys = self.total_last_keys
|
||||
logging.warning(f"[{self.player_name}] The Required Last Keys "
|
||||
f"({self.options.required_last_keys.value}) is higher than the Available Last Keys "
|
||||
f"({self.options.available_last_keys.value}). Lowering the required number to: "
|
||||
f"{self.required_last_keys}")
|
||||
self.options.required_last_keys.value = self.required_last_keys
|
||||
|
||||
# Place the Double or Roc Wing in local_early_items if the Early Escape option is being used.
|
||||
if self.options.early_escape_item == EarlyEscapeItem.option_double:
|
||||
self.multiworld.local_early_items[self.player][iname.double] = 1
|
||||
elif self.options.early_escape_item == EarlyEscapeItem.option_roc_wing:
|
||||
self.multiworld.local_early_items[self.player][iname.roc_wing] = 1
|
||||
elif self.options.early_escape_item == EarlyEscapeItem.option_double_or_roc_wing:
|
||||
self.multiworld.local_early_items[self.player][self.random.choice([iname.double, iname.roc_wing])] = 1
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Create every Region object.
|
||||
created_regions = [Region(name, self.player, self.multiworld) for name in get_all_region_names()]
|
||||
|
||||
# Attach the Regions to the Multiworld.
|
||||
self.multiworld.regions.extend(created_regions)
|
||||
|
||||
for reg in created_regions:
|
||||
|
||||
# Add the Entrances to all the Regions.
|
||||
ent_destinations_and_names = get_region_info(reg.name, "entrances")
|
||||
if ent_destinations_and_names is not None:
|
||||
reg.add_exits(ent_destinations_and_names)
|
||||
|
||||
# Add the Locations to all the Regions.
|
||||
loc_names = get_region_info(reg.name, "locations")
|
||||
if loc_names is None:
|
||||
continue
|
||||
locations_with_ids, locked_pairs = get_named_locations_data(loc_names, self.options)
|
||||
reg.add_locations(locations_with_ids, CVCotMLocation)
|
||||
|
||||
# Place locked Items on all of their associated Locations.
|
||||
for locked_loc, locked_item in locked_pairs.items():
|
||||
self.get_location(locked_loc).place_locked_item(self.create_item(locked_item,
|
||||
ItemClassification.progression))
|
||||
|
||||
def create_item(self, name: str, force_classification: typing.Optional[ItemClassification] = None) -> Item:
|
||||
if force_classification is not None:
|
||||
classification = force_classification
|
||||
else:
|
||||
classification = cvcotm_item_info[name].default_classification
|
||||
|
||||
code = cvcotm_item_info[name].code
|
||||
if code is not None:
|
||||
code += BASE_ID
|
||||
|
||||
created_item = CVCotMItem(name, classification, code, self.player)
|
||||
|
||||
return created_item
|
||||
|
||||
def create_items(self) -> None:
|
||||
item_counts = get_item_counts(self)
|
||||
|
||||
# Set up the items correctly
|
||||
self.multiworld.itempool += [self.create_item(item, classification) for classification in item_counts for item
|
||||
in item_counts[classification] for _ in range(item_counts[classification][item])]
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# Set all the Entrance and Location rules properly.
|
||||
CVCotMRules(self).set_cvcotm_rules()
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
# Get out all the Locations that are not Events. Only take the Iron Maiden switch if the Maiden Detonator is in
|
||||
# the item pool.
|
||||
active_locations = [loc for loc in self.multiworld.get_locations(self.player) if loc.address is not None and
|
||||
(loc.name != lname.ct21 or self.options.iron_maiden_behavior ==
|
||||
IronMaidenBehavior.option_detonator_in_pool)]
|
||||
|
||||
# Location data
|
||||
offset_data = get_location_data(self, active_locations)
|
||||
# Sub-weapons
|
||||
if self.options.sub_weapon_shuffle:
|
||||
offset_data.update(shuffle_sub_weapons(self))
|
||||
# Item drop randomization
|
||||
if self.options.item_drop_randomization:
|
||||
offset_data.update(populate_enemy_drops(self))
|
||||
# Countdown
|
||||
if self.options.countdown:
|
||||
offset_data.update(get_countdown_flags(self, active_locations))
|
||||
# Start Inventory
|
||||
start_inventory_data = get_start_inventory_data(self)
|
||||
offset_data.update(start_inventory_data[0])
|
||||
|
||||
patch = CVCotMProcedurePatch(player=self.player, player_name=self.player_name)
|
||||
patch_rom(self, patch, offset_data, start_inventory_data[1])
|
||||
|
||||
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
|
||||
f"{patch.patch_file_ending}")
|
||||
|
||||
patch.write(rom_path)
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
return {"death_link": self.options.death_link.value,
|
||||
"iron_maiden_behavior": self.options.iron_maiden_behavior.value,
|
||||
"ignore_cleansing": self.options.ignore_cleansing.value,
|
||||
"skip_tutorials": self.options.skip_tutorials.value,
|
||||
"required_last_keys": self.required_last_keys,
|
||||
"completion_goal": self.options.completion_goal.value}
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choice(FILLER_ITEM_NAMES)
|
||||
|
||||
def modify_multidata(self, multidata: typing.Dict[str, typing.Any]):
|
||||
# Put the player's unique authentication in connect_names.
|
||||
multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = \
|
||||
multidata["connect_names"][self.player_name]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user